package qslcard import ( "crypto/sha256" "encoding/hex" "fmt" "io" "os" "path/filepath" "strings" ) // TemplateDir returns the asset folder of a stored template: // /templates/. baseDir is /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_.). // 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 }