package storage import ( "bytes" "context" "fmt" "path/filepath" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3manager" ) type Storage interface { UploadVideo(ctx context.Context, videoData []byte, fileName string, userID int) (string, error) UploadThumbnail(ctx context.Context, thumbnailData []byte, userID int) (string, error) Delete(ctx context.Context, fileURL string) error DeleteVideo(ctx context.Context, videoURL string) error DeleteThumbnail(ctx context.Context, thumbnailURL string) error BulkDelete(ctx context.Context, fileURLs []string) error CheckObjectExists(ctx context.Context, fileURL string) (bool, error) } type S3Config struct { Endpoint string Bucket string Region string AccessKey string SecretKey string } type s3Storage struct { uploader *s3manager.Uploader s3Client *s3.S3 bucket string region string endpoint string } // NewS3Storage создает S3 storage - теперь требует конфигурацию func NewS3Storage(config S3Config) Storage { sess := session.Must(session.NewSession(&aws.Config{ Region: aws.String(config.Region), Endpoint: aws.String(config.Endpoint), S3ForcePathStyle: aws.Bool(true), Credentials: credentials.NewStaticCredentials( config.AccessKey, config.SecretKey, "", ), })) uploader := s3manager.NewUploader(sess) s3Client := s3.New(sess) return &s3Storage{ uploader: uploader, s3Client: s3Client, bucket: config.Bucket, region: config.Region, endpoint: config.Endpoint, } } func (s *s3Storage) UploadVideo(ctx context.Context, videoData []byte, fileName string, userID int) (string, error) { ext := strings.ToLower(filepath.Ext(fileName)) timestamp := time.Now().UnixNano() s3Key := fmt.Sprintf("clips/%d/videos/%d%s", userID, timestamp, ext) _, err := s.uploader.UploadWithContext(ctx, &s3manager.UploadInput{ Bucket: aws.String(s.bucket), Key: aws.String(s3Key), Body: bytes.NewReader(videoData), ContentType: aws.String(s.getVideoContentType(ext)), }) if err != nil { return "", fmt.Errorf("failed to upload video to S3: %w", err) } return fmt.Sprintf("%s/%s/%s", s.endpoint, s.bucket, s3Key), nil } func (s *s3Storage) UploadThumbnail(ctx context.Context, thumbnailData []byte, userID int) (string, error) { timestamp := time.Now().UnixNano() s3Key := fmt.Sprintf("clips/%d/thumbnails/%d.jpg", userID, timestamp) _, err := s.uploader.Upload(&s3manager.UploadInput{ Bucket: aws.String(s.bucket), Key: aws.String(s3Key), Body: aws.ReadSeekCloser(strings.NewReader(string(thumbnailData))), ContentType: aws.String("image/jpeg"), }) if err != nil { return "", fmt.Errorf("failed to upload thumbnail to S3: %w", err) } return fmt.Sprintf("%s/%s/%s", s.endpoint, s.bucket, s3Key), nil } func (s *s3Storage) Delete(ctx context.Context, fileURL string) error { s3Key, err := s.extractS3Key(fileURL) if err != nil { return err } return s.deleteObject(s3Key) } func (s *s3Storage) DeleteVideo(ctx context.Context, videoURL string) error { s3Key, err := s.extractS3Key(videoURL) if err != nil { return err } return s.deleteObject(s3Key) } func (s *s3Storage) DeleteThumbnail(ctx context.Context, thumbnailURL string) error { s3Key, err := s.extractS3Key(thumbnailURL) if err != nil { return err } return s.deleteObject(s3Key) } func (s *s3Storage) deleteObject(s3Key string) error { _, err := s.s3Client.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(s3Key), }) if err != nil { return fmt.Errorf("failed to delete object from S3: %w", err) } // Ждем пока объект действительно удалится err = s.s3Client.WaitUntilObjectNotExists(&s3.HeadObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(s3Key), }) if err != nil { return fmt.Errorf("failed to wait for object deletion: %w", err) } return nil } func (s *s3Storage) extractS3Key(fileURL string) (string, error) { // Убираем протокол и endpoint cleanURL := strings.TrimPrefix(fileURL, s.endpoint+"/") // Убираем bucket name parts := strings.SplitN(cleanURL, "/", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid S3 URL format: %s", fileURL) } if parts[0] != s.bucket { return "", fmt.Errorf("invalid bucket in URL: expected %s, got %s", s.bucket, parts[0]) } return parts[1], nil } func (s *s3Storage) getVideoContentType(ext string) string { switch ext { case ".mp4": return "video/mp4" case ".webm": return "video/webm" case ".mov": return "video/quicktime" case ".avi": return "video/x-msvideo" case ".mkv": return "video/x-matroska" case ".flv": return "video/x-flv" case ".wmv": return "video/x-ms-wmv" default: return "video/mp4" } } // BulkDelete удаляет несколько файлов func (s *s3Storage) BulkDelete(ctx context.Context, fileURLs []string) error { if len(fileURLs) == 0 { return nil } var objects []*s3.ObjectIdentifier for _, url := range fileURLs { s3Key, err := s.extractS3Key(url) if err != nil { return err } objects = append(objects, &s3.ObjectIdentifier{ Key: aws.String(s3Key), }) } _, err := s.s3Client.DeleteObjects(&s3.DeleteObjectsInput{ Bucket: aws.String(s.bucket), Delete: &s3.Delete{ Objects: objects, Quiet: aws.Bool(false), }, }) if err != nil { return fmt.Errorf("failed to delete objects from S3: %w", err) } return nil } // CheckObjectExists проверяет существует ли объект в S3 func (s *s3Storage) CheckObjectExists(ctx context.Context, fileURL string) (bool, error) { s3Key, err := s.extractS3Key(fileURL) if err != nil { return false, err } _, err = s.s3Client.HeadObject(&s3.HeadObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(s3Key), }) if err != nil { if strings.Contains(err.Error(), "NotFound") { return false, nil } return false, fmt.Errorf("failed to check object existence: %w", err) } return true, nil }