Commit ec5a5f9093
ec5a5f9093810bc596131924ad0d80a20b53bc57
parent: ffd964b971
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-25 23:15 UTC
Add format registry
Layout: unified · split
.gitignore
added
+4
| @@ -0,0 +1,4 @@ |
| 1 | /photos/ |
| 2 | /cache/ |
| 3 | /gallery |
| 4 | .superpowers/ |
go.mod
added
+3
| @@ -0,0 +1,3 @@ |
| 1 | module gitbay.org/cmc/gallery |
| 2 | |
| 3 | go 1.27.1 |
internal/format/format.go
added
+61
| @@ -0,0 +1,61 @@ |
| 1 | // Package format describes the file types the gallery can show. |
| 2 | package format |
| 3 | |
| 4 | import ( |
| 5 | "io" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | type Kind int |
| 12 | |
| 13 | const ( |
| 14 | KindImage Kind = iota |
| 15 | ) |
| 16 | |
| 17 | // Meta holds display metadata. Width and Height are after EXIF orientation. |
| 18 | type Meta struct { |
| 19 | Width, Height int |
| 20 | Taken time.Time |
| 21 | Camera, Lens string |
| 22 | Aperture string |
| 23 | Shutter string |
| 24 | ISO int |
| 25 | } |
| 26 | |
| 27 | type Format interface { |
| 28 | Name() string |
| 29 | Match(path string) bool |
| 30 | Metadata(path string) (Meta, error) |
| 31 | Resize(src string, width int, dst io.Writer) error |
| 32 | Kind() Kind |
| 33 | } |
| 34 | |
| 35 | // Registry resolves a path to the first registered Format that matches it. |
| 36 | type Registry struct { |
| 37 | formats []Format |
| 38 | } |
| 39 | |
| 40 | func (r *Registry) Register(f Format) { |
| 41 | r.formats = append(r.formats, f) |
| 42 | } |
| 43 | |
| 44 | func (r *Registry) Lookup(path string) Format { |
| 45 | for _, f := range r.formats { |
| 46 | if f.Match(path) { |
| 47 | return f |
| 48 | } |
| 49 | } |
| 50 | return nil |
| 51 | } |
| 52 | |
| 53 | func hasExt(path string, exts []string) bool { |
| 54 | ext := strings.ToLower(filepath.Ext(path)) |
| 55 | for _, e := range exts { |
| 56 | if ext == e { |
| 57 | return true |
| 58 | } |
| 59 | } |
| 60 | return false |
| 61 | } |
internal/format/format_test.go
added
+44
| @@ -0,0 +1,44 @@ |
| 1 | package format |
| 2 | |
| 3 | import ( |
| 4 | "io" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | type fake struct{ name, ext string } |
| 11 | |
| 12 | func (f fake) Name() string { return f.name } |
| 13 | func (f fake) Match(p string) bool { return strings.EqualFold(filepath.Ext(p), f.ext) } |
| 14 | func (fake) Metadata(string) (Meta, error) { return Meta{}, nil } |
| 15 | func (fake) Resize(string, int, io.Writer) error { return nil } |
| 16 | func (fake) Kind() Kind { return KindImage } |
| 17 | |
| 18 | func TestRegistryFirstMatchWins(t *testing.T) { |
| 19 | var r Registry |
| 20 | r.Register(fake{"a", ".jpg"}) |
| 21 | r.Register(fake{"b", ".jpg"}) |
| 22 | r.Register(fake{"c", ".png"}) |
| 23 | |
| 24 | if got := r.Lookup("x/IMG.JPG"); got == nil || got.Name() != "a" { |
| 25 | t.Fatalf("jpg: got %v, want a", got) |
| 26 | } |
| 27 | if got := r.Lookup("x.png"); got == nil || got.Name() != "c" { |
| 28 | t.Fatalf("png: got %v, want c", got) |
| 29 | } |
| 30 | if got := r.Lookup("notes.txt"); got != nil { |
| 31 | t.Fatalf("txt: got %v, want nil", got) |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | func TestHasExt(t *testing.T) { |
| 36 | exts := []string{".jpg", ".jpeg"} |
| 37 | for p, want := range map[string]bool{ |
| 38 | "a.jpg": true, "a.JPEG": true, "a.png": false, "jpg": false, "a.jpg.txt": false, |
| 39 | } { |
| 40 | if got := hasExt(p, exts); got != want { |
| 41 | t.Errorf("hasExt(%q) = %v, want %v", p, got, want) |
| 42 | } |
| 43 | } |
| 44 | } |