Init
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"cs-agent/internal/pkg/dto"
|
||||
"cs-agent/internal/pkg/enums"
|
||||
)
|
||||
|
||||
type UploadInfo struct {
|
||||
Prefix string
|
||||
Filename string
|
||||
FileSize int64
|
||||
MimeType string
|
||||
Principal *dto.AuthPrincipal
|
||||
}
|
||||
|
||||
type StoredFile struct {
|
||||
Provider enums.AssetProvider
|
||||
StorageKey string
|
||||
URL string
|
||||
Filename string
|
||||
FileSize int64
|
||||
MimeType string
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"cs-agent/internal/pkg/config"
|
||||
"cs-agent/internal/pkg/enums"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LocalStorage struct {
|
||||
cfg config.LocalStorageConfig
|
||||
}
|
||||
|
||||
func NewLocalStorage(cfg config.LocalStorageConfig) *LocalStorage {
|
||||
return &LocalStorage{cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *LocalStorage) ProviderType() enums.AssetProvider {
|
||||
return enums.AssetProviderLocal
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Upload(reader io.Reader, key string, info UploadInfo) (*StoredFile, error) {
|
||||
fullPath := filepath.Join(s.cfg.Root, filepath.FromSlash(key))
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dst, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StoredFile{
|
||||
Provider: enums.AssetProviderLocal,
|
||||
StorageKey: key,
|
||||
URL: strings.TrimRight(s.cfg.BaseURL, "/") + "/" + strings.TrimLeft(key, "/"),
|
||||
Filename: info.Filename,
|
||||
FileSize: info.FileSize,
|
||||
MimeType: info.MimeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LocalStorage) GetURL(key string) string {
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(s.cfg.BaseURL), "/")
|
||||
return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(key, "/")
|
||||
}
|
||||
|
||||
func (s *LocalStorage) GetSignedURL(key string) string {
|
||||
return s.GetURL(key)
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Delete(key string) error {
|
||||
fullPath := filepath.Join(s.cfg.Root, filepath.FromSlash(key))
|
||||
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(fullPath)
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Read(key string) (io.ReadCloser, error) {
|
||||
fullPath := filepath.Join(s.cfg.Root, filepath.FromSlash(strings.TrimSpace(key)))
|
||||
return os.Open(fullPath)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"cs-agent/internal/pkg/config"
|
||||
"cs-agent/internal/pkg/enums"
|
||||
"cs-agent/internal/pkg/errorsx"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/mlogclub/simple/common/strs"
|
||||
)
|
||||
|
||||
type OSSStorage struct {
|
||||
cfg config.OSSStorageConfig
|
||||
}
|
||||
|
||||
func NewOSSStorage(cfg config.OSSStorageConfig) *OSSStorage {
|
||||
return &OSSStorage{cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *OSSStorage) ProviderType() enums.AssetProvider {
|
||||
return enums.AssetProviderOSS
|
||||
}
|
||||
|
||||
func (s *OSSStorage) Upload(reader io.Reader, key string, info UploadInfo) (*StoredFile, error) {
|
||||
bucket, err := s.getBucket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key = normalizeOSSKey(key)
|
||||
options := []oss.Option{}
|
||||
if mimeType := strings.TrimSpace(info.MimeType); mimeType != "" {
|
||||
options = append(options, oss.ContentType(mimeType))
|
||||
}
|
||||
if info.FileSize > 0 {
|
||||
options = append(options, oss.ContentLength(info.FileSize))
|
||||
}
|
||||
|
||||
if err := bucket.PutObject(key, reader, options...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StoredFile{
|
||||
Provider: enums.AssetProviderOSS,
|
||||
StorageKey: key,
|
||||
URL: s.GetURL(key),
|
||||
Filename: info.Filename,
|
||||
FileSize: info.FileSize,
|
||||
MimeType: info.MimeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OSSStorage) GetURL(key string) string {
|
||||
key = normalizeOSSKey(key)
|
||||
if key == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if baseURL := strings.TrimRight(strings.TrimSpace(s.cfg.BaseURL), "/"); baseURL != "" {
|
||||
return baseURL + "/" + key
|
||||
}
|
||||
|
||||
if s.cfg.Private {
|
||||
bucket, err := s.getBucket()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
signedURL, err := bucket.SignURL(key, oss.HTTPGet, s.signedURLExpire())
|
||||
if err == nil {
|
||||
return signedURL
|
||||
}
|
||||
}
|
||||
|
||||
return s.objectURL(key)
|
||||
}
|
||||
|
||||
func (s *OSSStorage) GetSignedURL(key string) string {
|
||||
key = normalizeOSSKey(key)
|
||||
if strs.IsBlank(key) {
|
||||
return ""
|
||||
}
|
||||
if !s.cfg.Private {
|
||||
return s.GetURL(key)
|
||||
}
|
||||
|
||||
bucket, err := s.getBucket()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
signedURL, err := bucket.SignURL(key, oss.HTTPGet, s.signedURLExpire())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return signedURL
|
||||
}
|
||||
|
||||
func (s *OSSStorage) Delete(key string) error {
|
||||
bucket, err := s.getBucket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.DeleteObject(normalizeOSSKey(key))
|
||||
}
|
||||
|
||||
func (s *OSSStorage) Read(key string) (io.ReadCloser, error) {
|
||||
bucket, err := s.getBucket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bucket.GetObject(normalizeOSSKey(key))
|
||||
}
|
||||
|
||||
func (s *OSSStorage) getBucket() (*oss.Bucket, error) {
|
||||
if err := s.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := oss.New(
|
||||
s.endpoint(),
|
||||
strings.TrimSpace(s.cfg.AccessKeyID),
|
||||
strings.TrimSpace(s.cfg.AccessKeySecret),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.Bucket(strings.TrimSpace(s.cfg.Bucket))
|
||||
}
|
||||
|
||||
func (s *OSSStorage) validate() error {
|
||||
if strings.TrimSpace(s.cfg.Endpoint) == "" {
|
||||
return errorsx.InvalidParam("OSS endpoint 未配置")
|
||||
}
|
||||
if strings.TrimSpace(s.cfg.Bucket) == "" {
|
||||
return errorsx.InvalidParam("OSS bucket 未配置")
|
||||
}
|
||||
if strings.TrimSpace(s.cfg.AccessKeyID) == "" {
|
||||
return errorsx.InvalidParam("OSS accessKeyId 未配置")
|
||||
}
|
||||
if strings.TrimSpace(s.cfg.AccessKeySecret) == "" {
|
||||
return errorsx.InvalidParam("OSS accessKeySecret 未配置")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OSSStorage) endpoint() string {
|
||||
endpoint := strings.TrimSpace(s.cfg.Endpoint)
|
||||
if endpoint == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(endpoint, "://") {
|
||||
return endpoint
|
||||
}
|
||||
return "https://" + endpoint
|
||||
}
|
||||
|
||||
func (s *OSSStorage) objectURL(key string) string {
|
||||
u, err := url.Parse(s.endpoint())
|
||||
if err != nil || u.Host == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s://%s.%s/%s", u.Scheme, strings.TrimSpace(s.cfg.Bucket), u.Host, key)
|
||||
}
|
||||
|
||||
func (s *OSSStorage) signedURLExpire() int64 {
|
||||
if s.cfg.SignedURLExpire > 0 {
|
||||
return int64(s.cfg.SignedURLExpire)
|
||||
}
|
||||
return 600
|
||||
}
|
||||
|
||||
func normalizeOSSKey(key string) string {
|
||||
return strings.TrimLeft(strings.TrimSpace(key), "/")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"cs-agent/internal/pkg/config"
|
||||
"cs-agent/internal/pkg/enums"
|
||||
"cs-agent/internal/pkg/errorsx"
|
||||
"io"
|
||||
)
|
||||
|
||||
type FileStorageProvider interface {
|
||||
ProviderType() enums.AssetProvider
|
||||
Upload(reader io.Reader, key string, info UploadInfo) (*StoredFile, error)
|
||||
GetURL(key string) string
|
||||
GetSignedURL(key string) string
|
||||
Delete(key string) error
|
||||
Read(key string) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func GetDefault() (FileStorageProvider, error) {
|
||||
return NewProvider(config.Current().Storage.Default)
|
||||
}
|
||||
|
||||
func NewProvider(provider enums.AssetProvider) (FileStorageProvider, error) {
|
||||
cfg := config.Current().Storage
|
||||
|
||||
switch provider {
|
||||
case "", enums.AssetProviderLocal:
|
||||
return NewLocalStorage(cfg.Local), nil
|
||||
case enums.AssetProviderOSS:
|
||||
return NewOSSStorage(cfg.OSS), nil
|
||||
default:
|
||||
return nil, errorsx.InvalidParam("不支持的文件存储类型")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mlogclub/simple/common/strs"
|
||||
)
|
||||
|
||||
func GenerateStorageKey(info UploadInfo) (assetID string, storageKey string) {
|
||||
assetID = strs.UUID()
|
||||
var (
|
||||
env = currentAssetEnv()
|
||||
prefix = normalizeAssetPrefix(info.Prefix)
|
||||
datePath = time.Now().Format("2006/01/02")
|
||||
ext = getExt(info)
|
||||
)
|
||||
|
||||
storageKey = filepath.Join(
|
||||
env,
|
||||
prefix,
|
||||
datePath,
|
||||
assetID+ext,
|
||||
)
|
||||
storageKey = strings.TrimLeft(filepath.ToSlash(storageKey), "/")
|
||||
return
|
||||
}
|
||||
|
||||
func getExt(info UploadInfo) string {
|
||||
ext := strings.ToLower(filepath.Ext(strings.TrimSpace(info.Filename)))
|
||||
if ext == "" {
|
||||
ext = getExtByMimeType(info.MimeType)
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func getExtByMimeType(mimeType string) string {
|
||||
if strs.IsBlank(mimeType) {
|
||||
return ""
|
||||
}
|
||||
|
||||
mediaType, _, _ := mime.ParseMediaType(mimeType)
|
||||
if mediaType == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 处理一些非标准的 MIME 类型
|
||||
switch mediaType {
|
||||
case "image/jfif":
|
||||
return ".jpg"
|
||||
case "image/pjpeg":
|
||||
return ".jpg"
|
||||
case "image/jpeg":
|
||||
return ".jpg"
|
||||
default:
|
||||
exts, _ := mime.ExtensionsByType(mediaType)
|
||||
if len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeAssetPrefix(prefix string) string {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
prefix = strings.Trim(prefix, "/")
|
||||
prefix = strings.ReplaceAll(prefix, "..", "")
|
||||
prefix = filepath.ToSlash(prefix)
|
||||
for strings.Contains(prefix, "//") {
|
||||
prefix = strings.ReplaceAll(prefix, "//", "/")
|
||||
}
|
||||
return strings.Trim(prefix, "/")
|
||||
}
|
||||
|
||||
func currentAssetEnv() string {
|
||||
for _, key := range []string{"APP_ENV", "GO_ENV"} {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user