220 lines
7.2 KiB
Go
220 lines
7.2 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"io/fs"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/spf13/afero"
|
|
"github.com/supabase/cli/pkg/fetcher"
|
|
)
|
|
|
|
type ListObjectsQuery struct {
|
|
Prefix string `json:"prefix"`
|
|
Search string `json:"search,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
Offset int `json:"offset,omitempty"`
|
|
}
|
|
|
|
type ObjectResponse struct {
|
|
Name string `json:"name"` // "abstract.pdf"
|
|
Id *string `json:"id"` // "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9"
|
|
UpdatedAt *string `json:"updated_at"` // "2023-10-13T18:08:22.068Z"
|
|
CreatedAt *string `json:"created_at"` // "2023-10-13T18:08:22.068Z"
|
|
LastAccessedAt *string `json:"last_accessed_at"` // "2023-10-13T18:08:22.068Z"
|
|
Metadata *ObjectMetadata `json:"metadata"` // null
|
|
}
|
|
|
|
type ObjectMetadata struct {
|
|
ETag string `json:"eTag"` // "\"887ea9be3c68e6f2fca7fd2d7c77d8fe\""
|
|
Size int `json:"size"` // 82702
|
|
Mimetype string `json:"mimetype"` // "application/pdf"
|
|
CacheControl string `json:"cacheControl"` // "max-age=3600"
|
|
LastModified string `json:"lastModified"` // "2023-10-13T18:08:22.000Z"
|
|
ContentLength int `json:"contentLength"` // 82702
|
|
HttpStatusCode int `json:"httpStatusCode"` // 200
|
|
}
|
|
|
|
func (s *StorageAPI) ListObjects(ctx context.Context, bucket, prefix string, page int) ([]ObjectResponse, error) {
|
|
dir, name := path.Split(prefix)
|
|
query := ListObjectsQuery{
|
|
Prefix: dir,
|
|
Search: name,
|
|
Limit: PAGE_LIMIT,
|
|
Offset: PAGE_LIMIT * page,
|
|
}
|
|
resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/list/"+bucket, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fetcher.ParseJSON[[]ObjectResponse](resp.Body)
|
|
}
|
|
|
|
type FileOptions struct {
|
|
CacheControl string
|
|
ContentType string
|
|
Overwrite bool
|
|
}
|
|
|
|
func ParseFileOptions(f fs.File, opts ...func(*FileOptions)) (*FileOptions, error) {
|
|
// Customise file options
|
|
fo := &FileOptions{}
|
|
for _, apply := range opts {
|
|
apply(fo)
|
|
}
|
|
// Use default value of storage-js: https://github.com/supabase/storage-js/blob/main/src/packages/StorageFileApi.ts#L22
|
|
if len(fo.CacheControl) == 0 {
|
|
fo.CacheControl = "max-age=3600"
|
|
}
|
|
// Decode mimetype
|
|
if len(fo.ContentType) == 0 {
|
|
header := io.LimitReader(f, 512)
|
|
buf, err := io.ReadAll(header)
|
|
if err != nil {
|
|
return nil, errors.Errorf("failed to read file: %w", err)
|
|
}
|
|
fo.ContentType = http.DetectContentType(buf)
|
|
if s, ok := f.(io.Seeker); !ok {
|
|
return nil, errors.Errorf("file is not seekable")
|
|
} else if _, err = s.Seek(0, io.SeekStart); err != nil {
|
|
return nil, errors.Errorf("failed to seek file: %w", err)
|
|
}
|
|
}
|
|
return fo, nil
|
|
}
|
|
|
|
func (s *StorageAPI) UploadObject(ctx context.Context, remotePath, localPath string, fsys fs.FS, opts ...func(*FileOptions)) error {
|
|
f, err := fsys.Open(localPath)
|
|
if err != nil {
|
|
return errors.Errorf("failed to open file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
fo, err := ParseFileOptions(f, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// For text/plain content types, we try to determine a more specific type
|
|
// based on the file extension, as the initial detection might be too generic
|
|
if strings.Contains(fo.ContentType, "text/plain") {
|
|
if extensionType := mime.TypeByExtension(filepath.Ext(localPath)); extensionType != "" {
|
|
fo.ContentType = extensionType
|
|
}
|
|
}
|
|
return s.UploadObjectStream(ctx, remotePath, f, *fo)
|
|
}
|
|
|
|
func (s *StorageAPI) UploadObjectStream(ctx context.Context, remotePath string, localFile io.Reader, fo FileOptions) error {
|
|
headers := func(req *http.Request) {
|
|
if len(fo.ContentType) > 0 {
|
|
req.Header.Add("Content-Type", fo.ContentType)
|
|
}
|
|
if len(fo.CacheControl) > 0 {
|
|
req.Header.Add("Cache-Control", fo.CacheControl)
|
|
}
|
|
if fo.Overwrite {
|
|
req.Header.Add("x-upsert", "true")
|
|
}
|
|
}
|
|
// Prepare request
|
|
remotePath = strings.TrimPrefix(remotePath, "/")
|
|
resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/"+remotePath, io.NopCloser(localFile), headers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (s *StorageAPI) DownloadObject(ctx context.Context, remotePath, localPath string, fsys afero.Fs) error {
|
|
f, err := fsys.OpenFile(localPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
|
|
if err != nil {
|
|
return errors.Errorf("failed to create file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
return s.DownloadObjectStream(ctx, remotePath, f)
|
|
}
|
|
|
|
func (s *StorageAPI) DownloadObjectStream(ctx context.Context, remotePath string, localFile io.Writer) error {
|
|
remotePath = strings.TrimPrefix(remotePath, "/")
|
|
resp, err := s.Send(ctx, http.MethodGet, "/storage/v1/object/"+remotePath, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
_, err = io.Copy(localFile, resp.Body)
|
|
return err
|
|
}
|
|
|
|
type MoveObjectRequest struct {
|
|
BucketId string `json:"bucketId"`
|
|
SourceKey string `json:"sourceKey"`
|
|
DestinationKey string `json:"destinationKey"`
|
|
}
|
|
|
|
type MoveObjectResponse = DeleteBucketResponse
|
|
|
|
func (s *StorageAPI) MoveObject(ctx context.Context, bucketId, srcPath, dstPath string) (MoveObjectResponse, error) {
|
|
body := MoveObjectRequest{
|
|
BucketId: bucketId,
|
|
SourceKey: srcPath,
|
|
DestinationKey: dstPath,
|
|
}
|
|
resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/move", body)
|
|
if err != nil {
|
|
return MoveObjectResponse{}, err
|
|
}
|
|
return fetcher.ParseJSON[MoveObjectResponse](resp.Body)
|
|
}
|
|
|
|
type CopyObjectRequest = MoveObjectRequest
|
|
|
|
type CopyObjectResponse struct {
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
func (s *StorageAPI) CopyObject(ctx context.Context, bucketId, srcPath, dstPath string) (CopyObjectResponse, error) {
|
|
body := CopyObjectRequest{
|
|
BucketId: bucketId,
|
|
SourceKey: srcPath,
|
|
DestinationKey: dstPath,
|
|
}
|
|
resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/copy", body)
|
|
if err != nil {
|
|
return CopyObjectResponse{}, err
|
|
}
|
|
return fetcher.ParseJSON[CopyObjectResponse](resp.Body)
|
|
}
|
|
|
|
type DeleteObjectsRequest struct {
|
|
Prefixes []string `json:"prefixes"`
|
|
}
|
|
|
|
type DeleteObjectsResponse struct {
|
|
BucketId string `json:"bucket_id"` // "private"
|
|
Owner string `json:"owner"` // ""
|
|
OwnerId string `json:"owner_id"` // ""
|
|
Version string `json:"version"` // "cf5c5c53-ee73-4806-84e3-7d92c954b436"
|
|
Name string `json:"name"` // "abstract.pdf"
|
|
Id string `json:"id"` // "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9"
|
|
UpdatedAt string `json:"updated_at"` // "2023-10-13T18:08:22.068Z"
|
|
CreatedAt string `json:"created_at"` // "2023-10-13T18:08:22.068Z"
|
|
LastAccessedAt string `json:"last_accessed_at"` // "2023-10-13T18:08:22.068Z"
|
|
Metadata ObjectMetadata `json:"metadata"` // null
|
|
}
|
|
|
|
func (s *StorageAPI) DeleteObjects(ctx context.Context, bucket string, prefixes []string) ([]DeleteObjectsResponse, error) {
|
|
body := DeleteObjectsRequest{Prefixes: prefixes}
|
|
resp, err := s.Send(ctx, http.MethodDelete, "/storage/v1/object/"+bucket, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fetcher.ParseJSON[[]DeleteObjectsResponse](resp.Body)
|
|
}
|