package service import ( "archive/zip" "encoding/json" "fmt" "io" "os" "path" "path/filepath" "regexp" "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" ) type WebsiteTemplateService struct { } type IWebsiteTemplateService interface { PageTemplate(req request.WebsiteTemplateSearch) (int64, []response.WebsiteTemplateDTO, error) CreateTemplate(req request.WebsiteTemplateCreate) error UpdateTemplate(req request.WebsiteTemplateUpdate) error DeleteTemplate(id uint) error GetTemplate(id uint) (*response.WebsiteTemplateDTO, error) SaveUploadZip(fileName string, content []byte) (string, []string, error) PageOutput(req request.WebsiteTemplateOutputSearch) (int64, []response.WebsiteTemplateOutputDTO, error) CreateOutput(req request.WebsiteTemplateOutputCreate) error DeleteOutput(id uint) error GetOutput(id uint) (*response.WebsiteTemplateOutputDTO, error) Preview(req request.WebsitePreviewReq) (*response.WebsitePreviewDTO, error) } func NewIWebsiteTemplateService() IWebsiteTemplateService { return &WebsiteTemplateService{} } func templateBaseDir() string { return path.Join(global.Dir.DataDir, "templates") } func templateFileDir() string { return path.Join(templateBaseDir(), "files") } func templateOutputDir(id uint) string { return path.Join(templateBaseDir(), "outputs", fmt.Sprintf("%d", id)) } func (w WebsiteTemplateService) PageTemplate(req request.WebsiteTemplateSearch) (int64, []response.WebsiteTemplateDTO, error) { var opts []repo.DBOption if req.Name != "" { opts = append(opts, websiteTemplateRepo.WithName(req.Name)) } if req.Type != "" { opts = append(opts, websiteTemplateRepo.WithType(req.Type)) } opts = append(opts, repo.WithOrderDesc("created_at")) total, templates, err := websiteTemplateRepo.Page(req.Page, req.PageSize, opts...) if err != nil { return 0, nil, err } var dtos []response.WebsiteTemplateDTO for _, t := range templates { dtos = append(dtos, response.WebsiteTemplateDTO{WebsiteTemplate: t}) } return total, dtos, nil } func (w WebsiteTemplateService) CreateTemplate(req request.WebsiteTemplateCreate) error { if err := validateWebsiteTemplateFile(req.Type, req.FilePath); err != nil { return err } if exist, _ := websiteTemplateRepo.GetFirst(repo.WithByName(req.Name)); exist != nil { return buserr.New("ErrNameIsExist") } template := &model.WebsiteTemplate{ Name: req.Name, Type: req.Type, Content: req.Content, FilePath: req.FilePath, Variables: req.Variables, Remark: req.Remark, } return websiteTemplateRepo.Create(template) } func (w WebsiteTemplateService) UpdateTemplate(req request.WebsiteTemplateUpdate) error { template, err := websiteTemplateRepo.GetFirst(repo.WithByID(req.ID)) if err != nil { return err } if err := validateWebsiteTemplateFile(req.Type, req.FilePath); err != nil { return err } if exist, _ := websiteTemplateRepo.GetFirst(repo.WithByName(req.Name), repo.WithByNOTID(req.ID)); exist != nil { return buserr.New("ErrNameIsExist") } template.Name = req.Name template.Type = req.Type template.Content = req.Content if req.FilePath != template.FilePath && template.FilePath != "" && strings.HasPrefix(template.FilePath, templateBaseDir()) { _ = os.Remove(template.FilePath) } template.FilePath = req.FilePath template.Variables = req.Variables template.Remark = req.Remark return websiteTemplateRepo.Save(template) } func validateWebsiteTemplateFile(templateType, filePath string) error { if templateType != "multi" { return nil } if filePath == "" { return buserr.WithName("ErrFileNotFound", "ZIP") } if _, err := os.Stat(filePath); err != nil { if os.IsNotExist(err) { return buserr.WithName("ErrFileNotFound", "ZIP") } return err } return nil } func (w WebsiteTemplateService) DeleteTemplate(id uint) error { template, err := websiteTemplateRepo.GetFirst(repo.WithByID(id)) if err != nil { return err } outputs, _ := websiteTemplateOutputRepo.List(websiteTemplateOutputRepo.WithByTemplateID(id)) for _, output := range outputs { if output.OutputPath != "" && strings.HasPrefix(output.OutputPath, templateBaseDir()) { _ = os.RemoveAll(output.OutputPath) } } if template.FilePath != "" && strings.HasPrefix(template.FilePath, templateBaseDir()) { _ = os.Remove(template.FilePath) } if err := websiteTemplateOutputRepo.DeleteBy(websiteTemplateOutputRepo.WithByTemplateID(id)); err != nil { return err } return websiteTemplateRepo.DeleteBy(repo.WithByID(id)) } func (w WebsiteTemplateService) GetTemplate(id uint) (*response.WebsiteTemplateDTO, error) { template, err := websiteTemplateRepo.GetFirst(repo.WithByID(id)) if err != nil { return nil, err } return &response.WebsiteTemplateDTO{WebsiteTemplate: *template}, nil } func (w WebsiteTemplateService) SaveUploadZip(fileName string, content []byte) (string, []string, error) { if !strings.HasSuffix(strings.ToLower(fileName), ".zip") { return "", nil, buserr.WithName("ErrNotSupportType", fileName) } dir := templateFileDir() if err := os.MkdirAll(dir, constant.DirPerm); err != nil { return "", nil, err } filePath := path.Join(dir, fmt.Sprintf("%d_%s", time.Now().Unix(), filepath.Base(fileName))) if err := os.WriteFile(filePath, content, constant.FilePerm); err != nil { return "", nil, err } return filePath, scanZipVariables(filePath), nil } func scanZipVariables(zipPath string) []string { variables := []string{} reader, err := zip.OpenReader(zipPath) if err != nil { return variables } defer reader.Close() seen := make(map[string]struct{}) for _, file := range reader.File { if file.FileInfo().IsDir() || !isTextFile(file.Name) { continue } rc, err := file.Open() if err != nil { continue } content, err := io.ReadAll(rc) _ = rc.Close() if err != nil { continue } for _, match := range templateVarRegex.FindAllStringSubmatch(string(content), -1) { if _, ok := seen[match[1]]; !ok { seen[match[1]] = struct{}{} variables = append(variables, match[1]) } } } return variables } func (w WebsiteTemplateService) PageOutput(req request.WebsiteTemplateOutputSearch) (int64, []response.WebsiteTemplateOutputDTO, error) { var opts []repo.DBOption if req.TemplateID > 0 { opts = append(opts, websiteTemplateOutputRepo.WithByTemplateID(req.TemplateID)) } opts = append(opts, repo.WithOrderDesc("created_at")) total, outputs, err := websiteTemplateOutputRepo.Page(req.Page, req.PageSize, opts...) if err != nil { return 0, nil, err } var dtos []response.WebsiteTemplateOutputDTO for _, output := range outputs { item := response.WebsiteTemplateOutputDTO{WebsiteTemplateOutput: output} if template, err := websiteTemplateRepo.GetFirst(repo.WithByID(output.TemplateID)); err == nil { item.TemplateName = template.Name } dtos = append(dtos, item) } return total, dtos, nil } func (w WebsiteTemplateService) CreateOutput(req request.WebsiteTemplateOutputCreate) error { template, err := websiteTemplateRepo.GetFirst(repo.WithByID(req.TemplateID)) if err != nil { return err } valuesJSON, err := json.Marshal(req.VariableValues) if err != nil { return err } output := &model.WebsiteTemplateOutput{ Name: req.Name, TemplateID: template.ID, TemplateType: template.Type, VariableValues: string(valuesJSON), } if err := websiteTemplateOutputRepo.Create(output); err != nil { return err } outputDir := templateOutputDir(output.ID) if err := renderToDir(template, req.VariableValues, outputDir); err != nil { _ = websiteTemplateOutputRepo.DeleteBy(repo.WithByID(output.ID)) return err } output.OutputPath = outputDir return websiteTemplateOutputRepo.Save(output) } func (w WebsiteTemplateService) DeleteOutput(id uint) error { output, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(id)) if err != nil { return err } if output.OutputPath != "" && strings.HasPrefix(output.OutputPath, templateBaseDir()) { _ = os.RemoveAll(output.OutputPath) } return websiteTemplateOutputRepo.DeleteBy(repo.WithByID(id)) } func (w WebsiteTemplateService) GetOutput(id uint) (*response.WebsiteTemplateOutputDTO, error) { output, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(id)) if err != nil { return nil, err } item := &response.WebsiteTemplateOutputDTO{WebsiteTemplateOutput: *output} if template, err := websiteTemplateRepo.GetFirst(repo.WithByID(output.TemplateID)); err == nil { item.TemplateName = template.Name } return item, nil } func (w WebsiteTemplateService) Preview(req request.WebsitePreviewReq) (*response.WebsitePreviewDTO, error) { template, err := websiteTemplateRepo.GetFirst(repo.WithByID(req.TemplateID)) if err != nil { return nil, err } var html string if template.Type == "single" { html = renderContent(template.Content, req.VariableValues) } else { html, err = renderMainHTMLFromZip(template.FilePath, req.VariableValues) if err != nil { return nil, err } } return &response.WebsitePreviewDTO{HTML: html}, nil } var templateVarRegex = regexp.MustCompile(`\{\{(\w+)\}\}`) func renderContent(content string, values map[string]string) string { rendered := templateVarRegex.ReplaceAllStringFunc(content, func(match string) string { key := templateVarRegex.FindStringSubmatch(match)[1] if val, ok := values[key]; ok { return val } return "" }) return rendered } func renderToDir(template *model.WebsiteTemplate, values map[string]string, outputDir string) error { if err := os.MkdirAll(outputDir, constant.DirPerm); err != nil { return err } if template.Type == "single" { html := renderContent(template.Content, values) return os.WriteFile(path.Join(outputDir, "index.html"), []byte(html), constant.FilePerm) } return unzipAndRender(template.FilePath, outputDir, values) } func unzipAndRender(zipPath, outputDir string, values map[string]string) error { if zipPath == "" { return buserr.WithName("ErrFileNotFound", "ZIP") } reader, err := zip.OpenReader(zipPath) if err != nil { return err } defer reader.Close() for _, file := range reader.File { if err := extractAndRenderFile(file, outputDir, values); err != nil { return err } } return nil } func extractAndRenderFile(file *zip.File, outputDir string, values map[string]string) error { targetPath := filepath.Join(outputDir, file.Name) if !strings.HasPrefix(targetPath, filepath.Clean(outputDir)+string(os.PathSeparator)) { return fmt.Errorf("invalid file path in zip: %s", file.Name) } if file.FileInfo().IsDir() { return os.MkdirAll(targetPath, constant.DirPerm) } if err := os.MkdirAll(filepath.Dir(targetPath), constant.DirPerm); err != nil { return err } rc, err := file.Open() if err != nil { return err } defer rc.Close() content, err := io.ReadAll(rc) if err != nil { return err } if isTextFile(file.Name) { content = []byte(renderContent(string(content), values)) } return os.WriteFile(targetPath, content, constant.FilePerm) } func isTextFile(name string) bool { switch strings.ToLower(filepath.Ext(name)) { case ".html", ".htm", ".css", ".js", ".json", ".xml", ".txt", ".svg", ".md": return true } return false } func renderMainHTMLFromZip(zipPath string, values map[string]string) (string, error) { if zipPath == "" { return "", buserr.WithName("ErrFileNotFound", "ZIP") } reader, err := zip.OpenReader(zipPath) if err != nil { return "", err } defer reader.Close() var mainFile *zip.File for _, file := range reader.File { if file.FileInfo().IsDir() { continue } name := strings.ToLower(filepath.Base(file.Name)) if name == "index.html" || name == "index.htm" { if mainFile == nil || len(file.Name) < len(mainFile.Name) { mainFile = file } } } if mainFile == nil { return "", buserr.WithName("ErrFileNotFound", "index.html") } rc, err := mainFile.Open() if err != nil { return "", err } defer rc.Close() content, err := io.ReadAll(rc) if err != nil { return "", err } return renderContent(string(content), values), nil } func copyDir(src, dst string) error { return filepath.Walk(src, func(p string, info os.FileInfo, err error) error { if err != nil { return err } relPath, err := filepath.Rel(src, p) if err != nil { return err } targetPath := filepath.Join(dst, relPath) if info.IsDir() { return os.MkdirAll(targetPath, constant.DirPerm) } return copyFile(p, targetPath) }) } func copyFile(src, dst string) error { srcFile, err := os.Open(src) if err != nil { return err } defer srcFile.Close() if err := os.MkdirAll(filepath.Dir(dst), constant.DirPerm); err != nil { return err } dstFile, err := os.Create(dst) if err != nil { return err } defer dstFile.Close() _, err = io.Copy(dstFile, srcFile) return err }