diff options
| author | Chris Lu <chrislusf@users.noreply.github.com> | 2025-07-09 01:51:45 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-09 01:51:45 -0700 |
| commit | cf5a24983a0d6a5b6955f5cded4d5e1a4c6484ba (patch) | |
| tree | 3fb6c49d5a32e7a0518c268b984188e918c5e5ac /weed/s3api/s3api_object_handlers.go | |
| parent | 8fa1a69f8c915311326e75645681d10f66d9e222 (diff) | |
| download | seaweedfs-cf5a24983a0d6a5b6955f5cded4d5e1a4c6484ba.tar.xz seaweedfs-cf5a24983a0d6a5b6955f5cded4d5e1a4c6484ba.zip | |
S3: add object versioning (#6945)
* add object versioning
* add missing file
* Update weed/s3api/s3api_object_versioning.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update weed/s3api/s3api_object_versioning.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update weed/s3api/s3api_object_versioning.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* ListObjectVersionsResult is better to show multiple version entries
* fix test
* Update weed/s3api/s3api_object_handlers_put.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update weed/s3api/s3api_object_versioning.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* multiple improvements
* move PutBucketVersioningHandler into weed/s3api/s3api_bucket_handlers.go file
* duplicated code for reading bucket config, versioningEnabled, etc. try to use functions
* opportunity to cache bucket config
* error handling if bucket is not found
* in case bucket is not found
* fix build
* add object versioning tests
* remove non-existent tests
* add tests
* add versioning tests
* skip a new test
* ensure .versions directory exists before saving info into it
* fix creating version entry
* logging on creating version directory
* Update s3api_object_versioning_test.go
* retry and wait for directory creation
* revert add more logging
* Update s3api_object_versioning.go
* more debug messages
* clean up logs, and touch directory correctly
* log the .versions creation and then parent directory listing
* use mkFile instead of touch
touch is for update
* clean up data
* add versioning test in go
* change location
* if modified, latest version is moved to .versions directory, and create a new latest version
Core versioning functionality: WORKING
TestVersioningBasicWorkflow - PASS
TestVersioningDeleteMarkers - PASS
TestVersioningMultipleVersionsSameObject - PASS
TestVersioningDeleteAndRecreate - PASS
TestVersioningListWithPagination - PASS
❌ Some advanced features still failing:
ETag calculation issues (using mtime instead of proper MD5)
Specific version retrieval (EOF error)
Version deletion (internal errors)
Concurrent operations (race conditions)
* calculate multi chunk md5
Test Results - All Passing:
✅ TestBucketListReturnDataVersioning - PASS
✅ TestVersioningCreateObjectsInOrder - PASS
✅ TestVersioningBasicWorkflow - PASS
✅ TestVersioningMultipleVersionsSameObject - PASS
✅ TestVersioningDeleteMarkers - PASS
* dedupe
* fix TestVersioningErrorCases
* fix eof error of reading old versions
* get specific version also check current version
* enable integration tests for versioning
* trigger action to work for now
* Fix GitHub Actions S3 versioning tests workflow
- Fix syntax error (incorrect indentation)
- Update directory paths from weed/s3api/versioning_tests/ to test/s3/versioning/
- Add push trigger for add-object-versioning branch to enable CI during development
- Update artifact paths to match correct directory structure
* Improve CI robustness for S3 versioning tests
Makefile improvements:
- Increase server startup timeout from 30s to 90s for CI environments
- Add progressive timeout reporting (logs at 30s, full logs at 90s)
- Better error handling with server logs on failure
- Add server PID tracking for debugging
- Improved test failure reporting
GitHub Actions workflow improvements:
- Increase job timeouts to account for CI environment delays
- Add system information logging (memory, disk space)
- Add detailed failure reporting with server logs
- Add process and network diagnostics on failure
- Better error messaging and log collection
These changes should resolve the 'Server failed to start within 30 seconds' issue
that was causing the CI tests to fail.
* adjust testing volume size
* Update Makefile
* Update Makefile
* Update Makefile
* Update Makefile
* Update s3-versioning-tests.yml
* Update s3api_object_versioning.go
* Update Makefile
* do not clean up
* log received version id
* more logs
* printout response
* print out list version response
* use tmp files when put versioned object
* change to versions folder layout
* Delete weed-test.log
* test with mixed versioned and unversioned objects
* remove versionDirCache
* remove unused functions
* remove unused function
* remove fallback checking
* minor
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Diffstat (limited to 'weed/s3api/s3api_object_handlers.go')
| -rw-r--r-- | weed/s3api/s3api_object_handlers.go | 141 |
1 files changed, 137 insertions, 4 deletions
diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 8e5008219..5163a72c2 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -3,14 +3,15 @@ package s3api import ( "bytes" "fmt" - "github.com/seaweedfs/seaweedfs/weed/filer" - "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "io" "net/http" "net/url" "strings" "time" + "github.com/seaweedfs/seaweedfs/weed/filer" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" "github.com/seaweedfs/seaweedfs/weed/util/mem" @@ -120,7 +121,73 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) return } - destUrl := s3a.toFilerUrl(bucket, object) + // Check for specific version ID in query parameters + versionId := r.URL.Query().Get("versionId") + + // Check if versioning is enabled for the bucket + versioningEnabled, err := s3a.isVersioningEnabled(bucket) + if err != nil { + if err == filer_pb.ErrNotFound { + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) + return + } + glog.Errorf("Error checking versioning status for bucket %s: %v", bucket, err) + s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) + return + } + + var destUrl string + + if versioningEnabled { + // Handle versioned GET - all versions are stored in .versions directory + var targetVersionId string + var entry *filer_pb.Entry + + if versionId != "" { + // Request for specific version + glog.V(2).Infof("GetObject: requesting specific version %s for %s/%s", versionId, bucket, object) + entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId) + if err != nil { + glog.Errorf("Failed to get specific version %s: %v", versionId, err) + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) + return + } + targetVersionId = versionId + } else { + // Request for latest version + glog.V(2).Infof("GetObject: requesting latest version for %s/%s", bucket, object) + entry, err = s3a.getLatestObjectVersion(bucket, object) + if err != nil { + glog.Errorf("Failed to get latest version: %v", err) + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) + return + } + if entry.Extended != nil { + if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists { + targetVersionId = string(versionIdBytes) + } + } + } + + // Check if this is a delete marker + if entry.Extended != nil { + if deleteMarker, exists := entry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" { + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) + return + } + } + + // All versions are stored in .versions directory + versionObjectPath := object + ".versions/" + s3a.getVersionFileName(targetVersionId) + destUrl = s3a.toFilerUrl(bucket, versionObjectPath) + glog.V(2).Infof("GetObject: version %s URL: %s", targetVersionId, destUrl) + + // Set version ID in response header + w.Header().Set("x-amz-version-id", targetVersionId) + } else { + // Handle regular GET (non-versioned) + destUrl = s3a.toFilerUrl(bucket, object) + } s3a.proxyToFiler(w, r, destUrl, false, passThroughResponse) } @@ -130,7 +197,73 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request bucket, object := s3_constants.GetBucketAndObject(r) glog.V(3).Infof("HeadObjectHandler %s %s", bucket, object) - destUrl := s3a.toFilerUrl(bucket, object) + // Check for specific version ID in query parameters + versionId := r.URL.Query().Get("versionId") + + // Check if versioning is enabled for the bucket + versioningEnabled, err := s3a.isVersioningEnabled(bucket) + if err != nil { + if err == filer_pb.ErrNotFound { + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) + return + } + glog.Errorf("Error checking versioning status for bucket %s: %v", bucket, err) + s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) + return + } + + var destUrl string + + if versioningEnabled { + // Handle versioned HEAD - all versions are stored in .versions directory + var targetVersionId string + var entry *filer_pb.Entry + + if versionId != "" { + // Request for specific version + glog.V(2).Infof("HeadObject: requesting specific version %s for %s/%s", versionId, bucket, object) + entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId) + if err != nil { + glog.Errorf("Failed to get specific version %s: %v", versionId, err) + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) + return + } + targetVersionId = versionId + } else { + // Request for latest version + glog.V(2).Infof("HeadObject: requesting latest version for %s/%s", bucket, object) + entry, err = s3a.getLatestObjectVersion(bucket, object) + if err != nil { + glog.Errorf("Failed to get latest version: %v", err) + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) + return + } + if entry.Extended != nil { + if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists { + targetVersionId = string(versionIdBytes) + } + } + } + + // Check if this is a delete marker + if entry.Extended != nil { + if deleteMarker, exists := entry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" { + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) + return + } + } + + // All versions are stored in .versions directory + versionObjectPath := object + ".versions/" + s3a.getVersionFileName(targetVersionId) + destUrl = s3a.toFilerUrl(bucket, versionObjectPath) + glog.V(2).Infof("HeadObject: version %s URL: %s", targetVersionId, destUrl) + + // Set version ID in response header + w.Header().Set("x-amz-version-id", targetVersionId) + } else { + // Handle regular HEAD (non-versioned) + destUrl = s3a.toFilerUrl(bucket, object) + } s3a.proxyToFiler(w, r, destUrl, false, passThroughResponse) } |
