fix: minio prefix
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"path"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.company.lan/gopkg/filestore/remote"
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
|
||||||
"git.company.lan/gopkg/filestore/remote"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Убеждаемся в том, что мы всегда реализуем интерфейс remote.Storage.
|
// Убеждаемся в том, что мы всегда реализуем интерфейс remote.Storage.
|
||||||
@@ -24,86 +22,85 @@ func init() {
|
|||||||
type MinioStorage struct {
|
type MinioStorage struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
client *minio.Client
|
client *minio.Client
|
||||||
bucket string
|
cfg *Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MinioStorage) NewStorage(ctx context.Context, connString string) (remote.Storage, error) {
|
func (s *MinioStorage) NewStorage(ctx context.Context, connString string) (remote.Storage, error) {
|
||||||
u, err := url.Parse(connString)
|
cfg, err := NewConfig(connString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
queries := u.Query()
|
|
||||||
username, password := getUserPassword(u)
|
|
||||||
token := queries.Get("token")
|
|
||||||
|
|
||||||
opts := &minio.Options{
|
opts := &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(username, password, token),
|
Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretKey, cfg.Token),
|
||||||
Region: "us-east-1",
|
Region: cfg.Region,
|
||||||
}
|
Secure: cfg.Secure,
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := minio.New(u.Host, opts)
|
client, err := minio.New(cfg.Endpoint, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ctx = ctx
|
s.ctx = ctx
|
||||||
s.client = client
|
s.client = client
|
||||||
s.bucket = u.Path[1:]
|
s.cfg = cfg
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MinioStorage) Create(path string, opts ...remote.Option) (io.WriteCloser, error) {
|
func (s *MinioStorage) Create(name string, opts ...remote.Option) (io.WriteCloser, error) {
|
||||||
return newMinioWriter(s.ctx, s.client, s.bucket, path, opts...), nil
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &minioFileWrapper{Object: obj, name: name}, nil
|
return &minioFileWrapper{Object: obj, name: name}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MinioStorage) Remove(path string) error {
|
func (s *MinioStorage) Remove(name string) error {
|
||||||
return s.client.RemoveObject(s.ctx, s.bucket, path, minio.RemoveObjectOptions{})
|
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) {
|
func (s *MinioStorage) Stat(name string) (remote.FileInfo, error) {
|
||||||
info, err := s.client.StatObject(s.ctx, s.bucket, path, minio.StatObjectOptions{})
|
name = path.Join(s.cfg.Prefix, name)
|
||||||
|
|
||||||
|
info, err := s.client.StatObject(s.ctx, s.cfg.BucketName, name, minio.StatObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return newMinioFileInfo(info), nil
|
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 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{
|
options := minio.ListObjectsOptions{
|
||||||
Prefix: strings.TrimRight(path, "/") + "/",
|
Prefix: strings.TrimRight(name, "/") + "/",
|
||||||
Recursive: false,
|
Recursive: false,
|
||||||
MaxKeys: 1,
|
MaxKeys: 1,
|
||||||
}
|
}
|
||||||
objectChan := s.client.ListObjects(s.ctx, s.bucket, options)
|
objectChan := s.client.ListObjects(s.ctx, s.cfg.BucketName, options)
|
||||||
object, ok := <-objectChan
|
object, ok := <-objectChan
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -114,8 +111,10 @@ func (s *MinioStorage) IsDir(path string) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MinioStorage) IsFile(path string) (bool, error) {
|
func (s *MinioStorage) IsFile(name string) (bool, error) {
|
||||||
_, err := s.client.StatObject(s.ctx, s.bucket, path, minio.StatObjectOptions{})
|
name = path.Join(s.cfg.Prefix, name)
|
||||||
|
|
||||||
|
_, err := s.client.StatObject(s.ctx, s.cfg.BucketName, name, minio.StatObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -24,22 +24,22 @@ type Storage interface {
|
|||||||
Open(name string) (http.File, error)
|
Open(name string) (http.File, error)
|
||||||
|
|
||||||
// Create создаёт файл и возвращает io.WriteCloser.
|
// Create создаёт файл и возвращает io.WriteCloser.
|
||||||
Create(path string, opts ...Option) (io.WriteCloser, error)
|
Create(name string, opts ...Option) (io.WriteCloser, error)
|
||||||
|
|
||||||
// Remove удаляет файл.
|
// Remove удаляет файл.
|
||||||
Remove(path string) error
|
Remove(name string) error
|
||||||
|
|
||||||
// Stat получает информацию о файле/каталоге.
|
// Stat получает информацию о файле/каталоге.
|
||||||
Stat(path string) (FileInfo, error)
|
Stat(name string) (FileInfo, error)
|
||||||
|
|
||||||
// Exists определяет, существует ли файл или каталог.
|
// Exists определяет, существует ли файл или каталог.
|
||||||
Exists(path string) (bool, error)
|
Exists(name string) (bool, error)
|
||||||
|
|
||||||
// IsDir определяет, является ли путь каталогом.
|
// IsDir определяет, является ли путь каталогом.
|
||||||
IsDir(path string) (bool, error)
|
IsDir(name string) (bool, error)
|
||||||
|
|
||||||
// IsFile определяет, является ли путь файлом.
|
// IsFile определяет, является ли путь файлом.
|
||||||
IsFile(path string) (bool, error)
|
IsFile(name string) (bool, error)
|
||||||
|
|
||||||
Uploader() Uploader
|
Uploader() Uploader
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user