aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorchrislu <chris.lu@gmail.com>2025-07-01 19:59:45 -0700
committerchrislu <chris.lu@gmail.com>2025-07-01 19:59:45 -0700
commitae1d0a82cee1aac474b832c27556b76f55dd7adc (patch)
tree158d99bef96438c109d446e1f9e3bc87c5c29141
parent5c2b2e551376b3955c1bfb91d5b889c1ba506c65 (diff)
downloadseaweedfs-ae1d0a82cee1aac474b832c27556b76f55dd7adc.tar.xz
seaweedfs-ae1d0a82cee1aac474b832c27556b76f55dd7adc.zip
add bucket quota
-rw-r--r--weed/admin/dash/admin_server.go69
-rw-r--r--weed/admin/dash/bucket_handlers.go325
-rw-r--r--weed/admin/dash/handler_admin.go120
-rw-r--r--weed/admin/handlers/handlers.go2
-rw-r--r--weed/admin/static/js/admin.js147
-rw-r--r--weed/admin/view/app/s3_buckets.templ139
-rw-r--r--weed/admin/view/app/s3_buckets_templ.go209
7 files changed, 796 insertions, 215 deletions
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) {
<th>Created</th>
<th>Objects</th>
<th>Size</th>
+ <th>Quota</th>
<th>Status</th>
<th>Actions</th>
</tr>
@@ -153,6 +154,24 @@ templ S3Buckets(data dash.S3BucketsData) {
<td>{fmt.Sprintf("%d", bucket.ObjectCount)}</td>
<td>{formatBytes(bucket.Size)}</td>
<td>
+ if bucket.Quota > 0 {
+ <div>
+ <span class={fmt.Sprintf("badge bg-%s", getQuotaStatusColor(bucket.Size, bucket.Quota, bucket.QuotaEnabled))}>
+ {formatBytes(bucket.Quota)}
+ </span>
+ if bucket.QuotaEnabled {
+ <div class="small text-muted">
+ {fmt.Sprintf("%.1f%% used", float64(bucket.Size)/float64(bucket.Quota)*100)}
+ </div>
+ } else {
+ <div class="small text-muted">Disabled</div>
+ }
+ </div>
+ } else {
+ <span class="text-muted">No quota</span>
+ }
+ </td>
+ <td>
<span class={fmt.Sprintf("badge bg-%s", getBucketStatusColor(bucket.Status))}>
{bucket.Status}
</span>
@@ -170,6 +189,14 @@ templ S3Buckets(data dash.S3BucketsData) {
<i class="fas fa-eye"></i>
</a>
<button type="button"
+ class="btn btn-outline-warning btn-sm quota-btn"
+ data-bucket-name={bucket.Name}
+ data-current-quota={fmt.Sprintf("%d", getQuotaInMB(bucket.Quota))}
+ data-quota-enabled={fmt.Sprintf("%t", bucket.QuotaEnabled)}
+ title="Manage Quota">
+ <i class="fas fa-tachometer-alt"></i>
+ </button>
+ <button type="button"
class="btn btn-outline-danger btn-sm delete-bucket-btn"
data-bucket-name={bucket.Name}
title="Delete Bucket">
@@ -181,7 +208,7 @@ templ S3Buckets(data dash.S3BucketsData) {
}
if len(data.Buckets) == 0 {
<tr>
- <td colspan="6" class="text-center text-muted py-4">
+ <td colspan="7" class="text-center text-muted py-4">
<i class="fas fa-cube fa-3x mb-3 text-muted"></i>
<div>
<h5>No Object Store buckets found</h5>
@@ -236,6 +263,36 @@ templ S3Buckets(data dash.S3BucketsData) {
Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens.
</div>
</div>
+
+ <div class="mb-3">
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="enableQuota" name="quota_enabled">
+ <label class="form-check-label" for="enableQuota">
+ Enable Storage Quota
+ </label>
+ </div>
+ </div>
+
+ <div class="mb-3" id="quotaSettings" style="display: none;">
+ <div class="row">
+ <div class="col-md-8">
+ <label for="quotaSize" class="form-label">Quota Size</label>
+ <input type="number" class="form-control" id="quotaSize" name="quota_size"
+ placeholder="1024" min="1" step="1">
+ </div>
+ <div class="col-md-4">
+ <label for="quotaUnit" class="form-label">Unit</label>
+ <select class="form-select" id="quotaUnit" name="quota_unit">
+ <option value="MB" selected>MB</option>
+ <option value="GB">GB</option>
+ <option value="TB">TB</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-text">
+ Set the maximum storage size for this bucket.
+ </div>
+ </div>
</div>
<div class="modal-footer">
@@ -275,6 +332,64 @@ templ S3Buckets(data dash.S3BucketsData) {
</div>
</div>
</div>
+
+ <!-- Manage Quota Modal -->
+ <div class="modal fade" id="manageQuotaModal" tabindex="-1" aria-labelledby="manageQuotaModalLabel" aria-hidden="true">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title" id="manageQuotaModalLabel">
+ <i class="fas fa-tachometer-alt me-2"></i>Manage Bucket Quota
+ </h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <form id="quotaForm">
+ <div class="modal-body">
+ <div class="mb-3">
+ <label class="form-label">Bucket Name</label>
+ <input type="text" class="form-control" id="quotaBucketName" readonly>
+ </div>
+
+ <div class="mb-3">
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="quotaEnabled" name="quota_enabled">
+ <label class="form-check-label" for="quotaEnabled">
+ Enable Storage Quota
+ </label>
+ </div>
+ </div>
+
+ <div class="mb-3" id="quotaSizeSettings">
+ <div class="row">
+ <div class="col-md-8">
+ <label for="quotaSizeMB" class="form-label">Quota Size</label>
+ <input type="number" class="form-control" id="quotaSizeMB" name="quota_size"
+ placeholder="1024" min="0" step="1">
+ </div>
+ <div class="col-md-4">
+ <label for="quotaUnitMB" class="form-label">Unit</label>
+ <select class="form-select" id="quotaUnitMB" name="quota_unit">
+ <option value="MB" selected>MB</option>
+ <option value="GB">GB</option>
+ <option value="TB">TB</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-text">
+ Set the maximum storage size for this bucket. Set to 0 to remove quota.
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
+ <button type="submit" class="btn btn-warning">
+ <i class="fas fa-save me-1"></i>Update Quota
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
}
// Helper functions for template
@@ -299,4 +414,26 @@ func countActiveBuckets(buckets []dash.S3Bucket) int {
}
}
return count
+}
+
+func getQuotaStatusColor(used, quota int64, enabled bool) string {
+ if !enabled || quota <= 0 {
+ return "secondary"
+ }
+
+ percentage := float64(used) / float64(quota) * 100
+ if percentage >= 90 {
+ return "danger"
+ } else if percentage >= 75 {
+ return "warning"
+ } else {
+ return "success"
+ }
+}
+
+func getQuotaInMB(quotaBytes int64) int64 {
+ if quotaBytes < 0 {
+ quotaBytes = -quotaBytes // Handle disabled quotas (negative values)
+ }
+ return quotaBytes / (1024 * 1024)
} \ No newline at end of file
diff --git a/weed/admin/view/app/s3_buckets_templ.go b/weed/admin/view/app/s3_buckets_templ.go
index 252dde1c2..ce9b1ac82 100644
--- a/weed/admin/view/app/s3_buckets_templ.go
+++ b/weed/admin/view/app/s3_buckets_templ.go
@@ -86,7 +86,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div><div class=\"col-auto\"><i class=\"fas fa-clock fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Buckets Table --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-cube me-2\"></i>Object Store Buckets</h6><div class=\"dropdown no-arrow\"><a class=\"dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-ellipsis-v fa-sm fa-fw text-gray-400\"></i></a><div class=\"dropdown-menu dropdown-menu-right shadow animated--fade-in\"><div class=\"dropdown-header\">Actions:</div><a class=\"dropdown-item\" href=\"#\" onclick=\"exportBucketList()\"><i class=\"fas fa-download me-2\"></i>Export List</a></div></div></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\" id=\"bucketsTable\"><thead><tr><th>Name</th><th>Created</th><th>Objects</th><th>Size</th><th>Status</th><th>Actions</th></tr></thead> <tbody>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div><div class=\"col-auto\"><i class=\"fas fa-clock fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Buckets Table --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-cube me-2\"></i>Object Store Buckets</h6><div class=\"dropdown no-arrow\"><a class=\"dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-ellipsis-v fa-sm fa-fw text-gray-400\"></i></a><div class=\"dropdown-menu dropdown-menu-right shadow animated--fade-in\"><div class=\"dropdown-header\">Actions:</div><a class=\"dropdown-item\" href=\"#\" onclick=\"exportBucketList()\"><i class=\"fas fa-download me-2\"></i>Export List</a></div></div></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\" id=\"bucketsTable\"><thead><tr><th>Name</th><th>Created</th><th>Objects</th><th>Size</th><th>Quota</th><th>Status</th><th>Actions</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -107,7 +107,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 149, Col: 64}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 150, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -120,7 +120,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.CreatedAt.Format("2006-01-02 15:04"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 152, Col: 92}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 153, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -133,7 +133,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", bucket.ObjectCount))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 153, Col: 86}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 154, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -146,7 +146,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.Size))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 154, Col: 73}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 155, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -156,93 +156,210 @@ func S3Buckets(data dash.S3BucketsData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var11 = []any{fmt.Sprintf("badge bg-%s", getBucketStatusColor(bucket.Status))}
- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
+ if bucket.Quota > 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 = []any{fmt.Sprintf("badge bg-%s", getQuotaStatusColor(bucket.Size, bucket.Quota, bucket.QuotaEnabled))}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var12 string
+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 1, Col: 0}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var13 string
+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.Quota))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 160, Col: 86}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span> ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if bucket.QuotaEnabled {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"small text-muted\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var14 string
+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%% used", float64(bucket.Size)/float64(bucket.Quota)*100))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 164, Col: 139}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"small text-muted\">Disabled</div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"text-muted\">No quota</span>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><td>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var15 = []any{fmt.Sprintf("badge bg-%s", getBucketStatusColor(bucket.Status))}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var12 string
- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
+ var templ_7745c5c3_Var16 string
+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 1, Col: 0}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var13 string
- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Status)
+ var templ_7745c5c3_Var17 string
+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Status)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 157, Col: 66}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 176, Col: 66}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></td><td><div class=\"btn-group btn-group-sm\" role=\"group\"><a href=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span></td><td><div class=\"btn-group btn-group-sm\" role=\"group\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var14 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name))
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14)))
+ var templ_7745c5c3_Var18 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var18)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" class=\"btn btn-outline-success btn-sm\" title=\"Browse Files\"><i class=\"fas fa-folder-open\"></i></a> <a href=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"btn btn-outline-success btn-sm\" title=\"Browse Files\"><i class=\"fas fa-folder-open\"></i></a> <a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var15 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name))
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var15)))
+ var templ_7745c5c3_Var19 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var19)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"btn btn-outline-primary btn-sm\" title=\"View Details\"><i class=\"fas fa-eye\"></i></a> <button type=\"button\" class=\"btn btn-outline-danger btn-sm delete-bucket-btn\" data-bucket-name=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" class=\"btn btn-outline-primary btn-sm\" title=\"View Details\"><i class=\"fas fa-eye\"></i></a> <button type=\"button\" class=\"btn btn-outline-warning btn-sm quota-btn\" data-bucket-name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var16 string
- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name)
+ var templ_7745c5c3_Var20 string
+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 174, Col: 89}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 193, Col: 89}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" data-current-quota=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var21 string
+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", getQuotaInMB(bucket.Quota)))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 194, Col: 125}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" title=\"Delete Bucket\"><i class=\"fas fa-trash\"></i></button></div></td></tr>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" data-quota-enabled=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var22 string
+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%t", bucket.QuotaEnabled))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 195, Col: 118}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" title=\"Manage Quota\"><i class=\"fas fa-tachometer-alt\"></i></button> <button type=\"button\" class=\"btn btn-outline-danger btn-sm delete-bucket-btn\" data-bucket-name=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var23 string
+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 201, Col: 89}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" title=\"Delete Bucket\"><i class=\"fas fa-trash\"></i></button></div></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Buckets) == 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<tr><td colspan=\"6\" class=\"text-center text-muted py-4\"><i class=\"fas fa-cube fa-3x mb-3 text-muted\"></i><div><h5>No Object Store buckets found</h5><p>Create your first bucket to get started with S3 storage.</p><button type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createBucketModal\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></td></tr>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<tr><td colspan=\"7\" class=\"text-center text-muted py-4\"><i class=\"fas fa-cube fa-3x mb-3 text-muted\"></i><div><h5>No Object Store buckets found</h5><p>Create your first bucket to get started with S3 storage.</p><button type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createBucketModal\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</tbody></table></div></div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</tbody></table></div></div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var17 string
- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05"))
+ var templ_7745c5c3_Var24 string
+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 211, Col: 81}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 238, Col: 81}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</small></div></div></div><!-- Create Bucket Modal --><div class=\"modal fade\" id=\"createBucketModal\" tabindex=\"-1\" aria-labelledby=\"createBucketModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createBucketModalLabel\"><i class=\"fas fa-plus me-2\"></i>Create New S3 Bucket</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"createBucketForm\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"bucketName\" class=\"form-label\">Bucket Name</label> <input type=\"text\" class=\"form-control\" id=\"bucketName\" name=\"name\" placeholder=\"my-bucket-name\" required pattern=\"[a-z0-9.-]+\" title=\"Bucket name must contain only lowercase letters, numbers, dots, and hyphens\"><div class=\"form-text\">Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens.</div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></form></div></div></div><!-- Delete Confirmation Modal --><div class=\"modal fade\" id=\"deleteBucketModal\" tabindex=\"-1\" aria-labelledby=\"deleteBucketModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"deleteBucketModalLabel\"><i class=\"fas fa-exclamation-triangle me-2 text-warning\"></i>Delete Bucket</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><p>Are you sure you want to delete the bucket <strong id=\"deleteBucketName\"></strong>?</p><div class=\"alert alert-warning\"><i class=\"fas fa-exclamation-triangle me-2\"></i> <strong>Warning:</strong> This action cannot be undone. All objects in the bucket will be permanently deleted.</div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-danger\" onclick=\"deleteBucket()\"><i class=\"fas fa-trash me-1\"></i>Delete Bucket</button></div></div></div></div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</small></div></div></div><!-- Create Bucket Modal --><div class=\"modal fade\" id=\"createBucketModal\" tabindex=\"-1\" aria-labelledby=\"createBucketModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createBucketModalLabel\"><i class=\"fas fa-plus me-2\"></i>Create New S3 Bucket</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"createBucketForm\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"bucketName\" class=\"form-label\">Bucket Name</label> <input type=\"text\" class=\"form-control\" id=\"bucketName\" name=\"name\" placeholder=\"my-bucket-name\" required pattern=\"[a-z0-9.-]+\" title=\"Bucket name must contain only lowercase letters, numbers, dots, and hyphens\"><div class=\"form-text\">Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens.</div></div><div class=\"mb-3\"><div class=\"form-check\"><input class=\"form-check-input\" type=\"checkbox\" id=\"enableQuota\" name=\"quota_enabled\"> <label class=\"form-check-label\" for=\"enableQuota\">Enable Storage Quota</label></div></div><div class=\"mb-3\" id=\"quotaSettings\" style=\"display: none;\"><div class=\"row\"><div class=\"col-md-8\"><label for=\"quotaSize\" class=\"form-label\">Quota Size</label> <input type=\"number\" class=\"form-control\" id=\"quotaSize\" name=\"quota_size\" placeholder=\"1024\" min=\"1\" step=\"1\"></div><div class=\"col-md-4\"><label for=\"quotaUnit\" class=\"form-label\">Unit</label> <select class=\"form-select\" id=\"quotaUnit\" name=\"quota_unit\"><option value=\"MB\" selected>MB</option> <option value=\"GB\">GB</option> <option value=\"TB\">TB</option></select></div></div><div class=\"form-text\">Set the maximum storage size for this bucket.</div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></form></div></div></div><!-- Delete Confirmation Modal --><div class=\"modal fade\" id=\"deleteBucketModal\" tabindex=\"-1\" aria-labelledby=\"deleteBucketModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"deleteBucketModalLabel\"><i class=\"fas fa-exclamation-triangle me-2 text-warning\"></i>Delete Bucket</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><p>Are you sure you want to delete the bucket <strong id=\"deleteBucketName\"></strong>?</p><div class=\"alert alert-warning\"><i class=\"fas fa-exclamation-triangle me-2\"></i> <strong>Warning:</strong> This action cannot be undone. All objects in the bucket will be permanently deleted.</div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-danger\" onclick=\"deleteBucket()\"><i class=\"fas fa-trash me-1\"></i>Delete Bucket</button></div></div></div></div><!-- Manage Quota Modal --><div class=\"modal fade\" id=\"manageQuotaModal\" tabindex=\"-1\" aria-labelledby=\"manageQuotaModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"manageQuotaModalLabel\"><i class=\"fas fa-tachometer-alt me-2\"></i>Manage Bucket Quota</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"quotaForm\"><div class=\"modal-body\"><div class=\"mb-3\"><label class=\"form-label\">Bucket Name</label> <input type=\"text\" class=\"form-control\" id=\"quotaBucketName\" readonly></div><div class=\"mb-3\"><div class=\"form-check\"><input class=\"form-check-input\" type=\"checkbox\" id=\"quotaEnabled\" name=\"quota_enabled\"> <label class=\"form-check-label\" for=\"quotaEnabled\">Enable Storage Quota</label></div></div><div class=\"mb-3\" id=\"quotaSizeSettings\"><div class=\"row\"><div class=\"col-md-8\"><label for=\"quotaSizeMB\" class=\"form-label\">Quota Size</label> <input type=\"number\" class=\"form-control\" id=\"quotaSizeMB\" name=\"quota_size\" placeholder=\"1024\" min=\"0\" step=\"1\"></div><div class=\"col-md-4\"><label for=\"quotaUnitMB\" class=\"form-label\">Unit</label> <select class=\"form-select\" id=\"quotaUnitMB\" name=\"quota_unit\"><option value=\"MB\" selected>MB</option> <option value=\"GB\">GB</option> <option value=\"TB\">TB</option></select></div></div><div class=\"form-text\">Set the maximum storage size for this bucket. Set to 0 to remove quota.</div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"submit\" class=\"btn btn-warning\"><i class=\"fas fa-save me-1\"></i>Update Quota</button></div></form></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -274,4 +391,26 @@ func countActiveBuckets(buckets []dash.S3Bucket) int {
return count
}
+func getQuotaStatusColor(used, quota int64, enabled bool) string {
+ if !enabled || quota <= 0 {
+ return "secondary"
+ }
+
+ percentage := float64(used) / float64(quota) * 100
+ if percentage >= 90 {
+ return "danger"
+ } else if percentage >= 75 {
+ return "warning"
+ } else {
+ return "success"
+ }
+}
+
+func getQuotaInMB(quotaBytes int64) int64 {
+ if quotaBytes < 0 {
+ quotaBytes = -quotaBytes // Handle disabled quotas (negative values)
+ }
+ return quotaBytes / (1024 * 1024)
+}
+
var _ = templruntime.GeneratedTemplate