Files

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)
}