Files
fileupload/dir.go
T
4566704 455360bff0 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 根,宿主无法实现网盘式选目录上传
2026-09-26 08:47:55 +08:00

99 lines
2.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}