admin 57eba68496
Some checks failed
continuous-integration/drone/push Build is failing
v.0.0.1 Создан сервис клипов
2025-09-02 11:58:10 +03:00

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
}