package services import ( "bytes" "context" "fmt" "io" "mime/multipart" "path/filepath" "time" "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/google/uuid" ) type S3Service struct { client *s3.Client bucketName string } type S3Credentials struct { S3KeyId string S3AccessKey string } // NewS3Service initialise le client S3 pointant vers RustFS (accessible via VPN). func NewS3Service(region, bucketName, endpoint string, creds S3Credentials) (*S3Service, error) { var cfg aws.Config var err error if creds.S3KeyId != "" && creds.S3AccessKey != "" { cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithRegion(region), config.WithCredentialsProvider( credentials.NewStaticCredentialsProvider(creds.S3KeyId, creds.S3AccessKey, ""), ), ) } else { cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) } if err != nil { return nil, fmt.Errorf("erreur chargement config AWS: %w", err) } client := s3.NewFromConfig(cfg, func(o *s3.Options) { if endpoint != "" { o.BaseEndpoint = aws.String(endpoint) // ex: http://10.x.x.x:9000 (IP interne VPN de RustFS) o.UsePathStyle = true } }) return &S3Service{ client: client, bucketName: bucketName, }, nil } // UploadFile upload un fichier et renvoie sa clé S3 (pas d'URL publique, RustFS est privé). func (s *S3Service) UploadFile(fileHeader *multipart.FileHeader, folder string) (key string, err error) { ext := filepath.Ext(fileHeader.Filename) fileName := fmt.Sprintf("%s%s", uuid.New().String(), ext) return s.UploadFileWithName(fileHeader, folder, fileName) } // UploadFileWithName upload un fichier avec un nom déjà déterminé et renvoie la clé S3. func (s *S3Service) UploadFileWithName(fileHeader *multipart.FileHeader, folder, fileName string) (key string, err error) { file, err := fileHeader.Open() if err != nil { return "", fmt.Errorf("erreur ouverture fichier: %w", err) } defer file.Close() buf := bytes.NewBuffer(nil) if _, err := buf.ReadFrom(file); err != nil { return "", fmt.Errorf("erreur lecture fichier: %w", err) } key = fmt.Sprintf("%s/%s", folder, fileName) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() contentType := fileHeader.Header.Get("Content-Type") if contentType == "" { contentType = "application/octet-stream" } _, err = s.client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(s.bucketName), Key: aws.String(key), Body: bytes.NewReader(buf.Bytes()), ContentType: aws.String(contentType), }) if err != nil { return "", fmt.Errorf("erreur upload RustFS: %w", err) } return key, nil } // GetFile récupère un objet depuis RustFS (stream + content-type) pour le proxy. // Le contexte doit rester actif pendant toute la lecture du body par l'appelant. func (s *S3Service) GetFile(ctx context.Context, key string) (io.ReadCloser, string, error) { out, err := s.client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(s.bucketName), Key: aws.String(key), }) if err != nil { return nil, "", fmt.Errorf("erreur lecture RustFS: %w", err) } contentType := "application/octet-stream" if out.ContentType != nil { contentType = *out.ContentType } return out.Body, contentType, nil } // DeleteFile supprime un fichier à partir de sa clé S3. func (s *S3Service) DeleteFile(key string) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: aws.String(s.bucketName), Key: aws.String(key), }) if err != nil { return fmt.Errorf("erreur suppression RustFS: %w", err) } return nil }