feat: 支持 targetDir 指定上传目标目录(网盘式),固定路径模式不变
- 改动:init/check 接受可选 targetDir(相对 CloudDir,SanitizeDir 校验),merge 物理落盘 CloudDir/<targetDir>/ - 秒传跨目录 copy-on-hit:库把旧物理文件复制进目标目录,OnInstantHit 携带新路径;源缺失回退正常上传 - 断点续传按目录隔离:同 MD5 在途任务目录不同则另起新任务 - TaskInfo 新增 TargetDir(宿主须持久化);Repository 12 方法接口零变更,向后兼容 v1.0.x - 新增 dir.go(SanitizeDir/路径拼装反解/copyFile)与 service_test.go(10 个用例:双模式秒传/续传隔离/merge 回归) - 原因:上传位置原本固定在 CloudDir 根,宿主无法实现网盘式选目录上传
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package fileupload
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 目标目录(targetDir)约束:相对存储根(Options.CloudDir)。
|
||||
const (
|
||||
MaxDirLength = 500 // 总长度上限
|
||||
MaxDirDepth = 16 // 层级深度上限
|
||||
)
|
||||
|
||||
// SanitizeDir 规范客户端传入的目标目录,返回以 / 分隔的相对路径;
|
||||
// 空串表示根目录(固定路径模式)。防路径穿越规则:
|
||||
// - 反斜杠归一为 /,剔除控制字符,去首尾 /,按段校验;
|
||||
// - 拒绝 . / .. 段、空段、超长、超深、段内 Windows 非法字符(: * ? " < > |);
|
||||
// - 一律按相对路径处理(前导 / 仅剥离,不表示绝对路径)。
|
||||
func SanitizeDir(dir string) (string, error) {
|
||||
dir = strings.Map(func(r rune) rune {
|
||||
if r < 0x20 {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, strings.ReplaceAll(dir, "\\", "/"))
|
||||
dir = strings.TrimSpace(dir)
|
||||
dir = strings.Trim(dir, "/")
|
||||
if dir == "" {
|
||||
return "", nil
|
||||
}
|
||||
if len(dir) > MaxDirLength {
|
||||
return "", fmt.Errorf("目标目录超过长度上限 %d", MaxDirLength)
|
||||
}
|
||||
segs := strings.Split(dir, "/")
|
||||
if len(segs) > MaxDirDepth {
|
||||
return "", fmt.Errorf("目标目录超过层级上限 %d", MaxDirDepth)
|
||||
}
|
||||
for _, seg := range segs {
|
||||
switch seg {
|
||||
case "", ".", "..":
|
||||
return "", fmt.Errorf("目标目录含非法段 %q", seg)
|
||||
}
|
||||
if strings.ContainsAny(seg, `:*?"<>|`) {
|
||||
return "", fmt.Errorf("目标目录段 %q 含非法字符", seg)
|
||||
}
|
||||
}
|
||||
return strings.Join(segs, "/"), nil
|
||||
}
|
||||
|
||||
// buildStoragePath 拼 storagePath(Web 相对路径):
|
||||
// WebPathPrefix + "/" + targetDir + "/" + 产物名(前缀与目录为空时跳过对应段)。
|
||||
func buildStoragePath(webPathPrefix, targetDir, storageName string) string {
|
||||
parts := make([]string, 0, 3)
|
||||
if webPathPrefix != "" {
|
||||
parts = append(parts, webPathPrefix)
|
||||
}
|
||||
if targetDir != "" {
|
||||
parts = append(parts, targetDir)
|
||||
}
|
||||
parts = append(parts, storageName)
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
// physicalPath 由 storagePath(Web 相对路径)反解物理落盘路径:
|
||||
// 剥离 WebPathPrefix 后相对 CloudDir。秒传复制旧文件时用。
|
||||
func (s *service) physicalPath(storagePath string) string {
|
||||
rel := strings.ReplaceAll(storagePath, "\\", "/")
|
||||
if s.opts.WebPathPrefix != "" && strings.HasPrefix(rel, s.opts.WebPathPrefix+"/") {
|
||||
rel = rel[len(s.opts.WebPathPrefix)+1:]
|
||||
}
|
||||
return filepath.Join(s.opts.CloudDir, filepath.FromSlash(rel))
|
||||
}
|
||||
|
||||
// copyFile 文件级复制,失败时清理半成品。
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(dst)
|
||||
return err
|
||||
}
|
||||
if err = out.Close(); err != nil {
|
||||
_ = os.Remove(dst)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user