package storage import ( "bytes" "context" "fmt" "io" "net/http" "os" "path/filepath" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" storageconfig "github.com/tech/sendico/server/internal/server/fileserviceimp/storage/config" "go.uber.org/zap" ) type AWSS3Storage struct { logger mlogger.Logger s3Client *s3.Client bucketName string directory string service mservice.Type } func (storage *AWSS3Storage) Delete(ctx context.Context, objID string) error { fullPath := filepath.Join(storage.directory, objID) _, err := storage.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: aws.String(storage.bucketName), Key: aws.String(fullPath), }) if err != nil { storage.logger.Warn("Failed to delete file from AWS S3", zap.Error(err), zap.String("obj_ref", objID)) return err } // Wait for object to be deleted waiter := s3.NewObjectNotExistsWaiter(storage.s3Client) err = waiter.Wait(ctx, &s3.HeadObjectInput{ Bucket: aws.String(storage.bucketName), Key: aws.String(fullPath), }, 30) // 30 second timeout if err != nil { storage.logger.Warn("Error occurred while waiting for S3 file deletion", zap.Error(err), zap.String("obj_ref", objID)) return err } return nil } func (storage *AWSS3Storage) s3URL(fullPath string) string { return fmt.Sprintf("https://%s.s3.amazonaws.com/%s", storage.bucketName, fullPath) } func (storage *AWSS3Storage) Save(ctx context.Context, file io.Reader, objID string) (string, error) { fullPath := filepath.Join(storage.directory, objID) buf := new(bytes.Buffer) _, err := io.Copy(buf, file) if err != nil { storage.logger.Warn("Failed to read file content", zap.Error(err), zap.String("obj_ref", objID)) return "", err } _, err = storage.s3Client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(storage.bucketName), Key: aws.String(fullPath), Body: bytes.NewReader(buf.Bytes()), }) if err != nil { storage.logger.Warn("Failed to upload file to S3", zap.Error(err), zap.String("obj_ref", objID)) return "", err } s3URL := storage.s3URL(fullPath) storage.logger.Info("File upload complete", zap.String("obj_ref", objID), zap.String("s3_url", s3URL)) return s3URL, nil } func (storage *AWSS3Storage) Get(ctx context.Context, objID string) http.HandlerFunc { storage.logger.Warn("Indirect access to the object should be avoided", zap.String("obj_ref", objID)) fullPath := filepath.Join(storage.directory, objID) _, err := storage.s3Client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(storage.bucketName), Key: aws.String(fullPath), }) if err != nil { storage.logger.Warn("Failed to get file from S3", zap.Error(err), zap.String("obj_ref", objID)) return response.NotFound(storage.logger, storage.service, err.Error()) } res := func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, storage.s3URL(fullPath), http.StatusFound) } return res } func CreateAWSS3Storage(logger mlogger.Logger, service mservice.Type, directory string, cfg storageconfig.AWSS3SConfig) (*AWSS3Storage, error) { region := os.Getenv(cfg.RegionEnv) accessKeyID := os.Getenv(cfg.AccessKeyIDEnv) secretAccessKey := os.Getenv(cfg.SecretAccessKeyEnv) bucketName := os.Getenv(cfg.BucketNameEnv) // Create AWS config with static credentials awsConfig, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( accessKeyID, secretAccessKey, "", )), ) if err != nil { logger.Warn("Failed to create AWS config", zap.Error(err), zap.String("bucket", bucketName), zap.String("access_key_id", accessKeyID), zap.String("region", region)) return nil, err } // Create S3 client s3Client := s3.NewFromConfig(awsConfig) res := &AWSS3Storage{ logger: logger.Named("aws_s3").Named(directory), s3Client: s3Client, bucketName: bucketName, directory: directory, service: service, } res.logger.Info("Storage installed", zap.String("bucket", bucketName), zap.String("region", region), zap.String("access_key_id", accessKeyID)) return res, nil }