241 lines
6.1 KiB
Go
241 lines
6.1 KiB
Go
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
|
|
}
|