mirror of https://github.com/certd/certd
				
				
				
			perf: 数据库备份支持oss
							parent
							
								
									50a5fa15bb
								
							
						
					
					
						commit
						308d4600ef
					
				|  | @ -6,8 +6,8 @@ import { Challenge } from "@certd/acme-client/types/rfc8555"; | |||
| import { IContext } from "@certd/pipeline"; | ||||
| import { ILogger, utils } from "@certd/basic"; | ||||
| import { IDnsProvider, IDomainParser } from "../../dns-provider/index.js"; | ||||
| import { HttpChallengeUploader } from "./uploads/api.js"; | ||||
| import punycode from "node:punycode"; | ||||
| import { IOssClient } from "@certd/plugin-lib"; | ||||
| export type CnameVerifyPlan = { | ||||
|   type?: string; | ||||
|   domain: string; | ||||
|  | @ -18,7 +18,7 @@ export type CnameVerifyPlan = { | |||
| export type HttpVerifyPlan = { | ||||
|   type: string; | ||||
|   domain: string; | ||||
|   httpUploader: HttpChallengeUploader; | ||||
|   httpUploader: IOssClient; | ||||
| }; | ||||
| 
 | ||||
| export type DomainVerifyPlan = { | ||||
|  | @ -35,7 +35,7 @@ export type DomainsVerifyPlan = { | |||
| export type Providers = { | ||||
|   dnsProvider?: IDnsProvider; | ||||
|   domainsVerifyPlan?: DomainsVerifyPlan; | ||||
|   httpUploader?: HttpChallengeUploader; | ||||
|   httpUploader?: IOssClient; | ||||
| }; | ||||
| 
 | ||||
| export type CertInfo = { | ||||
|  | @ -184,7 +184,7 @@ export class AcmeService { | |||
|       return authz.challenges.find((c: any) => c.type === type); | ||||
|     }; | ||||
| 
 | ||||
|     const doHttpVerify = async (challenge: any, httpUploader: HttpChallengeUploader) => { | ||||
|     const doHttpVerify = async (challenge: any, httpUploader: IOssClient) => { | ||||
|       const keyAuthorization = await keyAuthorizationGetter(challenge); | ||||
|       this.logger.info("http校验"); | ||||
|       const filePath = `.well-known/acme-challenge/${challenge.token}`; | ||||
|  | @ -287,7 +287,7 @@ export class AcmeService { | |||
|    * @returns {Promise} | ||||
|    */ | ||||
| 
 | ||||
|   async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider?: IDnsProvider, httpUploader?: HttpChallengeUploader) { | ||||
|   async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider?: IDnsProvider, httpUploader?: IOssClient) { | ||||
|     this.logger.info("执行清理"); | ||||
| 
 | ||||
|     /* http-01 */ | ||||
|  |  | |||
|  | @ -9,8 +9,8 @@ import { CertReader } from "./cert-reader.js"; | |||
| import { CertApplyBasePlugin } from "./base.js"; | ||||
| import { GoogleClient } from "../../libs/google.js"; | ||||
| import { EabAccess } from "../../access"; | ||||
| import { httpChallengeUploaderFactory } from "./uploads/factory.js"; | ||||
| import { DomainParser } from "../../dns-provider/domain-parser.js"; | ||||
| import { ossClientFactory } from "@certd/plugin-lib"; | ||||
| export * from "./base.js"; | ||||
| export type { CertInfo }; | ||||
| export * from "./cert-reader.js"; | ||||
|  | @ -115,6 +115,7 @@ HTTP文件验证:不支持泛域名,需要配置网站文件上传`, | |||
|   }) | ||||
|   dnsProviderType!: string; | ||||
| 
 | ||||
|   // dns解析授权类型,勿删
 | ||||
|   dnsProviderAccessType!: string; | ||||
| 
 | ||||
|   @TaskInput({ | ||||
|  | @ -446,7 +447,7 @@ HTTP文件验证:不支持泛域名,需要配置网站文件上传`, | |||
|             rootDir = rootDir + "/"; | ||||
|           } | ||||
|           this.logger.info("上传方式", httpRecord.httpUploaderType); | ||||
|           const httpUploader = await httpChallengeUploaderFactory.createUploaderByType(httpRecord.httpUploaderType, { | ||||
|           const httpUploader = await ossClientFactory.createOssClientByType(httpRecord.httpUploaderType, { | ||||
|             access, | ||||
|             rootDir: rootDir, | ||||
|             ctx: httpUploaderContext, | ||||
|  |  | |||
|  | @ -1,35 +0,0 @@ | |||
| import { IAccessService } from "@certd/pipeline"; | ||||
| import { ILogger, utils } from "@certd/basic"; | ||||
| 
 | ||||
| export type HttpChallengeUploader = { | ||||
|   upload: (fileName: string, fileContent: Buffer) => Promise<void>; | ||||
|   remove: (fileName: string) => Promise<void>; | ||||
| }; | ||||
| 
 | ||||
| export type HttpChallengeUploadContext = { | ||||
|   accessService: IAccessService; | ||||
|   logger: ILogger; | ||||
|   utils: typeof utils; | ||||
| }; | ||||
| 
 | ||||
| export abstract class BaseHttpChallengeUploader<A> implements HttpChallengeUploader { | ||||
|   rootDir: string; | ||||
|   access: A = null; | ||||
|   logger: ILogger; | ||||
|   utils: typeof utils; | ||||
|   ctx: HttpChallengeUploadContext; | ||||
|   protected constructor(opts: { rootDir: string; access: A }) { | ||||
|     this.rootDir = opts.rootDir; | ||||
|     this.access = opts.access; | ||||
|   } | ||||
| 
 | ||||
|   async setCtx(ctx: any) { | ||||
|     // set context
 | ||||
|     this.ctx = ctx; | ||||
|     this.logger = ctx.logger; | ||||
|     this.utils = ctx.utils; | ||||
|   } | ||||
| 
 | ||||
|   abstract remove(fileName: string): Promise<void>; | ||||
|   abstract upload(fileName: string, fileContent: Buffer): Promise<void>; | ||||
| } | ||||
|  | @ -1,39 +0,0 @@ | |||
| import { BaseHttpChallengeUploader } from "../api.js"; | ||||
| import { AliossAccess, AliyunAccess } from "@certd/plugin-lib"; | ||||
| import { AliossClient } from "@certd/plugin-lib"; | ||||
| 
 | ||||
| export class AliossHttpChallengeUploader extends BaseHttpChallengeUploader<AliossAccess> { | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId); | ||||
|     const client = new AliossClient({ | ||||
|       access: aliyunAccess, | ||||
|       bucket: this.access.bucket, | ||||
|       region: this.access.region, | ||||
|     }); | ||||
| 
 | ||||
|     const key = this.rootDir + filePath; | ||||
|     this.logger.info(`开始上传文件: ${key}`); | ||||
|     await client.uploadFile(key, fileContent); | ||||
| 
 | ||||
|     this.logger.info(`校验文件上传成功: ${filePath}`); | ||||
|   } | ||||
| 
 | ||||
|   async remove(filePath: string) { | ||||
|     const key = this.rootDir + filePath; | ||||
|     // remove file from alioss
 | ||||
|     const client = await this.getAliossClient(); | ||||
|     await client.removeFile(key); | ||||
|     this.logger.info(`文件删除成功: ${key}`); | ||||
|   } | ||||
| 
 | ||||
|   private async getAliossClient() { | ||||
|     const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId); | ||||
|     const client = new AliossClient({ | ||||
|       access: aliyunAccess, | ||||
|       bucket: this.access.bucket, | ||||
|       region: this.access.region, | ||||
|     }); | ||||
|     await client.init(); | ||||
|     return client; | ||||
|   } | ||||
| } | ||||
|  | @ -1,31 +0,0 @@ | |||
| import { BaseHttpChallengeUploader } from "../api.js"; | ||||
| import { QiniuOssAccess, QiniuClient, QiniuAccess } from "@certd/plugin-lib"; | ||||
| 
 | ||||
| export class QiniuOssHttpChallengeUploader extends BaseHttpChallengeUploader<QiniuOssAccess> { | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId); | ||||
|     const client = new QiniuClient({ | ||||
|       access: qiniuAccess, | ||||
|       logger: this.logger, | ||||
|       http: this.ctx.utils.http, | ||||
|     }); | ||||
|     if (this.rootDir.endsWith("/")) { | ||||
|       this.rootDir = this.rootDir.slice(0, -1); | ||||
|     } | ||||
|     await client.uploadFile(this.access.bucket, this.rootDir + filePath, fileContent); | ||||
|   } | ||||
| 
 | ||||
|   async remove(filePath: string) { | ||||
|     const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId); | ||||
|     const client = new QiniuClient({ | ||||
|       access: qiniuAccess, | ||||
|       logger: this.logger, | ||||
|       http: this.ctx.utils.http, | ||||
|     }); | ||||
| 
 | ||||
|     if (this.rootDir.endsWith("/")) { | ||||
|       this.rootDir = this.rootDir.slice(0, -1); | ||||
|     } | ||||
|     await client.removeFile(this.access.bucket, this.rootDir + filePath); | ||||
|   } | ||||
| } | ||||
|  | @ -16,10 +16,11 @@ | |||
|   }, | ||||
|   "dependencies": { | ||||
|     "@alicloud/pop-core": "^1.7.10", | ||||
|     "@aws-sdk/client-s3": "^3.787.0", | ||||
|     "@certd/basic": "^1.33.7", | ||||
|     "@certd/pipeline": "^1.33.7", | ||||
|     "@kubernetes/client-node": "0.21.0", | ||||
|     "ali-oss": "^6.21.0", | ||||
|     "ali-oss": "^6.22.0", | ||||
|     "basic-ftp": "^5.0.5", | ||||
|     "cos-nodejs-sdk-v5": "^2.14.6", | ||||
|     "dayjs": "^1.11.7", | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { AliyunAccess } from "../access"; | ||||
| import { AliyunAccess } from "../access/index.js"; | ||||
| 
 | ||||
| export class AliossClient { | ||||
|   access: AliyunAccess; | ||||
|  | @ -61,4 +61,23 @@ export class AliossClient { | |||
|     await this.init(); | ||||
|     return await this.client.delete(filePath); | ||||
|   } | ||||
| 
 | ||||
|   async downloadFile(key: string, savePath: string) { | ||||
|     await this.init(); | ||||
|     return await this.client.get(key, savePath); | ||||
|   } | ||||
| 
 | ||||
|   async listDir(dirKey: string) { | ||||
|     await this.init(); | ||||
|     const res = await this.client.listV2({ | ||||
|       prefix: dirKey, | ||||
|       // max-keys: 100,
 | ||||
|       // continuation-token: "token",
 | ||||
|       // delimiter: "/",
 | ||||
|       // marker: "marker",
 | ||||
|       // encoding-type: "url",
 | ||||
|     }); | ||||
| 
 | ||||
|     return res.objects; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -44,4 +44,14 @@ export class FtpClient { | |||
|     this.logger.info(`开始删除文件${filePath}`); | ||||
|     await this.client.remove(filePath, true); | ||||
|   } | ||||
| 
 | ||||
|   async listDir(dir: string): Promise<any[]> { | ||||
|     this.logger.info(`开始列出目录${dir}`); | ||||
|     return await this.client.list(dir); | ||||
|   } | ||||
| 
 | ||||
|   async download(filePath: string, savePath: string): Promise<void> { | ||||
|     this.logger.info(`开始下载文件${filePath} -> ${savePath}`); | ||||
|     await this.client.downloadTo(savePath, filePath); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -5,3 +5,4 @@ export * from "./ftp/index.js"; | |||
| export * from "./tencent/index.js"; | ||||
| export * from "./qiniu/index.js"; | ||||
| export * from "./ctyun/index.js"; | ||||
| export * from "./oss/index.js"; | ||||
|  |  | |||
|  | @ -1,25 +0,0 @@ | |||
| import { AliyunAccess } from "../aliyun"; | ||||
| import { HttpClient, ILogger } from "@certd/basic"; | ||||
| import { IOssClient, OssClientDeleteReq } from "./api"; | ||||
| 
 | ||||
| export class AliossClient implements IOssClient { | ||||
|   access: AliyunAccess; | ||||
|   logger: ILogger; | ||||
|   http: HttpClient; | ||||
| 
 | ||||
|   constructor(opts: { access: AliyunAccess; http: HttpClient; logger: ILogger }) { | ||||
|     this.access = opts.access; | ||||
|     this.http = opts.http; | ||||
|     this.logger = opts.logger; | ||||
|   } | ||||
| 
 | ||||
|   upload(key: string, content: Buffer | string): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   download(key: string, savePath?: string): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   delete(opts: OssClientDeleteReq): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
| } | ||||
|  | @ -1,12 +1,89 @@ | |||
| export type OssClientDeleteReq = { | ||||
|   key: string; | ||||
| import { IAccessService } from "@certd/pipeline"; | ||||
| import { ILogger, utils } from "@certd/basic"; | ||||
| import dayjs from "dayjs"; | ||||
| 
 | ||||
| export type OssClientRemoveByOpts = { | ||||
|   dir?: string; | ||||
|   //删除多少天前的文件
 | ||||
|   beforeDays?: number; | ||||
| }; | ||||
| 
 | ||||
| export interface IOssClient { | ||||
|   upload(key: string, content: Buffer | string): Promise<void>; | ||||
| export type OssFileItem = { | ||||
|   name: string; | ||||
|   path: string; | ||||
|   size: number; | ||||
|   //毫秒时间戳
 | ||||
|   lastModified: number; | ||||
| }; | ||||
| 
 | ||||
|   download(key: string, savePath?: string): Promise<void>; | ||||
| export type IOssClient = { | ||||
|   upload: (fileName: string, fileContent: Buffer) => Promise<void>; | ||||
|   remove: (fileName: string) => Promise<void>; | ||||
| 
 | ||||
|   delete(opts: OssClientDeleteReq): Promise<void>; | ||||
|   download: (fileName: string, savePath: string) => Promise<void>; | ||||
| 
 | ||||
|   removeBy: (removeByOpts: OssClientRemoveByOpts) => Promise<void>; | ||||
| 
 | ||||
|   listDir: (dir: string) => Promise<OssFileItem[]>; | ||||
| }; | ||||
| 
 | ||||
| export type OssClientContext = { | ||||
|   accessService: IAccessService; | ||||
|   logger: ILogger; | ||||
|   utils: typeof utils; | ||||
| }; | ||||
| 
 | ||||
| export abstract class BaseOssClient<A> implements IOssClient { | ||||
|   rootDir: string = ""; | ||||
|   access: A = null; | ||||
|   logger: ILogger; | ||||
|   utils: typeof utils; | ||||
|   ctx: OssClientContext; | ||||
| 
 | ||||
|   protected constructor(opts: { rootDir?: string; access: A }) { | ||||
|     this.rootDir = opts.rootDir || ""; | ||||
|     this.access = opts.access; | ||||
|   } | ||||
| 
 | ||||
|   join(...strs: string[]) { | ||||
|     let res = ""; | ||||
|     for (const item of strs) { | ||||
|       if (item) { | ||||
|         if (!res) { | ||||
|           res = item; | ||||
|         } else { | ||||
|           res += "/" + item; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     res = res.replace(/[\\/]+/g, "/"); | ||||
|     return res; | ||||
|   } | ||||
| 
 | ||||
|   async setCtx(ctx: any) { | ||||
|     // set context
 | ||||
|     this.ctx = ctx; | ||||
|     this.logger = ctx.logger; | ||||
|     this.utils = ctx.utils; | ||||
|     await this.init(); | ||||
|   } | ||||
| 
 | ||||
|   async init() { | ||||
|     // do nothing
 | ||||
|   } | ||||
| 
 | ||||
|   abstract remove(fileName: string): Promise<void>; | ||||
|   abstract upload(fileName: string, fileContent: Buffer): Promise<void>; | ||||
|   abstract download(fileName: string, savePath: string): Promise<void>; | ||||
|   abstract listDir(dir: string): Promise<OssFileItem[]>; | ||||
| 
 | ||||
|   async removeBy(removeByOpts: OssClientRemoveByOpts): Promise<void> { | ||||
|     const list = await this.listDir(removeByOpts.dir); | ||||
|     const beforeDate = dayjs().subtract(removeByOpts.beforeDays, "day"); | ||||
|     for (const item of list) { | ||||
|       if (item.lastModified && item.lastModified < beforeDate.valueOf()) { | ||||
|         await this.remove(item.path); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,30 +1,33 @@ | |||
| import { HttpChallengeUploadContext } from "./api"; | ||||
| import { OssClientContext } from "./api"; | ||||
| 
 | ||||
| export class HttpChallengeUploaderFactory { | ||||
| export class OssClientFactory { | ||||
|   async getClassByType(type: string) { | ||||
|     if (type === "alioss") { | ||||
|       const module = await import("./impls/alioss.js"); | ||||
|       return module.AliossHttpChallengeUploader; | ||||
|       return module.default; | ||||
|     } else if (type === "ssh") { | ||||
|       const module = await import("./impls/ssh.js"); | ||||
|       return module.SshHttpChallengeUploader; | ||||
|       return module.default; | ||||
|     } else if (type === "sftp") { | ||||
|       const module = await import("./impls/sftp.js"); | ||||
|       return module.SftpHttpChallengeUploader; | ||||
|       return module.default; | ||||
|     } else if (type === "ftp") { | ||||
|       const module = await import("./impls/ftp.js"); | ||||
|       return module.FtpHttpChallengeUploader; | ||||
|       return module.default; | ||||
|     } else if (type === "tencentcos") { | ||||
|       const module = await import("./impls/tencentcos.js"); | ||||
|       return module.TencentCosHttpChallengeUploader; | ||||
|       return module.default; | ||||
|     } else if (type === "qiniuoss") { | ||||
|       const module = await import("./impls/qiniuoss.js"); | ||||
|       return module.QiniuOssHttpChallengeUploader; | ||||
|       return module.default; | ||||
|     } else if (type === "s3") { | ||||
|       const module = await import("./impls/s3.js"); | ||||
|       return module.default; | ||||
|     } else { | ||||
|       throw new Error(`暂不支持此文件上传方式: ${type}`); | ||||
|     } | ||||
|   } | ||||
|   async createUploaderByType(type: string, opts: { rootDir: string; access: any; ctx: HttpChallengeUploadContext }) { | ||||
|   async createOssClientByType(type: string, opts: { rootDir: string; access: any; ctx: OssClientContext }) { | ||||
|     const cls = await this.getClassByType(type); | ||||
|     if (cls) { | ||||
|       // @ts-ignore
 | ||||
|  | @ -35,4 +38,4 @@ export class HttpChallengeUploaderFactory { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| export const httpChallengeUploaderFactory = new HttpChallengeUploaderFactory(); | ||||
| export const ossClientFactory = new OssClientFactory(); | ||||
|  | @ -0,0 +1,54 @@ | |||
| import { BaseOssClient, OssFileItem } from "../api.js"; | ||||
| import { AliossAccess, AliossClient, AliyunAccess } from "../../aliyun/index.js"; | ||||
| import dayjs from "dayjs"; | ||||
| 
 | ||||
| export default class AliOssClientImpl extends BaseOssClient<AliossAccess> { | ||||
|   client: AliossClient; | ||||
|   join(...strs: string[]) { | ||||
|     const str = super.join(...strs); | ||||
|     if (str.startsWith("/")) { | ||||
|       return str.substring(1); | ||||
|     } | ||||
|     return str; | ||||
|   } | ||||
|   async init() { | ||||
|     const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId); | ||||
|     const client = new AliossClient({ | ||||
|       access: aliyunAccess, | ||||
|       bucket: this.access.bucket, | ||||
|       region: this.access.region, | ||||
|     }); | ||||
|     await client.init(); | ||||
|     this.client = client; | ||||
|   } | ||||
|   async download(filePath: string, savePath: string): Promise<void> { | ||||
|     const key = this.join(this.rootDir, filePath); | ||||
|     await this.client.downloadFile(key, savePath); | ||||
|   } | ||||
|   async listDir(dir: string): Promise<OssFileItem[]> { | ||||
|     const dirKey = this.join(this.rootDir, dir) + "/"; | ||||
|     const list = await this.client.listDir(dirKey); | ||||
|     this.logger.info(`列出目录: ${dirKey},文件数:${list.length}`); | ||||
|     return list.map(item => { | ||||
|       return { | ||||
|         path: item.name, | ||||
|         lastModified: dayjs(item.lastModified).valueOf(), | ||||
|         size: item.size, | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const key = this.join(this.rootDir, filePath); | ||||
|     this.logger.info(`开始上传文件: ${key}`); | ||||
|     await this.client.uploadFile(key, fileContent); | ||||
| 
 | ||||
|     this.logger.info(`文件上传成功: ${filePath}`); | ||||
|   } | ||||
| 
 | ||||
|   async remove(filePath: string) { | ||||
|     const key = this.join(this.rootDir, filePath); | ||||
|     // remove file from alioss
 | ||||
|     await this.client.removeFile(key); | ||||
|     this.logger.info(`文件删除成功: ${key}`); | ||||
|   } | ||||
| } | ||||
|  | @ -1,16 +1,17 @@ | |||
| import { BaseHttpChallengeUploader } from "../api.js"; | ||||
| import { FtpAccess, FtpClient } from "@certd/plugin-lib"; | ||||
| import { BaseOssClient } from "../api.js"; | ||||
| import path from "path"; | ||||
| import os from "os"; | ||||
| import fs from "fs"; | ||||
| import { FtpAccess, FtpClient } from "../../ftp/index.js"; | ||||
| 
 | ||||
| export class FtpHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAccess> { | ||||
| export default class FtpOssClientImpl extends BaseOssClient<FtpAccess> { | ||||
|   async download(fileName: string, savePath: string) {} | ||||
|   async listDir(dir: string) { | ||||
|     return []; | ||||
|   } | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const client = new FtpClient({ | ||||
|       access: this.access, | ||||
|       logger: this.logger, | ||||
|     }); | ||||
|     await client.connect(async (client) => { | ||||
|     const client = this.getFtpClient(); | ||||
|     await client.connect(async client => { | ||||
|       const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath); | ||||
|       const dir = path.dirname(tmpFilePath); | ||||
|       if (!fs.existsSync(dir)) { | ||||
|  | @ -19,7 +20,7 @@ export class FtpHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAcces | |||
|       fs.writeFileSync(tmpFilePath, fileContent); | ||||
|       try { | ||||
|         // Write file to temp path
 | ||||
|         const path = this.rootDir + filePath; | ||||
|         const path = this.join(this.rootDir, filePath); | ||||
|         await client.upload(path, tmpFilePath); | ||||
|       } finally { | ||||
|         // Remove temp file
 | ||||
|  | @ -28,13 +29,17 @@ export class FtpHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAcces | |||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async remove(filePath: string) { | ||||
|     const client = new FtpClient({ | ||||
|   private getFtpClient() { | ||||
|     return new FtpClient({ | ||||
|       access: this.access, | ||||
|       logger: this.logger, | ||||
|     }); | ||||
|     await client.connect(async (client) => { | ||||
|       const path = this.rootDir + filePath; | ||||
|   } | ||||
| 
 | ||||
|   async remove(filePath: string) { | ||||
|     const client = this.getFtpClient(); | ||||
|     await client.connect(async client => { | ||||
|       const path = this.join(this.rootDir, filePath); | ||||
|       await client.client.remove(path); | ||||
|     }); | ||||
|   } | ||||
|  | @ -0,0 +1,33 @@ | |||
| import { QiniuAccess, QiniuClient, QiniuOssAccess } from "../../qiniu/index.js"; | ||||
| import { BaseOssClient, OssClientRemoveByOpts, OssFileItem } from "../api.js"; | ||||
| 
 | ||||
| export default class QiniuOssClientImpl extends BaseOssClient<QiniuOssAccess> { | ||||
|   client: QiniuClient; | ||||
|   async init() { | ||||
|     const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId); | ||||
|     this.client = new QiniuClient({ | ||||
|       access: qiniuAccess, | ||||
|       logger: this.logger, | ||||
|       http: this.ctx.utils.http, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   download(fileName: string, savePath: string): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   removeBy(removeByOpts: OssClientRemoveByOpts): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   listDir(dir: string): Promise<OssFileItem[]> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const path = this.join(this.rootDir, filePath); | ||||
|     await this.client.uploadFile(this.access.bucket, path, fileContent); | ||||
|   } | ||||
| 
 | ||||
|   async remove(filePath: string) { | ||||
|     const path = this.join(this.rootDir, filePath); | ||||
|     await this.client.removeFile(this.access.bucket, path); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,55 @@ | |||
| import { BaseOssClient, OssClientRemoveByOpts, OssFileItem } from "../api.js"; | ||||
| import path from "node:path"; | ||||
| import { S3Access } from "../../s3/access.js"; | ||||
| export default class S3OssClientImpl extends BaseOssClient<S3Access> { | ||||
|   client: any; | ||||
| 
 | ||||
|   async init() { | ||||
|     // import { S3Client } from "@aws-sdk/client-s3";
 | ||||
|     const { S3Client } = await import("@aws-sdk/client-s3"); | ||||
|     this.client = new S3Client({ | ||||
|       forcePathStyle: true, | ||||
|       credentials: { | ||||
|         accessKeyId: this.access.accessKeyId, // 默认 MinIO 访问密钥
 | ||||
|         secretAccessKey: this.access.secretAccessKey, // 默认 MinIO 秘密密钥
 | ||||
|       }, | ||||
|       region: "us-east-1", | ||||
|       endpoint: this.access.endpoint, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   download(fileName: string, savePath: string): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   removeBy(removeByOpts: OssClientRemoveByOpts): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   listDir(dir: string): Promise<OssFileItem[]> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const { PutObjectCommand } = await import("@aws-sdk/client-s3"); | ||||
|     const key = path.join(this.rootDir, filePath); | ||||
|     this.logger.info(`开始上传文件: ${key}`); | ||||
|     const params = { | ||||
|       Bucket: this.access.bucket, // The name of the bucket. For example, 'sample_bucket_101'.
 | ||||
|       Key: key, // The name of the object. For example, 'sample_upload.txt'.
 | ||||
|     }; | ||||
|     await this.client.send(new PutObjectCommand({ Body: fileContent, ...params })); | ||||
| 
 | ||||
|     this.logger.info(`文件上传成功: ${filePath}`); | ||||
|   } | ||||
| 
 | ||||
|   async remove(filePath: string) { | ||||
|     const key = path.join(this.rootDir, filePath); | ||||
|     const { DeleteObjectCommand } = await import("@aws-sdk/client-s3"); | ||||
|     await this.client.send( | ||||
|       new DeleteObjectCommand({ | ||||
|         Bucket: this.access.bucket, | ||||
|         Key: key, | ||||
|       }) | ||||
|     ); | ||||
| 
 | ||||
|     this.logger.info(`文件删除成功: ${key}`); | ||||
|   } | ||||
| } | ||||
|  | @ -1,11 +1,19 @@ | |||
| import { BaseHttpChallengeUploader } from "../api.js"; | ||||
| import { SshAccess, SshClient } from "@certd/plugin-lib"; | ||||
| import { BaseOssClient, OssClientRemoveByOpts, OssFileItem } from "../api.js"; | ||||
| import path from "path"; | ||||
| import os from "os"; | ||||
| import fs from "fs"; | ||||
| import { SftpAccess } from "@certd/plugin-lib"; | ||||
| import { SftpAccess, SshAccess, SshClient } from "../../ssh/index.js"; | ||||
| 
 | ||||
| export class SftpHttpChallengeUploader extends BaseHttpChallengeUploader<SftpAccess> { | ||||
| export default class SftpOssClientImpl extends BaseOssClient<SftpAccess> { | ||||
|   download(fileName: string, savePath: string): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   removeBy(removeByOpts: OssClientRemoveByOpts): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   listDir(dir: string): Promise<OssFileItem[]> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath); | ||||
| 
 | ||||
|  | @ -1,10 +1,19 @@ | |||
| import { BaseHttpChallengeUploader } from "../api.js"; | ||||
| import { SshAccess, SshClient } from "@certd/plugin-lib"; | ||||
| import { BaseOssClient, OssClientRemoveByOpts, OssFileItem } from "../api.js"; | ||||
| import path from "path"; | ||||
| import os from "os"; | ||||
| import fs from "fs"; | ||||
| import { SshAccess, SshClient } from "../../ssh/index.js"; | ||||
| 
 | ||||
| export class SshHttpChallengeUploader extends BaseHttpChallengeUploader<SshAccess> { | ||||
| export default class SshOssClientImpl extends BaseOssClient<SshAccess> { | ||||
|   download(fileName: string, savePath: string): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   removeBy(removeByOpts: OssClientRemoveByOpts): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   listDir(dir: string): Promise<OssFileItem[]> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath); | ||||
| 
 | ||||
|  | @ -1,7 +1,16 @@ | |||
| import { BaseHttpChallengeUploader } from "../api.js"; | ||||
| import { TencentAccess, TencentCosAccess, TencentCosClient } from "@certd/plugin-lib"; | ||||
| import { TencentAccess, TencentCosAccess, TencentCosClient } from "../../tencent/index.js"; | ||||
| import { BaseOssClient, OssClientRemoveByOpts, OssFileItem } from "../api.js"; | ||||
| 
 | ||||
| export class TencentCosHttpChallengeUploader extends BaseHttpChallengeUploader<TencentCosAccess> { | ||||
| export default class TencentOssClientImpl extends BaseOssClient<TencentCosAccess> { | ||||
|   download(fileName: string, savePath: string): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   removeBy(removeByOpts: OssClientRemoveByOpts): Promise<void> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   listDir(dir: string): Promise<OssFileItem[]> { | ||||
|     throw new Error("Method not implemented."); | ||||
|   } | ||||
|   async upload(filePath: string, fileContent: Buffer) { | ||||
|     const access = await this.ctx.accessService.getById<TencentAccess>(this.access.accessId); | ||||
|     const client = new TencentCosClient({ | ||||
|  | @ -0,0 +1,2 @@ | |||
| export * from "./factory.js"; | ||||
| export * from "./api.js"; | ||||
|  | @ -0,0 +1,87 @@ | |||
| import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline"; | ||||
| 
 | ||||
| /** | ||||
|  * 这个注解将注册一个授权配置 | ||||
|  * 在certd的后台管理系统中,用户可以选择添加此类型的授权 | ||||
|  */ | ||||
| @IsAccess({ | ||||
|   name: "s3", | ||||
|   title: "s3/minio授权", | ||||
|   desc: "S3/minio oss授权", | ||||
|   icon: "mdi:folder-upload-outline", | ||||
| }) | ||||
| export class S3Access extends BaseAccess { | ||||
|   @AccessInput({ | ||||
|     title: "endpoint", | ||||
|     component: { | ||||
|       placeholder: "http://xxxxxx:9000", | ||||
|       name: "a-input", | ||||
|       vModel: "value", | ||||
|     }, | ||||
|     helper: "Minio的地址,如果是aws s3 则无需填写", | ||||
|     required: false, | ||||
|   }) | ||||
|   endpoint!: string; | ||||
| 
 | ||||
|   /** | ||||
|    * const minioClient = new S3Client({ | ||||
|    *   endpoint: "http://localhost:9000", | ||||
|    *   forcePathStyle: true, | ||||
|    *   credentials: { | ||||
|    *     accessKeyId: "minioadmin", // 默认 MinIO 访问密钥
 | ||||
|    *     secretAccessKey: "minioadmin", // 默认 MinIO 秘密密钥
 | ||||
|    *   }, | ||||
|    *   region: "us-east-1", | ||||
|    * }); | ||||
|    */ | ||||
| 
 | ||||
|   @AccessInput({ | ||||
|     title: "accessKeyId", | ||||
|     component: { | ||||
|       placeholder: "accessKeyId", | ||||
|     }, | ||||
|     helper: "accessKeyId", | ||||
|     required: true, | ||||
|   }) | ||||
|   accessKeyId!: string; | ||||
| 
 | ||||
|   @AccessInput({ | ||||
|     title: "secretAccessKey", | ||||
|     component: { | ||||
|       placeholder: "secretAccessKey", | ||||
|       component: { | ||||
|         name: "a-input", | ||||
|         vModel: "value", | ||||
|       }, | ||||
|     }, | ||||
|     helper: "secretAccessKey", | ||||
|     encrypt: true, | ||||
|     required: true, | ||||
|   }) | ||||
|   secretAccessKey!: string; | ||||
| 
 | ||||
|   @AccessInput({ | ||||
|     title: "地区", | ||||
|     value: "us-east-1", | ||||
|     component: { | ||||
|       name: "a-input", | ||||
|       vModel: "value", | ||||
|     }, | ||||
|     helper: "region", | ||||
|     required: true, | ||||
|   }) | ||||
|   region!: string; | ||||
| 
 | ||||
|   @AccessInput({ | ||||
|     title: "存储桶", | ||||
|     component: { | ||||
|       name: "a-input", | ||||
|       vModel: "value", | ||||
|     }, | ||||
|     helper: "bucket 名称", | ||||
|     required: true, | ||||
|   }) | ||||
|   bucket!: string; | ||||
| } | ||||
| 
 | ||||
| new S3Access(); | ||||
|  | @ -75,6 +75,7 @@ const uploaderTypeDict = dict({ | |||
|     { label: "阿里云OSS", value: "alioss" }, | ||||
|     { label: "腾讯云COS", value: "tencentcos" }, | ||||
|     { label: "七牛OSS", value: "qiniuoss" }, | ||||
|     { label: "S3/Minio", value: "s3" }, | ||||
|     { label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true }, | ||||
|   ], | ||||
| }); | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import {IsTaskPlugin, pluginGroups, RunStrategy, TaskInput} from '@certd/pipeline'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import dayjs from 'dayjs'; | ||||
| import {AbstractPlusTaskPlugin} from '@certd/plugin-plus'; | ||||
| import JSZip from 'jszip'; | ||||
| import * as os from 'node:os'; | ||||
| import {SshAccess, SshClient} from '@certd/plugin-lib'; | ||||
| import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import dayjs from "dayjs"; | ||||
| import { AbstractPlusTaskPlugin } from "@certd/plugin-plus"; | ||||
| import JSZip from "jszip"; | ||||
| import * as os from "node:os"; | ||||
| import { OssClientContext, ossClientFactory, OssClientRemoveByOpts, SshAccess, SshClient } from "@certd/plugin-lib"; | ||||
| 
 | ||||
| const defaultBackupDir = 'certd_backup'; | ||||
| const defaultFilePrefix = 'db-backup'; | ||||
|  | @ -63,12 +63,14 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin { | |||
|   @TaskInput({ | ||||
|     title: 'OSS类型', | ||||
|     component: { | ||||
|       name: 'a-input', | ||||
|       name: 'a-select', | ||||
|       options: [ | ||||
|         {value: "aliyun", label: "阿里云OSS"}, | ||||
|         {value: "alioss", label: "阿里云OSS"}, | ||||
|         {value: "s3", label: "MinIO/S3"}, | ||||
|         {value: "qiniu", label: "七牛云"}, | ||||
|         {value: "tencent", label: "腾讯云COS"} | ||||
|         {value: "qiniuoss", label: "七牛云"}, | ||||
|         {value: "tencentcos", label: "腾讯云COS"}, | ||||
|         {value: "ftp", label: "Ftp"}, | ||||
|         {value: "sftp", label: "Sftp"}, | ||||
|       ] | ||||
|     }, | ||||
|     mergeScript: ` | ||||
|  | @ -90,7 +92,7 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin { | |||
|     mergeScript: ` | ||||
|       return { | ||||
|         show:ctx.compute(({form})=>{ | ||||
|           return form.backupMode === 'ssh'; | ||||
|           return form.backupMode === 'oss'; | ||||
|         }), | ||||
|         component:{ | ||||
|           type: ctx.compute(({form})=>{ | ||||
|  | @ -270,7 +272,38 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin { | |||
|   } | ||||
| 
 | ||||
|   private async ossBackup(dbPath: string, backupDir: string, backupPath: string) { | ||||
|     // TODO
 | ||||
|     if (!this.ossAccessId) { | ||||
|         throw new Error('未配置ossAccessId'); | ||||
|     } | ||||
|     const access = await this.getAccess(this.ossAccessId); | ||||
|     const ossType = this.ossType | ||||
| 
 | ||||
|     const ctx: OssClientContext = { | ||||
|       logger: this.logger, | ||||
|       utils: this.ctx.utils, | ||||
|       accessService:this.accessService | ||||
|     } | ||||
| 
 | ||||
|     this.logger.info(`开始备份文件到:${ossType}`); | ||||
|     const client = await  ossClientFactory.createOssClientByType(ossType, { | ||||
|       access, | ||||
|       ctx, | ||||
|     }) | ||||
| 
 | ||||
|     await client.upload(backupPath, dbPath); | ||||
| 
 | ||||
|     if (this.retainDays > 0) { | ||||
|       // 删除过期备份
 | ||||
|       this.logger.info('开始删除过期备份文件'); | ||||
|       const removeByOpts: OssClientRemoveByOpts = { | ||||
|         dir: backupDir, | ||||
|         beforeDays: this.retainDays, | ||||
|       }; | ||||
|       await client.removeBy(removeByOpts); | ||||
|       this.logger.info('删除过期备份文件完成'); | ||||
|     }else{ | ||||
|       this.logger.info('已禁止删除过期文件'); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 xiaojunnuo
						xiaojunnuo