Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions modules/storage/minio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package storage

import (
"context"
"io"
"net/url"
"os"
"path"
"strings"
"time"

"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
core_module "github.com/oj-lab/platform/modules/core"
)

var (
_ ObjectStorage = &MinioStorage{}

quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
)

type minioObject struct {
*minio.Object
}

func (m *minioObject) Stat() (os.FileInfo, error) {
oi, err := m.Object.Stat()
if err != nil {
return nil, convertMinioErr(err)
}

Check warning on line 34 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L30-L34

Added lines #L30 - L34 were not covered by tests

return &minioFileInfo{oi}, nil

Check warning on line 36 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L36

Added line #L36 was not covered by tests
}

// MinioStorage returns a minio bucket storage
type MinioStorage struct {
ctx context.Context
client *minio.Client
bucket string
basePath string
}

func convertMinioErr(err error) error {
if err == nil {
return nil
}
errResp, ok := err.(minio.ErrorResponse)
if !ok {
return err
}

Check warning on line 54 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L47-L54

Added lines #L47 - L54 were not covered by tests

// Convert two responses to standard analogues
switch errResp.Code {
case "NoSuchKey":
return os.ErrNotExist
case "AccessDenied":
return os.ErrPermission

Check warning on line 61 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L57-L61

Added lines #L57 - L61 were not covered by tests
}

return err

Check warning on line 64 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L64

Added line #L64 was not covered by tests
}

// NewMinioStorage returns a minio storage
func NewMinioStorage(ctx context.Context) (ObjectStorage, error) {
endpoint := core_module.Config.GetString(objectStorageEndpointConfigKey)
accessKeyID := core_module.Config.GetString(objectStorageAccessKeyConfigKey)
secretAccessKey := core_module.Config.GetString(objectStorageSecretKeyConfigKey)

client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
})
if err != nil {
return nil, convertMinioErr(err)
}

Check warning on line 78 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L68-L78

Added lines #L68 - L78 were not covered by tests

bucket := core_module.Config.GetString(objectStorageBucketConfigKey)
basePath := core_module.Config.GetString(objectStorageBasePathConfigKey)

// Check to see if we already own this bucket
exists, err := client.BucketExists(ctx, bucket)
if err != nil {
return nil, convertMinioErr(err)
}
if !exists {
if err := client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}); err != nil {
return nil, convertMinioErr(err)
}

Check warning on line 91 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L80-L91

Added lines #L80 - L91 were not covered by tests
}

return &MinioStorage{
ctx: ctx,
client: client,
bucket: bucket,
basePath: basePath,
}, nil

Check warning on line 99 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L94-L99

Added lines #L94 - L99 were not covered by tests
}

func (m *MinioStorage) buildMinioPath(p string) string {
p = strings.TrimPrefix(path.Join(m.basePath, p), "/") // object store doesn't use slash for root path
if p == "." {
p = "" // object store doesn't use dot as relative path
}
return p

Check warning on line 107 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L102-L107

Added lines #L102 - L107 were not covered by tests
}

func (m *MinioStorage) buildMinioDirPrefix(p string) string {
// ending slash is required for avoiding matching like "foo/" and "foobar/" with prefix "foo"
p = m.buildMinioPath(p) + "/"
if p == "/" {
p = "" // object store doesn't use slash for root path
}
return p

Check warning on line 116 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L110-L116

Added lines #L110 - L116 were not covered by tests
}

// Open opens a file
func (m *MinioStorage) Open(path string) (Object, error) {
opts := minio.GetObjectOptions{}
object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
if err != nil {
return nil, convertMinioErr(err)
}
return &minioObject{object}, nil

Check warning on line 126 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L120-L126

Added lines #L120 - L126 were not covered by tests
}

// Save saves a file to minio
func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
uploadInfo, err := m.client.PutObject(
m.ctx,
m.bucket,
m.buildMinioPath(path),
r,
size,
minio.PutObjectOptions{
ContentType: "application/octet-stream",
},
)
if err != nil {
return 0, convertMinioErr(err)
}
return uploadInfo.Size, nil

Check warning on line 144 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L130-L144

Added lines #L130 - L144 were not covered by tests
}

type minioFileInfo struct {
minio.ObjectInfo
}

func (m minioFileInfo) Name() string {
return path.Base(m.ObjectInfo.Key)

Check warning on line 152 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L151-L152

Added lines #L151 - L152 were not covered by tests
}

func (m minioFileInfo) Size() int64 {
return m.ObjectInfo.Size

Check warning on line 156 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L155-L156

Added lines #L155 - L156 were not covered by tests
}

func (m minioFileInfo) ModTime() time.Time {
return m.LastModified

Check warning on line 160 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L159-L160

Added lines #L159 - L160 were not covered by tests
}

func (m minioFileInfo) IsDir() bool {
return strings.HasSuffix(m.ObjectInfo.Key, "/")

Check warning on line 164 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L163-L164

Added lines #L163 - L164 were not covered by tests
}

func (m minioFileInfo) Mode() os.FileMode {
return os.ModePerm

Check warning on line 168 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L167-L168

Added lines #L167 - L168 were not covered by tests
}

func (m minioFileInfo) Sys() any {
return nil

Check warning on line 172 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L171-L172

Added lines #L171 - L172 were not covered by tests
}

// Stat returns the stat information of the object
func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
info, err := m.client.StatObject(
m.ctx,
m.bucket,
m.buildMinioPath(path),
minio.StatObjectOptions{},
)
if err != nil {
return nil, convertMinioErr(err)
}
return &minioFileInfo{info}, nil

Check warning on line 186 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L176-L186

Added lines #L176 - L186 were not covered by tests
}

// Delete delete a file
func (m *MinioStorage) Delete(path string) error {
err := m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})

return convertMinioErr(err)

Check warning on line 193 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L190-L193

Added lines #L190 - L193 were not covered by tests
}

// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
func (m *MinioStorage) URL(path, name string, serveDirectReqParams url.Values) (*url.URL, error) {
// copy serveDirectReqParams
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
if err != nil {
return nil, err
}

Check warning on line 202 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L197-L202

Added lines #L197 - L202 were not covered by tests
// TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we?
reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
return u, convertMinioErr(err)

Check warning on line 206 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L204-L206

Added lines #L204 - L206 were not covered by tests
}

// IterateObjects iterates across the objects in the miniostorage
func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error {
opts := minio.GetObjectOptions{}
for mObjInfo := range m.client.ListObjects(m.ctx, m.bucket, minio.ListObjectsOptions{
Prefix: m.buildMinioDirPrefix(dirName),
Recursive: true,
}) {
object, err := m.client.GetObject(m.ctx, m.bucket, mObjInfo.Key, opts)
if err != nil {
return convertMinioErr(err)
}
if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
defer object.Close()
return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
}(object, fn); err != nil {
return convertMinioErr(err)
}

Check warning on line 225 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L210-L225

Added lines #L210 - L225 were not covered by tests
}
return nil

Check warning on line 227 in modules/storage/minio.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/minio.go#L227

Added line #L227 was not covered by tests
}
73 changes: 73 additions & 0 deletions modules/storage/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package storage

import (
"io"
"net/url"
"os"
)

const (
objectStorageEndpointConfigKey = "object_storage.endpoint"
objectStorageAccessKeyConfigKey = "object_storage.access_key"
objectStorageSecretKeyConfigKey = "object_storage.secret_key"
objectStorageBucketConfigKey = "object_storage.bucket"
objectStorageBasePathConfigKey = "object_storage.base_path"
)

// Object represents the object on the storage
type Object interface {
io.ReadCloser
io.Seeker
Stat() (os.FileInfo, error)
}

// ObjectStorage represents an object storage to handle a bucket and files
type ObjectStorage interface {
Open(path string) (Object, error)
// Save store a object, if size is unknown set -1
Save(path string, r io.Reader, size int64) (int64, error)
Stat(path string) (os.FileInfo, error)
Delete(path string) error
URL(path, name string, reqParams url.Values) (*url.URL, error)
IterateObjects(path string, iterator func(path string, obj Object) error) error
}

// Copy copies a file from source ObjectStorage to dest ObjectStorage
func Copy(dstStorage ObjectStorage, dstPath string, srcStorage ObjectStorage, srcPath string) (int64, error) {
f, err := srcStorage.Open(srcPath)
if err != nil {
return 0, err
}
defer f.Close()

size := int64(-1)
fsinfo, err := f.Stat()
if err == nil {
size = fsinfo.Size()
}

Check warning on line 47 in modules/storage/storage.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/storage.go#L36-L47

Added lines #L36 - L47 were not covered by tests

return dstStorage.Save(dstPath, f, size)

Check warning on line 49 in modules/storage/storage.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/storage.go#L49

Added line #L49 was not covered by tests
}

// Clean delete all the objects in this storage
func Clean(storage ObjectStorage) error {
return storage.IterateObjects("", func(path string, obj Object) error {
_ = obj.Close()
return storage.Delete(path)
})

Check warning on line 57 in modules/storage/storage.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/storage.go#L53-L57

Added lines #L53 - L57 were not covered by tests
}

// SaveFrom saves data to the ObjectStorage with path p from the callback
func SaveFrom(objStorage ObjectStorage, path string, callback func(w io.Writer) error) error {
pr, pw := io.Pipe()
defer pr.Close()
go func() {
defer pw.Close()
if err := callback(pw); err != nil {
_ = pw.CloseWithError(err)
}

Check warning on line 68 in modules/storage/storage.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/storage.go#L61-L68

Added lines #L61 - L68 were not covered by tests
}()

_, err := objStorage.Save(path, pr, -1)
return err

Check warning on line 72 in modules/storage/storage.go

View check run for this annotation

Codecov / codecov/patch

modules/storage/storage.go#L71-L72

Added lines #L71 - L72 were not covered by tests
}