The .bnp packers never carried preview.png (the .so-era source archives did implicitly), so any republish silently wiped a theme's registry preview. Both builders now pack an optional root preview.png and a screenshots/ dir (png/jpg/jpeg/webp, <=8MB each, <=12 files, validated), reported in the build/verify summaries. ninja theme screenshot gains --pages/--screenshots-dir to capture NN-<slug>.png per gallery page for the registry gallery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
122 lines
4.0 KiB
Go
122 lines
4.0 KiB
Go
package bnp
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Presentation members: a root preview.png plus an optional screenshots/ dir.
|
|
// Neither ships behavior — the registry ingests them at publish into the
|
|
// plugin's managed gallery (spec 2026-07-05-registry-presentation-reviews).
|
|
// The .so-era source archives carried preview.png implicitly; the .bnp
|
|
// packers dropped it, which silently wiped previews on republish — these
|
|
// entries restore that and add the multi-screenshot surface.
|
|
const (
|
|
filePreview = "preview.png"
|
|
dirScreenshots = "screenshots"
|
|
// maxScreenshotBytes caps one presentation image (registry gallery cap).
|
|
maxScreenshotBytes = 8 << 20
|
|
// maxScreenshotFiles caps the screenshots/ dir (registry gallery cap).
|
|
maxScreenshotFiles = 12
|
|
)
|
|
|
|
// screenshotExtOK reports whether a filename carries an accepted gallery
|
|
// image extension.
|
|
func screenshotExtOK(name string) bool {
|
|
switch strings.ToLower(filepath.Ext(name)) {
|
|
case ".png", ".jpg", ".jpeg", ".webp":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// validateScreenshotsDir checks the optional screenshots/ dir: accepted
|
|
// extensions only, ≤ maxScreenshotBytes per file, ≤ maxScreenshotFiles total,
|
|
// no subdirectories or irregular files. Returns the file count (0 when the
|
|
// dir is absent).
|
|
func validateScreenshotsDir(dir string) (int, error) {
|
|
root := filepath.Join(dir, dirScreenshots)
|
|
info, err := os.Stat(root)
|
|
if os.IsNotExist(err) {
|
|
return 0, nil
|
|
}
|
|
if err != nil {
|
|
return 0, fmt.Errorf("stat %s/: %w", dirScreenshots, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return 0, fmt.Errorf("%s exists but is not a directory", dirScreenshots)
|
|
}
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("read %s/: %w", dirScreenshots, err)
|
|
}
|
|
count := 0
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
return 0, fmt.Errorf("%s/%s: subdirectories are not allowed in screenshots/", dirScreenshots, e.Name())
|
|
}
|
|
fi, err := e.Info()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s/%s: %w", dirScreenshots, e.Name(), err)
|
|
}
|
|
if !fi.Mode().IsRegular() {
|
|
return 0, fmt.Errorf("%s/%s: not a regular file", dirScreenshots, e.Name())
|
|
}
|
|
if !screenshotExtOK(e.Name()) {
|
|
return 0, fmt.Errorf("%s/%s: unsupported image type (allowed: .png .jpg .jpeg .webp)", dirScreenshots, e.Name())
|
|
}
|
|
if fi.Size() > maxScreenshotBytes {
|
|
return 0, fmt.Errorf("%s/%s: %d bytes exceeds the %d MB per-image cap", dirScreenshots, e.Name(), fi.Size(), maxScreenshotBytes>>20)
|
|
}
|
|
count++
|
|
}
|
|
if count > maxScreenshotFiles {
|
|
return 0, fmt.Errorf("%s/ holds %d images; the gallery cap is %d", dirScreenshots, count, maxScreenshotFiles)
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// presentationEntries validates and returns pack entries for the optional
|
|
// root preview.png and screenshots/ dir. Both are optional; absence is not an
|
|
// error.
|
|
func presentationEntries(dir string) (entries []packEntry, previewIncluded bool, screenshotCount int, err error) {
|
|
previewPath := filepath.Join(dir, filePreview)
|
|
if fi, statErr := os.Stat(previewPath); statErr == nil {
|
|
if !fi.Mode().IsRegular() {
|
|
return nil, false, 0, fmt.Errorf("%s is not a regular file", filePreview)
|
|
}
|
|
if fi.Size() > maxScreenshotBytes {
|
|
return nil, false, 0, fmt.Errorf("%s: %d bytes exceeds the %d MB per-image cap", filePreview, fi.Size(), maxScreenshotBytes>>20)
|
|
}
|
|
entries = append(entries, packEntry{ArtifactPath: filePreview, Source: previewPath})
|
|
previewIncluded = true
|
|
}
|
|
|
|
screenshotCount, err = validateScreenshotsDir(dir)
|
|
if err != nil {
|
|
return nil, false, 0, err
|
|
}
|
|
if screenshotCount > 0 {
|
|
root := filepath.Join(dir, dirScreenshots)
|
|
files, readErr := os.ReadDir(root)
|
|
if readErr != nil {
|
|
return nil, false, 0, fmt.Errorf("read %s/: %w", dirScreenshots, readErr)
|
|
}
|
|
names := make([]string, 0, len(files))
|
|
for _, f := range files {
|
|
names = append(names, f.Name())
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
entries = append(entries, packEntry{
|
|
ArtifactPath: dirScreenshots + "/" + name,
|
|
Source: filepath.Join(root, name),
|
|
})
|
|
}
|
|
}
|
|
return entries, previewIncluded, screenshotCount, nil
|
|
}
|