219 lines
5.4 KiB
Go
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"
|
|
}
|
|
}
|