Files
Website/backend/services/theme_files/archive.go
T

187 lines
4.9 KiB
Go

package theme_files
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"image"
"image/jpeg"
_ "image/png"
"io"
"log"
"os"
"autolibrary.top/webserver/backend/models/themes_model"
)
func CompressPreviewImage(data []byte) ([]byte, error) {
config, _, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil || config.Width <= 0 || config.Height <= 0 || int64(config.Width)*int64(config.Height) > 40_000_000 {
return nil, fmt.Errorf("invalid image or image dimensions exceed the limit")
}
img, format, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("cannot decode image: %v", err)
}
// Resize if width exceeds 800px
bounds := img.Bounds()
w := bounds.Dx()
h := bounds.Dy()
maxW := 800
if w > maxW {
h = h * maxW / w
if h < 1 {
h = 1
}
w = maxW
// Simple resize using standard library
scaled := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
srcX := x * bounds.Dx() / w
srcY := y * bounds.Dy() / h
scaled.Set(x, y, img.At(srcX, srcY))
}
}
img = scaled
}
var buf bytes.Buffer
err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80})
if err != nil {
return nil, fmt.Errorf("cannot encode JPEG: %v", err)
}
log.Printf("[I] image compressed: %s %dx%d, %d -> %d bytes", format, bounds.Dx(), bounds.Dy(), len(data), buf.Len())
return buf.Bytes(), nil
}
// GET /api/themes/featured?limit=6
func ValidateAlthemeFile(data []byte) (map[string]interface{}, error) {
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, fmt.Errorf("invalid zip file: %v", err)
}
var hasInfoJSON, hasThemeQSS bool
var info map[string]interface{}
for _, f := range reader.File {
if f.Name == "info.json" {
hasInfoJSON = true
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("cannot open info.json: %v", err)
}
decoder := json.NewDecoder(rc)
if err := decoder.Decode(&info); err != nil {
rc.Close()
return nil, fmt.Errorf("invalid info.json: %v", err)
}
rc.Close()
}
if f.Name == "theme.qss" {
hasThemeQSS = true
}
}
if !hasInfoJSON {
return nil, fmt.Errorf("missing info.json in .altheme file")
}
if !hasThemeQSS {
return nil, fmt.Errorf("missing theme.qss in .altheme file")
}
required := []string{"name", "author", "need_theme", "brief"}
for _, key := range required {
if _, ok := info[key]; !ok {
return nil, fmt.Errorf("info.json missing required key: %s", key)
}
}
return info, nil
}
// buildInfoJSON constructs the info.json map from a Theme DB record.
// It merges Brief and Description with a newline separator into the "brief" field.
func BuildInfoJSON(theme themes_model.Theme) map[string]interface{} {
brief := theme.Brief
if theme.Description != "" {
brief = brief + "\n" + theme.Description
}
return map[string]interface{}{
"qss": "theme.qss",
"name": theme.Name,
"author": theme.Author,
"need_theme": theme.NeedTheme,
"brief": brief,
"version": theme.Version,
"compatible_version": theme.CompatVersion,
}
}
// createAlthemeArchive creates a .altheme ZIP archive in memory
// containing theme.qss (from qssData) and info.json (from info map).
func CreateAlthemeArchive(qssData []byte, info map[string]interface{}) ([]byte, error) {
var buf bytes.Buffer
w := zip.NewWriter(&buf)
// Add theme.qss
qssWriter, err := w.Create("theme.qss")
if err != nil {
return nil, fmt.Errorf("cannot create theme.qss in archive: %v", err)
}
if _, err := qssWriter.Write(qssData); err != nil {
return nil, fmt.Errorf("cannot write theme.qss to archive: %v", err)
}
// Add info.json
infoWriter, err := w.Create("info.json")
if err != nil {
return nil, fmt.Errorf("cannot create info.json in archive: %v", err)
}
infoBytes, err := json.MarshalIndent(info, "", " ")
if err != nil {
return nil, fmt.Errorf("cannot marshal info.json: %v", err)
}
if _, err := infoWriter.Write(infoBytes); err != nil {
return nil, fmt.Errorf("cannot write info.json to archive: %v", err)
}
if err := w.Close(); err != nil {
return nil, fmt.Errorf("cannot close archive: %v", err)
}
return buf.Bytes(), nil
}
// extractQSSFromAltheme reads an existing .altheme ZIP file from disk
// and returns the raw bytes of its theme.qss entry.
func ExtractQSSFromAltheme(filePath string) ([]byte, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("cannot read .altheme file: %v", err)
}
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, fmt.Errorf("cannot open .altheme as zip: %v", err)
}
for _, f := range reader.File {
if f.Name == "theme.qss" {
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("cannot open theme.qss in archive: %v", err)
}
defer rc.Close()
return io.ReadAll(rc)
}
}
return nil, fmt.Errorf("theme.qss not found in .altheme archive")
}