@dd-code/oss-uploader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +19 -0
- package/README.md +285 -0
- package/dist/cli.cjs +2 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/config/EnvConfigResolverImpl.d.ts +18 -0
- package/dist/config/EnvConfigResolverImpl.d.ts.map +1 -0
- package/dist/config/ProviderConfigResolverImpl.d.ts +15 -0
- package/dist/config/ProviderConfigResolverImpl.d.ts.map +1 -0
- package/dist/config/loadObsCredentialsFromUrl.d.ts +6 -0
- package/dist/config/loadObsCredentialsFromUrl.d.ts.map +1 -0
- package/dist/config/types.d.ts +41 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/core/DirectoryUploadFlow.d.ts +17 -0
- package/dist/core/DirectoryUploadFlow.d.ts.map +1 -0
- package/dist/core/StorageClient.d.ts +22 -0
- package/dist/core/StorageClient.d.ts.map +1 -0
- package/dist/core/StorageFactory.d.ts +8 -0
- package/dist/core/StorageFactory.d.ts.map +1 -0
- package/dist/core/UploadFlowTemplate.d.ts +49 -0
- package/dist/core/UploadFlowTemplate.d.ts.map +1 -0
- package/dist/core/fileProcessor.d.ts +35 -0
- package/dist/core/fileProcessor.d.ts.map +1 -0
- package/dist/core/filters.d.ts +10 -0
- package/dist/core/filters.d.ts.map +1 -0
- package/dist/core/reporter.d.ts +50 -0
- package/dist/core/reporter.d.ts.map +1 -0
- package/dist/core/urlHelper.d.ts +8 -0
- package/dist/core/urlHelper.d.ts.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/middleware/OssService.d.ts +27 -0
- package/dist/middleware/OssService.d.ts.map +1 -0
- package/dist/providers/huawei/HuaweiObsClient.d.ts +18 -0
- package/dist/providers/huawei/HuaweiObsClient.d.ts.map +1 -0
- package/package.json +43 -0
- package/rollup.config.mjs +102 -0
- package/src/cli.ts +55 -0
- package/src/config/EnvConfigResolverImpl.ts +44 -0
- package/src/config/ProviderConfigResolverImpl.ts +34 -0
- package/src/config/loadObsCredentialsFromUrl.ts +46 -0
- package/src/config/types.ts +47 -0
- package/src/core/DirectoryUploadFlow.ts +96 -0
- package/src/core/StorageClient.ts +25 -0
- package/src/core/StorageFactory.ts +17 -0
- package/src/core/UploadFlowTemplate.ts +119 -0
- package/src/core/fileProcessor.ts +40 -0
- package/src/core/filters.ts +31 -0
- package/src/core/reporter.ts +88 -0
- package/src/core/urlHelper.ts +11 -0
- package/src/index.ts +63 -0
- package/src/middleware/OssService.ts +54 -0
- package/src/providers/huawei/HuaweiObsClient.ts +72 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UploadFlowTemplate.d.ts","sourceRoot":"","sources":["../../src/core/UploadFlowTemplate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,uBAAuB;AACvB,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,8BAAsB,kBAAkB;IAChB,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,aAAa;IAA/D,SAAS,aAAgC,OAAO,EAAE,aAAa;IAE/D,eAAe;IACT,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC;IAyBvC,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IACvD,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC;IAE1E,gCAAgC;IAChC,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE;IAKtD;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAUhD,8BAA8B;cACd,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,SAAK,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAsB/E,4BAA4B;cACZ,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAM1E"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上传前文件处理抽象。
|
|
3
|
+
* 可在 putObject 前对内容做转换,例如图片压缩、水印、格式转换等,便于后期扩展。
|
|
4
|
+
*/
|
|
5
|
+
/** 传入处理器的文件信息与内容 */
|
|
6
|
+
export interface ProcessFileInput {
|
|
7
|
+
/** 本地绝对路径 */
|
|
8
|
+
localPath: string;
|
|
9
|
+
/** 相对 localDir 的路径 */
|
|
10
|
+
relativePath: string;
|
|
11
|
+
/** 文件内容,处理器可修改后返回 */
|
|
12
|
+
buffer: Buffer;
|
|
13
|
+
}
|
|
14
|
+
/** 处理器返回结果,将用于 putObject */
|
|
15
|
+
export interface ProcessFileResult {
|
|
16
|
+
/** 处理后的内容(如压缩后的图片) */
|
|
17
|
+
buffer: Buffer;
|
|
18
|
+
/** 可选:覆盖上传时的 Content-Type(如压缩后统一为 image/jpeg) */
|
|
19
|
+
contentType?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 上传前文件处理器接口。
|
|
23
|
+
* 实现后可在上传前对文件做压缩、转换等,未配置则直接上传原内容。
|
|
24
|
+
*/
|
|
25
|
+
export interface FileProcessor {
|
|
26
|
+
process(input: ProcessFileInput): Promise<ProcessFileResult>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 默认实现:不做任何处理,直接返回原 buffer。
|
|
30
|
+
* 后续可替换为图片压缩等实现。
|
|
31
|
+
*/
|
|
32
|
+
export declare class NoOpFileProcessor implements FileProcessor {
|
|
33
|
+
process(input: ProcessFileInput): Promise<ProcessFileResult>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=fileProcessor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileProcessor.d.ts","sourceRoot":"","sources":["../../src/core/fileProcessor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,oBAAoB;AACpB,MAAM,WAAW,gBAAgB;IAC/B,aAAa;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4BAA4B;AAC5B,MAAM,WAAW,iBAAiB;IAChC,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC9D;AAED;;;GAGG;AACH,qBAAa,iBAAkB,YAAW,aAAa;IAC/C,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAGnE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 根据 include/exclude 规则判断相对路径是否应参与上传。
|
|
3
|
+
* 使用 micromatch 支持 glob(如 **\/*.js)。
|
|
4
|
+
* @param relativePath 相对路径
|
|
5
|
+
* @param include 包含模式,不传则默认全部包含
|
|
6
|
+
* @param exclude 排除模式
|
|
7
|
+
* @returns 是否应包含该文件
|
|
8
|
+
*/
|
|
9
|
+
export declare function shouldInclude(relativePath: string, include?: string[], exclude?: string[]): boolean;
|
|
10
|
+
//# sourceMappingURL=filters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filters.d.ts","sourceRoot":"","sources":["../../src/core/filters.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM,EAAE,EAClB,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,OAAO,CAeT"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上传结果上报抽象(Reporter)。
|
|
3
|
+
* 默认实现为控制台输出,可扩展为企业微信、钉钉等通知。
|
|
4
|
+
*/
|
|
5
|
+
export type UploadStatus = 'success' | 'failed' | 'skipped';
|
|
6
|
+
/** 单个文件的上传结果,供 Reporter 使用 */
|
|
7
|
+
export interface UploadFileResult {
|
|
8
|
+
localPath: string;
|
|
9
|
+
relativePath: string;
|
|
10
|
+
bucket: string;
|
|
11
|
+
key: string;
|
|
12
|
+
status: UploadStatus;
|
|
13
|
+
error?: Error;
|
|
14
|
+
/** 配置了 CDN 时的完整访问地址,由 buildCdnAccessUrl 生成 */
|
|
15
|
+
accessUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
/** 整次上传的汇总统计 */
|
|
18
|
+
export interface UploadSummary {
|
|
19
|
+
total: number;
|
|
20
|
+
success: number;
|
|
21
|
+
failed: number;
|
|
22
|
+
skipped: number;
|
|
23
|
+
}
|
|
24
|
+
/** Reporter 接口:上传开始、每个文件结果、全部完成时回调,便于接入不同通知方式 */
|
|
25
|
+
export interface Reporter {
|
|
26
|
+
onStart?(context: {
|
|
27
|
+
env?: string;
|
|
28
|
+
bucket: string;
|
|
29
|
+
basePrefix: string;
|
|
30
|
+
pathPrefix?: string;
|
|
31
|
+
localDir: string;
|
|
32
|
+
cdnBaseUrl?: string;
|
|
33
|
+
}): void | Promise<void>;
|
|
34
|
+
onFileResult?(result: UploadFileResult): void | Promise<void>;
|
|
35
|
+
onComplete?(summary: UploadSummary): void | Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/** 默认实现:将上传进度与结果输出到控制台 */
|
|
38
|
+
export declare class ConsoleReporter implements Reporter {
|
|
39
|
+
onStart(context: {
|
|
40
|
+
env?: string;
|
|
41
|
+
bucket: string;
|
|
42
|
+
basePrefix: string;
|
|
43
|
+
pathPrefix?: string;
|
|
44
|
+
localDir: string;
|
|
45
|
+
cdnBaseUrl?: string;
|
|
46
|
+
}): void;
|
|
47
|
+
onFileResult(result: UploadFileResult): void;
|
|
48
|
+
onComplete(summary: UploadSummary): void;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/core/reporter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5D,8BAA8B;AAC9B,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gBAAgB;AAChB,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,iDAAiD;AACjD,MAAM,WAAW,QAAQ;IACvB,OAAO,CAAC,CAAC,OAAO,EAAE;QAChB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzB,YAAY,CAAC,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9D,UAAU,CAAC,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED,0BAA0B;AAC1B,qBAAa,eAAgB,YAAW,QAAQ;IAC9C,OAAO,CAAC,OAAO,EAAE;QACf,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,IAAI;IAaR,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IAc5C,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;CAOzC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 根据 CDN 加速域名与对象 key 拼接完整的访问地址。
|
|
3
|
+
* @param cdnBaseUrl CDN 根地址(如 https://cdn.example.com),可带或不带末尾斜杠
|
|
4
|
+
* @param key 对象在桶内的 key(如 basePrefix/path/file.js)
|
|
5
|
+
* @returns 完整访问 URL,如 https://cdn.example.com/basePrefix/path/file.js
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildCdnAccessUrl(cdnBaseUrl: string, key: string): string;
|
|
8
|
+
//# sourceMappingURL=urlHelper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"urlHelper.d.ts","sourceRoot":"","sources":["../../src/core/urlHelper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIzE"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("fs/promises"),t=require("path"),s=require("micromatch");function o(e,t){const s=e.replace(/\/+$/,""),o=t.replace(/^\/+/,"");return o?`${s}/${o}`:s}class r{constructor(e){this.context=e}async execute(){const{reporter:e}=this.context;e?.onStart&&await e.onStart({env:this.context.env,bucket:this.context.bucket,basePrefix:this.context.basePrefix,pathPrefix:this.context.pathPrefix,localDir:this.context.localDir,cdnBaseUrl:this.context.cdnBaseUrl});const t=await this.collectFiles(),s=this.filterFiles(t),o=await this.uploadFiles(s);return e?.onComplete&&await e.onComplete(o),o}filterFiles(e){const{include:t,exclude:o}=this.context;return e.filter(e=>function(e,t,o){let r=!0;return t&&t.length>0&&(r=s.isMatch(e,t)),!(!r||o&&o.length>0&&s.isMatch(e,o))}(e.relativePath,t,o))}buildKey(e){const t=[];return this.context.basePrefix&&t.push(this.context.basePrefix),this.context.env&&t.push(this.context.env),this.context.pathPrefix&&t.push(this.context.pathPrefix),t.push(e.replace(/\\/g,"/")),t.join("/")}async walkDir(s,o=""){const r=t.join(s,o),c=await e.readdir(r,{withFileTypes:!0}),i=[];for(const e of c){const r=t.join(o,e.name),c=t.join(s,r);e.isDirectory()?i.push(...await this.walkDir(s,r)):e.isFile()&&i.push({absolutePath:c,relativePath:r.replace(/\\/g,"/")})}return i}async reportFileResult(e){const{reporter:t}=this.context;t?.onFileResult&&await t.onFileResult(e)}}class c extends r{constructor(e){super(e)}async collectFiles(){return this.walkDir(this.context.localDir)}async uploadFiles(t){const{bucket:s,storageClient:r,cdnBaseUrl:c}=this.context,i={total:t.length,success:0,failed:0,skipped:0};for(const a of t){const t=this.buildKey(a.relativePath);try{if((await r.headObject(s,t)).exists){i.skipped+=1,await this.reportFileResult({localPath:a.absolutePath,relativePath:a.relativePath,bucket:s,key:t,status:"skipped",...c&&{accessUrl:o(c,t)}});continue}const n=await e.readFile(a.absolutePath),{buffer:l,contentType:u}=this.context.fileProcessor?await this.context.fileProcessor.process({localPath:a.absolutePath,relativePath:a.relativePath,buffer:n}):{buffer:n,contentType:void 0};await r.putObject({bucket:s,key:t,body:l,contentType:u}),i.success+=1,await this.reportFileResult({localPath:a.absolutePath,relativePath:a.relativePath,bucket:s,key:t,status:"success",...c&&{accessUrl:o(c,t)}})}catch(e){i.failed+=1,await this.reportFileResult({localPath:a.absolutePath,relativePath:a.relativePath,bucket:s,key:t,status:"failed",error:e instanceof Error?e:new Error(String(e))})}}return i}}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})}async headObject(e,t){try{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,body:o,contentType:r}=e,c=await this.client.putObject({Bucket:t,Key:s,Body:o});if(c.CommonMsg.Status>300){const{Status:e,Code:t,Message:s,RequestId:o}=c.CommonMsg,r=[e,t,s,o].filter(Boolean).join(" ");throw new Error(`OBS putObject 失败 (${r||"无详情"}). 请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com`)}}}function n(e){if("huawei"===e.type)return new a(e.options);throw new Error(`Unsupported provider type: ${e.type}`)}class l{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"}}}exports.ConsoleReporter=class{onStart(e){const{env:t,bucket:s,basePrefix:o,pathPrefix:r,localDir:c,cdnBaseUrl:i}=e;i&&console.log(` CDN 根地址: ${i}`)}onFileResult(e){const{status:t,localPath:s,key:o,error:r,accessUrl:c}=e;"success"===t?(console.log(`[SUCCESS] ${s} -> ${o}`),c&&console.log(` 访问地址: ${c}`)):"skipped"===t?(console.log(`[SKIPPED] ${s} -> ${o} (云端同名已存在)`),c&&console.log(` 访问地址: ${c}`)):(console.error(`[FAILED] ${s} -> ${o}`),r&&console.error(` Error: ${r.message}`))}onComplete(e){console.log("上传完成:"),console.log(` 总数: ${e.total}`),console.log(` 成功: ${e.success}`),console.log(` 失败: ${e.failed}`),console.log(` 跳过: ${e.skipped}`)}},exports.DirectoryUploadFlow=c,exports.EnvConfigResolverImpl=l,exports.NoOpFileProcessor=class{async process(e){return{buffer:e.buffer}}},exports.OssService=class{constructor(e){this.providerConfigResolver=e}async uploadDirectory(e){const{env:t,bucket:s="hr-uat",localDir:o,pathPrefix:r,include:i,exclude:a,reporter:l,fileProcessor:u}=e,h=this.providerConfigResolver.resolve(),{providerConfig:p,basePrefix:d,cdnBaseUrl:f}=h,x=n(p);return new c({env:t,bucket:s,localDir:o,pathPrefix:r,include:i,exclude:a,basePrefix:d,storageClient:x,reporter:l,cdnBaseUrl:f,fileProcessor:u}).execute()}},exports.ProviderConfigResolverImpl=class{constructor(){this.huaweiResolver=new l}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}},exports.UploadFlowTemplate=r,exports.buildCdnAccessUrl=o,exports.createStorageClient=n;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/core/urlHelper.ts","../src/core/reporter.ts","../src/core/fileProcessor.ts","../src/core/filters.ts","../src/core/UploadFlowTemplate.ts","../src/core/DirectoryUploadFlow.ts","../src/providers/huawei/HuaweiObsClient.ts","../src/core/StorageFactory.ts","../src/middleware/OssService.ts","../src/config/EnvConfigResolverImpl.ts","../src/config/ProviderConfigResolverImpl.ts"],"sourcesContent":["/**\r\n * 根据 CDN 加速域名与对象 key 拼接完整的访问地址。\r\n * @param cdnBaseUrl CDN 根地址(如 https://cdn.example.com),可带或不带末尾斜杠\r\n * @param key 对象在桶内的 key(如 basePrefix/path/file.js)\r\n * @returns 完整访问 URL,如 https://cdn.example.com/basePrefix/path/file.js\r\n */\r\nexport function buildCdnAccessUrl(cdnBaseUrl: string, key: string): string {\r\n const base = cdnBaseUrl.replace(/\\/+$/, '');\r\n const pathPart = key.replace(/^\\/+/, '');\r\n return pathPart ? `${base}/${pathPart}` : base;\r\n}\r\n","/**\r\n * 上传结果上报抽象(Reporter)。\r\n * 默认实现为控制台输出,可扩展为企业微信、钉钉等通知。\r\n */\r\n\r\nexport type UploadStatus = 'success' | 'failed' | 'skipped';\r\n\r\n/** 单个文件的上传结果,供 Reporter 使用 */\r\nexport interface UploadFileResult {\r\n localPath: string;\r\n relativePath: string;\r\n bucket: string;\r\n key: string;\r\n status: UploadStatus;\r\n error?: Error;\r\n /** 配置了 CDN 时的完整访问地址,由 buildCdnAccessUrl 生成 */\r\n accessUrl?: string;\r\n}\r\n\r\n/** 整次上传的汇总统计 */\r\nexport interface UploadSummary {\r\n total: number;\r\n success: number;\r\n failed: number;\r\n skipped: number;\r\n}\r\n\r\n/** Reporter 接口:上传开始、每个文件结果、全部完成时回调,便于接入不同通知方式 */\r\nexport interface Reporter {\r\n onStart?(context: {\r\n env?: string;\r\n bucket: string;\r\n basePrefix: string;\r\n pathPrefix?: string;\r\n localDir: string;\r\n cdnBaseUrl?: string;\r\n }): void | Promise<void>;\r\n\r\n onFileResult?(result: UploadFileResult): void | Promise<void>;\r\n\r\n onComplete?(summary: UploadSummary): void | Promise<void>;\r\n}\r\n\r\n/** 默认实现:将上传进度与结果输出到控制台 */\r\nexport class ConsoleReporter implements Reporter {\r\n onStart(context: {\r\n env?: string;\r\n bucket: string;\r\n basePrefix: string;\r\n pathPrefix?: string;\r\n localDir: string;\r\n cdnBaseUrl?: string;\r\n }): void {\r\n const { env, bucket, basePrefix, pathPrefix, localDir, cdnBaseUrl } = context;\r\n // console.log('开始上传目录:');\r\n // console.log(` 环境: ${env ?? '未指定'}`);\r\n // console.log(` Bucket: ${bucket}`);\r\n // console.log(` Base 前缀: ${basePrefix || '(空)'}`);\r\n // console.log(` 业务前缀(pathPrefix): ${pathPrefix || '(空)'}`);\r\n // console.log(` 本地目录: ${localDir}`);\r\n if (cdnBaseUrl) {\r\n console.log(` CDN 根地址: ${cdnBaseUrl}`);\r\n }\r\n }\r\n\r\n onFileResult(result: UploadFileResult): void {\r\n const { status, localPath, key, error, accessUrl } = result;\r\n if (status === 'success') {\r\n console.log(`[SUCCESS] ${localPath} -> ${key}`);\r\n if (accessUrl) console.log(` 访问地址: ${accessUrl}`);\r\n } else if (status === 'skipped') {\r\n console.log(`[SKIPPED] ${localPath} -> ${key} (云端同名已存在)`);\r\n if (accessUrl) console.log(` 访问地址: ${accessUrl}`);\r\n } else {\r\n console.error(`[FAILED] ${localPath} -> ${key}`);\r\n if (error) console.error(` Error: ${error.message}`);\r\n }\r\n }\r\n\r\n onComplete(summary: UploadSummary): void {\r\n console.log('上传完成:');\r\n console.log(` 总数: ${summary.total}`);\r\n console.log(` 成功: ${summary.success}`);\r\n console.log(` 失败: ${summary.failed}`);\r\n console.log(` 跳过: ${summary.skipped}`);\r\n }\r\n}\r\n\r\n","/**\r\n * 上传前文件处理抽象。\r\n * 可在 putObject 前对内容做转换,例如图片压缩、水印、格式转换等,便于后期扩展。\r\n */\r\n\r\n/** 传入处理器的文件信息与内容 */\r\nexport interface ProcessFileInput {\r\n /** 本地绝对路径 */\r\n localPath: string;\r\n /** 相对 localDir 的路径 */\r\n relativePath: string;\r\n /** 文件内容,处理器可修改后返回 */\r\n buffer: Buffer;\r\n}\r\n\r\n/** 处理器返回结果,将用于 putObject */\r\nexport interface ProcessFileResult {\r\n /** 处理后的内容(如压缩后的图片) */\r\n buffer: Buffer;\r\n /** 可选:覆盖上传时的 Content-Type(如压缩后统一为 image/jpeg) */\r\n contentType?: string;\r\n}\r\n\r\n/**\r\n * 上传前文件处理器接口。\r\n * 实现后可在上传前对文件做压缩、转换等,未配置则直接上传原内容。\r\n */\r\nexport interface FileProcessor {\r\n process(input: ProcessFileInput): Promise<ProcessFileResult>;\r\n}\r\n\r\n/**\r\n * 默认实现:不做任何处理,直接返回原 buffer。\r\n * 后续可替换为图片压缩等实现。\r\n */\r\nexport class NoOpFileProcessor implements FileProcessor {\r\n async process(input: ProcessFileInput): Promise<ProcessFileResult> {\r\n return { buffer: input.buffer };\r\n }\r\n}\r\n","import micromatch from 'micromatch';\r\n\r\n/**\r\n * 根据 include/exclude 规则判断相对路径是否应参与上传。\r\n * 使用 micromatch 支持 glob(如 **\\/*.js)。\r\n * @param relativePath 相对路径\r\n * @param include 包含模式,不传则默认全部包含\r\n * @param exclude 排除模式\r\n * @returns 是否应包含该文件\r\n */\r\nexport function shouldInclude(\r\n relativePath: string,\r\n include?: string[],\r\n exclude?: string[],\r\n): boolean {\r\n let included = true;\r\n\r\n if (include && include.length > 0) {\r\n included = micromatch.isMatch(relativePath, include);\r\n }\r\n if (!included) return false;\r\n\r\n if (exclude && exclude.length > 0) {\r\n if (micromatch.isMatch(relativePath, exclude)) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n}\r\n\r\n","import fs from 'fs/promises';\r\nimport path from 'path';\r\nimport { Reporter, UploadSummary, UploadFileResult } from './reporter';\r\nimport { StorageClient } from './StorageClient';\r\nimport { shouldInclude } from './filters';\r\nimport type { FileProcessor } from './fileProcessor';\r\n\r\n/** 上传流程的上下文:环境、bucket、前缀、本地目录、存储客户端、Reporter、CDN 根地址、上传前处理器等 */\r\nexport interface UploadContext {\r\n env?: string;\r\n bucket: string;\r\n basePrefix: string;\r\n pathPrefix?: string;\r\n localDir: string;\r\n include?: string[];\r\n exclude?: string[];\r\n storageClient: StorageClient;\r\n reporter?: Reporter;\r\n /** CDN 加速根地址,配置后可为每个文件生成 accessUrl */\r\n cdnBaseUrl?: string;\r\n /** 上传前对文件内容的处理器(如图片压缩),未配置则直接上传原内容 */\r\n fileProcessor?: FileProcessor;\r\n}\r\n\r\n/** 本地文件信息:绝对路径与相对路径 */\r\nexport interface LocalFile {\r\n absolutePath: string;\r\n relativePath: string;\r\n}\r\n\r\n/**\r\n * 上传流程模板(Template Method)。\r\n * 定义固定步骤:onStart -> collectFiles -> filterFiles -> uploadFiles -> onComplete,\r\n * 子类实现 collectFiles 与 uploadFiles,其余步骤可复用或重写。\r\n */\r\nexport abstract class UploadFlowTemplate {\r\n protected constructor(protected readonly context: UploadContext) {}\r\n\r\n /** 执行完整上传流程 */\r\n async execute(): Promise<UploadSummary> {\r\n const { reporter } = this.context;\r\n\r\n if (reporter?.onStart) {\r\n await reporter.onStart({\r\n env: this.context.env,\r\n bucket: this.context.bucket,\r\n basePrefix: this.context.basePrefix,\r\n pathPrefix: this.context.pathPrefix,\r\n localDir: this.context.localDir,\r\n cdnBaseUrl: this.context.cdnBaseUrl,\r\n });\r\n }\r\n\r\n const files = await this.collectFiles();\r\n const filtered = this.filterFiles(files);\r\n const summary = await this.uploadFiles(filtered);\r\n\r\n if (reporter?.onComplete) {\r\n await reporter.onComplete(summary);\r\n }\r\n\r\n return summary;\r\n }\r\n\r\n protected abstract collectFiles(): Promise<LocalFile[]>;\r\n protected abstract uploadFiles(files: LocalFile[]): Promise<UploadSummary>;\r\n\r\n /** 使用 include/exclude 过滤文件列表 */\r\n protected filterFiles(files: LocalFile[]): LocalFile[] {\r\n const { include, exclude } = this.context;\r\n return files.filter((f) => shouldInclude(f.relativePath, include, exclude));\r\n }\r\n\r\n /**\r\n * 构造远端 key:basePrefix / env / pathPrefix / relativePath。\r\n * 保持目录结构,且 base 前缀来自 config,不对外暴露。\r\n */\r\n protected buildKey(relativePath: string): string {\r\n const parts: string[] = [];\r\n if (this.context.basePrefix) parts.push(this.context.basePrefix);\r\n if (this.context.env) parts.push(this.context.env);\r\n if (this.context.pathPrefix) parts.push(this.context.pathPrefix);\r\n\r\n parts.push(relativePath.replace(/\\\\/g, '/'));\r\n return parts.join('/');\r\n }\r\n\r\n /** 递归遍历目录,收集所有文件的绝对路径与相对路径 */\r\n protected async walkDir(rootDir: string, currentDir = ''): Promise<LocalFile[]> {\r\n const dirPath = path.join(rootDir, currentDir);\r\n const entries = await fs.readdir(dirPath, { withFileTypes: true });\r\n const files: LocalFile[] = [];\r\n\r\n for (const entry of entries) {\r\n const rel = path.join(currentDir, entry.name);\r\n const abs = path.join(rootDir, rel);\r\n\r\n if (entry.isDirectory()) {\r\n files.push(...(await this.walkDir(rootDir, rel)));\r\n } else if (entry.isFile()) {\r\n files.push({\r\n absolutePath: abs,\r\n relativePath: rel.replace(/\\\\/g, '/'),\r\n });\r\n }\r\n }\r\n\r\n return files;\r\n }\r\n\r\n /** 将单个文件的上传结果交给 Reporter */\r\n protected async reportFileResult(result: UploadFileResult): Promise<void> {\r\n const { reporter } = this.context;\r\n if (reporter?.onFileResult) {\r\n await reporter.onFileResult(result);\r\n }\r\n }\r\n}\r\n\r\n","import fs from 'fs/promises';\r\nimport { UploadFlowTemplate, UploadContext, LocalFile } from './UploadFlowTemplate';\r\nimport { UploadSummary } from './reporter';\r\nimport { buildCdnAccessUrl } from './urlHelper';\r\n\r\n/**\r\n * 目录上传流程的具体实现:遍历本地目录,按 key 规则上传,云端同名则跳过。\r\n * 规则:先 headObject,exists 则 skipped,否则 putObject。\r\n */\r\nexport class DirectoryUploadFlow extends UploadFlowTemplate {\r\n constructor(context: UploadContext) {\r\n super(context);\r\n }\r\n\r\n /** 递归收集 localDir 下所有文件 */\r\n protected async collectFiles(): Promise<LocalFile[]> {\r\n return this.walkDir(this.context.localDir);\r\n }\r\n\r\n /**\r\n * 逐个文件上传:先 head 判断是否存在,存在则跳过,否则读取本地文件并 putObject。\r\n * 每个文件的结果通过 reportFileResult 交给 Reporter。\r\n */\r\n protected async uploadFiles(files: LocalFile[]): Promise<UploadSummary> {\r\n const { bucket, storageClient, cdnBaseUrl } = this.context;\r\n\r\n const summary: UploadSummary = {\r\n total: files.length,\r\n success: 0,\r\n failed: 0,\r\n skipped: 0,\r\n };\r\n\r\n for (const file of files) {\r\n const key = this.buildKey(file.relativePath);\r\n\r\n try {\r\n const head = await storageClient.headObject(bucket, key);\r\n\r\n // 云端已有同名对象则跳过,不比较内容\r\n if (head.exists) {\r\n summary.skipped += 1;\r\n await this.reportFileResult({\r\n localPath: file.absolutePath,\r\n relativePath: file.relativePath,\r\n bucket,\r\n key,\r\n status: 'skipped',\r\n ...(cdnBaseUrl && { accessUrl: buildCdnAccessUrl(cdnBaseUrl, key) }),\r\n });\r\n continue;\r\n }\r\n\r\n const data = await fs.readFile(file.absolutePath);\r\n\r\n const { buffer, contentType } = this.context.fileProcessor\r\n ? await this.context.fileProcessor.process({\r\n localPath: file.absolutePath,\r\n relativePath: file.relativePath,\r\n buffer: data,\r\n })\r\n : { buffer: data, contentType: undefined as string | undefined };\r\n\r\n await storageClient.putObject({\r\n bucket,\r\n key,\r\n body: buffer,\r\n contentType,\r\n });\r\n\r\n summary.success += 1;\r\n await this.reportFileResult({\r\n localPath: file.absolutePath,\r\n relativePath: file.relativePath,\r\n bucket,\r\n key,\r\n status: 'success',\r\n ...(cdnBaseUrl && { accessUrl: buildCdnAccessUrl(cdnBaseUrl, key) }),\r\n });\r\n } catch (err: any) {\r\n summary.failed += 1;\r\n await this.reportFileResult({\r\n localPath: file.absolutePath,\r\n relativePath: file.relativePath,\r\n bucket,\r\n key,\r\n status: 'failed',\r\n error: err instanceof Error ? err : new Error(String(err)),\r\n });\r\n }\r\n }\r\n\r\n return summary;\r\n }\r\n}\r\n\r\n","import { StorageClient, PutObjectOptions, HeadObjectResult } from '../../core/StorageClient';\r\nimport { HuaweiObsConfig } from '../../config/types';\r\n\r\n// 华为 OBS Node.js SDK(CommonJS)\r\nconst ObsClient = require('esdk-obs-nodejs');\r\n\r\n/**\r\n * 华为 OBS 的 StorageClient 实现(Adapter)。\r\n * 通过 SDK 的 getObjectMetadata 查元数据判断是否存在,putObject 上传对象。\r\n */\r\nexport class HuaweiObsClient implements StorageClient {\r\n private readonly client: InstanceType<typeof ObsClient>;\r\n\r\n constructor(private readonly config: HuaweiObsConfig) {\r\n this.client = new ObsClient({\r\n access_key_id: config.accessKey,\r\n secret_access_key: config.secretKey,\r\n server: config.endpoint,\r\n });\r\n }\r\n\r\n /**\r\n * 调用 OBS getObjectMetadata 查元数据,判断对象是否存在(同路径即跳过上传)。\r\n */\r\n async headObject(bucket: string, key: string): Promise<HeadObjectResult> {\r\n try {\r\n const result = await this.client.getObjectMetadata({\r\n Bucket: bucket,\r\n Key: key,\r\n });\r\n\r\n if (result.CommonMsg.Status <= 300) {\r\n return { exists: true };\r\n }\r\n\r\n // 对象不存在\r\n if (result.CommonMsg.Status === 404 || result.CommonMsg.Code === 'NoSuchKey' || result.CommonMsg.Code === 'NotFound') {\r\n return { exists: false };\r\n }\r\n\r\n throw new Error(`OBS getObjectMetadata 失败: ${result.CommonMsg.Status} ${result.CommonMsg.Code} ${result.CommonMsg.Message}`);\r\n } catch (err: unknown) {\r\n const msg = err && typeof (err as any).message === 'string' ? (err as Error).message : String(err);\r\n const code = (err as any)?.Code ?? (err as any)?.code;\r\n if (code === 'NoSuchKey' || code === 'NotFound' || msg.includes('404') || msg.includes('NoSuchKey')) {\r\n return { exists: false };\r\n }\r\n throw err;\r\n }\r\n }\r\n\r\n /** 调用 OBS putObject 上传对象 */\r\n async putObject(options: PutObjectOptions): Promise<void> {\r\n const { bucket, key, body, contentType } = options;\r\n // console.log('putObject', { bucket, key, endpoint: this.config.endpoint, accessKey: this.config.accessKey, secretKey: this.config.secretKey })\r\n const result = await this.client.putObject({\r\n Bucket: bucket,\r\n Key: key,\r\n Body: body,\r\n // ContentType: contentType,\r\n });\r\n\r\n if (result.CommonMsg.Status > 300) {\r\n const { Status, Code, Message, RequestId } = result.CommonMsg;\r\n const detail = [Status, Code, Message, RequestId].filter(Boolean).join(' ');\r\n throw new Error(\r\n `OBS putObject 失败 (${detail || '无详情'}). ` +\r\n '请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com'\r\n );\r\n }\r\n }\r\n}\r\n","import { StorageClient } from './StorageClient';\r\nimport { ProviderConfig } from '../config/types';\r\nimport { HuaweiObsClient } from '../providers/huawei/HuaweiObsClient';\r\n\r\n/**\r\n * 根据 Provider 类型创建对应的 StorageClient(简单工厂)。\r\n * 后续扩展阿里 OSS、AWS S3 时在此增加 case 即可。\r\n */\r\nexport function createStorageClient(config: ProviderConfig): StorageClient {\r\n switch (config.type) {\r\n case 'huawei':\r\n return new HuaweiObsClient(config.options);\r\n default:\r\n throw new Error(`Unsupported provider type: ${config.type}`);\r\n }\r\n}\r\n\r\n","import { Reporter, UploadSummary } from '../core/reporter';\r\nimport { ProviderConfigResolver } from '../config/types';\r\nimport { createStorageClient } from '../core/StorageFactory';\r\nimport { DirectoryUploadFlow } from '../core/DirectoryUploadFlow';\r\nimport type { FileProcessor } from '../core/fileProcessor';\r\n\r\n/** 上传目录的入参:环境、bucket、本地目录、路径前缀、过滤规则、Reporter、上传前文件处理器 */\r\nexport interface UploadDirectoryInput {\r\n env?: string;\r\n bucket: string;\r\n localDir: string;\r\n pathPrefix?: string;\r\n include?: string[];\r\n exclude?: string[];\r\n reporter?: Reporter;\r\n /** 上传前对文件内容的处理器(如图片压缩),未配置则直接上传原内容 */\r\n fileProcessor?: FileProcessor;\r\n}\r\n\r\n/**\r\n * 中间件:对主流程暴露统一 API(Facade)。\r\n * 内部只依赖 ProviderConfigResolver 得到当前 Provider 配置,再创建 StorageClient 与流程并执行。\r\n * 切换云厂商时只需改 ProviderConfigResolver 的【一个实现文件】。\r\n */\r\nexport class OssService {\r\n constructor(private readonly providerConfigResolver: ProviderConfigResolver) {}\r\n\r\n /** 上传指定本地目录到 OSS,key 规则为 basePrefix/env/pathPrefix/相对路径,同名则跳过 */\r\n async uploadDirectory(input: UploadDirectoryInput): Promise<UploadSummary> {\r\n const { env, bucket= 'hr-uat', localDir, pathPrefix, include, exclude, reporter, fileProcessor } = input;\r\n\r\n const resolved = this.providerConfigResolver.resolve();\r\n const { providerConfig, basePrefix, cdnBaseUrl } = resolved;\r\n\r\n const client = createStorageClient(providerConfig);\r\n\r\n const flow = new DirectoryUploadFlow({\r\n env,\r\n bucket,\r\n localDir,\r\n pathPrefix,\r\n include,\r\n exclude,\r\n basePrefix,\r\n storageClient: client,\r\n reporter,\r\n cdnBaseUrl,\r\n fileProcessor,\r\n });\r\n\r\n return flow.execute();\r\n }\r\n}\r\n\r\n","import { EnvConfigResolver, HuaweiObsConfig } from './types';\r\n\r\n/**\r\n * 从系统环境变量解析华为 OBS 配置的实现。\r\n * 只使用一套固定变量,不区分 dev/test/prod 等多环境前缀。\r\n */\r\nexport class EnvConfigResolverImpl implements EnvConfigResolver {\r\n /**\r\n * 解析华为 OBS 配置(endpoint、ak/sk、basePrefix 等)。\r\n * 当前实现忽略 env 参数,统一使用以下环境变量:\r\n * - OBS_ENDPOINT\r\n * - OBS_ACCESS_KEY / OBS_SECRET_KEY(也可通过 OBS_CREDENTIALS_URL 拉取 YAML 获得)\r\n * - OBS_REGION(可选)\r\n * - OBS_BASE_PREFIX(可选,作为所有 key 的基础前缀)\r\n * - OBS_CDN_BASE_URL(可选,CDN 加速根地址,上传完成后用于拼出访问 URL)\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n resolveHuaweiConfig(env?: string): HuaweiObsConfig {\r\n // 通过环境变量解构出 OBS 访问配置(不依赖 .env 文件,直接读取进程环境)\r\n const endpoint = process.env.OBS_ENDPOINT;\r\n const accessKey = process.env.OBS_ACCESS_KEY;\r\n const secretKey = process.env.OBS_SECRET_KEY;\r\n const region = process.env.OBS_REGION;\r\n // basePrefix 作为所有对象 key 的基础前缀,用于隔离项目/租户等\r\n const basePrefix = process.env.OBS_BASE_PREFIX;\r\n // CDN 加速根地址,配置后用于拼出上传完成后的访问 URL\r\n const cdnBaseUrl = process.env.OBS_CDN_BASE_URL;\r\n\r\n if (!endpoint || !accessKey || !secretKey) {\r\n // 缺少关键配置时立即抛错,避免在后续真正上传时才暴露连接失败的问题\r\n throw new Error('缺少华为 OBS 配置: OBS_ENDPOINT / OBS_ACCESS_KEY / OBS_SECRET_KEY');\r\n }\r\n\r\n return {\r\n endpoint,\r\n accessKey,\r\n secretKey,\r\n region,\r\n basePrefix: basePrefix || '',\r\n cdnBaseUrl,\r\n };\r\n }\r\n}\r\n\r\n","import { ProviderConfigResolver, ResolvedProviderConfig } from './types';\r\nimport { EnvConfigResolverImpl } from './EnvConfigResolverImpl';\r\n\r\n/**\r\n * 统一 Provider 配置解析的【唯一入口】。\r\n * 切换云厂商时只需改本文件:改 OSS_PROVIDER 环境变量或增加 case,无需改 OssService / CLI 等。\r\n *\r\n * 环境变量约定:\r\n * - OSS_PROVIDER:当前使用的存储,如 'huawei'(默认)\r\n * - 当 OSS_PROVIDER=huawei 时,使用 OBS_* 变量(见 EnvConfigResolverImpl)\r\n * - 后续接入阿里云等时,在此增加 case 并读取对应 env 即可。\r\n */\r\nexport class ProviderConfigResolverImpl implements ProviderConfigResolver {\r\n private readonly huaweiResolver = new EnvConfigResolverImpl();\r\n\r\n resolve(): ResolvedProviderConfig {\r\n const provider = process.env.OSS_PROVIDER;\r\n\r\n switch (provider) {\r\n case 'huawei': {\r\n const options = this.huaweiResolver.resolveHuaweiConfig();\r\n return {\r\n providerConfig: { type: 'huawei', options },\r\n basePrefix: options.basePrefix,\r\n cdnBaseUrl: options.cdnBaseUrl,\r\n };\r\n }\r\n // 后续切换或新增云厂商时,在此增加 case,例如:\r\n // case 'aliyun': { ... return { providerConfig: { type: 'aliyun', options }, basePrefix, cdnBaseUrl }; }\r\n default:\r\n throw new Error(`不支持的 OSS_PROVIDER: ${provider},当前仅支持 huawei`);\r\n }\r\n }\r\n}\r\n"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包入口:显式导出所有对外 API 与类型,便于调用方按需引用。
|
|
3
|
+
*/
|
|
4
|
+
export { buildCdnAccessUrl } from './core/urlHelper';
|
|
5
|
+
export { type StorageClient, type PutObjectOptions, type HeadObjectResult, } from './core/StorageClient';
|
|
6
|
+
export { type Reporter, type UploadFileResult, type UploadSummary, type UploadStatus, ConsoleReporter, } from './core/reporter';
|
|
7
|
+
export { type FileProcessor, type ProcessFileInput, type ProcessFileResult, NoOpFileProcessor, } from './core/fileProcessor';
|
|
8
|
+
export { UploadFlowTemplate, type UploadContext, type LocalFile, } from './core/UploadFlowTemplate';
|
|
9
|
+
export { DirectoryUploadFlow, } from './core/DirectoryUploadFlow';
|
|
10
|
+
export { createStorageClient, } from './core/StorageFactory';
|
|
11
|
+
export { OssService, type UploadDirectoryInput, } from './middleware/OssService';
|
|
12
|
+
export { type HuaweiObsConfig, type ProviderConfig, type ProviderType, type EnvConfigResolver, type ResolvedProviderConfig, type ProviderConfigResolver, } from './config/types';
|
|
13
|
+
export { EnvConfigResolverImpl, } from './config/EnvConfigResolverImpl';
|
|
14
|
+
export { ProviderConfigResolverImpl, } from './config/ProviderConfigResolverImpl';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACtB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,UAAU,EACV,KAAK,oBAAoB,GAC1B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,qBAAqB,GACtB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACL,0BAA0B,GAC3B,MAAM,qCAAqC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Reporter, UploadSummary } from '../core/reporter';
|
|
2
|
+
import { ProviderConfigResolver } from '../config/types';
|
|
3
|
+
import type { FileProcessor } from '../core/fileProcessor';
|
|
4
|
+
/** 上传目录的入参:环境、bucket、本地目录、路径前缀、过滤规则、Reporter、上传前文件处理器 */
|
|
5
|
+
export interface UploadDirectoryInput {
|
|
6
|
+
env?: string;
|
|
7
|
+
bucket: string;
|
|
8
|
+
localDir: string;
|
|
9
|
+
pathPrefix?: string;
|
|
10
|
+
include?: string[];
|
|
11
|
+
exclude?: string[];
|
|
12
|
+
reporter?: Reporter;
|
|
13
|
+
/** 上传前对文件内容的处理器(如图片压缩),未配置则直接上传原内容 */
|
|
14
|
+
fileProcessor?: FileProcessor;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 中间件:对主流程暴露统一 API(Facade)。
|
|
18
|
+
* 内部只依赖 ProviderConfigResolver 得到当前 Provider 配置,再创建 StorageClient 与流程并执行。
|
|
19
|
+
* 切换云厂商时只需改 ProviderConfigResolver 的【一个实现文件】。
|
|
20
|
+
*/
|
|
21
|
+
export declare class OssService {
|
|
22
|
+
private readonly providerConfigResolver;
|
|
23
|
+
constructor(providerConfigResolver: ProviderConfigResolver);
|
|
24
|
+
/** 上传指定本地目录到 OSS,key 规则为 basePrefix/env/pathPrefix/相对路径,同名则跳过 */
|
|
25
|
+
uploadDirectory(input: UploadDirectoryInput): Promise<UploadSummary>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=OssService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OssService.d.ts","sourceRoot":"","sources":["../../src/middleware/OssService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,sCAAsC;IACtC,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;GAIG;AACH,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,sBAAsB;gBAAtB,sBAAsB,EAAE,sBAAsB;IAE3E,iEAAiE;IAC3D,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;CAwB3E"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StorageClient, PutObjectOptions, HeadObjectResult } from '../../core/StorageClient';
|
|
2
|
+
import { HuaweiObsConfig } from '../../config/types';
|
|
3
|
+
/**
|
|
4
|
+
* 华为 OBS 的 StorageClient 实现(Adapter)。
|
|
5
|
+
* 通过 SDK 的 getObjectMetadata 查元数据判断是否存在,putObject 上传对象。
|
|
6
|
+
*/
|
|
7
|
+
export declare class HuaweiObsClient implements StorageClient {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly client;
|
|
10
|
+
constructor(config: HuaweiObsConfig);
|
|
11
|
+
/**
|
|
12
|
+
* 调用 OBS getObjectMetadata 查元数据,判断对象是否存在(同路径即跳过上传)。
|
|
13
|
+
*/
|
|
14
|
+
headObject(bucket: string, key: string): Promise<HeadObjectResult>;
|
|
15
|
+
/** 调用 OBS putObject 上传对象 */
|
|
16
|
+
putObject(options: PutObjectOptions): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=HuaweiObsClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HuaweiObsClient.d.ts","sourceRoot":"","sources":["../../../src/providers/huawei/HuaweiObsClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAKrD;;;GAGG;AACH,qBAAa,eAAgB,YAAW,aAAa;IAGvC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;gBAE3B,MAAM,EAAE,eAAe;IAQpD;;OAEG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA2BxE,4BAA4B;IACtB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB1D"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dd-code/oss-uploader",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Upload local directories to OSS (currently Huawei OBS) with pluggable middleware and providers.",
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"oss-uploader": "dist/cli.cjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "rollup -c",
|
|
12
|
+
"dev": "rollup -c -w",
|
|
13
|
+
"prepare": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"commander": "^12.0.0",
|
|
17
|
+
"esdk-obs-nodejs": "^3.25.0",
|
|
18
|
+
"js-yaml": "^4.1.1",
|
|
19
|
+
"micromatch": "^4.0.8"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@rollup/plugin-commonjs": "^26.0.0",
|
|
23
|
+
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
24
|
+
"@rollup/plugin-replace": "^6.0.0",
|
|
25
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
26
|
+
"@types/js-yaml": "^4.0.9",
|
|
27
|
+
"@types/micromatch": "^4.0.9",
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"dotenv": "^16.4.5",
|
|
30
|
+
"rollup": "^4.0.0",
|
|
31
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
32
|
+
"typescript": "^5.6.0"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"publishConfig": { "access": "public" },
|
|
36
|
+
"keywords": [
|
|
37
|
+
"oss",
|
|
38
|
+
"obs",
|
|
39
|
+
"huawei",
|
|
40
|
+
"uploader",
|
|
41
|
+
"middleware"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { builtinModules } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import typescript from 'rollup-plugin-typescript2';
|
|
5
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
6
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
7
|
+
import replace from '@rollup/plugin-replace';
|
|
8
|
+
import terser from '@rollup/plugin-terser';
|
|
9
|
+
import dotenv from 'dotenv';
|
|
10
|
+
import pkg from './package.json' with { type: 'json' };
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
// 当前环境(默认 development)
|
|
15
|
+
const envMode = process.env.NODE_ENV || 'development';
|
|
16
|
+
|
|
17
|
+
// 加载 .env:优先级 .env.${mode} > .env(环境文件覆盖基础文件同名变量)
|
|
18
|
+
function loadEnv(mode) {
|
|
19
|
+
const baseEnv = dotenv.config({ path: path.resolve(__dirname, '.env') }).parsed || {};
|
|
20
|
+
const modeEnv = dotenv.config({
|
|
21
|
+
path: path.resolve(__dirname, `.env.${mode}`),
|
|
22
|
+
override: true
|
|
23
|
+
}).parsed || {};
|
|
24
|
+
return { ...baseEnv, ...modeEnv };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const env = loadEnv(envMode);
|
|
28
|
+
|
|
29
|
+
// 转为 Rollup replace 用的键值:process.env.XXX -> 字面量
|
|
30
|
+
const envVars = Object.entries(env).reduce(
|
|
31
|
+
(acc, [key, value]) => {
|
|
32
|
+
acc[`process.env.${key}`] = JSON.stringify(value);
|
|
33
|
+
return acc;
|
|
34
|
+
},
|
|
35
|
+
{ 'process.env.NODE_ENV': JSON.stringify(envMode) }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const externalDeps = [
|
|
39
|
+
...Object.keys(pkg.dependencies || {}),
|
|
40
|
+
...Object.keys(pkg.peerDependencies || {})
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const external = (id) => {
|
|
44
|
+
if (builtinModules.includes(id) || builtinModules.includes(id.replace(/^node:/, ''))) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return externalDeps.some((dep) => id === dep || id.startsWith(`${dep}/`));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const tsPlugin = typescript({
|
|
51
|
+
tsconfig: 'tsconfig.json',
|
|
52
|
+
useTsconfigDeclarationDir: false,
|
|
53
|
+
clean: true
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const replaceEnv = replace({
|
|
57
|
+
values: envVars,
|
|
58
|
+
preventAssignment: true
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export default [
|
|
62
|
+
{
|
|
63
|
+
input: 'src/index.ts',
|
|
64
|
+
external,
|
|
65
|
+
output: {
|
|
66
|
+
file: 'dist/index.cjs',
|
|
67
|
+
format: 'cjs',
|
|
68
|
+
sourcemap: false
|
|
69
|
+
},
|
|
70
|
+
plugins: [
|
|
71
|
+
replaceEnv,
|
|
72
|
+
resolve({
|
|
73
|
+
extensions: ['.js', '.ts'],
|
|
74
|
+
preferBuiltins: true
|
|
75
|
+
}),
|
|
76
|
+
commonjs(),
|
|
77
|
+
tsPlugin,
|
|
78
|
+
terser({ format: { comments: false } })
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
input: 'src/cli.ts',
|
|
83
|
+
external,
|
|
84
|
+
output: {
|
|
85
|
+
file: 'dist/cli.cjs',
|
|
86
|
+
format: 'cjs',
|
|
87
|
+
sourcemap: false,
|
|
88
|
+
banner: '#!/usr/bin/env node\n',
|
|
89
|
+
inlineDynamicImports: true
|
|
90
|
+
},
|
|
91
|
+
plugins: [
|
|
92
|
+
replaceEnv,
|
|
93
|
+
resolve({
|
|
94
|
+
extensions: ['.js', '.ts'],
|
|
95
|
+
preferBuiltins: true
|
|
96
|
+
}),
|
|
97
|
+
commonjs(),
|
|
98
|
+
tsPlugin,
|
|
99
|
+
terser({ format: { comments: false } })
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
];
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 入口:oss-upload 命令。
|
|
3
|
+
* 使用动态 import 加载依赖,避免启动时加载全部模块。
|
|
4
|
+
* .env 在打包时已注入 dist/cli.cjs,运行时不再读取。
|
|
5
|
+
*/
|
|
6
|
+
(async () => {
|
|
7
|
+
const [{ Command }, { OssService }, { ProviderConfigResolverImpl }, { ConsoleReporter }] = await Promise.all([
|
|
8
|
+
import('commander'),
|
|
9
|
+
import('./middleware/OssService'),
|
|
10
|
+
import('./config/ProviderConfigResolverImpl'),
|
|
11
|
+
import('./core/reporter'),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('oss-upload')
|
|
18
|
+
.description('上传本地目录到 OSS(当前为华为 OBS)')
|
|
19
|
+
.argument('<localDir>', '本地目录')
|
|
20
|
+
.requiredOption('-b, --bucket <bucket>', 'Bucket 名称','hr-uat')
|
|
21
|
+
.option('-e, --env <env>', '环境标识,如 dev/test/prod')
|
|
22
|
+
.option('--path-prefix <pathPrefix>', '业务路径前缀,将拼在 base/env 后面')
|
|
23
|
+
.option('--include <pattern...>', '包含的文件模式(可多次)')
|
|
24
|
+
.option('--exclude <pattern...>', '排除的文件模式(可多次)')
|
|
25
|
+
.action(async (localDir: string, options: any) => {
|
|
26
|
+
const { loadObsCredentialsFromUrlIfNeeded } = await import('./config/loadObsCredentialsFromUrl');
|
|
27
|
+
await loadObsCredentialsFromUrlIfNeeded();
|
|
28
|
+
|
|
29
|
+
const service = new OssService(new ProviderConfigResolverImpl());
|
|
30
|
+
const reporter = new ConsoleReporter();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const summary = await service.uploadDirectory({
|
|
34
|
+
env: options.env,
|
|
35
|
+
bucket: options.bucket,
|
|
36
|
+
localDir,
|
|
37
|
+
pathPrefix: options.pathPrefix,
|
|
38
|
+
include: options.include,
|
|
39
|
+
exclude: options.exclude,
|
|
40
|
+
reporter,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (summary.failed > 0) {
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
}
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.error('上传过程中发生错误:', err?.message || err);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program.parse(process.argv);
|
|
54
|
+
})();
|
|
55
|
+
|
|
@@ -0,0 +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
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ProviderConfigResolver, ResolvedProviderConfig } from './types';
|
|
2
|
+
import { EnvConfigResolverImpl } from './EnvConfigResolverImpl';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 统一 Provider 配置解析的【唯一入口】。
|
|
6
|
+
* 切换云厂商时只需改本文件:改 OSS_PROVIDER 环境变量或增加 case,无需改 OssService / CLI 等。
|
|
7
|
+
*
|
|
8
|
+
* 环境变量约定:
|
|
9
|
+
* - OSS_PROVIDER:当前使用的存储,如 'huawei'(默认)
|
|
10
|
+
* - 当 OSS_PROVIDER=huawei 时,使用 OBS_* 变量(见 EnvConfigResolverImpl)
|
|
11
|
+
* - 后续接入阿里云等时,在此增加 case 并读取对应 env 即可。
|
|
12
|
+
*/
|
|
13
|
+
export class ProviderConfigResolverImpl implements ProviderConfigResolver {
|
|
14
|
+
private readonly huaweiResolver = new EnvConfigResolverImpl();
|
|
15
|
+
|
|
16
|
+
resolve(): ResolvedProviderConfig {
|
|
17
|
+
const provider = process.env.OSS_PROVIDER;
|
|
18
|
+
|
|
19
|
+
switch (provider) {
|
|
20
|
+
case 'huawei': {
|
|
21
|
+
const options = this.huaweiResolver.resolveHuaweiConfig();
|
|
22
|
+
return {
|
|
23
|
+
providerConfig: { type: 'huawei', options },
|
|
24
|
+
basePrefix: options.basePrefix,
|
|
25
|
+
cdnBaseUrl: options.cdnBaseUrl,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// 后续切换或新增云厂商时,在此增加 case,例如:
|
|
29
|
+
// case 'aliyun': { ... return { providerConfig: { type: 'aliyun', options }, basePrefix, cdnBaseUrl }; }
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`不支持的 OSS_PROVIDER: ${provider},当前仅支持 huawei`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 当配置了 OBS_CREDENTIALS_URL 且未配置 AK/SK 时,从该 URL 拉取 YAML 并解析出
|
|
3
|
+
* OBS_ACCESS_KEY、OBS_SECRET_KEY 写入 process.env,供 EnvConfigResolverImpl 使用。
|
|
4
|
+
*/
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
|
|
7
|
+
const CREDENTIALS_URL_KEY = 'OBS_CREDENTIALS_URL';
|
|
8
|
+
// const ACCESS_KEY_KEY = 'OBS_ACCESS_KEY';
|
|
9
|
+
// const SECRET_KEY_KEY = 'OBS_SECRET_KEY';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 若设置了 OBS_CREDENTIALS_URL 且当前未设置 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,
|
|
13
|
+
* 则请求该 URL,解析 YAML,并将 OBS_ACCESS_KEY、OBS_SECRET_KEY 写入 process.env。
|
|
14
|
+
*/
|
|
15
|
+
export async function loadObsCredentialsFromUrlIfNeeded(): Promise<void> {
|
|
16
|
+
const url = process.env.OBS_CREDENTIALS_URL;
|
|
17
|
+
const hasAccessKey = process.env.OBS_ACCESS_KEY;
|
|
18
|
+
const hasSecretKey = process.env.OBS_SECRET_KEY;
|
|
19
|
+
|
|
20
|
+
if (!url || (hasAccessKey && hasSecretKey)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const res = await fetch(url);
|
|
25
|
+
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
throw new Error(`拉取 OBS 凭证失败 (${res.status}): ${url}`);
|
|
28
|
+
}
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
const parsed = yaml.load(text) as Record<string, unknown> | undefined;
|
|
31
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`OBS 凭证 YAML 解析结果无效,请检查 ${CREDENTIALS_URL_KEY} 返回内容`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const accessKey = parsed.OBS_ACCESS_KEY;
|
|
37
|
+
const secretKey = parsed.OBS_SECRET_KEY;
|
|
38
|
+
if (typeof accessKey === 'string') process.env.OBS_ACCESS_KEY = accessKey;
|
|
39
|
+
if (typeof secretKey === 'string') process.env.OBS_SECRET_KEY = secretKey;
|
|
40
|
+
|
|
41
|
+
if (!process.env.OBS_ACCESS_KEY || !process.env.OBS_SECRET_KEY) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`OBS 凭证 YAML 中未找到 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,请检查 ${CREDENTIALS_URL_KEY} 返回内容`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|