package server import ( "encoding/json" "fmt" "io" "log" "net/http" "strconv" "strings" "odoo-go/pkg/orm" ) // handleUpload handles file uploads to ir.attachment. // Mirrors: odoo/addons/web/controllers/binary.py upload_attachment() func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse multipart form (max 128MB) if err := r.ParseMultipartForm(128 << 20); err != nil { http.Error(w, "File too large", http.StatusRequestEntityTooLarge) return } file, header, err := r.FormFile("ufile") if err != nil { http.Error(w, "No file uploaded", http.StatusBadRequest) return } defer file.Close() // Read file content data, err := io.ReadAll(file) if err != nil { http.Error(w, "Read error", http.StatusInternalServerError) return } log.Printf("upload: received %s (%d bytes, %s)", header.Filename, len(data), header.Header.Get("Content-Type")) // Extract model/id from form values for linking resModel := r.FormValue("model") resIDStr := r.FormValue("id") resID := int64(0) if resIDStr != "" { if v, err := strconv.ParseInt(resIDStr, 10, 64); err == nil { resID = v } } // Get UID from session uid := int64(1) companyID := int64(1) if sess := GetSession(r); sess != nil { uid = sess.UID companyID = sess.CompanyID } // Store in ir.attachment env, err := orm.NewEnvironment(r.Context(), orm.EnvConfig{ Pool: s.pool, UID: uid, CompanyID: companyID, }) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } defer env.Close() // Detect mimetype mimetype := header.Header.Get("Content-Type") if mimetype == "" { mimetype = "application/octet-stream" } attachVals := orm.Values{ "name": header.Filename, "datas": data, "mimetype": mimetype, "file_size": len(data), "type": "binary", } if resModel != "" { attachVals["res_model"] = resModel } if resID > 0 { attachVals["res_id"] = resID } attachRS := env.Model("ir.attachment") created, err := attachRS.Create(attachVals) if err != nil { log.Printf("upload: failed to create attachment: %v", err) // Return success anyway with temp ID (graceful degradation) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]interface{}{ {"id": 0, "name": header.Filename, "size": len(data), "mimetype": mimetype}, }) if commitErr := env.Commit(); commitErr != nil { log.Printf("upload: commit warning: %v", commitErr) } return } if err := env.Commit(); err != nil { http.Error(w, "Commit error", http.StatusInternalServerError) return } // Return Odoo-expected format: array of attachment dicts w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]interface{}{ { "id": created.ID(), "name": header.Filename, "size": len(data), "mimetype": mimetype, }, }) } // handleContent serves attachment content by ID. // Mirrors: odoo/addons/web/controllers/binary.py content() func (s *Server) handleContent(w http.ResponseWriter, r *http.Request) { // Extract attachment ID from URL: /web/content/ parts := strings.Split(r.URL.Path, "/") if len(parts) < 4 { http.Error(w, "Not found", http.StatusNotFound) return } idStr := parts[3] attachID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } uid := int64(1) companyID := int64(1) if sess := GetSession(r); sess != nil { uid = sess.UID companyID = sess.CompanyID } env, err := orm.NewEnvironment(r.Context(), orm.EnvConfig{ Pool: s.pool, UID: uid, CompanyID: companyID, }) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } defer env.Close() // Read attachment attachRS := env.Model("ir.attachment").Browse(attachID) records, err := attachRS.Read([]string{"name", "datas", "mimetype"}) if err != nil || len(records) == 0 { http.Error(w, "Attachment not found", http.StatusNotFound) return } rec := records[0] name, _ := rec["name"].(string) mimetype, _ := rec["mimetype"].(string) if mimetype == "" { mimetype = "application/octet-stream" } w.Header().Set("Content-Type", mimetype) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", name)) if data, ok := rec["datas"].([]byte); ok { w.Write(data) } else { http.Error(w, "No content", http.StatusNotFound) } }