fix: minio prefix

This commit is contained in:
S.Solodyagin
2025-10-29 16:18:08 +03:00
parent 4799a2175f
commit dfac158af5
5 changed files with 237 additions and 60 deletions
+81
View File
@@ -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()
}
+112
View File
@@ -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&region=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)
}
})
}
}
-15
View File
@@ -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
}
+38 -39
View File
@@ -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
}
+6 -6
View File
@@ -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
}