126 lines
3.3 KiB
Go
126 lines
3.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
gcsstorage "cloud.google.com/go/storage"
|
|
"google.golang.org/api/iterator"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
// GCSStore implements Store using Google Cloud Storage.
|
|
type GCSStore struct {
|
|
client *gcsstorage.Client
|
|
bucket string
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewGCSStore creates a GCS-backed store.
|
|
// credentialsJSON may be empty to use Application Default Credentials.
|
|
func NewGCSStore(ctx context.Context, bucket string, credentialsJSON string, logger *slog.Logger) (*GCSStore, error) {
|
|
var opts []option.ClientOption
|
|
if credentialsJSON != "" {
|
|
opts = append(opts, option.WithCredentialsJSON([]byte(credentialsJSON)))
|
|
}
|
|
|
|
client, err := gcsstorage.NewClient(ctx, opts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: failed to create GCS client: %w", err)
|
|
}
|
|
|
|
return &GCSStore{
|
|
client: client,
|
|
bucket: bucket,
|
|
logger: logger,
|
|
}, nil
|
|
}
|
|
|
|
func (s *GCSStore) Upload(ctx context.Context, path string, data []byte, contentType string) (string, error) {
|
|
obj := s.client.Bucket(s.bucket).Object(path)
|
|
w := obj.NewWriter(ctx)
|
|
w.ContentType = contentType
|
|
|
|
if _, err := w.Write(data); err != nil {
|
|
_ = w.Close()
|
|
return "", fmt.Errorf("storage: write failed: %w", err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return "", fmt.Errorf("storage: close failed: %w", err)
|
|
}
|
|
|
|
// Return a signed URL consistent with GetURL (bucket is private).
|
|
return s.GetURL(ctx, path)
|
|
}
|
|
|
|
func (s *GCSStore) UploadPresigned(ctx context.Context, path string, contentType string) (*PresignedUpload, error) {
|
|
expires := time.Now().Add(15 * time.Minute)
|
|
url, err := s.client.Bucket(s.bucket).SignedURL(path, &gcsstorage.SignedURLOptions{
|
|
Method: "PUT",
|
|
ContentType: contentType,
|
|
Expires: expires,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: presigned URL failed: %w", err)
|
|
}
|
|
|
|
return &PresignedUpload{
|
|
URL: url,
|
|
Headers: map[string]string{"Content-Type": contentType},
|
|
Method: "PUT",
|
|
Expires: expires,
|
|
}, nil
|
|
}
|
|
|
|
func (s *GCSStore) GetURL(ctx context.Context, path string) (string, error) {
|
|
url, err := s.client.Bucket(s.bucket).SignedURL(path, &gcsstorage.SignedURLOptions{
|
|
Method: "GET",
|
|
Expires: time.Now().Add(1 * time.Hour),
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("storage: signed URL failed: %w", err)
|
|
}
|
|
return url, nil
|
|
}
|
|
|
|
func (s *GCSStore) Delete(ctx context.Context, path string) error {
|
|
if err := s.client.Bucket(s.bucket).Object(path).Delete(ctx); err != nil {
|
|
return fmt.Errorf("storage: delete failed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *GCSStore) List(ctx context.Context, prefix string) ([]MediaObject, error) {
|
|
it := s.client.Bucket(s.bucket).Objects(ctx, &gcsstorage.Query{Prefix: prefix})
|
|
var objects []MediaObject
|
|
for {
|
|
attrs, err := it.Next()
|
|
if err == iterator.Done {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: list failed: %w", err)
|
|
}
|
|
signedURL, urlErr := s.GetURL(ctx, attrs.Name)
|
|
if urlErr != nil {
|
|
s.logger.Warn("failed to sign URL for listed object", "path", attrs.Name, "error", urlErr)
|
|
continue
|
|
}
|
|
objects = append(objects, MediaObject{
|
|
Path: attrs.Name,
|
|
URL: signedURL,
|
|
ContentType: attrs.ContentType,
|
|
Size: attrs.Size,
|
|
CreatedAt: attrs.Created,
|
|
})
|
|
}
|
|
return objects, nil
|
|
}
|
|
|
|
// Close releases GCS client resources.
|
|
func (s *GCSStore) Close() error {
|
|
return s.client.Close()
|
|
}
|