Introduces the Go photo-api service, nginx/systemd deploy wiring, and Next.js gallery/lightbox pages so event photos can be managed and browsed. Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
2.0 KiB
Go
92 lines
2.0 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// local stores objects under root (STORAGE_PATH, default ./data/photos).
|
|
// Deliberately disjoint from backend/uploads; nothing here is ever served
|
|
// statically — all reads go through the access-checked files handler.
|
|
type local struct {
|
|
root string
|
|
}
|
|
|
|
func newLocal(root string) (*local, error) {
|
|
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
return nil, fmt.Errorf("create storage dir %s: %w", root, err)
|
|
}
|
|
return &local{root: root}, nil
|
|
}
|
|
|
|
// path validates the key stays inside root (keys are generated internally,
|
|
// but defense-in-depth costs one Clean call).
|
|
func (l *local) path(key string) (string, error) {
|
|
clean := filepath.Clean("/" + key)
|
|
if strings.Contains(clean, "..") {
|
|
return "", fmt.Errorf("invalid key %q", key)
|
|
}
|
|
return filepath.Join(l.root, clean), nil
|
|
}
|
|
|
|
func (l *local) Put(_ context.Context, key string, r io.Reader, _ int64, _ string) error {
|
|
dst, err := l.path(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(dst), ".upload-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
if _, err := io.Copy(tmp, r); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp.Name(), dst)
|
|
}
|
|
|
|
func (l *local) Open(_ context.Context, key string) (io.ReadCloser, int64, error) {
|
|
p, err := l.path(key)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, 0, err
|
|
}
|
|
return f, info.Size(), nil
|
|
}
|
|
|
|
func (l *local) Delete(_ context.Context, key string) error {
|
|
p, err := l.path(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.Remove(p)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (l *local) PresignGet(context.Context, string, string, string, time.Duration) (string, error) {
|
|
return "", ErrNoPresign
|
|
}
|