Added Model Orders and Picture Upload
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user