@dd-code/oss-uploader 0.1.6 → 0.1.7
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/README.md +57 -78
- package/dist/cli.cjs +1 -1
- package/dist/index.cjs +1 -1
- package/package.json +1 -1
- package/src/config/EnvConfigResolverImpl.ts +44 -44
- package/src/core/UploadService.ts +1 -1
package/README.md
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
## 功能特性
|
|
8
8
|
|
|
9
9
|
- **目录上传**:按本地目录结构上传到指定 Bucket,远端 key 规则为 `basePrefix / env / pathPrefix / 相对路径`。
|
|
10
|
-
-
|
|
10
|
+
- **同名跳过**:上传前按前缀调用 OBS `listObjects` 拉取已有 key,内存 Set 判断是否存在,**仅 1 次(或少量)列表请求**,不再逐文件 head,大幅提速。
|
|
11
|
+
- **强制覆盖**:环境变量 `FILE_RE_WHITE_LIST`(JSON 数组)中匹配到的 key 始终上传、不跳过。
|
|
11
12
|
- **过滤规则**:支持 `--include` / `--exclude` 通配符(如 `**/*.js`、`**/*.map`),使用 [micromatch](https://github.com/micromatch/micromatch)。
|
|
13
|
+
- **上传进度**:控制台显示进度条(当前/总数、百分比),基于 [cli-progress](https://www.npmjs.com/package/cli-progress)。
|
|
12
14
|
- **CDN 地址**:可选配置 `OBS_CDN_BASE_URL`,上传完成后控制台打印每个文件的访问 URL。
|
|
13
|
-
- **结果上报抽象**:`Reporter` 接口抽象上传进度与结果,默认控制台输出,可扩展为企业微信、钉钉等。
|
|
14
15
|
- **可切换 Provider**:通过环境变量 `OSS_PROVIDER` 与单一配置文件 `ProviderConfigResolverImpl` 即可切换云厂商,无需改业务代码。
|
|
15
16
|
|
|
16
17
|
---
|
|
@@ -49,6 +50,7 @@ pnpm run build
|
|
|
49
50
|
| `OBS_REGION` | 否 | 区域,如 `cn-north-4` |
|
|
50
51
|
| `OBS_BASE_PREFIX` | 否 | 所有对象 key 的基础前缀(仅在配置中,不通过 CLI 暴露),如 `company/project-a` |
|
|
51
52
|
| `OBS_CDN_BASE_URL` | 否 | CDN 加速根地址,用于上传完成后拼出访问 URL,如 `https://cdn.example.com` |
|
|
53
|
+
| `FILE_RE_WHITE_LIST` | 否 | JSON 数组字符串,key 包含其中任一项则强制上传不跳过,如 `["index.html","sw.js"]` |
|
|
52
54
|
|
|
53
55
|
### 多云与切换
|
|
54
56
|
|
|
@@ -67,6 +69,9 @@ OBS_REGION=cn-north-4
|
|
|
67
69
|
OBS_BASE_PREFIX=company/project-a
|
|
68
70
|
OBS_CDN_BASE_URL=https://cdn.example.com
|
|
69
71
|
|
|
72
|
+
# 可选:强制覆盖的 key 片段(JSON 数组)
|
|
73
|
+
# FILE_RE_WHITE_LIST=["index.html","sw.js"]
|
|
74
|
+
|
|
70
75
|
# 可选:切换存储厂商
|
|
71
76
|
# OSS_PROVIDER=huawei
|
|
72
77
|
```
|
|
@@ -77,19 +82,19 @@ OBS_CDN_BASE_URL=https://cdn.example.com
|
|
|
77
82
|
|
|
78
83
|
### 一、命令行(CLI)
|
|
79
84
|
|
|
80
|
-
安装后可通过 `oss-
|
|
85
|
+
安装后可通过 `oss-uploader` 命令上传本地目录(若未全局安装,可使用 `npx oss-uploader` 或 `pnpm exec oss-uploader`)。
|
|
81
86
|
|
|
82
87
|
```bash
|
|
83
|
-
# 基本用法:上传 ./dist 到指定 bucket
|
|
84
|
-
oss-
|
|
88
|
+
# 基本用法:上传 ./dist 到指定 bucket
|
|
89
|
+
oss-uploader ./dist -b my-bucket --path-prefix app/v1/
|
|
85
90
|
|
|
86
91
|
# 带 include/exclude
|
|
87
|
-
oss-
|
|
92
|
+
oss-uploader ./dist -b my-bucket --path-prefix app/v1/ \
|
|
88
93
|
--include "**/*.js" --include "**/*.css" \
|
|
89
94
|
--exclude "**/*.map"
|
|
90
95
|
|
|
91
96
|
# 指定环境标识(会参与 key 拼接)
|
|
92
|
-
npx oss-
|
|
97
|
+
npx oss-uploader ./dist -e dev --path-prefix web/app
|
|
93
98
|
```
|
|
94
99
|
|
|
95
100
|
**CLI 参数说明**
|
|
@@ -97,43 +102,36 @@ npx oss-upload ./dist/ddd -e dev --path-prefix web/app
|
|
|
97
102
|
| 参数 | 简写 | 必填 | 说明 |
|
|
98
103
|
|------|------|------|------|
|
|
99
104
|
| `localDir` | - | 是 | 本地目录路径 |
|
|
100
|
-
| `--bucket` | `-b` |
|
|
101
|
-
| `--env` | `-e` | 否 | 环境标识,参与 key
|
|
105
|
+
| `--bucket` | `-b` | 否 | Bucket 名称,默认 `hr-uat` |
|
|
106
|
+
| `--env` | `-e` | 否 | 环境标识,参与 key 拼接,默认 `dev` |
|
|
102
107
|
| `--path-prefix` | - | 否 | 业务路径前缀,拼在 base/env 之后 |
|
|
103
108
|
| `--include` | - | 否 | 包含的文件模式,可多次 |
|
|
104
109
|
| `--exclude` | - | 否 | 排除的文件模式,可多次 |
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
上传过程中会显示进度条(当前/总数、百分比),结束后输出:成功/失败/跳过的 key 列表;若配置了 CDN 会打印访问地址。
|
|
107
112
|
|
|
108
113
|
---
|
|
109
114
|
|
|
110
115
|
### 二、作为库使用
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
包入口仅暴露一个上传方法 `upload` 及类型 `UploadOptions`、`UploadResult`,内部配置与 Reporter 固定(从环境变量读取,进度输出到控制台)。
|
|
113
118
|
|
|
114
119
|
```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();
|
|
120
|
+
import { upload, type UploadOptions, type UploadResult } from '@dd-code/oss-uploader';
|
|
123
121
|
|
|
124
|
-
const
|
|
125
|
-
bucket: 'my-bucket',
|
|
122
|
+
const result: UploadResult = await upload({
|
|
126
123
|
localDir: './dist',
|
|
124
|
+
bucket: 'my-bucket',
|
|
125
|
+
env: 'prod',
|
|
127
126
|
pathPrefix: 'app/v1',
|
|
128
127
|
include: ['**/*.js', '**/*.css'],
|
|
129
128
|
exclude: ['**/*.map'],
|
|
130
|
-
reporter,
|
|
131
129
|
});
|
|
132
130
|
|
|
133
|
-
console.log(
|
|
131
|
+
console.log(`总数: ${result.total}, 成功: ${result.success.length}, 失败: ${result.failed.length}, 跳过: ${result.skipped.length}`);
|
|
134
132
|
```
|
|
135
133
|
|
|
136
|
-
|
|
134
|
+
需要自定义 Reporter(如对接企业微信、钉钉)时,可直接使用中间件层 `uploadDirectory` 并传入 `reporter`(见源码 `middleware/OssService.ts`)。
|
|
137
135
|
|
|
138
136
|
---
|
|
139
137
|
|
|
@@ -160,58 +158,57 @@ basePrefix / env / pathPrefix / 相对路径
|
|
|
160
158
|
|
|
161
159
|
```
|
|
162
160
|
┌─────────────────────────────────────────────────────────┐
|
|
163
|
-
│ CLI /
|
|
164
|
-
│
|
|
161
|
+
│ CLI / 业务代码 │
|
|
162
|
+
│ CLI 调用 uploadDirectory,库调用 upload(options) │
|
|
165
163
|
└───────────────────────────┬─────────────────────────────┘
|
|
166
164
|
│
|
|
167
165
|
┌───────────────────────────▼─────────────────────────────┐
|
|
168
|
-
│ 中间件层:OssService
|
|
169
|
-
│
|
|
170
|
-
│ 创建 StorageClient,组装 DirectoryUploadFlow 并执行 │
|
|
166
|
+
│ 中间件层:OssService │
|
|
167
|
+
│ 解析配置、创建 StorageClient、组装 DirectoryUploadFlow │
|
|
171
168
|
└───────────────────────────┬─────────────────────────────┘
|
|
172
169
|
│
|
|
173
170
|
┌───────────────────────────▼─────────────────────────────┐
|
|
174
|
-
│ 流程层:
|
|
175
|
-
│
|
|
176
|
-
│
|
|
171
|
+
│ 流程层:UploadService / DirectoryUploadFlow │
|
|
172
|
+
│ 收集文件 → 过滤 → listObjectKeys 拉取已有 key → │
|
|
173
|
+
│ 逐文件 Set 判断 / head 回退 → putObject → Reporter │
|
|
177
174
|
└───────────────────────────┬─────────────────────────────┘
|
|
178
175
|
│
|
|
179
176
|
┌───────────────────────────▼─────────────────────────────┐
|
|
180
|
-
│ Provider 层:StorageClient 实现(如 HuaweiObsClient)
|
|
181
|
-
│
|
|
177
|
+
│ Provider 层:StorageClient 实现(如 HuaweiObsClient) │
|
|
178
|
+
│ listObjectKeys、headObject、putObject 对应 OBS API │
|
|
182
179
|
└─────────────────────────────────────────────────────────┘
|
|
183
180
|
```
|
|
184
181
|
|
|
185
182
|
### 设计模式简述
|
|
186
183
|
|
|
187
|
-
- **Facade
|
|
188
|
-
- **Template Method
|
|
189
|
-
- **Strategy / Adapter
|
|
190
|
-
- **简单工厂**:`StorageFactory.createStorageClient(config)` 根据 `config.type` 返回对应 Provider 实例。
|
|
184
|
+
- **Facade**:对外只暴露 `upload(options)`,内部固定配置与 Reporter。
|
|
185
|
+
- **Template Method**:`UploadService` 定义「收集 → 过滤 → 上传 → 上报」骨架,`DirectoryUploadFlow` 继承并执行。
|
|
186
|
+
- **Strategy / Adapter**:各云厂商实现 `StorageClient`(含可选 `listObjectKeys`),由 `StorageFactory` 按配置创建。
|
|
191
187
|
|
|
192
188
|
### 目录结构(源码)
|
|
193
189
|
|
|
194
190
|
```
|
|
195
191
|
src/
|
|
196
|
-
├── index.ts #
|
|
197
|
-
├── cli.ts # CLI
|
|
192
|
+
├── index.ts # 包入口,仅导出 upload / UploadOptions / UploadResult
|
|
193
|
+
├── cli.ts # CLI 入口(oss-uploader)
|
|
198
194
|
├── config/
|
|
199
|
-
│ ├── types.ts # 配置与 Provider
|
|
200
|
-
│ ├── EnvConfigResolverImpl.ts
|
|
201
|
-
│
|
|
195
|
+
│ ├── types.ts # 配置与 Provider 类型
|
|
196
|
+
│ ├── EnvConfigResolverImpl.ts
|
|
197
|
+
│ ├── ProviderConfigResolverImpl.ts
|
|
198
|
+
│ └── loadObsCredentialsFromUrl.ts
|
|
202
199
|
├── core/
|
|
203
|
-
│ ├── StorageClient.ts #
|
|
204
|
-
│ ├── StorageFactory.ts
|
|
205
|
-
│ ├──
|
|
206
|
-
│ ├── DirectoryUploadFlow.ts
|
|
207
|
-
│ ├── reporter.ts # Reporter
|
|
208
|
-
│ ├── filters.ts
|
|
209
|
-
│ └── urlHelper.ts
|
|
200
|
+
│ ├── StorageClient.ts # 存储客户端接口(含可选 listObjectKeys)
|
|
201
|
+
│ ├── StorageFactory.ts
|
|
202
|
+
│ ├── UploadService.ts # 上传流程基类(收集 → list/head 判断 → put)
|
|
203
|
+
│ ├── DirectoryUploadFlow.ts
|
|
204
|
+
│ ├── reporter.ts # Reporter 与 ConsoleReporter(含进度条)
|
|
205
|
+
│ ├── filters.ts
|
|
206
|
+
│ └── urlHelper.ts
|
|
210
207
|
├── middleware/
|
|
211
|
-
│ └── OssService.ts #
|
|
208
|
+
│ └── OssService.ts # 统一 API,解析配置并调用 Flow
|
|
212
209
|
└── providers/
|
|
213
210
|
└── huawei/
|
|
214
|
-
└── HuaweiObsClient.ts #
|
|
211
|
+
└── HuaweiObsClient.ts # OBS 的 listObjectKeys / headObject / putObject
|
|
215
212
|
```
|
|
216
213
|
|
|
217
214
|
---
|
|
@@ -223,27 +220,28 @@ src/
|
|
|
223
220
|
1. 在 `src/config/types.ts` 中扩展 `ProviderType` 与对应配置类型(如 `AliyunOssConfig`)、在 `ProviderConfig` 中增加分支。
|
|
224
221
|
2. 在 `src/config/ProviderConfigResolverImpl.ts` 的 `resolve()` 中增加 `case 'aliyun'`,从环境变量读取阿里云配置,返回 `{ providerConfig, basePrefix, cdnBaseUrl }`。
|
|
225
222
|
3. 在 `src/core/StorageFactory.ts` 中增加 `case 'aliyun'`,创建阿里云 Provider 实例。
|
|
226
|
-
4. 在 `src/providers/`
|
|
223
|
+
4. 在 `src/providers/` 下新增对应客户端,实现 `StorageClient` 的 `headObject`、`putObject`,可选实现 `listObjectKeys` 以启用「先 list 再判断」的加速逻辑。
|
|
227
224
|
|
|
228
225
|
业务侧只需设置 `OSS_PROVIDER=aliyun` 及对应环境变量,无需改 OssService / CLI。
|
|
229
226
|
|
|
230
227
|
### 自定义 Reporter
|
|
231
228
|
|
|
232
|
-
|
|
229
|
+
实现接口(含可选 `onProgress`):
|
|
233
230
|
|
|
234
231
|
```ts
|
|
235
232
|
interface Reporter {
|
|
236
|
-
onStart?(context: { env?, bucket, basePrefix, pathPrefix, localDir }): void | Promise<void>;
|
|
233
|
+
onStart?(context: { env?, bucket, basePrefix, pathPrefix, localDir, cdnBaseUrl? }): void | Promise<void>;
|
|
234
|
+
onProgress?(current: number, total: number): void | Promise<void>;
|
|
237
235
|
onFileResult?(result: UploadFileResult): void | Promise<void>;
|
|
238
236
|
onComplete?(summary: UploadSummary): void | Promise<void>;
|
|
239
237
|
}
|
|
240
238
|
```
|
|
241
239
|
|
|
242
|
-
|
|
240
|
+
需直接使用中间件层 `uploadDirectory` 并传入 `reporter`(包入口 `upload()` 不暴露 reporter 参数),详见 `middleware/OssService.ts`。
|
|
243
241
|
|
|
244
242
|
### 工具方法
|
|
245
243
|
|
|
246
|
-
-
|
|
244
|
+
- 包入口仅导出 `upload`、`UploadOptions`、`UploadResult`。`buildCdnAccessUrl` 等工具在内部使用,若需在业务中拼 CDN URL 可参考 `core/urlHelper.ts`。
|
|
247
245
|
|
|
248
246
|
---
|
|
249
247
|
|
|
@@ -257,29 +255,10 @@ pnpm install
|
|
|
257
255
|
pnpm run build
|
|
258
256
|
|
|
259
257
|
# 监听模式
|
|
260
|
-
pnpm run
|
|
258
|
+
pnpm run dev
|
|
261
259
|
```
|
|
262
260
|
|
|
263
|
-
发布前请将 `package.json` 中的 `name` 改为实际 scope
|
|
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` 中设置。
|
|
261
|
+
发布前请将 `package.json` 中的 `name` 改为实际 scope 与包名(当前为 `@dd-code/oss-uploader`),按需配置 `publishConfig` 与 registry。
|
|
280
262
|
|
|
281
263
|
---
|
|
282
264
|
|
|
283
|
-
## 许可证
|
|
284
|
-
|
|
285
|
-
MIT
|
package/dist/cli.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var e=require("cli-progress"),t=require("fs/promises"),s=require("path"),r=require("micromatch"),o=require("js-yaml");(async()=>{const[{Command:e},{uploadDirectory:t}]=await Promise.all([import("commander"),Promise.resolve().then(function(){return w})]),s=new e;s.name("oss-upload").description("上传本地目录到 OSS(当前为华为 OBS)").argument("<localDir>","本地目录").requiredOption("-b, --bucket <bucket>","Bucket 名称","hr-uat").option("-e, --env <env>","环境标识,如 dev/test/prod","dev").option("--path-prefix <pathPrefix>","业务路径前缀,将拼在 base/env 后面").option("--include <pattern...>","包含的文件模式(可多次)").option("--exclude <pattern...>","排除的文件模式(可多次)").action(async(e,s)=>{const{loadObsCredentialsFromUrlIfNeeded:r}=await Promise.resolve().then(function(){return y});await r();try{(await t({env:s.env,bucket:s.bucket,localDir:e,pathPrefix:s.pathPrefix,include:s.include,exclude:s.exclude})).failed.length>0&&(process.exitCode=1)}catch(e){console.error("上传过程中发生错误:",e?.message||e),process.exitCode=1}}),s.parse(process.argv)})();class i{constructor(){this.progressBar=null}onStart(e){}onProgress(t,s){null===this.progressBar&&(this.progressBar=new e.SingleBar({format:" 上传进度 |{bar}| {percentage}% | {value}/{total} 文件",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0},e.Presets.shades_classic),this.progressBar.start(s,0)),this.progressBar.update(t),t>=s&&(this.progressBar.stop(),this.progressBar=null)}onFileResult(e){}onComplete(e){console.log(`上传完成: 总数: ${e.total}`),console.log(`\n 跳过: ${e.skipped.length}`),console.log(`\n 成功:\n ${e.success.join("\n")}`),console.log(`\n\n\n 失败:\n ${e.failed.join("\n")}`)}}const a=require("esdk-obs-nodejs");class n{constructor(e){this.config=e,this.client=new a({access_key_id:e.accessKey,secret_access_key:e.secretKey,server:e.endpoint}),this.whiteList=JSON.parse('["/index.html"]')}async listObjectKeys(e,t){const s=[];let r;do{const o={Bucket:e,MaxKeys:1e3,Prefix:t||void 0};r&&(o.Marker=r);const i=await this.client.listObjects(o);if(i.CommonMsg.Status>300)throw new Error(`OBS listObjects 失败: ${i.CommonMsg.Status} ${i.CommonMsg.Code} ${i.CommonMsg.Message}`);const a=i.InterfaceResult?.Contents??[];for(const e of a)e.Key&&s.push(e.Key);r="true"===i.InterfaceResult?.IsTruncated?i.InterfaceResult?.NextMarker:void 0}while(r);return s}async headObject(e,t){try{if(this.whiteList.some(e=>t.includes(e)))return{exists:!1};const s=await this.client.getObjectMetadata({Bucket:e,Key:t});if(s.CommonMsg.Status<=300)return{exists:!0};if(404===s.CommonMsg.Status||"NoSuchKey"===s.CommonMsg.Code||"NotFound"===s.CommonMsg.Code)return{exists:!1};throw new Error(`OBS getObjectMetadata 失败: ${s.CommonMsg.Status} ${s.CommonMsg.Code} ${s.CommonMsg.Message}`)}catch(e){const t=e&&"string"==typeof e.message?e.message:String(e),s=e?.Code??e?.code;if("NoSuchKey"===s||"NotFound"===s||t.includes("404")||t.includes("NoSuchKey"))return{exists:!1};throw e}}async putObject(e){const{bucket:t,key:s,contentType:r}=e,o={Bucket:t,Key:s,ContentType:r};if(e.sourceFile)o.SourceFile=e.sourceFile;else{if(!e.body)throw new Error("putObject 需要 body 或 sourceFile");o.Body=e.body}const i=await this.client.putObject(o);if(i.CommonMsg.Status>300){const{Status:e,Code:t,Message:s,RequestId:r}=i.CommonMsg,o=[e,t,s,r].filter(Boolean).join(" ");throw new Error(`OBS putObject 失败 (${o||"无详情"}). 请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com`)}}}class c{resolveHuaweiConfig(e){const t=process.env.OBS_ACCESS_KEY,s=process.env.OBS_SECRET_KEY;if(!t||!s)throw new Error("缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY");return{endpoint:"https://obs.cn-east-3.myhuaweicloud.com",accessKey:t,secretKey:s,region:"cn-east-3",basePrefix:"jhr-static",cdnBaseUrl:"https://hruat-cos.jtexpress.com.cn"}}}class l{constructor(){this.huaweiResolver=new c}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}}function u(e,t){const s=e.replace(/\/+$/,""),r=t.replace(/^\/+/,"");return r?`${s}/${r}`:s}const p={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",ico:"image/x-icon",bmp:"image/bmp",tiff:"image/tiff",tif:"image/tiff",js:"application/javascript",mjs:"application/javascript",json:"application/json",css:"text/css",html:"text/html",htm:"text/html",txt:"text/plain",woff:"font/woff",woff2:"font/woff2",ttf:"font/ttf",eot:"application/vnd.ms-fontobject"};function h(e){const t=s.extname(e).slice(1).toLowerCase();return t?p[t]:void 0}class d{constructor(e,t,s,r,o){this.storageClient=e,this.basePrefix=t,this.cdnBaseUrl=s,this.reporter=r,this.fileProcessor=o}async uploadDirectory(e){const t=await this.collectFiles(e.localDir,e.include,e.exclude),s={total:t.length,success:[],failed:[],skipped:[]},r=this.buildKey(this.basePrefix,e.env,e.pathPrefix,"");let o;"function"==typeof this.storageClient.listObjectKeys&&(o=new Set(await this.storageClient.listObjectKeys(e.bucket,r))),this.reporter?.onStart&&await this.reporter.onStart({env:e.env,bucket:e.bucket,basePrefix:this.basePrefix,pathPrefix:e.pathPrefix,localDir:e.localDir,cdnBaseUrl:this.cdnBaseUrl});for(const r of t){const i=await this.uploadOneFile(r,e,o);s[i.status].push("success"===i.status?[this.cdnBaseUrl??"",i.key??""].join("/"):r.relativePath);const a=s.success.length+s.failed.length+s.skipped.length;this.reporter?.onProgress&&await this.reporter.onProgress(a,t.length)}return this.reporter?.onComplete&&await this.reporter.onComplete(s),s}async collectFiles(e,o,i){if((await t.stat(e)).isFile())return[{absolutePath:s.join(process.cwd(),e),relativePath:e}];return(await this.walkDir(e)).filter(e=>function(e,t,s){let o=!0;return t&&t.length>0&&(o=r.isMatch(e,t)),!(!o||s&&s.length>0&&r.isMatch(e,s))}(e.relativePath,o,i))}async uploadOneFile(e,s,r){const o=this.buildKey(this.basePrefix,s.env,s.pathPrefix,e.relativePath);try{const i=s.forceUploadPatterns?.some(e=>o.includes(e));if(void 0!==r){if(!i&&r.has(o))return await this.reportFile(e,s.bucket,o,"skipped"),{status:"skipped",key:o}}else{if((await this.storageClient.headObject(s.bucket,o)).exists)return await this.reportFile(e,s.bucket,o,"skipped"),{status:"skipped",key:o}}if(this.fileProcessor){const r=await t.readFile(e.absolutePath),{buffer:i,contentType:a}=await this.fileProcessor.process({localPath:e.absolutePath,relativePath:e.relativePath,buffer:r});await this.storageClient.putObject({bucket:s.bucket,key:o,body:i,contentType:a})}else await this.storageClient.putObject({bucket:s.bucket,key:o,sourceFile:e.absolutePath,contentType:h(e.relativePath)});return await this.reportFile(e,s.bucket,o,"success"),{status:"success",key:o}}catch(t){const r=t instanceof Error?t:new Error(String(t));return await this.reportFile(e,s.bucket,o,"failed",r),{status:"failed",key:o}}}buildKey(e,t,r,o){const i=(o??"").replace(/\\/g,"/");return s.join(e,
|
|
2
|
+
"use strict";var e=require("cli-progress"),t=require("fs/promises"),s=require("path"),r=require("micromatch"),o=require("js-yaml");(async()=>{const[{Command:e},{uploadDirectory:t}]=await Promise.all([import("commander"),Promise.resolve().then(function(){return w})]),s=new e;s.name("oss-upload").description("上传本地目录到 OSS(当前为华为 OBS)").argument("<localDir>","本地目录").requiredOption("-b, --bucket <bucket>","Bucket 名称","hr-uat").option("-e, --env <env>","环境标识,如 dev/test/prod","dev").option("--path-prefix <pathPrefix>","业务路径前缀,将拼在 base/env 后面").option("--include <pattern...>","包含的文件模式(可多次)").option("--exclude <pattern...>","排除的文件模式(可多次)").action(async(e,s)=>{const{loadObsCredentialsFromUrlIfNeeded:r}=await Promise.resolve().then(function(){return y});await r();try{(await t({env:s.env,bucket:s.bucket,localDir:e,pathPrefix:s.pathPrefix,include:s.include,exclude:s.exclude})).failed.length>0&&(process.exitCode=1)}catch(e){console.error("上传过程中发生错误:",e?.message||e),process.exitCode=1}}),s.parse(process.argv)})();class i{constructor(){this.progressBar=null}onStart(e){}onProgress(t,s){null===this.progressBar&&(this.progressBar=new e.SingleBar({format:" 上传进度 |{bar}| {percentage}% | {value}/{total} 文件",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0},e.Presets.shades_classic),this.progressBar.start(s,0)),this.progressBar.update(t),t>=s&&(this.progressBar.stop(),this.progressBar=null)}onFileResult(e){}onComplete(e){console.log(`上传完成: 总数: ${e.total}`),console.log(`\n 跳过: ${e.skipped.length}`),console.log(`\n 成功:\n ${e.success.join("\n")}`),console.log(`\n\n\n 失败:\n ${e.failed.join("\n")}`)}}const a=require("esdk-obs-nodejs");class n{constructor(e){this.config=e,this.client=new a({access_key_id:e.accessKey,secret_access_key:e.secretKey,server:e.endpoint}),this.whiteList=JSON.parse('["/index.html"]')}async listObjectKeys(e,t){const s=[];let r;do{const o={Bucket:e,MaxKeys:1e3,Prefix:t||void 0};r&&(o.Marker=r);const i=await this.client.listObjects(o);if(i.CommonMsg.Status>300)throw new Error(`OBS listObjects 失败: ${i.CommonMsg.Status} ${i.CommonMsg.Code} ${i.CommonMsg.Message}`);const a=i.InterfaceResult?.Contents??[];for(const e of a)e.Key&&s.push(e.Key);r="true"===i.InterfaceResult?.IsTruncated?i.InterfaceResult?.NextMarker:void 0}while(r);return s}async headObject(e,t){try{if(this.whiteList.some(e=>t.includes(e)))return{exists:!1};const s=await this.client.getObjectMetadata({Bucket:e,Key:t});if(s.CommonMsg.Status<=300)return{exists:!0};if(404===s.CommonMsg.Status||"NoSuchKey"===s.CommonMsg.Code||"NotFound"===s.CommonMsg.Code)return{exists:!1};throw new Error(`OBS getObjectMetadata 失败: ${s.CommonMsg.Status} ${s.CommonMsg.Code} ${s.CommonMsg.Message}`)}catch(e){const t=e&&"string"==typeof e.message?e.message:String(e),s=e?.Code??e?.code;if("NoSuchKey"===s||"NotFound"===s||t.includes("404")||t.includes("NoSuchKey"))return{exists:!1};throw e}}async putObject(e){const{bucket:t,key:s,contentType:r}=e,o={Bucket:t,Key:s,ContentType:r};if(e.sourceFile)o.SourceFile=e.sourceFile;else{if(!e.body)throw new Error("putObject 需要 body 或 sourceFile");o.Body=e.body}const i=await this.client.putObject(o);if(i.CommonMsg.Status>300){const{Status:e,Code:t,Message:s,RequestId:r}=i.CommonMsg,o=[e,t,s,r].filter(Boolean).join(" ");throw new Error(`OBS putObject 失败 (${o||"无详情"}). 请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com`)}}}class c{resolveHuaweiConfig(e){const t=process.env.OBS_ACCESS_KEY,s=process.env.OBS_SECRET_KEY;if(!t||!s)throw new Error("缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY");return{endpoint:"https://obs.cn-east-3.myhuaweicloud.com",accessKey:t,secretKey:s,region:"cn-east-3",basePrefix:"jhr-static",cdnBaseUrl:"https://hruat-cos.jtexpress.com.cn"}}}class l{constructor(){this.huaweiResolver=new c}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}}function u(e,t){const s=e.replace(/\/+$/,""),r=t.replace(/^\/+/,"");return r?`${s}/${r}`:s}const p={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",ico:"image/x-icon",bmp:"image/bmp",tiff:"image/tiff",tif:"image/tiff",js:"application/javascript",mjs:"application/javascript",json:"application/json",css:"text/css",html:"text/html",htm:"text/html",txt:"text/plain",woff:"font/woff",woff2:"font/woff2",ttf:"font/ttf",eot:"application/vnd.ms-fontobject"};function h(e){const t=s.extname(e).slice(1).toLowerCase();return t?p[t]:void 0}class d{constructor(e,t,s,r,o){this.storageClient=e,this.basePrefix=t,this.cdnBaseUrl=s,this.reporter=r,this.fileProcessor=o}async uploadDirectory(e){const t=await this.collectFiles(e.localDir,e.include,e.exclude),s={total:t.length,success:[],failed:[],skipped:[]},r=this.buildKey(this.basePrefix,e.env,e.pathPrefix,"");let o;"function"==typeof this.storageClient.listObjectKeys&&(o=new Set(await this.storageClient.listObjectKeys(e.bucket,r))),this.reporter?.onStart&&await this.reporter.onStart({env:e.env,bucket:e.bucket,basePrefix:this.basePrefix,pathPrefix:e.pathPrefix,localDir:e.localDir,cdnBaseUrl:this.cdnBaseUrl});for(const r of t){const i=await this.uploadOneFile(r,e,o);s[i.status].push("success"===i.status?[this.cdnBaseUrl??"",i.key??""].join("/"):r.relativePath);const a=s.success.length+s.failed.length+s.skipped.length;this.reporter?.onProgress&&await this.reporter.onProgress(a,t.length)}return this.reporter?.onComplete&&await this.reporter.onComplete(s),s}async collectFiles(e,o,i){if((await t.stat(e)).isFile())return[{absolutePath:s.join(process.cwd(),e),relativePath:e}];return(await this.walkDir(e)).filter(e=>function(e,t,s){let o=!0;return t&&t.length>0&&(o=r.isMatch(e,t)),!(!o||s&&s.length>0&&r.isMatch(e,s))}(e.relativePath,o,i))}async uploadOneFile(e,s,r){const o=this.buildKey(this.basePrefix,s.env,s.pathPrefix,e.relativePath);try{const i=s.forceUploadPatterns?.some(e=>o.includes(e));if(void 0!==r){if(!i&&r.has(o))return await this.reportFile(e,s.bucket,o,"skipped"),{status:"skipped",key:o}}else{if((await this.storageClient.headObject(s.bucket,o)).exists)return await this.reportFile(e,s.bucket,o,"skipped"),{status:"skipped",key:o}}if(this.fileProcessor){const r=await t.readFile(e.absolutePath),{buffer:i,contentType:a}=await this.fileProcessor.process({localPath:e.absolutePath,relativePath:e.relativePath,buffer:r});await this.storageClient.putObject({bucket:s.bucket,key:o,body:i,contentType:a})}else await this.storageClient.putObject({bucket:s.bucket,key:o,sourceFile:e.absolutePath,contentType:h(e.relativePath)});return await this.reportFile(e,s.bucket,o,"success"),{status:"success",key:o}}catch(t){const r=t instanceof Error?t:new Error(String(t));return await this.reportFile(e,s.bucket,o,"failed",r),{status:"failed",key:o}}}buildKey(e,t,r,o){const i=(o??"").replace(/\\/g,"/");return s.join(e,r??"",t??"dev",i).replace(/\\/g,"/")}async walkDir(e,r=""){const o=s.join(e,r),i=await t.readdir(o,{withFileTypes:!0}),a=[];for(const t of i){const o=s.join(r,t.name),i=s.join(e,o);t.isDirectory()?a.push(...await this.walkDir(e,o)):t.isFile()&&a.push({absolutePath:i,relativePath:o.replace(/\\/g,"/")})}return a}async reportFile(e,t,s,r,o){if(!this.reporter?.onFileResult)return;const i={localPath:e.absolutePath,relativePath:e.relativePath,bucket:t,key:s,status:r,...this.cdnBaseUrl&&{accessUrl:u(this.cdnBaseUrl,s)},...o&&{error:o}};await this.reporter.onFileResult(i)}}class f extends d{constructor(e,t,s,r,o){super(e,t,s,r,o)}}class g{constructor(){this.resolver=new l}async uploadDirectory(e){const t=this.resolver.resolve(),s=function(e){if("huawei"===e.type)return new n(e.options);throw new Error(`Unsupported provider type: ${e.type}`)}(t.providerConfig),r=e.reporter??new i;return new f(s,t.basePrefix,t.cdnBaseUrl,r,e.fileProcessor).uploadDirectory({bucket:e.bucket,localDir:e.localDir,env:e.env||"dev",pathPrefix:e.pathPrefix,include:e.include,exclude:e.exclude,forceUploadPatterns:m()})}}function m(){try{const e='["/index.html"]';return JSON.parse(e)}catch{return[]}}var w=Object.freeze({__proto__:null,OssService:g,uploadDirectory:async function(e){return(new g).uploadDirectory(e)}});const b="OBS_CREDENTIALS_URL";var y=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,s=process.env.OBS_SECRET_KEY;if(t&&s)return;const r=await fetch(e);if(!r.ok)throw new Error(`拉取 OBS 凭证失败 (${r.status}): ${e}`);const i=await r.text(),a=o.load(i);if(!a||"object"!=typeof a)throw new Error(`OBS 凭证 YAML 解析结果无效,请检查 ${b} 返回内容`);const n=a.OBS_ACCESS_KEY,c=a.OBS_SECRET_KEY;if("string"==typeof n&&(process.env.OBS_ACCESS_KEY=n),"string"==typeof c&&(process.env.OBS_SECRET_KEY=c),!process.env.OBS_ACCESS_KEY||!process.env.OBS_SECRET_KEY)throw new Error(`OBS 凭证 YAML 中未找到 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,请检查 ${b} 返回内容`)}});
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("cli-progress"),t=require("fs/promises"),s=require("path"),r=require("micromatch");class o{constructor(){this.progressBar=null}onStart(e){}onProgress(t,s){null===this.progressBar&&(this.progressBar=new e.SingleBar({format:" 上传进度 |{bar}| {percentage}% | {value}/{total} 文件",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0},e.Presets.shades_classic),this.progressBar.start(s,0)),this.progressBar.update(t),t>=s&&(this.progressBar.stop(),this.progressBar=null)}onFileResult(e){}onComplete(e){console.log(`上传完成: 总数: ${e.total}`),console.log(`\n 跳过: ${e.skipped.length}`),console.log(`\n 成功:\n ${e.success.join("\n")}`),console.log(`\n\n\n 失败:\n ${e.failed.join("\n")}`)}}const i=require("esdk-obs-nodejs");class a{constructor(e){this.config=e,this.client=new i({access_key_id:e.accessKey,secret_access_key:e.secretKey,server:e.endpoint}),this.whiteList=JSON.parse('["/index.html"]')}async listObjectKeys(e,t){const s=[];let r;do{const o={Bucket:e,MaxKeys:1e3,Prefix:t||void 0};r&&(o.Marker=r);const i=await this.client.listObjects(o);if(i.CommonMsg.Status>300)throw new Error(`OBS listObjects 失败: ${i.CommonMsg.Status} ${i.CommonMsg.Code} ${i.CommonMsg.Message}`);const a=i.InterfaceResult?.Contents??[];for(const e of a)e.Key&&s.push(e.Key);r="true"===i.InterfaceResult?.IsTruncated?i.InterfaceResult?.NextMarker:void 0}while(r);return s}async headObject(e,t){try{if(this.whiteList.some(e=>t.includes(e)))return{exists:!1};const s=await this.client.getObjectMetadata({Bucket:e,Key:t});if(s.CommonMsg.Status<=300)return{exists:!0};if(404===s.CommonMsg.Status||"NoSuchKey"===s.CommonMsg.Code||"NotFound"===s.CommonMsg.Code)return{exists:!1};throw new Error(`OBS getObjectMetadata 失败: ${s.CommonMsg.Status} ${s.CommonMsg.Code} ${s.CommonMsg.Message}`)}catch(e){const t=e&&"string"==typeof e.message?e.message:String(e),s=e?.Code??e?.code;if("NoSuchKey"===s||"NotFound"===s||t.includes("404")||t.includes("NoSuchKey"))return{exists:!1};throw e}}async putObject(e){const{bucket:t,key:s,contentType:r}=e,o={Bucket:t,Key:s,ContentType:r};if(e.sourceFile)o.SourceFile=e.sourceFile;else{if(!e.body)throw new Error("putObject 需要 body 或 sourceFile");o.Body=e.body}const i=await this.client.putObject(o);if(i.CommonMsg.Status>300){const{Status:e,Code:t,Message:s,RequestId:r}=i.CommonMsg,o=[e,t,s,r].filter(Boolean).join(" ");throw new Error(`OBS putObject 失败 (${o||"无详情"}). 请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com`)}}}class n{resolveHuaweiConfig(e){const t=process.env.OBS_ACCESS_KEY,s=process.env.OBS_SECRET_KEY;if(!t||!s)throw new Error("缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY");return{endpoint:"https://obs.cn-east-3.myhuaweicloud.com",accessKey:t,secretKey:s,region:"cn-east-3",basePrefix:"jhr-static",cdnBaseUrl:"https://hruat-cos.jtexpress.com.cn"}}}class c{constructor(){this.huaweiResolver=new n}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}}function l(e,t){const s=e.replace(/\/+$/,""),r=t.replace(/^\/+/,"");return r?`${s}/${r}`:s}const u={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",ico:"image/x-icon",bmp:"image/bmp",tiff:"image/tiff",tif:"image/tiff",js:"application/javascript",mjs:"application/javascript",json:"application/json",css:"text/css",html:"text/html",htm:"text/html",txt:"text/plain",woff:"font/woff",woff2:"font/woff2",ttf:"font/ttf",eot:"application/vnd.ms-fontobject"};function h(e){const t=s.extname(e).slice(1).toLowerCase();return t?u[t]:void 0}class p{constructor(e,t,s,r,o){this.storageClient=e,this.basePrefix=t,this.cdnBaseUrl=s,this.reporter=r,this.fileProcessor=o}async uploadDirectory(e){const t=await this.collectFiles(e.localDir,e.include,e.exclude),s={total:t.length,success:[],failed:[],skipped:[]},r=this.buildKey(this.basePrefix,e.env,e.pathPrefix,"");let o;"function"==typeof this.storageClient.listObjectKeys&&(o=new Set(await this.storageClient.listObjectKeys(e.bucket,r))),this.reporter?.onStart&&await this.reporter.onStart({env:e.env,bucket:e.bucket,basePrefix:this.basePrefix,pathPrefix:e.pathPrefix,localDir:e.localDir,cdnBaseUrl:this.cdnBaseUrl});for(const r of t){const i=await this.uploadOneFile(r,e,o);s[i.status].push("success"===i.status?[this.cdnBaseUrl??"",i.key??""].join("/"):r.relativePath);const a=s.success.length+s.failed.length+s.skipped.length;this.reporter?.onProgress&&await this.reporter.onProgress(a,t.length)}return this.reporter?.onComplete&&await this.reporter.onComplete(s),s}async collectFiles(e,o,i){if((await t.stat(e)).isFile())return[{absolutePath:s.join(process.cwd(),e),relativePath:e}];return(await this.walkDir(e)).filter(e=>function(e,t,s){let o=!0;return t&&t.length>0&&(o=r.isMatch(e,t)),!(!o||s&&s.length>0&&r.isMatch(e,s))}(e.relativePath,o,i))}async uploadOneFile(e,s,r){const o=this.buildKey(this.basePrefix,s.env,s.pathPrefix,e.relativePath);try{const i=s.forceUploadPatterns?.some(e=>o.includes(e));if(void 0!==r){if(!i&&r.has(o))return await this.reportFile(e,s.bucket,o,"skipped"),{status:"skipped",key:o}}else{if((await this.storageClient.headObject(s.bucket,o)).exists)return await this.reportFile(e,s.bucket,o,"skipped"),{status:"skipped",key:o}}if(this.fileProcessor){const r=await t.readFile(e.absolutePath),{buffer:i,contentType:a}=await this.fileProcessor.process({localPath:e.absolutePath,relativePath:e.relativePath,buffer:r});await this.storageClient.putObject({bucket:s.bucket,key:o,body:i,contentType:a})}else await this.storageClient.putObject({bucket:s.bucket,key:o,sourceFile:e.absolutePath,contentType:h(e.relativePath)});return await this.reportFile(e,s.bucket,o,"success"),{status:"success",key:o}}catch(t){const r=t instanceof Error?t:new Error(String(t));return await this.reportFile(e,s.bucket,o,"failed",r),{status:"failed",key:o}}}buildKey(e,t,r,o){const i=(o??"").replace(/\\/g,"/");return s.join(e,
|
|
1
|
+
"use strict";var e=require("cli-progress"),t=require("fs/promises"),s=require("path"),r=require("micromatch");class o{constructor(){this.progressBar=null}onStart(e){}onProgress(t,s){null===this.progressBar&&(this.progressBar=new e.SingleBar({format:" 上传进度 |{bar}| {percentage}% | {value}/{total} 文件",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0},e.Presets.shades_classic),this.progressBar.start(s,0)),this.progressBar.update(t),t>=s&&(this.progressBar.stop(),this.progressBar=null)}onFileResult(e){}onComplete(e){console.log(`上传完成: 总数: ${e.total}`),console.log(`\n 跳过: ${e.skipped.length}`),console.log(`\n 成功:\n ${e.success.join("\n")}`),console.log(`\n\n\n 失败:\n ${e.failed.join("\n")}`)}}const i=require("esdk-obs-nodejs");class a{constructor(e){this.config=e,this.client=new i({access_key_id:e.accessKey,secret_access_key:e.secretKey,server:e.endpoint}),this.whiteList=JSON.parse('["/index.html"]')}async listObjectKeys(e,t){const s=[];let r;do{const o={Bucket:e,MaxKeys:1e3,Prefix:t||void 0};r&&(o.Marker=r);const i=await this.client.listObjects(o);if(i.CommonMsg.Status>300)throw new Error(`OBS listObjects 失败: ${i.CommonMsg.Status} ${i.CommonMsg.Code} ${i.CommonMsg.Message}`);const a=i.InterfaceResult?.Contents??[];for(const e of a)e.Key&&s.push(e.Key);r="true"===i.InterfaceResult?.IsTruncated?i.InterfaceResult?.NextMarker:void 0}while(r);return s}async headObject(e,t){try{if(this.whiteList.some(e=>t.includes(e)))return{exists:!1};const s=await this.client.getObjectMetadata({Bucket:e,Key:t});if(s.CommonMsg.Status<=300)return{exists:!0};if(404===s.CommonMsg.Status||"NoSuchKey"===s.CommonMsg.Code||"NotFound"===s.CommonMsg.Code)return{exists:!1};throw new Error(`OBS getObjectMetadata 失败: ${s.CommonMsg.Status} ${s.CommonMsg.Code} ${s.CommonMsg.Message}`)}catch(e){const t=e&&"string"==typeof e.message?e.message:String(e),s=e?.Code??e?.code;if("NoSuchKey"===s||"NotFound"===s||t.includes("404")||t.includes("NoSuchKey"))return{exists:!1};throw e}}async putObject(e){const{bucket:t,key:s,contentType:r}=e,o={Bucket:t,Key:s,ContentType:r};if(e.sourceFile)o.SourceFile=e.sourceFile;else{if(!e.body)throw new Error("putObject 需要 body 或 sourceFile");o.Body=e.body}const i=await this.client.putObject(o);if(i.CommonMsg.Status>300){const{Status:e,Code:t,Message:s,RequestId:r}=i.CommonMsg,o=[e,t,s,r].filter(Boolean).join(" ");throw new Error(`OBS putObject 失败 (${o||"无详情"}). 请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com`)}}}class n{resolveHuaweiConfig(e){const t=process.env.OBS_ACCESS_KEY,s=process.env.OBS_SECRET_KEY;if(!t||!s)throw new Error("缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY");return{endpoint:"https://obs.cn-east-3.myhuaweicloud.com",accessKey:t,secretKey:s,region:"cn-east-3",basePrefix:"jhr-static",cdnBaseUrl:"https://hruat-cos.jtexpress.com.cn"}}}class c{constructor(){this.huaweiResolver=new n}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}}function l(e,t){const s=e.replace(/\/+$/,""),r=t.replace(/^\/+/,"");return r?`${s}/${r}`:s}const u={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",ico:"image/x-icon",bmp:"image/bmp",tiff:"image/tiff",tif:"image/tiff",js:"application/javascript",mjs:"application/javascript",json:"application/json",css:"text/css",html:"text/html",htm:"text/html",txt:"text/plain",woff:"font/woff",woff2:"font/woff2",ttf:"font/ttf",eot:"application/vnd.ms-fontobject"};function h(e){const t=s.extname(e).slice(1).toLowerCase();return t?u[t]:void 0}class p{constructor(e,t,s,r,o){this.storageClient=e,this.basePrefix=t,this.cdnBaseUrl=s,this.reporter=r,this.fileProcessor=o}async uploadDirectory(e){const t=await this.collectFiles(e.localDir,e.include,e.exclude),s={total:t.length,success:[],failed:[],skipped:[]},r=this.buildKey(this.basePrefix,e.env,e.pathPrefix,"");let o;"function"==typeof this.storageClient.listObjectKeys&&(o=new Set(await this.storageClient.listObjectKeys(e.bucket,r))),this.reporter?.onStart&&await this.reporter.onStart({env:e.env,bucket:e.bucket,basePrefix:this.basePrefix,pathPrefix:e.pathPrefix,localDir:e.localDir,cdnBaseUrl:this.cdnBaseUrl});for(const r of t){const i=await this.uploadOneFile(r,e,o);s[i.status].push("success"===i.status?[this.cdnBaseUrl??"",i.key??""].join("/"):r.relativePath);const a=s.success.length+s.failed.length+s.skipped.length;this.reporter?.onProgress&&await this.reporter.onProgress(a,t.length)}return this.reporter?.onComplete&&await this.reporter.onComplete(s),s}async collectFiles(e,o,i){if((await t.stat(e)).isFile())return[{absolutePath:s.join(process.cwd(),e),relativePath:e}];return(await this.walkDir(e)).filter(e=>function(e,t,s){let o=!0;return t&&t.length>0&&(o=r.isMatch(e,t)),!(!o||s&&s.length>0&&r.isMatch(e,s))}(e.relativePath,o,i))}async uploadOneFile(e,s,r){const o=this.buildKey(this.basePrefix,s.env,s.pathPrefix,e.relativePath);try{const i=s.forceUploadPatterns?.some(e=>o.includes(e));if(void 0!==r){if(!i&&r.has(o))return await this.reportFile(e,s.bucket,o,"skipped"),{status:"skipped",key:o}}else{if((await this.storageClient.headObject(s.bucket,o)).exists)return await this.reportFile(e,s.bucket,o,"skipped"),{status:"skipped",key:o}}if(this.fileProcessor){const r=await t.readFile(e.absolutePath),{buffer:i,contentType:a}=await this.fileProcessor.process({localPath:e.absolutePath,relativePath:e.relativePath,buffer:r});await this.storageClient.putObject({bucket:s.bucket,key:o,body:i,contentType:a})}else await this.storageClient.putObject({bucket:s.bucket,key:o,sourceFile:e.absolutePath,contentType:h(e.relativePath)});return await this.reportFile(e,s.bucket,o,"success"),{status:"success",key:o}}catch(t){const r=t instanceof Error?t:new Error(String(t));return await this.reportFile(e,s.bucket,o,"failed",r),{status:"failed",key:o}}}buildKey(e,t,r,o){const i=(o??"").replace(/\\/g,"/");return s.join(e,r??"",t??"dev",i).replace(/\\/g,"/")}async walkDir(e,r=""){const o=s.join(e,r),i=await t.readdir(o,{withFileTypes:!0}),a=[];for(const t of i){const o=s.join(r,t.name),i=s.join(e,o);t.isDirectory()?a.push(...await this.walkDir(e,o)):t.isFile()&&a.push({absolutePath:i,relativePath:o.replace(/\\/g,"/")})}return a}async reportFile(e,t,s,r,o){if(!this.reporter?.onFileResult)return;const i={localPath:e.absolutePath,relativePath:e.relativePath,bucket:t,key:s,status:r,...this.cdnBaseUrl&&{accessUrl:l(this.cdnBaseUrl,s)},...o&&{error:o}};await this.reporter.onFileResult(i)}}class f extends p{constructor(e,t,s,r,o){super(e,t,s,r,o)}}class d{constructor(){this.resolver=new c}async uploadDirectory(e){const t=this.resolver.resolve(),s=function(e){if("huawei"===e.type)return new a(e.options);throw new Error(`Unsupported provider type: ${e.type}`)}(t.providerConfig),r=e.reporter??new o;return new f(s,t.basePrefix,t.cdnBaseUrl,r,e.fileProcessor).uploadDirectory({bucket:e.bucket,localDir:e.localDir,env:e.env||"dev",pathPrefix:e.pathPrefix,include:e.include,exclude:e.exclude,forceUploadPatterns:g()})}}function g(){try{const e='["/index.html"]';return JSON.parse(e)}catch{return[]}}exports.upload=async function(e){return async function(e){return(new d).uploadDirectory(e)}({...e,env:e.env||"dev",bucket:e.bucket||"hr-uat"})};
|
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import { EnvConfigResolver, HuaweiObsConfig } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 从系统环境变量解析华为 OBS 配置的实现。
|
|
5
|
-
* 只使用一套固定变量,不区分 dev/test/prod 等多环境前缀。
|
|
6
|
-
*/
|
|
7
|
-
export class EnvConfigResolverImpl implements EnvConfigResolver {
|
|
8
|
-
/**
|
|
9
|
-
* 解析华为 OBS 配置(endpoint、ak/sk、basePrefix 等)。
|
|
10
|
-
* 当前实现忽略 env 参数,统一使用以下环境变量:
|
|
11
|
-
* - OBS_ENDPOINT
|
|
12
|
-
* - OBS_ACCESS_KEY / OBS_SECRET_KEY(也可通过 OBS_CREDENTIALS_URL 拉取 YAML 获得)
|
|
13
|
-
* - OBS_REGION(可选)
|
|
14
|
-
* - OBS_BASE_PREFIX(可选,作为所有 key 的基础前缀)
|
|
15
|
-
* - OBS_CDN_BASE_URL(可选,CDN 加速根地址,上传完成后用于拼出访问 URL)
|
|
16
|
-
*/
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
18
|
-
resolveHuaweiConfig(env?: string): HuaweiObsConfig {
|
|
19
|
-
// 通过环境变量解构出 OBS 访问配置(不依赖 .env 文件,直接读取进程环境)
|
|
20
|
-
const endpoint = process.env.OBS_ENDPOINT;
|
|
21
|
-
const accessKey = process.env.OBS_ACCESS_KEY;
|
|
22
|
-
const secretKey = process.env.OBS_SECRET_KEY;
|
|
23
|
-
const region = process.env.OBS_REGION;
|
|
24
|
-
// basePrefix 作为所有对象 key 的基础前缀,用于隔离项目/租户等
|
|
25
|
-
const basePrefix = process.env.OBS_BASE_PREFIX;
|
|
26
|
-
// CDN 加速根地址,配置后用于拼出上传完成后的访问 URL
|
|
27
|
-
const cdnBaseUrl = process.env.OBS_CDN_BASE_URL;
|
|
28
|
-
|
|
29
|
-
if (!endpoint || !accessKey || !secretKey) {
|
|
30
|
-
// 缺少关键配置时立即抛错,避免在后续真正上传时才暴露连接失败的问题
|
|
31
|
-
throw new Error('缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
endpoint,
|
|
36
|
-
accessKey,
|
|
37
|
-
secretKey,
|
|
38
|
-
region,
|
|
39
|
-
basePrefix: basePrefix || '',
|
|
40
|
-
cdnBaseUrl,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
1
|
+
import { EnvConfigResolver, HuaweiObsConfig } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 从系统环境变量解析华为 OBS 配置的实现。
|
|
5
|
+
* 只使用一套固定变量,不区分 dev/test/prod 等多环境前缀。
|
|
6
|
+
*/
|
|
7
|
+
export class EnvConfigResolverImpl implements EnvConfigResolver {
|
|
8
|
+
/**
|
|
9
|
+
* 解析华为 OBS 配置(endpoint、ak/sk、basePrefix 等)。
|
|
10
|
+
* 当前实现忽略 env 参数,统一使用以下环境变量:
|
|
11
|
+
* - OBS_ENDPOINT
|
|
12
|
+
* - OBS_ACCESS_KEY / OBS_SECRET_KEY(也可通过 OBS_CREDENTIALS_URL 拉取 YAML 获得)
|
|
13
|
+
* - OBS_REGION(可选)
|
|
14
|
+
* - OBS_BASE_PREFIX(可选,作为所有 key 的基础前缀)
|
|
15
|
+
* - OBS_CDN_BASE_URL(可选,CDN 加速根地址,上传完成后用于拼出访问 URL)
|
|
16
|
+
*/
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
18
|
+
resolveHuaweiConfig(env?: string): HuaweiObsConfig {
|
|
19
|
+
// 通过环境变量解构出 OBS 访问配置(不依赖 .env 文件,直接读取进程环境)
|
|
20
|
+
const endpoint = process.env.OBS_ENDPOINT;
|
|
21
|
+
const accessKey = process.env.OBS_ACCESS_KEY;
|
|
22
|
+
const secretKey = process.env.OBS_SECRET_KEY;
|
|
23
|
+
const region = process.env.OBS_REGION;
|
|
24
|
+
// basePrefix 作为所有对象 key 的基础前缀,用于隔离项目/租户等
|
|
25
|
+
const basePrefix = process.env.OBS_BASE_PREFIX;
|
|
26
|
+
// CDN 加速根地址,配置后用于拼出上传完成后的访问 URL
|
|
27
|
+
const cdnBaseUrl = process.env.OBS_CDN_BASE_URL;
|
|
28
|
+
|
|
29
|
+
if (!endpoint || !accessKey || !secretKey) {
|
|
30
|
+
// 缺少关键配置时立即抛错,避免在后续真正上传时才暴露连接失败的问题
|
|
31
|
+
throw new Error('缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
endpoint,
|
|
36
|
+
accessKey,
|
|
37
|
+
secretKey,
|
|
38
|
+
region,
|
|
39
|
+
basePrefix: basePrefix || '',
|
|
40
|
+
cdnBaseUrl,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -195,7 +195,7 @@ export abstract class UploadService {
|
|
|
195
195
|
relativePath?: string,
|
|
196
196
|
): string {
|
|
197
197
|
const relative = (relativePath ?? '').replace(/\\/g, '/');
|
|
198
|
-
return path.join(basePrefix,
|
|
198
|
+
return path.join(basePrefix, pathPrefix ?? '', env ?? 'dev', relative).replace(/\\/g, '/');
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
protected async walkDir(rootDir: string, currentDir = ''): Promise<LocalFile[]> {
|