2020-08-18 07:23:45 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2020-08-18 07:23:45 +03:00
package storage
import (
"context"
2023-02-27 19:26:13 +03:00
"crypto/tls"
2023-03-28 18:10:24 +03:00
"fmt"
2020-08-18 07:23:45 +03:00
"io"
2023-02-27 19:26:13 +03:00
"net/http"
2020-08-18 07:23:45 +03:00
"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"
2023-06-14 06:42:38 +03:00
"code.gitea.io/gitea/modules/setting"
2023-03-08 15:17:39 +03:00
"code.gitea.io/gitea/modules/util"
2020-10-16 06:51:06 +03:00
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-08-18 07:23:45 +03:00
// MinioStorage returns a minio bucket storage
type MinioStorage struct {
2023-06-14 06:42:38 +03:00
cfg * setting . MinioStorageConfig
2020-08-18 07:23:45 +03:00
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
}
2023-09-12 05:19:39 +03:00
var getBucketVersioning = func ( ctx context . Context , minioClient * minio . Client , bucket string ) error {
_ , err := minioClient . GetBucketVersioning ( ctx , bucket )
return err
}
2020-08-18 07:23:45 +03:00
// NewMinioStorage returns a minio storage
2023-06-14 06:42:38 +03:00
func NewMinioStorage ( ctx context . Context , cfg * setting . Storage ) ( ObjectStorage , error ) {
config := cfg . MinioConfig
2023-03-28 18:10:24 +03:00
if config . ChecksumAlgorithm != "" && config . ChecksumAlgorithm != "default" && config . ChecksumAlgorithm != "md5" {
return nil , fmt . Errorf ( "invalid minio checksum algorithm: %s" , config . ChecksumAlgorithm )
}
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 )
2024-05-15 16:56:17 +03:00
var lookup minio . BucketLookupType
if config . BucketLookUpType == "auto" || config . BucketLookUpType == "" {
lookup = minio . BucketLookupAuto
} else if config . BucketLookUpType == "dns" {
lookup = minio . BucketLookupDNS
} else if config . BucketLookUpType == "path" {
lookup = minio . BucketLookupPath
} else {
return nil , fmt . Errorf ( "invalid minio bucket lookup type: %s" , config . BucketLookUpType )
}
2020-10-13 06:58:34 +03:00
minioClient , err := minio . New ( config . Endpoint , & minio . Options {
2024-11-22 23:12:06 +03:00
Creds : buildMinioCredentials ( config ) ,
2024-05-15 16:56:17 +03:00
Secure : config . UseSSL ,
Transport : & http . Transport { TLSClientConfig : & tls . Config { InsecureSkipVerify : config . InsecureSkipVerify } } ,
Region : config . Location ,
BucketLookup : lookup ,
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
}
2023-09-12 05:19:39 +03:00
// The GetBucketVersioning is only used for checking whether the Object Storage parameters are generally good. It doesn't need to succeed.
// The assumption is that if the API returns the HTTP code 400, then the parameters could be incorrect.
// Otherwise even if the request itself fails (403, 404, etc), the code should still continue because the parameters seem "good" enough.
// Keep in mind that GetBucketVersioning requires "owner" to really succeed, so it can't be used to check the existence.
// Not using "BucketExists (HeadBucket)" because it doesn't include detailed failure reasons.
err = getBucketVersioning ( ctx , minioClient , config . Bucket )
if err != nil {
errResp , ok := err . ( minio . ErrorResponse )
if ! ok {
return nil , err
}
if errResp . StatusCode == http . StatusBadRequest {
log . Error ( "S3 storage connection failure at %s:%s with base path %s and region: %s" , config . Endpoint , config . Bucket , config . Location , errResp . Message )
return nil , err
}
}
2023-08-12 11:03:54 +03:00
// Check to see if we already own this bucket
2023-08-21 19:20:11 +03:00
exists , err := minioClient . BucketExists ( ctx , config . Bucket )
if err != nil {
2023-08-12 11:03:54 +03:00
return nil , convertMinioErr ( err )
}
if ! exists {
if err := minioClient . MakeBucket ( ctx , config . Bucket , minio . MakeBucketOptions {
Region : config . Location ,
} ) ; err != nil {
2020-10-16 06:51:06 +03:00
return nil , convertMinioErr ( err )
2020-08-18 07:23:45 +03:00
}
}
return & MinioStorage {
2023-03-28 18:10:24 +03:00
cfg : & config ,
2020-08-18 07:23:45 +03:00
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 {
2023-09-13 04:18:52 +03:00
p = strings . TrimPrefix ( util . PathJoinRelX ( m . basePath , p ) , "/" ) // object store doesn't use slash for root path
2023-05-14 01:33:25 +03:00
if p == "." {
2023-09-13 04:18:52 +03:00
p = "" // object store doesn't use dot as relative path
}
return p
}
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
2023-05-14 01:33:25 +03:00
}
return p
2020-08-18 07:23:45 +03:00
}
2024-11-22 23:12:06 +03:00
func buildMinioCredentials ( config setting . MinioStorageConfig ) * credentials . Credentials {
2024-05-27 15:56:04 +03:00
// If static credentials are provided, use those
if config . AccessKeyID != "" {
return credentials . NewStaticV4 ( config . AccessKeyID , config . SecretAccessKey , "" )
}
// Otherwise, fallback to a credentials chain for S3 access
chain := [ ] credentials . Provider {
// configure based upon MINIO_ prefixed environment variables
& credentials . EnvMinio { } ,
// configure based upon AWS_ prefixed environment variables
& credentials . EnvAWS { } ,
// read credentials from MINIO_SHARED_CREDENTIALS_FILE
// environment variable, or default json config files
& credentials . FileMinioClient { } ,
// read credentials from AWS_SHARED_CREDENTIALS_FILE
// environment variable, or default credentials file
& credentials . FileAWSCredentials { } ,
// read IAM role from EC2 metadata endpoint if available
& credentials . IAM {
2024-11-22 23:12:06 +03:00
// passing in an empty Endpoint lets the IAM Provider
// decide which endpoint to resolve internally
Endpoint : config . IamEndpoint ,
2024-05-27 15:56:04 +03:00
Client : & http . Client {
Transport : http . DefaultTransport ,
} ,
} ,
}
return credentials . NewChainCredentials ( chain )
}
2023-03-28 18:10:24 +03:00
// Open opens a file
2020-09-08 18:45:10 +03:00
func ( m * MinioStorage ) Open ( path string ) ( Object , error ) {
2022-01-20 20:46:10 +03:00
opts := minio . GetObjectOptions { }
2020-08-18 07:23:45 +03:00
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
}
2023-03-28 18:10:24 +03:00
// Save saves 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 ,
2023-03-28 18:10:24 +03: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 07:23:45 +03:00
)
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
}
2023-07-04 21:36:08 +03:00
func ( m minioFileInfo ) Sys ( ) any {
2020-09-29 12:05:13 +03:00
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.
2024-10-31 18:28:25 +03:00
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
}
2020-08-18 07:23:45 +03:00
// 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
2023-05-14 01:33:25 +03:00
func ( m * MinioStorage ) IterateObjects ( dirName string , fn func ( path string , obj Object ) error ) error {
2022-01-20 20:46:10 +03:00
opts := minio . GetObjectOptions { }
2023-09-13 04:18:52 +03:00
for mObjInfo := range m . client . ListObjects ( m . ctx , m . bucket , minio . ListObjectsOptions {
Prefix : m . buildMinioDirPrefix ( dirName ) ,
2020-09-29 12:05:13 +03:00
Recursive : true ,
} ) {
2023-09-13 04:18:52 +03:00
object , err := m . client . GetObject ( m . ctx , m . bucket , mObjInfo . Key , opts )
2020-09-29 12:05:13 +03:00
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 ( )
2023-05-14 01:33:25 +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 ( ) {
2023-06-14 06:42:38 +03:00
RegisterStorageType ( setting . MinioStorageType , NewMinioStorage )
2020-10-13 06:58:34 +03:00
}