|
|
|
@ -327,14 +327,14 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) |
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
|
|
|
return |
|
|
|
} |
|
|
|
glog.V(1).Infof("GetObject: bucket %s, object %s, versioningConfigured=%v, versionId=%s", bucket, object, versioningConfigured, versionId) |
|
|
|
glog.V(0).Infof("GetObject: bucket %s, object %s, versioningConfigured=%v, versionId=%s", bucket, object, versioningConfigured, versionId) |
|
|
|
|
|
|
|
if versioningConfigured { |
|
|
|
// Handle versioned GET - all versions are stored in .versions directory
|
|
|
|
// Handle versioned GET - check if specific version requested
|
|
|
|
var targetVersionId string |
|
|
|
|
|
|
|
if versionId != "" { |
|
|
|
// Request for specific version
|
|
|
|
// Request for specific version - must look in .versions directory
|
|
|
|
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 { |
|
|
|
@ -350,20 +350,63 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) |
|
|
|
} |
|
|
|
targetVersionId = versionId |
|
|
|
} else { |
|
|
|
// Request for latest version
|
|
|
|
glog.V(1).Infof("GetObject: requesting latest version for %s%s", bucket, object) |
|
|
|
// Request for latest version - OPTIMIZATION for suspended versioning:
|
|
|
|
// For suspended versioning, new objects are stored at regular path with version ID "null".
|
|
|
|
// Check regular path FIRST to avoid 12-second retry delay on .versions directory.
|
|
|
|
glog.V(0).Infof("GetObject: requesting latest version for %s%s, checking regular path first", bucket, object) |
|
|
|
bucketDir := s3a.option.BucketsPath + "/" + bucket |
|
|
|
normalizedObject := removeDuplicateSlashes(object) |
|
|
|
regularEntry, regularErr := s3a.getEntry(bucketDir, normalizedObject) |
|
|
|
|
|
|
|
if regularErr == nil && regularEntry != nil { |
|
|
|
// Found object at regular path - check if it's a null version or pre-versioning object
|
|
|
|
hasNullVersion := false |
|
|
|
if regularEntry.Extended != nil { |
|
|
|
if versionIdBytes, exists := regularEntry.Extended[s3_constants.ExtVersionIdKey]; exists { |
|
|
|
versionIdStr := string(versionIdBytes) |
|
|
|
if versionIdStr == "null" { |
|
|
|
hasNullVersion = true |
|
|
|
targetVersionId = "null" |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if hasNullVersion || regularEntry.Extended == nil || regularEntry.Extended[s3_constants.ExtVersionIdKey] == nil { |
|
|
|
// This is either a null version (suspended) or pre-versioning object
|
|
|
|
// Use it directly instead of checking .versions
|
|
|
|
glog.V(0).Infof("GetObject: found null/pre-versioning object at regular path for %s%s", bucket, object) |
|
|
|
entry = regularEntry |
|
|
|
if targetVersionId == "" { |
|
|
|
targetVersionId = "null" |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Has a real version ID, must be in .versions - fall through to getLatestObjectVersion
|
|
|
|
glog.V(0).Infof("GetObject: regular path object has version ID, checking .versions for %s%s", bucket, object) |
|
|
|
entry, err = s3a.getLatestObjectVersion(bucket, object) |
|
|
|
if err != nil { |
|
|
|
glog.Errorf("GetObject: Failed to get latest version for %s%s: %v", bucket, object, err) |
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
// No object at regular path, check .versions directory
|
|
|
|
glog.V(0).Infof("GetObject: no object at regular path, checking .versions for %s%s", bucket, object) |
|
|
|
entry, err = s3a.getLatestObjectVersion(bucket, object) |
|
|
|
if err != nil { |
|
|
|
glog.Errorf("GetObject: Failed to get latest version for %s%s: %v", bucket, object, err) |
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
// Safety check: entry must be valid after successful retrieval
|
|
|
|
if entry == nil { |
|
|
|
glog.Errorf("GetObject: getLatestObjectVersion returned nil entry without error for %s%s", bucket, object) |
|
|
|
glog.Errorf("GetObject: entry is nil after versioned lookup for %s%s", bucket, object) |
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) |
|
|
|
return |
|
|
|
} |
|
|
|
// Extract version ID if not already set
|
|
|
|
if targetVersionId == "" { |
|
|
|
if entry.Extended != nil { |
|
|
|
if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists { |
|
|
|
targetVersionId = string(versionIdBytes) |
|
|
|
@ -374,6 +417,7 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) |
|
|
|
targetVersionId = "null" |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Check if this is a delete marker
|
|
|
|
if entry.Extended != nil { |
|
|
|
@ -631,6 +675,7 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R |
|
|
|
|
|
|
|
// For small files stored inline in entry.Content
|
|
|
|
if len(entry.Content) > 0 && totalSize == int64(len(entry.Content)) { |
|
|
|
glog.V(0).Infof("streamFromVolumeServers: streaming %d bytes from entry.Content", len(entry.Content)) |
|
|
|
if isRangeRequest { |
|
|
|
_, err := w.Write(entry.Content[offset : offset+size]) |
|
|
|
return err |
|
|
|
@ -641,12 +686,19 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R |
|
|
|
|
|
|
|
// Get chunks
|
|
|
|
chunks := entry.GetChunks() |
|
|
|
glog.V(0).Infof("streamFromVolumeServers: entry has %d chunks, totalSize=%d, len(entry.Content)=%d", len(chunks), totalSize, len(entry.Content)) |
|
|
|
if len(chunks) == 0 { |
|
|
|
// BUG FIX: If totalSize > 0 but no chunks and no content, this is a data integrity issue
|
|
|
|
if totalSize > 0 && len(entry.Content) == 0 { |
|
|
|
glog.Errorf("streamFromVolumeServers: Data integrity error - entry reports size %d but has no content or chunks", totalSize) |
|
|
|
return fmt.Errorf("data integrity error: size %d reported but no content available", totalSize) |
|
|
|
} |
|
|
|
// Empty object - need to set headers before writing status
|
|
|
|
if !isRangeRequest { |
|
|
|
// Headers were already set by setResponseHeaders above
|
|
|
|
w.WriteHeader(http.StatusOK) |
|
|
|
} |
|
|
|
glog.V(0).Infof("streamFromVolumeServers: empty object (totalSize=%d), returning", totalSize) |
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
@ -1226,20 +1278,63 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request |
|
|
|
} |
|
|
|
targetVersionId = versionId |
|
|
|
} else { |
|
|
|
// Request for latest version
|
|
|
|
glog.V(2).Infof("HeadObject: requesting latest version for %s%s", bucket, object) |
|
|
|
// Request for latest version - OPTIMIZATION for suspended versioning:
|
|
|
|
// For suspended versioning, new objects are stored at regular path with version ID "null".
|
|
|
|
// Check regular path FIRST to avoid 12-second retry delay on .versions directory.
|
|
|
|
glog.V(0).Infof("HeadObject: requesting latest version for %s%s, checking regular path first", bucket, object) |
|
|
|
bucketDir := s3a.option.BucketsPath + "/" + bucket |
|
|
|
normalizedObject := removeDuplicateSlashes(object) |
|
|
|
regularEntry, regularErr := s3a.getEntry(bucketDir, normalizedObject) |
|
|
|
|
|
|
|
if regularErr == nil && regularEntry != nil { |
|
|
|
// Found object at regular path - check if it's a null version or pre-versioning object
|
|
|
|
hasNullVersion := false |
|
|
|
if regularEntry.Extended != nil { |
|
|
|
if versionIdBytes, exists := regularEntry.Extended[s3_constants.ExtVersionIdKey]; exists { |
|
|
|
versionIdStr := string(versionIdBytes) |
|
|
|
if versionIdStr == "null" { |
|
|
|
hasNullVersion = true |
|
|
|
targetVersionId = "null" |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if hasNullVersion || regularEntry.Extended == nil || regularEntry.Extended[s3_constants.ExtVersionIdKey] == nil { |
|
|
|
// This is either a null version (suspended) or pre-versioning object
|
|
|
|
// Use it directly instead of checking .versions
|
|
|
|
glog.V(0).Infof("HeadObject: found null/pre-versioning object at regular path for %s%s", bucket, object) |
|
|
|
entry = regularEntry |
|
|
|
if targetVersionId == "" { |
|
|
|
targetVersionId = "null" |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Has a real version ID, must be in .versions - fall through to getLatestObjectVersion
|
|
|
|
glog.V(0).Infof("HeadObject: regular path object has version ID, checking .versions for %s%s", bucket, object) |
|
|
|
entry, err = s3a.getLatestObjectVersion(bucket, object) |
|
|
|
if err != nil { |
|
|
|
glog.Errorf("Failed to get latest version: %v", err) |
|
|
|
glog.Errorf("HeadObject: Failed to get latest version for %s%s: %v", bucket, object, err) |
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
// No object at regular path, check .versions directory
|
|
|
|
glog.V(0).Infof("HeadObject: no object at regular path, checking .versions for %s%s", bucket, object) |
|
|
|
entry, err = s3a.getLatestObjectVersion(bucket, object) |
|
|
|
if err != nil { |
|
|
|
glog.Errorf("HeadObject: Failed to get latest version for %s%s: %v", bucket, object, err) |
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
// Safety check: entry must be valid after successful retrieval
|
|
|
|
if entry == nil { |
|
|
|
glog.Errorf("HeadObject: getLatestObjectVersion returned nil entry without error for %s%s", bucket, object) |
|
|
|
glog.Errorf("HeadObject: entry is nil after versioned lookup for %s%s", bucket, object) |
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) |
|
|
|
return |
|
|
|
} |
|
|
|
// Extract version ID if not already set
|
|
|
|
if targetVersionId == "" { |
|
|
|
if entry.Extended != nil { |
|
|
|
if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists { |
|
|
|
targetVersionId = string(versionIdBytes) |
|
|
|
@ -1250,6 +1345,7 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request |
|
|
|
targetVersionId = "null" |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Check if this is a delete marker
|
|
|
|
if entry.Extended != nil { |
|
|
|
|