mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-24 06:50:40 +02:00
* Fix: Support object tagging in versioned buckets (Issue #7868) This fix addresses the issue where setting tags on files in versioned buckets would fail with 'filer: no entry is found in filer store' error. Changes: - Updated GetObjectTaggingHandler to check versioning status and retrieve correct object versions - Updated PutObjectTaggingHandler to properly locate and update tags on versioned objects - Updated DeleteObjectTaggingHandler to delete tags from versioned objects - Added proper handling for both specific versions and latest versions - Added distinction between null versions (pre-versioning objects) and versioned objects The fix follows the same versioning-aware pattern already implemented in ACL handlers. Tests: - Added comprehensive test suite for tagging operations on versioned buckets - Tests cover PUT, GET, and DELETE tagging operations on specific versions and latest versions - Tests verify tag isolation between different versions of the same object * Fix: Ensure consistent directory path construction in tagging handlers Changed directory path construction to match the pattern used in ACL handlers: - Added missing '/' before object path when constructing .versions directory path - This ensures compatibility with the filer's expected path structure - Applied to both PutObjectTaggingHandler and DeleteObjectTaggingHandler * Revert: Remove redundant slash in path construction - object already has leading slash from NormalizeObjectKey * Fix: Remove redundant slashes in versioning path construction across handlers - getVersionedObjectDir: object already starts with '/', no need for extra '/' - ACL handlers: same pattern, fix both PutObjectAcl locations - Ensures consistent path construction with object parameter normalization * fix test compilation * Add: Comprehensive ACL tests for versioned and non-versioned buckets - Added s3_acl_versioning_test.go with 5 test cases covering: * GetObjectAcl on versioned buckets * GetObjectAcl on specific versions * PutObjectAcl on versioned buckets * PutObjectAcl on specific versions * Independent ACL management across versions These tests were missing and would have caught the path construction issues we just fixed in the ACL handler. Tests validate that ACL operations work correctly on both versioned and non-versioned objects. * Fix: Correct tagging versioning test file formatting * fix: Update AWS SDK endpoint config and improve cleanup to handle delete markers - Replace deprecated EndpointResolverWithOptions with BaseEndpoint in AWS SDK v2 client configuration - Update cleanupTestBucket to properly delete both object versions and delete markers - Apply changes to both ACL and tagging test files for consistency * Fix S3 multi-delete for versioned objects The bug was in getVersionedObjectDir() which was constructing paths without a slash between the bucket and object key: BEFORE (WRONG): /buckets/mybucket{key}.versions AFTER (FIXED): /buckets/mybucket/{key}/.versions This caused version deletions to claim success but not actually delete files, breaking S3 compatibility tests: - test_versioning_multi_object_delete - test_versioning_multi_object_delete_with_marker - test_versioning_concurrent_multi_object_delete - test_object_lock_multi_delete_object_with_retention Added comprehensive test that reproduces the issue and verifies the fix. * Remove emojis from test output
187 lines
6.1 KiB
Go
187 lines
6.1 KiB
Go
package s3api
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||
s3_constants "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||
)
|
||
|
||
// Version ID format constants
|
||
// New format uses inverted timestamps so newer versions sort first lexicographically
|
||
// Old format used raw timestamps where older versions sorted first
|
||
const (
|
||
// Threshold to distinguish old vs new format version IDs
|
||
// Around year 2024-2025:
|
||
// - Old format (raw ns): ~1.7×10¹⁸ ≈ 0x17... (BELOW threshold)
|
||
// - New format (MaxInt64 - ns): ~7.5×10¹⁸ ≈ 0x68... (ABOVE threshold)
|
||
// We use 0x4000000000000000 (~4.6×10¹⁸) as threshold
|
||
versionIdFormatThreshold = 0x4000000000000000
|
||
)
|
||
|
||
// generateVersionId creates a unique version ID
|
||
// If useInvertedFormat is true, uses inverted timestamps so newer versions sort first
|
||
// If false, uses raw timestamps (old format) for backward compatibility
|
||
func generateVersionId(useInvertedFormat bool) string {
|
||
now := time.Now().UnixNano()
|
||
var timestampHex string
|
||
|
||
if useInvertedFormat {
|
||
// INVERTED timestamp: newer versions have SMALLER values
|
||
// This makes lexicographic sorting return newest versions first
|
||
invertedTimestamp := math.MaxInt64 - now
|
||
timestampHex = fmt.Sprintf("%016x", invertedTimestamp)
|
||
} else {
|
||
// Raw timestamp: older versions have SMALLER values (old format)
|
||
timestampHex = fmt.Sprintf("%016x", now)
|
||
}
|
||
|
||
// Generate random 8 bytes for uniqueness (last 16 chars of version ID)
|
||
randBytes := make([]byte, 8)
|
||
if _, err := rand.Read(randBytes); err != nil {
|
||
glog.Errorf("Failed to generate random bytes for version ID: %v", err)
|
||
// Fallback to timestamp-only if random generation fails
|
||
return timestampHex + "0000000000000000"
|
||
}
|
||
|
||
// Combine timestamp (16 chars) + random (16 chars) = 32 chars total
|
||
randomHex := hex.EncodeToString(randBytes)
|
||
return timestampHex + randomHex
|
||
}
|
||
|
||
// isNewFormatVersionId returns true if the version ID uses the new inverted timestamp format
|
||
func isNewFormatVersionId(versionId string) bool {
|
||
if len(versionId) < 16 || versionId == "null" {
|
||
return false
|
||
}
|
||
// Parse the first 16 hex chars as the timestamp portion
|
||
timestampPart, err := strconv.ParseUint(versionId[:16], 16, 64)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
// New format has inverted timestamps (MaxInt64 - ns), which are ABOVE the threshold (~0x68...)
|
||
// Old format has raw timestamps, which are BELOW the threshold (~0x17...)
|
||
return timestampPart > versionIdFormatThreshold
|
||
}
|
||
|
||
// getVersionTimestamp extracts the actual timestamp from a version ID,
|
||
// handling both old (raw) and new (inverted) formats
|
||
func getVersionTimestamp(versionId string) int64 {
|
||
if len(versionId) < 16 || versionId == "null" {
|
||
return 0
|
||
}
|
||
timestampPart, err := strconv.ParseUint(versionId[:16], 16, 64)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
if timestampPart > versionIdFormatThreshold {
|
||
// New format: inverted timestamp (above threshold), convert back
|
||
return int64(math.MaxInt64 - timestampPart)
|
||
}
|
||
// Validate old format timestamp is within int64 range
|
||
if timestampPart > math.MaxInt64 {
|
||
return 0
|
||
}
|
||
// Old format: raw timestamp (below threshold)
|
||
return int64(timestampPart)
|
||
}
|
||
|
||
// compareVersionIds compares two version IDs for sorting (newest first)
|
||
// Returns: negative if a is newer, positive if b is newer, 0 if equal
|
||
// Handles both old and new format version IDs
|
||
func compareVersionIds(a, b string) int {
|
||
if a == b {
|
||
return 0
|
||
}
|
||
if a == "null" {
|
||
return 1 // null versions sort last
|
||
}
|
||
if b == "null" {
|
||
return -1
|
||
}
|
||
|
||
aIsNew := isNewFormatVersionId(a)
|
||
bIsNew := isNewFormatVersionId(b)
|
||
|
||
if aIsNew == bIsNew {
|
||
// Same format - compare lexicographically
|
||
// For new format: smaller value = newer (correct)
|
||
// For old format: smaller value = older (need to invert)
|
||
if aIsNew {
|
||
// New format: lexicographic order is correct (smaller = newer)
|
||
if a < b {
|
||
return -1
|
||
}
|
||
return 1
|
||
} else {
|
||
// Old format: lexicographic order is inverted (smaller = older)
|
||
if a < b {
|
||
return 1
|
||
}
|
||
return -1
|
||
}
|
||
}
|
||
|
||
// Mixed formats - compare by actual timestamp
|
||
aTime := getVersionTimestamp(a)
|
||
bTime := getVersionTimestamp(b)
|
||
if aTime > bTime {
|
||
return -1 // a is newer
|
||
}
|
||
if aTime < bTime {
|
||
return 1 // b is newer
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// getVersionedObjectDir returns the directory path for storing object versions
|
||
func (s3a *S3ApiServer) getVersionedObjectDir(bucket, object string) string {
|
||
return s3a.option.BucketsPath + "/" + bucket + "/" + object + s3_constants.VersionsFolder
|
||
}
|
||
|
||
// getVersionFileName returns the filename for a specific version
|
||
func (s3a *S3ApiServer) getVersionFileName(versionId string) string {
|
||
return fmt.Sprintf("v_%s", versionId)
|
||
}
|
||
|
||
// getVersionIdFormat checks the .versions directory to determine which version ID format to use.
|
||
// Returns true if inverted format (new format) should be used.
|
||
// For new .versions directories, returns true (use new format).
|
||
// For existing directories, infers format from the latest version ID.
|
||
func (s3a *S3ApiServer) getVersionIdFormat(bucket, object string) bool {
|
||
cleanObject := strings.TrimPrefix(object, "/")
|
||
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
||
versionsPath := cleanObject + s3_constants.VersionsFolder
|
||
|
||
// Try to get the .versions directory entry
|
||
versionsEntry, err := s3a.getEntry(bucketDir, versionsPath)
|
||
if err != nil {
|
||
// .versions directory doesn't exist yet - use new format
|
||
return true
|
||
}
|
||
|
||
// Infer format from the latest version ID stored in metadata
|
||
if versionsEntry.Extended != nil {
|
||
if latestVersionId, exists := versionsEntry.Extended[s3_constants.ExtLatestVersionIdKey]; exists {
|
||
return isNewFormatVersionId(string(latestVersionId))
|
||
}
|
||
}
|
||
|
||
// No latest version metadata - this is likely a new or empty directory
|
||
// Use new format
|
||
return true
|
||
}
|
||
|
||
// generateVersionIdForObject generates a version ID using the appropriate format for the object.
|
||
// For new objects, uses inverted format. For existing versioned objects, uses their existing format.
|
||
func (s3a *S3ApiServer) generateVersionIdForObject(bucket, object string) string {
|
||
useInvertedFormat := s3a.getVersionIdFormat(bucket, object)
|
||
return generateVersionId(useInvertedFormat)
|
||
}
|