@dd-code/oss-uploader 0.1.5 → 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 CHANGED
@@ -7,10 +7,11 @@
7
7
  ## 功能特性
8
8
 
9
9
  - **目录上传**:按本地目录结构上传到指定 Bucket,远端 key 规则为 `basePrefix / env / pathPrefix / 相对路径`。
10
- - **同名跳过**:通过 OBS 元数据(getObjectMetadata)判断对象是否已存在,同路径已存在则不再上传。
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-upload` 命令上传本地目录(若未全局安装,可使用 `npx oss-upload` 或 `pnpm exec oss-upload`)。
85
+ 安装后可通过 `oss-uploader` 命令上传本地目录(若未全局安装,可使用 `npx oss-uploader` 或 `pnpm exec oss-uploader`)。
81
86
 
82
87
  ```bash
83
- # 基本用法:上传 ./dist 到指定 bucket,前缀为 app/v1/
84
- oss-upload ./dist -b my-bucket --path-prefix app/v1/
88
+ # 基本用法:上传 ./dist 到指定 bucket
89
+ oss-uploader ./dist -b my-bucket --path-prefix app/v1/
85
90
 
86
91
  # 带 include/exclude
87
- oss-upload ./dist -b my-bucket --path-prefix app/v1/ \
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-upload ./dist/ddd -e dev --path-prefix web/app
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` | | Bucket 名称 |
101
- | `--env` | `-e` | 否 | 环境标识,参与 key 拼接,如 dev/test/prod |
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
- 上传过程中会输出:开始信息、每个文件的成功/跳过/失败及(若配置了 CDN)访问地址、最终统计。
111
+ 上传过程中会显示进度条(当前/总数、百分比),结束后输出:成功/失败/跳过的 key 列表;若配置了 CDN 会打印访问地址。
107
112
 
108
113
  ---
109
114
 
110
115
  ### 二、作为库使用
111
116
 
112
- 在代码中引入中间件 `OssService` 与配置解析器 `ProviderConfigResolverImpl`,调用 `uploadDirectory` 即可。
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 summary = await service.uploadDirectory({
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(`成功: ${summary.success}, 失败: ${summary.failed}, 跳过: ${summary.skipped}`);
131
+ console.log(`总数: ${result.total}, 成功: ${result.success.length}, 失败: ${result.failed.length}, 跳过: ${result.skipped.length}`);
134
132
  ```
135
133
 
136
- **自定义 Reporter**(如对接企业微信、钉钉):实现 `Reporter` 接口的 `onStart`、`onFileResult`、`onComplete`,传入 `uploadDirectory` `reporter` 即可。
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
- 只依赖 OssService.uploadDirectory()
161
+ │ CLI / 业务代码
162
+ CLI 调用 uploadDirectory,库调用 upload(options)
165
163
  └───────────────────────────┬─────────────────────────────┘
166
164
 
167
165
  ┌───────────────────────────▼─────────────────────────────┐
168
- │ 中间件层:OssService(Facade)
169
- 统一 API,内部通过 ProviderConfigResolver 解析配置,
170
- │ 创建 StorageClient,组装 DirectoryUploadFlow 并执行 │
166
+ │ 中间件层:OssService
167
+ 解析配置、创建 StorageClient、组装 DirectoryUploadFlow
171
168
  └───────────────────────────┬─────────────────────────────┘
172
169
 
173
170
  ┌───────────────────────────▼─────────────────────────────┐
174
- │ 流程层:UploadFlowTemplate / DirectoryUploadFlow
175
- 模板方法:收集文件 → 过滤 → 上传(headObject 判断 putObject)│
176
- 调用 StorageClient + Reporter
171
+ │ 流程层:UploadService / DirectoryUploadFlow
172
+ 收集文件 → 过滤 → listObjectKeys 拉取已有 key
173
+ 逐文件 Set 判断 / head 回退 → putObject → Reporter
177
174
  └───────────────────────────┬─────────────────────────────┘
178
175
 
179
176
  ┌───────────────────────────▼─────────────────────────────┐
180
- │ Provider 层:StorageClient 实现(如 HuaweiObsClient)
181
- putObject/headObject 翻译为具体云厂商 API
177
+ │ Provider 层:StorageClient 实现(如 HuaweiObsClient)
178
+ listObjectKeys、headObject、putObject 对应 OBS API
182
179
  └─────────────────────────────────────────────────────────┘
183
180
  ```
184
181
 
185
182
  ### 设计模式简述
186
183
 
187
- - **Facade(外观)**:`OssService` 对主流程暴露单一 `uploadDirectory`,隐藏 Provider 选择、配置解析、流程细节。
188
- - **Template Method(模板方法)**:`UploadFlowTemplate` 定义“收集 → 过滤 → 上传 → 上报”的骨架,`DirectoryUploadFlow` 实现具体收集与上传逻辑。
189
- - **Strategy / Adapter**:各云厂商实现统一接口 `StorageClient`(如 `HuaweiObsClient`),由 `StorageFactory` 按配置创建。
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 # 包入口,显式导出 API 与类型
197
- ├── cli.ts # CLI 入口(动态 import 依赖)
192
+ ├── index.ts # 包入口,仅导出 upload / UploadOptions / UploadResult
193
+ ├── cli.ts # CLI 入口(oss-uploader)
198
194
  ├── config/
199
- │ ├── types.ts # 配置与 Provider 类型定义
200
- │ ├── EnvConfigResolverImpl.ts # 华为 OBS 环境变量解析
201
- └── ProviderConfigResolverImpl.ts # 统一 Provider 解析(切换云厂商只改此文件)
195
+ │ ├── types.ts # 配置与 Provider 类型
196
+ │ ├── EnvConfigResolverImpl.ts
197
+ ├── ProviderConfigResolverImpl.ts
198
+ │ └── loadObsCredentialsFromUrl.ts
202
199
  ├── 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 等
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 # 中间件统一 API
208
+ │ └── OssService.ts # 统一 API,解析配置并调用 Flow
212
209
  └── providers/
213
210
  └── huawei/
214
- └── HuaweiObsClient.ts # 华为 OBS 的 StorageClient 实现
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/` 下新增 `aliyun/AliyunOssClient.ts`,实现 `StorageClient` 的 `headObject`、`putObject`。
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
- 将实例传入 `uploadDirectory({ ..., reporter: new MyReporter() })` CLI 侧通过封装调用传入即可。
240
+ 需直接使用中间件层 `uploadDirectory` 并传入 `reporter`(包入口 `upload()` 不暴露 reporter 参数),详见 `middleware/OssService.ts`。
243
241
 
244
242
  ### 工具方法
245
243
 
246
- - **buildCdnAccessUrl(cdnBaseUrl, key)**:根据 CDN 根地址与对象 key 拼出完整访问 URL,已从包入口导出。
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 build:watch
258
+ pnpm run dev
261
259
  ```
262
260
 
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` 中设置。
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 n=require("esdk-obs-nodejs");class a{constructor(e){this.config=e,this.client=new n({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 n=i.InterfaceResult?.Contents??[];for(const e of n)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 n=s.success.length+s.failed.length+s.skipped.length;this.reporter?.onProgress&&await this.reporter.onProgress(n,t.length)}return this.reporter?.onComplete&&await this.reporter.onComplete(s),s}async collectFiles(e,t,s){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,t,s))}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:n}=await this.fileProcessor.process({localPath:e.absolutePath,relativePath:e.relativePath,buffer:r});await this.storageClient.putObject({bucket:s.bucket,key:o,body:i,contentType:n})}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,t??"dev",r??"",i).replace(/\\/g,"/")}async walkDir(e,r=""){const o=s.join(e,r),i=await t.readdir(o,{withFileTypes:!0}),n=[];for(const t of i){const o=s.join(r,t.name),i=s.join(e,o);t.isDirectory()?n.push(...await this.walkDir(e,o)):t.isFile()&&n.push({absolutePath:i,relativePath:o.replace(/\\/g,"/")})}return n}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 a(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(),n=o.load(i);if(!n||"object"!=typeof n)throw new Error(`OBS 凭证 YAML 解析结果无效,请检查 ${b} 返回内容`);const a=n.OBS_ACCESS_KEY,c=n.OBS_SECRET_KEY;if("string"==typeof a&&(process.env.OBS_ACCESS_KEY=a),"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} 返回内容`)}});
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} 返回内容`)}});
@@ -1 +1 @@
1
- {"version":3,"file":"UploadService.d.ts","sourceRoot":"","sources":["../../src/core/UploadService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAgCrD,gBAAgB;AAChB,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,qDAAqD;IACrD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,8BAAsB,aAAa;IAE/B,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,aAAa;IAC/C,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM;IACrC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM;IACtC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ;IACtC,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa;IALlD,SAAS,aACY,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,QAAQ,CAAC,EAAE,QAAQ,YAAA,EACnB,aAAa,CAAC,EAAE,aAAa,YAAA;IAG5C,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC;IAgD9E,wBAAwB;cACR,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAAE,EAClB,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,SAAS,EAAE,CAAC;IAKvB,sEAAsE;cACtD,aAAa,CAC3B,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,sBAAsB,EAC/B,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAC5B,OAAO,CAAC;QAAE,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAsDrE,SAAS,CAAC,QAAQ,CAChB,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM;cAKO,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,SAAK,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;cAoB/D,UAAU,CACxB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAClC,KAAK,CAAC,EAAE,KAAK,GACZ,OAAO,CAAC,IAAI,CAAC;CAajB"}
1
+ {"version":3,"file":"UploadService.d.ts","sourceRoot":"","sources":["../../src/core/UploadService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAgCrD,gBAAgB;AAChB,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,qDAAqD;IACrD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,8BAAsB,aAAa;IAE/B,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,aAAa;IAC/C,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM;IACrC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM;IACtC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ;IACtC,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa;IALlD,SAAS,aACY,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,QAAQ,CAAC,EAAE,QAAQ,YAAA,EACnB,aAAa,CAAC,EAAE,aAAa,YAAA;IAG5C,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC;IAgD9E,wBAAwB;cACR,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAAE,EAClB,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,SAAS,EAAE,CAAC;IAUvB,sEAAsE;cACtD,aAAa,CAC3B,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,sBAAsB,EAC/B,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAC5B,OAAO,CAAC;QAAE,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAsDrE,SAAS,CAAC,QAAQ,CAChB,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM;cAKO,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,SAAK,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;cAoB/D,UAAU,CACxB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAClC,KAAK,CAAC,EAAE,KAAK,GACZ,OAAO,CAAC,IAAI,CAAC;CAajB"}
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,t,s){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,t,s))}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,t??"dev",r??"",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"})};
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@dd-code/oss-uploader",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Upload local directories to OSS (currently Huawei OBS) with pluggable middleware and providers.",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",
@@ -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
+
@@ -120,6 +120,11 @@ export abstract class UploadService {
120
120
  include?: string[],
121
121
  exclude?: string[],
122
122
  ): Promise<LocalFile[]> {
123
+ // 判断是否已经是个文件了
124
+ const stat = await fs.stat(localDir);
125
+ if (stat.isFile()) {
126
+ return [{ absolutePath: path.join(process.cwd(), localDir), relativePath: localDir }];
127
+ }
123
128
  const files = await this.walkDir(localDir);
124
129
  return files.filter((f) => shouldInclude(f.relativePath, include, exclude));
125
130
  }
@@ -190,7 +195,7 @@ export abstract class UploadService {
190
195
  relativePath?: string,
191
196
  ): string {
192
197
  const relative = (relativePath ?? '').replace(/\\/g, '/');
193
- return path.join(basePrefix, env ?? 'dev', pathPrefix ?? '', relative).replace(/\\/g, '/');
198
+ return path.join(basePrefix, pathPrefix ?? '', env ?? 'dev', relative).replace(/\\/g, '/');
194
199
  }
195
200
 
196
201
  protected async walkDir(rootDir: string, currentDir = ''): Promise<LocalFile[]> {