You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							249 lines
						
					
					
						
							8.8 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							249 lines
						
					
					
						
							8.8 KiB
						
					
					
				| package s3api | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"fmt" | |
| 	"net/http" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" | |
| ) | |
| 
 | |
| // executeUnifiedCopyStrategy executes the appropriate copy strategy based on encryption state | |
| // Returns chunks and destination metadata that should be applied to the destination entry | |
| func (s3a *S3ApiServer) executeUnifiedCopyStrategy(entry *filer_pb.Entry, r *http.Request, dstBucket, srcObject, dstObject string) ([]*filer_pb.FileChunk, map[string][]byte, error) { | |
| 	// Detect encryption state (using entry-aware detection for multipart objects) | |
| 	srcPath := fmt.Sprintf("/%s/%s", r.Header.Get("X-Amz-Copy-Source-Bucket"), srcObject) | |
| 	dstPath := fmt.Sprintf("/%s/%s", dstBucket, dstObject) | |
| 	state := DetectEncryptionStateWithEntry(entry, r, srcPath, dstPath) | |
| 
 | |
| 	// Debug logging for encryption state | |
|  | |
| 	// Apply bucket default encryption if no explicit encryption specified | |
| 	if !state.IsTargetEncrypted() { | |
| 		bucketMetadata, err := s3a.getBucketMetadata(dstBucket) | |
| 		if err == nil && bucketMetadata != nil && bucketMetadata.Encryption != nil { | |
| 			switch bucketMetadata.Encryption.SseAlgorithm { | |
| 			case "aws:kms": | |
| 				state.DstSSEKMS = true | |
| 			case "AES256": | |
| 				state.DstSSES3 = true | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	// Determine copy strategy | |
| 	strategy, err := DetermineUnifiedCopyStrategy(state, entry.Extended, r) | |
| 	if err != nil { | |
| 		return nil, nil, err | |
| 	} | |
| 
 | |
| 	glog.V(2).Infof("Unified copy strategy for %s → %s: %v", srcPath, dstPath, strategy) | |
| 
 | |
| 	// Calculate optimized sizes for the strategy | |
| 	sizeCalc := CalculateOptimizedSizes(entry, r, strategy) | |
| 	glog.V(2).Infof("Size calculation: src=%d, target=%d, actual=%d, overhead=%d, preallocate=%v", | |
| 		sizeCalc.SourceSize, sizeCalc.TargetSize, sizeCalc.ActualContentSize, | |
| 		sizeCalc.EncryptionOverhead, sizeCalc.CanPreallocate) | |
| 
 | |
| 	// Execute strategy | |
| 	switch strategy { | |
| 	case CopyStrategyDirect: | |
| 		chunks, err := s3a.copyChunks(entry, dstPath) | |
| 		return chunks, nil, err | |
| 
 | |
| 	case CopyStrategyKeyRotation: | |
| 		return s3a.executeKeyRotation(entry, r, state) | |
| 
 | |
| 	case CopyStrategyEncrypt: | |
| 		return s3a.executeEncryptCopy(entry, r, state, dstBucket, dstPath) | |
| 
 | |
| 	case CopyStrategyDecrypt: | |
| 		return s3a.executeDecryptCopy(entry, r, state, dstPath) | |
| 
 | |
| 	case CopyStrategyReencrypt: | |
| 		return s3a.executeReencryptCopy(entry, r, state, dstBucket, dstPath) | |
| 
 | |
| 	default: | |
| 		return nil, nil, fmt.Errorf("unknown unified copy strategy: %v", strategy) | |
| 	} | |
| } | |
| 
 | |
| // mapCopyErrorToS3Error maps various copy errors to appropriate S3 error codes | |
| func (s3a *S3ApiServer) mapCopyErrorToS3Error(err error) s3err.ErrorCode { | |
| 	if err == nil { | |
| 		return s3err.ErrNone | |
| 	} | |
| 
 | |
| 	// Check for KMS errors first | |
| 	if kmsErr := MapKMSErrorToS3Error(err); kmsErr != s3err.ErrInvalidRequest { | |
| 		return kmsErr | |
| 	} | |
| 
 | |
| 	// Check for SSE-C errors | |
| 	if ssecErr := MapSSECErrorToS3Error(err); ssecErr != s3err.ErrInvalidRequest { | |
| 		return ssecErr | |
| 	} | |
| 
 | |
| 	// Default to internal error for unknown errors | |
| 	return s3err.ErrInternalError | |
| } | |
| 
 | |
| // executeKeyRotation handles key rotation for same-object copies | |
| func (s3a *S3ApiServer) executeKeyRotation(entry *filer_pb.Entry, r *http.Request, state *EncryptionState) ([]*filer_pb.FileChunk, map[string][]byte, error) { | |
| 	// For key rotation, we only need to update metadata, not re-copy chunks | |
| 	// This is a significant optimization for same-object key changes | |
|  | |
| 	if state.SrcSSEC && state.DstSSEC { | |
| 		// SSE-C key rotation - need to handle new key/IV, use reencrypt logic | |
| 		return s3a.executeReencryptCopy(entry, r, state, "", "") | |
| 	} | |
| 
 | |
| 	if state.SrcSSEKMS && state.DstSSEKMS { | |
| 		// SSE-KMS key rotation - return existing chunks, metadata will be updated by caller | |
| 		return entry.GetChunks(), nil, nil | |
| 	} | |
| 
 | |
| 	// Fallback to reencrypt if we can't do metadata-only rotation | |
| 	return s3a.executeReencryptCopy(entry, r, state, "", "") | |
| } | |
| 
 | |
| // executeEncryptCopy handles plain → encrypted copies | |
| func (s3a *S3ApiServer) executeEncryptCopy(entry *filer_pb.Entry, r *http.Request, state *EncryptionState, dstBucket, dstPath string) ([]*filer_pb.FileChunk, map[string][]byte, error) { | |
| 	if state.DstSSEC { | |
| 		// Use existing SSE-C copy logic | |
| 		return s3a.copyChunksWithSSEC(entry, r) | |
| 	} | |
| 
 | |
| 	if state.DstSSEKMS { | |
| 		// Use existing SSE-KMS copy logic - metadata is now generated internally | |
| 		chunks, dstMetadata, err := s3a.copyChunksWithSSEKMS(entry, r, dstBucket) | |
| 		return chunks, dstMetadata, err | |
| 	} | |
| 
 | |
| 	if state.DstSSES3 { | |
| 		// Use streaming copy for SSE-S3 encryption | |
| 		chunks, err := s3a.executeStreamingReencryptCopy(entry, r, state, dstPath) | |
| 		return chunks, nil, err | |
| 	} | |
| 
 | |
| 	return nil, nil, fmt.Errorf("unknown target encryption type") | |
| } | |
| 
 | |
| // executeDecryptCopy handles encrypted → plain copies | |
| func (s3a *S3ApiServer) executeDecryptCopy(entry *filer_pb.Entry, r *http.Request, state *EncryptionState, dstPath string) ([]*filer_pb.FileChunk, map[string][]byte, error) { | |
| 	// Use unified multipart-aware decrypt copy for all encryption types | |
| 	if state.SrcSSEC || state.SrcSSEKMS { | |
| 		glog.V(2).Infof("Encrypted→Plain copy: using unified multipart decrypt copy") | |
| 		return s3a.copyMultipartCrossEncryption(entry, r, state, "", dstPath) | |
| 	} | |
| 
 | |
| 	if state.SrcSSES3 { | |
| 		// Use streaming copy for SSE-S3 decryption | |
| 		chunks, err := s3a.executeStreamingReencryptCopy(entry, r, state, dstPath) | |
| 		return chunks, nil, err | |
| 	} | |
| 
 | |
| 	return nil, nil, fmt.Errorf("unknown source encryption type") | |
| } | |
| 
 | |
| // executeReencryptCopy handles encrypted → encrypted copies with different keys/methods | |
| func (s3a *S3ApiServer) executeReencryptCopy(entry *filer_pb.Entry, r *http.Request, state *EncryptionState, dstBucket, dstPath string) ([]*filer_pb.FileChunk, map[string][]byte, error) { | |
| 	// Check if we should use streaming copy for better performance | |
| 	if s3a.shouldUseStreamingCopy(entry, state) { | |
| 		chunks, err := s3a.executeStreamingReencryptCopy(entry, r, state, dstPath) | |
| 		return chunks, nil, err | |
| 	} | |
| 
 | |
| 	// Fallback to chunk-by-chunk approach for compatibility | |
| 	if state.SrcSSEC && state.DstSSEC { | |
| 		return s3a.copyChunksWithSSEC(entry, r) | |
| 	} | |
| 
 | |
| 	if state.SrcSSEKMS && state.DstSSEKMS { | |
| 		// Use existing SSE-KMS copy logic - metadata is now generated internally | |
| 		chunks, dstMetadata, err := s3a.copyChunksWithSSEKMS(entry, r, dstBucket) | |
| 		return chunks, dstMetadata, err | |
| 	} | |
| 
 | |
| 	if state.SrcSSEC && state.DstSSEKMS { | |
| 		// SSE-C → SSE-KMS: use unified multipart-aware cross-encryption copy | |
| 		glog.V(2).Infof("SSE-C→SSE-KMS cross-encryption copy: using unified multipart copy") | |
| 		return s3a.copyMultipartCrossEncryption(entry, r, state, dstBucket, dstPath) | |
| 	} | |
| 
 | |
| 	if state.SrcSSEKMS && state.DstSSEC { | |
| 		// SSE-KMS → SSE-C: use unified multipart-aware cross-encryption copy | |
| 		glog.V(2).Infof("SSE-KMS→SSE-C cross-encryption copy: using unified multipart copy") | |
| 		return s3a.copyMultipartCrossEncryption(entry, r, state, dstBucket, dstPath) | |
| 	} | |
| 
 | |
| 	// Handle SSE-S3 cross-encryption scenarios | |
| 	if state.SrcSSES3 || state.DstSSES3 { | |
| 		// Any scenario involving SSE-S3 uses streaming copy | |
| 		chunks, err := s3a.executeStreamingReencryptCopy(entry, r, state, dstPath) | |
| 		return chunks, nil, err | |
| 	} | |
| 
 | |
| 	return nil, nil, fmt.Errorf("unsupported cross-encryption scenario") | |
| } | |
| 
 | |
| // shouldUseStreamingCopy determines if streaming copy should be used | |
| func (s3a *S3ApiServer) shouldUseStreamingCopy(entry *filer_pb.Entry, state *EncryptionState) bool { | |
| 	// Use streaming copy for large files or when beneficial | |
| 	fileSize := entry.Attributes.FileSize | |
| 
 | |
| 	// Use streaming for files larger than 10MB | |
| 	if fileSize > 10*1024*1024 { | |
| 		return true | |
| 	} | |
| 
 | |
| 	// Check if this is a multipart encrypted object | |
| 	isMultipartEncrypted := false | |
| 	if state.IsSourceEncrypted() { | |
| 		encryptedChunks := 0 | |
| 		for _, chunk := range entry.GetChunks() { | |
| 			if chunk.GetSseType() != filer_pb.SSEType_NONE { | |
| 				encryptedChunks++ | |
| 			} | |
| 		} | |
| 		isMultipartEncrypted = encryptedChunks > 1 | |
| 	} | |
| 
 | |
| 	// For multipart encrypted objects, avoid streaming copy to use per-chunk metadata approach | |
| 	if isMultipartEncrypted { | |
| 		glog.V(3).Infof("Multipart encrypted object detected, using chunk-by-chunk approach") | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Use streaming for cross-encryption scenarios (for single-part objects only) | |
| 	if state.IsSourceEncrypted() && state.IsTargetEncrypted() { | |
| 		srcType := s3a.getEncryptionTypeString(state.SrcSSEC, state.SrcSSEKMS, state.SrcSSES3) | |
| 		dstType := s3a.getEncryptionTypeString(state.DstSSEC, state.DstSSEKMS, state.DstSSES3) | |
| 		if srcType != dstType { | |
| 			return true | |
| 		} | |
| 	} | |
| 
 | |
| 	// Use streaming for compressed files | |
| 	if isCompressedEntry(entry) { | |
| 		return true | |
| 	} | |
| 
 | |
| 	// Use streaming for SSE-S3 scenarios (always) | |
| 	if state.SrcSSES3 || state.DstSSES3 { | |
| 		return true | |
| 	} | |
| 
 | |
| 	return false | |
| } | |
| 
 | |
| // executeStreamingReencryptCopy performs streaming re-encryption copy | |
| func (s3a *S3ApiServer) executeStreamingReencryptCopy(entry *filer_pb.Entry, r *http.Request, state *EncryptionState, dstPath string) ([]*filer_pb.FileChunk, error) { | |
| 	// Create streaming copy manager | |
| 	streamingManager := NewStreamingCopyManager(s3a) | |
| 
 | |
| 	// Execute streaming copy | |
| 	return streamingManager.ExecuteStreamingCopy(context.Background(), entry, r, dstPath, state) | |
| }
 |