127 lines
4.0 KiB
Go
127 lines
4.0 KiB
Go
package service
|
||
|
||
import (
|
||
"crypto/md5"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"mime/multipart"
|
||
"strings"
|
||
"time"
|
||
|
||
"imagehost/internal/config"
|
||
"imagehost/internal/pan123"
|
||
)
|
||
|
||
type UploadService struct {
|
||
client *pan123.Client
|
||
}
|
||
|
||
func NewUploadService(client *pan123.Client) *UploadService {
|
||
return &UploadService{client: client}
|
||
}
|
||
|
||
// UploadFile 处理单一文件的五步上传编排
|
||
func (s *UploadService) UploadFile(fileHeader *multipart.FileHeader) (pan123.FileItem, error) {
|
||
file, err := fileHeader.Open()
|
||
if err != nil {
|
||
return pan123.FileItem{}, fmt.Errorf("打开分段文件失败: %w", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
// 【致命漏洞修复】使用流式处理替换无痛但高危的被动全量加载 (ReadAll)
|
||
h := md5.New()
|
||
if _, err := io.Copy(h, file); err != nil {
|
||
return pan123.FileItem{}, fmt.Errorf("动态计算文件 MD5 失败: %w", err)
|
||
}
|
||
etag := fmt.Sprintf("%x", h.Sum(nil))
|
||
size := fileHeader.Size
|
||
|
||
// 重置读取游标,准备让底层的 123pan.Client 直传该 file IO 节点
|
||
file.Seek(0, io.SeekStart)
|
||
fileName := fileHeader.Filename
|
||
|
||
var fileID string
|
||
|
||
// 步骤 1: 创建文件申报
|
||
createResp, err := s.client.CreateFile(config.GlobalConfig.ParentFileID, fileName, etag, size)
|
||
if err != nil {
|
||
return pan123.FileItem{}, fmt.Errorf("123pan 发送申报异常: %w", err)
|
||
}
|
||
|
||
if createResp.Reuse {
|
||
// 秒传命中
|
||
fileID = createResp.FileID
|
||
log.Printf("[上传] 文件 %s 命中服务器秒传!", fileName)
|
||
} else {
|
||
// 没有秒传,需要物理 PUT 上传切片
|
||
preuploadID := createResp.PreuploadID
|
||
|
||
// 步骤 2: 获取签发 URL
|
||
urlResp, err := s.client.GetUploadURL(preuploadID, 1)
|
||
if err != nil {
|
||
return pan123.FileItem{}, fmt.Errorf("123pan 获取直传地址失败: %w", err)
|
||
}
|
||
|
||
log.Printf("[上传] 开始推送 %s 到 123pan (大小: %d bytes)...", fileName, size)
|
||
// 步骤 3: 真正的二进制推送 (此处直接透传管道 file,坚决不在 RAM 内过境缓存!)
|
||
err = s.client.DoRawPUT(urlResp.PresignedURL, file, size)
|
||
if err != nil {
|
||
return pan123.FileItem{}, fmt.Errorf("123pan 数据切片推送失败: %w", err)
|
||
}
|
||
|
||
// 步骤 4: 推送完毕申报
|
||
completeResp, err := s.client.UploadComplete(preuploadID)
|
||
if err != nil {
|
||
return pan123.FileItem{}, fmt.Errorf("123pan 汇报上传完结失败: %w", err)
|
||
}
|
||
|
||
// 判决处理策略
|
||
if completeResp.Completed {
|
||
fileID = completeResp.FileID
|
||
} else if completeResp.Async {
|
||
// 步骤 5: 异步轮询 (最大容忍 20s)
|
||
log.Printf("[上传] 123pan 正在后端异步处理合并 %s...", fileName)
|
||
for i := 0; i < 20; i++ {
|
||
time.Sleep(1 * time.Second)
|
||
asyncResp, err := s.client.CheckAsyncResult(preuploadID)
|
||
if err != nil {
|
||
log.Printf("轮询报错 (将继续尝试): %v", err)
|
||
continue
|
||
}
|
||
if asyncResp.Completed {
|
||
fileID = asyncResp.FileID
|
||
break
|
||
}
|
||
}
|
||
if fileID == "" {
|
||
return pan123.FileItem{}, fmt.Errorf("123pan 异步处理落盘超时")
|
||
}
|
||
} else {
|
||
return pan123.FileItem{}, fmt.Errorf("123pan 返回的完结状态不能被理解: %v", completeResp)
|
||
}
|
||
}
|
||
|
||
log.Printf("[上传] 文件 %s 整体保存完毕, ID: %s", fileName, fileID)
|
||
|
||
// 根据图床需求,组装格式化的信息,此时可返回。
|
||
// 这里通过自定义协议拼接URL假若没有详情API能立即获取。(或者使用 FileID 查询)
|
||
downloadURL := ""
|
||
if config.GlobalConfig.CustomDomain != "" {
|
||
// 假设其静态资源访问域名形式为 domain/fileID/filename 或者是普通形式。
|
||
// 大部分直链空间只需: customDomain / fileName / file_id
|
||
// 作为通用兼容处理,如果直链域名未配特定的子径,先假设能直接根据路径拼接,具体的按实际调整。
|
||
domain := strings.TrimRight(config.GlobalConfig.CustomDomain, "/")
|
||
downloadURL = fmt.Sprintf("%s/%s", domain, fileName)
|
||
}
|
||
|
||
return pan123.FileItem{
|
||
FileID: fileID,
|
||
Filename: fileName,
|
||
Size: size,
|
||
Etag: etag,
|
||
DownloadURL: downloadURL,
|
||
UserSelfURL: downloadURL, // 我们以此值为向外暴露的主要直链URL
|
||
}, nil
|
||
}
|