mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-09-19 00:49:23 +08:00
S3 API: conditional read and write (#7154)
* conditional put * more tests * check all conditions * address comments * conditional multipart complete * conditional reads Read Operations (GET, HEAD): If-None-Match / If-Modified-Since failures → 304 Not Modified ✅ If-Match / If-Unmodified-Since failures → 412 Precondition Failed ✅ Write Operations (PUT, CompleteMultipartUpload): All conditional failures → 412 Precondition Failed ✅ Copy Operations (CopyObject): Copy-source conditionals → 412 Precondition Failed (already implemented) ✅ * test actual code * Interface-Based Testing * cleanup * Testing Interface * Update s3api_object_handlers_put.go * refactor
This commit is contained in:
@@ -57,6 +57,12 @@ const (
|
|||||||
AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date"
|
AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date"
|
||||||
AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold"
|
AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold"
|
||||||
|
|
||||||
|
// S3 conditional headers
|
||||||
|
IfMatch = "If-Match"
|
||||||
|
IfNoneMatch = "If-None-Match"
|
||||||
|
IfModifiedSince = "If-Modified-Since"
|
||||||
|
IfUnmodifiedSince = "If-Unmodified-Since"
|
||||||
|
|
||||||
// S3 conditional copy headers
|
// S3 conditional copy headers
|
||||||
AmzCopySourceIfMatch = "X-Amz-Copy-Source-If-Match"
|
AmzCopySourceIfMatch = "X-Amz-Copy-Source-If-Match"
|
||||||
AmzCopySourceIfNoneMatch = "X-Amz-Copy-Source-If-None-Match"
|
AmzCopySourceIfNoneMatch = "X-Amz-Copy-Source-If-None-Match"
|
||||||
|
849
weed/s3api/s3api_conditional_headers_test.go
Normal file
849
weed/s3api/s3api_conditional_headers_test.go
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
package s3api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestConditionalHeadersWithExistingObjects tests conditional headers against existing objects
|
||||||
|
// This addresses the PR feedback about missing test coverage for object existence scenarios
|
||||||
|
func TestConditionalHeadersWithExistingObjects(t *testing.T) {
|
||||||
|
bucket := "test-bucket"
|
||||||
|
object := "/test-object"
|
||||||
|
|
||||||
|
// Mock object with known ETag and modification time
|
||||||
|
testObject := &filer_pb.Entry{
|
||||||
|
Name: "test-object",
|
||||||
|
Extended: map[string][]byte{
|
||||||
|
s3_constants.ExtETagKey: []byte("\"abc123\""),
|
||||||
|
},
|
||||||
|
Attributes: &filer_pb.FuseAttributes{
|
||||||
|
Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(), // June 15, 2024
|
||||||
|
FileSize: 1024, // Add file size
|
||||||
|
},
|
||||||
|
Chunks: []*filer_pb.FileChunk{
|
||||||
|
// Add a mock chunk to make calculateETagFromChunks work
|
||||||
|
{
|
||||||
|
FileId: "test-file-id",
|
||||||
|
Offset: 0,
|
||||||
|
Size: 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test If-None-Match with existing object
|
||||||
|
t.Run("IfNoneMatch_ObjectExists", func(t *testing.T) {
|
||||||
|
// Test case 1: If-None-Match=* when object exists (should fail)
|
||||||
|
t.Run("Asterisk_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object exists with If-None-Match=*, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: If-None-Match with matching ETag (should fail)
|
||||||
|
t.Run("MatchingETag_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "\"abc123\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when ETag matches, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 3: If-None-Match with non-matching ETag (should succeed)
|
||||||
|
t.Run("NonMatchingETag_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when ETag doesn't match, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 4: If-None-Match with multiple ETags, one matching (should fail)
|
||||||
|
t.Run("MultipleETags_OneMatches_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\", \"abc123\", \"def456\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when one ETag matches, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 5: If-None-Match with multiple ETags, none matching (should succeed)
|
||||||
|
t.Run("MultipleETags_NoneMatch_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\", \"def456\", \"ghi123\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when no ETags match, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Match with existing object
|
||||||
|
t.Run("IfMatch_ObjectExists", func(t *testing.T) {
|
||||||
|
// Test case 1: If-Match with matching ETag (should succeed)
|
||||||
|
t.Run("MatchingETag_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"abc123\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when ETag matches, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: If-Match with non-matching ETag (should fail)
|
||||||
|
t.Run("NonMatchingETag_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"xyz789\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when ETag doesn't match, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 3: If-Match with multiple ETags, one matching (should succeed)
|
||||||
|
t.Run("MultipleETags_OneMatches_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"xyz789\", \"abc123\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when one ETag matches, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 4: If-Match with wildcard * (should succeed if object exists)
|
||||||
|
t.Run("Wildcard_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when If-Match=* and object exists, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Modified-Since with existing object
|
||||||
|
t.Run("IfModifiedSince_ObjectExists", func(t *testing.T) {
|
||||||
|
// Test case 1: If-Modified-Since with date before object modification (should succeed)
|
||||||
|
t.Run("DateBefore_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
dateBeforeModification := time.Date(2024, 6, 14, 12, 0, 0, 0, time.UTC)
|
||||||
|
req.Header.Set(s3_constants.IfModifiedSince, dateBeforeModification.Format(time.RFC1123))
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object was modified after date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: If-Modified-Since with date after object modification (should fail)
|
||||||
|
t.Run("DateAfter_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
dateAfterModification := time.Date(2024, 6, 16, 12, 0, 0, 0, time.UTC)
|
||||||
|
req.Header.Set(s3_constants.IfModifiedSince, dateAfterModification.Format(time.RFC1123))
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object wasn't modified since date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 3: If-Modified-Since with exact modification date (should fail - not after)
|
||||||
|
t.Run("ExactDate_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
exactDate := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
|
||||||
|
req.Header.Set(s3_constants.IfModifiedSince, exactDate.Format(time.RFC1123))
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object modification time equals header date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Unmodified-Since with existing object
|
||||||
|
t.Run("IfUnmodifiedSince_ObjectExists", func(t *testing.T) {
|
||||||
|
// Test case 1: If-Unmodified-Since with date after object modification (should succeed)
|
||||||
|
t.Run("DateAfter_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
dateAfterModification := time.Date(2024, 6, 16, 12, 0, 0, 0, time.UTC)
|
||||||
|
req.Header.Set(s3_constants.IfUnmodifiedSince, dateAfterModification.Format(time.RFC1123))
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object wasn't modified after date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: If-Unmodified-Since with date before object modification (should fail)
|
||||||
|
t.Run("DateBefore_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(testObject)
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
dateBeforeModification := time.Date(2024, 6, 14, 12, 0, 0, 0, time.UTC)
|
||||||
|
req.Header.Set(s3_constants.IfUnmodifiedSince, dateBeforeModification.Format(time.RFC1123))
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object was modified after date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConditionalHeadersForReads tests conditional headers for read operations (GET, HEAD)
|
||||||
|
// This implements AWS S3 conditional reads behavior where different conditions return different status codes
|
||||||
|
// See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-reads.html
|
||||||
|
func TestConditionalHeadersForReads(t *testing.T) {
|
||||||
|
bucket := "test-bucket"
|
||||||
|
object := "/test-read-object"
|
||||||
|
|
||||||
|
// Mock existing object to test conditional headers against
|
||||||
|
existingObject := &filer_pb.Entry{
|
||||||
|
Name: "test-read-object",
|
||||||
|
Extended: map[string][]byte{
|
||||||
|
s3_constants.ExtETagKey: []byte("\"read123\""),
|
||||||
|
},
|
||||||
|
Attributes: &filer_pb.FuseAttributes{
|
||||||
|
Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(),
|
||||||
|
FileSize: 1024,
|
||||||
|
},
|
||||||
|
Chunks: []*filer_pb.FileChunk{
|
||||||
|
{
|
||||||
|
FileId: "read-file-id",
|
||||||
|
Offset: 0,
|
||||||
|
Size: 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test conditional reads with existing object
|
||||||
|
t.Run("ConditionalReads_ObjectExists", func(t *testing.T) {
|
||||||
|
// Test If-None-Match with existing object (should return 304 Not Modified)
|
||||||
|
t.Run("IfNoneMatch_ObjectExists_ShouldReturn304", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "\"read123\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNotModified {
|
||||||
|
t.Errorf("Expected ErrNotModified when If-None-Match matches, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-None-Match=* with existing object (should return 304 Not Modified)
|
||||||
|
t.Run("IfNoneMatchAsterisk_ObjectExists_ShouldReturn304", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNotModified {
|
||||||
|
t.Errorf("Expected ErrNotModified when If-None-Match=* with existing object, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-None-Match with non-matching ETag (should succeed)
|
||||||
|
t.Run("IfNoneMatch_NonMatchingETag_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "\"different-etag\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when If-None-Match doesn't match, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Match with matching ETag (should succeed)
|
||||||
|
t.Run("IfMatch_MatchingETag_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"read123\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when If-Match matches, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Match with non-matching ETag (should return 412 Precondition Failed)
|
||||||
|
t.Run("IfMatch_NonMatchingETag_ShouldReturn412", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"different-etag\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when If-Match doesn't match, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Match=* with existing object (should succeed)
|
||||||
|
t.Run("IfMatchAsterisk_ObjectExists_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when If-Match=* with existing object, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Modified-Since (object modified after date - should succeed)
|
||||||
|
t.Run("IfModifiedSince_ObjectModifiedAfter_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfModifiedSince, "Sat, 14 Jun 2024 12:00:00 GMT") // Before object mtime
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object modified after If-Modified-Since date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Modified-Since (object not modified since date - should return 304)
|
||||||
|
t.Run("IfModifiedSince_ObjectNotModified_ShouldReturn304", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfModifiedSince, "Sun, 16 Jun 2024 12:00:00 GMT") // After object mtime
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNotModified {
|
||||||
|
t.Errorf("Expected ErrNotModified when object not modified since If-Modified-Since date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Unmodified-Since (object not modified since date - should succeed)
|
||||||
|
t.Run("IfUnmodifiedSince_ObjectNotModified_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfUnmodifiedSince, "Sun, 16 Jun 2024 12:00:00 GMT") // After object mtime
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object not modified since If-Unmodified-Since date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Unmodified-Since (object modified since date - should return 412)
|
||||||
|
t.Run("IfUnmodifiedSince_ObjectModified_ShouldReturn412", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfUnmodifiedSince, "Fri, 14 Jun 2024 12:00:00 GMT") // Before object mtime
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object modified since If-Unmodified-Since date, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test conditional reads with non-existent object
|
||||||
|
t.Run("ConditionalReads_ObjectNotExists", func(t *testing.T) {
|
||||||
|
// Test If-None-Match with non-existent object (should succeed)
|
||||||
|
t.Run("IfNoneMatch_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "\"any-etag\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object doesn't exist with If-None-Match, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Match with non-existent object (should return 412)
|
||||||
|
t.Run("IfMatch_ObjectNotExists_ShouldReturn412", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"any-etag\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Modified-Since with non-existent object (should succeed)
|
||||||
|
t.Run("IfModifiedSince_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfModifiedSince, "Sat, 15 Jun 2024 12:00:00 GMT")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object doesn't exist with If-Modified-Since, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Unmodified-Since with non-existent object (should return 412)
|
||||||
|
t.Run("IfUnmodifiedSince_ObjectNotExists_ShouldReturn412", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object
|
||||||
|
|
||||||
|
req := createTestGetRequest(bucket, object)
|
||||||
|
req.Header.Set(s3_constants.IfUnmodifiedSince, "Sat, 15 Jun 2024 12:00:00 GMT")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Unmodified-Since, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a GET request for testing
|
||||||
|
func createTestGetRequest(bucket, object string) *http.Request {
|
||||||
|
return &http.Request{
|
||||||
|
Method: "GET",
|
||||||
|
Header: make(http.Header),
|
||||||
|
URL: &url.URL{
|
||||||
|
Path: fmt.Sprintf("/%s%s", bucket, object),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConditionalHeadersWithNonExistentObjects tests the original scenarios (object doesn't exist)
|
||||||
|
func TestConditionalHeadersWithNonExistentObjects(t *testing.T) {
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
if s3a == nil {
|
||||||
|
t.Skip("S3ApiServer not available for testing")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket := "test-bucket"
|
||||||
|
object := "/test-object"
|
||||||
|
|
||||||
|
// Test If-None-Match header when object doesn't exist
|
||||||
|
t.Run("IfNoneMatch_ObjectDoesNotExist", func(t *testing.T) {
|
||||||
|
// Test case 1: If-None-Match=* when object doesn't exist (should return ErrNone)
|
||||||
|
t.Run("Asterisk_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object exists
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object doesn't exist, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: If-None-Match with specific ETag when object doesn't exist
|
||||||
|
t.Run("SpecificETag_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object exists
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "\"some-etag\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object doesn't exist, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test If-Match header when object doesn't exist
|
||||||
|
t.Run("IfMatch_ObjectDoesNotExist", func(t *testing.T) {
|
||||||
|
// Test case 1: If-Match with specific ETag when object doesn't exist (should fail - critical bug fix)
|
||||||
|
t.Run("SpecificETag_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object exists
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"some-etag\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match header, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: If-Match with wildcard * when object doesn't exist (should fail)
|
||||||
|
t.Run("Wildcard_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object exists
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match=*, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test date format validation (works regardless of object existence)
|
||||||
|
t.Run("DateFormatValidation", func(t *testing.T) {
|
||||||
|
// Test case 1: Valid If-Modified-Since date format
|
||||||
|
t.Run("IfModifiedSince_ValidFormat", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object exists
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfModifiedSince, time.Now().Format(time.RFC1123))
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone with valid date format, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: Invalid If-Modified-Since date format
|
||||||
|
t.Run("IfModifiedSince_InvalidFormat", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object exists
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfModifiedSince, "invalid-date")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrInvalidRequest {
|
||||||
|
t.Errorf("Expected ErrInvalidRequest for invalid date format, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 3: Invalid If-Unmodified-Since date format
|
||||||
|
t.Run("IfUnmodifiedSince_InvalidFormat", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object exists
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
req.Header.Set(s3_constants.IfUnmodifiedSince, "invalid-date")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrInvalidRequest {
|
||||||
|
t.Errorf("Expected ErrInvalidRequest for invalid date format, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test no conditional headers
|
||||||
|
t.Run("NoConditionalHeaders", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No object exists
|
||||||
|
req := createTestPutRequest(bucket, object, "test content")
|
||||||
|
// Don't set any conditional headers
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when no conditional headers, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestETagMatching tests the etagMatches helper function
|
||||||
|
func TestETagMatching(t *testing.T) {
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
if s3a == nil {
|
||||||
|
t.Skip("S3ApiServer not available for testing")
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
headerValue string
|
||||||
|
objectETag string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ExactMatch",
|
||||||
|
headerValue: "\"abc123\"",
|
||||||
|
objectETag: "abc123",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExactMatchWithQuotes",
|
||||||
|
headerValue: "\"abc123\"",
|
||||||
|
objectETag: "\"abc123\"",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NoMatch",
|
||||||
|
headerValue: "\"abc123\"",
|
||||||
|
objectETag: "def456",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleETags_FirstMatch",
|
||||||
|
headerValue: "\"abc123\", \"def456\"",
|
||||||
|
objectETag: "abc123",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleETags_SecondMatch",
|
||||||
|
headerValue: "\"abc123\", \"def456\"",
|
||||||
|
objectETag: "def456",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleETags_NoMatch",
|
||||||
|
headerValue: "\"abc123\", \"def456\"",
|
||||||
|
objectETag: "ghi789",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithSpaces",
|
||||||
|
headerValue: " \"abc123\" , \"def456\" ",
|
||||||
|
objectETag: "def456",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := s3a.etagMatches(tc.headerValue, tc.objectETag)
|
||||||
|
if result != tc.expected {
|
||||||
|
t.Errorf("Expected %v, got %v for headerValue='%s', objectETag='%s'",
|
||||||
|
tc.expected, result, tc.headerValue, tc.objectETag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConditionalHeadersIntegration tests conditional headers with full integration
|
||||||
|
func TestConditionalHeadersIntegration(t *testing.T) {
|
||||||
|
// This would be a full integration test that requires a running SeaweedFS instance
|
||||||
|
t.Skip("Integration test - requires running SeaweedFS instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestPutRequest creates a test HTTP PUT request
|
||||||
|
func createTestPutRequest(bucket, object, content string) *http.Request {
|
||||||
|
req, _ := http.NewRequest("PUT", "/"+bucket+object, bytes.NewReader([]byte(content)))
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
|
||||||
|
// Set up mux vars to simulate the bucket and object extraction
|
||||||
|
// In real tests, this would be handled by the gorilla mux router
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewS3ApiServerForTest creates a minimal S3ApiServer for testing
|
||||||
|
// Note: This is a simplified version for unit testing conditional logic
|
||||||
|
func NewS3ApiServerForTest() *S3ApiServer {
|
||||||
|
// In a real test environment, this would set up a proper S3ApiServer
|
||||||
|
// with filer connection, etc. For unit testing conditional header logic,
|
||||||
|
// we create a minimal instance
|
||||||
|
return &S3ApiServer{
|
||||||
|
option: &S3ApiServerOption{
|
||||||
|
BucketsPath: "/buckets",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockEntryGetter implements the simplified EntryGetter interface for testing
|
||||||
|
// Only mocks the data access dependency - tests use production getObjectETag and etagMatches
|
||||||
|
type MockEntryGetter struct {
|
||||||
|
mockEntry *filer_pb.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement only the simplified EntryGetter interface
|
||||||
|
func (m *MockEntryGetter) getEntry(parentDirectoryPath, entryName string) (*filer_pb.Entry, error) {
|
||||||
|
if m.mockEntry != nil {
|
||||||
|
return m.mockEntry, nil
|
||||||
|
}
|
||||||
|
return nil, filer_pb.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMockEntryGetter creates a mock EntryGetter for testing
|
||||||
|
func createMockEntryGetter(mockEntry *filer_pb.Entry) *MockEntryGetter {
|
||||||
|
return &MockEntryGetter{
|
||||||
|
mockEntry: mockEntry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConditionalHeadersMultipartUpload tests conditional headers with multipart uploads
|
||||||
|
// This verifies AWS S3 compatibility where conditional headers only apply to CompleteMultipartUpload
|
||||||
|
func TestConditionalHeadersMultipartUpload(t *testing.T) {
|
||||||
|
bucket := "test-bucket"
|
||||||
|
object := "/test-multipart-object"
|
||||||
|
|
||||||
|
// Mock existing object to test conditional headers against
|
||||||
|
existingObject := &filer_pb.Entry{
|
||||||
|
Name: "test-multipart-object",
|
||||||
|
Extended: map[string][]byte{
|
||||||
|
s3_constants.ExtETagKey: []byte("\"existing123\""),
|
||||||
|
},
|
||||||
|
Attributes: &filer_pb.FuseAttributes{
|
||||||
|
Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(),
|
||||||
|
FileSize: 2048,
|
||||||
|
},
|
||||||
|
Chunks: []*filer_pb.FileChunk{
|
||||||
|
{
|
||||||
|
FileId: "existing-file-id",
|
||||||
|
Offset: 0,
|
||||||
|
Size: 2048,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test CompleteMultipartUpload with If-None-Match: * (should fail when object exists)
|
||||||
|
t.Run("CompleteMultipartUpload_IfNoneMatchAsterisk_ObjectExists_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
// Create a mock CompleteMultipartUpload request with If-None-Match: *
|
||||||
|
req := &http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
Header: make(http.Header),
|
||||||
|
URL: &url.URL{
|
||||||
|
RawQuery: "uploadId=test-upload-id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object exists with If-None-Match=*, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test CompleteMultipartUpload with If-None-Match: * (should succeed when object doesn't exist)
|
||||||
|
t.Run("CompleteMultipartUpload_IfNoneMatchAsterisk_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No existing object
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
Header: make(http.Header),
|
||||||
|
URL: &url.URL{
|
||||||
|
RawQuery: "uploadId=test-upload-id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Header.Set(s3_constants.IfNoneMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object doesn't exist with If-None-Match=*, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test CompleteMultipartUpload with If-Match (should succeed when ETag matches)
|
||||||
|
t.Run("CompleteMultipartUpload_IfMatch_ETagMatches_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
Header: make(http.Header),
|
||||||
|
URL: &url.URL{
|
||||||
|
RawQuery: "uploadId=test-upload-id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"existing123\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when ETag matches, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test CompleteMultipartUpload with If-Match (should fail when object doesn't exist)
|
||||||
|
t.Run("CompleteMultipartUpload_IfMatch_ObjectNotExists_ShouldFail", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(nil) // No existing object
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
Header: make(http.Header),
|
||||||
|
URL: &url.URL{
|
||||||
|
RawQuery: "uploadId=test-upload-id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "\"any-etag\"")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrPreconditionFailed {
|
||||||
|
t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test CompleteMultipartUpload with If-Match wildcard (should succeed when object exists)
|
||||||
|
t.Run("CompleteMultipartUpload_IfMatchWildcard_ObjectExists_ShouldSucceed", func(t *testing.T) {
|
||||||
|
getter := createMockEntryGetter(existingObject)
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
Header: make(http.Header),
|
||||||
|
URL: &url.URL{
|
||||||
|
RawQuery: "uploadId=test-upload-id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Header.Set(s3_constants.IfMatch, "*")
|
||||||
|
|
||||||
|
s3a := NewS3ApiServerForTest()
|
||||||
|
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
t.Errorf("Expected ErrNone when object exists with If-Match=*, got %v", errCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@@ -246,6 +246,13 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
return // Directory object request was handled
|
return // Directory object request was handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check conditional headers for read operations
|
||||||
|
if errCode := s3a.checkConditionalHeadersForReads(r, bucket, object); errCode != s3err.ErrNone {
|
||||||
|
glog.V(3).Infof("GetObjectHandler: Conditional header check failed for %s/%s with error %v", bucket, object, errCode)
|
||||||
|
s3err.WriteErrorResponse(w, r, errCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check for specific version ID in query parameters
|
// Check for specific version ID in query parameters
|
||||||
versionId := r.URL.Query().Get("versionId")
|
versionId := r.URL.Query().Get("versionId")
|
||||||
|
|
||||||
@@ -378,6 +385,13 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
|
|||||||
return // Directory object request was handled
|
return // Directory object request was handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check conditional headers for read operations
|
||||||
|
if errCode := s3a.checkConditionalHeadersForReads(r, bucket, object); errCode != s3err.ErrNone {
|
||||||
|
glog.V(3).Infof("HeadObjectHandler: Conditional header check failed for %s/%s with error %v", bucket, object, errCode)
|
||||||
|
s3err.WriteErrorResponse(w, r, errCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check for specific version ID in query parameters
|
// Check for specific version ID in query parameters
|
||||||
versionId := r.URL.Query().Get("versionId")
|
versionId := r.URL.Query().Get("versionId")
|
||||||
|
|
||||||
|
@@ -114,6 +114,14 @@ func (s3a *S3ApiServer) CompleteMultipartUploadHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check conditional headers before completing multipart upload
|
||||||
|
// This implements AWS S3 behavior where conditional headers apply to CompleteMultipartUpload
|
||||||
|
if errCode := s3a.checkConditionalHeaders(r, bucket, object); errCode != s3err.ErrNone {
|
||||||
|
glog.V(3).Infof("CompleteMultipartUploadHandler: Conditional header check failed for %s/%s", bucket, object)
|
||||||
|
s3err.WriteErrorResponse(w, r, errCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
response, errCode := s3a.completeMultipartUpload(r, &s3.CompleteMultipartUploadInput{
|
response, errCode := s3a.completeMultipartUpload(r, &s3.CompleteMultipartUploadInput{
|
||||||
Bucket: aws.String(bucket),
|
Bucket: aws.String(bucket),
|
||||||
Key: objectKey(aws.String(object)),
|
Key: objectKey(aws.String(object)),
|
||||||
|
@@ -72,6 +72,12 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check conditional headers
|
||||||
|
if errCode := s3a.checkConditionalHeaders(r, bucket, object); errCode != s3err.ErrNone {
|
||||||
|
s3err.WriteErrorResponse(w, r, errCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if r.Header.Get("Cache-Control") != "" {
|
if r.Header.Get("Cache-Control") != "" {
|
||||||
if _, err = cacheobject.ParseRequestCacheControl(r.Header.Get("Cache-Control")); err != nil {
|
if _, err = cacheobject.ParseRequestCacheControl(r.Header.Get("Cache-Control")); err != nil {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidDigest)
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidDigest)
|
||||||
@@ -1012,3 +1018,269 @@ func mapValidationErrorToS3Error(err error) s3err.ErrorCode {
|
|||||||
|
|
||||||
return s3err.ErrInvalidRequest
|
return s3err.ErrInvalidRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EntryGetter interface for dependency injection in tests
|
||||||
|
// Simplified to only mock the data access dependency
|
||||||
|
type EntryGetter interface {
|
||||||
|
getEntry(parentDirectoryPath, entryName string) (*filer_pb.Entry, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// conditionalHeaders holds parsed conditional header values
|
||||||
|
type conditionalHeaders struct {
|
||||||
|
ifMatch string
|
||||||
|
ifNoneMatch string
|
||||||
|
ifModifiedSince time.Time
|
||||||
|
ifUnmodifiedSince time.Time
|
||||||
|
isSet bool // true if any conditional headers are present
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseConditionalHeaders extracts and validates conditional headers from the request
|
||||||
|
func parseConditionalHeaders(r *http.Request) (conditionalHeaders, s3err.ErrorCode) {
|
||||||
|
headers := conditionalHeaders{
|
||||||
|
ifMatch: r.Header.Get(s3_constants.IfMatch),
|
||||||
|
ifNoneMatch: r.Header.Get(s3_constants.IfNoneMatch),
|
||||||
|
}
|
||||||
|
|
||||||
|
ifModifiedSinceStr := r.Header.Get(s3_constants.IfModifiedSince)
|
||||||
|
ifUnmodifiedSinceStr := r.Header.Get(s3_constants.IfUnmodifiedSince)
|
||||||
|
|
||||||
|
// Check if any conditional headers are present
|
||||||
|
headers.isSet = headers.ifMatch != "" || headers.ifNoneMatch != "" ||
|
||||||
|
ifModifiedSinceStr != "" || ifUnmodifiedSinceStr != ""
|
||||||
|
|
||||||
|
if !headers.isSet {
|
||||||
|
return headers, s3err.ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse date headers with validation
|
||||||
|
var err error
|
||||||
|
if ifModifiedSinceStr != "" {
|
||||||
|
headers.ifModifiedSince, err = time.Parse(time.RFC1123, ifModifiedSinceStr)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(3).Infof("parseConditionalHeaders: Invalid If-Modified-Since format: %v", err)
|
||||||
|
return headers, s3err.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ifUnmodifiedSinceStr != "" {
|
||||||
|
headers.ifUnmodifiedSince, err = time.Parse(time.RFC1123, ifUnmodifiedSinceStr)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(3).Infof("parseConditionalHeaders: Invalid If-Unmodified-Since format: %v", err)
|
||||||
|
return headers, s3err.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers, s3err.ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3ApiServer implements EntryGetter interface
|
||||||
|
func (s3a *S3ApiServer) getObjectETag(entry *filer_pb.Entry) string {
|
||||||
|
// Try to get ETag from Extended attributes first
|
||||||
|
if etagBytes, hasETag := entry.Extended[s3_constants.ExtETagKey]; hasETag {
|
||||||
|
return string(etagBytes)
|
||||||
|
}
|
||||||
|
// Fallback: calculate ETag from chunks
|
||||||
|
return s3a.calculateETagFromChunks(entry.Chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s3a *S3ApiServer) etagMatches(headerValue, objectETag string) bool {
|
||||||
|
// Clean the object ETag
|
||||||
|
objectETag = strings.Trim(objectETag, `"`)
|
||||||
|
|
||||||
|
// Split header value by commas to handle multiple ETags
|
||||||
|
etags := strings.Split(headerValue, ",")
|
||||||
|
for _, etag := range etags {
|
||||||
|
etag = strings.TrimSpace(etag)
|
||||||
|
etag = strings.Trim(etag, `"`)
|
||||||
|
if etag == objectETag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConditionalHeadersWithGetter is a testable method that accepts a simple EntryGetter
|
||||||
|
// Uses the production getObjectETag and etagMatches methods to ensure testing of real logic
|
||||||
|
func (s3a *S3ApiServer) checkConditionalHeadersWithGetter(getter EntryGetter, r *http.Request, bucket, object string) s3err.ErrorCode {
|
||||||
|
headers, errCode := parseConditionalHeaders(r)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: Invalid date format")
|
||||||
|
return errCode
|
||||||
|
}
|
||||||
|
if !headers.isSet {
|
||||||
|
return s3err.ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get object entry for conditional checks.
|
||||||
|
bucketDir := "/buckets/" + bucket
|
||||||
|
entry, entryErr := getter.getEntry(bucketDir, object)
|
||||||
|
objectExists := entryErr == nil
|
||||||
|
|
||||||
|
// For PUT requests, all specified conditions must be met.
|
||||||
|
// The evaluation order follows AWS S3 behavior for consistency.
|
||||||
|
|
||||||
|
// 1. Check If-Match
|
||||||
|
if headers.ifMatch != "" {
|
||||||
|
if !objectExists {
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-Match failed - object %s/%s does not exist", bucket, object)
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
// If `ifMatch` is "*", the condition is met if the object exists.
|
||||||
|
// Otherwise, we need to check the ETag.
|
||||||
|
if headers.ifMatch != "*" {
|
||||||
|
// Use production getObjectETag method
|
||||||
|
objectETag := s3a.getObjectETag(entry)
|
||||||
|
// Use production etagMatches method
|
||||||
|
if !s3a.etagMatches(headers.ifMatch, objectETag) {
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-Match failed for object %s/%s - expected ETag %s, got %s", bucket, object, headers.ifMatch, objectETag)
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-Match passed for object %s/%s", bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check If-Unmodified-Since
|
||||||
|
if !headers.ifUnmodifiedSince.IsZero() {
|
||||||
|
if objectExists {
|
||||||
|
objectModTime := time.Unix(entry.Attributes.Mtime, 0)
|
||||||
|
if objectModTime.After(headers.ifUnmodifiedSince) {
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-Unmodified-Since failed - object modified after %s", r.Header.Get(s3_constants.IfUnmodifiedSince))
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-Unmodified-Since passed - object not modified since %s", r.Header.Get(s3_constants.IfUnmodifiedSince))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check If-None-Match
|
||||||
|
if headers.ifNoneMatch != "" {
|
||||||
|
if objectExists {
|
||||||
|
if headers.ifNoneMatch == "*" {
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-None-Match=* failed - object %s/%s exists", bucket, object)
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
// Use production getObjectETag method
|
||||||
|
objectETag := s3a.getObjectETag(entry)
|
||||||
|
// Use production etagMatches method
|
||||||
|
if s3a.etagMatches(headers.ifNoneMatch, objectETag) {
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-None-Match failed - ETag matches %s", objectETag)
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-None-Match passed - ETag %s doesn't match %s", objectETag, headers.ifNoneMatch)
|
||||||
|
} else {
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-None-Match passed - object %s/%s does not exist", bucket, object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check If-Modified-Since
|
||||||
|
if !headers.ifModifiedSince.IsZero() {
|
||||||
|
if objectExists {
|
||||||
|
objectModTime := time.Unix(entry.Attributes.Mtime, 0)
|
||||||
|
if !objectModTime.After(headers.ifModifiedSince) {
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-Modified-Since failed - object not modified since %s", r.Header.Get(s3_constants.IfModifiedSince))
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
glog.V(3).Infof("checkConditionalHeaders: If-Modified-Since passed - object modified after %s", r.Header.Get(s3_constants.IfModifiedSince))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s3err.ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConditionalHeaders is the production method that uses the S3ApiServer as EntryGetter
|
||||||
|
func (s3a *S3ApiServer) checkConditionalHeaders(r *http.Request, bucket, object string) s3err.ErrorCode {
|
||||||
|
return s3a.checkConditionalHeadersWithGetter(s3a, r, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConditionalHeadersForReadsWithGetter is a testable method for read operations
|
||||||
|
// Uses the production getObjectETag and etagMatches methods to ensure testing of real logic
|
||||||
|
func (s3a *S3ApiServer) checkConditionalHeadersForReadsWithGetter(getter EntryGetter, r *http.Request, bucket, object string) s3err.ErrorCode {
|
||||||
|
headers, errCode := parseConditionalHeaders(r)
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: Invalid date format")
|
||||||
|
return errCode
|
||||||
|
}
|
||||||
|
if !headers.isSet {
|
||||||
|
return s3err.ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get object entry for conditional checks.
|
||||||
|
bucketDir := "/buckets/" + bucket
|
||||||
|
entry, entryErr := getter.getEntry(bucketDir, object)
|
||||||
|
objectExists := entryErr == nil
|
||||||
|
|
||||||
|
// If object doesn't exist, fail for If-Match and If-Unmodified-Since
|
||||||
|
if !objectExists {
|
||||||
|
if headers.ifMatch != "" {
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-Match failed - object %s/%s does not exist", bucket, object)
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
if !headers.ifUnmodifiedSince.IsZero() {
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-Unmodified-Since failed - object %s/%s does not exist", bucket, object)
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
// If-None-Match and If-Modified-Since succeed when object doesn't exist
|
||||||
|
return s3err.ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object exists - check all conditions
|
||||||
|
// The evaluation order follows AWS S3 behavior for consistency.
|
||||||
|
|
||||||
|
// 1. Check If-Match (412 Precondition Failed if fails)
|
||||||
|
if headers.ifMatch != "" {
|
||||||
|
// If `ifMatch` is "*", the condition is met if the object exists.
|
||||||
|
// Otherwise, we need to check the ETag.
|
||||||
|
if headers.ifMatch != "*" {
|
||||||
|
// Use production getObjectETag method
|
||||||
|
objectETag := s3a.getObjectETag(entry)
|
||||||
|
// Use production etagMatches method
|
||||||
|
if !s3a.etagMatches(headers.ifMatch, objectETag) {
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-Match failed for object %s/%s - expected ETag %s, got %s", bucket, object, headers.ifMatch, objectETag)
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-Match passed for object %s/%s", bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check If-Unmodified-Since (412 Precondition Failed if fails)
|
||||||
|
if !headers.ifUnmodifiedSince.IsZero() {
|
||||||
|
objectModTime := time.Unix(entry.Attributes.Mtime, 0)
|
||||||
|
if objectModTime.After(headers.ifUnmodifiedSince) {
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-Unmodified-Since failed - object modified after %s", r.Header.Get(s3_constants.IfUnmodifiedSince))
|
||||||
|
return s3err.ErrPreconditionFailed
|
||||||
|
}
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-Unmodified-Since passed - object not modified since %s", r.Header.Get(s3_constants.IfUnmodifiedSince))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check If-None-Match (304 Not Modified if fails)
|
||||||
|
if headers.ifNoneMatch != "" {
|
||||||
|
if headers.ifNoneMatch == "*" {
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-None-Match=* failed - object %s/%s exists", bucket, object)
|
||||||
|
return s3err.ErrNotModified
|
||||||
|
}
|
||||||
|
// Use production getObjectETag method
|
||||||
|
objectETag := s3a.getObjectETag(entry)
|
||||||
|
// Use production etagMatches method
|
||||||
|
if s3a.etagMatches(headers.ifNoneMatch, objectETag) {
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-None-Match failed - ETag matches %s", objectETag)
|
||||||
|
return s3err.ErrNotModified
|
||||||
|
}
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-None-Match passed - ETag %s doesn't match %s", objectETag, headers.ifNoneMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check If-Modified-Since (304 Not Modified if fails)
|
||||||
|
if !headers.ifModifiedSince.IsZero() {
|
||||||
|
objectModTime := time.Unix(entry.Attributes.Mtime, 0)
|
||||||
|
if !objectModTime.After(headers.ifModifiedSince) {
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-Modified-Since failed - object not modified since %s", r.Header.Get(s3_constants.IfModifiedSince))
|
||||||
|
return s3err.ErrNotModified
|
||||||
|
}
|
||||||
|
glog.V(3).Infof("checkConditionalHeadersForReads: If-Modified-Since passed - object modified after %s", r.Header.Get(s3_constants.IfModifiedSince))
|
||||||
|
}
|
||||||
|
|
||||||
|
return s3err.ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConditionalHeadersForReads is the production method that uses the S3ApiServer as EntryGetter
|
||||||
|
func (s3a *S3ApiServer) checkConditionalHeadersForReads(r *http.Request, bucket, object string) s3err.ErrorCode {
|
||||||
|
return s3a.checkConditionalHeadersForReadsWithGetter(s3a, r, bucket, object)
|
||||||
|
}
|
||||||
|
@@ -102,6 +102,7 @@ const (
|
|||||||
ErrAuthNotSetup
|
ErrAuthNotSetup
|
||||||
ErrNotImplemented
|
ErrNotImplemented
|
||||||
ErrPreconditionFailed
|
ErrPreconditionFailed
|
||||||
|
ErrNotModified
|
||||||
|
|
||||||
ErrExistingObjectIsDirectory
|
ErrExistingObjectIsDirectory
|
||||||
ErrExistingObjectIsFile
|
ErrExistingObjectIsFile
|
||||||
@@ -451,6 +452,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
|
|||||||
Description: "At least one of the pre-conditions you specified did not hold",
|
Description: "At least one of the pre-conditions you specified did not hold",
|
||||||
HTTPStatusCode: http.StatusPreconditionFailed,
|
HTTPStatusCode: http.StatusPreconditionFailed,
|
||||||
},
|
},
|
||||||
|
ErrNotModified: {
|
||||||
|
Code: "NotModified",
|
||||||
|
Description: "The object was not modified since the specified time",
|
||||||
|
HTTPStatusCode: http.StatusNotModified,
|
||||||
|
},
|
||||||
ErrExistingObjectIsDirectory: {
|
ErrExistingObjectIsDirectory: {
|
||||||
Code: "ExistingObjectIsDirectory",
|
Code: "ExistingObjectIsDirectory",
|
||||||
Description: "Existing Object is a directory.",
|
Description: "Existing Object is a directory.",
|
||||||
|
Reference in New Issue
Block a user