# oss-uploader **Repository Path**: bingbingyihao/oss-uploader ## Basic Information - **Project Name**: oss-uploader - **Description**: No description available - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-09 - **Last Updated**: 2026-09-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # oss-uploader > 按目录层级批量上传本地文件到阿里云 OSS 的命令行小工具,专为「保留目录层级」这一诉求设计。 它是**纯命令行工具**(不启动 Web 容器),打成一个可直接 `java -jar` 运行的 fat jar。内置 `--dry-run` 预览、文件类型过滤、并发/重试、断点续传(增量)、大文件分片上传、失败清单等能力,并对常见坑(JDK 17 缺 JAXB、V4 签名、Windows 保留文件名等)做了针对性处理。 --- ## ✨ 功能特性 - **目录层级原样保留**:递归扫描本地目录,相对路径直接映射为 OSS object key(`/` 分隔),可按需加前缀 / 去掉源目录名那一层。 - **文件类型过滤**:默认只传 `pdf`,支持 `pdf,docx,xlsx`、`*.pdf`、`合同*`、`*`(全部)等写法。 - **上传前先预览**:`--dry-run` 只打印目录树与 key 映射,不上传,首次运行强烈建议先跑一遍。 - **并发 + 重试**:多文件并发上传,单文件失败自动指数退避重试;可识别「配置类错误」不盲目重试。 - **大文件分片上传**:超过阈值自动 multipart,分片级并发,失败自动 abort(不留碎片计费)。 - **断点续传 / 增量**:`--skip-existing` 跳过远端已存在对象,配合 `--incremental` 只传本地更新过的文件,重跑即续传。 - **可读的统计与排错**:成功/跳过/失败统计、失败原因 Top、失败明细 CSV、每个被排除项都有数量统计(解释「为什么少了文件」)。 - **友好 CLI 体验**:支持裸参数/`--source-dir`/`--source.dir` 多种写法、布尔开关 `--dry-run` / `--no-dry-run`、拼错参数给出「你是不是想写 --xxx」提示、`--help` / `--version`。 - **无密钥也能联调**:`--storage=local` 可把文件按同样层级复制到本地目录,用于先验证目录映射是否正确。 - **密钥不进代码**:AK/SK 从环境变量注入,支持 STS 临时凭证;命令行、配置文件优先级清晰。 --- ## 🛠 技术栈与环境要求 | 项 | 值 | | --- | --- | | JDK | 17+ | | 构建 | Maven(项目自带 `spring-boot-starter-parent`) | | Spring Boot | 3.4.6(仅 `spring-boot-starter`,**未引入 Web 容器**) | | 阿里云 OSS SDK | `aliyun-sdk-oss` 3.17.4 | | 运行形态 | 可执行 fat jar(`java -jar`) | > 注:JDK 9+ 已移除 JAXB,而 OSS SDK 3.17.4 仍引用 `javax.xml.bind`,pom 中已显式引入 `jaxb-api` / `javax.activation-api` 兜底,否则运行期会抛 `NoClassDefFoundError` 掩盖真实错误。 --- ## 🚀 快速开始 ### 1. 构建打包 ```bash mvn -DskipTests clean package ``` 产物为 `target/oss-uploader.jar`(pom 已配置 `finalName=oss-uploader`)。 ### 2. 配置访问密钥(推荐环境变量) **Windows(PowerShell,永久生效):** ```powershell setx OSS_ACCESS_KEY_ID "LTAI5t..." setx OSS_ACCESS_KEY_SECRET "你的AccessKeySecret" ``` **Linux / macOS:** ```bash export OSS_ACCESS_KEY_ID=LTAI5t... export OSS_ACCESS_KEY_SECRET=你的AccessKeySecret ``` > 使用 STS 临时凭证时再设置 `OSS_SESSION_TOKEN`;SDK 也兼容 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET` / `ALIBABA_CLOUD_SECURITY_TOKEN` 三套环境变量名。 > > ⚠️ 密钥务必通过环境变量注入,**不要写进 application.yml 或提交进 git**。 ### 3. 先预览,不实际上传(强烈建议) ```bash # Windows java -jar target\oss-uploader.jar --source-dir=D:\docs --dry-run # Linux / macOS java -jar target/oss-uploader.jar --source-dir=/data/docs --dry-run ``` 此模式**不需要** endpoint / bucket / AK,会打印上传后的目录树与 key 映射明细。 ### 4. 实际上传 ```bash java -jar target/oss-uploader.jar \ --source-dir=D:\docs \ --endpoint=oss-cn-hangzhou.aliyuncs.com \ --bucket=my-bucket \ --prefix=backup/ ``` 如果已把 `endpoint` / `bucket` 写进外置配置文件,命令可以极简: ```bash java -jar target/oss-uploader.jar D:\docs # 第 1 个裸参数即源目录;第二个裸参数可再指定类型,如:java -jar app.jar D:\docs pdf,docx ``` --- ## 🗂 目录层级映射规则 `object key = [prefix] + [源目录名] + 相对子目录层级 + 文件名` ``` 本地 D:\docs\2024\合同\采购.pdf 命令 --source-dir=D:\docs --prefix=contract/ OSS contract/docs/2024/合同/采购.pdf └─┬─┘└─┬─┘└────┬────┘ 前缀 根目录名 相对层级(原样保留) ``` - 默认 `include-root-name=true`:保留 `docs/` 这一层,便于辨认来源。 - 加 `--no-include-root-name`:`D:\docs\2024\a.pdf → 2024/a.pdf`。 - 目录深度过深导致 key 超过 OSS 1023 字节上限时,会在发起请求前拦截并提示你减小 `--prefix` 或去掉根目录名层。 --- ## ⚙️ 命令行参数一览 > 支持三种等价写法(Spring Boot 宽松绑定):`--source-dir=D:\docs`、`--source.dir=D:\docs`、`--source_dir D:\docs`。 > 布尔开关支持 `--dry-run` / `--no-dry-run` / `--dry-run=true`。`*` 在 shell 中记得加引号。 | 参数 | 默认值 | 说明 | | --- | --- | --- | | **必填类** | | | | `--source-dir=<路径>` | — | 要上传的本地目录;也可作为第 1 个裸参数 | | `--endpoint=<域名>` | 环境变量 | 如 `oss-cn-hangzhou.aliyuncs.com`(带不带 `https://` 均可) | | `--bucket=<名称>` | 环境变量 | OSS Bucket | | `--access-key-id` / `--access-key-secret` | 环境变量 | 临时调试用;生产推荐环境变量 | | **核心上传选项** | | | | `--file-types=<类型>` | `pdf` | `pdf,docx` / `*.pdf,*.doc` / `合同*,*_backup.docx` / `*`(全部) | | `--prefix=<路径>` | 空 | OSS 前缀,如 `--prefix=contract/2024/` | | `--no-include-root-name` | 保留根目录名 | 是否在 key 中保留源目录名这一层 | | `--recursive` / `--no-recursive` | 递归 | 是否递归子目录 | | `--excludes=<规则>` | 见 yml | 排除规则(Ant 风格),如 `--excludes=**/tmp/**,~$*.docx,*.tmp` | | `--include-hidden` | 关 | 是否包含 `.` 开头的隐藏文件/目录 | | `--follow-symbolic-links` | 关 | 跟随符号链接(注意死循环风险) | | `--max-size=500MB` / `--min-size=1KB` | 不限 | 按大小过滤(跳过超大/0 字节占位文件) | | `--limit=` | 0(不限) | 最多上传 n 个,分批验证用 | | **并发与重试** | | | | `--concurrency=` / `--threads` | 8 | 同时上传的文件数 | | `--retries=` / `--retry` | 2 | 单文件失败重试次数(指数退避) | | `--multipart-threshold=100MB` | 100MB | 超过则自动走分片上传 | | `--part-size=8MB` | 8MB | 分片大小(OSS 要求 ≥100KB,分片数 ≤10000,超限自动调大) | | **跳过与增量** | | | | `--dry-run` / `--preview` | 关 | 只预览映射结果,不做任何上传 | | `--skip-existing` | 关 | 远端已存在则跳过(重跑即断点续传) | | `--incremental` | 关 | 配合 `--skip-existing`:仅上传本地比远端新的文件 | | **OSS 连接 / 元数据** | | | | `--security-token` | 环境变量 | STS 临时凭证 | | `--signature-version=V1/V2/V4` | V1 | 新建 Bucket 若要求 V4 签名时改 V4 并填 region | | `--region=cn-hangzhou` | 空 | V4 签名必填 | | `--object-acl=` | default | `default / private / public-read / public-read-write` | | `--content-type=` | 按扩展名推断 | 留空时 pdf → `application/pdf`,浏览器可直接预览 | | `--crc-check=true/false` | true | 端到端 CRC64 校验 | | `--support-cname` / `--path-style` | 关 | 自定义域名访问 / 自建对象存储的路径式访问 | | **其它** | | | | `--failed-list=<路径>` | `upload-failed.csv` | 失败清单输出路径;`none` 表示不输出 | | `--verbose` / `--no-verbose` | 开 | 是否打印逐文件明细 | | `--config=<文件>` | — | 额外配置文件(.yml/.yaml/.properties),可重复指定多个 | | `--storage=local --local-dir=` | oss | 改为复制到本地目录,用于验证层级映射 | | `--help` / `-h`、`--version` / `-v` | | 帮助 / 版本 | | `-Doss-uploader.debug=true` | | JVM 属性:启动异常时打印完整堆栈 | 完整帮助可用 `java -jar oss-uploader.jar --help` 查看(也可直接双击运行空参数,会提示用法并以退出码 3 结束)。 --- ## 📁 配置体系 ### 配置来源与优先级 ``` 命令行参数(最高) > --config 外置文件 > ./config/ 目录 > jar 内 application.yml(最低) ``` - **命令行**:`CliParser` 把友好参数改写为 `--upload.xxx=...` 交给 Spring,利用 `commandLineArgs` 属性源天然获得最高优先级与宽松绑定,无需自造配置框架。 - **外置配置**:`--config=./config.yml`(可重复),文件缺失不会导致启动失败(自动加 `optional:` 前缀)。 - **约定目录**:在 jar 同目录放 `config/`,Spring Boot 会自动加载。 - **jar 内默认**:`src/main/resources/application.yml`,打包后要改默认需重新打包(一般不推荐,用上面两种)。 ### 三类配置命名空间 | 命名空间 | 作用 | 主要键 | | --- | --- | --- | | `upload.*` | 上传行为 | `source-dir`、`file-types`、`prefix`、`excludes`、`concurrency`、`retries`、`dry-run`、`skip-existing`、`incremental`、`multipart-threshold`、`part-size`、`limit`、`verbose`、`failed-list-file`、`max-detail-lines`… | | `oss.*` | OSS 连接 | `endpoint`、`bucket`、`access-key-id`、`access-key-secret`、`security-token`、`signature-version`、`region`、`crc-check`、`proxy-*`、`metadata.object-acl`、`metadata.content-type`、`metadata.user-metadata`… | | `storage.*` | 目标存储 | `type`(oss/local)、`local.dir`、`local.overwrite` | 密钥支持在 yml 里写成 `${OSS_ACCESS_KEY_ID:}` 占位符由环境变量填充。 --- ## 🧩 场景示例 ```bash # 1) 先看效果,不实际上传(无需 AK) java -jar oss-uploader.jar --source-dir=D:\docs --dry-run # 2) 把 D:\docs 传到 oss://my-bucket/backup/docs/,只传 pdf java -jar oss-uploader.jar --source-dir=D:\docs ^ --endpoint=oss-cn-hangzhou.aliyuncs.com --bucket=my-bucket ^ --prefix=backup/ # 3) pdf + docx,8 并发,跳过已存在,重跑即断点续传 java -jar oss-uploader.jar --source-dir=D:\docs --file-types=pdf,docx ^ --endpoint=oss-cn-hangzhou.aliyuncs.com --bucket=my-bucket --skip-existing # 4) 只传更新过的文件(增量) java -jar oss-uploader.jar --source-dir=D:\docs --skip-existing --incremental # 5) 本地镜像,先验证目录映射是否正确(无需 AK) java -jar oss-uploader.jar --source-dir=D:\docs --storage=local --local-dir=D:/tmp/oss-mirror # 6) 分批验证:每次只传 10 个,观察效果 java -jar oss-uploader.jar D:\docs --limit=10 --dry-run ``` 另有现成启动脚本(需先打包): - Windows:`scripts\upload.bat D:\docs pdf,docx`(连接信息改脚本顶部变量或用环境变量) - Linux/macOS:`./scripts/upload.sh /data/docs pdf,docx` --- ## 📊 输出与退出码 程序会打印:扫描摘要(命中/被排除/各类型数量)→ 进度明细 → 结果汇总(成功率、耗时、速率、类型分布、失败原因 Top、失败清单路径)。 **退出码**(可供脚本 `if errorlevel` / `$?` 判断): | 退出码 | 含义 | | --- | --- | | 0 | 全部成功 | | 1 | 存在失败项或被人为中断 | | 2 | 没有匹配到任何文件 | | 3 | 参数或环境错误(含 `--help` 空参提示) | 失败清单默认写到 `upload-failed.csv`(相对工作目录),记录 `relativePath,objectKey,size,lastModified,error`,修好问题后可直接重跑同一命令续传。 --- ## 📐 项目结构 ``` oss-uploader/ ├── pom.xml # Spring Boot 3.4.6 / aliyun-sdk-oss 3.17.4 / JDK 17 ├── scripts/ │ ├── upload.bat # Windows 一键上传脚本 │ └── upload.sh # Linux/macOS 一键上传脚本 └── src/ ├── main/ │ ├── java/com/example/ossupload/ │ │ ├── OssUploadApplication.java # 入口:ApplicationRunner + ExitCodeGenerator │ │ ├── BootLauncher.java # CLI → Spring 属性翻译、help/version、拼写纠错 │ │ ├── cli/ │ │ │ ├── CliParser.java # 参数解析:别名映射、布尔开关、裸参数、宽松绑定 │ │ │ └── CliOverrides.java # 帮助/用法文案(集中维护的参数权威表) │ │ ├── config/ │ │ │ ├── UploadProperties.java # @ConfigurationProperties("upload") │ │ │ ├── OssProperties.java # @ConfigurationProperties("oss"),AK 环境变量兜底 │ │ │ ├── StorageProperties.java # @ConfigurationProperties("storage") │ │ │ ├── OssClientConfig.java # OSSClient Bean(@Lazy,dry-run/local 不强制需要 AK) │ │ │ └── ByteSize.java # "8MB"/"1.5G" → 字节数 │ │ ├── service/ │ │ │ ├── UploadService.java # 主流程编排:校验→扫描→映射→预览/并发上传→汇总 │ │ │ ├── FileScanner.java # 递归扫描 + 各类过滤统计(walkFileTree) │ │ │ └── KeyResolver.java # 相对路径 → object key(前缀/根目录名/长度校验) │ │ ├── model/ │ │ │ ├── UploadItem.java # 待上传文件(record) │ │ │ ├── FileTypeFilter.java # 类型过滤:扩展名/通配符/文件名模式/* 全部 │ │ │ ├── UploadStats.java # 线程安全统计(成功/失败/字节/原因 Top) │ │ │ └── RelativePathTree.java # 扁平路径还原成目录树(dry-run 预览用) │ │ ├── storage/ │ │ │ ├── FileStorage.java # 目标存储抽象接口(新增 MinIO/S3 只需实现它) │ │ │ ├── OssFileStorage.java # OSS 实现:普通上传 + 分片上传(并发/abort 防碎片) │ │ │ ├── LocalStorage.java # 本地镜像实现(联调验证层级映射) │ │ │ ├── PartInputStream.java # 定长分片输入流(RandomAccessFile 精确定位) │ │ │ └── StorageResult.java # 传输结果(ETag/字节/是否跳过) │ │ └── util/Fmt.java # 字节/耗时/速率/时间戳/路径截断格式化 │ └── resources/application.yml # 内置默认配置(含详细注释) └── test/java/com/example/ossupload/ ├── CliParserTest.java # 命令行解析测试(13 例) └── CoreLogicTest.java # 过滤/key 映射/扫描/排序/统计/本地镜像等(27 例) ``` ### 主流程(UploadService.run) ``` validate() → FileTypeFilter.parse() → FileScanner.scan() → KeyResolver 计算 key → key 合法性校验(长度/风险文件名) → dry-run ? 预览输出 : doUpload() 并发上传(重试/跳过/统计/失败清单) ``` --- ## 🏗 架构与设计要点 1. **CLI 复用 Spring 配置体系(BootLauncher)** 将 `--xxx=yyy` 改写为 `--upload.xxx=yyy` 交给 `SpringApplication`,自动获得「命令行 > 外部配置 > jar 内默认」的优先级与宽松绑定/类型转换,不自己再造一套属性源。 2. **纯命令行形态** `spring.main.web-application-type=none`,不引入 Web 容器与内嵌 Tomcat,产物是干净的 CLI fat jar;关闭 banner / startup 噪音日志。 3. **按需懒加载 OSS Client(@Lazy)** 不加 `@Lazy` 的话,`--dry-run` / `--storage=local` 这类不需要 OSS 凭据的场景也会因缺 AK 启动失败。此处延迟到真正上传时才构建,并把缺密钥/缺 endpoint 翻译成可读中文提示。 4. **存储层抽象(FileStorage)** 上传行为只依赖接口(`upload / exists / remoteLastModified`),OSS 与 Local 互为可替换实现;将来接 MinIO / 腾讯 COS / AWS S3 只需新写一个实现。 5. **两级并发模型,避免线程池死锁** 文件级线程池(`--concurrency`)里提交分片任务到**独立**的分片线程池(并发 × 分片并发,上限 64)。若共用同一池,worker 提交分片后会阻塞等待,池被占满即死锁。OSS 客户端连接数也自动钳制为 ≥ 文件并发。 6. **分片上传的实现细节** - 用 `RandomAccessFile` + `PartInputStream` 从指定 offset 精确定位读取,避免从头 skip,每个分片独占句柄天然线程安全; - 显式设置 `Content-Length` 走定长实体,避免 chunked 编码带来的重试问题; - 任一/全部分片失败立即 `abort`,防止遗留碎片持续计费; - complete 前按 `partNumber` 升序排列(否则 OSS 报 InvalidPart); - 分片数自动扩张 partSize 以不超过 OSS 上限 10000。 7. **「该重试的才重试」的错误分类** 网络超时 / 连接异常 / 非明确的服务端错误码才走指数退避重试;`AccessDenied`、`NoSuchBucket`、`SignatureDoesNotMatch`、参数非法等配置类错误直接判失败——避免重试浪费时间并重复计费。 8. **防御性输入处理** - 过滤条件、排除规则、key 映射全部大小写/分隔符规范化(统一 `/`); - 扫描时统计每一步被排除的数量(类型/大小/规则/隐藏),并在汇总里明示「为什么少了文件」; - key 超 1023 字节在发起请求前批量拦截;上传前预警 Windows 保留名(con/nul…)与含 `~` 的文件名(SDK 签名已知缺陷); - LocalStorage 对目标路径做逃逸校验(`startsWith(root)`); - 支持自然排序(`a2.pdf` 排在 `a10.pdf` 前)。 9. **进程级优雅中断** 注册 shutdown hook:Ctrl+C 时标记中断、停止接收新任务,已上传结果照常统计与落失败清单,退出码为 1。 10. **不依赖 Spring 容器的核心可测性** `UploadService` / `FileScanner` 等核心逻辑可脱离容器直接构造调用,配合 `@TempDir` 做真实目录扫描测试。 --- ## 🧪 测试 ```bash mvn test ``` - `CliParserTest`:别名、布尔开关、否定式(`--no-dry-run`)、裸参数兜底、未知参数收集等解析行为。 - `CoreLogicTest`:类型过滤解析、key 层级映射与前缀规整、风险文件名/key 超长校验、递归与非递归扫描、排除规则与大小过滤、自然排序、LocalStorage 层级镜像与路径逃逸防护、ByteSize 解析、线程安全统计、目录树渲染、扩展名直方图等。 --- ## ❓ 常见问题(FAQ) **Q1:`java -jar` 报找不到 `javax/xml/bind/JAXBException`?** pom 已引入 `jaxb-api` / `javax.activation-api`,正常情况下不会出现。若自行裁剪依赖,需注意 JDK 9+ 已移除 JAXB 模块。 **Q2:上传报 `403 SignatureDoesNotMatch`?** - 先检查系统时间是否与网络同步(签名依赖时间); - 2024-06 后新建的部分地域 Bucket 强制要求 V4 签名:加 `--signature-version=V4 --region=cn-hangzhou`; - 文件名含 `~` 时,OSS SDK 3.17.4 的 V1/V4 签名可能触发该错误,工具会在上传前预警,建议先重命名。 **Q3:PDF 上传后浏览器强制下载而不是预览?** 工具按扩展名推断 Content-Type(pdf → `application/pdf`),正常可直接预览。若你通过 `--content-type` 覆盖成了 `application/octet-stream` 或使用了自定义元数据,需自行留意。 **Q4:重跑会重复计费 / 能断点续传吗?** 加 `--skip-existing`:远端已存在则跳过(HEAD 请求判断,几乎不耗流量),失败过的文件重跑即可续传;再加 `--incremental` 则只传本地更新过的文件。未加时已上传文件会整体覆盖一遍(OSS 按覆盖流量计费)。 **Q5:目录太大、文件太多怎么办?** - 用 `--limit=100` 先跑小批量验证; - 明细默认最多打印 2000 行(`--max-detail-lines` 可调),其余只更新进度; - 用 `--concurrency` 提升并行度,注意 OSS 客户端连接数、本地文件句柄与带宽上限。 **Q6:为什么扫描结果里少了文件?** 汇总会分类列出被排除数量:类型不符、大小不符、排除规则、隐藏文件、符号链接(默认不跟随)。`--file-types=*` 可上传全部文件;`--include-hidden` 包含隐藏文件。 **Q7:能否先不配 AK 就试用?** 可以。`--dry-run`(预览)与 `--storage=local --local-dir=...`(本地镜像)都不需要 OSS 凭据,可先验证目录层级映射与过滤规则是否正确,再切回 `--storage=oss` 正式上线。 **Q8:上传失败会留下碎片计费吗?** 分片上传任一环节失败都会主动 `abort` 释放碎片;若进程被强杀来不及 abort,可到 OSS 控制台「碎片管理」清理残留。 **Q9:如何看更详细的网络日志?** 把 `application.yml` 中 `com.aliyun.oss: debug` 那行注释打开即可看到 SDK 每个请求细节;排查启动期异常可用 `-Doss-uploader.debug=true` 打印完整堆栈。