package handlers import ( "errors" "fmt" "io" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "veza-backend-api/internal/services" ) type CloudHandler struct { cloudService *services.CloudService logger *zap.Logger } func NewCloudHandler(cloudService *services.CloudService, logger *zap.Logger) *CloudHandler { return &CloudHandler{ cloudService: cloudService, logger: logger, } } func (h *CloudHandler) getUserID(c *gin.Context) (uuid.UUID, error) { userIDStr, exists := c.Get("user_id") if !exists { return uuid.Nil, fmt.Errorf("user_id not found in context") } switch v := userIDStr.(type) { case string: return uuid.Parse(v) case uuid.UUID: return v, nil default: return uuid.Nil, fmt.Errorf("invalid user_id type") } } func (h *CloudHandler) ListFolders(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } var parentID *uuid.UUID if pidStr := c.Query("parent_id"); pidStr != "" { pid, err := uuid.Parse(pidStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"}) return } parentID = &pid } folders, err := h.cloudService.ListFolders(c.Request.Context(), userID, parentID) if err != nil { h.logger.Error("Failed to list folders", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list folders"}) return } c.JSON(http.StatusOK, gin.H{"folders": folders}) } func (h *CloudHandler) CreateFolder(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } var req struct { Name string `json:"name" binding:"required"` ParentID *string `json:"parent_id"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) return } var parentID *uuid.UUID if req.ParentID != nil && *req.ParentID != "" { pid, err := uuid.Parse(*req.ParentID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"}) return } parentID = &pid } folder, err := h.cloudService.CreateFolder(c.Request.Context(), userID, req.Name, parentID) if err != nil { if errors.Is(err, services.ErrFolderNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "parent folder not found"}) return } h.logger.Error("Failed to create folder", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create folder"}) return } c.JSON(http.StatusCreated, gin.H{"folder": folder}) } func (h *CloudHandler) RenameFolder(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } folderID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder id"}) return } var req struct { Name string `json:"name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) return } if err := h.cloudService.RenameFolder(c.Request.Context(), userID, folderID, req.Name); err != nil { if errors.Is(err, services.ErrFolderNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "folder not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to rename folder"}) return } c.JSON(http.StatusOK, gin.H{"message": "folder renamed"}) } func (h *CloudHandler) DeleteFolder(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } folderID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder id"}) return } if err := h.cloudService.DeleteFolder(c.Request.Context(), userID, folderID); err != nil { if errors.Is(err, services.ErrFolderNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "folder not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete folder"}) return } c.JSON(http.StatusOK, gin.H{"message": "folder deleted"}) } func (h *CloudHandler) ListFiles(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } var folderID *uuid.UUID if fidStr := c.Query("folder_id"); fidStr != "" { fid, err := uuid.Parse(fidStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder_id"}) return } folderID = &fid } files, err := h.cloudService.ListFiles(c.Request.Context(), userID, folderID) if err != nil { h.logger.Error("Failed to list files", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list files"}) return } c.JSON(http.StatusOK, gin.H{"files": files}) } func (h *CloudHandler) UploadFile(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) return } defer file.Close() data, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"}) return } mimeType := header.Header.Get("Content-Type") if mimeType == "" { mimeType = "application/octet-stream" } var folderID *uuid.UUID if fidStr := c.PostForm("folder_id"); fidStr != "" { fid, err := uuid.Parse(fidStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder_id"}) return } folderID = &fid } result, err := h.cloudService.UploadFile(c.Request.Context(), userID, folderID, header.Filename, data, mimeType) if err != nil { if errors.Is(err, services.ErrQuotaExceeded) { c.JSON(http.StatusForbidden, gin.H{"error": "storage quota exceeded"}) return } if errors.Is(err, services.ErrInvalidMimeType) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file type"}) return } if errors.Is(err, services.ErrFileTooLarge) { c.JSON(http.StatusBadRequest, gin.H{"error": "file too large (max 500MB)"}) return } h.logger.Error("Failed to upload file", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to upload file"}) return } c.JSON(http.StatusCreated, gin.H{"file": result}) } func (h *CloudHandler) GetFile(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } fileID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file id"}) return } file, err := h.cloudService.GetFileByID(c.Request.Context(), userID, fileID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "file not found"}) return } c.JSON(http.StatusOK, gin.H{"file": file}) } func (h *CloudHandler) DeleteFile(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } fileID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file id"}) return } if err := h.cloudService.DeleteFile(c.Request.Context(), userID, fileID); err != nil { if errors.Is(err, services.ErrFileNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "file not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete file"}) return } c.JSON(http.StatusOK, gin.H{"message": "file deleted"}) } // C1-04: Stream file with HTTP Range support func (h *CloudHandler) StreamFile(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } fileID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file id"}) return } data, file, err := h.cloudService.StreamFile(c.Request.Context(), userID, fileID) if err != nil { if errors.Is(err, services.ErrFileNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "file not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to stream file"}) return } totalSize := int64(len(data)) c.Header("Accept-Ranges", "bytes") c.Header("Content-Type", file.MimeType) rangeHeader := c.GetHeader("Range") if rangeHeader == "" { c.Header("Content-Length", strconv.FormatInt(totalSize, 10)) c.Data(http.StatusOK, file.MimeType, data) return } rangeStr := strings.TrimPrefix(rangeHeader, "bytes=") parts := strings.SplitN(rangeStr, "-", 2) if len(parts) != 2 { c.JSON(http.StatusRequestedRangeNotSatisfiable, gin.H{"error": "invalid range"}) return } var start, end int64 if parts[0] != "" { start, err = strconv.ParseInt(parts[0], 10, 64) if err != nil || start < 0 || start >= totalSize { c.JSON(http.StatusRequestedRangeNotSatisfiable, gin.H{"error": "invalid range start"}) return } } if parts[1] != "" { end, err = strconv.ParseInt(parts[1], 10, 64) if err != nil || end >= totalSize { end = totalSize - 1 } } else { end = totalSize - 1 } if start > end { c.JSON(http.StatusRequestedRangeNotSatisfiable, gin.H{"error": "invalid range"}) return } contentLength := end - start + 1 c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, totalSize)) c.Header("Content-Length", strconv.FormatInt(contentLength, 10)) c.Data(http.StatusPartialContent, file.MimeType, data[start:end+1]) } func (h *CloudHandler) GetQuota(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } quota, err := h.cloudService.GetQuota(c.Request.Context(), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get quota"}) return } c.JSON(http.StatusOK, gin.H{ "quota": gin.H{ "user_id": quota.UserID, "max_bytes": quota.MaxBytes, "used_bytes": quota.UsedBytes, "available": quota.MaxBytes - quota.UsedBytes, "percentage": float64(quota.UsedBytes) / float64(quota.MaxBytes) * 100, }, }) } // C1-05: Publish cloud file as track func (h *CloudHandler) PublishAsTrack(c *gin.Context) { userID, err := h.getUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } fileID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file id"}) return } file, err := h.cloudService.GetFileByID(c.Request.Context(), userID, fileID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "file not found"}) return } if !strings.HasPrefix(file.MimeType, "audio/") { c.JSON(http.StatusBadRequest, gin.H{"error": "only audio files can be published as tracks"}) return } c.JSON(http.StatusOK, gin.H{ "message": "track creation initiated from cloud file", "file_id": file.ID, "filename": file.Filename, "s3_key": file.S3Key, }) }