github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/storage/minio.go (about)

     1  // Copyright 2023 The GitBundle Inc. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // Use of this source code is governed by a MIT-style
     4  // license that can be found in the LICENSE file.
     5  
     6  package storage
     7  
     8  import (
     9  	"context"
    10  	"io"
    11  	"net/url"
    12  	"os"
    13  	"path"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/gitbundle/modules/log"
    18  
    19  	"github.com/minio/minio-go/v7"
    20  	"github.com/minio/minio-go/v7/pkg/credentials"
    21  )
    22  
    23  var (
    24  	_ ObjectStorage = &MinioStorage{}
    25  
    26  	quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
    27  )
    28  
    29  type minioObject struct {
    30  	*minio.Object
    31  }
    32  
    33  func (m *minioObject) Stat() (os.FileInfo, error) {
    34  	oi, err := m.Object.Stat()
    35  	if err != nil {
    36  		return nil, convertMinioErr(err)
    37  	}
    38  
    39  	return &minioFileInfo{oi}, nil
    40  }
    41  
    42  // MinioStorageType is the type descriptor for minio storage
    43  const MinioStorageType Type = "minio"
    44  
    45  // MinioStorageConfig represents the configuration for a minio storage
    46  type MinioStorageConfig struct {
    47  	Endpoint        string `ini:"MINIO_ENDPOINT"`
    48  	AccessKeyID     string `ini:"MINIO_ACCESS_KEY_ID"`
    49  	SecretAccessKey string `ini:"MINIO_SECRET_ACCESS_KEY"`
    50  	Bucket          string `ini:"MINIO_BUCKET"`
    51  	Location        string `ini:"MINIO_LOCATION"`
    52  	BasePath        string `ini:"MINIO_BASE_PATH"`
    53  	UseSSL          bool   `ini:"MINIO_USE_SSL"`
    54  }
    55  
    56  // MinioStorage returns a minio bucket storage
    57  type MinioStorage struct {
    58  	ctx      context.Context
    59  	client   *minio.Client
    60  	bucket   string
    61  	basePath string
    62  }
    63  
    64  func convertMinioErr(err error) error {
    65  	if err == nil {
    66  		return nil
    67  	}
    68  	errResp, ok := err.(minio.ErrorResponse)
    69  	if !ok {
    70  		return err
    71  	}
    72  
    73  	// Convert two responses to standard analogues
    74  	switch errResp.Code {
    75  	case "NoSuchKey":
    76  		return os.ErrNotExist
    77  	case "AccessDenied":
    78  		return os.ErrPermission
    79  	}
    80  
    81  	return err
    82  }
    83  
    84  // NewMinioStorage returns a minio storage
    85  func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error) {
    86  	configInterface, err := toConfig(MinioStorageConfig{}, cfg)
    87  	if err != nil {
    88  		return nil, convertMinioErr(err)
    89  	}
    90  	config := configInterface.(MinioStorageConfig)
    91  
    92  	log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
    93  
    94  	minioClient, err := minio.New(config.Endpoint, &minio.Options{
    95  		Creds:  credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
    96  		Secure: config.UseSSL,
    97  	})
    98  	if err != nil {
    99  		return nil, convertMinioErr(err)
   100  	}
   101  
   102  	if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{
   103  		Region: config.Location,
   104  	}); err != nil {
   105  		// Check to see if we already own this bucket (which happens if you run this twice)
   106  		exists, errBucketExists := minioClient.BucketExists(ctx, config.Bucket)
   107  		if !exists || errBucketExists != nil {
   108  			return nil, convertMinioErr(err)
   109  		}
   110  	}
   111  
   112  	return &MinioStorage{
   113  		ctx:      ctx,
   114  		client:   minioClient,
   115  		bucket:   config.Bucket,
   116  		basePath: config.BasePath,
   117  	}, nil
   118  }
   119  
   120  func (m *MinioStorage) buildMinioPath(p string) string {
   121  	return strings.TrimPrefix(path.Join(m.basePath, path.Clean("/" + strings.ReplaceAll(p, "\\", "/"))[1:]), "/")
   122  }
   123  
   124  // Open open a file
   125  func (m *MinioStorage) Open(path string) (Object, error) {
   126  	opts := minio.GetObjectOptions{}
   127  	object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
   128  	if err != nil {
   129  		return nil, convertMinioErr(err)
   130  	}
   131  	return &minioObject{object}, nil
   132  }
   133  
   134  // Save save a file to minio
   135  func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
   136  	uploadInfo, err := m.client.PutObject(
   137  		m.ctx,
   138  		m.bucket,
   139  		m.buildMinioPath(path),
   140  		r,
   141  		size,
   142  		minio.PutObjectOptions{ContentType: "application/octet-stream"},
   143  	)
   144  	if err != nil {
   145  		return 0, convertMinioErr(err)
   146  	}
   147  	return uploadInfo.Size, nil
   148  }
   149  
   150  type minioFileInfo struct {
   151  	minio.ObjectInfo
   152  }
   153  
   154  func (m minioFileInfo) Name() string {
   155  	return path.Base(m.ObjectInfo.Key)
   156  }
   157  
   158  func (m minioFileInfo) Size() int64 {
   159  	return m.ObjectInfo.Size
   160  }
   161  
   162  func (m minioFileInfo) ModTime() time.Time {
   163  	return m.LastModified
   164  }
   165  
   166  func (m minioFileInfo) IsDir() bool {
   167  	return strings.HasSuffix(m.ObjectInfo.Key, "/")
   168  }
   169  
   170  func (m minioFileInfo) Mode() os.FileMode {
   171  	return os.ModePerm
   172  }
   173  
   174  func (m minioFileInfo) Sys() interface{} {
   175  	return nil
   176  }
   177  
   178  // Stat returns the stat information of the object
   179  func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
   180  	info, err := m.client.StatObject(
   181  		m.ctx,
   182  		m.bucket,
   183  		m.buildMinioPath(path),
   184  		minio.StatObjectOptions{},
   185  	)
   186  	if err != nil {
   187  		return nil, convertMinioErr(err)
   188  	}
   189  	return &minioFileInfo{info}, nil
   190  }
   191  
   192  // Delete delete a file
   193  func (m *MinioStorage) Delete(path string) error {
   194  	err := m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})
   195  
   196  	return convertMinioErr(err)
   197  }
   198  
   199  // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
   200  func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
   201  	reqParams := make(url.Values)
   202  	// 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?
   203  	reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
   204  	u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
   205  	return u, convertMinioErr(err)
   206  }
   207  
   208  // IterateObjects iterates across the objects in the miniostorage
   209  func (m *MinioStorage) IterateObjects(fn func(path string, obj Object) error) error {
   210  	opts := minio.GetObjectOptions{}
   211  	lobjectCtx, cancel := context.WithCancel(m.ctx)
   212  	defer cancel()
   213  	for mObjInfo := range m.client.ListObjects(lobjectCtx, m.bucket, minio.ListObjectsOptions{
   214  		Prefix:    m.basePath,
   215  		Recursive: true,
   216  	}) {
   217  		object, err := m.client.GetObject(lobjectCtx, m.bucket, mObjInfo.Key, opts)
   218  		if err != nil {
   219  			return convertMinioErr(err)
   220  		}
   221  		if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
   222  			defer object.Close()
   223  			return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
   224  		}(object, fn); err != nil {
   225  			return convertMinioErr(err)
   226  		}
   227  	}
   228  	return nil
   229  }
   230  
   231  func init() {
   232  	RegisterStorageType(MinioStorageType, NewMinioStorage)
   233  }