feat(media): add MinIO storage switch and HTTPS OSS endpoint
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user