package handler import ( "errors" "fmt" "io" "net/http" "net/url" "strconv" "github.com/gin-gonic/gin" "github.com/dhao2001/mygo/internal/api" "github.com/dhao2001/mygo/internal/middleware" "github.com/dhao2001/mygo/internal/model" "github.com/dhao2001/mygo/internal/service" ) // FileHandler handles file management endpoints. type FileHandler struct { fileService *service.FileService } // NewFileHandler creates a FileHandler. func NewFileHandler(fileService *service.FileService) *FileHandler { return &FileHandler{fileService: fileService} } type createDirRequest struct { Name string `json:"name" binding:"required"` ParentID *string `json:"parent_id"` } type updateFileRequest struct { Name string `json:"name"` ParentID *string `json:"parent_id"` } // Upload handles POST /api/v1/files. // If the content type is multipart/form-data, it uploads a file. // If the content type is application/json, it creates a directory. func (h *FileHandler) Upload(c *gin.Context) { userID := middleware.MustGetUserID(c) contentType := c.GetHeader("Content-Type") // Directory creation via JSON. if len(contentType) >= 16 && contentType[:16] == "application/json" { var req createDirRequest if err := c.ShouldBindJSON(&req); err != nil { api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error()) return } dir, err := h.fileService.CreateDir(c.Request.Context(), userID, req.ParentID, req.Name) if err != nil { api.Error(c, mapError(err), err.Error()) return } c.JSON(http.StatusCreated, dir) return } // File upload via multipart. if err := c.Request.ParseMultipartForm(32 << 20); err != nil { api.Error(c, http.StatusBadRequest, "invalid multipart form: "+err.Error()) return } parentIDStr := c.PostForm("parent_id") var parentID *string if parentIDStr != "" { parentID = &parentIDStr } header, err := c.FormFile("file") if err != nil { api.Error(c, http.StatusBadRequest, "missing file field: "+err.Error()) return } file, err := header.Open() if err != nil { api.Error(c, http.StatusInternalServerError, "open uploaded file: "+err.Error()) return } defer file.Close() info, err := h.fileService.Upload(c.Request.Context(), userID, parentID, header.Filename, file, header.Header.Get("Content-Type")) if err != nil { api.Error(c, mapError(err), err.Error()) return } c.JSON(http.StatusCreated, info) } // List handles GET /api/v1/files. func (h *FileHandler) List(c *gin.Context) { userID := middleware.MustGetUserID(c) parentIDStr := c.Query("parent_id") var parentID *string if parentIDStr != "" { parentID = &parentIDStr } offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")) if err != nil || offset < 0 { api.Error(c, http.StatusBadRequest, "invalid offset") return } limit, err := strconv.Atoi(c.DefaultQuery("limit", "50")) if err != nil || limit < 1 { api.Error(c, http.StatusBadRequest, "invalid limit") return } if limit > 200 { limit = 200 } list, err := h.fileService.List(c.Request.Context(), userID, parentID, offset, limit) if err != nil { api.Error(c, mapError(err), err.Error()) return } c.JSON(http.StatusOK, list) } // Get handles GET /api/v1/files/:id. func (h *FileHandler) Get(c *gin.Context) { userID := middleware.MustGetUserID(c) fileID := c.Param("id") info, err := h.fileService.Get(c.Request.Context(), userID, fileID) if err != nil { api.Error(c, mapError(err), err.Error()) return } c.JSON(http.StatusOK, info) } // Download handles GET /api/v1/files/:id/content. func (h *FileHandler) Download(c *gin.Context) { userID := middleware.MustGetUserID(c) fileID := c.Param("id") reader, info, err := h.fileService.Download(c.Request.Context(), userID, fileID) if err != nil { api.Error(c, mapError(err), err.Error()) return } defer reader.Close() mimeType := info.MimeType if mimeType == "" { mimeType = "application/octet-stream" } c.Header("Content-Type", mimeType) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", url.PathEscape(info.Name))) c.Header("Content-Length", strconv.FormatInt(info.Size, 10)) c.Status(http.StatusOK) io.Copy(c.Writer, reader) } // Update handles PUT /api/v1/files/:id. func (h *FileHandler) Update(c *gin.Context) { userID := middleware.MustGetUserID(c) fileID := c.Param("id") var req updateFileRequest if err := c.ShouldBindJSON(&req); err != nil { api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error()) return } // Validate at least one field is provided. if req.Name == "" && req.ParentID == nil { api.Error(c, http.StatusBadRequest, "at least one of name or parent_id must be provided") return } info, err := h.fileService.Update(c.Request.Context(), userID, fileID, req.Name, req.ParentID) if err != nil { api.Error(c, mapError(err), err.Error()) return } c.JSON(http.StatusOK, info) } // Delete handles DELETE /api/v1/files/:id. func (h *FileHandler) Delete(c *gin.Context) { userID := middleware.MustGetUserID(c) fileID := c.Param("id") if err := h.fileService.Delete(c.Request.Context(), userID, fileID); err != nil { api.Error(c, mapError(err), err.Error()) return } c.Status(http.StatusNoContent) } // mapError maps sentinel errors to HTTP status codes. func mapError(err error) int { switch { case errors.Is(err, model.ErrNotFound): return http.StatusNotFound case errors.Is(err, model.ErrForbidden): return http.StatusForbidden case errors.Is(err, model.ErrDuplicate): return http.StatusConflict default: return http.StatusInternalServerError } }