mirror of
https://github.com/Sosokker/go-chi-oapi-codegen-todolist.git
synced 2025-12-19 05:54:07 +01:00
95 lines
3.2 KiB
Go
95 lines
3.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"mime"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"cloud.google.com/go/storage"
|
|
"github.com/Sosokker/todolist-backend/internal/config"
|
|
"github.com/google/uuid"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
type gcsStorageService struct {
|
|
bucket string
|
|
client *storage.Client
|
|
logger *slog.Logger
|
|
baseDir string
|
|
}
|
|
|
|
func NewGCStorageService(cfg config.GCSStorageConfig, logger *slog.Logger) (FileStorageService, error) {
|
|
opts := []option.ClientOption{}
|
|
if cfg.CredentialsFile != "" {
|
|
opts = append(opts, option.WithCredentialsFile(cfg.CredentialsFile))
|
|
}
|
|
client, err := storage.NewClient(context.Background(), opts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCS client: %w", err)
|
|
}
|
|
return &gcsStorageService{
|
|
bucket: cfg.BucketName,
|
|
client: client,
|
|
logger: logger.With("service", "gcsstorage"),
|
|
baseDir: cfg.BaseDir,
|
|
}, nil
|
|
}
|
|
|
|
func (s *gcsStorageService) GenerateUniqueObjectName(originalFilename string) string {
|
|
ext := filepath.Ext(originalFilename)
|
|
return uuid.NewString() + ext
|
|
}
|
|
|
|
func (s *gcsStorageService) Upload(ctx context.Context, userID, todoID uuid.UUID, originalFilename string, reader io.Reader, size int64) (string, string, error) {
|
|
objectName := filepath.Join(s.baseDir, userID.String(), todoID.String(), s.GenerateUniqueObjectName(originalFilename))
|
|
wc := s.client.Bucket(s.bucket).Object(objectName).NewWriter(ctx)
|
|
wc.ContentType = mime.TypeByExtension(filepath.Ext(originalFilename))
|
|
wc.ChunkSize = 0
|
|
written, err := io.Copy(wc, reader)
|
|
if err != nil {
|
|
wc.Close()
|
|
s.logger.ErrorContext(ctx, "Failed to upload to GCS", "error", err, "object", objectName)
|
|
return "", "", fmt.Errorf("failed to upload to GCS: %w", err)
|
|
}
|
|
if written != size {
|
|
wc.Close()
|
|
s.logger.WarnContext(ctx, "File size mismatch during GCS upload", "expected", size, "written", written, "object", objectName)
|
|
return "", "", fmt.Errorf("file size mismatch during upload")
|
|
}
|
|
if err := wc.Close(); err != nil {
|
|
s.logger.ErrorContext(ctx, "Failed to finalize GCS upload", "error", err, "object", objectName)
|
|
return "", "", fmt.Errorf("failed to finalize upload: %w", err)
|
|
}
|
|
contentType := wc.ContentType
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
return objectName, contentType, nil
|
|
}
|
|
|
|
func (s *gcsStorageService) Delete(ctx context.Context, storageID string) error {
|
|
objectName := filepath.Clean(storageID)
|
|
if strings.Contains(objectName, "..") {
|
|
s.logger.WarnContext(ctx, "Attempted directory traversal in GCS delete", "storageId", storageID)
|
|
return fmt.Errorf("invalid storage ID")
|
|
}
|
|
o := s.client.Bucket(s.bucket).Object(objectName)
|
|
err := o.Delete(ctx)
|
|
if err != nil && err != storage.ErrObjectNotExist {
|
|
s.logger.ErrorContext(ctx, "Failed to delete GCS object", "error", err, "storageId", storageID)
|
|
return fmt.Errorf("could not delete GCS object: %w", err)
|
|
}
|
|
s.logger.InfoContext(ctx, "GCS object deleted", "storageId", storageID)
|
|
return nil
|
|
}
|
|
|
|
func (s *gcsStorageService) GetURL(ctx context.Context, storageID string) (string, error) {
|
|
objectName := filepath.Clean(storageID)
|
|
url := fmt.Sprintf("https://storage.googleapis.com/%s/%s", s.bucket, objectName)
|
|
return url, nil
|
|
}
|