@eyotang/s3flow 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +289 -2
- package/npm/cli.cjs +12 -0
- package/npm/install.cjs +74 -0
- package/npm/native/darwin-arm64/s3get +0 -0
- package/npm/native/darwin-arm64/s3push +0 -0
- package/npm/native/darwin-x64/s3get +0 -0
- package/npm/native/darwin-x64/s3push +0 -0
- package/npm/native/linux-arm64/s3get +0 -0
- package/npm/native/linux-arm64/s3push +0 -0
- package/npm/native/linux-x64/s3get +0 -0
- package/npm/native/linux-x64/s3push +0 -0
- package/npm/native/win32-arm64/s3get.exe +0 -0
- package/npm/native/win32-arm64/s3push.exe +0 -0
- package/npm/native/win32-x64/s3get.exe +0 -0
- package/npm/native/win32-x64/s3push.exe +0 -0
- package/npm/run.cjs +44 -0
- package/npm/s3get.cjs +3 -0
- package/npm/s3push.cjs +3 -0
- package/package.json +17 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 笨大神
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,290 @@
|
|
|
1
|
-
#
|
|
1
|
+
# s3flow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`s3flow` 包含两个独立命令:`s3get` 用于下载,`s3push` 用于上传文件或目录,共用 AWS 配置。
|
|
4
|
+
|
|
5
|
+
`s3get` 是一个基于 Bubble Tea 的 S3 并发分片下载工具。S3 URI 模式通过 `HeadObject` 获取对象大小,预签名 URL 模式通过 `GET bytes=0-0` 探测大小,随后自动规划 Range 分片并发下载;界面使用 Bubbles Animated Progress 展示实时进度、速度、剩余时间和最终耗时。
|
|
6
|
+
|
|
7
|
+
下载内容先写入目标目录中的临时文件。所有分片完成并同步到磁盘后才提交为最终文件;失败或取消时会清理临时文件。每个分片最多尝试 3 次,请求通过 ETag `If-Match` 固定对象版本,并校验每片响应的 ETag。若服务端未提供强 ETag,工具会自动降为单 Range 请求,避免并发拼接到不同版本的内容。
|
|
8
|
+
|
|
9
|
+
## 安装与运行(优先使用 npx)
|
|
10
|
+
|
|
11
|
+
### npm 一键安装
|
|
12
|
+
|
|
13
|
+
安装体验参考 [lark-cli](https://github.com/larksuite/cli) 的 `npx @larksuite/cli@latest install`:执行一次安装命令,之后直接使用两个工具。
|
|
14
|
+
|
|
15
|
+
需要 Node.js 18 或更高版本(含 npm/npx),无需 Go 或 GitHub 账号。
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @eyotang/s3flow@latest install
|
|
19
|
+
s3get --help
|
|
20
|
+
s3push --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
安装器使用 npm 的全局安装目录,同时安装两个命令。可指定可写目录:`npx @eyotang/s3flow@latest install --prefix <目录>`,并将输出提示中的命令目录加入 `PATH`。全局安装只保留当前平台的二进制,不依赖 npx 缓存;同一条安装命令可用于升级。已有 AWS 配置继续使用,不需要在安装时填写凭据。
|
|
24
|
+
|
|
25
|
+
临时使用而不进行全局安装:`npx @eyotang/s3flow@latest s3get --help` 或 `npx @eyotang/s3flow@latest s3push --help`。公开 npm 包内置二进制,不要求使用者访问私有 GitHub 仓库。
|
|
26
|
+
|
|
27
|
+
### 可选:GitHub Release 安装
|
|
28
|
+
|
|
29
|
+
需要 Node.js 18 或更高版本(含 npm/npx)。本仓库为私有仓库,先使用有仓库访问权限的 GitHub 账号登录 GitHub CLI(首次使用执行 `gh auth login`),下载安装包后用 npx 运行:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gh release download v0.1.2 --repo eyotang/s3flow --pattern eyotang-s3flow-0.1.2.tgz
|
|
33
|
+
npx --yes --package=./eyotang-s3flow-0.1.2.tgz s3get --help
|
|
34
|
+
npx --yes --package=./eyotang-s3flow-0.1.2.tgz s3push --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
也可在已登录 GitHub 的浏览器中从 [v0.1.2 发布页](https://github.com/eyotang/s3flow/releases/tag/v0.1.2) 下载 `eyotang-s3flow-0.1.2.tgz`,然后在下载目录执行上述 npx 命令。私有 Release 的匿名 URL 会返回 404,因此不能直接把下载 URL 传给 npx。
|
|
38
|
+
|
|
39
|
+
把 `--help` 替换成下文对应命令的参数即可下载或上传。npx 会自动将包安装到 npm 缓存,并选择当前系统的二进制;Windows、macOS、Linux 的 x86_64 和 ARM64 使用相同命令。包中包含两个工具的全部平台二进制,约 50 MB,无需 Go、编译器或安装脚本。再次运行可复用缓存;运行目录改变时请将 `--package` 指向安装包的实际路径。
|
|
40
|
+
|
|
41
|
+
也可以一次安装,之后直接使用 `s3get` 和 `s3push`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install --global ./eyotang-s3flow-0.1.2.tgz
|
|
45
|
+
s3get --help
|
|
46
|
+
s3push --help
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
GitHub Release 方式无需 npm 账号,不依赖 npm registry 中的同名包。npx 的包执行及缓存行为见 [npm 官方文档](https://docs.npmjs.com/cli/v11/commands/npx/)。
|
|
50
|
+
|
|
51
|
+
### 可选:直接下载二进制
|
|
52
|
+
|
|
53
|
+
从 [GitHub Releases](https://github.com/eyotang/s3flow/releases/latest) 下载对应系统和 CPU 架构的压缩包。每个包都包含 `s3get`、`s3push`、README 和许可证,无需安装 Go。
|
|
54
|
+
|
|
55
|
+
| 系统 | x86_64 / Intel / AMD | ARM64 |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| Windows | `s3flow_<版本>_windows_amd64.zip` | `s3flow_<版本>_windows_arm64.zip` |
|
|
58
|
+
| macOS | `s3flow_<版本>_darwin_amd64.tar.gz` | `s3flow_<版本>_darwin_arm64.tar.gz`(Apple Silicon) |
|
|
59
|
+
| Linux | `s3flow_<版本>_linux_amd64.tar.gz` | `s3flow_<版本>_linux_arm64.tar.gz` |
|
|
60
|
+
|
|
61
|
+
解压后可直接运行,或将两个程序放入 `PATH` 中的目录。Windows 的程序名为 `s3get.exe` 和 `s3push.exe`。发布页提供 `checksums.txt`,macOS/Linux 可用 `shasum -a 256 <压缩包>`,Windows PowerShell 可用 `Get-FileHash <压缩包> -Algorithm SHA256`,与文件中的对应值比较。
|
|
62
|
+
|
|
63
|
+
## 构建
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
go build -o s3get ./cmd/s3get
|
|
67
|
+
go build -o s3push ./cmd/s3push
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
也可以使用 Makefile,产物位于 `bin/s3get` 和 `bin/s3push`:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
make build
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 发布
|
|
77
|
+
|
|
78
|
+
本地构建全部六种系统/架构的发布包及 npx 安装包(需要 Go、Node.js/npm、Bash、tar、zip 和 shasum):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
make release VERSION=v0.1.2
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
产物位于 `dist/v0.1.2/`,包含六个系统压缩包、通用 npm 安装包和 `checksums.txt`。构建禁用 CGO,每个包包含两个命令。相同版本的输出目录存在时会拒绝覆盖。版本标签必须与 `package.json` 的版本一致;npm 安装包文件名由包名与版本号生成。
|
|
85
|
+
|
|
86
|
+
将发布配置提交并推送后,推送版本标签即可触发 GitHub Actions:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git tag v0.1.2
|
|
90
|
+
git push origin v0.1.2
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
工作流先在 Windows、macOS、Linux 上运行 Go 测试、Node.js 启动器测试和真实 npm 包安装/运行测试,通过后构建全部发布包并创建 GitHub Release。npm 测试在独立缓存中禁用网络和安装脚本,验证两个命令、退出码以及全局安装后清理缓存仍可运行。手动触发工作流只运行测试。带连字符的版本(例如 `v0.2.0-rc.1`)会标记为预发布。
|
|
94
|
+
|
|
95
|
+
需要发布到 npm registry 时,先确认包名的发布权限并完成 `npm login`,再使用 `npm publish <构建好的安装包.tgz> --access public` 发布;不能直接发布源码目录,因为其中没有内置二进制。当前 GitHub 工作流只向 GitHub Release 上传安装包。
|
|
96
|
+
|
|
97
|
+
## AWS 标准配置
|
|
98
|
+
|
|
99
|
+
未指定 `--aws` 时,工具直接使用 AWS SDK 的标准配置目录 `~/.aws`,读取原生 INI 格式的 `config` 与 `credentials`,不再使用 YAML。
|
|
100
|
+
|
|
101
|
+
`~/.aws/credentials`:
|
|
102
|
+
|
|
103
|
+
```ini
|
|
104
|
+
[default]
|
|
105
|
+
aws_access_key_id = your-access-key
|
|
106
|
+
aws_secret_access_key = your-secret-key
|
|
107
|
+
# 临时凭据可选
|
|
108
|
+
aws_session_token = your-session-token
|
|
109
|
+
|
|
110
|
+
[production]
|
|
111
|
+
aws_access_key_id = production-access-key
|
|
112
|
+
aws_secret_access_key = production-secret-key
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`~/.aws/config`:
|
|
116
|
+
|
|
117
|
+
```ini
|
|
118
|
+
[default]
|
|
119
|
+
region = cn-sh
|
|
120
|
+
endpoint_url = https://s3.example.com
|
|
121
|
+
|
|
122
|
+
[profile production]
|
|
123
|
+
region = cn-sh
|
|
124
|
+
endpoint_url = https://production-s3.example.com
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`-p NAME` 是 `--profile NAME` 的缩写,含义与 AWS CLI 的 `aws --profile NAME` 一致。它在 `credentials` 中选择 `[NAME]`,在 `config` 中选择 `[profile NAME]`;未指定时使用 `AWS_PROFILE`/`AWS_DEFAULT_PROFILE`,再缺省为 `default`。
|
|
128
|
+
|
|
129
|
+
`--aws DIR` 用来指定另一套 AWS 配置目录,工具读取 `DIR/config` 和 `DIR/credentials`。例如:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
./s3get --aws ./aws-production --profile production \
|
|
133
|
+
s3://my-bucket/releases/app.tar.zst
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
未指定 `--aws` 时,也兼容 AWS 标准的 `AWS_CONFIG_FILE` 与 `AWS_SHARED_CREDENTIALS_FILE`。
|
|
137
|
+
|
|
138
|
+
凭据也可以通过环境变量传入:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
export AWS_ACCESS_KEY_ID=your-access-key
|
|
142
|
+
export AWS_SECRET_ACCESS_KEY=your-secret-key
|
|
143
|
+
export AWS_SESSION_TOKEN=your-session-token # 可选
|
|
144
|
+
export AWS_REGION=cn-sh
|
|
145
|
+
export AWS_ENDPOINT_URL_S3=https://s3.example.com
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
配置与凭据作为两个独立分组解析:
|
|
149
|
+
|
|
150
|
+
- 配置组 `endpoint/region`:命令行参数优先于 AWS 环境变量,AWS 环境变量优先于选中的 shared config profile;region 最终缺省为 `cn-sh`。
|
|
151
|
+
- 凭据组 `ak/sk/session_token`:直接传入的 `--ak/--sk/--session-token` 优先级最高;显式使用 `-p/--profile` 时,按 AWS CLI 语义使用该 profile 的凭据;未显式选择 profile 时,环境凭据优先于 `AWS_PROFILE`/`default` 对应的 shared credentials。AK/SK 按完整的一组读取,不会跨来源拼接。
|
|
152
|
+
|
|
153
|
+
因此可以在命令行提供 endpoint 和 region,同时只从环境变量读取凭据。
|
|
154
|
+
|
|
155
|
+
`--path-style` 默认为 `true`,工具会将 bucket 放入 URL 路径:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
https://s3.example.com/my-bucket/path/to/object.bin
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
这可直接兼容 XSky、MinIO 等不提供 `bucket.endpoint` DNS 解析的 S3 兼容服务。如果目标服务要求虚拟主机寻址,可传入 `--path-style=false`,请求会改为 `https://my-bucket.s3.example.com/path/to/object.bin`。
|
|
162
|
+
|
|
163
|
+
S3 URI 的对象 key 既可以直接使用 Unicode,也可以使用 URL percent 编码。工具会将路径编码解码一次,再交给 AWS SDK 生成请求,例如下面两种写法指向同一个对象:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
s3://checkpoint/线上服/object.gz
|
|
167
|
+
s3://checkpoint/%E7%BA%BF%E4%B8%8A%E6%9C%8D/object.gz
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`+` 按路径字符处理,不会转换为空格;对象名中的字面 `%` 应写成 `%25`。
|
|
171
|
+
|
|
172
|
+
## 使用
|
|
173
|
+
|
|
174
|
+
参数定义和帮助文案由 Go `flag` 包统一生成:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
./s3get --help
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
使用 profile:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
./s3get -p production s3://my-bucket/releases/app.tar.zst
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
使用环境变量凭据和默认配置:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
AWS_ACCESS_KEY_ID=your-access-key \
|
|
190
|
+
AWS_SECRET_ACCESS_KEY=your-secret-key \
|
|
191
|
+
./s3get s3://my-bucket/releases/app.tar.zst
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
命令行配置配合环境变量凭据:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
AWS_ACCESS_KEY_ID=your-access-key \
|
|
198
|
+
AWS_SECRET_ACCESS_KEY=your-secret-key \
|
|
199
|
+
./s3get --endpoint https://s3.example.com --region cn-sh \
|
|
200
|
+
s3://my-bucket/releases/app.tar.zst
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
直接传入连接参数:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
./s3get \
|
|
207
|
+
--endpoint https://s3.example.com \
|
|
208
|
+
--ak your-access-key \
|
|
209
|
+
--sk your-secret-key \
|
|
210
|
+
--region cn-sh \
|
|
211
|
+
s3://my-bucket/path/to/object.bin \
|
|
212
|
+
./object.bin
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
调整并发和分片大小:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
./s3get --profile production --concurrency 16 --part-size 32MiB \
|
|
219
|
+
-o ./downloads/object.bin s3://my-bucket/path/to/object.bin
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## 下载目录
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
./s3get -p production s3://my-bucket/backups/ ./backups
|
|
226
|
+
./s3get --recursive s3://my-bucket/backups ./backups
|
|
227
|
+
./s3get s3://my-bucket/ ./bucket-copy
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
以 `/` 结尾的 S3 地址或仅指定 bucket 时按目录下载;也可用 `--recursive` / `-r` 明确指定。不带 `/` 时优先读取同名文件;仅当 `HeadObject` 返回 404,才用 `ListObjectsV2(Prefix=key+"/", MaxKeys=1)` 探测目录,找到对象后自动切换为目录下载。文件与同名目录同时存在时优先下载文件。403、连接失败等错误不会触发目录探测;探测失败会明确报告,文件和目录都不存在则报错。目录内容保留相对路径,例如 `backups/a/b.txt` 保存为 `./backups/a/b.txt`。未指定输出目录时使用前缀末级名称,bucket 根目录使用 bucket 名称。目录列举支持分页,需要 `ListBucket` 权限;预签名 GET URL 仅支持单对象下载。
|
|
231
|
+
|
|
232
|
+
目录内文件依次下载,每个文件的分片并行。界面显示整体和当前文件进度条、文件数量及分片汇总,不逐条展示分片。零字节目录标记跳过;不安全的相对路径或文件/目录冲突会报错。下载文件通过受限目录句柄写入,不能通过对象路径或符号链接逃逸输出目录。已有文件仍需 `--force` 才覆盖;失败时保留之前完成的文件并清理当前临时文件。
|
|
233
|
+
|
|
234
|
+
## 预签名 URL
|
|
235
|
+
|
|
236
|
+
可以把带 endpoint 和签名查询参数的 GET URL 直接作为下载源。查询参数应使用引号保护,避免 shell 把 `&` 解释为控制符:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
./s3get --concurrency 16 --part-size 32MiB \
|
|
240
|
+
'https://s3.example.com/my-bucket/path/object.bin?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=...'
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
预签名模式不读取本地 AK/SK,也不使用 `--endpoint` 重写 URL。工具先发送 `GET` 与 `Range: bytes=0-0`,从 `Content-Range` 获得对象总大小,再复用原始 URL 并行请求各字节范围。原始 query 会逐请求保留,但不会显示在终端中。
|
|
244
|
+
|
|
245
|
+
该模式要求服务端支持 Range,并且 `X-Amz-SignedHeaders` 不能包含 `range`。若 Range 已参与签名,同一个 URL 只能使用签名时的固定 Range,无法改写成多个分片;请重新生成只签名 `host` 的 GET URL,或为每个分片分别签名。GET 预签名 URL 也不能改成 HEAD,因此工具使用 `bytes=0-0` GET 探测大小。
|
|
246
|
+
|
|
247
|
+
常用参数:
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
--endpoint URL S3 endpoint
|
|
251
|
+
--ak VALUE access key
|
|
252
|
+
--sk VALUE secret key
|
|
253
|
+
--session-token VALUE 临时凭据 token
|
|
254
|
+
--region VALUE region,默认 cn-sh
|
|
255
|
+
--aws DIR AWS 配置目录,默认 ~/.aws
|
|
256
|
+
-p, --profile NAME AWS 命名 profile
|
|
257
|
+
--path-style 是否使用 path-style,默认 true;false 使用 bucket.endpoint
|
|
258
|
+
-o, --output FILE 输出文件
|
|
259
|
+
--part-size SIZE 分片大小,默认 128MiB
|
|
260
|
+
--concurrency N 并发数,默认 16,最大 128
|
|
261
|
+
--force 覆盖已有输出文件
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
下载过程中按 `q`、`esc` 或 `ctrl+c` 会取消请求并清理临时文件。
|
|
265
|
+
|
|
266
|
+
默认模式会用同目录 hard link 提交文件,以保证不覆盖下载期间新建的同名文件。若目标文件系统不支持 hard link,工具会安全退出;确认目标路径允许覆盖后,可用 `--force` 通过 rename 提交。
|
|
267
|
+
|
|
268
|
+
## 上传文件或目录
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
./s3push --help
|
|
272
|
+
./s3push -p production ./archive.tar.zst s3://my-bucket/backups/archive.tar.zst
|
|
273
|
+
./s3push --concurrency 16 --part-size 128MiB ./data s3://my-bucket/backups/
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
文件目标以 `/` 结尾(或仅指定 bucket)时,自动追加本地文件名;否则使用指定的完整 key。目录上传其内容并保留相对路径,例如 `data/a/b.txt` 上传到 `backups/a/b.txt`,不包含顶层 `data`。本地目录为空或仅含空子目录时,在连接 S3 前报错退出;`--force` 不跳过此检查。零字节文件仍可上传。符号链接和特殊文件会在上传前报错。
|
|
277
|
+
|
|
278
|
+
小文件和空文件使用 `PutObject`,超过分片大小的文件使用并行 multipart upload。目录中的文件依次上传,每个大文件内部并行;分片默认 128MiB,允许 5MiB–5GiB,并发默认 16、最大 128。请求重试由 AWS SDK 处理。
|
|
279
|
+
|
|
280
|
+
默认在写入任何文件之前检查目标目录前缀,存在任何文件(包括不同名或子目录中的文件)都会拒绝上传,需显式指定 `--force` 强推;仅有零字节目录标记视为空目录。单文件上传检查目标 key 所在的父前缀,上传至 bucket 根目录则检查整个 bucket。检查需要 `ListBucket` 权限,检查失败会停止上传。写入时仍通过 `If-None-Match: *` 保护同名对象;`--force` 跳过空目录检查并允许覆盖同名对象,不删除其他对象。前缀检查不是目录锁,检查后其他客户端仍可能写入。S3 兼容服务需要支持条件写入,不支持时请求可能失败。失败或 Ctrl+C 取消时会尝试清理未完成的 multipart upload,清理失败会报告 upload ID;已上传成功的文件保留,目录上传不是原子操作。上传期间请保持源文件不变。
|
|
281
|
+
|
|
282
|
+
上传使用与下载一致的紫色 Bubble Tea 动态进度界面,显示当前文件、目标、已完成/总文件数、当前文件分片数、整体字节进度、平均速度和预计剩余时间,结束时显示耗时。界面以整体和当前文件两条进度条为主,未完成部分显示可见轨道,分片仅显示汇总,不逐条铺满屏幕。按 `q`、`esc` 或 `Ctrl+C` 取消并等待未完成分片清理。字节进度按请求体读取量估算,SDK 重试回卷时会回退,完成状态以服务端成功响应为准。上传支持上述 AWS 连接和认证参数,不支持下载专用的 `--output` 或预签名 GET URL。
|
|
283
|
+
|
|
284
|
+
## 代码结构
|
|
285
|
+
|
|
286
|
+
- `cmd/s3get/`:下载命令的入口、参数处理、下载逻辑、Bubble Tea 界面和测试。
|
|
287
|
+
- `cmd/s3push/`:上传命令的入口、参数处理、目录遍历、分片上传、Bubble Tea 界面和测试。
|
|
288
|
+
- `internal/pkg/`:两个命令共用的 AWS 配置、连接参数、S3 客户端、URI 解析、字节大小和分片规划。
|
|
289
|
+
|
|
290
|
+
两个命令使用一致的文件职责:`main.go` 负责入口、执行流程和退出码,`cli.go` 负责参数、帮助和配置解析,`logic.go` 负责传输逻辑;对应测试放在同名 `_test.go` 中。两个命令均使用各自的 `ui.go` 展示动态进度。
|
package/npm/cli.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { run } = require('./run.cjs');
|
|
5
|
+
const [command, ...args] = process.argv.slice(2);
|
|
6
|
+
if (!command || command === '--help' || command === '-h') {
|
|
7
|
+
console.log('Usage: s3flow <install|s3get|s3push> [arguments]\n\n install Install s3get and s3push globally\n s3get Download S3 files or directories\n s3push Upload files or directories\n\nRun s3flow <command> --help for command options.');
|
|
8
|
+
} else if (command === 'install') {
|
|
9
|
+
require('./install.cjs').install(args);
|
|
10
|
+
} else {
|
|
11
|
+
run(command, args);
|
|
12
|
+
}
|
package/npm/install.cjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
const { binaryPath } = require('./run.cjs');
|
|
8
|
+
|
|
9
|
+
function parseArgs(args) {
|
|
10
|
+
if (args.length === 0) return {};
|
|
11
|
+
if (args.length === 1 && ['--help', '-h'].includes(args[0])) return { help: true };
|
|
12
|
+
if (args.length === 2 && args[0] === '--prefix' && args[1] && !args[1].startsWith('-')) {
|
|
13
|
+
return { prefix: path.resolve(args[1]) };
|
|
14
|
+
}
|
|
15
|
+
throw new Error('Usage: s3flow install [--prefix DIRECTORY]');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function install(args) {
|
|
19
|
+
let temp;
|
|
20
|
+
try {
|
|
21
|
+
const options = parseArgs(args);
|
|
22
|
+
if (options.help) {
|
|
23
|
+
console.log('Usage: s3flow install [--prefix DIRECTORY]\n\nInstall both commands into the npm global prefix.\nUse --prefix to select a writable installation directory.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const npmCLI = process.env.npm_execpath;
|
|
27
|
+
if (!npmCLI || !fs.existsSync(npmCLI)) {
|
|
28
|
+
throw new Error('Run this installer through npx so it can locate npm.');
|
|
29
|
+
}
|
|
30
|
+
// Check both binaries before changing the global installation.
|
|
31
|
+
for (const command of ['s3get', 's3push']) fs.accessSync(binaryPath(command), fs.constants.R_OK);
|
|
32
|
+
|
|
33
|
+
temp = fs.mkdtempSync(path.join(os.tmpdir(), 's3flow-install-'));
|
|
34
|
+
const stage = path.join(temp, 'package');
|
|
35
|
+
const native = path.join(stage, 'npm', 'native', `${process.platform}-${process.arch}`);
|
|
36
|
+
fs.mkdirSync(native, { recursive: true });
|
|
37
|
+
const root = path.resolve(__dirname, '..');
|
|
38
|
+
for (const file of ['package.json', 'README.md', 'LICENSE']) fs.copyFileSync(path.join(root, file), path.join(stage, file));
|
|
39
|
+
for (const file of fs.readdirSync(__dirname).filter(name => name.endsWith('.cjs'))) {
|
|
40
|
+
fs.copyFileSync(path.join(__dirname, file), path.join(stage, 'npm', file));
|
|
41
|
+
}
|
|
42
|
+
for (const command of ['s3get', 's3push']) {
|
|
43
|
+
const source = binaryPath(command);
|
|
44
|
+
fs.copyFileSync(source, path.join(native, path.basename(source)));
|
|
45
|
+
fs.chmodSync(path.join(native, path.basename(source)), 0o755);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function npm(args, cwd = stage) {
|
|
49
|
+
const result = spawnSync(process.execPath, [npmCLI, ...args], { cwd, stdio: 'inherit', shell: false });
|
|
50
|
+
if (result.error) throw result.error;
|
|
51
|
+
if (result.status !== 0) throw new Error(`npm exited with ${result.status ?? result.signal}`);
|
|
52
|
+
}
|
|
53
|
+
// Pack first: a global link to the npx cache would break after cache cleanup.
|
|
54
|
+
npm(['pack', '--ignore-scripts', '--pack-destination', temp]);
|
|
55
|
+
const archives = fs.readdirSync(temp).filter(name => name.endsWith('.tgz'));
|
|
56
|
+
if (archives.length !== 1) throw new Error('Unable to locate the installation package.');
|
|
57
|
+
const prefixArgs = options.prefix ? ['--prefix', options.prefix] : [];
|
|
58
|
+
npm(['install', '--global', '--ignore-scripts', '--no-audit', '--no-fund', ...prefixArgs, path.join(temp, archives[0])], temp);
|
|
59
|
+
const prefix = options.prefix || (() => {
|
|
60
|
+
const result = spawnSync(process.execPath, [npmCLI, 'prefix', '--global'], { encoding: 'utf8', shell: false });
|
|
61
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
62
|
+
})();
|
|
63
|
+
console.log('Installed s3get and s3push. Run: s3get --help / s3push --help');
|
|
64
|
+
if (prefix) console.log(`Command directory (must be on PATH): ${process.platform === 'win32' ? prefix : path.join(prefix, 'bin')}`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`s3flow install: ${error.message}`);
|
|
67
|
+
console.error('If the global directory is not writable, retry with install --prefix <writable-directory>.');
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
} finally {
|
|
70
|
+
if (temp) fs.rmSync(temp, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { parseArgs, install };
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/npm/run.cjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
const { constants } = require('node:os');
|
|
6
|
+
|
|
7
|
+
function binaryPath(command, platform = process.platform, arch = process.arch) {
|
|
8
|
+
if (!['s3get', 's3push'].includes(command)) {
|
|
9
|
+
throw new Error(`Unknown command: ${command}. Use s3get or s3push.`);
|
|
10
|
+
}
|
|
11
|
+
if (!['win32', 'darwin', 'linux'].includes(platform) || !['x64', 'arm64'].includes(arch)) {
|
|
12
|
+
throw new Error(`Unsupported platform: ${platform}/${arch}. Supported: Windows, macOS, Linux on x64 or arm64.`);
|
|
13
|
+
}
|
|
14
|
+
return path.join(__dirname, 'native', `${platform}-${arch}`, command + (platform === 'win32' ? '.exe' : ''));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function run(command, args) {
|
|
18
|
+
let executable;
|
|
19
|
+
try {
|
|
20
|
+
executable = binaryPath(command);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(`s3flow: ${error.message}`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Inherit the terminal for Bubble Tea and forward signals sent to the wrapper.
|
|
28
|
+
const child = spawn(executable, args, { stdio: 'inherit', shell: false });
|
|
29
|
+
const handlers = new Map();
|
|
30
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
31
|
+
const handler = () => child.kill(signal);
|
|
32
|
+
handlers.set(signal, handler);
|
|
33
|
+
process.on(signal, handler);
|
|
34
|
+
}
|
|
35
|
+
child.on('error', (error) => {
|
|
36
|
+
console.error(`s3flow: Cannot start ${command}: ${error.message}. Reinstall the release npm package.`);
|
|
37
|
+
});
|
|
38
|
+
child.on('close', (code, signal) => {
|
|
39
|
+
for (const [name, handler] of handlers) process.removeListener(name, handler);
|
|
40
|
+
process.exitCode = code ?? (signal ? 128 + (constants.signals[signal] || 1) : 1);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { binaryPath, run };
|
package/npm/s3get.cjs
ADDED
package/npm/s3push.cjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eyotang/s3flow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Cross-platform S3 download and upload tools: s3get and s3push",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" },
|
|
7
|
+
"repository": { "type": "git", "url": "git+https://github.com/eyotang/s3flow.git" },
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
9
|
+
"bin": {
|
|
10
|
+
"s3flow": "npm/cli.cjs",
|
|
11
|
+
"s3get": "npm/s3get.cjs",
|
|
12
|
+
"s3push": "npm/s3push.cjs"
|
|
13
|
+
},
|
|
14
|
+
"files": ["npm/*.cjs", "npm/native/", "README.md", "LICENSE"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test npm/test/*.test.cjs",
|
|
17
|
+
"test:package": "node scripts/test-npm-package.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|