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"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var (
_ ObjectStorage = & MinioStorage { }
quoteEscaper = strings . NewReplacer ( "\\" , "\\\\" , ` " ` , "\\\"" )
)
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 {
return nil , err
}
return & minioFileInfo { oi } , nil
}
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
}
// NewMinioStorage returns a minio storage
func NewMinioStorage ( ctx context . Context , endpoint , accessKeyID , secretAccessKey , bucket , location , basePath string , useSSL bool ) ( * MinioStorage , error ) {
minioClient , err := minio . New ( endpoint , & minio . Options {
Creds : credentials . NewStaticV4 ( accessKeyID , secretAccessKey , "" ) ,
Secure : useSSL ,
} )
if err != nil {
return nil , err
}
if err := minioClient . MakeBucket ( ctx , bucket , minio . MakeBucketOptions {
Region : location ,
} ) ; err != nil {
// Check to see if we already own this bucket (which happens if you run this twice)
exists , errBucketExists := minioClient . BucketExists ( ctx , bucket )
if ! exists || errBucketExists != nil {
return nil , err
}
}
return & MinioStorage {
ctx : ctx ,
client : minioClient ,
bucket : bucket ,
basePath : basePath ,
} , 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 {
return nil , err
}
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
func ( m * MinioStorage ) Save ( path string , r io . Reader ) ( int64 , error ) {
uploadInfo , err := m . client . PutObject (
m . ctx ,
m . bucket ,
m . buildMinioPath ( path ) ,
r ,
- 1 ,
minio . PutObjectOptions { ContentType : "application/octet-stream" } ,
)
if err != nil {
return 0 , err
}
return uploadInfo . Size , nil
}
2020-09-08 18:45:10 +03:00
type minioFileInfo struct {
minio . ObjectInfo
}
func ( m minioFileInfo ) Name ( ) string {
return m . ObjectInfo . Key
}
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 {
if errResp , ok := err . ( minio . ErrorResponse ) ; ok {
if errResp . Code == "NoSuchKey" {
return nil , os . ErrNotExist
}
}
return nil , err
}
return & minioFileInfo { info } , nil
}
2020-08-18 07:23:45 +03:00
// Delete delete a file
func ( m * MinioStorage ) Delete ( path string ) error {
return m . client . RemoveObject ( m . ctx , m . bucket , m . buildMinioPath ( path ) , minio . RemoveObjectOptions { } )
}
// 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 ) + "\"" )
return m . client . PresignedGetObject ( m . ctx , m . bucket , m . buildMinioPath ( path ) , 5 * time . Minute , reqParams )
}
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 {
return err
}
if err := func ( object * minio . Object , fn func ( path string , obj Object ) error ) error {
defer object . Close ( )
return fn ( strings . TrimPrefix ( m . basePath , mObjInfo . Key ) , & minioObject { object } )
} ( object , fn ) ; err != nil {
return err
}
}
return nil
}