mirror of
https://github.com/hanzoui/registry-backend.git
synced 2026-10-30 20:30:45 +00:00
* Fix to respect environment specific cloud storage * fix integration test --------- Co-authored-by: James Kwon <[email protected]>
150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
|
|
"registry-backend/config"
|
|
"registry-backend/tracing"
|
|
|
|
"cloud.google.com/go/storage"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// StorageService defines the interface for interacting with cloud storage.
|
|
type StorageService interface {
|
|
UploadFile(ctx context.Context, bucket, object, filePath string) (string, error)
|
|
GetFileUrl(ctx context.Context, bucketName, objectPath string) (string, error)
|
|
GenerateSignedURL(bucketName, objectName string) (string, error)
|
|
}
|
|
|
|
// Ensure storageService struct implements StorageService interface
|
|
var _ StorageService = (*storageService)(nil)
|
|
|
|
// storageService struct holds the GCP storage client and configuration.
|
|
type storageService struct {
|
|
client *storage.Client
|
|
config *config.Config
|
|
}
|
|
|
|
// NewStorageService creates a new storage service using the provided config or returns a noop implementation if the config is missing.
|
|
func NewStorageService(cfg *config.Config) (StorageService, error) {
|
|
if cfg == nil {
|
|
// Return a noop implementation if config is nil or storage is not enabled
|
|
log.Info().Msg("No storage configuration found, using noop implementation")
|
|
return &storageNoop{}, nil
|
|
}
|
|
|
|
// Initialize GCP storage client
|
|
client, err := storage.NewClient(context.Background())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("NewStorageClient: %v", err)
|
|
}
|
|
|
|
return &storageService{
|
|
client: client,
|
|
config: cfg,
|
|
}, nil
|
|
}
|
|
|
|
// UploadFile uploads an object to GCP storage.
|
|
func (s *storageService) UploadFile(ctx context.Context, bucket, object, filePath string) (string, error) {
|
|
defer tracing.TraceDefaultSegment(ctx, "StorageService.UploadFile")()
|
|
|
|
log.Ctx(ctx).Info().Msgf("Uploading %v to %v/%v.\n", filePath, bucket, object)
|
|
|
|
// Open local file
|
|
f, err := os.Open(filePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("os.Open: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, time.Second*50)
|
|
defer cancel()
|
|
|
|
o := s.client.Bucket(bucket).Object(object)
|
|
|
|
// Ensure that we don't overwrite an existing object.
|
|
o = o.If(storage.Conditions{DoesNotExist: true})
|
|
|
|
// Upload the file to the cloud
|
|
wc := o.NewWriter(ctx)
|
|
if _, err := io.Copy(wc, f); err != nil {
|
|
return "", fmt.Errorf("io.Copy: %w", err)
|
|
}
|
|
if err := wc.Close(); err != nil {
|
|
return "", fmt.Errorf("Writer.Close: %w", err)
|
|
}
|
|
|
|
log.Ctx(ctx).Info().Msgf("Blob %v uploaded.\n", object)
|
|
|
|
// Set the ACL to make the file publicly accessible
|
|
if err := o.ACL().Set(ctx, storage.AllUsers, storage.RoleReader); err != nil {
|
|
return "", fmt.Errorf("ACL().Set: %w", err)
|
|
}
|
|
|
|
// Construct the public URL
|
|
publicURL := fmt.Sprintf("https://storage.googleapis.com/%s/%s", bucket, object)
|
|
log.Ctx(ctx).Info().Msgf("Blob is publicly accessible at %v.\n", publicURL)
|
|
return publicURL, nil
|
|
}
|
|
|
|
// GetFileUrl gets the public URL of a file from GCP storage.
|
|
func (s *storageService) GetFileUrl(ctx context.Context, bucketName, objectPath string) (string, error) {
|
|
defer tracing.TraceDefaultSegment(ctx, "StorageService.GetFileUrl")()
|
|
|
|
// Get the public URL of a file in a bucket
|
|
attrs, err := s.client.Bucket(bucketName).Object(objectPath).Attrs(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("object.Attrs: %w", err)
|
|
}
|
|
|
|
publicURL := attrs.MediaLink
|
|
log.Ctx(ctx).Info().Msgf("Public URL: %v", publicURL)
|
|
return publicURL, nil
|
|
}
|
|
|
|
// GenerateSignedURL generates a signed URL for uploading to GCP storage.
|
|
func (s *storageService) GenerateSignedURL(bucketName, objectName string) (string, error) {
|
|
expires := time.Now().Add(15 * time.Minute)
|
|
url, err := s.client.Bucket(bucketName).SignedURL(objectName, &storage.SignedURLOptions{
|
|
ContentType: "application/zip",
|
|
Method: "PUT",
|
|
Expires: expires,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return url, nil
|
|
}
|
|
|
|
// storageNoop is a noop implementation of the StorageService interface.
|
|
type storageNoop struct{}
|
|
|
|
// Implement all StorageService methods for noop behavior.
|
|
|
|
func (s *storageNoop) UploadFile(ctx context.Context, bucket, object, filePath string) (string, error) {
|
|
// No-op, return nil to avoid side-effects
|
|
return "", nil
|
|
}
|
|
|
|
func (s *storageNoop) StreamFileUpload(w io.Writer, objectName, blob string) (string, string, error) {
|
|
// No-op, return nil to avoid side-effects
|
|
return "", "", nil
|
|
}
|
|
|
|
func (s *storageNoop) GetFileUrl(ctx context.Context, bucketName, objectPath string) (string, error) {
|
|
// No-op, return empty string and nil error
|
|
return "", nil
|
|
}
|
|
|
|
func (s *storageNoop) GenerateSignedURL(bucketName, objectName string) (string, error) {
|
|
// No-op, return empty string and nil error
|
|
return "", nil
|
|
}
|