2020-08-18 07:23:45 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package storage
import (
"context"
"io"
"net/url"
2020-09-08 18:45:10 +03:00
"os"
2020-08-18 07:23:45 +03:00
"path"
"strings"
"time"
2020-10-16 06:51:06 +03:00
"code.gitea.io/gitea/modules/log"
2020-08-18 07:23:45 +03:00
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var (
2020-10-13 06:58:34 +03:00
_ ObjectStorage = & MinioStorage { }
quoteEscaper = strings . NewReplacer ( "\\" , "\\\\" , ` " ` , "\\\"" )
2020-08-18 07:23:45 +03:00
)
2020-09-29 12:05:13 +03:00
type minioObject struct {
* minio . Object
}
func ( m * minioObject ) Stat ( ) ( os . FileInfo , error ) {
oi , err := m . Object . Stat ( )
if err != nil {
2020-10-18 04:29:06 +03:00
return nil , convertMinioErr ( err )
2020-09-29 12:05:13 +03:00
}
return & minioFileInfo { oi } , nil
}
2020-10-13 06:58:34 +03:00
// MinioStorageType is the type descriptor for minio storage
const MinioStorageType Type = "minio"
// MinioStorageConfig represents the configuration for a minio storage
type MinioStorageConfig struct {
Endpoint string ` ini:"MINIO_ENDPOINT" `
AccessKeyID string ` ini:"MINIO_ACCESS_KEY_ID" `
SecretAccessKey string ` ini:"MINIO_SECRET_ACCESS_KEY" `
Bucket string ` ini:"MINIO_BUCKET" `
Location string ` ini:"MINIO_LOCATION" `
BasePath string ` ini:"MINIO_BASE_PATH" `
UseSSL bool ` ini:"MINIO_USE_SSL" `
}
2020-08-18 07:23:45 +03:00
// MinioStorage returns a minio bucket storage
type MinioStorage struct {
ctx context . Context
client * minio . Client
bucket string
basePath string
}
2020-10-16 06:51:06 +03: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 07:23:45 +03:00
// NewMinioStorage returns a minio storage
2020-10-13 06:58:34 +03:00
func NewMinioStorage ( ctx context . Context , cfg interface { } ) ( ObjectStorage , error ) {
configInterface , err := toConfig ( MinioStorageConfig { } , cfg )
if err != nil {
2020-10-16 06:51:06 +03:00
return nil , convertMinioErr ( err )
2020-10-13 06:58:34 +03:00
}
config := configInterface . ( MinioStorageConfig )
2020-10-16 06:51:06 +03:00
log . Info ( "Creating Minio storage at %s:%s with base path %s" , config . Endpoint , config . Bucket , config . BasePath )
2020-10-13 06:58:34 +03:00
minioClient , err := minio . New ( config . Endpoint , & minio . Options {
Creds : credentials . NewStaticV4 ( config . AccessKeyID , config . SecretAccessKey , "" ) ,
Secure : config . UseSSL ,
2020-08-18 07:23:45 +03:00
} )
if err != nil {
2020-10-16 06:51:06 +03:00
return nil , convertMinioErr ( err )
2020-08-18 07:23:45 +03:00
}
2020-10-13 06:58:34 +03:00
if err := minioClient . MakeBucket ( ctx , config . Bucket , minio . MakeBucketOptions {
Region : config . Location ,
2020-08-18 07:23:45 +03:00
} ) ; err != nil {
// Check to see if we already own this bucket (which happens if you run this twice)
2020-10-13 06:58:34 +03:00
exists , errBucketExists := minioClient . BucketExists ( ctx , config . Bucket )
2020-08-18 07:23:45 +03:00
if ! exists || errBucketExists != nil {
2020-10-16 06:51:06 +03:00
return nil , convertMinioErr ( err )
2020-08-18 07:23:45 +03:00
}
}
return & MinioStorage {
ctx : ctx ,
client : minioClient ,
2020-10-13 06:58:34 +03:00
bucket : config . Bucket ,
basePath : config . BasePath ,
2020-08-18 07:23:45 +03:00
} , nil
}
func ( m * MinioStorage ) buildMinioPath ( p string ) string {
return strings . TrimPrefix ( path . Join ( m . basePath , p ) , "/" )
}
// Open open a file
2020-09-08 18:45:10 +03:00
func ( m * MinioStorage ) Open ( path string ) ( Object , error ) {
2020-08-18 07:23:45 +03:00
var opts = minio . GetObjectOptions { }
object , err := m . client . GetObject ( m . ctx , m . bucket , m . buildMinioPath ( path ) , opts )
if err != nil {
2020-10-16 06:51:06 +03:00
return nil , convertMinioErr ( err )
2020-08-18 07:23:45 +03:00
}
2020-09-29 12:05:13 +03:00
return & minioObject { object } , nil
2020-08-18 07:23:45 +03:00
}
// Save save a file to minio
2021-04-03 19:19:59 +03:00
func ( m * MinioStorage ) Save ( path string , r io . Reader , size int64 ) ( int64 , error ) {
2020-08-18 07:23:45 +03:00
uploadInfo , err := m . client . PutObject (
m . ctx ,
m . bucket ,
m . buildMinioPath ( path ) ,
r ,
2021-04-03 19:19:59 +03:00
size ,
2020-08-18 07:23:45 +03:00
minio . PutObjectOptions { ContentType : "application/octet-stream" } ,
)
if err != nil {
2020-10-16 06:51:06 +03:00
return 0 , convertMinioErr ( err )
2020-08-18 07:23:45 +03:00
}
return uploadInfo . Size , nil
}
2020-09-08 18:45:10 +03:00
type minioFileInfo struct {
minio . ObjectInfo
}
func ( m minioFileInfo ) Name ( ) string {
2021-09-06 17:46:20 +03:00
return path . Base ( m . ObjectInfo . Key )
2020-09-08 18:45:10 +03:00
}
func ( m minioFileInfo ) Size ( ) int64 {
return m . ObjectInfo . Size
}
func ( m minioFileInfo ) ModTime ( ) time . Time {
return m . LastModified
}
2020-09-29 12:05:13 +03:00
func ( m minioFileInfo ) IsDir ( ) bool {
return strings . HasSuffix ( m . ObjectInfo . Key , "/" )
}
func ( m minioFileInfo ) Mode ( ) os . FileMode {
return os . ModePerm
}
func ( m minioFileInfo ) Sys ( ) interface { } {
return nil
}
2020-09-08 18:45:10 +03:00
// Stat returns the stat information of the object
2020-09-29 12:05:13 +03:00
func ( m * MinioStorage ) Stat ( path string ) ( os . FileInfo , error ) {
2020-09-08 18:45:10 +03:00
info , err := m . client . StatObject (
m . ctx ,
m . bucket ,
m . buildMinioPath ( path ) ,
minio . StatObjectOptions { } ,
)
if err != nil {
2020-10-16 06:51:06 +03:00
return nil , convertMinioErr ( err )
2020-09-08 18:45:10 +03:00
}
return & minioFileInfo { info } , nil
}
2020-08-18 07:23:45 +03:00
// Delete delete a file
func ( m * MinioStorage ) Delete ( path string ) error {
2020-10-16 06:51:06 +03:00
err := m . client . RemoveObject ( m . ctx , m . bucket , m . buildMinioPath ( path ) , minio . RemoveObjectOptions { } )
return convertMinioErr ( err )
2020-08-18 07:23:45 +03: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 06:51:06 +03:00
u , err := m . client . PresignedGetObject ( m . ctx , m . bucket , m . buildMinioPath ( path ) , 5 * time . Minute , reqParams )
return u , convertMinioErr ( err )
2020-08-18 07:23:45 +03:00
}
2020-09-29 12:05:13 +03:00
// IterateObjects iterates across the objects in the miniostorage
func ( m * MinioStorage ) IterateObjects ( fn func ( path string , obj Object ) error ) error {
var opts = minio . GetObjectOptions { }
lobjectCtx , cancel := context . WithCancel ( m . ctx )
defer cancel ( )
for mObjInfo := range m . client . ListObjects ( lobjectCtx , m . bucket , minio . ListObjectsOptions {
Prefix : m . basePath ,
Recursive : true ,
} ) {
object , err := m . client . GetObject ( lobjectCtx , m . bucket , mObjInfo . Key , opts )
if err != nil {
2020-10-16 06:51:06 +03:00
return convertMinioErr ( err )
2020-09-29 12:05:13 +03:00
}
if err := func ( object * minio . Object , fn func ( path string , obj Object ) error ) error {
defer object . Close ( )
2021-09-06 17:46:20 +03:00
return fn ( strings . TrimPrefix ( mObjInfo . Key , m . basePath ) , & minioObject { object } )
2020-09-29 12:05:13 +03:00
} ( object , fn ) ; err != nil {
2020-10-16 06:51:06 +03:00
return convertMinioErr ( err )
2020-09-29 12:05:13 +03:00
}
}
return nil
}
2020-10-13 06:58:34 +03:00
func init ( ) {
RegisterStorageType ( MinioStorageType , NewMinioStorage )
}