Add gallery admin and video media support.
This updates gallery handling to support video playback with generated poster thumbnails, adds authenticated admin upload/delete flows, and improves dev/runtime behavior including reliable thumbnail generation and media-safe response handling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
124
app/internal/gallery/files.go
Normal file
124
app/internal/gallery/files.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var albumNameRe = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,63}$`)
|
||||
|
||||
// AlbumsOnDisk returns top-level album folder names (excluding thumbs/hero).
|
||||
func AlbumsOnDisk(imagesDir string) ([]string, error) {
|
||||
entries, err := os.ReadDir(imagesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var albums []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || e.Name() == thumbDirName || e.Name() == heroDirName {
|
||||
continue
|
||||
}
|
||||
if e.Name()[0] == '.' {
|
||||
continue
|
||||
}
|
||||
albums = append(albums, e.Name())
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
// SanitizeAlbum normalizes a folder name for new uploads.
|
||||
func SanitizeAlbum(name string) (string, error) {
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
name = strings.ReplaceAll(name, " ", "-")
|
||||
if !albumNameRe.MatchString(name) {
|
||||
return "", fmt.Errorf("invalid album name")
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// SanitizeUploadName keeps a safe JPEG filename.
|
||||
func SanitizeUploadName(name string) (string, error) {
|
||||
name = filepath.Base(strings.TrimSpace(name))
|
||||
name = strings.ReplaceAll(name, " ", "_")
|
||||
if !isJPEG(name) {
|
||||
return "", fmt.Errorf("only .jpg and .jpeg files are allowed")
|
||||
}
|
||||
if strings.Contains(name, "..") {
|
||||
return "", fmt.Errorf("invalid filename")
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// SaveUpload writes an uploaded JPEG into the gallery tree.
|
||||
func SaveUpload(imagesDir, album, filename string, r io.Reader) (string, error) {
|
||||
album, err := SanitizeAlbum(album)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filename, err = SanitizeUploadName(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir := filepath.Join(imagesDir, album)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
dst := filepath.Join(dir, filename)
|
||||
f, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(f, r); err != nil {
|
||||
os.Remove(dst)
|
||||
return "", err
|
||||
}
|
||||
rel := filepath.ToSlash(filepath.Join(album, filename))
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// DeleteImage removes an image and its derivatives.
|
||||
func DeleteImage(imagesDir, rel string) error {
|
||||
if !safeRelPath(rel) {
|
||||
return fmt.Errorf("invalid path")
|
||||
}
|
||||
paths := []string{filepath.Join(imagesDir, filepath.FromSlash(rel))}
|
||||
if isVideo(filepath.Base(rel)) {
|
||||
paths = append(paths, filepath.Join(imagesDir, thumbDirName, filepath.FromSlash(videoPosterRel(rel))))
|
||||
} else {
|
||||
paths = append(paths,
|
||||
filepath.Join(imagesDir, thumbDirName, filepath.FromSlash(rel)),
|
||||
filepath.Join(imagesDir, heroDirName, filepath.FromSlash(rel)),
|
||||
)
|
||||
}
|
||||
for _, p := range paths {
|
||||
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDerivatives rebuilds thumb and hero for one image.
|
||||
func EnsureDerivatives(imagesDir, rel string) error {
|
||||
if !safeRelPath(rel) {
|
||||
return fmt.Errorf("invalid path")
|
||||
}
|
||||
src := filepath.Join(imagesDir, filepath.FromSlash(rel))
|
||||
thumbDst := filepath.Join(imagesDir, thumbDirName, filepath.FromSlash(rel))
|
||||
heroDst := filepath.Join(imagesDir, heroDirName, filepath.FromSlash(rel))
|
||||
if err := os.MkdirAll(filepath.Dir(thumbDst), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(heroDst), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeThumb(src, thumbDst, gridThumbMaxEdge, gridThumbQuality); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeThumb(src, heroDst, heroThumbMaxEdge, heroThumbQuality)
|
||||
}
|
||||
Reference in New Issue
Block a user