@dd-code/oss-uploader 0.1.11 → 0.1.13
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/dist/cli.cjs +1 -1
- package/dist/config/loadObsCredentialsFromUrl.d.ts.map +1 -1
- package/dist/core/UploadService.d.ts +2 -0
- package/dist/core/UploadService.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/middleware/OssService.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +51 -57
- package/src/config/loadObsCredentialsFromUrl.ts +15 -4
- package/src/core/UploadService.ts +17 -3
- package/src/middleware/OssService.ts +1 -0
package/dist/cli.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var e=require("chalk"),t=require("cli-progress"),s=require("fs/promises"),r=require("path"),o=require("micromatch"),i=require("js-yaml");(async()=>{const[{Command:e},{uploadDirectory:t}]=await Promise.all([import("commander"),Promise.resolve().then(function(){return S})]),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...>","排除的文件模式(可多次)").option("--a <a>","访问密钥 AK").option("--s <s>","访问密钥 SK").action(async(e,s)=>{const{loadObsCredentialsFromUrlIfNeeded:r}=await Promise.resolve().then(function(){return y});await r(),s.a&&s.s&&(process.env.OBS_ACCESS_KEY=s.a,process.env.OBS_SECRET_KEY=s.s);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 a{constructor(){this.progressBar=null}onStart(e){}onProgress(e,s){null===this.progressBar&&(this.progressBar=new t.SingleBar({format:" 上传进度 |{bar}| {percentage}% | {value}/{total} 文件",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0},t.Presets.shades_classic),this.progressBar.start(s,0)),this.progressBar.update(e),e>=s&&(this.progressBar.stop(),this.progressBar=null)}onFileResult(e){}onComplete(t){const{total:s,success:r,failed:o,skipped:i,context:a}=t;console.log(e.bold.cyan("\n ─────────── 上传完成 ───────────")),a&&(console.log(e.bold(" 前缀 / 配置(便于查验)")),console.log(` bucket: ${a.bucket}`),console.log(` basePrefix: ${a.basePrefix}`),void 0!==a.pathPrefix&&""!==a.pathPrefix&&console.log(` pathPrefix: ${a.pathPrefix}`),console.log(` localDir: ${a.localDir}`),a.cdnBaseUrl&&console.log(` cdnBaseUrl: ${a.cdnBaseUrl}`),console.log("")),console.log(` 总数: ${e.bold(s)} `+e.green(`成功: ${r.length}`)+" "+e.gray(`跳过: ${i.length}`)+" "+(o.length>0?e.red.bold(`失败: ${o.length}`):`失败: ${o.length}`)),r.length>0&&(console.log(e.green(`\n ✓ 成功 (${r.length})`)),r.forEach(t=>console.log(e.green(` ${t}`)))),i.length>0&&(console.log(e.gray(`\n - 跳过 (${i.length})`)),i.forEach(t=>console.log(e.gray(` ${t}`)))),o.length>0&&(console.log(e.red(`\n ✗ 失败 (${o.length})`)),o.forEach(t=>console.log(e.red(` ${t}`)))),console.log("")}}const n=require("esdk-obs-nodejs");class c{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 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 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"}}}class u{constructor(){this.huaweiResolver=new l}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}}function h(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 d(e){const t=r.extname(e).slice(1).toLowerCase();return t?p[t]:void 0}class f{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 s.context={bucket:e.bucket,basePrefix:this.basePrefix,pathPrefix:e.pathPrefix,localDir:e.localDir,cdnBaseUrl:this.cdnBaseUrl},this.reporter?.onComplete&&await this.reporter.onComplete(s),s}async collectFiles(e,t,i){const a=r.isAbsolute(e)?e:r.join(process.cwd(),e);if((await s.stat(a)).isFile())return[{absolutePath:a,relativePath:e}];return(await this.walkDir(a)).filter(e=>function(e,t,s){let r=!0;return t&&t.length>0&&(r=o.isMatch(e,t)),!(!r||s&&s.length>0&&o.isMatch(e,s))}(e.relativePath,t,i))}async uploadOneFile(e,t,r){const o=this.buildKey(this.basePrefix,t.env,t.pathPrefix,e.relativePath);try{const i=t.forceUploadPatterns?.some(e=>o.includes(e));if(void 0!==r){if(!i&&r.has(o))return await this.reportFile(e,t.bucket,o,"skipped"),{status:"skipped",key:o}}else{console.log(t.bucket,o);if((await this.storageClient.headObject(t.bucket,o)).exists)return await this.reportFile(e,t.bucket,o,"skipped"),{status:"skipped",key:o}}if(this.fileProcessor){const r=await s.readFile(e.absolutePath),{buffer:i,contentType:a}=await this.fileProcessor.process({localPath:e.absolutePath,relativePath:e.relativePath,buffer:r});await this.storageClient.putObject({bucket:t.bucket,key:o,body:i,contentType:a})}else await this.storageClient.putObject({bucket:t.bucket,key:o,sourceFile:e.absolutePath,contentType:d(e.relativePath)});return await this.reportFile(e,t.bucket,o,"success"),{status:"success",key:o}}catch(s){const r=s instanceof Error?s:new Error(String(s));return await this.reportFile(e,t.bucket,o,"failed",r),{status:"failed",key:o}}}buildKey(e,t,s,o){const i=(o??"").replace(/\\/g,"/");return r.join(e,s??"",t??"dev",i).replace(/\\/g,"/")}async walkDir(e,t=""){const o=r.join(e,t),i=await s.readdir(o,{withFileTypes:!0}),a=[];for(const s of i){const o=r.join(t,s.name),i=r.join(e,o);s.isDirectory()?a.push(...await this.walkDir(e,o)):s.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:h(this.cdnBaseUrl,s)},...o&&{error:o}};await this.reporter.onFileResult(i)}}class g extends f{constructor(e,t,s,r,o){super(e,t,s,r,o)}}class b{constructor(){this.resolver=new u}async uploadDirectory(e){const t=this.resolver.resolve(),s=function(e){if("huawei"===e.type)return new c(e.options);throw new Error(`Unsupported provider type: ${e.type}`)}(t.providerConfig),r=e.reporter??new a;return new g(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 S=Object.freeze({__proto__:null,OssService:b,uploadDirectory:async function(e){return(new b).uploadDirectory(e)}});const w="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=new AbortController,o=setTimeout(()=>r.abort(),15e3);let a;try{a=await fetch(e,{signal:r.signal})}catch(t){clearTimeout(o);throw new Error("AbortError"===t?.name?`拉取 OBS 凭证超时 (15000ms): ${e}。若在 Jenkins/CI 中无法访问该地址,请直接配置环境变量 OBS_ACCESS_KEY 和 OBS_SECRET_KEY。`:`拉取 OBS 凭证失败: ${t?.message??t}。若在 Jenkins/CI 中无法访问该地址,请直接配置环境变量 OBS_ACCESS_KEY 和 OBS_SECRET_KEY。`)}if(clearTimeout(o),!a.ok)throw new Error(`拉取 OBS 凭证失败 (${a.status}): ${e}`);const n=await a.text(),c=i.load(n);if(!c||"object"!=typeof c)throw new Error(`OBS 凭证 YAML 解析结果无效,请检查 ${w} 返回内容`);const l=c.OBS_ACCESS_KEY,u=c.OBS_SECRET_KEY;if("string"==typeof l&&(process.env.OBS_ACCESS_KEY=l),"string"==typeof u&&(process.env.OBS_SECRET_KEY=u),!process.env.OBS_ACCESS_KEY||!process.env.OBS_SECRET_KEY)throw new Error(`OBS 凭证 YAML 中未找到 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,请检查 ${w} 返回内容`)}});
|
|
2
|
+
"use strict";var e=require("chalk"),t=require("cli-progress"),s=require("fs/promises"),r=require("path"),o=require("crypto"),i=require("micromatch"),a=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...>","排除的文件模式(可多次)").option("--hash","上传的文件名后拼接文件内容 hash(用于缓存失效)").action(async(e,s)=>{const{loadObsCredentialsFromUrlIfNeeded:r}=await Promise.resolve().then(function(){return v});await r();try{(await t({env:s.env,bucket:s.bucket,localDir:e,pathPrefix:s.pathPrefix,include:s.include,exclude:s.exclude,hash:s.hash})).failed.length>0&&(process.exitCode=1)}catch(e){console.error("上传过程中发生错误:",e?.message||e),process.exitCode=1}}),s.parse(process.argv)})();class n{constructor(){this.progressBar=null}onStart(e){}onProgress(e,s){null===this.progressBar&&(this.progressBar=new t.SingleBar({format:" 上传进度 |{bar}| {percentage}% | {value}/{total} 文件",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0},t.Presets.shades_classic),this.progressBar.start(s,0)),this.progressBar.update(e),e>=s&&(this.progressBar.stop(),this.progressBar=null)}onFileResult(e){}onComplete(t){const{total:s,success:r,failed:o,skipped:i,context:a}=t;console.log(e.bold.cyan("\n ─────────── 上传完成 ───────────")),a&&(console.log(e.bold(" 前缀 / 配置(便于查验)")),console.log(` bucket: ${a.bucket}`),console.log(` basePrefix: ${a.basePrefix}`),void 0!==a.pathPrefix&&""!==a.pathPrefix&&console.log(` pathPrefix: ${a.pathPrefix}`),console.log(` localDir: ${a.localDir}`),a.cdnBaseUrl&&console.log(` cdnBaseUrl: ${a.cdnBaseUrl}`),console.log("")),console.log(` 总数: ${e.bold(s)} `+e.green(`成功: ${r.length}`)+" "+e.gray(`跳过: ${i.length}`)+" "+(o.length>0?e.red.bold(`失败: ${o.length}`):`失败: ${o.length}`)),r.length>0&&(console.log(e.green(`\n ✓ 成功 (${r.length})`)),r.forEach(t=>console.log(e.green(` ${t}`)))),i.length>0&&(console.log(e.gray(`\n - 跳过 (${i.length})`)),i.forEach(t=>console.log(e.gray(` ${t}`)))),o.length>0&&(console.log(e.red(`\n ✗ 失败 (${o.length})`)),o.forEach(t=>console.log(e.red(` ${t}`)))),console.log("")}}const c=require("esdk-obs-nodejs");class l{constructor(e){this.config=e,this.client=new c({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 u{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 h{constructor(){this.huaweiResolver=new u}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}}function p(e,t){const s=e.replace(/\/+$/,""),r=t.replace(/^\/+/,"");return r?`${s}/${r}`:s}const d={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 f(e){const t=r.extname(e).slice(1).toLowerCase();return t?d[t]:void 0}class g{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 s.context={bucket:e.bucket,basePrefix:this.basePrefix,pathPrefix:e.pathPrefix,localDir:e.localDir,cdnBaseUrl:this.cdnBaseUrl},this.reporter?.onComplete&&await this.reporter.onComplete(s),s}async collectFiles(e,t,o){const a=r.isAbsolute(e)?e:r.join(process.cwd(),e);if((await s.stat(a)).isFile())return[{absolutePath:a,relativePath:e}];return(await this.walkDir(a)).filter(e=>function(e,t,s){let r=!0;return t&&t.length>0&&(r=i.isMatch(e,t)),!(!r||s&&s.length>0&&i.isMatch(e,s))}(e.relativePath,t,o))}async uploadOneFile(e,t,r){let i=e.relativePath;if(t.hash){const t=await s.readFile(e.absolutePath),r=o.createHash("md5").update(t).digest("hex").slice(0,8);i=function(e,t){return e.replace(/\\/g,"/")+"."+t}(e.relativePath,r)}const a=this.buildKey(this.basePrefix,t.env,t.pathPrefix,i);try{const o=t.forceUploadPatterns?.some(e=>a.includes(e));if(void 0!==r){if(!o&&r.has(a))return await this.reportFile(e,t.bucket,a,"skipped"),{status:"skipped",key:a}}else{if((await this.storageClient.headObject(t.bucket,a)).exists)return await this.reportFile(e,t.bucket,a,"skipped"),{status:"skipped",key:a}}if(this.fileProcessor){const r=await s.readFile(e.absolutePath),{buffer:o,contentType:i}=await this.fileProcessor.process({localPath:e.absolutePath,relativePath:e.relativePath,buffer:r});await this.storageClient.putObject({bucket:t.bucket,key:a,body:o,contentType:i})}else await this.storageClient.putObject({bucket:t.bucket,key:a,sourceFile:e.absolutePath,contentType:f(e.relativePath)});return await this.reportFile(e,t.bucket,a,"success"),{status:"success",key:a}}catch(s){const r=s instanceof Error?s:new Error(String(s));return await this.reportFile(e,t.bucket,a,"failed",r),{status:"failed",key:a}}}buildKey(e,t,s,o){const i=(o??"").replace(/\\/g,"/");return r.join(e,s??"",t??"dev",i).replace(/\\/g,"/")}async walkDir(e,t=""){const o=r.join(e,t),i=await s.readdir(o,{withFileTypes:!0}),a=[];for(const s of i){const o=r.join(t,s.name),i=r.join(e,o);s.isDirectory()?a.push(...await this.walkDir(e,o)):s.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:p(this.cdnBaseUrl,s)},...o&&{error:o}};await this.reporter.onFileResult(i)}}class b extends g{constructor(e,t,s,r,o){super(e,t,s,r,o)}}class S{constructor(){this.resolver=new h}async uploadDirectory(e){const t=this.resolver.resolve(),s=function(e){if("huawei"===e.type)return new l(e.options);throw new Error(`Unsupported provider type: ${e.type}`)}(t.providerConfig),r=e.reporter??new n;return new b(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,hash:e.hash,forceUploadPatterns:m()})}}function m(){try{const e='["/index.html"]';return JSON.parse(e)}catch{return[]}}var w=Object.freeze({__proto__:null,OssService:S,uploadDirectory:async function(e){return(new S).uploadDirectory(e)}});const y="OBS_CREDENTIALS_URL";function C(){return process.env.OBS_ACCESS_KEY??process.env.OBS_AK}function E(){return process.env.OBS_SECRET_KEY??process.env.OBS_SK}var v=Object.freeze({__proto__:null,loadObsCredentialsFromUrlIfNeeded:async function(){const e="https://hr-uat.jtexpress.com.cn/hrpt/ast/static/obs-config.yaml",t=C(),s=E();if(t&&s)return t&&!process.env.OBS_ACCESS_KEY&&(process.env.OBS_ACCESS_KEY=C()),void(s&&!process.env.OBS_SECRET_KEY&&(process.env.OBS_SECRET_KEY=E()));const r=new AbortController,o=setTimeout(()=>r.abort(),15e3);let i;try{i=await fetch(e,{signal:r.signal})}catch(t){clearTimeout(o);throw new Error("AbortError"===t?.name?`拉取 OBS 凭证超时 (15000ms): ${e}。若在 Jenkins/CI 中无法访问该地址,请直接配置环境变量 OBS_ACCESS_KEY 和 OBS_SECRET_KEY。`:`拉取 OBS 凭证失败: ${t?.message??t}。若在 Jenkins/CI 中无法访问该地址,请直接配置环境变量 OBS_ACCESS_KEY 和 OBS_SECRET_KEY。`)}if(clearTimeout(o),!i.ok)throw new Error(`拉取 OBS 凭证失败 (${i.status}): ${e}`);const n=await i.text(),c=a.load(n);if(!c||"object"!=typeof c)throw new Error(`OBS 凭证 YAML 解析结果无效,请检查 ${y} 返回内容`);const l=c.OBS_ACCESS_KEY,u=c.OBS_SECRET_KEY;if("string"==typeof l&&(process.env.OBS_ACCESS_KEY=l),"string"==typeof u&&(process.env.OBS_SECRET_KEY=u),!process.env.OBS_ACCESS_KEY||!process.env.OBS_SECRET_KEY)throw new Error(`OBS 凭证 YAML 中未找到 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,请检查 ${y} 返回内容`)}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadObsCredentialsFromUrl.d.ts","sourceRoot":"","sources":["../../src/config/loadObsCredentialsFromUrl.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"loadObsCredentialsFromUrl.d.ts","sourceRoot":"","sources":["../../src/config/loadObsCredentialsFromUrl.ts"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,wBAAsB,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC,CA8CvE"}
|
|
@@ -9,6 +9,8 @@ export interface UploadDirectoryOptions {
|
|
|
9
9
|
pathPrefix?: string;
|
|
10
10
|
include?: string[];
|
|
11
11
|
exclude?: string[];
|
|
12
|
+
/** 上传后的文件名在扩展名前拼接文件内容 hash(如 file.js -> file.abc12def.js) */
|
|
13
|
+
hash?: boolean;
|
|
12
14
|
/** 匹配到的 key 强制上传(不跳过),如从 FILE_RE_WHITE_LIST 解析的列表 */
|
|
13
15
|
forceUploadPatterns?: string[];
|
|
14
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UploadService.d.ts","sourceRoot":"","sources":["../../src/core/UploadService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"UploadService.d.ts","sourceRoot":"","sources":["../../src/core/UploadService.ts"],"names":[],"mappings":"AAGA,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;AAsCrD,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,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,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;IAuD9E,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;IA6DrE,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("chalk"),t=require("cli-progress"),s=require("fs/promises"),r=require("path"),o=require("micromatch");class
|
|
1
|
+
"use strict";var e=require("chalk"),t=require("cli-progress"),s=require("fs/promises"),r=require("path"),o=require("crypto"),i=require("micromatch");class a{constructor(){this.progressBar=null}onStart(e){}onProgress(e,s){null===this.progressBar&&(this.progressBar=new t.SingleBar({format:" 上传进度 |{bar}| {percentage}% | {value}/{total} 文件",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0},t.Presets.shades_classic),this.progressBar.start(s,0)),this.progressBar.update(e),e>=s&&(this.progressBar.stop(),this.progressBar=null)}onFileResult(e){}onComplete(t){const{total:s,success:r,failed:o,skipped:i,context:a}=t;console.log(e.bold.cyan("\n ─────────── 上传完成 ───────────")),a&&(console.log(e.bold(" 前缀 / 配置(便于查验)")),console.log(` bucket: ${a.bucket}`),console.log(` basePrefix: ${a.basePrefix}`),void 0!==a.pathPrefix&&""!==a.pathPrefix&&console.log(` pathPrefix: ${a.pathPrefix}`),console.log(` localDir: ${a.localDir}`),a.cdnBaseUrl&&console.log(` cdnBaseUrl: ${a.cdnBaseUrl}`),console.log("")),console.log(` 总数: ${e.bold(s)} `+e.green(`成功: ${r.length}`)+" "+e.gray(`跳过: ${i.length}`)+" "+(o.length>0?e.red.bold(`失败: ${o.length}`):`失败: ${o.length}`)),r.length>0&&(console.log(e.green(`\n ✓ 成功 (${r.length})`)),r.forEach(t=>console.log(e.green(` ${t}`)))),i.length>0&&(console.log(e.gray(`\n - 跳过 (${i.length})`)),i.forEach(t=>console.log(e.gray(` ${t}`)))),o.length>0&&(console.log(e.red(`\n ✗ 失败 (${o.length})`)),o.forEach(t=>console.log(e.red(` ${t}`)))),console.log("")}}const n=require("esdk-obs-nodejs");class c{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 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 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"}}}class u{constructor(){this.huaweiResolver=new l}resolve(){{const e=this.huaweiResolver.resolveHuaweiConfig();return{providerConfig:{type:"huawei",options:e},basePrefix:e.basePrefix,cdnBaseUrl:e.cdnBaseUrl}}}}function h(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 f(e){const t=r.extname(e).slice(1).toLowerCase();return t?p[t]:void 0}class g{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 s.context={bucket:e.bucket,basePrefix:this.basePrefix,pathPrefix:e.pathPrefix,localDir:e.localDir,cdnBaseUrl:this.cdnBaseUrl},this.reporter?.onComplete&&await this.reporter.onComplete(s),s}async collectFiles(e,t,o){const a=r.isAbsolute(e)?e:r.join(process.cwd(),e);if((await s.stat(a)).isFile())return[{absolutePath:a,relativePath:e}];return(await this.walkDir(a)).filter(e=>function(e,t,s){let r=!0;return t&&t.length>0&&(r=i.isMatch(e,t)),!(!r||s&&s.length>0&&i.isMatch(e,s))}(e.relativePath,t,o))}async uploadOneFile(e,t,r){let i=e.relativePath;if(t.hash){const t=await s.readFile(e.absolutePath),r=o.createHash("md5").update(t).digest("hex").slice(0,8);i=function(e,t){return e.replace(/\\/g,"/")+"."+t}(e.relativePath,r)}const a=this.buildKey(this.basePrefix,t.env,t.pathPrefix,i);try{const o=t.forceUploadPatterns?.some(e=>a.includes(e));if(void 0!==r){if(!o&&r.has(a))return await this.reportFile(e,t.bucket,a,"skipped"),{status:"skipped",key:a}}else{if((await this.storageClient.headObject(t.bucket,a)).exists)return await this.reportFile(e,t.bucket,a,"skipped"),{status:"skipped",key:a}}if(this.fileProcessor){const r=await s.readFile(e.absolutePath),{buffer:o,contentType:i}=await this.fileProcessor.process({localPath:e.absolutePath,relativePath:e.relativePath,buffer:r});await this.storageClient.putObject({bucket:t.bucket,key:a,body:o,contentType:i})}else await this.storageClient.putObject({bucket:t.bucket,key:a,sourceFile:e.absolutePath,contentType:f(e.relativePath)});return await this.reportFile(e,t.bucket,a,"success"),{status:"success",key:a}}catch(s){const r=s instanceof Error?s:new Error(String(s));return await this.reportFile(e,t.bucket,a,"failed",r),{status:"failed",key:a}}}buildKey(e,t,s,o){const i=(o??"").replace(/\\/g,"/");return r.join(e,s??"",t??"dev",i).replace(/\\/g,"/")}async walkDir(e,t=""){const o=r.join(e,t),i=await s.readdir(o,{withFileTypes:!0}),a=[];for(const s of i){const o=r.join(t,s.name),i=r.join(e,o);s.isDirectory()?a.push(...await this.walkDir(e,o)):s.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:h(this.cdnBaseUrl,s)},...o&&{error:o}};await this.reporter.onFileResult(i)}}class d extends g{constructor(e,t,s,r,o){super(e,t,s,r,o)}}class b{constructor(){this.resolver=new u}async uploadDirectory(e){const t=this.resolver.resolve(),s=function(e){if("huawei"===e.type)return new c(e.options);throw new Error(`Unsupported provider type: ${e.type}`)}(t.providerConfig),r=e.reporter??new a;return new d(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,hash:e.hash,forceUploadPatterns:y()})}}function y(){try{const e='["/index.html"]';return JSON.parse(e)}catch{return[]}}exports.upload=async function(e){return async function(e){return(new b).uploadDirectory(e)}({...e,env:e.env||"dev",bucket:e.bucket||"hr-uat"})};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OssService.d.ts","sourceRoot":"","sources":["../../src/middleware/OssService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAmB,MAAM,kBAAkB,CAAC;AAE5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,KAAK,sBAAsB,IAAI,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAKnF,sDAAsD;AACtD,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,aAAa,CAAC,CAExB;AAID;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAEvD,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"OssService.d.ts","sourceRoot":"","sources":["../../src/middleware/OssService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAmB,MAAM,kBAAkB,CAAC;AAE5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,KAAK,sBAAsB,IAAI,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAKnF,sDAAsD;AACtD,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,aAAa,CAAC,CAExB;AAID;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAEvD,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC;CAwB/E"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,57 +1,51 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI 入口:oss-upload 命令。
|
|
3
|
-
* 使用动态 import 加载依赖,避免启动时加载全部模块。
|
|
4
|
-
* .env 在打包时已注入 dist/cli.cjs,运行时不再读取。
|
|
5
|
-
*/
|
|
6
|
-
(async () => {
|
|
7
|
-
const [{ Command }, { uploadDirectory }] = await Promise.all([
|
|
8
|
-
import('commander'),
|
|
9
|
-
import('./middleware/OssService'),
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
const program = new Command();
|
|
13
|
-
|
|
14
|
-
program
|
|
15
|
-
.name('oss-upload')
|
|
16
|
-
.description('上传本地目录到 OSS(当前为华为 OBS)')
|
|
17
|
-
.argument('<localDir>', '本地目录')
|
|
18
|
-
.requiredOption('-b, --bucket <bucket>', 'Bucket 名称', 'hr-uat')
|
|
19
|
-
.option('-e, --env <env>', '环境标识,如 dev/test/prod', 'dev')
|
|
20
|
-
.option('--path-prefix <pathPrefix>', '业务路径前缀,将拼在 base/env 后面')
|
|
21
|
-
.option('--include <pattern...>', '包含的文件模式(可多次)')
|
|
22
|
-
.option('--exclude <pattern...>', '排除的文件模式(可多次)')
|
|
23
|
-
.option('--
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
program.parse(process.argv);
|
|
56
|
-
})();
|
|
57
|
-
|
|
1
|
+
/**
|
|
2
|
+
* CLI 入口:oss-upload 命令。
|
|
3
|
+
* 使用动态 import 加载依赖,避免启动时加载全部模块。
|
|
4
|
+
* .env 在打包时已注入 dist/cli.cjs,运行时不再读取。
|
|
5
|
+
*/
|
|
6
|
+
(async () => {
|
|
7
|
+
const [{ Command }, { uploadDirectory }] = await Promise.all([
|
|
8
|
+
import('commander'),
|
|
9
|
+
import('./middleware/OssService'),
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('oss-upload')
|
|
16
|
+
.description('上传本地目录到 OSS(当前为华为 OBS)')
|
|
17
|
+
.argument('<localDir>', '本地目录')
|
|
18
|
+
.requiredOption('-b, --bucket <bucket>', 'Bucket 名称', 'hr-uat')
|
|
19
|
+
.option('-e, --env <env>', '环境标识,如 dev/test/prod', 'dev')
|
|
20
|
+
.option('--path-prefix <pathPrefix>', '业务路径前缀,将拼在 base/env 后面')
|
|
21
|
+
.option('--include <pattern...>', '包含的文件模式(可多次)')
|
|
22
|
+
.option('--exclude <pattern...>', '排除的文件模式(可多次)')
|
|
23
|
+
.option('--hash', '上传的文件名后拼接文件内容 hash(用于缓存失效)')
|
|
24
|
+
.action(async (localDir: string, options: any) => {
|
|
25
|
+
const { loadObsCredentialsFromUrlIfNeeded } = await import('./config/loadObsCredentialsFromUrl');
|
|
26
|
+
await loadObsCredentialsFromUrlIfNeeded();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const summary = await uploadDirectory({
|
|
30
|
+
env: options.env,
|
|
31
|
+
bucket: options.bucket,
|
|
32
|
+
localDir,
|
|
33
|
+
pathPrefix: options.pathPrefix,
|
|
34
|
+
include: options.include,
|
|
35
|
+
exclude: options.exclude,
|
|
36
|
+
hash: options.hash,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (summary.failed.length > 0) {
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.error('上传过程中发生错误:', err?.message || err);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
program.parse(process.argv);
|
|
50
|
+
})();
|
|
51
|
+
|
|
@@ -2,24 +2,35 @@
|
|
|
2
2
|
* 当配置了 OBS_CREDENTIALS_URL 且未配置 AK/SK 时,从该 URL 拉取 YAML 并解析出
|
|
3
3
|
* OBS_ACCESS_KEY、OBS_SECRET_KEY 写入 process.env,供 EnvConfigResolverImpl 使用。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* OBS_ACCESS_KEY
|
|
5
|
+
* Jenkins/CI 用法:在 Job 中配置环境变量即可,不依赖 URL 拉取。
|
|
6
|
+
* - 标准名:OBS_ACCESS_KEY、OBS_SECRET_KEY(推荐)
|
|
7
|
+
* - 别名:OBS_AK、OBS_SK(若 Jenkins 凭证绑定使用此命名)
|
|
8
|
+
* 只要 AK/SK 任一组合存在则不会请求 OBS_CREDENTIALS_URL。
|
|
7
9
|
*/
|
|
8
10
|
import yaml from 'js-yaml';
|
|
9
11
|
|
|
10
12
|
const CREDENTIALS_URL_KEY = 'OBS_CREDENTIALS_URL';
|
|
11
13
|
const FETCH_TIMEOUT_MS = 15000;
|
|
12
14
|
|
|
15
|
+
function getObsAccessKey(): string | undefined {
|
|
16
|
+
return process.env.OBS_ACCESS_KEY ?? process.env.OBS_AK;
|
|
17
|
+
}
|
|
18
|
+
function getObsSecretKey(): string | undefined {
|
|
19
|
+
return process.env.OBS_SECRET_KEY ?? process.env.OBS_SK;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* 若设置了 OBS_CREDENTIALS_URL 且当前未设置 OBS_ACCESS_KEY 或 OBS_SECRET_KEY,
|
|
15
24
|
* 则请求该 URL,解析 YAML,并将 OBS_ACCESS_KEY、OBS_SECRET_KEY 写入 process.env。
|
|
16
25
|
*/
|
|
17
26
|
export async function loadObsCredentialsFromUrlIfNeeded(): Promise<void> {
|
|
18
27
|
const url = process.env.OBS_CREDENTIALS_URL;
|
|
19
|
-
const hasAccessKey =
|
|
20
|
-
const hasSecretKey =
|
|
28
|
+
const hasAccessKey = getObsAccessKey();
|
|
29
|
+
const hasSecretKey = getObsSecretKey();
|
|
21
30
|
|
|
22
31
|
if (!url || (hasAccessKey && hasSecretKey)) {
|
|
32
|
+
if (hasAccessKey && !process.env.OBS_ACCESS_KEY) process.env.OBS_ACCESS_KEY = getObsAccessKey();
|
|
33
|
+
if (hasSecretKey && !process.env.OBS_SECRET_KEY) process.env.OBS_SECRET_KEY = getObsSecretKey();
|
|
23
34
|
return;
|
|
24
35
|
}
|
|
25
36
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
3
4
|
import { Reporter, UploadSummary, UploadFileResult } from './reporter';
|
|
4
5
|
import { StorageClient } from './StorageClient';
|
|
5
6
|
import { shouldInclude } from './filters';
|
|
@@ -36,6 +37,12 @@ function getContentType(relativePath: string): string | undefined {
|
|
|
36
37
|
return ext ? EXT_TO_MIME[ext] : undefined;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/** 在文件名后面拼接 hash,如 file.js -> file.js.abc12def */
|
|
41
|
+
function relativePathWithHash(relativePath: string, fileHash: string): string {
|
|
42
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
43
|
+
return normalized + '.' + fileHash;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
/** 单次上传的目录参数 */
|
|
40
47
|
export interface UploadDirectoryOptions {
|
|
41
48
|
bucket: string;
|
|
@@ -44,6 +51,8 @@ export interface UploadDirectoryOptions {
|
|
|
44
51
|
pathPrefix?: string;
|
|
45
52
|
include?: string[];
|
|
46
53
|
exclude?: string[];
|
|
54
|
+
/** 上传后的文件名在扩展名前拼接文件内容 hash(如 file.js -> file.abc12def.js) */
|
|
55
|
+
hash?: boolean;
|
|
47
56
|
/** 匹配到的 key 强制上传(不跳过),如从 FILE_RE_WHITE_LIST 解析的列表 */
|
|
48
57
|
forceUploadPatterns?: string[];
|
|
49
58
|
}
|
|
@@ -142,11 +151,18 @@ export abstract class UploadService {
|
|
|
142
151
|
options: UploadDirectoryOptions,
|
|
143
152
|
existingKeysSet?: Set<string>,
|
|
144
153
|
): Promise<{ status: 'success' | 'skipped' | 'failed'; key: string }> {
|
|
154
|
+
let keyRelativePath = file.relativePath;
|
|
155
|
+
if (options.hash) {
|
|
156
|
+
const data = await fs.readFile(file.absolutePath);
|
|
157
|
+
const fileHash = crypto.createHash('md5').update(data).digest('hex').slice(0, 8);
|
|
158
|
+
keyRelativePath = relativePathWithHash(file.relativePath, fileHash);
|
|
159
|
+
}
|
|
160
|
+
|
|
145
161
|
const key = this.buildKey(
|
|
146
162
|
this.basePrefix,
|
|
147
163
|
options.env,
|
|
148
164
|
options.pathPrefix,
|
|
149
|
-
|
|
165
|
+
keyRelativePath,
|
|
150
166
|
);
|
|
151
167
|
|
|
152
168
|
try {
|
|
@@ -157,8 +173,6 @@ export abstract class UploadService {
|
|
|
157
173
|
return { status: 'skipped', key };
|
|
158
174
|
}
|
|
159
175
|
} else {
|
|
160
|
-
console.log(options.bucket, key);
|
|
161
|
-
|
|
162
176
|
const head = await this.storageClient.headObject(options.bucket, key);
|
|
163
177
|
if (head.exists) {
|
|
164
178
|
await this.reportFile(file, options.bucket, key, 'skipped');
|