Files
1Panel/agent/app/service/website_template.go
2026-08-05 16:56:08 +08:00

441 lines
13 KiB
Go

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
}