package handlers import ( "Noooste/garage-ui/internal/models" "Noooste/garage-ui/internal/services" "github.com/gofiber/fiber/v3" ) // BucketHandler handles bucket-related HTTP requests. type BucketHandler struct { adminService services.AdminService s3Service services.S3Storage } // NewBucketHandler creates a new bucket handler. func NewBucketHandler(adminService services.AdminService, s3Service services.S3Storage) *BucketHandler { return &BucketHandler{ adminService: adminService, s3Service: s3Service, } } // ListBuckets lists all buckets // // @Summary List all buckets // @Description Retrieves a list of all buckets in the Garage storage system with object count and size // @Tags Buckets // @Accept json // @Produce json // @Success 200 {object} models.APIResponse{data=models.BucketListResponse} "Successfully retrieved list of buckets" // @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to list buckets" // @Router /api/v1/buckets [get] func (h *BucketHandler) ListBuckets(c fiber.Ctx) error { ctx := c.Context() // List all buckets from Garage Admin API adminBuckets, err := h.adminService.ListBuckets(ctx) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeListFailed, "Failed to list buckets: "+err.Error()), ) } // Convert admin bucket response to BucketInfo buckets := make([]models.BucketInfo, 0, len(adminBuckets)) for _, adminBucket := range adminBuckets { // Get the bucket name from global aliases var bucketName string if len(adminBucket.GlobalAliases) > 0 { bucketName = adminBucket.GlobalAliases[0] } else { // Skip buckets without global aliases continue } // Get detailed bucket info from Admin API to retrieve object count and size detailedInfo, err := h.adminService.GetBucketInfoByAlias(ctx, bucketName) if err != nil { // If we can't get detailed info, return basic info without stats buckets = append(buckets, models.BucketInfo{ Name: bucketName, CreationDate: adminBucket.Created, Region: "", }) continue } bucketInfo := models.BucketInfo{ Name: bucketName, CreationDate: adminBucket.Created, Region: "", ObjectCount: &detailedInfo.Objects, Size: &detailedInfo.Bytes, WebsiteAccess: detailedInfo.WebsiteAccess, WebsiteConfig: detailedInfo.WebsiteConfig, } buckets = append(buckets, bucketInfo) } response := models.BucketListResponse{ Buckets: buckets, Count: len(buckets), } return c.JSON(models.SuccessResponse(response)) } // CreateBucket creates a new bucket // // @Summary Create a new bucket // @Description Creates a new bucket in the Garage storage system // @Tags Buckets // @Accept json // @Produce json // @Param payload body models.CreateBucketRequest true "Bucket creation payload" // @Success 201 {object} models.APIResponse{data=object{bucket=string,message=string}} "Bucket created successfully" // @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request body or bucket name is required" // @Failure 409 {object} models.APIResponse{error=models.APIError} "Bucket already exists" // @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to create bucket" // @Router /api/v1/buckets [post] func (h *BucketHandler) CreateBucket(c fiber.Ctx) error { ctx := c.Context() // Parse request body var req models.CreateBucketRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Invalid request body: "+err.Error()), ) } // Validate bucket name if req.Name == "" { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Bucket name is required"), ) } // Create the bucket createBucketReq := models.CreateBucketAdminRequest{ GlobalAlias: &req.Name, } if _, err := h.adminService.CreateBucket(ctx, createBucketReq); err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeInternalError, "Failed to create bucket: "+err.Error()), ) } // Return success response response := map[string]interface{}{ "bucket": req.Name, "message": "Bucket created successfully", } return c.Status(fiber.StatusCreated).JSON(models.SuccessResponse(response)) } // DeleteBucket deletes a bucket // // @Summary Delete a bucket // @Description Deletes an existing bucket from the Garage storage system. The bucket must be empty before deletion. // @Tags Buckets // @Accept json // @Produce json // @Param name path string true "Name of the bucket to delete" // @Success 200 {object} models.APIResponse{data=object{bucket=string,message=string}} "Bucket deleted successfully" // @Failure 400 {object} models.APIResponse{error=models.APIError} "Bucket name is required" // @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket does not exist" // @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to delete bucket" // @Router /api/v1/buckets/{name} [delete] func (h *BucketHandler) DeleteBucket(c fiber.Ctx) error { ctx := c.Context() // Get bucket name from URL parameter bucketName := c.Params("name") if bucketName == "" { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Bucket name is required"), ) } // Check if bucket already exists bucketInfo, err := h.adminService.GetBucketInfoByAlias(ctx, bucketName) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeInternalError, "Failed to check bucket existence: "+err.Error()), ) } if bucketInfo == nil { return c.Status(fiber.StatusNotFound).JSON( models.ErrorResponse(models.ErrCodeBucketNotFound, "Bucket does not exist"), ) } // Delete the bucket if err := h.adminService.DeleteBucket(ctx, bucketInfo.ID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeDeleteFailed, "Failed to delete bucket: "+err.Error()), ) } // Return success response response := map[string]interface{}{ "bucket": bucketName, "message": "Bucket deleted successfully", } return c.JSON(models.SuccessResponse(response)) } // GetBucketInfo returns information about a specific bucket // // @Summary Get bucket information // @Description Retrieves detailed information about a specific bucket including creation date and region // @Tags Buckets // @Accept json // @Produce json // @Param name path string true "Name of the bucket to retrieve information for" // @Success 200 {object} models.APIResponse{data=models.BucketInfo} "Successfully retrieved bucket information" // @Failure 400 {object} models.APIResponse{error=models.APIError} "Bucket name is required" // @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket does not exist" // @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to retrieve bucket information" // @Router /api/v1/buckets/{name} [get] func (h *BucketHandler) GetBucketInfo(c fiber.Ctx) error { ctx := c.Context() // Get bucket name from URL parameter bucketName := c.Params("name") if bucketName == "" { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Bucket name is required"), ) } // Check if bucket already exists bucketInfo, err := h.adminService.GetBucketInfoByAlias(ctx, bucketName) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeInternalError, "Failed to check bucket existence: "+err.Error()), ) } if bucketInfo == nil { return c.Status(fiber.StatusNotFound).JSON( models.ErrorResponse(models.ErrCodeBucketNotFound, "Bucket does not exist"), ) } return c.JSON(models.SuccessResponse(bucketInfo)) } // GrantBucketPermission grants permissions for an access key on a bucket // // @Summary Grant bucket permissions // @Description Grants read/write/owner permissions for an access key on a specific bucket // @Tags Buckets // @Accept json // @Produce json // @Param name path string true "Name of the bucket" // @Param request body models.GrantBucketPermissionRequest true "Permission grant request" // @Success 200 {object} models.APIResponse{data=models.GarageBucketInfo} "Permissions granted successfully" // @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request" // @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found" // @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to grant permissions" // @Router /api/v1/buckets/{name}/permissions [post] func (h *BucketHandler) GrantBucketPermission(c fiber.Ctx) error { ctx := c.Context() // Get bucket name from URL parameter bucketName := c.Params("name") if bucketName == "" { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Bucket name is required"), ) } // Parse request body var req models.GrantBucketPermissionRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Invalid request body: "+err.Error()), ) } // Validate access key ID if req.AccessKeyID == "" { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Access key ID is required"), ) } // Get bucket info to retrieve bucket ID bucketInfo, err := h.adminService.GetBucketInfoByAlias(ctx, bucketName) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeInternalError, "Failed to get bucket info: "+err.Error()), ) } if bucketInfo == nil { return c.Status(fiber.StatusNotFound).JSON( models.ErrorResponse(models.ErrCodeBucketNotFound, "Bucket does not exist"), ) } // Build the permission request for Garage Admin API permRequest := models.BucketKeyPermRequest{ BucketID: bucketInfo.ID, AccessKeyID: req.AccessKeyID, Permissions: models.BucketKeyPermission{ Read: req.Permissions.Read, Write: req.Permissions.Write, Owner: req.Permissions.Owner, }, } // Grant permissions using Garage Admin API result, err := h.adminService.AllowBucketKey(ctx, permRequest) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeInternalError, "Failed to grant permissions: "+err.Error()), ) } return c.JSON(models.SuccessResponse(result)) } // UpdateBucketWebsite updates the website access configuration for a bucket // // @Summary Update bucket website configuration // @Description Enables or disables static website hosting for a bucket // @Tags Buckets // @Accept json // @Produce json // @Param name path string true "Name of the bucket" // @Param request body models.UpdateBucketWebsiteRequest true "Website configuration" // @Success 200 {object} models.APIResponse{data=models.GarageBucketInfo} "Website configuration updated" // @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request" // @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found" // @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to update bucket" // @Router /api/v1/buckets/{name}/website [put] func (h *BucketHandler) UpdateBucketWebsite(c fiber.Ctx) error { ctx := c.Context() bucketName := c.Params("name") if bucketName == "" { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Bucket name is required"), ) } var req models.UpdateBucketWebsiteRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "Invalid request body: "+err.Error()), ) } if req.Enabled && req.IndexDocument == "" { return c.Status(fiber.StatusBadRequest).JSON( models.ErrorResponse(models.ErrCodeBadRequest, "indexDocument is required when enabling website access"), ) } bucketInfo, err := h.adminService.GetBucketInfoByAlias(ctx, bucketName) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeInternalError, "Failed to get bucket info: "+err.Error()), ) } if bucketInfo == nil { return c.Status(fiber.StatusNotFound).JSON( models.ErrorResponse(models.ErrCodeBucketNotFound, "Bucket does not exist"), ) } websiteAccess := &models.UpdateBucketWebsiteAccess{ Enabled: req.Enabled, } if req.Enabled { websiteAccess.IndexDocument = &req.IndexDocument if req.ErrorDocument != "" { websiteAccess.ErrorDocument = &req.ErrorDocument } } updateReq := models.UpdateBucketRequest{ WebsiteAccess: websiteAccess, } result, err := h.adminService.UpdateBucket(ctx, bucketInfo.ID, updateReq) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON( models.ErrorResponse(models.ErrCodeInternalError, "Failed to update bucket website: "+err.Error()), ) } return c.JSON(models.SuccessResponse(result)) }