@dd-code/oss-uploader 0.1.0
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/.env +19 -0
- package/README.md +285 -0
- package/dist/cli.cjs +2 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/config/EnvConfigResolverImpl.d.ts +18 -0
- package/dist/config/EnvConfigResolverImpl.d.ts.map +1 -0
- package/dist/config/ProviderConfigResolverImpl.d.ts +15 -0
- package/dist/config/ProviderConfigResolverImpl.d.ts.map +1 -0
- package/dist/config/loadObsCredentialsFromUrl.d.ts +6 -0
- package/dist/config/loadObsCredentialsFromUrl.d.ts.map +1 -0
- package/dist/config/types.d.ts +41 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/core/DirectoryUploadFlow.d.ts +17 -0
- package/dist/core/DirectoryUploadFlow.d.ts.map +1 -0
- package/dist/core/StorageClient.d.ts +22 -0
- package/dist/core/StorageClient.d.ts.map +1 -0
- package/dist/core/StorageFactory.d.ts +8 -0
- package/dist/core/StorageFactory.d.ts.map +1 -0
- package/dist/core/UploadFlowTemplate.d.ts +49 -0
- package/dist/core/UploadFlowTemplate.d.ts.map +1 -0
- package/dist/core/fileProcessor.d.ts +35 -0
- package/dist/core/fileProcessor.d.ts.map +1 -0
- package/dist/core/filters.d.ts +10 -0
- package/dist/core/filters.d.ts.map +1 -0
- package/dist/core/reporter.d.ts +50 -0
- package/dist/core/reporter.d.ts.map +1 -0
- package/dist/core/urlHelper.d.ts +8 -0
- package/dist/core/urlHelper.d.ts.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/middleware/OssService.d.ts +27 -0
- package/dist/middleware/OssService.d.ts.map +1 -0
- package/dist/providers/huawei/HuaweiObsClient.d.ts +18 -0
- package/dist/providers/huawei/HuaweiObsClient.d.ts.map +1 -0
- package/package.json +43 -0
- package/rollup.config.mjs +102 -0
- package/src/cli.ts +55 -0
- package/src/config/EnvConfigResolverImpl.ts +44 -0
- package/src/config/ProviderConfigResolverImpl.ts +34 -0
- package/src/config/loadObsCredentialsFromUrl.ts +46 -0
- package/src/config/types.ts +47 -0
- package/src/core/DirectoryUploadFlow.ts +96 -0
- package/src/core/StorageClient.ts +25 -0
- package/src/core/StorageFactory.ts +17 -0
- package/src/core/UploadFlowTemplate.ts +119 -0
- package/src/core/fileProcessor.ts +40 -0
- package/src/core/filters.ts +31 -0
- package/src/core/reporter.ts +88 -0
- package/src/core/urlHelper.ts +11 -0
- package/src/index.ts +63 -0
- package/src/middleware/OssService.ts +54 -0
- package/src/providers/huawei/HuaweiObsClient.ts +72 -0
- package/tsconfig.json +16 -0
package/.env
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 华为 OBS 访问配置(示例值,请按实际环境填写)
|
|
2
|
+
|
|
3
|
+
# OBS 服务访问地址(必填),只填区域地址,不要带桶名。cn-east-3 示例:
|
|
4
|
+
OBS_ENDPOINT=https://obs.cn-east-3.myhuaweicloud.com
|
|
5
|
+
|
|
6
|
+
# 访问密钥对:不在此填写,通过下面地址拉取 YAML 获取(YAML 内需包含 OBS_ACCESS_KEY、OBS_SECRET_KEY)
|
|
7
|
+
OBS_CREDENTIALS_URL=https://hr-uat.jtexpress.com.cn/hrpt/ast/static/obs-config.yaml
|
|
8
|
+
|
|
9
|
+
# 区域
|
|
10
|
+
OBS_REGION=cn-east-3
|
|
11
|
+
|
|
12
|
+
# 所有对象 key 的基础前缀,用于区分项目/业务
|
|
13
|
+
OBS_BASE_PREFIX=jhr-static
|
|
14
|
+
|
|
15
|
+
# CDN 加速根地址(可选),配置后会在控制台打印每个文件的访问 URL
|
|
16
|
+
OBS_CDN_BASE_URL=https://hruat-cos.jtexpress.com.cn
|
|
17
|
+
|
|
18
|
+
OSS_PROVIDER=huawei
|
|
19
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# OSS Uploader
|
|
2
|
+
|
|
3
|
+
将本地目录上传到对象存储(当前支持华为云 OBS)的 npm 包,提供 CLI 与库两种使用方式。采用中间件 + Provider 抽象,便于后续切换或接入其他云厂商(如阿里云 OSS、AWS S3)。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 功能特性
|
|
8
|
+
|
|
9
|
+
- **目录上传**:按本地目录结构上传到指定 Bucket,远端 key 规则为 `basePrefix / env / pathPrefix / 相对路径`。
|
|
10
|
+
- **同名跳过**:通过 OBS 元数据(getObjectMetadata)判断对象是否已存在,同路径已存在则不再上传。
|
|
11
|
+
- **过滤规则**:支持 `--include` / `--exclude` 通配符(如 `**/*.js`、`**/*.map`),使用 [micromatch](https://github.com/micromatch/micromatch)。
|
|
12
|
+
- **CDN 地址**:可选配置 `OBS_CDN_BASE_URL`,上传完成后控制台打印每个文件的访问 URL。
|
|
13
|
+
- **结果上报抽象**:`Reporter` 接口抽象上传进度与结果,默认控制台输出,可扩展为企业微信、钉钉等。
|
|
14
|
+
- **可切换 Provider**:通过环境变量 `OSS_PROVIDER` 与单一配置文件 `ProviderConfigResolverImpl` 即可切换云厂商,无需改业务代码。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 环境要求
|
|
19
|
+
|
|
20
|
+
- **Node.js**:>= 18
|
|
21
|
+
- **包管理**:npm / pnpm / yarn 均可
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 从本地或私有 registry 安装(请将 @your-scope/oss-uploader 替换为实际包名与版本)
|
|
29
|
+
npm install @your-scope/oss-uploader
|
|
30
|
+
|
|
31
|
+
# 或在本仓库根目录
|
|
32
|
+
pnpm install
|
|
33
|
+
pnpm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 配置
|
|
39
|
+
|
|
40
|
+
所有配置通过**环境变量**读取,不依赖 `.env` 文件(若使用 dotenv 可在进程内自行加载)。
|
|
41
|
+
|
|
42
|
+
### 华为 OBS(当前默认)
|
|
43
|
+
|
|
44
|
+
| 变量名 | 必填 | 说明 |
|
|
45
|
+
|--------|------|------|
|
|
46
|
+
| `OBS_ENDPOINT` | 是 | OBS 服务地址,如 `https://obs.cn-north-4.myhuaweicloud.com` |
|
|
47
|
+
| `OBS_ACCESS_KEY` | 是 | 访问密钥 AK |
|
|
48
|
+
| `OBS_SECRET_KEY` | 是 | 访问密钥 SK |
|
|
49
|
+
| `OBS_REGION` | 否 | 区域,如 `cn-north-4` |
|
|
50
|
+
| `OBS_BASE_PREFIX` | 否 | 所有对象 key 的基础前缀(仅在配置中,不通过 CLI 暴露),如 `company/project-a` |
|
|
51
|
+
| `OBS_CDN_BASE_URL` | 否 | CDN 加速根地址,用于上传完成后拼出访问 URL,如 `https://cdn.example.com` |
|
|
52
|
+
|
|
53
|
+
### 多云与切换
|
|
54
|
+
|
|
55
|
+
| 变量名 | 必填 | 说明 |
|
|
56
|
+
|--------|------|------|
|
|
57
|
+
| `OSS_PROVIDER` | 否 | 当前使用的存储,默认 `huawei`。后续可扩展 `aliyun`、`aws` 等。 |
|
|
58
|
+
|
|
59
|
+
示例 `.env.example`(可复制为 `.env` 并按实际填写):
|
|
60
|
+
|
|
61
|
+
```env
|
|
62
|
+
# 华为 OBS
|
|
63
|
+
OBS_ENDPOINT=https://obs.cn-north-4.myhuaweicloud.com
|
|
64
|
+
OBS_ACCESS_KEY=your-access-key-id
|
|
65
|
+
OBS_SECRET_KEY=your-secret-access-key
|
|
66
|
+
OBS_REGION=cn-north-4
|
|
67
|
+
OBS_BASE_PREFIX=company/project-a
|
|
68
|
+
OBS_CDN_BASE_URL=https://cdn.example.com
|
|
69
|
+
|
|
70
|
+
# 可选:切换存储厂商
|
|
71
|
+
# OSS_PROVIDER=huawei
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 使用方式
|
|
77
|
+
|
|
78
|
+
### 一、命令行(CLI)
|
|
79
|
+
|
|
80
|
+
安装后可通过 `oss-upload` 命令上传本地目录(若未全局安装,可使用 `npx oss-upload` 或 `pnpm exec oss-upload`)。
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 基本用法:上传 ./dist 到指定 bucket,前缀为 app/v1/
|
|
84
|
+
oss-upload ./dist -b my-bucket --path-prefix app/v1/
|
|
85
|
+
|
|
86
|
+
# 带 include/exclude
|
|
87
|
+
oss-upload ./dist -b my-bucket --path-prefix app/v1/ \
|
|
88
|
+
--include "**/*.js" --include "**/*.css" \
|
|
89
|
+
--exclude "**/*.map"
|
|
90
|
+
|
|
91
|
+
# 指定环境标识(会参与 key 拼接)
|
|
92
|
+
npx oss-upload ./dist/ddd -e dev --path-prefix web/app
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**CLI 参数说明**
|
|
96
|
+
|
|
97
|
+
| 参数 | 简写 | 必填 | 说明 |
|
|
98
|
+
|------|------|------|------|
|
|
99
|
+
| `localDir` | - | 是 | 本地目录路径 |
|
|
100
|
+
| `--bucket` | `-b` | 是 | Bucket 名称 |
|
|
101
|
+
| `--env` | `-e` | 否 | 环境标识,参与 key 拼接,如 dev/test/prod |
|
|
102
|
+
| `--path-prefix` | - | 否 | 业务路径前缀,拼在 base/env 之后 |
|
|
103
|
+
| `--include` | - | 否 | 包含的文件模式,可多次 |
|
|
104
|
+
| `--exclude` | - | 否 | 排除的文件模式,可多次 |
|
|
105
|
+
|
|
106
|
+
上传过程中会输出:开始信息、每个文件的成功/跳过/失败及(若配置了 CDN)访问地址、最终统计。
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 二、作为库使用
|
|
111
|
+
|
|
112
|
+
在代码中引入中间件 `OssService` 与配置解析器 `ProviderConfigResolverImpl`,调用 `uploadDirectory` 即可。
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import {
|
|
116
|
+
OssService,
|
|
117
|
+
ProviderConfigResolverImpl,
|
|
118
|
+
ConsoleReporter,
|
|
119
|
+
} from '@your-scope/oss-uploader';
|
|
120
|
+
|
|
121
|
+
const service = new OssService(new ProviderConfigResolverImpl());
|
|
122
|
+
const reporter = new ConsoleReporter();
|
|
123
|
+
|
|
124
|
+
const summary = await service.uploadDirectory({
|
|
125
|
+
bucket: 'my-bucket',
|
|
126
|
+
localDir: './dist',
|
|
127
|
+
pathPrefix: 'app/v1',
|
|
128
|
+
include: ['**/*.js', '**/*.css'],
|
|
129
|
+
exclude: ['**/*.map'],
|
|
130
|
+
reporter,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log(`成功: ${summary.success}, 失败: ${summary.failed}, 跳过: ${summary.skipped}`);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**自定义 Reporter**(如对接企业微信、钉钉):实现 `Reporter` 接口的 `onStart`、`onFileResult`、`onComplete`,传入 `uploadDirectory` 的 `reporter` 即可。
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 远端 Key 规则
|
|
141
|
+
|
|
142
|
+
单个文件的远端 key 由以下部分按顺序拼接(空段不参与):
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
basePrefix / env / pathPrefix / 相对路径
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **basePrefix**:来自配置 `OBS_BASE_PREFIX`,仅在配置中设置,不通过 CLI 暴露,默认都会带上前缀。
|
|
149
|
+
- **env**:CLI 的 `--env` 或库入参的 `env`。
|
|
150
|
+
- **pathPrefix**:CLI 的 `--path-prefix` 或库入参的 `pathPrefix`。
|
|
151
|
+
- **相对路径**:相对 `localDir` 的路径,保持目录结构。
|
|
152
|
+
|
|
153
|
+
示例:`basePrefix=company/proj`、`env=prod`、`pathPrefix=web`、本地文件 `dist/js/app.js` → 远端 key:`company/proj/prod/web/js/app.js`。
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 架构与设计
|
|
158
|
+
|
|
159
|
+
### 分层结构
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
┌─────────────────────────────────────────────────────────┐
|
|
163
|
+
│ CLI / 业务代码(主流程) │
|
|
164
|
+
│ 只依赖 OssService.uploadDirectory() │
|
|
165
|
+
└───────────────────────────┬─────────────────────────────┘
|
|
166
|
+
│
|
|
167
|
+
┌───────────────────────────▼─────────────────────────────┐
|
|
168
|
+
│ 中间件层:OssService(Facade) │
|
|
169
|
+
│ 统一 API,内部通过 ProviderConfigResolver 解析配置, │
|
|
170
|
+
│ 创建 StorageClient,组装 DirectoryUploadFlow 并执行 │
|
|
171
|
+
└───────────────────────────┬─────────────────────────────┘
|
|
172
|
+
│
|
|
173
|
+
┌───────────────────────────▼─────────────────────────────┐
|
|
174
|
+
│ 流程层:UploadFlowTemplate / DirectoryUploadFlow │
|
|
175
|
+
│ 模板方法:收集文件 → 过滤 → 上传(headObject 判断 → putObject)│
|
|
176
|
+
│ 调用 StorageClient + Reporter │
|
|
177
|
+
└───────────────────────────┬─────────────────────────────┘
|
|
178
|
+
│
|
|
179
|
+
┌───────────────────────────▼─────────────────────────────┐
|
|
180
|
+
│ Provider 层:StorageClient 实现(如 HuaweiObsClient) │
|
|
181
|
+
│ 将 putObject/headObject 翻译为具体云厂商 API │
|
|
182
|
+
└─────────────────────────────────────────────────────────┘
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 设计模式简述
|
|
186
|
+
|
|
187
|
+
- **Facade(外观)**:`OssService` 对主流程暴露单一 `uploadDirectory`,隐藏 Provider 选择、配置解析、流程细节。
|
|
188
|
+
- **Template Method(模板方法)**:`UploadFlowTemplate` 定义“收集 → 过滤 → 上传 → 上报”的骨架,`DirectoryUploadFlow` 实现具体收集与上传逻辑。
|
|
189
|
+
- **Strategy / Adapter**:各云厂商实现统一接口 `StorageClient`(如 `HuaweiObsClient`),由 `StorageFactory` 按配置创建。
|
|
190
|
+
- **简单工厂**:`StorageFactory.createStorageClient(config)` 根据 `config.type` 返回对应 Provider 实例。
|
|
191
|
+
|
|
192
|
+
### 目录结构(源码)
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
src/
|
|
196
|
+
├── index.ts # 包入口,显式导出 API 与类型
|
|
197
|
+
├── cli.ts # CLI 入口(动态 import 依赖)
|
|
198
|
+
├── config/
|
|
199
|
+
│ ├── types.ts # 配置与 Provider 类型定义
|
|
200
|
+
│ ├── EnvConfigResolverImpl.ts # 华为 OBS 环境变量解析
|
|
201
|
+
│ └── ProviderConfigResolverImpl.ts # 统一 Provider 解析(切换云厂商只改此文件)
|
|
202
|
+
├── core/
|
|
203
|
+
│ ├── StorageClient.ts # 存储客户端统一接口
|
|
204
|
+
│ ├── StorageFactory.ts # 创建 StorageClient 的工厂
|
|
205
|
+
│ ├── UploadFlowTemplate.ts # 上传流程模板
|
|
206
|
+
│ ├── DirectoryUploadFlow.ts # 目录上传具体实现
|
|
207
|
+
│ ├── reporter.ts # Reporter 抽象与 ConsoleReporter
|
|
208
|
+
│ ├── filters.ts # include/exclude 过滤
|
|
209
|
+
│ └── urlHelper.ts # buildCdnAccessUrl 等
|
|
210
|
+
├── middleware/
|
|
211
|
+
│ └── OssService.ts # 中间件统一 API
|
|
212
|
+
└── providers/
|
|
213
|
+
└── huawei/
|
|
214
|
+
└── HuaweiObsClient.ts # 华为 OBS 的 StorageClient 实现
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 扩展与二次开发
|
|
220
|
+
|
|
221
|
+
### 切换云厂商(如阿里云 OSS)
|
|
222
|
+
|
|
223
|
+
1. 在 `src/config/types.ts` 中扩展 `ProviderType` 与对应配置类型(如 `AliyunOssConfig`)、在 `ProviderConfig` 中增加分支。
|
|
224
|
+
2. 在 `src/config/ProviderConfigResolverImpl.ts` 的 `resolve()` 中增加 `case 'aliyun'`,从环境变量读取阿里云配置,返回 `{ providerConfig, basePrefix, cdnBaseUrl }`。
|
|
225
|
+
3. 在 `src/core/StorageFactory.ts` 中增加 `case 'aliyun'`,创建阿里云 Provider 实例。
|
|
226
|
+
4. 在 `src/providers/` 下新增 `aliyun/AliyunOssClient.ts`,实现 `StorageClient` 的 `headObject`、`putObject`。
|
|
227
|
+
|
|
228
|
+
业务侧只需设置 `OSS_PROVIDER=aliyun` 及对应环境变量,无需改 OssService / CLI。
|
|
229
|
+
|
|
230
|
+
### 自定义 Reporter
|
|
231
|
+
|
|
232
|
+
实现接口:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
interface Reporter {
|
|
236
|
+
onStart?(context: { env?, bucket, basePrefix, pathPrefix, localDir }): void | Promise<void>;
|
|
237
|
+
onFileResult?(result: UploadFileResult): void | Promise<void>;
|
|
238
|
+
onComplete?(summary: UploadSummary): void | Promise<void>;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
将实例传入 `uploadDirectory({ ..., reporter: new MyReporter() })` 或 CLI 侧通过封装调用传入即可。
|
|
243
|
+
|
|
244
|
+
### 工具方法
|
|
245
|
+
|
|
246
|
+
- **buildCdnAccessUrl(cdnBaseUrl, key)**:根据 CDN 根地址与对象 key 拼出完整访问 URL,已从包入口导出。
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 构建与发布
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# 安装依赖
|
|
254
|
+
pnpm install
|
|
255
|
+
|
|
256
|
+
# 构建(Rollup 输出 dist/index.cjs、dist/cli.cjs 等)
|
|
257
|
+
pnpm run build
|
|
258
|
+
|
|
259
|
+
# 监听模式
|
|
260
|
+
pnpm run build:watch
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
发布前请将 `package.json` 中的 `name` 改为实际 scope 与包名,按需配置 `publishConfig` 与 registry。
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 常见问题
|
|
268
|
+
|
|
269
|
+
**Q:必须配置 OBS_CDN_BASE_URL 吗?**
|
|
270
|
+
A:不必。不配置则不会打印访问地址,上传与跳过逻辑不受影响。
|
|
271
|
+
|
|
272
|
+
**Q:判断“是否存在”是查 OBS 还是 CDN?**
|
|
273
|
+
A:查 **OBS 元数据**(getObjectMetadata),以存储源为准,不依赖 CDN 是否已缓存。
|
|
274
|
+
|
|
275
|
+
**Q:如何只上传某几类文件?**
|
|
276
|
+
A:使用 `--include`,例如 `--include "**/*.js" --include "**/*.css"`;用 `--exclude "**/*.map"` 排除不需要的。
|
|
277
|
+
|
|
278
|
+
**Q:basePrefix 可以不在 .env 里配吗?**
|
|
279
|
+
A:可以。若不需要统一前缀,将 `OBS_BASE_PREFIX` 留空即可;若通过代码传入配置,可在自定义 `ProviderConfigResolver` 或 `EnvConfigResolver` 中设置。
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 许可证
|
|
284
|
+
|
|
285
|
+
MIT
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var e=require("fs/promises"),t=require("path"),o=require("micromatch"),s=require("js-yaml");(async()=>{const[{Command:e},{OssService:t},{ProviderConfigResolverImpl:o},{ConsoleReporter:s}]=await Promise.all([import("commander"),Promise.resolve().then(function(){return l}),Promise.resolve().then(function(){return h}),Promise.resolve().then(function(){return p})]),r=new e;r.name("oss-upload").description("上传本地目录到 OSS(当前为华为 OBS)").argument("<localDir>","本地目录").requiredOption("-b, --bucket <bucket>","Bucket 名称","hr-uat").option("-e, --env <env>","环境标识,如 dev/test/prod").option("--path-prefix <pathPrefix>","业务路径前缀,将拼在 base/env 后面").option("--include <pattern...>","包含的文件模式(可多次)").option("--exclude <pattern...>","排除的文件模式(可多次)").action(async(e,r)=>{const{loadObsCredentialsFromUrlIfNeeded:n}=await Promise.resolve().then(function(){return f});await n();const c=new t(new o),i=new s;try{(await c.uploadDirectory({env:r.env,bucket:r.bucket,localDir:e,pathPrefix:r.pathPrefix,include:r.include,exclude:r.exclude,reporter:i})).failed>0&&(process.exitCode=1)}catch(e){console.error("上传过程中发生错误:",e?.message||e),process.exitCode=1}}),r.parse(process.argv)})();const r=require("esdk-obs-nodejs");class n{constructor(e){this.config=e,this.client=new r({access_key_id:e.accessKey,secret_access_key:e.secretKey,server:e.endpoint})}async headObject(e,t){try{const o=await this.client.getObjectMetadata({Bucket:e,Key:t});if(o.CommonMsg.Status<=300)return{exists:!0};if(404===o.CommonMsg.Status||"NoSuchKey"===o.CommonMsg.Code||"NotFound"===o.CommonMsg.Code)return{exists:!1};throw new Error(`OBS getObjectMetadata 失败: ${o.CommonMsg.Status} ${o.CommonMsg.Code} ${o.CommonMsg.Message}`)}catch(e){const t=e&&"string"==typeof e.message?e.message:String(e),o=e?.Code??e?.code;if("NoSuchKey"===o||"NotFound"===o||t.includes("404")||t.includes("NoSuchKey"))return{exists:!1};throw e}}async putObject(e){const{bucket:t,key:o,body:s,contentType:r}=e,n=await this.client.putObject({Bucket:t,Key:o,Body:s});if(n.CommonMsg.Status>300){const{Status:e,Code:t,Message:o,RequestId:s}=n.CommonMsg,r=[e,t,o,s].filter(Boolean).join(" ");throw new Error(`OBS putObject 失败 (${r||"无详情"}). 请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com`)}}}class c{constructor(e){this.context=e}async execute(){const{reporter:e}=this.context;e?.onStart&&await e.onStart({env:this.context.env,bucket:this.context.bucket,basePrefix:this.context.basePrefix,pathPrefix:this.context.pathPrefix,localDir:this.context.localDir,cdnBaseUrl:this.context.cdnBaseUrl});const t=await this.collectFiles(),o=this.filterFiles(t),s=await this.uploadFiles(o);return e?.onComplete&&await e.onComplete(s),s}filterFiles(e){const{include:t,exclude:s}=this.context;return e.filter(e=>function(e,t,s){let r=!0;return t&&t.length>0&&(r=o.isMatch(e,t)),!(!r||s&&s.length>0&&o.isMatch(e,s))}(e.relativePath,t,s))}buildKey(e){const t=[];return this.context.basePrefix&&t.push(this.context.basePrefix),this.context.env&&t.push(this.context.env),this.context.pathPrefix&&t.push(this.context.pathPrefix),t.push(e.replace(/\\/g,"/")),t.join("/")}async walkDir(o,s=""){const r=t.join(o,s),n=await e.readdir(r,{withFileTypes:!0}),c=[];for(const e of n){const r=t.join(s,e.name),n=t.join(o,r);e.isDirectory()?c.push(...await this.walkDir(o,r)):e.isFile()&&c.push({absolutePath:n,relativePath:r.replace(/\\/g,"/")})}return c}async reportFileResult(e){const{reporter:t}=this.context;t?.onFileResult&&await t.onFileResult(e)}}function i(e,t){const o=e.replace(/\/+$/,""),s=t.replace(/^\/+/,"");return s?`${o}/${s}`:o}class a extends c{constructor(e){super(e)}async collectFiles(){return this.walkDir(this.context.localDir)}async uploadFiles(t){const{bucket:o,storageClient:s,cdnBaseUrl:r}=this.context,n={total:t.length,success:0,failed:0,skipped:0};for(const c of t){const t=this.buildKey(c.relativePath);try{if((await s.headObject(o,t)).exists){n.skipped+=1,await this.reportFileResult({localPath:c.absolutePath,relativePath:c.relativePath,bucket:o,key:t,status:"skipped",...r&&{accessUrl:i(r,t)}});continue}const a=await e.readFile(c.absolutePath),{buffer:l,contentType:u}=this.context.fileProcessor?await this.context.fileProcessor.process({localPath:c.absolutePath,relativePath:c.relativePath,buffer:a}):{buffer:a,contentType:void 0};await s.putObject({bucket:o,key:t,body:l,contentType:u}),n.success+=1,await this.reportFileResult({localPath:c.absolutePath,relativePath:c.relativePath,bucket:o,key:t,status:"success",...r&&{accessUrl:i(r,t)}})}catch(e){n.failed+=1,await this.reportFileResult({localPath:c.absolutePath,relativePath:c.relativePath,bucket:o,key:t,status:"failed",error:e instanceof Error?e:new Error(String(e))})}}return n}}var l=Object.freeze({__proto__:null,OssService:class{constructor(e){this.providerConfigResolver=e}async uploadDirectory(e){const{env:t,bucket:o="hr-uat",localDir:s,pathPrefix:r,include:c,exclude:i,reporter:l,fileProcessor:u}=e,h=this.providerConfigResolver.resolve(),{providerConfig:p,basePrefix:d,cdnBaseUrl:f}=h,S=function(e){if("huawei"===e.type)return new n(e.options);throw new Error(`Unsupported provider type: ${e.type}`)}(p);return new a({env:t,bucket:o,localDir:s,pathPrefix:r,include:c,exclude:i,basePrefix:d,storageClient:S,reporter:l,cdnBaseUrl:f,fileProcessor:u}).execute()}}});class u{resolveHuaweiConfig(e){const t=process.env.OBS_ACCESS_KEY,o=process.env.OBS_SECRET_KEY;if(!t||!o)throw new Error("缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY");return{endpoint:"https://obs.cn-east-3.myhuaweicloud.com",accessKey:t,secretKey:o,region:"cn-east-3",basePrefix:"jhr-static",cdnBaseUrl:"https://hruat-cos.jtexpress.com.cn"}}}var h=Object.freeze({__proto__:null,ProviderConfigResolverImpl:class{constructor(){this.huaweiResolver=new u}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}}});var p=Object.freeze({__proto__:null,ConsoleReporter:class{onStart(e){const{env:t,bucket:o,basePrefix:s,pathPrefix:r,localDir:n,cdnBaseUrl:c}=e;c&&console.log(` CDN 根地址: ${c}`)}onFileResult(e){const{status:t,localPath:o,key:s,error:r,accessUrl:n}=e;"success"===t?(console.log(`[SUCCESS] ${o} -> ${s}`),n&&console.log(` 访问地址: ${n}`)):"skipped"===t?(console.log(`[SKIPPED] ${o} -> ${s} (云端同名已存在)`),n&&console.log(` 访问地址: ${n}`)):(console.error(`[FAILED] ${o} -> ${s}`),r&&console.error(` Error: ${r.message}`))}onComplete(e){console.log("上传完成:"),console.log(` 总数: ${e.total}`),console.log(` 成功: ${e.success}`),console.log(` 失败: ${e.failed}`),console.log(` 跳过: ${e.skipped}`)}}});const d="OBS_CREDENTIALS_URL";var f=Object.freeze({__proto__:null,loadObsCredentialsFromUrlIfNeeded:async function(){const e="https://hr-uat.jtexpress.com.cn/hrpt/ast/static/obs-config.yaml",t=process.env.OBS_ACCESS_KEY,o=process.env.OBS_SECRET_KEY;if(t&&o)return;const r=await fetch(e);if(!r.ok)throw new Error(`拉取 OBS 凭证失败 (${r.status}): ${e}`);const n=await r.text(),c=s.load(n);if(!c||"object"!=typeof c)throw new Error(`OBS 凭证 YAML 解析结果无效,请检查 ${d} 返回内容`);const i=c.OBS_ACCESS_KEY,a=c.OBS_SECRET_KEY;if("string"==typeof i&&(process.env.OBS_ACCESS_KEY=i),"string"==typeof a&&(process.env.OBS_SECRET_KEY=a),!process.env.OBS_ACCESS_KEY||!process.env.OBS_SECRET_KEY)throw new Error(`OBS 凭证 YAML 中未找到 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,请检查 ${d} 返回内容`)}});
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.cjs","sources":["../src/cli.ts","../src/providers/huawei/HuaweiObsClient.ts","../src/core/StorageFactory.ts","../src/core/filters.ts","../src/core/UploadFlowTemplate.ts","../src/core/urlHelper.ts","../src/core/DirectoryUploadFlow.ts","../src/middleware/OssService.ts","../src/config/EnvConfigResolverImpl.ts","../src/config/ProviderConfigResolverImpl.ts","../src/core/reporter.ts","../src/config/loadObsCredentialsFromUrl.ts"],"sourcesContent":["/**\r\n * CLI 入口:oss-upload 命令。\r\n * 使用动态 import 加载依赖,避免启动时加载全部模块。\r\n * .env 在打包时已注入 dist/cli.cjs,运行时不再读取。\r\n */\r\n(async () => {\r\n const [{ Command }, { OssService }, { ProviderConfigResolverImpl }, { ConsoleReporter }] = await Promise.all([\r\n import('commander'),\r\n import('./middleware/OssService'),\r\n import('./config/ProviderConfigResolverImpl'),\r\n import('./core/reporter'),\r\n ]);\r\n\r\n const program = new Command();\r\n\r\n program\r\n .name('oss-upload')\r\n .description('上传本地目录到 OSS(当前为华为 OBS)')\r\n .argument('<localDir>', '本地目录')\r\n .requiredOption('-b, --bucket <bucket>', 'Bucket 名称','hr-uat')\r\n .option('-e, --env <env>', '环境标识,如 dev/test/prod')\r\n .option('--path-prefix <pathPrefix>', '业务路径前缀,将拼在 base/env 后面')\r\n .option('--include <pattern...>', '包含的文件模式(可多次)')\r\n .option('--exclude <pattern...>', '排除的文件模式(可多次)')\r\n .action(async (localDir: string, options: any) => {\r\n const { loadObsCredentialsFromUrlIfNeeded } = await import('./config/loadObsCredentialsFromUrl');\r\n await loadObsCredentialsFromUrlIfNeeded();\r\n\r\n const service = new OssService(new ProviderConfigResolverImpl());\r\n const reporter = new ConsoleReporter();\r\n\r\n try {\r\n const summary = await service.uploadDirectory({\r\n env: options.env,\r\n bucket: options.bucket,\r\n localDir,\r\n pathPrefix: options.pathPrefix,\r\n include: options.include,\r\n exclude: options.exclude,\r\n reporter,\r\n });\r\n\r\n if (summary.failed > 0) {\r\n process.exitCode = 1;\r\n }\r\n } catch (err: any) {\r\n // eslint-disable-next-line no-console\r\n console.error('上传过程中发生错误:', err?.message || err);\r\n process.exitCode = 1;\r\n }\r\n });\r\n\r\n program.parse(process.argv);\r\n})();\r\n\r\n","import { StorageClient, PutObjectOptions, HeadObjectResult } from '../../core/StorageClient';\r\nimport { HuaweiObsConfig } from '../../config/types';\r\n\r\n// 华为 OBS Node.js SDK(CommonJS)\r\nconst ObsClient = require('esdk-obs-nodejs');\r\n\r\n/**\r\n * 华为 OBS 的 StorageClient 实现(Adapter)。\r\n * 通过 SDK 的 getObjectMetadata 查元数据判断是否存在,putObject 上传对象。\r\n */\r\nexport class HuaweiObsClient implements StorageClient {\r\n private readonly client: InstanceType<typeof ObsClient>;\r\n\r\n constructor(private readonly config: HuaweiObsConfig) {\r\n this.client = new ObsClient({\r\n access_key_id: config.accessKey,\r\n secret_access_key: config.secretKey,\r\n server: config.endpoint,\r\n });\r\n }\r\n\r\n /**\r\n * 调用 OBS getObjectMetadata 查元数据,判断对象是否存在(同路径即跳过上传)。\r\n */\r\n async headObject(bucket: string, key: string): Promise<HeadObjectResult> {\r\n try {\r\n const result = await this.client.getObjectMetadata({\r\n Bucket: bucket,\r\n Key: key,\r\n });\r\n\r\n if (result.CommonMsg.Status <= 300) {\r\n return { exists: true };\r\n }\r\n\r\n // 对象不存在\r\n if (result.CommonMsg.Status === 404 || result.CommonMsg.Code === 'NoSuchKey' || result.CommonMsg.Code === 'NotFound') {\r\n return { exists: false };\r\n }\r\n\r\n throw new Error(`OBS getObjectMetadata 失败: ${result.CommonMsg.Status} ${result.CommonMsg.Code} ${result.CommonMsg.Message}`);\r\n } catch (err: unknown) {\r\n const msg = err && typeof (err as any).message === 'string' ? (err as Error).message : String(err);\r\n const code = (err as any)?.Code ?? (err as any)?.code;\r\n if (code === 'NoSuchKey' || code === 'NotFound' || msg.includes('404') || msg.includes('NoSuchKey')) {\r\n return { exists: false };\r\n }\r\n throw err;\r\n }\r\n }\r\n\r\n /** 调用 OBS putObject 上传对象 */\r\n async putObject(options: PutObjectOptions): Promise<void> {\r\n const { bucket, key, body, contentType } = options;\r\n // console.log('putObject', { bucket, key, endpoint: this.config.endpoint, accessKey: this.config.accessKey, secretKey: this.config.secretKey })\r\n const result = await this.client.putObject({\r\n Bucket: bucket,\r\n Key: key,\r\n Body: body,\r\n // ContentType: contentType,\r\n });\r\n\r\n if (result.CommonMsg.Status > 300) {\r\n const { Status, Code, Message, RequestId } = result.CommonMsg;\r\n const detail = [Status, Code, Message, RequestId].filter(Boolean).join(' ');\r\n throw new Error(\r\n `OBS putObject 失败 (${detail || '无详情'}). ` +\r\n '请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com'\r\n );\r\n }\r\n }\r\n}\r\n","import { StorageClient } from './StorageClient';\r\nimport { ProviderConfig } from '../config/types';\r\nimport { HuaweiObsClient } from '../providers/huawei/HuaweiObsClient';\r\n\r\n/**\r\n * 根据 Provider 类型创建对应的 StorageClient(简单工厂)。\r\n * 后续扩展阿里 OSS、AWS S3 时在此增加 case 即可。\r\n */\r\nexport function createStorageClient(config: ProviderConfig): StorageClient {\r\n switch (config.type) {\r\n case 'huawei':\r\n return new HuaweiObsClient(config.options);\r\n default:\r\n throw new Error(`Unsupported provider type: ${config.type}`);\r\n }\r\n}\r\n\r\n","import micromatch from 'micromatch';\r\n\r\n/**\r\n * 根据 include/exclude 规则判断相对路径是否应参与上传。\r\n * 使用 micromatch 支持 glob(如 **\\/*.js)。\r\n * @param relativePath 相对路径\r\n * @param include 包含模式,不传则默认全部包含\r\n * @param exclude 排除模式\r\n * @returns 是否应包含该文件\r\n */\r\nexport function shouldInclude(\r\n relativePath: string,\r\n include?: string[],\r\n exclude?: string[],\r\n): boolean {\r\n let included = true;\r\n\r\n if (include && include.length > 0) {\r\n included = micromatch.isMatch(relativePath, include);\r\n }\r\n if (!included) return false;\r\n\r\n if (exclude && exclude.length > 0) {\r\n if (micromatch.isMatch(relativePath, exclude)) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n}\r\n\r\n","import fs from 'fs/promises';\r\nimport path from 'path';\r\nimport { Reporter, UploadSummary, UploadFileResult } from './reporter';\r\nimport { StorageClient } from './StorageClient';\r\nimport { shouldInclude } from './filters';\r\nimport type { FileProcessor } from './fileProcessor';\r\n\r\n/** 上传流程的上下文:环境、bucket、前缀、本地目录、存储客户端、Reporter、CDN 根地址、上传前处理器等 */\r\nexport interface UploadContext {\r\n env?: string;\r\n bucket: string;\r\n basePrefix: string;\r\n pathPrefix?: string;\r\n localDir: string;\r\n include?: string[];\r\n exclude?: string[];\r\n storageClient: StorageClient;\r\n reporter?: Reporter;\r\n /** CDN 加速根地址,配置后可为每个文件生成 accessUrl */\r\n cdnBaseUrl?: string;\r\n /** 上传前对文件内容的处理器(如图片压缩),未配置则直接上传原内容 */\r\n fileProcessor?: FileProcessor;\r\n}\r\n\r\n/** 本地文件信息:绝对路径与相对路径 */\r\nexport interface LocalFile {\r\n absolutePath: string;\r\n relativePath: string;\r\n}\r\n\r\n/**\r\n * 上传流程模板(Template Method)。\r\n * 定义固定步骤:onStart -> collectFiles -> filterFiles -> uploadFiles -> onComplete,\r\n * 子类实现 collectFiles 与 uploadFiles,其余步骤可复用或重写。\r\n */\r\nexport abstract class UploadFlowTemplate {\r\n protected constructor(protected readonly context: UploadContext) {}\r\n\r\n /** 执行完整上传流程 */\r\n async execute(): Promise<UploadSummary> {\r\n const { reporter } = this.context;\r\n\r\n if (reporter?.onStart) {\r\n await reporter.onStart({\r\n env: this.context.env,\r\n bucket: this.context.bucket,\r\n basePrefix: this.context.basePrefix,\r\n pathPrefix: this.context.pathPrefix,\r\n localDir: this.context.localDir,\r\n cdnBaseUrl: this.context.cdnBaseUrl,\r\n });\r\n }\r\n\r\n const files = await this.collectFiles();\r\n const filtered = this.filterFiles(files);\r\n const summary = await this.uploadFiles(filtered);\r\n\r\n if (reporter?.onComplete) {\r\n await reporter.onComplete(summary);\r\n }\r\n\r\n return summary;\r\n }\r\n\r\n protected abstract collectFiles(): Promise<LocalFile[]>;\r\n protected abstract uploadFiles(files: LocalFile[]): Promise<UploadSummary>;\r\n\r\n /** 使用 include/exclude 过滤文件列表 */\r\n protected filterFiles(files: LocalFile[]): LocalFile[] {\r\n const { include, exclude } = this.context;\r\n return files.filter((f) => shouldInclude(f.relativePath, include, exclude));\r\n }\r\n\r\n /**\r\n * 构造远端 key:basePrefix / env / pathPrefix / relativePath。\r\n * 保持目录结构,且 base 前缀来自 config,不对外暴露。\r\n */\r\n protected buildKey(relativePath: string): string {\r\n const parts: string[] = [];\r\n if (this.context.basePrefix) parts.push(this.context.basePrefix);\r\n if (this.context.env) parts.push(this.context.env);\r\n if (this.context.pathPrefix) parts.push(this.context.pathPrefix);\r\n\r\n parts.push(relativePath.replace(/\\\\/g, '/'));\r\n return parts.join('/');\r\n }\r\n\r\n /** 递归遍历目录,收集所有文件的绝对路径与相对路径 */\r\n protected async walkDir(rootDir: string, currentDir = ''): Promise<LocalFile[]> {\r\n const dirPath = path.join(rootDir, currentDir);\r\n const entries = await fs.readdir(dirPath, { withFileTypes: true });\r\n const files: LocalFile[] = [];\r\n\r\n for (const entry of entries) {\r\n const rel = path.join(currentDir, entry.name);\r\n const abs = path.join(rootDir, rel);\r\n\r\n if (entry.isDirectory()) {\r\n files.push(...(await this.walkDir(rootDir, rel)));\r\n } else if (entry.isFile()) {\r\n files.push({\r\n absolutePath: abs,\r\n relativePath: rel.replace(/\\\\/g, '/'),\r\n });\r\n }\r\n }\r\n\r\n return files;\r\n }\r\n\r\n /** 将单个文件的上传结果交给 Reporter */\r\n protected async reportFileResult(result: UploadFileResult): Promise<void> {\r\n const { reporter } = this.context;\r\n if (reporter?.onFileResult) {\r\n await reporter.onFileResult(result);\r\n }\r\n }\r\n}\r\n\r\n","/**\r\n * 根据 CDN 加速域名与对象 key 拼接完整的访问地址。\r\n * @param cdnBaseUrl CDN 根地址(如 https://cdn.example.com),可带或不带末尾斜杠\r\n * @param key 对象在桶内的 key(如 basePrefix/path/file.js)\r\n * @returns 完整访问 URL,如 https://cdn.example.com/basePrefix/path/file.js\r\n */\r\nexport function buildCdnAccessUrl(cdnBaseUrl: string, key: string): string {\r\n const base = cdnBaseUrl.replace(/\\/+$/, '');\r\n const pathPart = key.replace(/^\\/+/, '');\r\n return pathPart ? `${base}/${pathPart}` : base;\r\n}\r\n","import fs from 'fs/promises';\r\nimport { UploadFlowTemplate, UploadContext, LocalFile } from './UploadFlowTemplate';\r\nimport { UploadSummary } from './reporter';\r\nimport { buildCdnAccessUrl } from './urlHelper';\r\n\r\n/**\r\n * 目录上传流程的具体实现:遍历本地目录,按 key 规则上传,云端同名则跳过。\r\n * 规则:先 headObject,exists 则 skipped,否则 putObject。\r\n */\r\nexport class DirectoryUploadFlow extends UploadFlowTemplate {\r\n constructor(context: UploadContext) {\r\n super(context);\r\n }\r\n\r\n /** 递归收集 localDir 下所有文件 */\r\n protected async collectFiles(): Promise<LocalFile[]> {\r\n return this.walkDir(this.context.localDir);\r\n }\r\n\r\n /**\r\n * 逐个文件上传:先 head 判断是否存在,存在则跳过,否则读取本地文件并 putObject。\r\n * 每个文件的结果通过 reportFileResult 交给 Reporter。\r\n */\r\n protected async uploadFiles(files: LocalFile[]): Promise<UploadSummary> {\r\n const { bucket, storageClient, cdnBaseUrl } = this.context;\r\n\r\n const summary: UploadSummary = {\r\n total: files.length,\r\n success: 0,\r\n failed: 0,\r\n skipped: 0,\r\n };\r\n\r\n for (const file of files) {\r\n const key = this.buildKey(file.relativePath);\r\n\r\n try {\r\n const head = await storageClient.headObject(bucket, key);\r\n\r\n // 云端已有同名对象则跳过,不比较内容\r\n if (head.exists) {\r\n summary.skipped += 1;\r\n await this.reportFileResult({\r\n localPath: file.absolutePath,\r\n relativePath: file.relativePath,\r\n bucket,\r\n key,\r\n status: 'skipped',\r\n ...(cdnBaseUrl && { accessUrl: buildCdnAccessUrl(cdnBaseUrl, key) }),\r\n });\r\n continue;\r\n }\r\n\r\n const data = await fs.readFile(file.absolutePath);\r\n\r\n const { buffer, contentType } = this.context.fileProcessor\r\n ? await this.context.fileProcessor.process({\r\n localPath: file.absolutePath,\r\n relativePath: file.relativePath,\r\n buffer: data,\r\n })\r\n : { buffer: data, contentType: undefined as string | undefined };\r\n\r\n await storageClient.putObject({\r\n bucket,\r\n key,\r\n body: buffer,\r\n contentType,\r\n });\r\n\r\n summary.success += 1;\r\n await this.reportFileResult({\r\n localPath: file.absolutePath,\r\n relativePath: file.relativePath,\r\n bucket,\r\n key,\r\n status: 'success',\r\n ...(cdnBaseUrl && { accessUrl: buildCdnAccessUrl(cdnBaseUrl, key) }),\r\n });\r\n } catch (err: any) {\r\n summary.failed += 1;\r\n await this.reportFileResult({\r\n localPath: file.absolutePath,\r\n relativePath: file.relativePath,\r\n bucket,\r\n key,\r\n status: 'failed',\r\n error: err instanceof Error ? err : new Error(String(err)),\r\n });\r\n }\r\n }\r\n\r\n return summary;\r\n }\r\n}\r\n\r\n","import { Reporter, UploadSummary } from '../core/reporter';\r\nimport { ProviderConfigResolver } from '../config/types';\r\nimport { createStorageClient } from '../core/StorageFactory';\r\nimport { DirectoryUploadFlow } from '../core/DirectoryUploadFlow';\r\nimport type { FileProcessor } from '../core/fileProcessor';\r\n\r\n/** 上传目录的入参:环境、bucket、本地目录、路径前缀、过滤规则、Reporter、上传前文件处理器 */\r\nexport interface UploadDirectoryInput {\r\n env?: string;\r\n bucket: string;\r\n localDir: string;\r\n pathPrefix?: string;\r\n include?: string[];\r\n exclude?: string[];\r\n reporter?: Reporter;\r\n /** 上传前对文件内容的处理器(如图片压缩),未配置则直接上传原内容 */\r\n fileProcessor?: FileProcessor;\r\n}\r\n\r\n/**\r\n * 中间件:对主流程暴露统一 API(Facade)。\r\n * 内部只依赖 ProviderConfigResolver 得到当前 Provider 配置,再创建 StorageClient 与流程并执行。\r\n * 切换云厂商时只需改 ProviderConfigResolver 的【一个实现文件】。\r\n */\r\nexport class OssService {\r\n constructor(private readonly providerConfigResolver: ProviderConfigResolver) {}\r\n\r\n /** 上传指定本地目录到 OSS,key 规则为 basePrefix/env/pathPrefix/相对路径,同名则跳过 */\r\n async uploadDirectory(input: UploadDirectoryInput): Promise<UploadSummary> {\r\n const { env, bucket= 'hr-uat', localDir, pathPrefix, include, exclude, reporter, fileProcessor } = input;\r\n\r\n const resolved = this.providerConfigResolver.resolve();\r\n const { providerConfig, basePrefix, cdnBaseUrl } = resolved;\r\n\r\n const client = createStorageClient(providerConfig);\r\n\r\n const flow = new DirectoryUploadFlow({\r\n env,\r\n bucket,\r\n localDir,\r\n pathPrefix,\r\n include,\r\n exclude,\r\n basePrefix,\r\n storageClient: client,\r\n reporter,\r\n cdnBaseUrl,\r\n fileProcessor,\r\n });\r\n\r\n return flow.execute();\r\n }\r\n}\r\n\r\n","import { EnvConfigResolver, HuaweiObsConfig } from './types';\r\n\r\n/**\r\n * 从系统环境变量解析华为 OBS 配置的实现。\r\n * 只使用一套固定变量,不区分 dev/test/prod 等多环境前缀。\r\n */\r\nexport class EnvConfigResolverImpl implements EnvConfigResolver {\r\n /**\r\n * 解析华为 OBS 配置(endpoint、ak/sk、basePrefix 等)。\r\n * 当前实现忽略 env 参数,统一使用以下环境变量:\r\n * - OBS_ENDPOINT\r\n * - OBS_ACCESS_KEY / OBS_SECRET_KEY(也可通过 OBS_CREDENTIALS_URL 拉取 YAML 获得)\r\n * - OBS_REGION(可选)\r\n * - OBS_BASE_PREFIX(可选,作为所有 key 的基础前缀)\r\n * - OBS_CDN_BASE_URL(可选,CDN 加速根地址,上传完成后用于拼出访问 URL)\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n resolveHuaweiConfig(env?: string): HuaweiObsConfig {\r\n // 通过环境变量解构出 OBS 访问配置(不依赖 .env 文件,直接读取进程环境)\r\n const endpoint = process.env.OBS_ENDPOINT;\r\n const accessKey = process.env.OBS_ACCESS_KEY;\r\n const secretKey = process.env.OBS_SECRET_KEY;\r\n const region = process.env.OBS_REGION;\r\n // basePrefix 作为所有对象 key 的基础前缀,用于隔离项目/租户等\r\n const basePrefix = process.env.OBS_BASE_PREFIX;\r\n // CDN 加速根地址,配置后用于拼出上传完成后的访问 URL\r\n const cdnBaseUrl = process.env.OBS_CDN_BASE_URL;\r\n\r\n if (!endpoint || !accessKey || !secretKey) {\r\n // 缺少关键配置时立即抛错,避免在后续真正上传时才暴露连接失败的问题\r\n throw new Error('缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY');\r\n }\r\n\r\n return {\r\n endpoint,\r\n accessKey,\r\n secretKey,\r\n region,\r\n basePrefix: basePrefix || '',\r\n cdnBaseUrl,\r\n };\r\n }\r\n}\r\n\r\n","import { ProviderConfigResolver, ResolvedProviderConfig } from './types';\r\nimport { EnvConfigResolverImpl } from './EnvConfigResolverImpl';\r\n\r\n/**\r\n * 统一 Provider 配置解析的【唯一入口】。\r\n * 切换云厂商时只需改本文件:改 OSS_PROVIDER 环境变量或增加 case,无需改 OssService / CLI 等。\r\n *\r\n * 环境变量约定:\r\n * - OSS_PROVIDER:当前使用的存储,如 'huawei'(默认)\r\n * - 当 OSS_PROVIDER=huawei 时,使用 OBS_* 变量(见 EnvConfigResolverImpl)\r\n * - 后续接入阿里云等时,在此增加 case 并读取对应 env 即可。\r\n */\r\nexport class ProviderConfigResolverImpl implements ProviderConfigResolver {\r\n private readonly huaweiResolver = new EnvConfigResolverImpl();\r\n\r\n resolve(): ResolvedProviderConfig {\r\n const provider = process.env.OSS_PROVIDER;\r\n\r\n switch (provider) {\r\n case 'huawei': {\r\n const options = this.huaweiResolver.resolveHuaweiConfig();\r\n return {\r\n providerConfig: { type: 'huawei', options },\r\n basePrefix: options.basePrefix,\r\n cdnBaseUrl: options.cdnBaseUrl,\r\n };\r\n }\r\n // 后续切换或新增云厂商时,在此增加 case,例如:\r\n // case 'aliyun': { ... return { providerConfig: { type: 'aliyun', options }, basePrefix, cdnBaseUrl }; }\r\n default:\r\n throw new Error(`不支持的 OSS_PROVIDER: ${provider},当前仅支持 huawei`);\r\n }\r\n }\r\n}\r\n","/**\r\n * 上传结果上报抽象(Reporter)。\r\n * 默认实现为控制台输出,可扩展为企业微信、钉钉等通知。\r\n */\r\n\r\nexport type UploadStatus = 'success' | 'failed' | 'skipped';\r\n\r\n/** 单个文件的上传结果,供 Reporter 使用 */\r\nexport interface UploadFileResult {\r\n localPath: string;\r\n relativePath: string;\r\n bucket: string;\r\n key: string;\r\n status: UploadStatus;\r\n error?: Error;\r\n /** 配置了 CDN 时的完整访问地址,由 buildCdnAccessUrl 生成 */\r\n accessUrl?: string;\r\n}\r\n\r\n/** 整次上传的汇总统计 */\r\nexport interface UploadSummary {\r\n total: number;\r\n success: number;\r\n failed: number;\r\n skipped: number;\r\n}\r\n\r\n/** Reporter 接口:上传开始、每个文件结果、全部完成时回调,便于接入不同通知方式 */\r\nexport interface Reporter {\r\n onStart?(context: {\r\n env?: string;\r\n bucket: string;\r\n basePrefix: string;\r\n pathPrefix?: string;\r\n localDir: string;\r\n cdnBaseUrl?: string;\r\n }): void | Promise<void>;\r\n\r\n onFileResult?(result: UploadFileResult): void | Promise<void>;\r\n\r\n onComplete?(summary: UploadSummary): void | Promise<void>;\r\n}\r\n\r\n/** 默认实现:将上传进度与结果输出到控制台 */\r\nexport class ConsoleReporter implements Reporter {\r\n onStart(context: {\r\n env?: string;\r\n bucket: string;\r\n basePrefix: string;\r\n pathPrefix?: string;\r\n localDir: string;\r\n cdnBaseUrl?: string;\r\n }): void {\r\n const { env, bucket, basePrefix, pathPrefix, localDir, cdnBaseUrl } = context;\r\n // console.log('开始上传目录:');\r\n // console.log(` 环境: ${env ?? '未指定'}`);\r\n // console.log(` Bucket: ${bucket}`);\r\n // console.log(` Base 前缀: ${basePrefix || '(空)'}`);\r\n // console.log(` 业务前缀(pathPrefix): ${pathPrefix || '(空)'}`);\r\n // console.log(` 本地目录: ${localDir}`);\r\n if (cdnBaseUrl) {\r\n console.log(` CDN 根地址: ${cdnBaseUrl}`);\r\n }\r\n }\r\n\r\n onFileResult(result: UploadFileResult): void {\r\n const { status, localPath, key, error, accessUrl } = result;\r\n if (status === 'success') {\r\n console.log(`[SUCCESS] ${localPath} -> ${key}`);\r\n if (accessUrl) console.log(` 访问地址: ${accessUrl}`);\r\n } else if (status === 'skipped') {\r\n console.log(`[SKIPPED] ${localPath} -> ${key} (云端同名已存在)`);\r\n if (accessUrl) console.log(` 访问地址: ${accessUrl}`);\r\n } else {\r\n console.error(`[FAILED] ${localPath} -> ${key}`);\r\n if (error) console.error(` Error: ${error.message}`);\r\n }\r\n }\r\n\r\n onComplete(summary: UploadSummary): void {\r\n console.log('上传完成:');\r\n console.log(` 总数: ${summary.total}`);\r\n console.log(` 成功: ${summary.success}`);\r\n console.log(` 失败: ${summary.failed}`);\r\n console.log(` 跳过: ${summary.skipped}`);\r\n }\r\n}\r\n\r\n","/**\r\n * 当配置了 OBS_CREDENTIALS_URL 且未配置 AK/SK 时,从该 URL 拉取 YAML 并解析出\r\n * OBS_ACCESS_KEY、OBS_SECRET_KEY 写入 process.env,供 EnvConfigResolverImpl 使用。\r\n */\r\nimport yaml from 'js-yaml';\r\n\r\nconst CREDENTIALS_URL_KEY = 'OBS_CREDENTIALS_URL';\r\n// const ACCESS_KEY_KEY = 'OBS_ACCESS_KEY';\r\n// const SECRET_KEY_KEY = 'OBS_SECRET_KEY';\r\n\r\n/**\r\n * 若设置了 OBS_CREDENTIALS_URL 且当前未设置 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,\r\n * 则请求该 URL,解析 YAML,并将 OBS_ACCESS_KEY、OBS_SECRET_KEY 写入 process.env。\r\n */\r\nexport async function loadObsCredentialsFromUrlIfNeeded(): Promise<void> {\r\n const url = process.env.OBS_CREDENTIALS_URL;\r\n const hasAccessKey = process.env.OBS_ACCESS_KEY;\r\n const hasSecretKey = process.env.OBS_SECRET_KEY;\r\n \r\n if (!url || (hasAccessKey && hasSecretKey)) {\r\n return;\r\n }\r\n\r\n const res = await fetch(url);\r\n \r\n if (!res.ok) {\r\n throw new Error(`拉取 OBS 凭证失败 (${res.status}): ${url}`);\r\n }\r\n const text = await res.text();\r\n const parsed = yaml.load(text) as Record<string, unknown> | undefined;\r\n if (!parsed || typeof parsed !== 'object') {\r\n throw new Error(\r\n `OBS 凭证 YAML 解析结果无效,请检查 ${CREDENTIALS_URL_KEY} 返回内容`\r\n );\r\n }\r\n const accessKey = parsed.OBS_ACCESS_KEY;\r\n const secretKey = parsed.OBS_SECRET_KEY;\r\n if (typeof accessKey === 'string') process.env.OBS_ACCESS_KEY = accessKey;\r\n if (typeof secretKey === 'string') process.env.OBS_SECRET_KEY = secretKey;\r\n \r\n if (!process.env.OBS_ACCESS_KEY || !process.env.OBS_SECRET_KEY) {\r\n throw new Error(\r\n `OBS 凭证 YAML 中未找到 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,请检查 ${CREDENTIALS_URL_KEY} 返回内容`\r\n );\r\n }\r\n}\r\n"],"names":[],"mappings":";;;;;;;;;AAAA;;;;AAIG;AACH,CAAC,YAAW;IACV,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,0BAA0B,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC3G,OAAO,WAAW,CAAC;QACnB,4DAAiC;QACjC,4EAA6C;QAC7C,wDAAyB;AAC1B,KAAA,CAAC;AAEF,IAAA,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE;IAE7B;SACG,IAAI,CAAC,YAAY;SACjB,WAAW,CAAC,wBAAwB;AACpC,SAAA,QAAQ,CAAC,YAAY,EAAE,MAAM;AAC7B,SAAA,cAAc,CAAC,uBAAuB,EAAE,WAAW,EAAC,QAAQ;AAC5D,SAAA,MAAM,CAAC,iBAAiB,EAAE,sBAAsB;AAChD,SAAA,MAAM,CAAC,4BAA4B,EAAE,wBAAwB;AAC7D,SAAA,MAAM,CAAC,wBAAwB,EAAE,cAAc;AAC/C,SAAA,MAAM,CAAC,wBAAwB,EAAE,cAAc;AAC/C,SAAA,MAAM,CAAC,OAAO,QAAgB,EAAE,OAAY,KAAI;QAC/C,MAAM,EAAE,iCAAiC,EAAE,GAAG,MAAM,yEAA4C;QAChG,MAAM,iCAAiC,EAAE;QAEzC,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,IAAI,0BAA0B,EAAE,CAAC;AAChE,QAAA,MAAM,QAAQ,GAAG,IAAI,eAAe,EAAE;AAEtC,QAAA,IAAI;AACF,YAAA,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC;gBAC5C,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,QAAQ;gBACR,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,QAAQ;AACT,aAAA,CAAC;AAEF,YAAA,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE;AACtB,gBAAA,OAAO,CAAC,QAAQ,GAAG,CAAC;YACtB;QACF;QAAE,OAAO,GAAQ,EAAE;;YAEjB,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC;AAChD,YAAA,OAAO,CAAC,QAAQ,GAAG,CAAC;QACtB;AACF,IAAA,CAAC,CAAC;AAEJ,IAAA,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;AAC7B,CAAC,GAAG;;AClDJ;AACA,MAAM,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAE5C;;;AAGG;MACU,eAAe,CAAA;AAG1B,IAAA,WAAA,CAA6B,MAAuB,EAAA;QAAvB,IAAA,CAAA,MAAM,GAAN,MAAM;AACjC,QAAA,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC;YAC1B,aAAa,EAAE,MAAM,CAAC,SAAS;YAC/B,iBAAiB,EAAE,MAAM,CAAC,SAAS;YACnC,MAAM,EAAE,MAAM,CAAC,QAAQ;AACxB,SAAA,CAAC;IACJ;AAEA;;AAEG;AACH,IAAA,MAAM,UAAU,CAAC,MAAc,EAAE,GAAW,EAAA;AAC1C,QAAA,IAAI;YACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC;AACjD,gBAAA,MAAM,EAAE,MAAM;AACd,gBAAA,GAAG,EAAE,GAAG;AACT,aAAA,CAAC;YAEF,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,IAAI,GAAG,EAAE;AAClC,gBAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE;YACzB;;YAGA,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,KAAK,WAAW,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,KAAK,UAAU,EAAE;AACpH,gBAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE;YAC1B;YAEA,MAAM,IAAI,KAAK,CAAC,CAAA,0BAAA,EAA6B,MAAM,CAAC,SAAS,CAAC,MAAM,CAAA,CAAA,EAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAA,CAAA,EAAI,MAAM,CAAC,SAAS,CAAC,OAAO,CAAA,CAAE,CAAC;QAC9H;QAAE,OAAO,GAAY,EAAE;YACrB,MAAM,GAAG,GAAG,GAAG,IAAI,OAAQ,GAAW,CAAC,OAAO,KAAK,QAAQ,GAAI,GAAa,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC;YAClG,MAAM,IAAI,GAAI,GAAW,EAAE,IAAI,IAAK,GAAW,EAAE,IAAI;YACrD,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,UAAU,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE;AACnG,gBAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE;YAC1B;AACA,YAAA,MAAM,GAAG;QACX;IACF;;IAGA,MAAM,SAAS,CAAC,OAAyB,EAAA;QACvC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,OAAO;;QAElD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC;AACzC,YAAA,MAAM,EAAE,MAAM;AACd,YAAA,GAAG,EAAE,GAAG;AACR,YAAA,IAAI,EAAE,IAAI;;AAEX,SAAA,CAAC;QAEF,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,GAAG,EAAE;AACjC,YAAA,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,SAAS;YAC7D,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;AAC3E,YAAA,MAAM,IAAI,KAAK,CACb,qBAAqB,MAAM,IAAI,KAAK,CAAA,GAAA,CAAK;AACvC,gBAAA,wFAAwF,CAC3F;QACH;IACF;AACD;;ACnED;;;AAGG;AACG,SAAU,mBAAmB,CAAC,MAAsB,EAAA;AACxD,IAAA,QAAQ,MAAM,CAAC,IAAI;AACjB,QAAA,KAAK,QAAQ;AACX,YAAA,OAAO,IAAI,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC;AAC5C,QAAA;YACE,MAAM,IAAI,KAAK,CAAC,CAAA,2BAAA,EAA8B,MAAM,CAAC,IAAI,CAAA,CAAE,CAAC;;AAElE;;ACbA;;;;;;;AAOG;SACa,aAAa,CAC3B,YAAoB,EACpB,OAAkB,EAClB,OAAkB,EAAA;IAElB,IAAI,QAAQ,GAAG,IAAI;IAEnB,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE;QACjC,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC;IACtD;AACA,IAAA,IAAI,CAAC,QAAQ;AAAE,QAAA,OAAO,KAAK;IAE3B,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE;QACjC,IAAI,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE;AAC7C,YAAA,OAAO,KAAK;QACd;IACF;AAEA,IAAA,OAAO,IAAI;AACb;;ACCA;;;;AAIG;MACmB,kBAAkB,CAAA;AACtC,IAAA,WAAA,CAAyC,OAAsB,EAAA;QAAtB,IAAA,CAAA,OAAO,GAAP,OAAO;IAAkB;;AAGlE,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,OAAO;AAEjC,QAAA,IAAI,QAAQ,EAAE,OAAO,EAAE;YACrB,MAAM,QAAQ,CAAC,OAAO,CAAC;AACrB,gBAAA,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG;AACrB,gBAAA,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM;AAC3B,gBAAA,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU;AACnC,gBAAA,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU;AACnC,gBAAA,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;AAC/B,gBAAA,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU;AACpC,aAAA,CAAC;QACJ;AAEA,QAAA,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;QACxC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;AAEhD,QAAA,IAAI,QAAQ,EAAE,UAAU,EAAE;AACxB,YAAA,MAAM,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC;QACpC;AAEA,QAAA,OAAO,OAAO;IAChB;;AAMU,IAAA,WAAW,CAAC,KAAkB,EAAA;QACtC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO;QACzC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC7E;AAEA;;;AAGG;AACO,IAAA,QAAQ,CAAC,YAAoB,EAAA;QACrC,MAAM,KAAK,GAAa,EAAE;AAC1B,QAAA,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;AAChE,QAAA,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;AAClD,QAAA,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;AAEhE,QAAA,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC5C,QAAA,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;IACxB;;AAGU,IAAA,MAAM,OAAO,CAAC,OAAe,EAAE,UAAU,GAAG,EAAE,EAAA;QACtD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC;AAC9C,QAAA,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;QAClE,MAAM,KAAK,GAAgB,EAAE;AAE7B,QAAA,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE;AAC3B,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC;YAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC;AAEnC,YAAA,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE;AACvB,gBAAA,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;YACnD;AAAO,iBAAA,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE;gBACzB,KAAK,CAAC,IAAI,CAAC;AACT,oBAAA,YAAY,EAAE,GAAG;oBACjB,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;AACtC,iBAAA,CAAC;YACJ;QACF;AAEA,QAAA,OAAO,KAAK;IACd;;IAGU,MAAM,gBAAgB,CAAC,MAAwB,EAAA;AACvD,QAAA,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,OAAO;AACjC,QAAA,IAAI,QAAQ,EAAE,YAAY,EAAE;AAC1B,YAAA,MAAM,QAAQ,CAAC,YAAY,CAAC,MAAM,CAAC;QACrC;IACF;AACD;;ACrHD;;;;;AAKG;AACG,SAAU,iBAAiB,CAAC,UAAkB,EAAE,GAAW,EAAA;IAC/D,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IAC3C,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;AACxC,IAAA,OAAO,QAAQ,GAAG,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAE,GAAG,IAAI;AAChD;;ACLA;;;AAGG;AACG,MAAO,mBAAoB,SAAQ,kBAAkB,CAAA;AACzD,IAAA,WAAA,CAAY,OAAsB,EAAA;QAChC,KAAK,CAAC,OAAO,CAAC;IAChB;;AAGU,IAAA,MAAM,YAAY,GAAA;QAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;IAC5C;AAEA;;;AAGG;IACO,MAAM,WAAW,CAAC,KAAkB,EAAA;QAC5C,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,OAAO;AAE1D,QAAA,MAAM,OAAO,GAAkB;YAC7B,KAAK,EAAE,KAAK,CAAC,MAAM;AACnB,YAAA,OAAO,EAAE,CAAC;AACV,YAAA,MAAM,EAAE,CAAC;AACT,YAAA,OAAO,EAAE,CAAC;SACX;AAED,QAAA,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;YACxB,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC;AAE5C,YAAA,IAAI;gBACF,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC;;AAGxD,gBAAA,IAAI,IAAI,CAAC,MAAM,EAAE;AACf,oBAAA,OAAO,CAAC,OAAO,IAAI,CAAC;oBACpB,MAAM,IAAI,CAAC,gBAAgB,CAAC;wBAC1B,SAAS,EAAE,IAAI,CAAC,YAAY;wBAC5B,YAAY,EAAE,IAAI,CAAC,YAAY;wBAC/B,MAAM;wBACN,GAAG;AACH,wBAAA,MAAM,EAAE,SAAS;AACjB,wBAAA,IAAI,UAAU,IAAI,EAAE,SAAS,EAAE,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC;AACrE,qBAAA,CAAC;oBACF;gBACF;gBAEA,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC;gBAEjD,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;sBACzC,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC;wBACvC,SAAS,EAAE,IAAI,CAAC,YAAY;wBAC5B,YAAY,EAAE,IAAI,CAAC,YAAY;AAC/B,wBAAA,MAAM,EAAE,IAAI;qBACb;sBACD,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,SAA+B,EAAE;gBAElE,MAAM,aAAa,CAAC,SAAS,CAAC;oBAC5B,MAAM;oBACN,GAAG;AACH,oBAAA,IAAI,EAAE,MAAM;oBACZ,WAAW;AACZ,iBAAA,CAAC;AAEF,gBAAA,OAAO,CAAC,OAAO,IAAI,CAAC;gBACpB,MAAM,IAAI,CAAC,gBAAgB,CAAC;oBAC1B,SAAS,EAAE,IAAI,CAAC,YAAY;oBAC5B,YAAY,EAAE,IAAI,CAAC,YAAY;oBAC/B,MAAM;oBACN,GAAG;AACH,oBAAA,MAAM,EAAE,SAAS;AACjB,oBAAA,IAAI,UAAU,IAAI,EAAE,SAAS,EAAE,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC;AACrE,iBAAA,CAAC;YACJ;YAAE,OAAO,GAAQ,EAAE;AACjB,gBAAA,OAAO,CAAC,MAAM,IAAI,CAAC;gBACnB,MAAM,IAAI,CAAC,gBAAgB,CAAC;oBAC1B,SAAS,EAAE,IAAI,CAAC,YAAY;oBAC5B,YAAY,EAAE,IAAI,CAAC,YAAY;oBAC/B,MAAM;oBACN,GAAG;AACH,oBAAA,MAAM,EAAE,QAAQ;AAChB,oBAAA,KAAK,EAAE,GAAG,YAAY,KAAK,GAAG,GAAG,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC3D,iBAAA,CAAC;YACJ;QACF;AAEA,QAAA,OAAO,OAAO;IAChB;AACD;;AC3ED;;;;AAIG;MACU,UAAU,CAAA;AACrB,IAAA,WAAA,CAA6B,sBAA8C,EAAA;QAA9C,IAAA,CAAA,sBAAsB,GAAtB,sBAAsB;IAA2B;;IAG9E,MAAM,eAAe,CAAC,KAA2B,EAAA;QAC/C,MAAM,EAAE,GAAG,EAAE,MAAM,GAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,KAAK;QAExG,MAAM,QAAQ,GAAG,IAAI,CAAC,sBAAsB,CAAC,OAAO,EAAE;QACtD,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,QAAQ;AAE3D,QAAA,MAAM,MAAM,GAAG,mBAAmB,CAAC,cAAc,CAAC;AAElD,QAAA,MAAM,IAAI,GAAG,IAAI,mBAAmB,CAAC;YACnC,GAAG;YACH,MAAM;YACN,QAAQ;YACR,UAAU;YACV,OAAO;YACP,OAAO;YACP,UAAU;AACV,YAAA,aAAa,EAAE,MAAM;YACrB,QAAQ;YACR,UAAU;YACV,aAAa;AACd,SAAA,CAAC;AAEF,QAAA,OAAO,IAAI,CAAC,OAAO,EAAE;IACvB;AACD;;;;;;;AClDD;;;AAGG;MACU,qBAAqB,CAAA;AAChC;;;;;;;;AAQG;;AAEH,IAAA,mBAAmB,CAAC,GAAY,EAAA;;QAE9B,MAAM,QAAQ,GAAG,yCAAwB;AACzC,QAAA,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc;AAC5C,QAAA,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc;QAC5C,MAAM,MAAM,GAAG,WAAsB;;QAErC,MAAM,UAAU,GAAG,YAA2B;;QAE9C,MAAM,UAAU,GAAG,oCAA4B;QAE/C,IAAiB,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE;;AAEzC,YAAA,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC;QAChF;QAEA,OAAO;YACL,QAAQ;YACR,SAAS;YACT,SAAS;YACT,MAAM;YACN,UAAU,EAAE,UAAgB;YAC5B,UAAU;SACX;IACH;AACD;;ACvCD;;;;;;;;AAQG;MACU,0BAA0B,CAAA;AAAvC,IAAA,WAAA,GAAA;AACmB,QAAA,IAAA,CAAA,cAAc,GAAG,IAAI,qBAAqB,EAAE;IAoB/D;IAlBE,OAAO,GAAA;QACL,MAAM,QAAQ,GAAG,QAAwB;QAEzC,QAAQ,QAAQ;YACd,KAAK,QAAQ,EAAE;gBACb,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,mBAAmB,EAAE;gBACzD,OAAO;AACL,oBAAA,cAAc,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE;oBAC3C,UAAU,EAAE,OAAO,CAAC,UAAU;oBAC9B,UAAU,EAAE,OAAO,CAAC,UAAU;iBAC/B;YACH;;;AAGA,YAAA;AACE,gBAAA,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,CAAA,aAAA,CAAe,CAAC;;IAEpE;AACD;;;;;;;ACjCD;;;AAGG;AAwCH;MACa,eAAe,CAAA;AAC1B,IAAA,OAAO,CAAC,OAOP,EAAA;AACC,QAAA,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO;;;;;;;QAO7E,IAAI,UAAU,EAAE;AACd,YAAA,OAAO,CAAC,GAAG,CAAC,cAAc,UAAU,CAAA,CAAE,CAAC;QACzC;IACF;AAEA,IAAA,YAAY,CAAC,MAAwB,EAAA;AACnC,QAAA,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM;AAC3D,QAAA,IAAI,MAAM,KAAK,SAAS,EAAE;YACxB,OAAO,CAAC,GAAG,CAAC,CAAA,UAAA,EAAa,SAAS,CAAA,IAAA,EAAO,GAAG,CAAA,CAAE,CAAC;AAC/C,YAAA,IAAI,SAAS;AAAE,gBAAA,OAAO,CAAC,GAAG,CAAC,WAAW,SAAS,CAAA,CAAE,CAAC;QACpD;AAAO,aAAA,IAAI,MAAM,KAAK,SAAS,EAAE;YAC/B,OAAO,CAAC,GAAG,CAAC,CAAA,UAAA,EAAa,SAAS,CAAA,IAAA,EAAO,GAAG,CAAA,UAAA,CAAY,CAAC;AACzD,YAAA,IAAI,SAAS;AAAE,gBAAA,OAAO,CAAC,GAAG,CAAC,WAAW,SAAS,CAAA,CAAE,CAAC;QACpD;aAAO;YACL,OAAO,CAAC,KAAK,CAAC,CAAA,UAAA,EAAa,SAAS,CAAA,IAAA,EAAO,GAAG,CAAA,CAAE,CAAC;AACjD,YAAA,IAAI,KAAK;gBAAE,OAAO,CAAC,KAAK,CAAC,CAAA,SAAA,EAAY,KAAK,CAAC,OAAO,CAAA,CAAE,CAAC;QACvD;IACF;AAEA,IAAA,UAAU,CAAC,OAAsB,EAAA;AAC/B,QAAA,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,CAAA,MAAA,EAAS,OAAO,CAAC,KAAK,CAAA,CAAE,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,CAAA,MAAA,EAAS,OAAO,CAAC,OAAO,CAAA,CAAE,CAAC;QACvC,OAAO,CAAC,GAAG,CAAC,CAAA,MAAA,EAAS,OAAO,CAAC,MAAM,CAAA,CAAE,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,CAAA,MAAA,EAAS,OAAO,CAAC,OAAO,CAAA,CAAE,CAAC;IACzC;AACD;;;;;;;ACtFD;;;AAGG;AAGH,MAAM,mBAAmB,GAAG,qBAAqB;AACjD;AACA;AAEA;;;AAGG;AACI,eAAe,iCAAiC,GAAA;IACrD,MAAM,GAAG,GAAG,iEAA+B;AAC3C,IAAA,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc;AAC/C,IAAA,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc;IAE/C,IAAY,CAAC,YAAY,IAAI,YAAY,CAAC,EAAE;QAC1C;IACF;AAEA,IAAA,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC;AAE5B,IAAA,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE;QACX,MAAM,IAAI,KAAK,CAAC,CAAA,aAAA,EAAgB,GAAG,CAAC,MAAM,CAAA,GAAA,EAAM,GAAG,CAAA,CAAE,CAAC;IACxD;AACA,IAAA,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE;IAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAwC;IACrE,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AACzC,QAAA,MAAM,IAAI,KAAK,CACb,0BAA0B,mBAAmB,CAAA,KAAA,CAAO,CACrD;IACH;AACA,IAAA,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc;AACvC,IAAA,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc;IACvC,IAAI,OAAO,SAAS,KAAK,QAAQ;AAAE,QAAA,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,SAAS;IACzE,IAAI,OAAO,SAAS,KAAK,QAAQ;AAAE,QAAA,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,SAAS;AAEzE,IAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE;AAC9D,QAAA,MAAM,IAAI,KAAK,CACb,wDAAwD,mBAAmB,CAAA,KAAA,CAAO,CACnF;IACH;AACF;;;;;;;"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EnvConfigResolver, HuaweiObsConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 从系统环境变量解析华为 OBS 配置的实现。
|
|
4
|
+
* 只使用一套固定变量,不区分 dev/test/prod 等多环境前缀。
|
|
5
|
+
*/
|
|
6
|
+
export declare class EnvConfigResolverImpl implements EnvConfigResolver {
|
|
7
|
+
/**
|
|
8
|
+
* 解析华为 OBS 配置(endpoint、ak/sk、basePrefix 等)。
|
|
9
|
+
* 当前实现忽略 env 参数,统一使用以下环境变量:
|
|
10
|
+
* - OBS_ENDPOINT
|
|
11
|
+
* - OBS_ACCESS_KEY / OBS_SECRET_KEY(也可通过 OBS_CREDENTIALS_URL 拉取 YAML 获得)
|
|
12
|
+
* - OBS_REGION(可选)
|
|
13
|
+
* - OBS_BASE_PREFIX(可选,作为所有 key 的基础前缀)
|
|
14
|
+
* - OBS_CDN_BASE_URL(可选,CDN 加速根地址,上传完成后用于拼出访问 URL)
|
|
15
|
+
*/
|
|
16
|
+
resolveHuaweiConfig(env?: string): HuaweiObsConfig;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=EnvConfigResolverImpl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EnvConfigResolverImpl.d.ts","sourceRoot":"","sources":["../../src/config/EnvConfigResolverImpl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE7D;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,iBAAiB;IAC7D;;;;;;;;OAQG;IAEH,mBAAmB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,eAAe;CAyBnD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ProviderConfigResolver, ResolvedProviderConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 统一 Provider 配置解析的【唯一入口】。
|
|
4
|
+
* 切换云厂商时只需改本文件:改 OSS_PROVIDER 环境变量或增加 case,无需改 OssService / CLI 等。
|
|
5
|
+
*
|
|
6
|
+
* 环境变量约定:
|
|
7
|
+
* - OSS_PROVIDER:当前使用的存储,如 'huawei'(默认)
|
|
8
|
+
* - 当 OSS_PROVIDER=huawei 时,使用 OBS_* 变量(见 EnvConfigResolverImpl)
|
|
9
|
+
* - 后续接入阿里云等时,在此增加 case 并读取对应 env 即可。
|
|
10
|
+
*/
|
|
11
|
+
export declare class ProviderConfigResolverImpl implements ProviderConfigResolver {
|
|
12
|
+
private readonly huaweiResolver;
|
|
13
|
+
resolve(): ResolvedProviderConfig;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=ProviderConfigResolverImpl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProviderConfigResolverImpl.d.ts","sourceRoot":"","sources":["../../src/config/ProviderConfigResolverImpl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAGzE;;;;;;;;GAQG;AACH,qBAAa,0BAA2B,YAAW,sBAAsB;IACvE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+B;IAE9D,OAAO,IAAI,sBAAsB;CAkBlC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 若设置了 OBS_CREDENTIALS_URL 且当前未设置 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,
|
|
3
|
+
* 则请求该 URL,解析 YAML,并将 OBS_ACCESS_KEY、OBS_SECRET_KEY 写入 process.env。
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadObsCredentialsFromUrlIfNeeded(): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=loadObsCredentialsFromUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loadObsCredentialsFromUrl.d.ts","sourceRoot":"","sources":["../../src/config/loadObsCredentialsFromUrl.ts"],"names":[],"mappings":"AAUA;;;GAGG;AACH,wBAAsB,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC,CA+BvE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置与类型定义:华为 OBS、Provider、环境解析器等。
|
|
3
|
+
*/
|
|
4
|
+
/** 华为 OBS 连接与鉴权配置,含 basePrefix(所有 key 的基础前缀,仅在 config 中配置) */
|
|
5
|
+
export interface HuaweiObsConfig {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
accessKey: string;
|
|
8
|
+
secretKey: string;
|
|
9
|
+
region?: string;
|
|
10
|
+
basePrefix: string;
|
|
11
|
+
/** CDN 加速根地址,配置后上传完成可拼出完整访问 URL(如 https://cdn.example.com) */
|
|
12
|
+
cdnBaseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
/** 当前支持的存储 Provider 类型 */
|
|
15
|
+
export type ProviderType = 'huawei';
|
|
16
|
+
/** 创建 StorageClient 时传入的配置,按 type 选择具体实现 */
|
|
17
|
+
export interface ProviderConfig {
|
|
18
|
+
type: ProviderType;
|
|
19
|
+
options: HuaweiObsConfig;
|
|
20
|
+
}
|
|
21
|
+
/** 根据环境标识解析华为 OBS 配置的接口,便于替换为配置文件等实现 */
|
|
22
|
+
export interface EnvConfigResolver {
|
|
23
|
+
resolveHuaweiConfig(env?: string): HuaweiObsConfig;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 解析后的 Provider 配置(含上传流程所需的 basePrefix、cdnBaseUrl)。
|
|
27
|
+
* 由 ProviderConfigResolver 统一返回,OssService 只依赖此结构,不关心具体云厂商。
|
|
28
|
+
*/
|
|
29
|
+
export interface ResolvedProviderConfig {
|
|
30
|
+
providerConfig: ProviderConfig;
|
|
31
|
+
basePrefix: string;
|
|
32
|
+
cdnBaseUrl?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 统一 Provider 配置解析器:返回当前要用的 Provider 及其配置。
|
|
36
|
+
* 切换云厂商时只需改此接口的【一个实现文件】即可(如改 env 或增加 case)。
|
|
37
|
+
*/
|
|
38
|
+
export interface ProviderConfigResolver {
|
|
39
|
+
resolve(): ResolvedProviderConfig;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,0BAA0B;AAC1B,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;AAEpC,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,eAAe,CAAC;CAC1B;AAED,wCAAwC;AACxC,MAAM,WAAW,iBAAiB;IAChC,mBAAmB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;CACpD;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,IAAI,sBAAsB,CAAC;CACnC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { UploadFlowTemplate, UploadContext, LocalFile } from './UploadFlowTemplate';
|
|
2
|
+
import { UploadSummary } from './reporter';
|
|
3
|
+
/**
|
|
4
|
+
* 目录上传流程的具体实现:遍历本地目录,按 key 规则上传,云端同名则跳过。
|
|
5
|
+
* 规则:先 headObject,exists 则 skipped,否则 putObject。
|
|
6
|
+
*/
|
|
7
|
+
export declare class DirectoryUploadFlow extends UploadFlowTemplate {
|
|
8
|
+
constructor(context: UploadContext);
|
|
9
|
+
/** 递归收集 localDir 下所有文件 */
|
|
10
|
+
protected collectFiles(): Promise<LocalFile[]>;
|
|
11
|
+
/**
|
|
12
|
+
* 逐个文件上传:先 head 判断是否存在,存在则跳过,否则读取本地文件并 putObject。
|
|
13
|
+
* 每个文件的结果通过 reportFileResult 交给 Reporter。
|
|
14
|
+
*/
|
|
15
|
+
protected uploadFiles(files: LocalFile[]): Promise<UploadSummary>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=DirectoryUploadFlow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DirectoryUploadFlow.d.ts","sourceRoot":"","sources":["../../src/core/DirectoryUploadFlow.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,kBAAkB;gBAC7C,OAAO,EAAE,aAAa;IAIlC,0BAA0B;cACV,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAIpD;;;OAGG;cACa,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC;CAuExE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存储客户端统一接口(中间件层与各云厂商 Adapter 的契约)。
|
|
3
|
+
* 实现类负责将 putObject/headObject 翻译为具体云厂商的 API 调用。
|
|
4
|
+
*/
|
|
5
|
+
/** 上传单个对象时的参数 */
|
|
6
|
+
export interface PutObjectOptions {
|
|
7
|
+
bucket: string;
|
|
8
|
+
key: string;
|
|
9
|
+
body: Buffer | NodeJS.ReadableStream;
|
|
10
|
+
contentType?: string;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
/** headObject 的返回结果,仅用于判断云端是否已有同名对象(决定是否跳过上传) */
|
|
14
|
+
export interface HeadObjectResult {
|
|
15
|
+
exists: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** 统一存储客户端接口,各 Provider(华为 OBS、阿里 OSS、S3 等)需实现此接口 */
|
|
18
|
+
export interface StorageClient {
|
|
19
|
+
putObject(options: PutObjectOptions): Promise<void>;
|
|
20
|
+
headObject(bucket: string, key: string): Promise<HeadObjectResult>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=StorageClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StorageClient.d.ts","sourceRoot":"","sources":["../../src/core/StorageClient.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,iBAAiB;AACjB,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACpE"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { StorageClient } from './StorageClient';
|
|
2
|
+
import { ProviderConfig } from '../config/types';
|
|
3
|
+
/**
|
|
4
|
+
* 根据 Provider 类型创建对应的 StorageClient(简单工厂)。
|
|
5
|
+
* 后续扩展阿里 OSS、AWS S3 时在此增加 case 即可。
|
|
6
|
+
*/
|
|
7
|
+
export declare function createStorageClient(config: ProviderConfig): StorageClient;
|
|
8
|
+
//# sourceMappingURL=StorageFactory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StorageFactory.d.ts","sourceRoot":"","sources":["../../src/core/StorageFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,CAOzE"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Reporter, UploadSummary, UploadFileResult } from './reporter';
|
|
2
|
+
import { StorageClient } from './StorageClient';
|
|
3
|
+
import type { FileProcessor } from './fileProcessor';
|
|
4
|
+
/** 上传流程的上下文:环境、bucket、前缀、本地目录、存储客户端、Reporter、CDN 根地址、上传前处理器等 */
|
|
5
|
+
export interface UploadContext {
|
|
6
|
+
env?: string;
|
|
7
|
+
bucket: string;
|
|
8
|
+
basePrefix: string;
|
|
9
|
+
pathPrefix?: string;
|
|
10
|
+
localDir: string;
|
|
11
|
+
include?: string[];
|
|
12
|
+
exclude?: string[];
|
|
13
|
+
storageClient: StorageClient;
|
|
14
|
+
reporter?: Reporter;
|
|
15
|
+
/** CDN 加速根地址,配置后可为每个文件生成 accessUrl */
|
|
16
|
+
cdnBaseUrl?: string;
|
|
17
|
+
/** 上传前对文件内容的处理器(如图片压缩),未配置则直接上传原内容 */
|
|
18
|
+
fileProcessor?: FileProcessor;
|
|
19
|
+
}
|
|
20
|
+
/** 本地文件信息:绝对路径与相对路径 */
|
|
21
|
+
export interface LocalFile {
|
|
22
|
+
absolutePath: string;
|
|
23
|
+
relativePath: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 上传流程模板(Template Method)。
|
|
27
|
+
* 定义固定步骤:onStart -> collectFiles -> filterFiles -> uploadFiles -> onComplete,
|
|
28
|
+
* 子类实现 collectFiles 与 uploadFiles,其余步骤可复用或重写。
|
|
29
|
+
*/
|
|
30
|
+
export declare abstract class UploadFlowTemplate {
|
|
31
|
+
protected readonly context: UploadContext;
|
|
32
|
+
protected constructor(context: UploadContext);
|
|
33
|
+
/** 执行完整上传流程 */
|
|
34
|
+
execute(): Promise<UploadSummary>;
|
|
35
|
+
protected abstract collectFiles(): Promise<LocalFile[]>;
|
|
36
|
+
protected abstract uploadFiles(files: LocalFile[]): Promise<UploadSummary>;
|
|
37
|
+
/** 使用 include/exclude 过滤文件列表 */
|
|
38
|
+
protected filterFiles(files: LocalFile[]): LocalFile[];
|
|
39
|
+
/**
|
|
40
|
+
* 构造远端 key:basePrefix / env / pathPrefix / relativePath。
|
|
41
|
+
* 保持目录结构,且 base 前缀来自 config,不对外暴露。
|
|
42
|
+
*/
|
|
43
|
+
protected buildKey(relativePath: string): string;
|
|
44
|
+
/** 递归遍历目录,收集所有文件的绝对路径与相对路径 */
|
|
45
|
+
protected walkDir(rootDir: string, currentDir?: string): Promise<LocalFile[]>;
|
|
46
|
+
/** 将单个文件的上传结果交给 Reporter */
|
|
47
|
+
protected reportFileResult(result: UploadFileResult): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=UploadFlowTemplate.d.ts.map
|