diff --git a/go.mod b/go.mod index 5a4cd2e..d10b57e 100644 --- a/go.mod +++ b/go.mod @@ -19,31 +19,43 @@ require ( github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.100 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tinylib/msgp v1.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.uber.org/multierr v1.10.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect diff --git a/go.sum b/go.sum index d951e3e..c124a41 100644 --- a/go.sum +++ b/go.sum @@ -12,12 +12,16 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -35,14 +39,21 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -53,6 +64,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.100 h1:ShkWi8Tyj9RtU57OQB2HIXKz4bFgtVib0bbT1sbtLI8= +github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -60,6 +77,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -69,6 +88,8 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -80,6 +101,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= @@ -94,6 +117,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= diff --git a/internal/storage/minio.go b/internal/storage/minio.go new file mode 100644 index 0000000..92f0729 --- /dev/null +++ b/internal/storage/minio.go @@ -0,0 +1,286 @@ +package storage + +import ( + "context" + "fmt" + "io" + "time" + + "gcy_hpc_server/internal/config" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +// ObjectInfo contains metadata about a stored object. +type ObjectInfo struct { + Key string + Size int64 + LastModified time.Time + ETag string + ContentType string +} + +// UploadInfo contains metadata about an uploaded object. +type UploadInfo struct { + ETag string + Size int64 +} + +// GetOptions specifies parameters for GetObject, including optional Range. +type GetOptions struct { + Start *int64 // Range start byte offset (nil = no range) + End *int64 // Range end byte offset (nil = no range) +} + +// MultipartUpload represents an incomplete multipart upload. +type MultipartUpload struct { + ObjectName string + UploadID string + Initiated time.Time +} + +// RemoveObjectsOptions specifies options for removing multiple objects. +type RemoveObjectsOptions struct { + ForceDelete bool +} + +// PutObjectOptions for PutObject. +type PutObjectOptions struct { + ContentType string + DisableMultipart bool // true for small chunks (already pre-split) +} + +// RemoveObjectOptions for RemoveObject. +type RemoveObjectOptions struct { + ForceDelete bool +} + +// MakeBucketOptions for MakeBucket. +type MakeBucketOptions struct { + Region string +} + +// StatObjectOptions for StatObject. +type StatObjectOptions struct{} + +// ObjectStorage defines the interface for object storage operations. +// Implementations should wrap MinIO SDK calls with custom transfer types. +type ObjectStorage interface { + // PutObject uploads an object from a reader. + PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64, opts PutObjectOptions) (UploadInfo, error) + + // GetObject retrieves an object. opts may contain Range parameters. + GetObject(ctx context.Context, bucket, key string, opts GetOptions) (io.ReadCloser, ObjectInfo, error) + + // ComposeObject merges multiple source objects into a single destination. + ComposeObject(ctx context.Context, bucket, dst string, sources []string) (UploadInfo, error) + + // AbortMultipartUpload aborts an incomplete multipart upload by upload ID. + AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error + + // RemoveIncompleteUpload removes all incomplete uploads for an object. + // This is the preferred cleanup method — it encapsulates list + abort logic. + RemoveIncompleteUpload(ctx context.Context, bucket, object string) error + + // RemoveObject deletes a single object. + RemoveObject(ctx context.Context, bucket, key string, opts RemoveObjectOptions) error + + // ListObjects lists objects with a given prefix. + ListObjects(ctx context.Context, bucket, prefix string, recursive bool) ([]ObjectInfo, error) + + // RemoveObjects deletes multiple objects. + RemoveObjects(ctx context.Context, bucket string, keys []string, opts RemoveObjectsOptions) error + + // BucketExists checks if a bucket exists. + BucketExists(ctx context.Context, bucket string) (bool, error) + + // MakeBucket creates a new bucket. + MakeBucket(ctx context.Context, bucket string, opts MakeBucketOptions) error + + // StatObject gets metadata about an object without downloading it. + StatObject(ctx context.Context, bucket, key string, opts StatObjectOptions) (ObjectInfo, error) +} + +var _ ObjectStorage = (*MinioClient)(nil) + +type MinioClient struct { + core *minio.Core + bucket string +} + +func NewMinioClient(cfg config.MinioConfig) (*MinioClient, error) { + transport, err := minio.DefaultTransport(cfg.UseSSL) + if err != nil { + return nil, fmt.Errorf("create default transport: %w", err) + } + transport.MaxIdleConnsPerHost = 100 + transport.IdleConnTimeout = 90 * time.Second + + core, err := minio.NewCore(cfg.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), + Secure: cfg.UseSSL, + Transport: transport, + }) + if err != nil { + return nil, fmt.Errorf("create minio client: %w", err) + } + + mc := &MinioClient{core: core, bucket: cfg.Bucket} + + ctx := context.Background() + exists, err := core.BucketExists(ctx, cfg.Bucket) + if err != nil { + return nil, fmt.Errorf("check bucket: %w", err) + } + if !exists { + if err := core.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{}); err != nil { + return nil, fmt.Errorf("create bucket: %w", err) + } + } + + return mc, nil +} + +func (m *MinioClient) PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64, opts PutObjectOptions) (UploadInfo, error) { + info, err := m.core.PutObject(ctx, bucket, key, reader, size, "", "", minio.PutObjectOptions{ + ContentType: opts.ContentType, + DisableMultipart: opts.DisableMultipart, + }) + if err != nil { + return UploadInfo{}, fmt.Errorf("put object %s/%s: %w", bucket, key, err) + } + return UploadInfo{ETag: info.ETag, Size: info.Size}, nil +} + +func (m *MinioClient) GetObject(ctx context.Context, bucket, key string, opts GetOptions) (io.ReadCloser, ObjectInfo, error) { + var gopts minio.GetObjectOptions + if opts.Start != nil || opts.End != nil { + start := int64(0) + end := int64(0) + if opts.Start != nil { + start = *opts.Start + } + if opts.End != nil { + end = *opts.End + } + if err := gopts.SetRange(start, end); err != nil { + return nil, ObjectInfo{}, fmt.Errorf("set range: %w", err) + } + } + + body, info, _, err := m.core.GetObject(ctx, bucket, key, gopts) + if err != nil { + return nil, ObjectInfo{}, fmt.Errorf("get object %s/%s: %w", bucket, key, err) + } + return body, toObjectInfo(info), nil +} + +func (m *MinioClient) ComposeObject(ctx context.Context, bucket, dst string, sources []string) (UploadInfo, error) { + srcs := make([]minio.CopySrcOptions, len(sources)) + for i, src := range sources { + srcs[i] = minio.CopySrcOptions{Bucket: bucket, Object: src} + } + + do := minio.CopyDestOptions{ + Bucket: bucket, + Object: dst, + ReplaceMetadata: true, + } + + info, err := m.core.ComposeObject(ctx, do, srcs...) + if err != nil { + return UploadInfo{}, fmt.Errorf("compose object %s/%s: %w", bucket, dst, err) + } + return UploadInfo{ETag: info.ETag, Size: info.Size}, nil +} + +func (m *MinioClient) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error { + if err := m.core.AbortMultipartUpload(ctx, bucket, object, uploadID); err != nil { + return fmt.Errorf("abort multipart upload %s/%s %s: %w", bucket, object, uploadID, err) + } + return nil +} + +func (m *MinioClient) RemoveIncompleteUpload(ctx context.Context, bucket, object string) error { + if err := m.core.RemoveIncompleteUpload(ctx, bucket, object); err != nil { + return fmt.Errorf("remove incomplete upload %s/%s: %w", bucket, object, err) + } + return nil +} + +func (m *MinioClient) RemoveObject(ctx context.Context, bucket, key string, opts RemoveObjectOptions) error { + if err := m.core.RemoveObject(ctx, bucket, key, minio.RemoveObjectOptions{ + ForceDelete: opts.ForceDelete, + }); err != nil { + return fmt.Errorf("remove object %s/%s: %w", bucket, key, err) + } + return nil +} + +func (m *MinioClient) ListObjects(ctx context.Context, bucket, prefix string, recursive bool) ([]ObjectInfo, error) { + ch := m.core.Client.ListObjects(ctx, bucket, minio.ListObjectsOptions{ + Prefix: prefix, + Recursive: recursive, + }) + + var result []ObjectInfo + for obj := range ch { + if obj.Err != nil { + return result, fmt.Errorf("list objects %s/%s: %w", bucket, prefix, obj.Err) + } + result = append(result, toObjectInfo(obj)) + } + return result, nil +} + +func (m *MinioClient) RemoveObjects(ctx context.Context, bucket string, keys []string, opts RemoveObjectsOptions) error { + objectsCh := make(chan minio.ObjectInfo, len(keys)) + for _, key := range keys { + objectsCh <- minio.ObjectInfo{Key: key} + } + close(objectsCh) + + errCh := m.core.RemoveObjects(ctx, bucket, objectsCh, minio.RemoveObjectsOptions{}) + + for err := range errCh { + if err.Err != nil { + return fmt.Errorf("remove object %s: %w", err.ObjectName, err.Err) + } + } + return nil +} + +func (m *MinioClient) BucketExists(ctx context.Context, bucket string) (bool, error) { + ok, err := m.core.BucketExists(ctx, bucket) + if err != nil { + return false, fmt.Errorf("bucket exists %s: %w", bucket, err) + } + return ok, nil +} + +func (m *MinioClient) MakeBucket(ctx context.Context, bucket string, opts MakeBucketOptions) error { + if err := m.core.MakeBucket(ctx, bucket, minio.MakeBucketOptions{ + Region: opts.Region, + }); err != nil { + return fmt.Errorf("make bucket %s: %w", bucket, err) + } + return nil +} + +func (m *MinioClient) StatObject(ctx context.Context, bucket, key string, _ StatObjectOptions) (ObjectInfo, error) { + info, err := m.core.StatObject(ctx, bucket, key, minio.StatObjectOptions{}) + if err != nil { + return ObjectInfo{}, fmt.Errorf("stat object %s/%s: %w", bucket, key, err) + } + return toObjectInfo(info), nil +} + +func toObjectInfo(info minio.ObjectInfo) ObjectInfo { + return ObjectInfo{ + Key: info.Key, + Size: info.Size, + LastModified: info.LastModified, + ETag: info.ETag, + ContentType: info.ContentType, + } +} diff --git a/internal/storage/minio_test.go b/internal/storage/minio_test.go new file mode 100644 index 0000000..dd88adb --- /dev/null +++ b/internal/storage/minio_test.go @@ -0,0 +1,7 @@ +package storage + +import "testing" + +func TestMinioClientImplementsObjectStorage(t *testing.T) { + var _ ObjectStorage = (*MinioClient)(nil) +}