Browse Source

Fix SeaweedFS S3 bucket extended attributes handling (#7854)

* refactor: Convert versioning to three-state string model matching AWS S3

- Change VersioningEnabled bool to VersioningStatus string in S3Bucket struct
- Add GetVersioningStatus() function returning empty string (never enabled), 'Enabled', or 'Suspended'
- Update StoreVersioningInExtended() to delete key instead of setting 'Suspended'
- Ensures Admin UI and S3 API use consistent versioning state representation

* fix: Add validation for bucket quota and Object Lock configuration

- Prevent buckets with quota enabled but size=0 (validation check)
- Fix Object Lock mode handling to only pass mode when setDefaultRetention is true
- Ensures proper extended attribute storage for Object Lock configuration
- Matches AWS S3 behavior for Object Lock setup

* feat: Handle versioned objects in bucket details view

- Recognize .versions directories as versioned objects in listBucketObjects()
- Extract size and mtime from extended attribute metadata (ExtLatestVersionSizeKey, ExtLatestVersionMtimeKey)
- Add length validation (8 bytes) before parsing extended attribute byte arrays
- Update GetBucketDetails() and GetS3Buckets() to use new GetVersioningStatus()
- Properly display versioned objects without .versions suffix in bucket details

* ui: Update bucket management UI to show three-state versioning and Object Lock

- Change versioning display from binary (Enabled/Disabled) to three-state (Not configured/Enabled/Suspended)
- Update Object Lock display to show 'Not configured' instead of 'Disabled'
- Fix bucket details modal to use bucket.versioning_status instead of bucket.versioning_enabled
- Update displayBucketDetails() JavaScript to handle three versioning states

* chore: Regenerate template code for bucket UI changes

- Generated from updated s3_buckets.templ
- Reflects three-state versioning and Object Lock UI improvements
pull/7835/merge
Chris Lu 15 hours ago
committed by GitHub
parent
commit
289ec5e2f5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 57
      weed/admin/dash/admin_server.go
  2. 11
      weed/admin/dash/bucket_management.go
  3. 2
      weed/admin/dash/types.go
  4. 26
      weed/admin/view/app/s3_buckets.templ
  5. 47
      weed/admin/view/app/s3_buckets_templ.go
  6. 17
      weed/s3api/object_lock_utils.go

57
weed/admin/dash/admin_server.go

@ -7,6 +7,7 @@ import (
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
@ -340,7 +341,7 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
}
// Get versioning, object lock, and owner information from extended attributes
versioningEnabled := false
versioningStatus := ""
objectLockEnabled := false
objectLockMode := ""
var objectLockDuration int32 = 0
@ -348,7 +349,7 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
if resp.Entry.Extended != nil {
// Use shared utility to extract versioning information
versioningEnabled = extractVersioningFromEntry(resp.Entry)
versioningStatus = extractVersioningFromEntry(resp.Entry)
// Use shared utility to extract Object Lock information
objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(resp.Entry)
@ -367,7 +368,7 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
LastModified: time.Unix(resp.Entry.Attributes.Mtime, 0),
Quota: quota,
QuotaEnabled: quotaEnabled,
VersioningEnabled: versioningEnabled,
VersioningStatus: versioningStatus,
ObjectLockEnabled: objectLockEnabled,
ObjectLockMode: objectLockMode,
ObjectLockDuration: objectLockDuration,
@ -430,7 +431,7 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
details.Bucket.QuotaEnabled = quotaEnabled
// Get versioning, object lock, and owner information from extended attributes
versioningEnabled := false
versioningStatus := ""
objectLockEnabled := false
objectLockMode := ""
var objectLockDuration int32 = 0
@ -438,7 +439,7 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
if bucketResp.Entry.Extended != nil {
// Use shared utility to extract versioning information
versioningEnabled = extractVersioningFromEntry(bucketResp.Entry)
versioningStatus = extractVersioningFromEntry(bucketResp.Entry)
// Use shared utility to extract Object Lock information
objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(bucketResp.Entry)
@ -449,7 +450,7 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
}
}
details.Bucket.VersioningEnabled = versioningEnabled
details.Bucket.VersioningStatus = versioningStatus
details.Bucket.ObjectLockEnabled = objectLockEnabled
details.Bucket.ObjectLockMode = objectLockMode
details.Bucket.ObjectLockDuration = objectLockDuration
@ -491,6 +492,45 @@ func (s *AdminServer) listBucketObjects(client filer_pb.SeaweedFilerClient, buck
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)
@ -1902,9 +1942,8 @@ func extractObjectLockInfoFromEntry(entry *filer_pb.Entry) (bool, string, int32)
}
// Function to extract versioning information from bucket entry using shared utilities
func extractVersioningFromEntry(entry *filer_pb.Entry) bool {
enabled, _ := s3api.LoadVersioningFromExtended(entry)
return enabled
func extractVersioningFromEntry(entry *filer_pb.Entry) string {
return s3api.GetVersioningStatus(entry)
}
// GetConfigPersistence returns the config persistence manager

11
weed/admin/dash/bucket_management.go

@ -125,6 +125,12 @@ func (s *AdminServer) CreateBucket(c *gin.Context) {
// Convert quota to bytes
quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit)
// Validate quota: if enabled, size must be greater than 0
if req.QuotaEnabled && quotaBytes <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Quota size must be greater than 0 when quota is enabled"})
return
}
// Sanitize owner: trim whitespace and enforce max length
owner := strings.TrimSpace(req.Owner)
if len(owner) > MaxOwnerNameLength {
@ -466,16 +472,19 @@ func (s *AdminServer) CreateS3BucketWithObjectLock(bucketName string, quotaBytes
// Handle Object Lock configuration using shared utilities
if objectLockEnabled {
var duration int32 = 0
var mode string = ""
if setDefaultRetention {
// Validate Object Lock parameters only when setting default retention
if err := s3api.ValidateObjectLockParameters(objectLockEnabled, objectLockMode, objectLockDuration); err != nil {
return fmt.Errorf("invalid Object Lock parameters: %w", err)
}
duration = objectLockDuration
mode = objectLockMode
}
// Create Object Lock configuration using shared utility
objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, objectLockMode, duration)
objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, mode, duration)
// Store Object Lock configuration in extended attributes using shared utility
if err := s3api.StoreObjectLockConfigurationInExtended(bucketEntry, objectLockConfig); err != nil {

2
weed/admin/dash/types.go

@ -78,7 +78,7 @@ type S3Bucket struct {
LastModified time.Time `json:"last_modified"`
Quota int64 `json:"quota"` // Quota in bytes, 0 means no quota
QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled
VersioningEnabled bool `json:"versioning_enabled"` // Whether versioning is enabled
VersioningStatus string `json:"versioning_status"` // Versioning status: "" (never enabled), "Enabled", or "Suspended"
ObjectLockEnabled bool `json:"object_lock_enabled"` // Whether object lock is enabled
ObjectLockMode string `json:"object_lock_mode"` // Object lock mode: "GOVERNANCE" or "COMPLIANCE"
ObjectLockDuration int32 `json:"object_lock_duration"` // Default retention duration in days

26
weed/admin/view/app/s3_buckets.templ

@ -164,14 +164,16 @@ templ S3Buckets(data dash.S3BucketsData) {
}
</td>
<td>
if bucket.VersioningEnabled {
if bucket.VersioningStatus == "Enabled" {
<span class="badge bg-success">
<i class="fas fa-check me-1"></i>Enabled
</span>
} else {
<span class="badge bg-secondary">
<i class="fas fa-times me-1"></i>Disabled
} else if bucket.VersioningStatus == "Suspended" {
<span class="badge bg-warning">
<i class="fas fa-pause me-1"></i>Suspended
</span>
} else {
<span class="text-muted">Not configured</span>
}
</td>
<td>
@ -185,9 +187,7 @@ templ S3Buckets(data dash.S3BucketsData) {
</div>
</div>
} else {
<span class="badge bg-secondary">
<i class="fas fa-unlock me-1"></i>Disabled
</span>
<span class="text-muted">Not configured</span>
}
</td>
<td>
@ -1044,9 +1044,11 @@ templ S3Buckets(data dash.S3BucketsData) {
'<tr>' +
'<td><strong>Versioning:</strong></td>' +
'<td>' +
(bucket.versioning_enabled ?
(bucket.versioning_status === 'Enabled' ?
'<span class="badge bg-success"><i class="fas fa-check me-1"></i>Enabled</span>' :
'<span class="badge bg-secondary"><i class="fas fa-times me-1"></i>Disabled</span>'
bucket.versioning_status === 'Suspended' ?
'<span class="badge bg-warning"><i class="fas fa-pause me-1"></i>Suspended</span>' :
'<span class="text-muted">Not configured</span>'
) +
'</td>' +
'</tr>' +
@ -1055,8 +1057,10 @@ templ S3Buckets(data dash.S3BucketsData) {
'<td>' +
(bucket.object_lock_enabled ?
'<span class="badge bg-warning"><i class="fas fa-lock me-1"></i>Enabled</span>' +
'<br><small class="text-muted">' + escapeHtml(bucket.object_lock_mode) + ' • ' + bucket.object_lock_duration + ' days</small>' :
'<span class="badge bg-secondary"><i class="fas fa-unlock me-1"></i>Disabled</span>'
(bucket.object_lock_mode && bucket.object_lock_duration > 0 ?
'<br><small class="text-muted">' + escapeHtml(bucket.object_lock_mode) + ' • ' + bucket.object_lock_duration + ' days</small>' :
'') :
'<span class="text-muted">Not configured</span>'
) +
'</td>' +
'</tr>' +

47
weed/admin/view/app/s3_buckets_templ.go
File diff suppressed because it is too large
View File

17
weed/s3api/object_lock_utils.go

@ -28,7 +28,8 @@ func StoreVersioningInExtended(entry *filer_pb.Entry, enabled bool) error {
if enabled {
entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningEnabled)
} else {
entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningSuspended)
// Don't set the header when versioning is not enabled
delete(entry.Extended, s3_constants.ExtVersioningKey)
}
return nil
@ -49,6 +50,20 @@ func LoadVersioningFromExtended(entry *filer_pb.Entry) (bool, bool) {
return false, false // not found
}
// GetVersioningStatus returns the versioning status as a string: "", "Enabled", or "Suspended"
// Empty string means versioning was never enabled
func GetVersioningStatus(entry *filer_pb.Entry) string {
if entry == nil || entry.Extended == nil {
return "" // Never enabled
}
if versioningBytes, exists := entry.Extended[s3_constants.ExtVersioningKey]; exists {
return string(versioningBytes) // "Enabled" or "Suspended"
}
return "" // Never enabled
}
// CreateObjectLockConfiguration creates a new ObjectLockConfiguration with the specified parameters
func CreateObjectLockConfiguration(enabled bool, mode string, days int, years int) *ObjectLockConfiguration {
if !enabled {

Loading…
Cancel
Save