diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index ccfcab6d6..79bce365b 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -4,9 +4,7 @@ import ( "context" "fmt" "net/http" - "os" "sort" - "strings" "time" "github.com/seaweedfs/seaweedfs/weed/cluster" @@ -83,6 +81,8 @@ type S3Bucket struct { ObjectCount int64 `json:"object_count"` LastModified time.Time `json:"last_modified"` Status string `json:"status"` + Quota int64 `json:"quota"` // Quota in bytes, 0 means no quota + QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled } type S3Object struct { @@ -499,6 +499,15 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { objectCount = collectionData.FileCount } + // Get quota information from entry + quota := resp.Entry.Quota + quotaEnabled := quota > 0 + if quota < 0 { + // Negative quota means disabled + quota = -quota + quotaEnabled = false + } + bucket := S3Bucket{ Name: bucketName, CreatedAt: time.Unix(resp.Entry.Attributes.Crtime, 0), @@ -506,6 +515,8 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { ObjectCount: objectCount, LastModified: time.Unix(resp.Entry.Attributes.Mtime, 0), Status: "active", + Quota: quota, + QuotaEnabled: quotaEnabled, } buckets = append(buckets, bucket) } @@ -620,59 +631,7 @@ func (s *AdminServer) listBucketObjects(client filer_pb.SeaweedFilerClient, dire // CreateS3Bucket creates a new S3 bucket func (s *AdminServer) CreateS3Bucket(bucketName string) error { - return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { - // First ensure /buckets directory exists - _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ - Directory: "/", - Entry: &filer_pb.Entry{ - Name: "buckets", - IsDirectory: true, - Attributes: &filer_pb.FuseAttributes{ - FileMode: uint32(0755 | os.ModeDir), // Directory mode - Uid: uint32(1000), - Gid: uint32(1000), - Crtime: time.Now().Unix(), - Mtime: time.Now().Unix(), - TtlSec: 0, - }, - }, - }) - // Ignore error if directory already exists - if err != nil && !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "existing entry") { - return fmt.Errorf("failed to create /buckets directory: %v", err) - } - - // Check if bucket already exists - _, err = client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ - Directory: "/buckets", - Name: bucketName, - }) - if err == nil { - return fmt.Errorf("bucket %s already exists", bucketName) - } - - // Create bucket directory under /buckets - _, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ - Directory: "/buckets", - Entry: &filer_pb.Entry{ - Name: bucketName, - IsDirectory: true, - Attributes: &filer_pb.FuseAttributes{ - FileMode: uint32(0755 | os.ModeDir), // Directory mode - Uid: uint32(1000), - Gid: uint32(1000), - Crtime: time.Now().Unix(), - Mtime: time.Now().Unix(), - TtlSec: 0, - }, - }, - }) - if err != nil { - return fmt.Errorf("failed to create bucket directory: %v", err) - } - - return nil - }) + return s.CreateS3BucketWithQuota(bucketName, 0, false) } // DeleteS3Bucket deletes an S3 bucket and all its contents diff --git a/weed/admin/dash/bucket_handlers.go b/weed/admin/dash/bucket_handlers.go new file mode 100644 index 000000000..e6edaa217 --- /dev/null +++ b/weed/admin/dash/bucket_handlers.go @@ -0,0 +1,325 @@ +package dash + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" +) + +// S3 Bucket management data structures for templates +type S3BucketsData struct { + Username string `json:"username"` + Buckets []S3Bucket `json:"buckets"` + TotalBuckets int `json:"total_buckets"` + TotalSize int64 `json:"total_size"` + LastUpdated time.Time `json:"last_updated"` +} + +type CreateBucketRequest struct { + Name string `json:"name" binding:"required"` + Region string `json:"region"` + QuotaSize int64 `json:"quota_size"` // Quota size in bytes + QuotaUnit string `json:"quota_unit"` // Unit: MB, GB, TB + QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled +} + +// S3 Bucket Management Handlers + +// ShowS3Buckets displays the Object Store buckets management page +func (s *AdminServer) ShowS3Buckets(c *gin.Context) { + username := c.GetString("username") + + buckets, err := s.GetS3Buckets() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Object Store buckets: " + err.Error()}) + return + } + + // Calculate totals + var totalSize int64 + for _, bucket := range buckets { + totalSize += bucket.Size + } + + data := S3BucketsData{ + Username: username, + Buckets: buckets, + TotalBuckets: len(buckets), + TotalSize: totalSize, + LastUpdated: time.Now(), + } + + c.JSON(http.StatusOK, data) +} + +// ShowBucketDetails displays detailed information about a specific bucket +func (s *AdminServer) ShowBucketDetails(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + details, err := s.GetBucketDetails(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, details) +} + +// CreateBucket creates a new S3 bucket +func (s *AdminServer) CreateBucket(c *gin.Context) { + var req CreateBucketRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Validate bucket name (basic validation) + if len(req.Name) < 3 || len(req.Name) > 63 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"}) + return + } + + // Convert quota to bytes + quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit) + + err := s.CreateS3BucketWithQuota(req.Name, quotaBytes, req.QuotaEnabled) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "Bucket created successfully", + "bucket": req.Name, + "quota_size": req.QuotaSize, + "quota_unit": req.QuotaUnit, + "quota_enabled": req.QuotaEnabled, + }) +} + +// UpdateBucketQuota updates the quota settings for a bucket +func (s *AdminServer) UpdateBucketQuota(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + var req struct { + QuotaSize int64 `json:"quota_size"` + QuotaUnit string `json:"quota_unit"` + QuotaEnabled bool `json:"quota_enabled"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Convert quota to bytes + quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit) + + err := s.SetBucketQuota(bucketName, quotaBytes, req.QuotaEnabled) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bucket quota: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Bucket quota updated successfully", + "bucket": bucketName, + "quota_size": req.QuotaSize, + "quota_unit": req.QuotaUnit, + "quota_enabled": req.QuotaEnabled, + }) +} + +// DeleteBucket deletes an S3 bucket +func (s *AdminServer) DeleteBucket(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + err := s.DeleteS3Bucket(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Bucket deleted successfully", + "bucket": bucketName, + }) +} + +// ListBucketsAPI returns the list of buckets as JSON +func (s *AdminServer) ListBucketsAPI(c *gin.Context) { + buckets, err := s.GetS3Buckets() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get buckets: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "buckets": buckets, + "total": len(buckets), + }) +} + +// Helper function to convert quota size and unit to bytes +func convertQuotaToBytes(size int64, unit string) int64 { + if size <= 0 { + return 0 + } + + switch strings.ToUpper(unit) { + case "TB": + return size * 1024 * 1024 * 1024 * 1024 + case "GB": + return size * 1024 * 1024 * 1024 + case "MB": + return size * 1024 * 1024 + default: + // Default to MB if unit is not recognized + return size * 1024 * 1024 + } +} + +// Helper function to convert bytes to appropriate unit and size +func convertBytesToQuota(bytes int64) (int64, string) { + if bytes == 0 { + return 0, "MB" + } + + // Convert to TB if >= 1TB + if bytes >= 1024*1024*1024*1024 && bytes%(1024*1024*1024*1024) == 0 { + return bytes / (1024 * 1024 * 1024 * 1024), "TB" + } + + // Convert to GB if >= 1GB + if bytes >= 1024*1024*1024 && bytes%(1024*1024*1024) == 0 { + return bytes / (1024 * 1024 * 1024), "GB" + } + + // Convert to MB (default) + return bytes / (1024 * 1024), "MB" +} + +// SetBucketQuota sets the quota for a bucket +func (s *AdminServer) SetBucketQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + // Get the current bucket entry + lookupResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: "/buckets", + Name: bucketName, + }) + if err != nil { + return fmt.Errorf("bucket not found: %v", err) + } + + bucketEntry := lookupResp.Entry + + // Determine quota value (negative if disabled) + var quota int64 + if quotaEnabled && quotaBytes > 0 { + quota = quotaBytes + } else if !quotaEnabled && quotaBytes > 0 { + quota = -quotaBytes + } else { + quota = 0 + } + + // Update the quota + bucketEntry.Quota = quota + + // Update the entry + _, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ + Directory: "/buckets", + Entry: bucketEntry, + }) + if err != nil { + return fmt.Errorf("failed to update bucket quota: %v", err) + } + + return nil + }) +} + +// CreateS3BucketWithQuota creates a new S3 bucket with quota settings +func (s *AdminServer) CreateS3BucketWithQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + // First ensure /buckets directory exists + _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ + Directory: "/", + Entry: &filer_pb.Entry{ + Name: "buckets", + IsDirectory: true, + Attributes: &filer_pb.FuseAttributes{ + FileMode: uint32(0755 | os.ModeDir), // Directory mode + Uid: uint32(1000), + Gid: uint32(1000), + Crtime: time.Now().Unix(), + Mtime: time.Now().Unix(), + TtlSec: 0, + }, + }, + }) + // Ignore error if directory already exists + if err != nil && !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "existing entry") { + return fmt.Errorf("failed to create /buckets directory: %v", err) + } + + // Check if bucket already exists + _, err = client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: "/buckets", + Name: bucketName, + }) + if err == nil { + return fmt.Errorf("bucket %s already exists", bucketName) + } + + // Determine quota value (negative if disabled) + var quota int64 + if quotaEnabled && quotaBytes > 0 { + quota = quotaBytes + } else if !quotaEnabled && quotaBytes > 0 { + quota = -quotaBytes + } else { + quota = 0 + } + + // Create bucket directory under /buckets + _, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ + Directory: "/buckets", + Entry: &filer_pb.Entry{ + Name: bucketName, + IsDirectory: true, + Attributes: &filer_pb.FuseAttributes{ + FileMode: uint32(0755 | os.ModeDir), // Directory mode + Uid: uint32(1000), + Gid: uint32(1000), + Crtime: time.Now().Unix(), + Mtime: time.Now().Unix(), + TtlSec: 0, + }, + Quota: quota, + }, + }) + if err != nil { + return fmt.Errorf("failed to create bucket directory: %v", err) + } + + return nil + }) +} diff --git a/weed/admin/dash/handler_admin.go b/weed/admin/dash/handler_admin.go index 72a59b2ff..a7a783aaf 100644 --- a/weed/admin/dash/handler_admin.go +++ b/weed/admin/dash/handler_admin.go @@ -25,20 +25,6 @@ type AdminData struct { SystemHealth string `json:"system_health"` } -// S3 Bucket management data structures for templates -type S3BucketsData struct { - Username string `json:"username"` - Buckets []S3Bucket `json:"buckets"` - TotalBuckets int `json:"total_buckets"` - TotalSize int64 `json:"total_size"` - LastUpdated time.Time `json:"last_updated"` -} - -type CreateBucketRequest struct { - Name string `json:"name" binding:"required"` - Region string `json:"region"` -} - // Object Store Users management structures type ObjectStoreUser struct { Username string `json:"username"` @@ -128,112 +114,6 @@ func (s *AdminServer) ShowOverview(c *gin.Context) { c.JSON(http.StatusOK, topology) } -// S3 Bucket Management Handlers - -// ShowS3Buckets displays the Object Store buckets management page -func (s *AdminServer) ShowS3Buckets(c *gin.Context) { - username := c.GetString("username") - - buckets, err := s.GetS3Buckets() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Object Store buckets: " + err.Error()}) - return - } - - // Calculate totals - var totalSize int64 - for _, bucket := range buckets { - totalSize += bucket.Size - } - - data := S3BucketsData{ - Username: username, - Buckets: buckets, - TotalBuckets: len(buckets), - TotalSize: totalSize, - LastUpdated: time.Now(), - } - - c.JSON(http.StatusOK, data) -} - -// ShowBucketDetails displays detailed information about a specific bucket -func (s *AdminServer) ShowBucketDetails(c *gin.Context) { - bucketName := c.Param("bucket") - if bucketName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) - return - } - - details, err := s.GetBucketDetails(bucketName) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, details) -} - -// CreateBucket creates a new S3 bucket -func (s *AdminServer) CreateBucket(c *gin.Context) { - var req CreateBucketRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) - return - } - - // Validate bucket name (basic validation) - if len(req.Name) < 3 || len(req.Name) > 63 { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"}) - return - } - - err := s.CreateS3Bucket(req.Name) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{ - "message": "Bucket created successfully", - "bucket": req.Name, - }) -} - -// DeleteBucket deletes an S3 bucket -func (s *AdminServer) DeleteBucket(c *gin.Context) { - bucketName := c.Param("bucket") - if bucketName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) - return - } - - err := s.DeleteS3Bucket(bucketName) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Bucket deleted successfully", - "bucket": bucketName, - }) -} - -// ListBucketsAPI returns buckets as JSON API -func (s *AdminServer) ListBucketsAPI(c *gin.Context) { - buckets, err := s.GetS3Buckets() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "buckets": buckets, - "count": len(buckets), - }) -} - // getMasterNodesStatus checks status of all master nodes func (s *AdminServer) getMasterNodesStatus() []MasterNode { var masterNodes []MasterNode diff --git a/weed/admin/handlers/handlers.go b/weed/admin/handlers/handlers.go index 69b06923f..d57fb0d14 100644 --- a/weed/admin/handlers/handlers.go +++ b/weed/admin/handlers/handlers.go @@ -80,6 +80,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, s3Api.POST("/buckets", h.adminServer.CreateBucket) s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) + s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) } // File management API routes @@ -126,6 +127,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, s3Api.POST("/buckets", h.adminServer.CreateBucket) s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) + s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) } // File management API routes diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js index 6aab0cd37..cefba4c9a 100644 --- a/weed/admin/static/js/admin.js +++ b/weed/admin/static/js/admin.js @@ -357,7 +357,48 @@ function initializeEventHandlers() { const bucketName = button.getAttribute('data-bucket-name'); confirmDeleteBucket(bucketName); } + + // Quota management buttons + if (e.target.closest('.quota-btn')) { + const button = e.target.closest('.quota-btn'); + const bucketName = button.getAttribute('data-bucket-name'); + const currentQuota = parseInt(button.getAttribute('data-current-quota')) || 0; + const quotaEnabled = button.getAttribute('data-quota-enabled') === 'true'; + showQuotaModal(bucketName, currentQuota, quotaEnabled); + } }); + + // Quota form submission + const quotaForm = document.getElementById('quotaForm'); + if (quotaForm) { + quotaForm.addEventListener('submit', handleUpdateQuota); + } + + // Enable quota checkbox for create bucket form + const enableQuotaCheckbox = document.getElementById('enableQuota'); + if (enableQuotaCheckbox) { + enableQuotaCheckbox.addEventListener('change', function() { + const quotaSettings = document.getElementById('quotaSettings'); + if (this.checked) { + quotaSettings.style.display = 'block'; + } else { + quotaSettings.style.display = 'none'; + } + }); + } + + // Enable quota checkbox for quota modal + const quotaEnabledCheckbox = document.getElementById('quotaEnabled'); + if (quotaEnabledCheckbox) { + quotaEnabledCheckbox.addEventListener('change', function() { + const quotaSizeSettings = document.getElementById('quotaSizeSettings'); + if (this.checked) { + quotaSizeSettings.style.display = 'block'; + } else { + quotaSizeSettings.style.display = 'none'; + } + }); + } } // Setup form validation @@ -379,7 +420,10 @@ async function handleCreateBucket(event) { const formData = new FormData(form); const bucketData = { name: formData.get('name'), - region: formData.get('region') || 'us-east-1' + region: formData.get('region') || 'us-east-1', + quota_enabled: formData.get('quota_enabled') === 'on', + quota_size: parseInt(formData.get('quota_size')) || 0, + quota_unit: formData.get('quota_unit') || 'MB' }; try { @@ -491,25 +535,27 @@ function exportBucketList() { const rows = Array.from(table.querySelectorAll('tbody tr')); const data = rows.map(row => { const cells = row.querySelectorAll('td'); - if (cells.length < 5) return null; // Skip empty state row + if (cells.length < 6) return null; // Skip empty state row return { name: cells[0].textContent.trim(), created: cells[1].textContent.trim(), objects: cells[2].textContent.trim(), size: cells[3].textContent.trim(), - status: cells[4].textContent.trim() + quota: cells[4].textContent.trim(), + status: cells[5].textContent.trim() }; }).filter(item => item !== null); // Convert to CSV const csv = [ - ['Name', 'Created', 'Objects', 'Size', 'Status'].join(','), + ['Name', 'Created', 'Objects', 'Size', 'Quota', 'Status'].join(','), ...data.map(row => [ row.name, row.created, row.objects, row.size, + row.quota, row.status ].join(',')) ].join('\n'); @@ -1573,4 +1619,97 @@ function getFileIconByName(fileName) { } } +// Quota Management Functions + +// Show quota management modal +function showQuotaModal(bucketName, currentQuotaMB, quotaEnabled) { + document.getElementById('quotaBucketName').value = bucketName; + document.getElementById('quotaEnabled').checked = quotaEnabled; + + // Convert quota to appropriate unit and set values + const quotaBytes = currentQuotaMB * 1024 * 1024; // Convert MB to bytes + const { size, unit } = convertBytesToBestUnit(quotaBytes); + + document.getElementById('quotaSizeMB').value = size; + document.getElementById('quotaUnitMB').value = unit; + + // Show/hide quota size settings based on enabled state + const quotaSizeSettings = document.getElementById('quotaSizeSettings'); + if (quotaEnabled) { + quotaSizeSettings.style.display = 'block'; + } else { + quotaSizeSettings.style.display = 'none'; + } + + const modal = new bootstrap.Modal(document.getElementById('manageQuotaModal')); + modal.show(); +} + +// Convert bytes to the best unit (TB, GB, or MB) +function convertBytesToBestUnit(bytes) { + if (bytes === 0) { + return { size: 0, unit: 'MB' }; + } + + // Check if it's a clean TB value + if (bytes >= 1024 * 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024 * 1024) === 0) { + return { size: bytes / (1024 * 1024 * 1024 * 1024), unit: 'TB' }; + } + + // Check if it's a clean GB value + if (bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) === 0) { + return { size: bytes / (1024 * 1024 * 1024), unit: 'GB' }; + } + + // Default to MB + return { size: bytes / (1024 * 1024), unit: 'MB' }; +} + +// Handle quota update form submission +async function handleUpdateQuota(event) { + event.preventDefault(); + + const form = event.target; + const formData = new FormData(form); + const bucketName = document.getElementById('quotaBucketName').value; + + const quotaData = { + quota_enabled: formData.get('quota_enabled') === 'on', + quota_size: parseInt(formData.get('quota_size')) || 0, + quota_unit: formData.get('quota_unit') || 'MB' + }; + + try { + const response = await fetch(`/api/s3/buckets/${bucketName}/quota`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(quotaData) + }); + + const result = await response.json(); + + if (response.ok) { + // Success + showAlert('success', `Quota for bucket "${bucketName}" updated successfully!`); + + // Close modal + const modal = bootstrap.Modal.getInstance(document.getElementById('manageQuotaModal')); + modal.hide(); + + // Refresh the page after a short delay + setTimeout(() => { + location.reload(); + }, 1500); + } else { + // Error + showAlert('danger', result.error || 'Failed to update bucket quota'); + } + } catch (error) { + console.error('Error updating bucket quota:', error); + showAlert('danger', 'Network error occurred while updating bucket quota'); + } +} + \ No newline at end of file diff --git a/weed/admin/view/app/s3_buckets.templ b/weed/admin/view/app/s3_buckets.templ index 620aab20b..0ea07068c 100644 --- a/weed/admin/view/app/s3_buckets.templ +++ b/weed/admin/view/app/s3_buckets.templ @@ -135,6 +135,7 @@ templ S3Buckets(data dash.S3BucketsData) {