You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

105 lines
4.5 KiB

package console
// 文件直传(固件升级包 / 产品图片等)—— 后端签发「阿里云 OSS 预签名 PUT URL」,浏览器直传。
//
// 设计要点:
// 1. OSS 凭证来自【第三方服务配置(svc_config) → 存储 → 阿里云 OSS】(provider=aliyun_oss),
// 即 serviceconfig.vue 里落库、并按应用分离(app_name='' 为全局默认)的那份配置。
// ——不再走腾讯云 COS / 也不读那份纯演示未落库的「系统配置」页。
// 2. object key 由服务端强制置于 uploadRoot("EAIMAR") 根前缀下,形如
// EAIMAR/product/<产品id>/<文件名>,并做防路径穿越清洗。
// 3. 预签名 PUT 会把 Content-Type 计入签名,故前端 PUT 必须带同样的 Content-Type(uploadFile 已对齐)。
import (
"path"
"strings"
"yunyan/comm"
"yunyan/pb"
"yunyan/sys/aliyun/oss"
"github.com/gin-gonic/gin"
)
// ossStorageProvider svc_config 中对象存储服务的 provider 值(与 api_svctemplate.go 的
// storage_aliyun_oss 模板 provider 一致)。
const ossStorageProvider = comm.AliyunOSSProvider
// uploadRoot 定义已抽到 comm.OSSUploadRoot,与 App 上传(user_getuploadurl)共用。
const uploadRoot = comm.OSSUploadRoot
// aliyunOSSConf 定义已抽到 comm,供 console 后台直传与 App 上传(user_getuploadurl)
// 共用同一份读取逻辑——两边必须落在同一个桶里,否则后台传的固件和 App 传的录音会分家。
type aliyunOSSConf = comm.AliyunOSSConf
// loadAliyunOSSConf 读取阿里云 OSS 有效配置(取值顺序见 comm.LoadAliyunOSSConf)。
func (this *serverComp) loadAliyunOSSConf(appName string) (*aliyunOSSConf, error) {
return comm.LoadAliyunOSSConf(appName, this.options.EncryptKey)
}
// buildObjectKey 生成对象存储 key,强制置于 uploadRoot 根下并做防路径穿越清洗。
//
// dir 调用方给的逻辑目录(如 "product/12/"),可空。
// name 文件名(取 base 去掉任何目录部分)。
//
// 例:buildObjectKey("product/12/", "fw_v1.bin") => "EAIMAR/product/12/fw_v1.bin"
func buildObjectKey(dir, name string) string {
name = path.Base(strings.TrimSpace(name)) // 去掉任何目录部分,防止 name 里夹带 ../
if name == "" || name == "." || name == "/" {
name = "file"
}
key := path.Clean(path.Join(uploadRoot, dir, name)) // Join+Clean 合并 //、解析 ..
// 兜底:若 Clean 后逃逸出根前缀(dir 里塞了过量 ../),强制退回根下只保留文件名。
if key != uploadRoot && !strings.HasPrefix(key, uploadRoot+"/") {
key = uploadRoot + "/" + name
}
return key
}
// getUploadURL 前端固件/图片直传:读取阿里云 OSS 配置,为目标对象签发一个预签名 PUT URL。
//
// 路由沿用 api_getcostoken(前端 useApi.uploadFile 用该名,避免前端改路由)。
// 请求体 { path, filename, content_type };返回 { upload_url(预签名PUT), url(去签名的公网URL), key }。
func (this *serverComp) getUploadURL(c *gin.Context) {
var req struct {
Path string `json:"path"`
Filename string `json:"filename"`
ContentType string `json:"content_type"`
}
_ = c.ShouldBindJSON(&req)
if strings.TrimSpace(req.Filename) == "" {
writeErr(c, pb.ErrorCode_ReqParameterError, "filename 必填")
return
}
// 作用域:X-App-Id → 应用名(缺失/解析失败即全局默认)。
appName := ""
if appId := parseAppId(c.GetHeader("X-App-Id")); appId > 0 {
if app, err := this.module.model.getApp(appId); err == nil && app != nil {
appName = app.Name
}
}
conf, err := this.loadAliyunOSSConf(appName)
if err != nil {
writeErr(c, pb.ErrorCode_SystemError, err.Error())
return
}
presigner, err := oss.NewPresigner(conf.Endpoint, conf.AccessKeyId, conf.AccessSecret, conf.Bucket)
if err != nil {
writeErr(c, pb.ErrorCode_SystemError, "初始化阿里云 OSS 失败: "+err.Error())
return
}
key := buildObjectKey(req.Path, req.Filename)
ct := strings.TrimSpace(req.ContentType)
if ct == "" {
ct = "application/octet-stream"
}
uploadURL, err := presigner.SignPutURL(key, ct, 900) // 有效期 15 分钟
if err != nil {
writeErr(c, pb.ErrorCode_SystemError, "生成上传地址失败: "+err.Error())
return
}
// 公网访问 URL(存进 updatepackageaddress 的裸地址):
// 配了自定义域名 → https://<domain>/<key>(走 CDN);否则去掉预签名 query 的裸 OSS 地址。
publicURL := conf.PublicURL(uploadURL, key)
writeOK(c, gin.H{"upload_url": uploadURL, "url": publicURL, "key": key})
}