feat: ota
This commit is contained in:
+174
@@ -0,0 +1,174 @@
|
||||
package oss
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lone-services/pkg/utils"
|
||||
|
||||
alioss "github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
)
|
||||
|
||||
const MaxUploadBytes = 128 << 20 // 128MB
|
||||
|
||||
var unsafeFileChars = regexp.MustCompile(`[^\w.\-\p{Han}]+`)
|
||||
|
||||
type Config struct {
|
||||
AccessKeyId string
|
||||
AccessKeySecret string
|
||||
BucketName string
|
||||
Endpoint string
|
||||
Host string
|
||||
Dir string // 固定前缀,如 test/chore/files/
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
URL string `json:"url"`
|
||||
Path string `json:"path"` // object key,如 test/chore/files/20260403/a.zip
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type UploadInput struct {
|
||||
Filename string
|
||||
Content []byte
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func LoadConfig() Config {
|
||||
cfg := Config{
|
||||
AccessKeyId: utils.GetConfigString("oss.accessKeyId"),
|
||||
AccessKeySecret: utils.GetConfigString("oss.accessKeySecret"),
|
||||
BucketName: utils.GetConfigString("oss.bucketName"),
|
||||
Endpoint: utils.GetConfigString("oss.endpoint"),
|
||||
Host: utils.GetConfigString("oss.host"),
|
||||
Dir: utils.GetConfigString("oss.dir"),
|
||||
}
|
||||
if cfg.BucketName == utils.StringEmpty {
|
||||
cfg.BucketName = "lone-images"
|
||||
}
|
||||
if cfg.Endpoint == utils.StringEmpty {
|
||||
cfg.Endpoint = "https://oss-accelerate.aliyuncs.com"
|
||||
}
|
||||
if cfg.Host == utils.StringEmpty {
|
||||
cfg.Host = "https://images.ailuowan.com"
|
||||
}
|
||||
if cfg.Dir == utils.StringEmpty {
|
||||
cfg.Dir = "test/chore/files/"
|
||||
}
|
||||
if !strings.HasPrefix(cfg.Host, "http") {
|
||||
cfg.Host = "https://" + cfg.Host
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.AccessKeyId == utils.StringEmpty || c.AccessKeySecret == utils.StringEmpty {
|
||||
return fmt.Errorf("oss access key missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Upload(in UploadInput) (*Result, error) {
|
||||
cfg := LoadConfig()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in.Content) == 0 {
|
||||
return nil, fmt.Errorf("empty file content")
|
||||
}
|
||||
if len(in.Content) > MaxUploadBytes {
|
||||
return nil, fmt.Errorf("file too large, max %d bytes", MaxUploadBytes)
|
||||
}
|
||||
|
||||
objectKey := buildObjectKey(cfg.Dir, in.Filename)
|
||||
endpoint := normalizeEndpoint(cfg.Endpoint, cfg.BucketName)
|
||||
|
||||
client, err := alioss.New(endpoint, cfg.AccessKeyId, cfg.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create oss client: %w", err)
|
||||
}
|
||||
bucket, err := client.Bucket(cfg.BucketName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get oss bucket: %w", err)
|
||||
}
|
||||
|
||||
opts := make([]alioss.Option, 0, 1)
|
||||
if ct := strings.TrimSpace(in.ContentType); ct != "" {
|
||||
opts = append(opts, alioss.ContentType(ct))
|
||||
}
|
||||
if err = bucket.PutObject(objectKey, bytes.NewReader(in.Content), opts...); err != nil {
|
||||
return nil, fmt.Errorf("put object: %w", err)
|
||||
}
|
||||
|
||||
host := strings.TrimRight(cfg.Host, "/")
|
||||
return &Result{
|
||||
URL: host + "/" + strings.TrimLeft(objectKey, "/"),
|
||||
Path: objectKey,
|
||||
Host: host,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeEndpoint(endpoint, bucket string) string {
|
||||
ep := strings.TrimSpace(endpoint)
|
||||
ep = strings.TrimPrefix(ep, "https://")
|
||||
ep = strings.TrimPrefix(ep, "http://")
|
||||
ep = strings.TrimSuffix(ep, "/")
|
||||
prefix := bucket + "."
|
||||
if bucket != "" && strings.HasPrefix(ep, prefix) {
|
||||
ep = strings.TrimPrefix(ep, prefix)
|
||||
}
|
||||
return ep
|
||||
}
|
||||
|
||||
// buildObjectKey: {oss.dir}{YYYYMMDD}/{filename}
|
||||
func buildObjectKey(configDir, filename string) string {
|
||||
base := strings.Trim(sanitizeDir(configDir), "/")
|
||||
if base != "" {
|
||||
base += "/"
|
||||
}
|
||||
dateDir := time.Now().In(utils.AsiaShanghai).Format(utils.YMD)
|
||||
name := sanitizeFilename(filename)
|
||||
return base + dateDir + "/" + name
|
||||
}
|
||||
|
||||
func sanitizeDir(dir string) string {
|
||||
dir = strings.TrimSpace(dir)
|
||||
dir = strings.ReplaceAll(dir, "\\", "/")
|
||||
dir = strings.Trim(dir, "/")
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
segs := strings.Split(dir, "/")
|
||||
clean := make([]string, 0, len(segs))
|
||||
for _, s := range segs {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" || s == "." || s == ".." {
|
||||
continue
|
||||
}
|
||||
clean = append(clean, s)
|
||||
}
|
||||
return strings.Join(clean, "/")
|
||||
}
|
||||
|
||||
func sanitizeFilename(filename string) string {
|
||||
name := path.Base(strings.ReplaceAll(filename, "\\", "/"))
|
||||
name = strings.TrimSpace(name)
|
||||
name = unsafeFileChars.ReplaceAllString(name, "_")
|
||||
name = strings.Trim(name, "._")
|
||||
if name == "" || name == "." || name == ".." {
|
||||
return "file"
|
||||
}
|
||||
if len(name) > 200 {
|
||||
ext := path.Ext(name)
|
||||
base := strings.TrimSuffix(name, ext)
|
||||
if len(base) > 180 {
|
||||
base = base[:180]
|
||||
}
|
||||
name = base + ext
|
||||
}
|
||||
return name
|
||||
}
|
||||
Reference in New Issue
Block a user