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" } }