mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 16:13:59 +08:00
441 lines
13 KiB
Go
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
|
|
}
|