osaet/backend/internal/admin/assets.go
yarnom f0b50d13ea feat: add admin publishing workflow and yar theme
Add Go/Postgres admin APIs, Angular admin UI, manual build flow, asset uploads, markdown import/export, configurable slug generation, and the Yar reading theme. Exclude local docs and generated development artifacts from version control.
2026-06-01 15:48:04 +08:00

80 lines
2.1 KiB
Go

package admin
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
const maxAssetSizeBytes = 12 * 1024 * 1024
var safeAssetNamePattern = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
type AssetUploader struct {
store *Store
assetsDir string
}
func NewAssetUploader(store *Store, assetsDir string) *AssetUploader {
return &AssetUploader{store: store, assetsDir: assetsDir}
}
func (u *AssetUploader) Upload(ctx context.Context, file multipart.File, header *multipart.FileHeader) (Asset, error) {
if header.Size > maxAssetSizeBytes {
return Asset{}, fmt.Errorf("asset is too large: max %d bytes", maxAssetSizeBytes)
}
data, err := io.ReadAll(io.LimitReader(file, maxAssetSizeBytes+1))
if err != nil {
return Asset{}, fmt.Errorf("read asset: %w", err)
}
if int64(len(data)) > maxAssetSizeBytes {
return Asset{}, fmt.Errorf("asset is too large: max %d bytes", maxAssetSizeBytes)
}
mimeType := http.DetectContentType(data)
if !strings.HasPrefix(mimeType, "image/") {
return Asset{}, fmt.Errorf("unsupported asset type: %s", mimeType)
}
sum := sha256.Sum256(data)
sha := hex.EncodeToString(sum[:])
name := assetFilename(header.Filename, sha)
if err := os.MkdirAll(u.assetsDir, 0o755); err != nil {
return Asset{}, fmt.Errorf("create assets dir: %w", err)
}
if err := os.WriteFile(filepath.Join(u.assetsDir, name), data, 0o644); err != nil {
return Asset{}, fmt.Errorf("write asset: %w", err)
}
asset := Asset{
Path: "/assets/" + name,
OriginalName: header.Filename,
MimeType: mimeType,
SizeBytes: int64(len(data)),
SHA256: sha,
}
return u.store.CreateAsset(ctx, asset)
}
func assetFilename(original string, sha string) string {
ext := strings.ToLower(filepath.Ext(original))
base := strings.TrimSuffix(filepath.Base(original), filepath.Ext(original))
base = strings.Trim(safeAssetNamePattern.ReplaceAllString(base, "-"), "-._")
if base == "" {
base = "image"
}
if ext == "" {
ext = ".bin"
}
return fmt.Sprintf("%s-%s%s", base, sha[:12], ext)
}