2020-08-18 12:23:45 +08:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2020-08-18 12:23:45 +08:00
package storage
import (
"context"
2023-02-27 18:26:13 +02:00
"crypto/tls"
2023-03-28 23:10:24 +08:00
"fmt"
2020-08-18 12:23:45 +08:00
"io"
2023-02-27 18:26:13 +02:00
"net/http"
2020-08-18 12:23:45 +08:00
"net/url"
2020-09-08 23:45:10 +08:00
"os"
2020-08-18 12:23:45 +08:00
"path"
"strings"
"time"
2020-10-16 04:51:06 +01:00
"code.gitea.io/gitea/modules/log"
2023-06-14 11:42:38 +08:00
"code.gitea.io/gitea/modules/setting"
2023-03-08 20:17:39 +08:00
"code.gitea.io/gitea/modules/util"
2020-10-16 04:51:06 +01:00
2020-08-18 12:23:45 +08:00
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var (
2020-10-13 04:58:34 +01:00
_ ObjectStorage = & MinioStorage { }
quoteEscaper = strings . NewReplacer ( "\\" , "\\\\" , ` " ` , "\\\"" )
2020-08-18 12:23:45 +08:00
)
2020-09-29 17:05:13 +08:00
type minioObject struct {
* minio . Object
}
func ( m * minioObject ) Stat ( ) ( os . FileInfo , error ) {
oi , err := m . Object . Stat ( )
if err != nil {
2020-10-18 02:29:06 +01:00
return nil , convertMinioErr ( err )
2020-09-29 17:05:13 +08:00
}
return & minioFileInfo { oi } , nil
}
2020-08-18 12:23:45 +08:00
// MinioStorage returns a minio bucket storage
type MinioStorage struct {
2023-06-14 11:42:38 +08:00
cfg * setting . MinioStorageConfig
2020-08-18 12:23:45 +08:00
ctx context . Context
client * minio . Client
bucket string
basePath string
}
2020-10-16 04:51:06 +01:00
func convertMinioErr ( err error ) error {
if err == nil {
return nil
}
errResp , ok := err . ( minio . ErrorResponse )
if ! ok {
return err
}
// Convert two responses to standard analogues
switch errResp . Code {
case "NoSuchKey" :
return os . ErrNotExist
case "AccessDenied" :
return os . ErrPermission
}
return err
}
2020-08-18 12:23:45 +08:00
// NewMinioStorage returns a minio storage
2023-06-14 11:42:38 +08:00
func NewMinioStorage ( ctx context . Context , cfg * setting . Storage ) ( ObjectStorage , error ) {
config := cfg . MinioConfig
2023-03-28 23:10:24 +08:00
if config . ChecksumAlgorithm != "" && config . ChecksumAlgorithm != "default" && config . ChecksumAlgorithm != "md5" {
return nil , fmt . Errorf ( "invalid minio checksum algorithm: %s" , config . ChecksumAlgorithm )
}
2020-10-16 04:51:06 +01:00
log . Info ( "Creating Minio storage at %s:%s with base path %s" , config . Endpoint , config . Bucket , config . BasePath )
2020-10-13 04:58:34 +01:00
minioClient , err := minio . New ( config . Endpoint , & minio . Options {
2023-02-27 18:26:13 +02:00
Creds : credentials . NewStaticV4 ( config . AccessKeyID , config . SecretAccessKey , "" ) ,
Secure : config . UseSSL ,
Transport : & http . Transport { TLSClientConfig : & tls . Config { InsecureSkipVerify : config . InsecureSkipVerify } } ,
2023-08-10 13:21:09 +02:00
Region : config . Location ,
2020-08-18 12:23:45 +08:00
} )
if err != nil {
2020-10-16 04:51:06 +01:00
return nil , convertMinioErr ( err )
2020-08-18 12:23:45 +08:00
}
2020-10-13 04:58:34 +01:00
if err := minioClient . MakeBucket ( ctx , config . Bucket , minio . MakeBucketOptions {
Region : config . Location ,
2020-08-18 12:23:45 +08:00
} ) ; err != nil {
// Check to see if we already own this bucket (which happens if you run this twice)
2020-10-13 04:58:34 +01:00
exists , errBucketExists := minioClient . BucketExists ( ctx , config . Bucket )
2020-08-18 12:23:45 +08:00
if ! exists || errBucketExists != nil {
2020-10-16 04:51:06 +01:00
return nil , convertMinioErr ( err )
2020-08-18 12:23:45 +08:00
}
}
return & MinioStorage {
2023-03-28 23:10:24 +08:00
cfg : & config ,
2020-08-18 12:23:45 +08:00
ctx : ctx ,
client : minioClient ,
2020-10-13 04:58:34 +01:00
bucket : config . Bucket ,
basePath : config . BasePath ,
2020-08-18 12:23:45 +08:00
} , nil
}
func ( m * MinioStorage ) buildMinioPath ( p string ) string {
2023-05-14 06:33:25 +08:00
p = util . PathJoinRelX ( m . basePath , p )
if p == "." {
p = "" // minio doesn't use dot as relative path
}
return p
2020-08-18 12:23:45 +08:00
}
2023-03-28 23:10:24 +08:00
// Open opens a file
2020-09-08 23:45:10 +08:00
func ( m * MinioStorage ) Open ( path string ) ( Object , error ) {
2022-01-20 18:46:10 +01:00
opts := minio . GetObjectOptions { }
2020-08-18 12:23:45 +08:00
object , err := m . client . GetObject ( m . ctx , m . bucket , m . buildMinioPath ( path ) , opts )
if err != nil {
2020-10-16 04:51:06 +01:00
return nil , convertMinioErr ( err )
2020-08-18 12:23:45 +08:00
}
2020-09-29 17:05:13 +08:00
return & minioObject { object } , nil
2020-08-18 12:23:45 +08:00
}
2023-03-28 23:10:24 +08:00
// Save saves a file to minio
2021-04-03 17:19:59 +01:00
func ( m * MinioStorage ) Save ( path string , r io . Reader , size int64 ) ( int64 , error ) {
2020-08-18 12:23:45 +08:00
uploadInfo , err := m . client . PutObject (
m . ctx ,
m . bucket ,
m . buildMinioPath ( path ) ,
r ,
2021-04-03 17:19:59 +01:00
size ,
2023-03-28 23:10:24 +08:00
minio . PutObjectOptions {
ContentType : "application/octet-stream" ,
// some storages like:
// * https://developers.cloudflare.com/r2/api/s3/api/
// * https://www.backblaze.com/b2/docs/s3_compatible_api.html
// do not support "x-amz-checksum-algorithm" header, so use legacy MD5 checksum
SendContentMd5 : m . cfg . ChecksumAlgorithm == "md5" ,
} ,
2020-08-18 12:23:45 +08:00
)
if err != nil {
2020-10-16 04:51:06 +01:00
return 0 , convertMinioErr ( err )
2020-08-18 12:23:45 +08:00
}
return uploadInfo . Size , nil
}
2020-09-08 23:45:10 +08:00
type minioFileInfo struct {
minio . ObjectInfo
}
func ( m minioFileInfo ) Name ( ) string {
2021-09-06 22:46:20 +08:00
return path . Base ( m . ObjectInfo . Key )
2020-09-08 23:45:10 +08:00
}
func ( m minioFileInfo ) Size ( ) int64 {
return m . ObjectInfo . Size
}
func ( m minioFileInfo ) ModTime ( ) time . Time {
return m . LastModified
}
2020-09-29 17:05:13 +08:00
func ( m minioFileInfo ) IsDir ( ) bool {
return strings . HasSuffix ( m . ObjectInfo . Key , "/" )
}
func ( m minioFileInfo ) Mode ( ) os . FileMode {
return os . ModePerm
}
2023-07-04 20:36:08 +02:00
func ( m minioFileInfo ) Sys ( ) any {
2020-09-29 17:05:13 +08:00
return nil
}
2020-09-08 23:45:10 +08:00
// Stat returns the stat information of the object
2020-09-29 17:05:13 +08:00
func ( m * MinioStorage ) Stat ( path string ) ( os . FileInfo , error ) {
2020-09-08 23:45:10 +08:00
info , err := m . client . StatObject (
m . ctx ,
m . bucket ,
m . buildMinioPath ( path ) ,
minio . StatObjectOptions { } ,
)
if err != nil {
2020-10-16 04:51:06 +01:00
return nil , convertMinioErr ( err )
2020-09-08 23:45:10 +08:00
}
return & minioFileInfo { info } , nil
}
2020-08-18 12:23:45 +08:00
// Delete delete a file
func ( m * MinioStorage ) Delete ( path string ) error {
2020-10-16 04:51:06 +01:00
err := m . client . RemoveObject ( m . ctx , m . bucket , m . buildMinioPath ( path ) , minio . RemoveObjectOptions { } )
return convertMinioErr ( err )
2020-08-18 12:23:45 +08:00
}
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
func ( m * MinioStorage ) URL ( path , name string ) ( * url . URL , error ) {
reqParams := make ( url . Values )
// 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 ) + "\"" )
2020-10-16 04:51:06 +01:00
u , err := m . client . PresignedGetObject ( m . ctx , m . bucket , m . buildMinioPath ( path ) , 5 * time . Minute , reqParams )
return u , convertMinioErr ( err )
2020-08-18 12:23:45 +08:00
}
2020-09-29 17:05:13 +08:00
// IterateObjects iterates across the objects in the miniostorage
2023-05-14 06:33:25 +08:00
func ( m * MinioStorage ) IterateObjects ( dirName string , fn func ( path string , obj Object ) error ) error {
2022-01-20 18:46:10 +01:00
opts := minio . GetObjectOptions { }
2020-09-29 17:05:13 +08:00
lobjectCtx , cancel := context . WithCancel ( m . ctx )
defer cancel ( )
2023-03-13 18:23:51 +08:00
basePath := m . basePath
2023-05-14 06:33:25 +08:00
if dirName != "" {
// ending slash is required for avoiding matching like "foo/" and "foobar/" with prefix "foo"
basePath = m . buildMinioPath ( dirName ) + "/"
2023-03-13 18:23:51 +08:00
}
2020-09-29 17:05:13 +08:00
for mObjInfo := range m . client . ListObjects ( lobjectCtx , m . bucket , minio . ListObjectsOptions {
2023-03-13 18:23:51 +08:00
Prefix : basePath ,
2020-09-29 17:05:13 +08:00
Recursive : true ,
} ) {
object , err := m . client . GetObject ( lobjectCtx , m . bucket , mObjInfo . Key , opts )
if err != nil {
2020-10-16 04:51:06 +01:00
return convertMinioErr ( err )
2020-09-29 17:05:13 +08:00
}
if err := func ( object * minio . Object , fn func ( path string , obj Object ) error ) error {
defer object . Close ( )
2023-05-14 06:33:25 +08:00
return fn ( strings . TrimPrefix ( mObjInfo . Key , m . basePath ) , & minioObject { object } )
2020-09-29 17:05:13 +08:00
} ( object , fn ) ; err != nil {
2020-10-16 04:51:06 +01:00
return convertMinioErr ( err )
2020-09-29 17:05:13 +08:00
}
}
return nil
}
2020-10-13 04:58:34 +01:00
func init ( ) {
2023-06-14 11:42:38 +08:00
RegisterStorageType ( setting . MinioStorageType , NewMinioStorage )
2020-10-13 04:58:34 +01:00
}