package objectstorage import ( "context" "errors" "fmt" "io" "net/http" "strings" "time" "go.uber.org/zap" "github.com/geo-platform/tenant-api/internal/shared/config" ) var ErrNotConfigured = errors.New("object storage is not configured") var ErrObjectNotFound = errors.New("object storage object not found") var ErrObjectNotPublic = errors.New("object storage object is not publicly readable") var ErrUsageUnsupported = errors.New("object storage usage stat is not supported") type Client interface { Validate() error PutBytes(ctx context.Context, objectKey string, content []byte, contentType string) error GetBytes(ctx context.Context, objectKey string) ([]byte, error) Exists(ctx context.Context, objectKey string) (bool, error) Delete(ctx context.Context, objectKey string) error PublicURL(objectKey string) (string, error) DownloadURL(objectKey string) (string, error) } type ReaderClient interface { PutReader(ctx context.Context, objectKey string, reader io.Reader, size int64, contentType string) error } type ObjectReaderClient interface { GetReader(ctx context.Context, objectKey string) (io.ReadCloser, error) } type PresignedPutResult struct { URL string Headers http.Header ExpiresAt time.Time } type PresignedPutClient interface { PresignedPutURL(ctx context.Context, objectKey string, contentType string, contentMD5Base64 string, ttl time.Duration) (*PresignedPutResult, error) } type ManagementClient interface { Client List(ctx context.Context, in ListInput) (*ListResult, error) Stat(ctx context.Context, objectKey string) (*ObjectInfo, error) Copy(ctx context.Context, sourceKey, destinationKey string) error } type UsageClient interface { Usage(ctx context.Context) (*UsageInfo, error) } type PublicReadableClient interface { PublicReadableURL(objectKey string) (string, error) } type ListInput struct { Prefix string ContinuationToken string StartAfter string Delimiter string Limit int Recursive bool } type ListResult struct { Objects []ObjectInfo `json:"objects"` CommonPrefixes []string `json:"common_prefixes"` ContinuationToken string `json:"continuation_token,omitempty"` NextContinuationToken string `json:"next_continuation_token,omitempty"` IsTruncated bool `json:"is_truncated"` Prefix string `json:"prefix"` Delimiter string `json:"delimiter"` } type ObjectInfo struct { Key string `json:"key"` Size int64 `json:"size"` ETag string `json:"etag,omitempty"` LastModified time.Time `json:"last_modified"` ContentType string `json:"content_type,omitempty"` StorageClass string `json:"storage_class,omitempty"` } type UsageInfo struct { Available bool `json:"available"` Provider string `json:"provider"` StorageSize int64 `json:"storage_size"` ObjectCount int64 `json:"object_count"` } func New(cfg config.ObjectStorageConfig, logger *zap.Logger) Client { provider := strings.ToLower(strings.TrimSpace(cfg.Provider)) switch provider { case "", "disabled": return disabledClient{reason: "object storage is disabled"} case "minio", "mino", "r2", "cloudflare_r2", "cloudflare-r2", "s3", "aws_s3", "aws-s3", "custom", "custom_s3", "custom-s3": return NewMinIOClient(cfg, logger) case "aliyun", "aliyun_oss", "aliyun-oss", "oss": return NewAliyunClient(cfg, logger) default: return disabledClient{reason: fmt.Sprintf("unsupported object storage provider %q", cfg.Provider)} } } type disabledClient struct { reason string } func (c disabledClient) Validate() error { if c.reason == "" { c.reason = "missing object storage configuration" } return fmt.Errorf("%w: %s", ErrNotConfigured, c.reason) } func (c disabledClient) PutBytes(context.Context, string, []byte, string) error { return c.Validate() } func (c disabledClient) PutReader(context.Context, string, io.Reader, int64, string) error { return c.Validate() } func (c disabledClient) GetBytes(context.Context, string) ([]byte, error) { return nil, c.Validate() } func (c disabledClient) Exists(context.Context, string) (bool, error) { return false, c.Validate() } func (c disabledClient) Delete(context.Context, string) error { return c.Validate() } func (c disabledClient) PublicURL(string) (string, error) { return "", c.Validate() } func (c disabledClient) DownloadURL(string) (string, error) { return "", c.Validate() } func (c disabledClient) List(context.Context, ListInput) (*ListResult, error) { return nil, c.Validate() } func (c disabledClient) Stat(context.Context, string) (*ObjectInfo, error) { return nil, c.Validate() } func (c disabledClient) Copy(context.Context, string, string) error { return c.Validate() } func (c disabledClient) Usage(context.Context) (*UsageInfo, error) { return nil, c.Validate() }