code.gitea.io/gitea@v1.19.3/modules/storage/minio.go (about)

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