persona-community-1/pkg/storage/gcs.go
jordan 4004f88f4a
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-23 10:20:59 +00:00

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()
}