537 lines
15 KiB
Go
537 lines
15 KiB
Go
|
|
package services
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/url"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/minio/minio-go/v7"
|
||
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||
|
|
"go.etcd.io/bbolt"
|
||
|
|
)
|
||
|
|
|
||
|
|
var storageBackendsBucket = []byte("storage_backends")
|
||
|
|
|
||
|
|
const (
|
||
|
|
StorageBackendLocal = "local"
|
||
|
|
StorageBackendS3 = "s3"
|
||
|
|
|
||
|
|
StorageProviderS3 = "s3"
|
||
|
|
StorageProviderContabo = "contabo"
|
||
|
|
)
|
||
|
|
|
||
|
|
type StorageObject struct {
|
||
|
|
Key string
|
||
|
|
Size int64
|
||
|
|
ContentType string
|
||
|
|
ModTime time.Time
|
||
|
|
Body io.ReadCloser
|
||
|
|
}
|
||
|
|
|
||
|
|
type StorageBackend interface {
|
||
|
|
ID() string
|
||
|
|
Type() string
|
||
|
|
Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) error
|
||
|
|
Get(ctx context.Context, key string) (StorageObject, error)
|
||
|
|
Delete(ctx context.Context, key string) error
|
||
|
|
DeletePrefix(ctx context.Context, prefix string) error
|
||
|
|
Usage(ctx context.Context) (int64, error)
|
||
|
|
Test(ctx context.Context) error
|
||
|
|
}
|
||
|
|
|
||
|
|
type StorageBackendConfig struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
Type string `json:"type"`
|
||
|
|
Provider string `json:"provider,omitempty"`
|
||
|
|
Enabled bool `json:"enabled"`
|
||
|
|
LocalPath string `json:"localPath,omitempty"`
|
||
|
|
Endpoint string `json:"endpoint,omitempty"`
|
||
|
|
Region string `json:"region,omitempty"`
|
||
|
|
Bucket string `json:"bucket,omitempty"`
|
||
|
|
AccessKey string `json:"accessKey,omitempty"`
|
||
|
|
SecretKey string `json:"secretKey,omitempty"`
|
||
|
|
UseSSL bool `json:"useSsl,omitempty"`
|
||
|
|
PathStyle bool `json:"pathStyle,omitempty"`
|
||
|
|
CreatedAt time.Time `json:"createdAt"`
|
||
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
||
|
|
LastTestedAt time.Time `json:"lastTestedAt,omitempty"`
|
||
|
|
LastTestError string `json:"lastTestError,omitempty"`
|
||
|
|
LastTestSuccess bool `json:"lastTestSuccess,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type StorageBackendView struct {
|
||
|
|
Config StorageBackendConfig
|
||
|
|
UsageBytes int64
|
||
|
|
UsageLabel string
|
||
|
|
InUse bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type StorageService struct {
|
||
|
|
db *bbolt.DB
|
||
|
|
localFilesDir string
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewStorageService(db *bbolt.DB, dataDir string) (*StorageService, error) {
|
||
|
|
filesDir := filepath.Join(dataDir, "files")
|
||
|
|
if err := os.MkdirAll(filesDir, 0o755); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
service := &StorageService{db: db, localFilesDir: filesDir}
|
||
|
|
err := db.Update(func(tx *bbolt.Tx) error {
|
||
|
|
_, err := tx.CreateBucketIfNotExists(storageBackendsBucket)
|
||
|
|
return err
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return service, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) LocalFilesDir() string {
|
||
|
|
return s.localFilesDir
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) Backend(id string) (StorageBackend, error) {
|
||
|
|
cfg, err := s.BackendConfig(id)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if !cfg.Enabled {
|
||
|
|
return nil, fmt.Errorf("storage backend is disabled")
|
||
|
|
}
|
||
|
|
return s.backendFromConfig(cfg)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) BackendConfig(id string) (StorageBackendConfig, error) {
|
||
|
|
id = strings.TrimSpace(id)
|
||
|
|
if id == "" || id == StorageBackendLocal {
|
||
|
|
return s.localConfig(), nil
|
||
|
|
}
|
||
|
|
var cfg StorageBackendConfig
|
||
|
|
err := s.db.View(func(tx *bbolt.Tx) error {
|
||
|
|
data := tx.Bucket(storageBackendsBucket).Get([]byte(id))
|
||
|
|
if data == nil {
|
||
|
|
return os.ErrNotExist
|
||
|
|
}
|
||
|
|
return json.Unmarshal(data, &cfg)
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return StorageBackendConfig{}, err
|
||
|
|
}
|
||
|
|
return cfg, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) ListBackendConfigs() ([]StorageBackendConfig, error) {
|
||
|
|
configs := []StorageBackendConfig{s.localConfig()}
|
||
|
|
err := s.db.View(func(tx *bbolt.Tx) error {
|
||
|
|
return tx.Bucket(storageBackendsBucket).ForEach(func(_, value []byte) error {
|
||
|
|
var cfg StorageBackendConfig
|
||
|
|
if err := json.Unmarshal(value, &cfg); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
configs = append(configs, cfg)
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
})
|
||
|
|
sort.Slice(configs, func(i, j int) bool {
|
||
|
|
if configs[i].ID == StorageBackendLocal {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
if configs[j].ID == StorageBackendLocal {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return strings.ToLower(configs[i].Name) < strings.ToLower(configs[j].Name)
|
||
|
|
})
|
||
|
|
return configs, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) CreateS3Backend(input StorageBackendConfig) (StorageBackendConfig, error) {
|
||
|
|
input.ID = randomID(10)
|
||
|
|
input.Type = StorageBackendS3
|
||
|
|
input.Provider = normalizeStorageProvider(input.Provider)
|
||
|
|
if input.Provider == StorageProviderContabo {
|
||
|
|
input.UseSSL = true
|
||
|
|
input.PathStyle = true
|
||
|
|
}
|
||
|
|
input.Name = strings.TrimSpace(input.Name)
|
||
|
|
input.Endpoint = strings.TrimSpace(input.Endpoint)
|
||
|
|
input.Region = strings.TrimSpace(input.Region)
|
||
|
|
input.Bucket = strings.TrimSpace(input.Bucket)
|
||
|
|
input.AccessKey = strings.TrimSpace(input.AccessKey)
|
||
|
|
input.SecretKey = strings.TrimSpace(input.SecretKey)
|
||
|
|
if input.Name == "" {
|
||
|
|
input.Name = input.Bucket
|
||
|
|
}
|
||
|
|
if input.Name == "" || input.Endpoint == "" || input.Bucket == "" || input.AccessKey == "" || input.SecretKey == "" {
|
||
|
|
return StorageBackendConfig{}, fmt.Errorf("name, endpoint, bucket, access key, and secret key are required")
|
||
|
|
}
|
||
|
|
now := time.Now().UTC()
|
||
|
|
input.Enabled = true
|
||
|
|
input.CreatedAt = now
|
||
|
|
input.UpdatedAt = now
|
||
|
|
if err := s.SaveBackendConfig(input); err != nil {
|
||
|
|
return StorageBackendConfig{}, err
|
||
|
|
}
|
||
|
|
return input, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) UpdateS3Backend(id string, input StorageBackendConfig) (StorageBackendConfig, error) {
|
||
|
|
current, err := s.BackendConfig(id)
|
||
|
|
if err != nil {
|
||
|
|
return StorageBackendConfig{}, err
|
||
|
|
}
|
||
|
|
if current.ID == StorageBackendLocal || current.Type != StorageBackendS3 {
|
||
|
|
return StorageBackendConfig{}, fmt.Errorf("only S3-compatible storage can be edited")
|
||
|
|
}
|
||
|
|
|
||
|
|
current.Provider = normalizeStorageProvider(input.Provider)
|
||
|
|
if current.Provider == StorageProviderContabo {
|
||
|
|
input.UseSSL = true
|
||
|
|
input.PathStyle = true
|
||
|
|
}
|
||
|
|
current.Name = strings.TrimSpace(input.Name)
|
||
|
|
current.Endpoint = strings.TrimSpace(input.Endpoint)
|
||
|
|
current.Region = strings.TrimSpace(input.Region)
|
||
|
|
current.Bucket = strings.TrimSpace(input.Bucket)
|
||
|
|
current.AccessKey = strings.TrimSpace(input.AccessKey)
|
||
|
|
if strings.TrimSpace(input.SecretKey) != "" {
|
||
|
|
current.SecretKey = strings.TrimSpace(input.SecretKey)
|
||
|
|
}
|
||
|
|
current.UseSSL = input.UseSSL
|
||
|
|
current.PathStyle = input.PathStyle
|
||
|
|
if current.Name == "" {
|
||
|
|
current.Name = current.Bucket
|
||
|
|
}
|
||
|
|
if current.Name == "" || current.Endpoint == "" || current.Bucket == "" || current.AccessKey == "" || current.SecretKey == "" {
|
||
|
|
return StorageBackendConfig{}, fmt.Errorf("name, endpoint, bucket, access key, and secret key are required")
|
||
|
|
}
|
||
|
|
if err := s.SaveBackendConfig(current); err != nil {
|
||
|
|
return StorageBackendConfig{}, err
|
||
|
|
}
|
||
|
|
return current, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) SaveBackendConfig(cfg StorageBackendConfig) error {
|
||
|
|
if cfg.ID == "" || cfg.ID == StorageBackendLocal {
|
||
|
|
return fmt.Errorf("invalid storage backend id")
|
||
|
|
}
|
||
|
|
cfg.UpdatedAt = time.Now().UTC()
|
||
|
|
data, err := json.Marshal(cfg)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return s.db.Update(func(tx *bbolt.Tx) error {
|
||
|
|
return tx.Bucket(storageBackendsBucket).Put([]byte(cfg.ID), data)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) DisableBackend(id string, inUse bool) error {
|
||
|
|
if id == "" || id == StorageBackendLocal {
|
||
|
|
return fmt.Errorf("local storage cannot be disabled")
|
||
|
|
}
|
||
|
|
if inUse {
|
||
|
|
return fmt.Errorf("storage backend is in use")
|
||
|
|
}
|
||
|
|
cfg, err := s.BackendConfig(id)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
cfg.Enabled = false
|
||
|
|
return s.SaveBackendConfig(cfg)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) DeleteBackend(id string, inUse bool) error {
|
||
|
|
if id == "" || id == StorageBackendLocal {
|
||
|
|
return fmt.Errorf("local storage cannot be deleted")
|
||
|
|
}
|
||
|
|
if inUse {
|
||
|
|
return fmt.Errorf("storage backend is in use")
|
||
|
|
}
|
||
|
|
return s.db.Update(func(tx *bbolt.Tx) error {
|
||
|
|
return tx.Bucket(storageBackendsBucket).Delete([]byte(id))
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) TestBackend(id string) (StorageBackendConfig, error) {
|
||
|
|
cfg, err := s.BackendConfig(id)
|
||
|
|
if err != nil {
|
||
|
|
return StorageBackendConfig{}, err
|
||
|
|
}
|
||
|
|
backend, err := s.backendFromConfig(cfg)
|
||
|
|
if err != nil {
|
||
|
|
return StorageBackendConfig{}, err
|
||
|
|
}
|
||
|
|
err = backend.Test(context.Background())
|
||
|
|
cfg.LastTestedAt = time.Now().UTC()
|
||
|
|
cfg.LastTestError = ""
|
||
|
|
cfg.LastTestSuccess = err == nil
|
||
|
|
if err != nil {
|
||
|
|
cfg.LastTestError = err.Error()
|
||
|
|
}
|
||
|
|
if cfg.ID != StorageBackendLocal {
|
||
|
|
_ = s.SaveBackendConfig(cfg)
|
||
|
|
}
|
||
|
|
return cfg, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) backendFromConfig(cfg StorageBackendConfig) (StorageBackend, error) {
|
||
|
|
switch cfg.Type {
|
||
|
|
case StorageBackendLocal:
|
||
|
|
return localStorageBackend{id: cfg.ID, root: cfg.LocalPath}, nil
|
||
|
|
case StorageBackendS3:
|
||
|
|
return newS3StorageBackend(cfg)
|
||
|
|
default:
|
||
|
|
return nil, fmt.Errorf("unsupported storage backend type %q", cfg.Type)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *StorageService) localConfig() StorageBackendConfig {
|
||
|
|
now := time.Now().UTC()
|
||
|
|
return StorageBackendConfig{
|
||
|
|
ID: StorageBackendLocal,
|
||
|
|
Name: "Local files",
|
||
|
|
Type: StorageBackendLocal,
|
||
|
|
Provider: StorageBackendLocal,
|
||
|
|
Enabled: true,
|
||
|
|
LocalPath: s.localFilesDir,
|
||
|
|
CreatedAt: now,
|
||
|
|
UpdatedAt: now,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type localStorageBackend struct {
|
||
|
|
id string
|
||
|
|
root string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b localStorageBackend) ID() string { return b.id }
|
||
|
|
func (b localStorageBackend) Type() string { return StorageBackendLocal }
|
||
|
|
|
||
|
|
func (b localStorageBackend) Put(_ context.Context, key string, body io.Reader, _ int64, _ string) error {
|
||
|
|
path, err := b.path(key)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
target, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer target.Close()
|
||
|
|
_, err = io.Copy(target, body)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b localStorageBackend) Get(_ context.Context, key string) (StorageObject, error) {
|
||
|
|
path, err := b.path(key)
|
||
|
|
if err != nil {
|
||
|
|
return StorageObject{}, err
|
||
|
|
}
|
||
|
|
source, err := os.Open(path)
|
||
|
|
if err != nil {
|
||
|
|
return StorageObject{}, err
|
||
|
|
}
|
||
|
|
stat, err := source.Stat()
|
||
|
|
if err != nil {
|
||
|
|
source.Close()
|
||
|
|
return StorageObject{}, err
|
||
|
|
}
|
||
|
|
return StorageObject{Key: key, Size: stat.Size(), ModTime: stat.ModTime(), Body: source}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b localStorageBackend) Delete(_ context.Context, key string) error {
|
||
|
|
path, err := b.path(key)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b localStorageBackend) DeletePrefix(_ context.Context, prefix string) error {
|
||
|
|
path, err := b.path(prefix)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := os.RemoveAll(path); err != nil && !os.IsNotExist(err) {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b localStorageBackend) Usage(_ context.Context) (int64, error) {
|
||
|
|
var total int64
|
||
|
|
err := filepath.WalkDir(b.root, func(path string, entry os.DirEntry, err error) error {
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if entry.IsDir() {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
info, err := entry.Info()
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
total += info.Size()
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return 0, nil
|
||
|
|
}
|
||
|
|
return total, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b localStorageBackend) Test(ctx context.Context) error {
|
||
|
|
key := ".warpbox-storage-test-" + randomID(6)
|
||
|
|
if err := b.Put(ctx, key, strings.NewReader("ok"), 2, "text/plain"); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return b.Delete(ctx, key)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b localStorageBackend) path(key string) (string, error) {
|
||
|
|
key = filepath.Clean(strings.TrimPrefix(key, "/"))
|
||
|
|
if key == "." || strings.HasPrefix(key, "..") || filepath.IsAbs(key) {
|
||
|
|
return "", fmt.Errorf("invalid storage key")
|
||
|
|
}
|
||
|
|
path := filepath.Join(b.root, key)
|
||
|
|
root, err := filepath.Abs(b.root)
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
abs, err := filepath.Abs(path)
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
if abs != root && !strings.HasPrefix(abs, root+string(os.PathSeparator)) {
|
||
|
|
return "", fmt.Errorf("invalid storage key")
|
||
|
|
}
|
||
|
|
return abs, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type s3StorageBackend struct {
|
||
|
|
cfg StorageBackendConfig
|
||
|
|
client *minio.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
func newS3StorageBackend(cfg StorageBackendConfig) (*s3StorageBackend, error) {
|
||
|
|
endpoint := normalizeS3Endpoint(cfg.Endpoint)
|
||
|
|
client, err := minio.New(endpoint, &minio.Options{
|
||
|
|
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
|
||
|
|
Secure: cfg.UseSSL,
|
||
|
|
Region: cfg.Region,
|
||
|
|
BucketLookup: s3BucketLookup(cfg.PathStyle),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return &s3StorageBackend{cfg: cfg, client: client}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b *s3StorageBackend) ID() string { return b.cfg.ID }
|
||
|
|
func (b *s3StorageBackend) Type() string { return StorageBackendS3 }
|
||
|
|
|
||
|
|
func (b *s3StorageBackend) Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) error {
|
||
|
|
opts := minio.PutObjectOptions{ContentType: contentType}
|
||
|
|
_, err := b.client.PutObject(ctx, b.cfg.Bucket, cleanObjectKey(key), body, size, opts)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b *s3StorageBackend) Get(ctx context.Context, key string) (StorageObject, error) {
|
||
|
|
object, err := b.client.GetObject(ctx, b.cfg.Bucket, cleanObjectKey(key), minio.GetObjectOptions{})
|
||
|
|
if err != nil {
|
||
|
|
return StorageObject{}, err
|
||
|
|
}
|
||
|
|
info, err := object.Stat()
|
||
|
|
if err != nil {
|
||
|
|
object.Close()
|
||
|
|
return StorageObject{}, err
|
||
|
|
}
|
||
|
|
return StorageObject{Key: key, Size: info.Size, ContentType: info.ContentType, ModTime: info.LastModified, Body: object}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b *s3StorageBackend) Delete(ctx context.Context, key string) error {
|
||
|
|
return b.client.RemoveObject(ctx, b.cfg.Bucket, cleanObjectKey(key), minio.RemoveObjectOptions{})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b *s3StorageBackend) DeletePrefix(ctx context.Context, prefix string) error {
|
||
|
|
prefix = strings.TrimSuffix(cleanObjectKey(prefix), "/") + "/"
|
||
|
|
objects := b.client.ListObjects(ctx, b.cfg.Bucket, minio.ListObjectsOptions{Prefix: prefix, Recursive: true})
|
||
|
|
for object := range objects {
|
||
|
|
if object.Err != nil {
|
||
|
|
return object.Err
|
||
|
|
}
|
||
|
|
if err := b.Delete(ctx, object.Key); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b *s3StorageBackend) Usage(ctx context.Context) (int64, error) {
|
||
|
|
var total int64
|
||
|
|
for object := range b.client.ListObjects(ctx, b.cfg.Bucket, minio.ListObjectsOptions{Recursive: true}) {
|
||
|
|
if object.Err != nil {
|
||
|
|
return 0, object.Err
|
||
|
|
}
|
||
|
|
total += object.Size
|
||
|
|
}
|
||
|
|
return total, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b *s3StorageBackend) Test(ctx context.Context) error {
|
||
|
|
exists, err := b.client.BucketExists(ctx, b.cfg.Bucket)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if !exists {
|
||
|
|
return fmt.Errorf("bucket %q does not exist", b.cfg.Bucket)
|
||
|
|
}
|
||
|
|
key := ".warpbox-storage-test-" + randomID(6)
|
||
|
|
if err := b.Put(ctx, key, bytes.NewReader([]byte("ok")), 2, "text/plain"); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return b.Delete(ctx, key)
|
||
|
|
}
|
||
|
|
|
||
|
|
func s3BucketLookup(pathStyle bool) minio.BucketLookupType {
|
||
|
|
if pathStyle {
|
||
|
|
return minio.BucketLookupPath
|
||
|
|
}
|
||
|
|
return minio.BucketLookupAuto
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeS3Endpoint(endpoint string) string {
|
||
|
|
endpoint = strings.TrimSpace(endpoint)
|
||
|
|
if parsed, err := url.Parse(endpoint); err == nil && parsed.Host != "" {
|
||
|
|
return parsed.Host
|
||
|
|
}
|
||
|
|
return strings.TrimPrefix(strings.TrimPrefix(endpoint, "https://"), "http://")
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeStorageProvider(provider string) string {
|
||
|
|
switch strings.TrimSpace(provider) {
|
||
|
|
case StorageProviderContabo:
|
||
|
|
return StorageProviderContabo
|
||
|
|
default:
|
||
|
|
return StorageProviderS3
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func cleanObjectKey(key string) string {
|
||
|
|
return strings.TrimPrefix(filepath.ToSlash(filepath.Clean(strings.TrimPrefix(key, "/"))), "./")
|
||
|
|
}
|