package themes_handler import ( "encoding/json" "errors" "fmt" "io" "net/http" "path/filepath" "strings" "unicode/utf8" "autolibrary.top/webserver/backend/models/themes_model" "autolibrary.top/webserver/backend/services/theme_files" "autolibrary.top/webserver/utils" "github.com/gin-gonic/gin" "gorm.io/gorm" ) func fail(c *gin.Context, status int, message string) { c.JSON(status, gin.H{"code": status, "msg": message, "data": nil}) } func success(c *gin.Context, status int, message string, data interface{}) { c.JSON(status, gin.H{"code": status, "msg": message, "data": data}) } func (h *Handler) ownTheme(c *gin.Context) (*themes_model.Theme, bool) { var theme themes_model.Theme id, err := parseThemeID(c) if err != nil { fail(c, http.StatusBadRequest, "Invalid theme ID") return nil, false } err = h.DB.First(&theme, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { fail(c, http.StatusNotFound, "Theme not found") return nil, false } if err != nil { fail(c, http.StatusInternalServerError, "Unable to load theme") return nil, false } if theme.UserID != c.GetString("user_id") { fail(c, http.StatusForbidden, "You can only edit your own themes") return nil, false } return &theme, true } // readUpload enforces the same limits advertised by the web form. func readUpload(c *gin.Context, field string, extensions []string, max int64) ([]byte, error) { file, header, err := c.Request.FormFile(field) if err != nil { return nil, err } defer file.Close() ext := strings.ToLower(filepath.Ext(header.Filename)) valid := false for _, candidate := range extensions { if ext == candidate { valid = true } } if !valid { return nil, fmt.Errorf("invalid %s file type", field) } if header.Size > max { return nil, fmt.Errorf("%s file is too large", field) } data, err := io.ReadAll(io.LimitReader(file, max+1)) if err != nil { return nil, err } if int64(len(data)) > max { return nil, fmt.Errorf("%s file is too large", field) } if len(data) == 0 { return nil, fmt.Errorf("%s file is empty", field) } return data, nil } func (h *Handler) savePreview(data []byte) (string, error) { compressed, err := theme_files.CompressPreviewImage(data) if err != nil { return "", err } name := fmt.Sprintf("preview_%d.jpg", utils.NowInShanghai().UnixNano()) if err := h.Files.WriteFile(filepath.Join(h.Files.Root(), "previews", name), compressed, 0644); err != nil { return "", err } return "/marketplace/themes/preview/" + name, nil } func (h *Handler) PostTheme(c *gin.Context) { var metadata struct { Name string `json:"name"` Brief string `json:"brief"` Description string `json:"description"` NeedTheme string `json:"need_theme"` Tags []string `json:"tags"` Version string `json:"version"` CompatVersion string `json:"compat_version"` } if err := json.Unmarshal([]byte(c.PostForm("metadata")), &metadata); err != nil { fail(c, 400, "Invalid theme metadata") return } metadata.Name = strings.TrimSpace(metadata.Name) metadata.Brief = strings.TrimSpace(metadata.Brief) if metadata.Name == "" || metadata.Brief == "" || utf8.RuneCountInString(metadata.Brief) > 50 || utf8.RuneCountInString(metadata.Description) > 100 { fail(c, 400, "Invalid name, brief or description") return } if metadata.NeedTheme != "dark" && metadata.NeedTheme != "light" && metadata.NeedTheme != "both" { fail(c, 400, "Invalid theme mode") return } var count int64 if err := h.DB.Model(&themes_model.Theme{}).Where("user_id = ? AND name = ?", c.GetString("user_id"), metadata.Name).Count(&count).Error; err != nil { fail(c, 500, "Unable to check theme name") return } if count > 0 { fail(c, 409, "You already have a theme with this name") return } qss, err := readUpload(c, "file", []string{".qss"}, 8<<20) if err != nil { fail(c, 400, err.Error()) return } previewData, err := readUpload(c, "preview", []string{".png", ".jpg", ".jpeg"}, 4<<20) if err != nil && !errors.Is(err, http.ErrMissingFile) { fail(c, 400, err.Error()) return } var preview string if len(previewData) > 0 { preview, err = h.savePreview(previewData) if err != nil { fail(c, 400, "Unable to process preview image") return } } author := c.GetString("display_name") if author == "" { author = c.GetString("user_name") } theme := themes_model.Theme{Name: metadata.Name, Brief: metadata.Brief, Description: metadata.Description, NeedTheme: metadata.NeedTheme, Tags: metadata.Tags, Version: metadata.Version, CompatVersion: metadata.CompatVersion, UserID: c.GetString("user_id"), Author: author, Status: themes_model.ThemePending, PreviewURL: preview, CreatedAt: utils.NowInShanghai(), UpdatedAt: utils.NowInShanghai()} if err := h.Themes.Create(&theme, qss); err != nil { if preview != "" { _ = h.Files.Remove(filepath.Join(h.Files.Root(), "previews", filepath.Base(preview))) } fail(c, 500, "Unable to save theme and archive") return } success(c, http.StatusCreated, "Theme submitted for review", theme) } func (h *Handler) CheckThemeName(c *gin.Context) { name := strings.TrimSpace(c.Query("name")) if name == "" { fail(c, 400, "Name is required") return } var count int64 if err := h.DB.Model(&themes_model.Theme{}).Where("user_id = ? AND name = ?", c.GetString("user_id"), name).Count(&count).Error; err != nil { fail(c, 500, "Unable to check theme name") return } success(c, 200, "Theme name checked", count > 0) } func (h *Handler) UploadPreview(c *gin.Context) { theme, ok := h.ownTheme(c) if !ok { return } data, err := readUpload(c, "preview", []string{".png", ".jpg", ".jpeg"}, 4<<20) if err != nil { fail(c, 400, err.Error()) return } preview, err := h.savePreview(data) if err != nil { fail(c, 400, "Unable to process preview image") return } previous := theme.PreviewURL theme.PreviewURL = preview theme.Status = themes_model.ThemePending theme.UpdatedAt = utils.NowInShanghai() if err := h.Themes.Save(theme, nil); err != nil { _ = h.Files.Remove(filepath.Join(h.Files.Root(), "previews", filepath.Base(preview))) fail(c, 500, "Unable to save preview") return } if previous != "" { _ = h.Files.Remove(filepath.Join(h.Files.Root(), "previews", filepath.Base(previous))) } success(c, 200, "Preview updated; review required", gin.H{"preview_url": preview}) } func (h *Handler) ReplaceThemeFile(c *gin.Context) { theme, ok := h.ownTheme(c) if !ok { return } qss, err := readUpload(c, "file", []string{".qss"}, 8<<20) if err != nil { fail(c, 400, err.Error()) return } theme.Status = themes_model.ThemePending theme.UpdatedAt = utils.NowInShanghai() if err := h.Themes.Save(theme, qss); err != nil { fail(c, 500, "Unable to replace theme file") return } success(c, 200, "File replaced; review required", theme) }