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