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 }