package storage import ( "bytes" "context" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/DeBrosOfficial/network/pkg/httputil" "github.com/DeBrosOfficial/network/pkg/logging" "go.uber.org/zap" ) // Note: Context keys are imported from the gateway package // This avoids duplication and ensures compatibility with middleware // UploadHandler handles POST /v1/storage/upload. // It supports both multipart/form-data and JSON-based uploads with base64-encoded data. // Files are added to IPFS and optionally pinned for persistence. func (h *Handlers) UploadHandler(w http.ResponseWriter, r *http.Request) { if h.ipfsClient == nil { httputil.WriteError(w, http.StatusServiceUnavailable, "IPFS storage not available") return } if !httputil.CheckMethod(w, r, http.MethodPost) { return } // Get namespace from context namespace := h.getNamespaceFromContext(r.Context()) if namespace == "" { httputil.WriteError(w, http.StatusUnauthorized, "namespace required") return } // Get replication factor from config (default: 3) replicationFactor := h.config.IPFSReplicationFactor if replicationFactor == 0 { replicationFactor = 3 } // Check if it's multipart/form-data or JSON contentType := r.Header.Get("Content-Type") var reader io.Reader var name string var shouldPin bool = true // Default to true if strings.HasPrefix(contentType, "multipart/form-data") { // Handle multipart upload if err := r.ParseMultipartForm(32 << 20); err != nil { // 32MB max httputil.WriteError(w, http.StatusBadRequest, fmt.Sprintf("failed to parse multipart form: %v", err)) return } file, header, err := r.FormFile("file") if err != nil { httputil.WriteError(w, http.StatusBadRequest, fmt.Sprintf("failed to get file: %v", err)) return } defer file.Close() reader = file name = header.Filename // Parse pin flag from form (default: true) if pinValue := r.FormValue("pin"); pinValue != "" { shouldPin = strings.ToLower(pinValue) == "true" } } else { // Handle JSON request with base64 data var req StorageUploadRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { httputil.WriteError(w, http.StatusBadRequest, fmt.Sprintf("failed to decode request: %v", err)) return } if req.Data == "" { httputil.WriteError(w, http.StatusBadRequest, "data field required") return } // Decode base64 data data, err := base64Decode(req.Data) if err != nil { httputil.WriteError(w, http.StatusBadRequest, fmt.Sprintf("failed to decode base64 data: %v", err)) return } reader = bytes.NewReader(data) name = req.Name // For JSON requests, pin defaults to true (can be extended if needed) } // Add to IPFS ctx := r.Context() addResp, err := h.ipfsClient.Add(ctx, reader, name) if err != nil { h.logger.ComponentError(logging.ComponentGeneral, "failed to add content to IPFS", zap.Error(err)) httputil.WriteError(w, http.StatusInternalServerError, fmt.Sprintf("failed to add content: %v", err)) return } // Record ownership in database for namespace isolation // Use wallet or API key as uploaded_by identifier uploadedBy := namespace // Could be enhanced to track wallet address if available if err := h.recordCIDOwnership(ctx, addResp.Cid, namespace, addResp.Name, uploadedBy, addResp.Size); err != nil { h.logger.ComponentWarn(logging.ComponentGeneral, "failed to record CID ownership (non-fatal)", zap.Error(err), zap.String("cid", addResp.Cid), zap.String("namespace", namespace)) // Don't fail the upload - this is just for tracking } // Return response immediately - don't block on pinning response := StorageUploadResponse{ Cid: addResp.Cid, Name: addResp.Name, Size: addResp.Size, } // Pin asynchronously in background if requested if shouldPin { go h.pinAsync(addResp.Cid, name, replicationFactor, namespace) } httputil.WriteJSON(w, http.StatusOK, response) } // pinAsync pins a CID asynchronously in the background with retry logic. // It retries once if the first attempt fails, then gives up. func (h *Handlers) pinAsync(cid, name string, replicationFactor int, namespace string) { ctx := context.Background() // First attempt _, err := h.ipfsClient.Pin(ctx, cid, name, replicationFactor) if err == nil { h.logger.ComponentWarn(logging.ComponentGeneral, "async pin succeeded", zap.String("cid", cid)) // Update pin status in database h.updatePinStatus(ctx, cid, namespace, true) return } // Log first failure h.logger.ComponentWarn(logging.ComponentGeneral, "async pin failed, retrying once", zap.Error(err), zap.String("cid", cid)) // Retry once after a short delay time.Sleep(2 * time.Second) _, err = h.ipfsClient.Pin(ctx, cid, name, replicationFactor) if err != nil { // Final failure - log and give up h.logger.ComponentWarn(logging.ComponentGeneral, "async pin retry failed, giving up", zap.Error(err), zap.String("cid", cid)) } else { h.logger.ComponentWarn(logging.ComponentGeneral, "async pin succeeded on retry", zap.String("cid", cid)) // Update pin status in database h.updatePinStatus(ctx, cid, namespace, true) } } // base64Decode decodes a base64 string to bytes. func base64Decode(s string) ([]byte, error) { return base64.StdEncoding.DecodeString(s) }