123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
package qslcard
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// TemplateDir returns the asset folder of a stored template:
|
|
// <baseDir>/templates/<id>. baseDir is <dataDir>/qsl.
|
|
func TemplateDir(baseDir string, id int64) string {
|
|
return filepath.Join(baseDir, "templates", fmt.Sprintf("%d", id))
|
|
}
|
|
|
|
// IsDraftPhoto reports whether a photo reference still points at the user's
|
|
// original file (absolute path) rather than a name inside the template dir.
|
|
func IsDraftPhoto(ref string) bool { return filepath.IsAbs(ref) }
|
|
|
|
// ImportPhotos copies every absolute-path photo the template references into
|
|
// dir and rewrites the references to the copied names (img_<hash8>.<ext>).
|
|
// Users move files around — a saved template must never depend on the
|
|
// original location. Already-relative references are left untouched.
|
|
func ImportPhotos(t *Template, dir string) error {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("create template dir: %w", err)
|
|
}
|
|
imported := map[string]string{} // original path → copied name
|
|
importOne := func(ref string) (string, error) {
|
|
if !IsDraftPhoto(ref) {
|
|
return ref, nil
|
|
}
|
|
if name, ok := imported[ref]; ok {
|
|
return name, nil
|
|
}
|
|
name, err := copyPhoto(ref, dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
imported[ref] = name
|
|
return name, nil
|
|
}
|
|
name, err := importOne(t.Hero.Photo)
|
|
if err != nil {
|
|
return fmt.Errorf("import hero photo: %w", err)
|
|
}
|
|
t.Hero.Photo = name
|
|
for i := range t.Elements {
|
|
if t.Elements[i].Type != ElemInsert {
|
|
continue
|
|
}
|
|
name, err := importOne(t.Elements[i].Photo)
|
|
if err != nil {
|
|
return fmt.Errorf("import insert photo: %w", err)
|
|
}
|
|
t.Elements[i].Photo = name
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// copyPhoto copies src into dir under a content-derived name so identical
|
|
// photos dedupe and re-saving is idempotent.
|
|
func copyPhoto(src, dir string) (string, error) {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open photo %s: %w", src, err)
|
|
}
|
|
defer in.Close()
|
|
h := sha256.New()
|
|
if _, err := io.Copy(h, in); err != nil {
|
|
return "", fmt.Errorf("hash photo %s: %w", src, err)
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(src))
|
|
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
|
|
return "", fmt.Errorf("unsupported photo type %q (jpeg/png only)", ext)
|
|
}
|
|
name := "img_" + hex.EncodeToString(h.Sum(nil))[:8] + ext
|
|
dst := filepath.Join(dir, name)
|
|
if _, err := os.Stat(dst); err == nil {
|
|
return name, nil // identical content already imported
|
|
}
|
|
if _, err := in.Seek(0, io.SeekStart); err != nil {
|
|
return "", fmt.Errorf("rewind photo %s: %w", src, err)
|
|
}
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create %s: %w", dst, err)
|
|
}
|
|
defer out.Close()
|
|
if _, err := io.Copy(out, in); err != nil {
|
|
return "", fmt.Errorf("copy photo to %s: %w", dst, err)
|
|
}
|
|
if err := out.Close(); err != nil {
|
|
return "", fmt.Errorf("flush %s: %w", dst, err)
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
// PhotoExistsIn returns a Validate callback that checks photo references
|
|
// against a template asset dir (relative names) or the filesystem (drafts).
|
|
func PhotoExistsIn(dir string) func(string) bool {
|
|
return func(ref string) bool {
|
|
p := ref
|
|
if !IsDraftPhoto(ref) {
|
|
p = filepath.Join(dir, ref)
|
|
}
|
|
_, err := os.Stat(p)
|
|
return err == nil
|
|
}
|
|
}
|
|
|
|
// RemoveTemplateDir deletes a template's asset folder (photos + preview).
|
|
func RemoveTemplateDir(baseDir string, id int64) error {
|
|
dir := TemplateDir(baseDir, id)
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
return fmt.Errorf("remove template dir %s: %w", dir, err)
|
|
}
|
|
return nil
|
|
}
|