package service import ( "fmt" "io" "time" "aws-sts-mock/internal/kvdb" "aws-sts-mock/internal/storage" "aws-sts-mock/pkg/s3" ) // S3Service handles S3 business logic with user isolation type S3Service struct { storage *storage.UserAwareStorage db *kvdb.BoltKVDB } // NewS3Service creates a new S3 service func NewS3Service(storage *storage.UserAwareStorage, db *kvdb.BoltKVDB) *S3Service { return &S3Service{ storage: storage, db: db, } } // ListBuckets returns all buckets for a specific user func (s *S3Service) ListBuckets(accountID, accessKeyID string) (*s3.ListAllMyBucketsResult, error) { // Get bucket configs from database to get actual bucket names configs, err := s.db.ListBucketConfigs(accountID) if err != nil { return nil, err } var s3Buckets []s3.Bucket for _, config := range configs { // Parse the creation date createdAt, err := time.Parse(time.RFC3339, config.CreatedAt) if err != nil { createdAt = time.Now() } s3Buckets = append(s3Buckets, s3.Bucket{ Name: config.BucketName, // Use actual bucket name, not ID CreationDate: createdAt.UTC().Format(time.RFC3339), }) } return &s3.ListAllMyBucketsResult{ Owner: s3.Owner{ ID: accountID, DisplayName: accessKeyID, }, Buckets: s3Buckets, }, nil } // CreateBucket creates a new bucket for a specific user func (s *S3Service) CreateBucket(accountID, name string) error { if name == "" { return fmt.Errorf("bucket name is required") } // Validate bucket name (basic validation) if len(name) < 3 || len(name) > 63 { return fmt.Errorf("bucket name must be between 3 and 63 characters") } // Create bucket in storage (returns bucket ID) bucketID, err := s.storage.CreateBucketForUser(accountID, name) if err != nil { return err } // Store bucket config in database config := &kvdb.BucketConfig{ BucketID: bucketID, AccountID: accountID, BucketName: name, PublicViewing: false, // Default to private CreatedAt: time.Now().UTC().Format(time.RFC3339), } return s.db.SetBucketConfig(config) } // DeleteBucket deletes a bucket for a specific user func (s *S3Service) DeleteBucket(accountID, name string) error { // Get bucket ID first bucketID, err := s.storage.GetBucketIDForUser(accountID, name) if err != nil { return err } // Delete from storage if err := s.storage.DeleteBucketForUser(accountID, name); err != nil { return err } // Delete config from database return s.db.DeleteBucketConfig(accountID, bucketID) } // BucketExists checks if a bucket exists for a specific user func (s *S3Service) BucketExists(accountID, name string) (bool, error) { return s.storage.BucketExistsForUser(accountID, name) } // BucketIsEmpty checks if a bucket has no objects func (s *S3Service) BucketIsEmpty(accountID, bucketName string) (bool, error) { objects, err := s.storage.ListObjectsForUser(accountID, bucketName) if err != nil { return false, err } return len(objects) == 0, nil } // ListObjects returns all objects in a bucket for a specific user func (s *S3Service) ListObjects(accountID, bucketName string) ([]storage.ObjectInfo, error) { return s.storage.ListObjectsForUser(accountID, bucketName) } // PutObject stores an object for a specific user func (s *S3Service) PutObject(accountID, bucket, key string, reader io.Reader) error { return s.storage.PutObjectForUser(accountID, bucket, key, reader) } // GetObject retrieves an object for a specific user func (s *S3Service) GetObject(accountID, bucket, key string) (io.ReadCloser, *storage.ObjectInfo, error) { return s.storage.GetObjectForUser(accountID, bucket, key) } // DeleteObject removes an object for a specific user func (s *S3Service) DeleteObject(accountID, bucket, key string) error { return s.storage.DeleteObjectForUser(accountID, bucket, key) } // DeleteObjects removes multiple objects for a specific user // Returns a map of key -> error for failed deletions func (s *S3Service) DeleteObjects(accountID, bucket string, keys []string) map[string]error { errors := make(map[string]error) for _, key := range keys { if err := s.storage.DeleteObjectForUser(accountID, bucket, key); err != nil { // Only add to errors if it's not "object not found" // S3 behavior: deleting non-existent objects succeeds if err != storage.ErrObjectNotFound { errors[key] = err } } } return errors } // GetObjectInfo retrieves object metadata for a specific user func (s *S3Service) GetObjectInfo(accountID, bucket, key string) (*storage.ObjectInfo, error) { return s.storage.GetObjectInfoForUser(accountID, bucket, key) } // GetStorageStats returns storage statistics for a user func (s *S3Service) GetStorageStats(accountID string) (*storage.StorageStats, error) { return s.storage.GetStorageStatsForUser(accountID) } // SetBucketPublicViewing enables or disables public viewing for a bucket func (s *S3Service) SetBucketPublicViewing(accountID, bucketName string, enabled bool) error { // Get bucket ID bucketID, err := s.storage.GetBucketIDForUser(accountID, bucketName) if err != nil { return err } // Get existing config config, err := s.db.GetBucketConfig(accountID, bucketID) if err != nil { return fmt.Errorf("bucket config not found: %w", err) } // Update public viewing setting config.PublicViewing = enabled return s.db.SetBucketConfig(config) } // GetBucketConfig retrieves bucket configuration func (s *S3Service) GetBucketConfig(accountID, bucketName string) (*kvdb.BucketConfig, error) { bucketID, err := s.storage.GetBucketIDForUser(accountID, bucketName) if err != nil { return nil, err } return s.db.GetBucketConfig(accountID, bucketID) } // GetBucketByID retrieves bucket configuration by bucket ID func (s *S3Service) GetBucketByID(accountID, bucketID string) (*kvdb.BucketConfig, error) { return s.db.GetBucketConfig(accountID, bucketID) } // UpdateBucketConfig updates bucket configuration func (s *S3Service) UpdateBucketConfig(config *kvdb.BucketConfig) error { return s.db.SetBucketConfig(config) } // CopyObject copies an object within the same bucket or to another bucket func (s *S3Service) CopyObject(accountID, srcBucket, srcKey, dstBucket, dstKey string) error { // Get source object reader, info, err := s.storage.GetObjectForUser(accountID, srcBucket, srcKey) if err != nil { return fmt.Errorf("failed to get source object: %w", err) } defer reader.Close() // Put to destination if err := s.storage.PutObjectForUser(accountID, dstBucket, dstKey, reader); err != nil { return fmt.Errorf("failed to put destination object: %w", err) } // Note: Metadata is not copied in this simple implementation // You might want to extend storage to support metadata copying _ = info // Metadata could be used here if storage interface supported it return nil } // ObjectExists checks if an object exists func (s *S3Service) ObjectExists(accountID, bucket, key string) (bool, error) { _, err := s.storage.GetObjectInfoForUser(accountID, bucket, key) if err != nil { if err == storage.ErrObjectNotFound { return false, nil } return false, err } return true, nil } // GetBucketSize returns the total size of all objects in a bucket func (s *S3Service) GetBucketSize(accountID, bucketName string) (int64, error) { objects, err := s.storage.ListObjectsForUser(accountID, bucketName) if err != nil { return 0, err } var totalSize int64 for _, obj := range objects { totalSize += obj.Size } return totalSize, nil } // GetBucketObjectCount returns the number of objects in a bucket func (s *S3Service) GetBucketObjectCount(accountID, bucketName string) (int, error) { objects, err := s.storage.ListObjectsForUser(accountID, bucketName) if err != nil { return 0, err } return len(objects), nil } // ListObjectsWithPrefix returns objects with a specific prefix func (s *S3Service) ListObjectsWithPrefix(accountID, bucketName, prefix string) ([]storage.ObjectInfo, error) { allObjects, err := s.storage.ListObjectsForUser(accountID, bucketName) if err != nil { return nil, err } var filtered []storage.ObjectInfo for _, obj := range allObjects { if len(obj.Key) >= len(prefix) && obj.Key[:len(prefix)] == prefix { filtered = append(filtered, obj) } } return filtered, nil } // DeleteObjectsWithPrefix deletes all objects with a specific prefix func (s *S3Service) DeleteObjectsWithPrefix(accountID, bucketName, prefix string) (int, error) { objects, err := s.ListObjectsWithPrefix(accountID, bucketName, prefix) if err != nil { return 0, err } count := 0 for _, obj := range objects { if err := s.storage.DeleteObjectForUser(accountID, bucketName, obj.Key); err != nil { // Log error but continue continue } count++ } return count, nil } // ValidateBucketName validates bucket name according to S3 rules func (s *S3Service) ValidateBucketName(name string) error { if len(name) < 3 || len(name) > 63 { return fmt.Errorf("bucket name must be between 3 and 63 characters") } // Check for valid characters (simplified version) for i, c := range name { if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.') { return fmt.Errorf("bucket name contains invalid character: %c", c) } // Must start and end with letter or number if i == 0 || i == len(name)-1 { if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { return fmt.Errorf("bucket name must start and end with a letter or number") } } } // Cannot be formatted as IP address if isIPAddress(name) { return fmt.Errorf("bucket name cannot be formatted as IP address") } return nil } // isIPAddress checks if string looks like an IP address func isIPAddress(s string) bool { parts := splitString(s, '.') if len(parts) != 4 { return false } for _, part := range parts { if len(part) == 0 || len(part) > 3 { return false } num := 0 for _, c := range part { if c < '0' || c > '9' { return false } num = num*10 + int(c-'0') } if num > 255 { return false } } return true } // splitString splits string by delimiter func splitString(s string, delim rune) []string { var parts []string var current string for _, c := range s { if c == delim { parts = append(parts, current) current = "" } else { current += string(c) } } parts = append(parts, current) return parts } // GetAllUserBuckets returns all bucket names for a user func (s *S3Service) GetAllUserBuckets(accountID string) ([]string, error) { configs, err := s.db.ListBucketConfigs(accountID) if err != nil { return nil, err } bucketNames := make([]string, len(configs)) for i, config := range configs { bucketNames[i] = config.BucketName } return bucketNames, nil } // DeleteAllBuckets deletes all buckets for a user (useful for cleanup/testing) func (s *S3Service) DeleteAllBuckets(accountID string) error { buckets, err := s.GetAllUserBuckets(accountID) if err != nil { return err } for _, bucket := range buckets { // Delete all objects first objects, err := s.ListObjects(accountID, bucket) if err != nil { continue } for _, obj := range objects { _ = s.DeleteObject(accountID, bucket, obj.Key) } // Delete bucket _ = s.DeleteBucket(accountID, bucket) } return nil } // RenameObject renames an object (copy + delete) func (s *S3Service) RenameObject(accountID, bucket, oldKey, newKey string) error { // Copy object if err := s.CopyObject(accountID, bucket, oldKey, bucket, newKey); err != nil { return err } // Delete old object return s.DeleteObject(accountID, bucket, oldKey) } // MoveObject moves an object to another bucket (copy + delete) func (s *S3Service) MoveObject(accountID, srcBucket, srcKey, dstBucket, dstKey string) error { // Copy object if err := s.CopyObject(accountID, srcBucket, srcKey, dstBucket, dstKey); err != nil { return err } // Delete source object return s.DeleteObject(accountID, srcBucket, srcKey) } // CreateMultipartUpload initiates a multipart upload func (s *S3Service) CreateMultipartUpload(accountID, bucket, key string) (string, error) { // Check if bucket exists exists, err := s.storage.BucketExistsForUser(accountID, bucket) if err != nil { return "", err } if !exists { return "", storage.ErrBucketNotFound } return s.storage.CreateMultipartUpload(accountID, bucket, key) } // UploadPart uploads a part for a multipart upload func (s *S3Service) UploadPart(uploadID string, partNumber int, data io.Reader) (string, error) { // Validate part number (1-10000) if partNumber < 1 || partNumber > 10000 { return "", fmt.Errorf("part number must be between 1 and 10000") } return s.storage.UploadPart(uploadID, partNumber, data) } // ListParts lists parts of a multipart upload func (s *S3Service) ListParts(uploadID string, maxParts, partNumberMarker int) ([]storage.PartInfo, int, bool, error) { if maxParts <= 0 { maxParts = 1000 // Default max parts } if maxParts > 1000 { maxParts = 1000 // S3 limit } return s.storage.ListParts(uploadID, maxParts, partNumberMarker) } // GetMultipartUpload gets multipart upload information func (s *S3Service) GetMultipartUpload(uploadID string) (*storage.MultipartUploadInfo, error) { return s.storage.GetMultipartUpload(uploadID) } // AbortMultipartUpload cancels a multipart upload func (s *S3Service) AbortMultipartUpload(uploadID string) error { return s.storage.AbortMultipartUpload(uploadID) } // CompleteMultipartUpload finalizes a multipart upload func (s *S3Service) CompleteMultipartUpload(uploadID string, parts []int) error { // Validate parts are in order for i := 1; i < len(parts); i++ { if parts[i] <= parts[i-1] { return fmt.Errorf("parts must be in ascending order") } } return s.storage.CompleteMultipartUpload(uploadID, parts) } // ListMultipartUploads lists in-progress multipart uploads for a bucket func (s *S3Service) ListMultipartUploads(accountID, bucket string, maxUploads int, keyMarker, uploadIDMarker string) ([]storage.MultipartUploadInfo, string, string, bool, error) { // Check if bucket exists exists, err := s.storage.BucketExistsForUser(accountID, bucket) if err != nil { return nil, "", "", false, err } if !exists { return nil, "", "", false, storage.ErrBucketNotFound } if maxUploads <= 0 { maxUploads = 1000 } if maxUploads > 1000 { maxUploads = 1000 } return s.storage.ListMultipartUploads(accountID, bucket, maxUploads, keyMarker, uploadIDMarker) }