219 lines
5.4 KiB
Go

package store
import (
"fmt"
"mime"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"printer.backend/internal/model"
"printer.backend/internal/paths"
)
// OrderStore persists orders as JSON files with images in per-order subdirectories.
type OrderStore struct {
dir string
}
// NewOrderStore creates a store under dir (default data/orders).
func NewOrderStore(dir string) *OrderStore {
if dir == "" {
dir = paths.OrdersDir
}
return &OrderStore{dir: dir}
}
// List returns all orders sorted by creation time (newest first).
func (s *OrderStore) List() ([]model.Order, error) {
return listFromDir(s.dir,
func(name string) bool { return filepath.Ext(name) == ".json" },
"orders dir",
func(o model.Order) time.Time { return o.CreatedAt },
)
}
// Get returns an order by ID.
func (s *OrderStore) Get(id string) (model.Order, error) {
path := s.metaPath(id)
o, err := readJSON[model.Order](path)
if err != nil {
if os.IsNotExist(err) {
return model.Order{}, fmt.Errorf("order not found: %s", id)
}
return model.Order{}, err
}
if o.Images == nil {
o.Images = []model.OrderImage{}
}
return o, nil
}
// Save writes a new order and assigns an ID when empty.
func (s *OrderStore) Save(o model.Order) (model.Order, error) {
if err := ensureDir(s.dir); err != nil {
return model.Order{}, err
}
if o.ID == "" {
o.ID = uuid.NewString()
}
if o.Images == nil {
o.Images = []model.OrderImage{}
}
now := stampNew(&o.CreatedAt)
o.CreatedAt = now
o.UpdatedAt = now
if err := writeJSON(s.metaPath(o.ID), o); err != nil {
return model.Order{}, fmt.Errorf("write order: %w", err)
}
return o, nil
}
// Update replaces order metadata (preserves id, images, created_at).
func (s *OrderStore) Update(id string, o model.Order) (model.Order, error) {
existing, err := s.Get(id)
if err != nil {
return model.Order{}, err
}
o.ID = existing.ID
o.Images = existing.Images
o.CreatedAt = existing.CreatedAt
o.UpdatedAt = time.Now().UTC()
if err := writeJSON(s.metaPath(id), o); err != nil {
return model.Order{}, fmt.Errorf("write order: %w", err)
}
return o, nil
}
// Delete removes an order and all of its images.
func (s *OrderStore) Delete(id string) error {
if _, err := s.Get(id); err != nil {
return err
}
if err := os.Remove(s.metaPath(id)); err != nil && !os.IsNotExist(err) {
return err
}
return os.RemoveAll(s.imagesDir(id))
}
// AddImage stores image bytes and appends metadata to the order.
func (s *OrderStore) AddImage(orderID, originalName string, data []byte, contentType string) (model.OrderImage, error) {
if len(data) == 0 {
return model.OrderImage{}, fmt.Errorf("empty image data")
}
order, err := s.Get(orderID)
if err != nil {
return model.OrderImage{}, err
}
ext := extensionForImage(originalName, contentType)
imageID := uuid.NewString()
filename := imageID + ext
if err := ensureDir(s.imagesDir(orderID)); err != nil {
return model.OrderImage{}, err
}
path := filepath.Join(s.imagesDir(orderID), filename)
if err := os.WriteFile(path, data, 0o644); err != nil {
return model.OrderImage{}, fmt.Errorf("write image: %w", err)
}
img := model.OrderImage{
ID: imageID,
OriginalName: filepath.Base(originalName),
ContentType: contentType,
Filename: filename,
CreatedAt: time.Now().UTC(),
}
order.Images = append(order.Images, img)
order.UpdatedAt = time.Now().UTC()
if err := writeJSON(s.metaPath(orderID), order); err != nil {
_ = os.Remove(path)
return model.OrderImage{}, fmt.Errorf("update order: %w", err)
}
return img, nil
}
// DeleteImage removes one image from an order.
func (s *OrderStore) DeleteImage(orderID, imageID string) error {
order, err := s.Get(orderID)
if err != nil {
return err
}
idx := -1
var filename string
for i, img := range order.Images {
if img.ID == imageID {
idx = i
filename = img.Filename
break
}
}
if idx < 0 {
return fmt.Errorf("image not found: %s", imageID)
}
path := filepath.Join(s.imagesDir(orderID), filename)
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return err
}
order.Images = append(order.Images[:idx], order.Images[idx+1:]...)
order.UpdatedAt = time.Now().UTC()
return writeJSON(s.metaPath(orderID), order)
}
// ImagePath returns the filesystem path for an order image.
func (s *OrderStore) ImagePath(orderID, imageID string) (string, model.OrderImage, error) {
order, err := s.Get(orderID)
if err != nil {
return "", model.OrderImage{}, err
}
for _, img := range order.Images {
if img.ID == imageID {
path := filepath.Join(s.imagesDir(orderID), img.Filename)
if _, err := os.Stat(path); err != nil {
return "", model.OrderImage{}, fmt.Errorf("image file missing: %w", err)
}
return path, img, nil
}
}
return "", model.OrderImage{}, fmt.Errorf("image not found: %s", imageID)
}
func (s *OrderStore) metaPath(id string) string {
return filepath.Join(s.dir, id+".json")
}
func (s *OrderStore) imagesDir(orderID string) string {
return filepath.Join(s.dir, orderID)
}
func extensionForImage(originalName, contentType string) string {
if ext := filepath.Ext(originalName); ext != "" {
return strings.ToLower(ext)
}
if exts, _ := mime.ExtensionsByType(contentType); len(exts) > 0 {
return exts[0]
}
switch contentType {
case "image/jpeg":
return ".jpg"
case "image/png":
return ".png"
case "image/webp":
return ".webp"
case "image/gif":
return ".gif"
case "image/svg+xml":
return ".svg"
default:
return ".bin"
}
}