224 lines
6.7 KiB
Go
224 lines
6.7 KiB
Go
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)
|
|
}
|