diff --git a/remote/miniostorage/config.go b/remote/miniostorage/config.go new file mode 100644 index 0000000..0eccc24 --- /dev/null +++ b/remote/miniostorage/config.go @@ -0,0 +1,81 @@ +package miniostorage + +import ( + "net/url" + "path" + "strconv" + "strings" +) + +type Config struct { + Endpoint string + AccessKeyID string + SecretKey string + Token string + BucketName string + Prefix string + Region string + Secure bool +} + +// NewConfig парсирует строку подключения +func NewConfig(connString string) (*Config, error) { + u, err := url.Parse(connString) + if err != nil { + return nil, err + } + + queries := u.Query() + var accessKeyID, secretKey string + if u.User != nil { + accessKeyID = u.User.Username() + if s, ok := u.User.Password(); ok { + secretKey = s + } + } + token := queries.Get("token") + + cfg := &Config{} + cfg.Endpoint = u.Host + cfg.AccessKeyID = accessKeyID + cfg.SecretKey = secretKey + cfg.Token = token + if queries.Has("secure") { + secure, err := strconv.ParseBool(queries.Get("secure")) + if err != nil { + return nil, err + } + cfg.Secure = secure + } + if queries.Has("region") { + cfg.Region = queries.Get("region") + } else { + cfg.Region = "us-east-1" + } + + parts := strings.SplitN(strings.Trim(u.Path, "/"), "/", 2) + cfg.BucketName = parts[0] + if len(parts) > 1 { + cfg.Prefix = parts[1] + } + + return cfg, nil +} + +func ConnString(cfg Config) string { + params := url.Values{} + if cfg.Region != "" { + params.Add("region", cfg.Region) + } + if cfg.Secure { + params.Add("secure", "1") + } + u := url.URL{ + Scheme: "minio", + Host: cfg.Endpoint, + User: url.UserPassword(cfg.AccessKeyID, cfg.SecretKey), + Path: path.Join("/", cfg.BucketName, cfg.Prefix), + RawQuery: params.Encode(), + } + return u.String() +} diff --git a/remote/miniostorage/config_test.go b/remote/miniostorage/config_test.go new file mode 100644 index 0000000..8e48fd8 --- /dev/null +++ b/remote/miniostorage/config_test.go @@ -0,0 +1,112 @@ +package miniostorage + +import ( + "reflect" + "testing" +) + +func TestNewConfig(t *testing.T) { + cases := []struct { + name string + connString string + expected *Config + expectedErr error + }{ + { + name: "Test 1", + connString: "minio://e5JXrHpr093RVk1s9IcE:QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ@play.min.io/bucket", + expected: &Config{ + Endpoint: "play.min.io", + AccessKeyID: "e5JXrHpr093RVk1s9IcE", + SecretKey: "QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ", + BucketName: "bucket", + Prefix: "", + Region: "us-east-1", + Secure: false, + }, + }, + { + name: "Test 2", + connString: "minio://e5JXrHpr093RVk1s9IcE:QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ@play.min.io:9443/bucket?secure=1®ion=us-west-1", + expected: &Config{ + Endpoint: "play.min.io:9443", + AccessKeyID: "e5JXrHpr093RVk1s9IcE", + SecretKey: "QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ", + BucketName: "bucket", + Prefix: "", + Region: "us-west-1", + Secure: true, + }, + }, + { + name: "Test 3", + connString: "minio://e5JXrHpr093RVk1s9IcE:QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ@play.min.io:9443/bucket/prefix?secure=1", + expected: &Config{ + Endpoint: "play.min.io:9443", + AccessKeyID: "e5JXrHpr093RVk1s9IcE", + SecretKey: "QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ", + BucketName: "bucket", + Prefix: "prefix", + Region: "us-east-1", + Secure: true, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg, err := NewConfig(tc.connString) + if err != nil { + if err != tc.expectedErr { + t.Fatalf("Expected %q, got %q", tc.expectedErr, err) + } + } + + if !reflect.DeepEqual(cfg, tc.expected) { + t.Errorf("expected %v, got %v", cfg, tc.expected) + } + }) + } +} + +func TestConnString(t *testing.T) { + cases := []struct { + name string + cfg Config + expected string + }{ + { + name: "Test 1", + cfg: Config{ + Endpoint: "play.min.io", + AccessKeyID: "e5JXrHpr093RVk1s9IcE", + SecretKey: "QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ", + BucketName: "bucket", + Prefix: "", + Region: "us-east-1", + Secure: false, + }, + expected: "minio://e5JXrHpr093RVk1s9IcE:QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ@play.min.io/bucket?region=us-east-1", + }, + { + name: "Test 2", + cfg: Config{ + Endpoint: "play.min.io:9443", + AccessKeyID: "e5JXrHpr093RVk1s9IcE", + SecretKey: "QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ", + BucketName: "bucket", + Prefix: "prefix", + Region: "us-east-1", + Secure: true, + }, + expected: "minio://e5JXrHpr093RVk1s9IcE:QJSEqip9gX2b3041deUR1K5BCJjSDubYwy48K3SQ@play.min.io:9443/bucket/prefix?region=us-east-1&secure=1", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + str := ConnString(tc.cfg) + if str != tc.expected { + t.Errorf("expected %q, got %q", str, tc.expected) + } + }) + } +} diff --git a/remote/miniostorage/helper.go b/remote/miniostorage/helper.go deleted file mode 100644 index 31e62af..0000000 --- a/remote/miniostorage/helper.go +++ /dev/null @@ -1,15 +0,0 @@ -package miniostorage - -import "net/url" - -// getUserPassword возвращает имя пользователя и пароль. -func getUserPassword(u *url.URL) (string, string) { - var user, password string - if u.User != nil { - user = u.User.Username() - if p, ok := u.User.Password(); ok { - password = p - } - } - return user, password -} diff --git a/remote/miniostorage/miniostorage.go b/remote/miniostorage/miniostorage.go index cbdfaab..2303512 100644 --- a/remote/miniostorage/miniostorage.go +++ b/remote/miniostorage/miniostorage.go @@ -4,14 +4,12 @@ import ( "context" "io" "net/http" - "net/url" - "strconv" + "path" "strings" + "git.company.lan/gopkg/filestore/remote" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" - - "git.company.lan/gopkg/filestore/remote" ) // Убеждаемся в том, что мы всегда реализуем интерфейс remote.Storage. @@ -24,86 +22,85 @@ func init() { type MinioStorage struct { ctx context.Context client *minio.Client - bucket string + cfg *Config } func (s *MinioStorage) NewStorage(ctx context.Context, connString string) (remote.Storage, error) { - u, err := url.Parse(connString) + cfg, err := NewConfig(connString) if err != nil { return nil, err } - queries := u.Query() - username, password := getUserPassword(u) - token := queries.Get("token") - opts := &minio.Options{ - Creds: credentials.NewStaticV4(username, password, token), - Region: "us-east-1", - } - if queries.Has("secure") { - secure, err := strconv.ParseBool(queries.Get("secure")) - if err != nil { - return nil, err - } - opts.Secure = secure - } - if queries.Has("region") { - opts.Region = queries.Get("region") + Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretKey, cfg.Token), + Region: cfg.Region, + Secure: cfg.Secure, } - client, err := minio.New(u.Host, opts) + client, err := minio.New(cfg.Endpoint, opts) if err != nil { return nil, err } s.ctx = ctx s.client = client - s.bucket = u.Path[1:] + s.cfg = cfg return s, nil } -func (s *MinioStorage) Create(path string, opts ...remote.Option) (io.WriteCloser, error) { - return newMinioWriter(s.ctx, s.client, s.bucket, path, opts...), nil +func (s *MinioStorage) Create(name string, opts ...remote.Option) (io.WriteCloser, error) { + name = path.Join(s.cfg.Prefix, name) + + return newMinioWriter(s.ctx, s.client, s.cfg.BucketName, name, opts...), nil } func (s *MinioStorage) Open(name string) (http.File, error) { - obj, err := s.client.GetObject(s.ctx, s.bucket, name, minio.GetObjectOptions{}) + name = path.Join(s.cfg.Prefix, name) + + obj, err := s.client.GetObject(s.ctx, s.cfg.BucketName, name, minio.GetObjectOptions{}) if err != nil { return nil, err } return &minioFileWrapper{Object: obj, name: name}, nil } -func (s *MinioStorage) Remove(path string) error { - return s.client.RemoveObject(s.ctx, s.bucket, path, minio.RemoveObjectOptions{}) +func (s *MinioStorage) Remove(name string) error { + name = path.Join(s.cfg.Prefix, name) + + return s.client.RemoveObject(s.ctx, s.cfg.BucketName, name, minio.RemoveObjectOptions{}) } -func (s *MinioStorage) Stat(path string) (remote.FileInfo, error) { - info, err := s.client.StatObject(s.ctx, s.bucket, path, minio.StatObjectOptions{}) +func (s *MinioStorage) Stat(name string) (remote.FileInfo, error) { + name = path.Join(s.cfg.Prefix, name) + + info, err := s.client.StatObject(s.ctx, s.cfg.BucketName, name, minio.StatObjectOptions{}) if err != nil { return nil, err } return newMinioFileInfo(info), nil } -func (s *MinioStorage) Exists(path string) (bool, error) { +func (s *MinioStorage) Exists(name string) (bool, error) { + name = path.Join(s.cfg.Prefix, name) + // Сначала проверяем, является ли путь файлом - if ok, err := s.IsFile(path); err == nil && ok { + if ok, err := s.IsFile(name); err == nil && ok { return true, nil } // Если не файл, то проверяем, является ли путь каталогом - return s.IsDir(path) + return s.IsDir(name) } -func (s *MinioStorage) IsDir(path string) (bool, error) { +func (s *MinioStorage) IsDir(name string) (bool, error) { + name = path.Join(s.cfg.Prefix, name) + options := minio.ListObjectsOptions{ - Prefix: strings.TrimRight(path, "/") + "/", + Prefix: strings.TrimRight(name, "/") + "/", Recursive: false, MaxKeys: 1, } - objectChan := s.client.ListObjects(s.ctx, s.bucket, options) + objectChan := s.client.ListObjects(s.ctx, s.cfg.BucketName, options) object, ok := <-objectChan if !ok { return false, nil @@ -114,8 +111,10 @@ func (s *MinioStorage) IsDir(path string) (bool, error) { return true, nil } -func (s *MinioStorage) IsFile(path string) (bool, error) { - _, err := s.client.StatObject(s.ctx, s.bucket, path, minio.StatObjectOptions{}) +func (s *MinioStorage) IsFile(name string) (bool, error) { + name = path.Join(s.cfg.Prefix, name) + + _, err := s.client.StatObject(s.ctx, s.cfg.BucketName, name, minio.StatObjectOptions{}) if err == nil { return true, nil } diff --git a/remote/storage.go b/remote/storage.go index 609e5c3..bf0cd37 100644 --- a/remote/storage.go +++ b/remote/storage.go @@ -24,22 +24,22 @@ type Storage interface { Open(name string) (http.File, error) // Create создаёт файл и возвращает io.WriteCloser. - Create(path string, opts ...Option) (io.WriteCloser, error) + Create(name string, opts ...Option) (io.WriteCloser, error) // Remove удаляет файл. - Remove(path string) error + Remove(name string) error // Stat получает информацию о файле/каталоге. - Stat(path string) (FileInfo, error) + Stat(name string) (FileInfo, error) // Exists определяет, существует ли файл или каталог. - Exists(path string) (bool, error) + Exists(name string) (bool, error) // IsDir определяет, является ли путь каталогом. - IsDir(path string) (bool, error) + IsDir(name string) (bool, error) // IsFile определяет, является ли путь файлом. - IsFile(path string) (bool, error) + IsFile(name string) (bool, error) Uploader() Uploader }