From 24556ebdccd64892232872bcea3e1cdf490d6f84 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 2 Jan 2026 18:28:00 -0800 Subject: [PATCH] Refine Bucket Size Metrics: Logical and Physical Size (#7943) * refactor: implement logical size calculation with replication factor using dedicated helper * ui: update bucket list to show logical/physical size --- weed/admin/dash/admin_server.go | 281 ++++++++++-------------- weed/admin/dash/bucket_management.go | 17 +- weed/admin/dash/cluster_topology.go | 6 +- weed/admin/dash/types.go | 10 +- weed/admin/handlers/admin_handlers.go | 19 +- weed/admin/view/app/s3_buckets.templ | 267 ++++++++++------------ weed/admin/view/app/s3_buckets_templ.go | 230 +++++++++++-------- 7 files changed, 378 insertions(+), 452 deletions(-) diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 34ce82329..5fe94a884 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -23,6 +23,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/pb/mq_pb" "github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" "github.com/seaweedfs/seaweedfs/weed/security" + "github.com/seaweedfs/seaweedfs/weed/storage/super_block" "github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/wdclient" "google.golang.org/grpc" @@ -96,6 +97,11 @@ type AdminServer struct { // Worker gRPC server workerGrpcServer *WorkerGrpcServer + + // Collection statistics caching + collectionStatsCache map[string]collectionStats + lastCollectionStatsUpdate time.Time + collectionStatsCacheThreshold time.Duration } // Type definitions moved to types.go @@ -119,13 +125,14 @@ func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string) go masterClient.KeepConnectedToMaster(ctx) server := &AdminServer{ - masterClient: masterClient, - templateFS: templateFS, - dataDir: dataDir, - grpcDialOption: grpcDialOption, - cacheExpiration: 10 * time.Second, - filerCacheExpiration: 30 * time.Second, // Cache filers for 30 seconds - configPersistence: NewConfigPersistence(dataDir), + masterClient: masterClient, + templateFS: templateFS, + dataDir: dataDir, + grpcDialOption: grpcDialOption, + cacheExpiration: 10 * time.Second, + filerCacheExpiration: 30 * time.Second, // Cache filers for 30 seconds + configPersistence: NewConfigPersistence(dataDir), + collectionStatsCacheThreshold: 30 * time.Second, } // Initialize topic retention purger @@ -236,57 +243,32 @@ func (s *AdminServer) GetCredentialManager() *credential.CredentialManager { // InvalidateCache method moved to cluster_topology.go -// GetS3Buckets retrieves all Object Store buckets from the filer and collects size/object data from collections -func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { - var buckets []S3Bucket +// GetS3BucketsData retrieves all Object Store buckets and aggregates total storage metrics +func (s *AdminServer) GetS3BucketsData() (S3BucketsData, error) { + buckets, err := s.GetS3Buckets() + if err != nil { + return S3BucketsData{}, err + } - // Build a map of collection name to collection data - collectionMap := make(map[string]struct { - Size int64 - FileCount int64 - }) + var totalSize int64 + for _, bucket := range buckets { + totalSize += bucket.PhysicalSize + } - // Collect volume information by collection - err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { - resp, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{}) - if err != nil { - return err - } + return S3BucketsData{ + Buckets: buckets, + TotalBuckets: len(buckets), + TotalSize: totalSize, + LastUpdated: time.Now(), + }, nil +} - if resp.TopologyInfo != nil { - for _, dc := range resp.TopologyInfo.DataCenterInfos { - for _, rack := range dc.RackInfos { - for _, node := range rack.DataNodeInfos { - for _, diskInfo := range node.DiskInfos { - for _, volInfo := range diskInfo.VolumeInfos { - collection := volInfo.Collection - if collection == "" { - collection = "default" - } - - if _, exists := collectionMap[collection]; !exists { - collectionMap[collection] = struct { - Size int64 - FileCount int64 - }{} - } - - data := collectionMap[collection] - data.Size += int64(volInfo.Size) - data.FileCount += int64(volInfo.FileCount) - collectionMap[collection] = data - } - } - } - } - } - } - return nil - }) +// GetS3Buckets retrieves all Object Store buckets from the filer and collects size/object data from collections +func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { + var buckets []S3Bucket - if err != nil { - return nil, fmt.Errorf("failed to get volume information: %w", err) - } + // Collect volume information by collection with caching + collectionMap, _ := s.getCollectionStats() // Get filer configuration (buckets path and filer group) filerConfig, err := s.getFilerConfig() @@ -324,10 +306,12 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { collectionName := getCollectionName(filerConfig.FilerGroup, bucketName) // Get size and object count from collection data - var size int64 + var physicalSize int64 + var logicalSize int64 var objectCount int64 if collectionData, exists := collectionMap[collectionName]; exists { - size = collectionData.Size + physicalSize = collectionData.PhysicalSize + logicalSize = collectionData.LogicalSize objectCount = collectionData.FileCount } @@ -363,7 +347,8 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { bucket := S3Bucket{ Name: bucketName, CreatedAt: time.Unix(resp.Entry.Attributes.Crtime, 0), - Size: size, + LogicalSize: logicalSize, + PhysicalSize: physicalSize, ObjectCount: objectCount, LastModified: time.Unix(resp.Entry.Attributes.Mtime, 0), Quota: quota, @@ -389,6 +374,7 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { } // GetBucketDetails retrieves detailed information about a specific bucket +// Note: This no longer lists objects for performance reasons. Use GetS3Buckets for size/count data. func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error) { // Get filer configuration (buckets path) filerConfig, err := s.getFilerConfig() @@ -396,16 +382,25 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error glog.Warningf("Failed to get filer configuration, using defaults: %v", err) } - bucketPath := fmt.Sprintf("%s/%s", filerConfig.BucketsPath, bucketName) - details := &BucketDetails{ Bucket: S3Bucket{ Name: bucketName, }, - Objects: []S3Object{}, UpdatedAt: time.Now(), } + // Get collection data for size and object count with caching + collectionName := getCollectionName(filerConfig.FilerGroup, bucketName) + stats, err := s.getCollectionStats() + if err != nil { + glog.Warningf("Failed to get collection data: %v", err) + // Continue without collection data - use zero values + } else if data, ok := stats[collectionName]; ok { + details.Bucket.LogicalSize = data.LogicalSize + details.Bucket.PhysicalSize = data.PhysicalSize + details.Bucket.ObjectCount = data.FileCount + } + err = s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { // Get bucket info bucketResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ @@ -456,8 +451,7 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error details.Bucket.ObjectLockDuration = objectLockDuration details.Bucket.Owner = owner - // List objects in bucket (recursively) - return s.listBucketObjects(client, bucketPath, bucketPath, "", details) + return nil }) if err != nil { @@ -467,106 +461,6 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error return details, nil } -// listBucketObjects recursively lists all objects in a bucket -// bucketBasePath is the full path to the bucket (e.g., /buckets/mybucket) -func (s *AdminServer) listBucketObjects(client filer_pb.SeaweedFilerClient, bucketBasePath, directory, prefix string, details *BucketDetails) error { - stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ - Directory: directory, - Prefix: prefix, - StartFromFileName: "", - InclusiveStartFrom: false, - Limit: 1000, - }) - if err != nil { - return err - } - - for { - resp, err := stream.Recv() - if err != nil { - if err.Error() == "EOF" { - break - } - return err - } - - entry := resp.Entry - if entry.IsDirectory { - // Check if this is a .versions directory (represents a versioned object) - if strings.HasSuffix(entry.Name, ".versions") { - // This directory represents an object, add it as an object without the .versions suffix - objectName := strings.TrimSuffix(entry.Name, ".versions") - objectKey := objectName - if directory != bucketBasePath { - relativePath := directory[len(bucketBasePath)+1:] - objectKey = fmt.Sprintf("%s/%s", relativePath, objectName) - } - - // Extract latest version metadata from extended attributes - var size int64 = 0 - var mtime int64 = entry.Attributes.Mtime - if entry.Extended != nil { - // Get size of latest version - if sizeBytes, ok := entry.Extended[s3_constants.ExtLatestVersionSizeKey]; ok && len(sizeBytes) == 8 { - size = int64(util.BytesToUint64(sizeBytes)) - } - // Get mtime of latest version - if mtimeBytes, ok := entry.Extended[s3_constants.ExtLatestVersionMtimeKey]; ok && len(mtimeBytes) == 8 { - mtime = int64(util.BytesToUint64(mtimeBytes)) - } - } - - obj := S3Object{ - Key: objectKey, - Size: size, - LastModified: time.Unix(mtime, 0), - ETag: "", - StorageClass: "STANDARD", - } - - details.Objects = append(details.Objects, obj) - details.TotalCount++ - details.TotalSize += size - // Don't recurse into .versions directories - continue - } - - // Recursively list subdirectories - subDir := fmt.Sprintf("%s/%s", directory, entry.Name) - err := s.listBucketObjects(client, bucketBasePath, subDir, "", details) - if err != nil { - return err - } - } else { - // Add file object - objectKey := entry.Name - if directory != bucketBasePath { - // Remove bucket prefix to get relative path - relativePath := directory[len(bucketBasePath)+1:] - objectKey = fmt.Sprintf("%s/%s", relativePath, entry.Name) - } - - obj := S3Object{ - Key: objectKey, - Size: int64(entry.Attributes.FileSize), - LastModified: time.Unix(entry.Attributes.Mtime, 0), - ETag: "", // Could be calculated from chunks if needed - StorageClass: "STANDARD", - } - - details.Objects = append(details.Objects, obj) - details.TotalSize += obj.Size - details.TotalCount++ - } - } - - // Update bucket totals - details.Bucket.Size = details.TotalSize - details.Bucket.ObjectCount = details.TotalCount - - return nil -} - // CreateS3Bucket creates a new S3 bucket func (s *AdminServer) CreateS3Bucket(bucketName string) error { return s.CreateS3BucketWithQuota(bucketName, 0, false) @@ -2108,3 +2002,66 @@ func getBoolFromMap(m map[string]interface{}, key string) bool { } return false } + +type collectionStats struct { + PhysicalSize int64 + LogicalSize int64 + FileCount int64 +} + +func collectCollectionStats(topologyInfo *master_pb.TopologyInfo) map[string]collectionStats { + collectionMap := make(map[string]collectionStats) + for _, dc := range topologyInfo.DataCenterInfos { + for _, rack := range dc.RackInfos { + for _, node := range rack.DataNodeInfos { + for _, diskInfo := range node.DiskInfos { + for _, volInfo := range diskInfo.VolumeInfos { + collection := volInfo.Collection + if collection == "" { + collection = "default" + } + + data := collectionMap[collection] + data.PhysicalSize += int64(volInfo.Size) + rp, _ := super_block.NewReplicaPlacementFromByte(byte(volInfo.ReplicaPlacement)) + // NewReplicaPlacementFromByte never returns a nil rp. If there's an error, + // it returns a zero-valued ReplicaPlacement, for which GetCopyCount() is 1. + // This provides a safe fallback, so we can ignore the error. + replicaCount := int64(rp.GetCopyCount()) + if volInfo.Size >= volInfo.DeletedByteCount { + data.LogicalSize += int64(volInfo.Size-volInfo.DeletedByteCount) / replicaCount + } + if volInfo.FileCount >= volInfo.DeleteCount { + data.FileCount += int64(volInfo.FileCount-volInfo.DeleteCount) / replicaCount + } + collectionMap[collection] = data + } + } + } + } + } + return collectionMap +} + +// getCollectionStats returns current collection statistics with caching +func (s *AdminServer) getCollectionStats() (map[string]collectionStats, error) { + now := time.Now() + if s.collectionStatsCache != nil && now.Sub(s.lastCollectionStatsUpdate) < s.collectionStatsCacheThreshold { + return s.collectionStatsCache, nil + } + + err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { + resp, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{}) + if err != nil { + return err + } + + if resp.TopologyInfo != nil { + s.collectionStatsCache = collectCollectionStats(resp.TopologyInfo) + s.lastCollectionStatsUpdate = now + } + return nil + }) + + return s.collectionStatsCache, err +} diff --git a/weed/admin/dash/bucket_management.go b/weed/admin/dash/bucket_management.go index 7104aa8c6..6c8fc9205 100644 --- a/weed/admin/dash/bucket_management.go +++ b/weed/admin/dash/bucket_management.go @@ -48,26 +48,13 @@ type CreateBucketRequest struct { func (s *AdminServer) ShowS3Buckets(c *gin.Context) { username := c.GetString("username") - buckets, err := s.GetS3Buckets() + data, err := s.GetS3BucketsData() 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(), - } - + data.Username = username c.JSON(http.StatusOK, data) } diff --git a/weed/admin/dash/cluster_topology.go b/weed/admin/dash/cluster_topology.go index 8c25cc2ac..aca29cd4a 100644 --- a/weed/admin/dash/cluster_topology.go +++ b/weed/admin/dash/cluster_topology.go @@ -120,8 +120,10 @@ func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error { // InvalidateCache forces a refresh of cached data func (s *AdminServer) InvalidateCache() { - s.lastCacheUpdate = time.Time{} + s.lastCacheUpdate = time.Now().Add(-s.cacheExpiration) s.cachedTopology = nil - s.lastFilerUpdate = time.Time{} + s.lastFilerUpdate = time.Now().Add(-s.filerCacheExpiration) s.cachedFilers = nil + s.lastCollectionStatsUpdate = time.Now().Add(-s.collectionStatsCacheThreshold) + s.collectionStatsCache = nil } diff --git a/weed/admin/dash/types.go b/weed/admin/dash/types.go index 4881243a5..3943bc7a3 100644 --- a/weed/admin/dash/types.go +++ b/weed/admin/dash/types.go @@ -73,7 +73,8 @@ type VolumeServerEcInfo struct { type S3Bucket struct { Name string `json:"name"` CreatedAt time.Time `json:"created_at"` - Size int64 `json:"size"` + LogicalSize int64 `json:"logical_size"` // Actual data size (used space) + PhysicalSize int64 `json:"physical_size"` // Total allocated volume space ObjectCount int64 `json:"object_count"` LastModified time.Time `json:"last_modified"` Quota int64 `json:"quota"` // Quota in bytes, 0 means no quota @@ -94,11 +95,8 @@ type S3Object struct { } type BucketDetails struct { - Bucket S3Bucket `json:"bucket"` - Objects []S3Object `json:"objects"` - TotalSize int64 `json:"total_size"` - TotalCount int64 `json:"total_count"` - UpdatedAt time.Time `json:"updated_at"` + Bucket S3Bucket `json:"bucket"` + UpdatedAt time.Time `json:"updated_at"` } // ObjectStoreUser is defined in admin_data.go diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go index 4c493427f..6ed8781f9 100644 --- a/weed/admin/handlers/admin_handlers.go +++ b/weed/admin/handlers/admin_handlers.go @@ -409,8 +409,8 @@ func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData { username = "admin" } - // Get Object Store buckets - buckets, err := h.adminServer.GetS3Buckets() + // Get Object Store buckets data + data, err := h.adminServer.GetS3BucketsData() if err != nil { // Return empty data on error return dash.S3BucketsData{ @@ -422,19 +422,8 @@ func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData { } } - // Calculate totals - var totalSize int64 - for _, bucket := range buckets { - totalSize += bucket.Size - } - - return dash.S3BucketsData{ - Username: username, - Buckets: buckets, - TotalBuckets: len(buckets), - TotalSize: totalSize, - LastUpdated: time.Now(), - } + data.Username = username + return data } // getAdminData retrieves admin data from the server (now uses consolidated method) diff --git a/weed/admin/view/app/s3_buckets.templ b/weed/admin/view/app/s3_buckets.templ index 890697926..9f7c138e0 100644 --- a/weed/admin/view/app/s3_buckets.templ +++ b/weed/admin/view/app/s3_buckets.templ @@ -116,7 +116,8 @@ templ S3Buckets(data dash.S3BucketsData) { Owner Created Objects - Size + Logical Size + Physical Size Quota Versioning Object Lock @@ -127,7 +128,7 @@ templ S3Buckets(data dash.S3BucketsData) { for _, bucket := range data.Buckets { - {bucket.Name} @@ -144,16 +145,24 @@ templ S3Buckets(data dash.S3BucketsData) { {bucket.CreatedAt.Format("2006-01-02 15:04")} {fmt.Sprintf("%d", bucket.ObjectCount)} - {formatBytes(bucket.Size)} + +
{formatBytes(bucket.LogicalSize)}
+ if bucket.PhysicalSize > 0 && bucket.LogicalSize > 0 && bucket.PhysicalSize > bucket.LogicalSize { +
+ {fmt.Sprintf("%.1fx overhead", float64(bucket.PhysicalSize)/float64(bucket.LogicalSize))} +
+ } + + {formatBytes(bucket.PhysicalSize)} if bucket.Quota > 0 {
- + {formatBytes(bucket.Quota)} if bucket.QuotaEnabled {
- {fmt.Sprintf("%.1f%% used", float64(bucket.Size)/float64(bucket.Quota)*100)} + {fmt.Sprintf("%.1f%% used", float64(bucket.LogicalSize)/float64(bucket.Quota)*100)}
} else {
Disabled
@@ -192,7 +201,7 @@ templ S3Buckets(data dash.S3BucketsData) {
- @@ -230,7 +239,7 @@ templ S3Buckets(data dash.S3BucketsData) { } if len(data.Buckets) == 0 { - +
No Object Store buckets found
@@ -880,9 +889,9 @@ templ S3Buckets(data dash.S3BucketsData) { '
' + '
' + 'Loading...' + - '
' + + '<\\/div>' + '
Loading bucket details...
' + - '
'; + '<\\/div>'; detailsModalInstance.show(); @@ -895,7 +904,7 @@ templ S3Buckets(data dash.S3BucketsData) { '
' + '' + 'Error loading bucket details: ' + data.error + - '
'; + '<\\/div>'; } else { displayBucketDetails(data); } @@ -906,7 +915,7 @@ templ S3Buckets(data dash.S3BucketsData) { '
' + '' + 'Error loading bucket details: ' + error.message + - '
'; + '<\\/div>'; }); }); }); @@ -938,146 +947,91 @@ templ S3Buckets(data dash.S3BucketsData) { }); } - function displayBucketDetails(data) { - const bucket = data.bucket; - const objects = data.objects || []; +function displayBucketDetails(data) { + const bucket = data.bucket; - // Helper function to escape HTML to prevent XSS - function escapeHtml(v) { - return String(v ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - // Helper function to format bytes - function formatBytes(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - } - - // Helper function to format date - function formatDate(dateString) { - const date = new Date(dateString); - return date.toLocaleString(); - } - - // Generate objects table - let objectsTable = ''; - if (objects.length > 0) { - objectsTable = '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - objects.map(obj => - '' + - '' + - '' + - '' + - '' + - '' - ).join('') + - '' + - '
Object KeySizeLast ModifiedStorage Class
' + escapeHtml(obj.key) + '' + formatBytes(obj.size) + '' + formatDate(obj.last_modified) + '' + escapeHtml(obj.storage_class) + '
' + - '
'; - } else { - objectsTable = '
' + - '' + - '
No objects found in this bucket
' + - '
'; + function escapeHtml(v) { + return String(v ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function formatBytes(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleString(); + } + + let ownerHtml = 'No owner (admin-only)'; + if (bucket.owner) { + ownerHtml = '' + escapeHtml(bucket.owner) + ''; + } + + let usageHtml = ''; + if (bucket.physical_size > 0 && bucket.logical_size > 0 && bucket.physical_size > bucket.logical_size) { + const overhead = (bucket.physical_size / bucket.logical_size).toFixed(1); + usageHtml = '
' + overhead + 'x overhead<\/small>'; + } + + let quotaHtml = 'Disabled'; + if (bucket.quota_enabled) { + quotaHtml = '' + formatBytes(bucket.quota) + ''; + } + + let versioningHtml = 'Not configured'; + if (bucket.versioning_status === 'Enabled') { + versioningHtml = 'Enabled'; + } else if (bucket.versioning_status === 'Suspended') { + versioningHtml = 'Suspended'; + } + + let objectLockHtml = 'Not configured'; + if (bucket.object_lock_enabled) { + let details = ''; + if (bucket.object_lock_mode && bucket.object_lock_duration > 0) { + details = '
' + escapeHtml(bucket.object_lock_mode) + ' • ' + bucket.object_lock_duration + ' days<\/small>'; } - - const content = '
' + - '
' + - '
Bucket Information
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Name:' + escapeHtml(bucket.name) + '
Owner:' + (bucket.owner ? '' + escapeHtml(bucket.owner) + '' : 'No owner (admin-only)') + '
Created:' + formatDate(bucket.created_at) + '
Last Modified:' + formatDate(bucket.last_modified) + '
Total Size:' + formatBytes(bucket.size) + '
Object Count:' + bucket.object_count + '
' + - '
' + - '
' + - '
Configuration
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Quota:' + - (bucket.quota_enabled ? - '' + formatBytes(bucket.quota) + '' : - 'Disabled' - ) + - '
Versioning:' + - (bucket.versioning_status === 'Enabled' ? - 'Enabled' : - bucket.versioning_status === 'Suspended' ? - 'Suspended' : - 'Not configured' - ) + - '
Object Lock:' + - (bucket.object_lock_enabled ? - 'Enabled' + - (bucket.object_lock_mode && bucket.object_lock_duration > 0 ? - '
' + escapeHtml(bucket.object_lock_mode) + ' • ' + bucket.object_lock_duration + ' days' : - '') : - 'Not configured' - ) + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
Objects (' + objects.length + ')
' + - objectsTable + - '
' + - '
'; - - document.getElementById('bucketDetailsContent').innerHTML = content; + objectLockHtml = 'Enabled' + details; } + const rows = [ + '
', + '
', + '
Bucket Information
', + '', + '
Name:' + escapeHtml(bucket.name) + '<\/td><\/tr>', + '
Owner:' + ownerHtml + '<\/td><\/tr>', + '
Created:' + formatDate(bucket.created_at) + '<\/td><\/tr>', + '
Last Modified:' + formatDate(bucket.last_modified) + '<\/td><\/tr>', + '
Logical Size:' + formatBytes(bucket.logical_size) + '<\/td><\/tr>', + '
Physical Size:' + formatBytes(bucket.physical_size) + usageHtml + '<\/td><\/tr>', + '
Object Count:' + bucket.object_count + '<\/td><\/tr>', + '<\/table>', + '<\/div>', + '
', + '
Configuration
', + '', + '
Quota:' + quotaHtml + '<\/td><\/tr>', + '
Versioning:' + versioningHtml + '<\/td><\/tr>', + '
Object Lock:' + objectLockHtml + '<\/td><\/tr>', + '<\/table>', + '<\/div>', + '<\/div>' + ]; + + document.getElementById('bucketDetailsContent').innerHTML = rows.join(''); +} + function exportBucketList() { // RFC 4180 compliant CSV escaping: escape double quotes by doubling them function escapeCsvField(value) { @@ -1097,23 +1051,26 @@ templ S3Buckets(data dash.S3BucketsData) { owner: cells[1].textContent.trim(), created: cells[2].textContent.trim(), objects: cells[3].textContent.trim(), - size: cells[4].textContent.trim(), - quota: cells[5].textContent.trim(), - versioning: cells[6].textContent.trim(), - objectLock: cells[7].textContent.trim() + + logicalSize: cells[4].textContent.trim(), + physicalSize: cells[5].textContent.trim(), + quota: cells[6].textContent.trim(), + versioning: cells[7].textContent.trim(), + objectLock: cells[8].textContent.trim() }; } return null; }).filter(bucket => bucket !== null); const csvContent = "data:text/csv;charset=utf-8," + - "Name,Owner,Created,Objects,Size,Quota,Versioning,Object Lock\n" + + "Name,Owner,Logical Size,Physical Size,Object Count,Created,Quota,Versioning,Object Lock\n" + buckets.map(b => [ escapeCsvField(b.name), escapeCsvField(b.owner), - escapeCsvField(b.created), + escapeCsvField(b.logicalSize), + escapeCsvField(b.physicalSize), escapeCsvField(b.objects), - escapeCsvField(b.size), + escapeCsvField(b.created), escapeCsvField(b.quota), escapeCsvField(b.versioning), escapeCsvField(b.objectLock) @@ -1151,4 +1108,4 @@ func getQuotaInMB(quotaBytes int64) int64 { quotaBytes = -quotaBytes // Handle disabled quotas (negative values) } return quotaBytes / (1024 * 1024) -} \ No newline at end of file +} \ 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 3474a1a48..6b58b5580 100644 --- a/weed/admin/view/app/s3_buckets_templ.go +++ b/weed/admin/view/app/s3_buckets_templ.go @@ -73,7 +73,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, 4, "
Object Store Buckets
Actions:
Export List
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
Object Store Buckets
Actions:
Export List
NameOwnerCreatedObjectsSizeQuotaVersioningObject LockActions
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -85,7 +85,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { var templ_7745c5c3_Var5 templ.SafeURL templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 130, Col: 123} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 131, Col: 123} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -98,7 +98,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, 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: 133, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 134, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -116,7 +116,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Owner) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 139, Col: 101} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 140, Col: 101} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -139,7 +139,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: 145, Col: 92} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 146, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -152,293 +152,329 @@ 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: 146, Col: 86} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 147, Col: 86} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 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, 45, "\" title=\"Delete Bucket\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(data.Buckets) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
NameOwnerCreatedObjectsLogical SizePhysical SizeQuotaVersioningObject LockActions
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.Size)) + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.LogicalSize)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 147, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 149, Col: 85} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if bucket.PhysicalSize > 0 && bucket.LogicalSize > 0 && bucket.PhysicalSize > bucket.LogicalSize { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1fx overhead", float64(bucket.PhysicalSize)/float64(bucket.LogicalSize))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 152, Col: 144} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.PhysicalSize)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 156, Col: 81} + } + _, 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, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if bucket.Quota > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") 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...) + var templ_7745c5c3_Var13 = []any{fmt.Sprintf("badge bg-%s", getQuotaStatusColor(bucket.LogicalSize, bucket.Quota, bucket.QuotaEnabled))} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">") 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)) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, 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: 152, Col: 86} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 161, Col: 86} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if bucket.QuotaEnabled { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") 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)) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%% used", float64(bucket.LogicalSize)/float64(bucket.Quota)*100)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 156, Col: 139} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 165, Col: 146} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, 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, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Disabled
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
Disabled
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "No quota") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "No quota") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if bucket.VersioningStatus == "Enabled" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Enabled") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Enabled") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if bucket.VersioningStatus == "Suspended" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Suspended") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Suspended") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Not configured") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Not configured") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if bucket.ObjectLockEnabled { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
Enabled
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
Enabled
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ObjectLockMode) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ObjectLockMode) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 186, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 195, Col: 82} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + _, 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, 30, " • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d days", bucket.ObjectLockDuration)) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d days", bucket.ObjectLockDuration)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 186, Col: 138} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 195, Col: 138} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "Not configured") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Not configured") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
No Object Store buckets found

Create your first bucket to get started with S3 storage.

No Object Store buckets found

Create your first bucket to get started with S3 storage.

Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, 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: 260, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 269, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
Create New S3 Bucket
Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens.
The S3 identity that owns this bucket. Non-admin users can only access buckets they own.
Set the maximum storage size for this bucket.
Keep multiple versions of objects in this bucket.
Prevent objects from being deleted or overwritten for a specified period. Automatically enables versioning.
Governance allows override with special permissions, Compliance is immutable.
Apply default retention to all new objects in this bucket.
Default retention period for new objects (1-36500 days).
Delete Bucket

Are you sure you want to delete the bucket ?

Warning: This action cannot be undone. All objects in the bucket will be permanently deleted.
Manage Bucket Quota
Set the maximum storage size for this bucket. Set to 0 to remove quota.
Bucket Details
Loading...
Loading bucket details...
Manage Bucket Owner
Select the S3 identity that owns this bucket. Non-admin users can only access buckets they own.
Loading users...
Loading users...
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
Create New S3 Bucket
Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens.
The S3 identity that owns this bucket. Non-admin users can only access buckets they own.
Set the maximum storage size for this bucket.
Keep multiple versions of objects in this bucket.
Prevent objects from being deleted or overwritten for a specified period. Automatically enables versioning.
Governance allows override with special permissions, Compliance is immutable.
Apply default retention to all new objects in this bucket.
Default retention period for new objects (1-36500 days).
Delete Bucket

Are you sure you want to delete the bucket ?

Warning: This action cannot be undone. All objects in the bucket will be permanently deleted.
Manage Bucket Quota
Set the maximum storage size for this bucket. Set to 0 to remove quota.
Bucket Details
Loading...
Loading bucket details...
Manage Bucket Owner
Select the S3 identity that owns this bucket. Non-admin users can only access buckets they own.
Loading users...
Loading users...
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }