Files
shorebird-server/internal/storage/storage.go
T
2026-06-12 04:22:58 +08:00

109 lines
3.3 KiB
Go

package storage
import (
"context"
"fmt"
"io"
"net/url"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// Service handles object storage operations (MinIO / S3 compatible).
type Service struct {
client *minio.Client
releaseBucket string
patchBucket string
}
// NewService creates a new storage service.
func NewService(endpoint, accessKeyID, secretAccessKey, releaseBucket, patchBucket string, useSSL bool) (*Service, error) {
client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: useSSL,
})
if err != nil {
return nil, fmt.Errorf("failed to create minio client: %w", err)
}
return &Service{
client: client,
releaseBucket: releaseBucket,
patchBucket: patchBucket,
}, nil
}
// GeneratePresignedUploadURL creates a presigned URL for uploading an object.
func (s *Service) GeneratePresignedUploadURL(ctx context.Context, objectKey string, isPublic bool, expiry time.Duration) (string, error) {
bucket := s.releaseBucket
if isPublic {
bucket = s.patchBucket
}
u, err := s.client.PresignedPutObject(ctx, bucket, objectKey, expiry)
if err != nil {
return "", fmt.Errorf("failed to generate presigned URL: %w", err)
}
return u.String(), nil
}
// GeneratePublicDownloadURL creates a public download URL for a patch artifact.
func (s *Service) GeneratePublicDownloadURL(ctx context.Context, objectKey string) (string, error) {
// For MinIO, construct the public URL directly
// In production with a CDN, this would be a CDN URL
endpoint := s.client.EndpointURL()
return fmt.Sprintf("%s/%s/%s", endpoint.String(), s.patchBucket, objectKey), nil
}
// UploadObject directly uploads data to storage.
func (s *Service) UploadObject(ctx context.Context, objectKey string, reader io.Reader, size int64, contentType string, isPublic bool) error {
bucket := s.releaseBucket
if isPublic {
bucket = s.patchBucket
}
opts := minio.PutObjectOptions{
ContentType: contentType,
}
if size > 0 {
// Let minio handle unknown size
}
_, err := s.client.PutObject(ctx, bucket, objectKey, reader, size, opts)
if err != nil {
return fmt.Errorf("failed to upload object: %w", err)
}
return nil
}
// GetObject retrieves an object from storage.
func (s *Service) GetObject(ctx context.Context, objectKey string, isPublic bool) (io.ReadCloser, error) {
bucket := s.releaseBucket
if isPublic {
bucket = s.patchBucket
}
obj, err := s.client.GetObject(ctx, bucket, objectKey, minio.GetObjectOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get object: %w", err)
}
return obj, nil
}
// GenerateReleaseStorageKey creates a standardized storage key for releases.
func GenerateReleaseStorageKey(appID, version, platform, arch, filename string) string {
return fmt.Sprintf("apps/%s/releases/%s/%s/%s/%s", appID, version, platform, arch, filename)
}
// GeneratePatchStorageKey creates a standardized storage key for patches.
func GeneratePatchStorageKey(appID string, releaseID int, patchNumber int, platform, arch, filename string) string {
return fmt.Sprintf("apps/%s/releases/%d/patches/%d/%s/%s/%s", appID, releaseID, patchNumber, platform, arch, filename)
}
// PresignedURL returns a parsed presigned URL.
func PresignedURL(raw string) (*url.URL, error) {
return url.Parse(raw)
}