You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sonic/service/impl/theme.go

487 lines
15 KiB
Go

2 years ago
package impl
import (
"context"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"go.uber.org/fx"
2 years ago
"github.com/go-sonic/sonic/config"
"github.com/go-sonic/sonic/dal"
"github.com/go-sonic/sonic/event"
"github.com/go-sonic/sonic/model/dto"
"github.com/go-sonic/sonic/model/entity"
"github.com/go-sonic/sonic/model/property"
"github.com/go-sonic/sonic/service"
"github.com/go-sonic/sonic/service/theme"
"github.com/go-sonic/sonic/util"
"github.com/go-sonic/sonic/util/xerr"
)
type themeServiceImpl struct {
OptionService service.OptionService
Config *config.Config
Event event.Bus
PropertyScanner theme.PropertyScanner
FileScanner theme.FileScanner
ThemeFetchers themeFetchers
}
type themeFetchers struct {
fx.In
MultipartZipThemeFetcher theme.ThemeFetcher `name:"multipartZipThemeFetcher"`
GitRepoThemeFetcher theme.ThemeFetcher `name:"gitRepoThemeFetcher"`
2 years ago
}
func NewThemeService(optionService service.OptionService, config *config.Config, event event.Bus, propertyScanner theme.PropertyScanner, fileScanner theme.FileScanner, themeFetcher themeFetchers) service.ThemeService {
2 years ago
return &themeServiceImpl{
OptionService: optionService,
Config: config,
Event: event,
PropertyScanner: propertyScanner,
FileScanner: fileScanner,
ThemeFetchers: themeFetcher,
2 years ago
}
}
func (t *themeServiceImpl) GetActivateTheme(ctx context.Context) (*dto.ThemeProperty, error) {
2 years ago
activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
2 years ago
if err != nil {
return nil, err
}
return t.GetThemeByID(ctx, activatedThemeID)
}
func (t *themeServiceImpl) GetThemeByID(ctx context.Context, themeID string) (*dto.ThemeProperty, error) {
themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
if err != nil {
return nil, err
}
if themeProperty == nil {
return nil, xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg(themeID + " not exist")
}
2 years ago
activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
2 years ago
if err != nil {
return nil, err
}
if themeProperty.ID == activatedThemeID {
themeProperty.Activated = true
}
return themeProperty, nil
}
func (t *themeServiceImpl) ListAllTheme(ctx context.Context) ([]*dto.ThemeProperty, error) {
themes, err := t.PropertyScanner.ListAll(ctx, t.Config.Sonic.ThemeDir)
if err != nil {
return nil, err
}
2 years ago
activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
2 years ago
if err != nil {
return nil, err
}
for _, t := range themes {
if t.ID == activatedThemeID {
t.Activated = true
}
}
return themes, nil
}
func (t *themeServiceImpl) ListThemeFiles(ctx context.Context, themeID string) ([]*dto.ThemeFile, error) {
themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
if err != nil {
return nil, err
}
if themeProperty == nil {
return nil, xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg(themeID + " not exist")
}
return t.FileScanner.ListThemeFiles(ctx, themeProperty.ThemePath)
}
func (t *themeServiceImpl) GetThemeFileContent(ctx context.Context, themeID, absPath string) (string, error) {
themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
if err != nil {
return "", err
}
if themeProperty == nil {
return "", xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg(themeID + " not exist")
}
if err = t.checkPathValid(themeProperty.ThemePath, absPath); err != nil {
return "", err
}
return t.ReadThemeFile(ctx, absPath)
}
func (t *themeServiceImpl) ReadThemeFile(ctx context.Context, absPath string) (content string, err error) {
file, err := os.Open(absPath)
defer func() {
cerr := file.Close()
if err == nil && cerr != nil {
err = xerr.WithStatus(cerr, xerr.StatusInternalServerError).WithMsg("close file err")
}
}()
if os.IsNotExist(err) {
return "", xerr.WithMsg(err, "file not exist").WithStatus(xerr.StatusBadRequest)
} else if os.IsPermission(err) {
return "", xerr.WithMsg(err, "file permission err").WithStatus(xerr.StatusForbidden)
}
contentBytes, err := io.ReadAll(file)
if err != nil {
err = xerr.WithMsg(err, "read file error").WithStatus(xerr.StatusInternalServerError)
return
}
content = util.BytesToString(contentBytes)
return
}
func (t *themeServiceImpl) UpdateThemeFile(ctx context.Context, themeID, absPath, content string) error {
themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
if err != nil {
return err
}
if themeProperty == nil {
return xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg(themeID + " not exist")
}
if err = t.checkPathValid(themeProperty.ThemePath, absPath); err != nil {
return err
}
file, err := os.OpenFile(absPath, os.O_WRONLY, 0)
if err != nil {
return xerr.WithMsg(err, "open file error")
}
defer file.Close()
_, err = file.WriteString(content)
if err != nil {
return xerr.WithMsg(err, "write to file err")
}
return nil
}
func (t *themeServiceImpl) checkPathValid(themePath, absPath string) error {
absPath = filepath.Clean(absPath)
if !filepath.IsAbs(absPath) {
return xerr.WithMsg(nil, "path error").WithStatus(xerr.StatusForbidden)
}
if !strings.HasPrefix(absPath, themePath) {
return xerr.WithMsg(nil, "path error").WithStatus(xerr.StatusForbidden)
}
return nil
}
func (t *themeServiceImpl) ListCustomTemplates(ctx context.Context, themeID, prefix string) ([]string, error) {
themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
if err != nil {
return nil, err
}
if themeProperty == nil {
return nil, xerr.WithMsg(nil, "theme does not exist").WithStatus(xerr.StatusInternalServerError)
}
files, err := os.ReadDir(themeProperty.ThemePath)
if err != nil {
return nil, xerr.WithMsg(err, "read theme files error").WithStatus(xerr.StatusInternalServerError)
}
result := make([]string, 0)
for _, file := range files {
if file.IsDir() {
continue
}
fileName := file.Name()
if !strings.HasPrefix(fileName, prefix) {
continue
}
customName := strings.TrimPrefix(fileName, prefix)
customName = strings.TrimSuffix(customName, ".ftl")
result = append(result, customName)
}
return result, nil
}
func (t *themeServiceImpl) ActivateTheme(ctx context.Context, themeID string) (*dto.ThemeProperty, error) {
err := t.OptionService.Save(ctx, map[string]string{property.Theme.KeyValue: themeID})
if err != nil {
return nil, err
}
t.Event.Publish(ctx, &event.ThemeActivatedEvent{})
return t.GetThemeByID(ctx, themeID)
}
func (t *themeServiceImpl) GetThemeConfig(ctx context.Context, themeID string) ([]*dto.ThemeConfigGroup, error) {
themeProperty, err := t.GetThemeByID(ctx, themeID)
if err != nil {
return nil, err
}
return t.PropertyScanner.ReadThemeConfig(ctx, themeProperty.ThemePath)
}
func (t *themeServiceImpl) GetThemeSettingMap(ctx context.Context, themeID string) (map[string]interface{}, error) {
itemMap, err := t.getThemeConfigItemMap(ctx, themeID)
if err != nil {
return nil, err
}
return t.getThemeSettingMapByItemMap(ctx, themeID, itemMap)
}
func (t *themeServiceImpl) GetThemeGroupSettingMap(ctx context.Context, themeID, group string) (map[string]interface{}, error) {
themeConfig, err := t.GetThemeConfig(ctx, themeID)
if err != nil {
return nil, err
}
var groupConfig *dto.ThemeConfigGroup
for _, themeGroup := range themeConfig {
if themeGroup.Name == group {
groupConfig = themeGroup
break
}
}
if groupConfig == nil {
return nil, nil
}
itemMap := make(map[string]*dto.ThemeConfigItem)
for _, item := range groupConfig.Items {
itemMap[item.Name] = item
}
return t.getThemeSettingMapByItemMap(ctx, themeID, itemMap)
}
func (t *themeServiceImpl) getThemeSettingMapByItemMap(ctx context.Context, themeID string, itemMap map[string]*dto.ThemeConfigItem) (map[string]interface{}, error) {
themeSettingDAL := dal.GetQueryByCtx(ctx).ThemeSetting
2 years ago
themeSettings, err := themeSettingDAL.WithContext(ctx).Where(themeSettingDAL.ThemeID.Eq(themeID)).Find()
if err != nil {
return nil, WrapDBErr(err)
}
result := make(map[string]interface{})
for _, themeSetting := range themeSettings {
item, ok := itemMap[themeSetting.SettingKey]
if !ok {
continue
}
value, err := item.DataType.Convert(themeSetting.SettingValue)
if err != nil {
return nil, xerr.WithStatus(err, xerr.StatusInternalServerError)
}
result[themeSetting.SettingKey] = value
}
for _, item := range itemMap {
if _, ok := result[item.Name]; ok || item.DefaultValue == nil {
continue
}
result[item.Name] = item.DefaultValue
}
return result, nil
}
func (t *themeServiceImpl) SaveThemeSettings(ctx context.Context, themeID string, settings map[string]interface{}) error {
if len(settings) == 0 {
return nil
}
itemMap, err := t.getThemeConfigItemMap(ctx, themeID)
if err != nil {
return err
}
themeSettingDAL := dal.GetQueryByCtx(ctx).ThemeSetting
2 years ago
allThemeSetting, err := themeSettingDAL.WithContext(ctx).Where(themeSettingDAL.ThemeID.Eq(themeID)).Find()
if err != nil {
return WrapDBErr(err)
}
allThemeSettingMap := make(map[string]*entity.ThemeSetting, len(allThemeSetting))
for _, themeSetting := range allThemeSetting {
allThemeSettingMap[themeSetting.SettingKey] = themeSetting
}
toDeleteIDs := make([]int32, 0)
toCreate := make([]*entity.ThemeSetting, 0)
toUpdate := make([]*entity.ThemeSetting, 0)
now := time.Now()
for name, value := range settings {
item, ok := itemMap[name]
if !ok {
return xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg("setting name invalid: " + name)
}
if themeSetting, ok := allThemeSettingMap[name]; ok {
if value == "" {
toDeleteIDs = append(toDeleteIDs, themeSetting.ID)
} else {
valueStr, err := item.DataType.FormatToStr(value)
if err != nil {
return xerr.WithErrMsgf(err, "value=%s type invalid ,setting name=%s", value, name).WithStatus(xerr.StatusBadRequest)
}
if value != themeSetting.SettingValue {
themeSetting.SettingValue = valueStr
themeSetting.UpdateTime = util.TimePtr(now)
toUpdate = append(toUpdate, themeSetting)
}
}
} else {
if value != "" {
valueStr, err := item.DataType.FormatToStr(value)
if err != nil {
return xerr.WithErrMsgf(err, "value=%s type invalid ,setting name=%s", value, name).WithStatus(xerr.StatusBadRequest)
}
toCreate = append(toCreate, &entity.ThemeSetting{SettingKey: name, SettingValue: valueStr, ThemeID: themeID})
}
}
}
err = dal.Transaction(ctx, func(txCtx context.Context) error {
themeSettingDAL := dal.GetQueryByCtx(txCtx).ThemeSetting
2 years ago
_, err := themeSettingDAL.WithContext(txCtx).Where(themeSettingDAL.ID.In(toDeleteIDs...)).Delete()
if err != nil {
return WrapDBErr(err)
}
err = themeSettingDAL.WithContext(txCtx).Save(toUpdate...)
if err != nil {
return WrapDBErr(err)
}
err = themeSettingDAL.WithContext(txCtx).Create(toCreate...)
return WrapDBErr(err)
})
if err != nil {
return err
}
t.Event.Publish(ctx, &event.ThemeUpdateEvent{})
return nil
}
func (t *themeServiceImpl) DeleteThemeSettings(ctx context.Context, themeID string) error {
2 years ago
activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
2 years ago
if err != nil {
return err
}
if activatedThemeID == themeID {
return xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg("can not delete the theme being used")
}
themeSettingDAL := dal.GetQueryByCtx(ctx).ThemeSetting
2 years ago
_, err = themeSettingDAL.WithContext(ctx).Where(themeSettingDAL.ThemeID.Eq(themeID)).Delete()
return WrapDBErr(err)
}
func (t *themeServiceImpl) DeleteTheme(ctx context.Context, themeID string, deleteSettings bool) error {
2 years ago
activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
2 years ago
if err != nil {
return err
}
if activatedThemeID == themeID {
return xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg("can not delete the theme being used")
}
if deleteSettings {
err = t.DeleteThemeSettings(ctx, themeID)
if err != nil {
return err
}
}
themeProperty, err := t.GetThemeByID(ctx, themeID)
if err != nil {
return err
}
err = os.RemoveAll(themeProperty.ThemePath)
if err != nil {
return xerr.WithStatus(err, xerr.StatusInternalServerError).WithMsg("delete theme directory err")
}
return nil
}
func (t *themeServiceImpl) UploadTheme(ctx context.Context, file *multipart.FileHeader) (*dto.ThemeProperty, error) {
themeProperty, err := t.ThemeFetchers.MultipartZipThemeFetcher.FetchTheme(ctx, file)
2 years ago
if err != nil {
return nil, err
}
return t.addTheme(ctx, themeProperty)
}
func (t *themeServiceImpl) UpdateThemeByUpload(ctx context.Context, themeID string, file *multipart.FileHeader) (*dto.ThemeProperty, error) {
oldThemeProperty, err := t.GetThemeByID(ctx, themeID)
if err != nil {
return nil, err
}
newThemeProperty, err := t.ThemeFetchers.MultipartZipThemeFetcher.FetchTheme(ctx, file)
2 years ago
if err != nil {
return nil, err
}
err = os.RemoveAll(oldThemeProperty.ThemePath)
if err != nil {
return nil, xerr.WithMsg(err, "delete old theme err").WithStatus(xerr.StatusInternalServerError)
}
return t.addTheme(ctx, newThemeProperty)
}
func (t *themeServiceImpl) ReloadTheme(ctx context.Context) error {
t.Event.Publish(ctx, &event.ThemeUpdateEvent{})
t.Event.Publish(ctx, &event.ThemeFileUpdatedEvent{})
return nil
}
func (t *themeServiceImpl) TemplateExist(ctx context.Context, template string) (bool, error) {
2 years ago
activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
2 years ago
if err != nil {
return false, err
}
themeProperty, err := t.GetThemeByID(ctx, activatedThemeID)
if err != nil {
return false, err
}
return util.FileIsExisted(filepath.Join(themeProperty.ThemePath, template)), nil
}
func (t *themeServiceImpl) getThemeConfigItemMap(ctx context.Context, themeID string) (map[string]*dto.ThemeConfigItem, error) {
themeConfigGroup, err := t.GetThemeConfig(ctx, themeID)
if err != nil {
return nil, err
}
itemMap := make(map[string]*dto.ThemeConfigItem)
for _, themeConfig := range themeConfigGroup {
for _, item := range themeConfig.Items {
itemMap[item.Name] = item
}
}
return itemMap, nil
}
func (t *themeServiceImpl) addTheme(ctx context.Context, themeProperty *dto.ThemeProperty) (*dto.ThemeProperty, error) {
existTheme, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeProperty.ID)
if err != nil {
return nil, err
}
if existTheme != nil {
return nil, xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg("theme already exist")
}
err = util.CopyDir(themeProperty.ThemePath, filepath.Join(t.Config.Sonic.ThemeDir, themeProperty.FolderName))
if err != nil {
return nil, xerr.WithStatus(err, xerr.StatusBadRequest)
}
return t.PropertyScanner.ReadThemeProperty(ctx, filepath.Join(t.Config.Sonic.ThemeDir, themeProperty.FolderName))
}
func (t *themeServiceImpl) Render(ctx context.Context, name string) (string, error) {
2 years ago
activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
2 years ago
if err != nil {
return "", err
}
return activatedThemeID + "/" + name, nil
}
func (t *themeServiceImpl) Fetch(ctx context.Context, themeURL string) (*dto.ThemeProperty, error) {
fetchTheme, err := t.ThemeFetchers.GitRepoThemeFetcher.FetchTheme(ctx, themeURL)
if err != nil {
return nil, xerr.WithStatus(err, xerr.StatusBadRequest).WithMsg(err.Error())
}
return t.addTheme(ctx, fetchTheme)
}