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 var unsafeFileChars = regexp.MustCompile(`[^\w.\-\p{Han}]+`) type Config struct { AccessKeyId string AccessKeySecret string BucketName string Endpoint string Host string Dir string } type Result struct { URL string `json:"url"` Path string `json:"path"` Host string `json:"host"` } type UploadInput struct { Filename string Content []byte ContentType string Dir 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) } dir := cfg.Dir if strings.TrimSpace(in.Dir) != "" { dir = in.Dir } objectKey := buildObjectKey(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 } 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 }