123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- 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)
- }
|