Files
im_server/server/media/oss/minio.go
T

162 lines
4.6 KiB
Go

package oss
import (
"context"
"errors"
"io"
"net/url"
"strings"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/tinode/chat/server/media"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
const minioLocationPrefix = "minio://"
type minioConfig struct {
Endpoint string `json:"endpoint"`
Bucket string `json:"bucket"`
Region string `json:"region"`
AccessKeyID string `json:"access_key_id"`
SecretAccessKey string `json:"secret_access_key"`
}
type minioStorage struct {
client *minio.Client
bucket string
}
func newMinIOStorage(conf minioConfig) (*minioStorage, error) {
if conf.Endpoint == "" || conf.Bucket == "" || conf.AccessKeyID == "" || conf.SecretAccessKey == "" {
return nil, nil
}
endpoint := strings.TrimSpace(conf.Endpoint)
secure := true
if strings.Contains(endpoint, "://") {
parsed, err := url.Parse(endpoint)
if err != nil || parsed.Host == "" || parsed.Path != "" || parsed.RawQuery != "" || parsed.Fragment != "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return nil, errors.New("invalid MinIO endpoint")
}
endpoint = parsed.Host
secure = parsed.Scheme == "https"
}
client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(conf.AccessKeyID, conf.SecretAccessKey, ""),
Secure: secure,
Region: conf.Region,
})
if err != nil {
return nil, err
}
return &minioStorage{client: client, bucket: conf.Bucket}, nil
}
func (oh *ossHandler) uploadMinIO(fd *types.FileDef, file io.Reader) (string, int64, error) {
key, err := tenantObjectKey(fd.TenantID, fd.Uid().String32(), fd.CreatedAt)
if err != nil {
return "", 0, err
}
if err := store.Files.StartUpload(fd.TenantID, fd); err != nil {
return "", 0, err
}
result, err := oh.minio.client.PutObject(context.Background(), oh.minio.bucket, key, file, remainingSize(file), minio.PutObjectOptions{
ContentType: fd.MimeType,
CacheControl: oh.conf.CacheControl,
})
if err != nil {
return "", 0, err
}
fd.Location = minioLocation(oh.minio.bucket, key)
fd.ETag = strings.Trim(result.ETag, "\"")
return oh.conf.ServeURL + media.FileURLName(fd), result.Size, nil
}
func (oh *ossHandler) uploadPublicMinIO(fd *media.PublicFileDef, file io.Reader) (string, int64, error) {
key, err := publicObjectKey(fd.Purpose, fd.ID, fd.CreatedAt)
if err != nil {
return "", 0, err
}
result, err := oh.minio.client.PutObject(context.Background(), oh.minio.bucket, key, file, remainingSize(file), minio.PutObjectOptions{
ContentType: fd.MimeType,
CacheControl: oh.conf.CacheControl,
})
if err != nil {
return "", 0, err
}
fd.Location = minioLocation(oh.minio.bucket, key)
fd.ETag = strings.Trim(result.ETag, "\"")
return fd.Location, result.Size, nil
}
func (oh *ossHandler) downloadMinIO(fd *types.FileDef) (*types.FileDef, media.ReadSeekCloser, error) {
bucket, key, err := oh.minioObject(fd.Location)
if err != nil {
return nil, nil, err
}
if fd.Size <= 0 {
meta, err := oh.minio.client.StatObject(context.Background(), bucket, key, minio.StatObjectOptions{})
if err != nil {
return nil, nil, err
}
fd.Size = meta.Size
}
reader := &ossReadSeekCloser{size: fd.Size}
reader.open = func(offset int64) (io.ReadCloser, error) {
options := minio.GetObjectOptions{}
if offset > 0 {
if err := options.SetRange(offset, fd.Size-1); err != nil {
return nil, err
}
}
return oh.minio.client.GetObject(context.Background(), bucket, key, options)
}
return fd, reader, nil
}
func (oh *ossHandler) deleteMinIO(location string) error {
bucket, key, err := oh.minioObject(location)
if err != nil {
return err
}
return oh.minio.client.RemoveObject(context.Background(), bucket, key, minio.RemoveObjectOptions{})
}
func (oh *ossHandler) minioObject(location string) (string, string, error) {
if oh.minio == nil {
return "", "", errors.New("MinIO is not configured for stored file")
}
bucket, key, ok := strings.Cut(strings.TrimPrefix(location, minioLocationPrefix), "/")
if !strings.HasPrefix(location, minioLocationPrefix) || !ok || bucket != oh.minio.bucket || key == "" {
return "", "", types.ErrMalformed
}
return bucket, key, nil
}
func minioLocation(bucket, key string) string {
return minioLocationPrefix + bucket + "/" + key
}
func remainingSize(reader io.Reader) int64 {
seeker, ok := reader.(io.Seeker)
if !ok {
return -1
}
current, err := seeker.Seek(0, io.SeekCurrent)
if err != nil {
return -1
}
end, err := seeker.Seek(0, io.SeekEnd)
if err != nil {
_, _ = seeker.Seek(current, io.SeekStart)
return -1
}
if _, err := seeker.Seek(current, io.SeekStart); err != nil {
return -1
}
return end - current
}