package api import ( "encoding/json" "errors" "io" "mime" "mime/multipart" "net/http" "os" "path/filepath" "printer.backend/internal/model" "printer.backend/internal/store" ) func registerOrderRoutes(mux *http.ServeMux, orders *store.OrderStore) { mux.HandleFunc("GET /orders", listOrders(orders)) mux.HandleFunc("POST /orders", saveOrder(orders)) mux.HandleFunc("GET /orders/{id}", getOrder(orders)) mux.HandleFunc("PUT /orders/{id}", updateOrder(orders)) mux.HandleFunc("DELETE /orders/{id}", deleteOrder(orders)) mux.HandleFunc("POST /orders/{id}/images", addOrderImages(orders)) mux.HandleFunc("DELETE /orders/{id}/images/{imageId}", deleteOrderImage(orders)) mux.HandleFunc("GET /orders/{id}/images/{imageId}", serveOrderImage(orders)) } func listOrders(s *store.OrderStore) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { list, err := s.List() if err != nil { writeError(w, http.StatusInternalServerError, err) return } if list == nil { list = []model.Order{} } for i := range list { if list[i].Images == nil { list[i].Images = []model.OrderImage{} } } writeJSON(w, http.StatusOK, list) } } type saveOrderRequest struct { Name string `json:"name"` } func saveOrder(s *store.OrderStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req saveOrderRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, err) return } saved, err := s.Save(model.Order{Name: req.Name}) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusCreated, saved) } } func getOrder(s *store.OrderStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") o, err := s.Get(id) if err != nil { writeError(w, http.StatusNotFound, err) return } writeJSON(w, http.StatusOK, o) } } func updateOrder(s *store.OrderStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var req saveOrderRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, err) return } saved, err := s.Update(id, model.Order{Name: req.Name}) if err != nil { writeError(w, http.StatusNotFound, err) return } writeJSON(w, http.StatusOK, saved) } } func deleteOrder(s *store.OrderStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := s.Delete(id); err != nil { writeError(w, http.StatusNotFound, err) return } w.WriteHeader(http.StatusNoContent) } } func addOrderImages(s *store.OrderStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { orderID := r.PathValue("id") if _, err := s.Get(orderID); err != nil { writeError(w, http.StatusNotFound, err) return } if err := r.ParseMultipartForm(32 << 20); err != nil { writeError(w, http.StatusBadRequest, err) return } var files []*multipart.FileHeader if r.MultipartForm != nil && len(r.MultipartForm.File["images"]) > 0 { files = r.MultipartForm.File["images"] } else if _, fh, err := r.FormFile("image"); err == nil { files = []*multipart.FileHeader{fh} } else { writeError(w, http.StatusBadRequest, errors.New("image or images field required")) return } added := make([]model.OrderImage, 0, len(files)) for _, fh := range files { data, contentType, err := readUpload(fh) if err != nil { writeError(w, http.StatusBadRequest, err) return } img, err := s.AddImage(orderID, fh.Filename, data, contentType) if err != nil { writeError(w, http.StatusBadRequest, err) return } added = append(added, img) } order, err := s.Get(orderID) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusCreated, map[string]any{ "added": added, "order": order, }) } } func readUpload(fh *multipart.FileHeader) ([]byte, string, error) { rc, err := fh.Open() if err != nil { return nil, "", err } defer rc.Close() const maxImageSize = 32 << 20 data, err := io.ReadAll(io.LimitReader(rc, maxImageSize+1)) if err != nil { return nil, "", err } if len(data) > maxImageSize { return nil, "", errors.New("image too large") } contentType := http.DetectContentType(data) return data, contentType, nil } func deleteOrderImage(s *store.OrderStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { orderID := r.PathValue("id") imageID := r.PathValue("imageId") if err := s.DeleteImage(orderID, imageID); err != nil { writeError(w, http.StatusNotFound, err) return } w.WriteHeader(http.StatusNoContent) } } func serveOrderImage(s *store.OrderStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { orderID := r.PathValue("id") imageID := r.PathValue("imageId") path, img, err := s.ImagePath(orderID, imageID) if err != nil { writeError(w, http.StatusNotFound, err) return } data, err := os.ReadFile(path) if err != nil { writeError(w, http.StatusInternalServerError, err) return } contentType := img.ContentType if contentType == "" { contentType = mime.TypeByExtension(filepath.Ext(path)) } if contentType == "" || contentType == "application/octet-stream" { contentType = http.DetectContentType(data) } w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", "no-cache") _, _ = w.Write(data) } }