import { StorageClient, PutObjectOptions, HeadObjectResult } from '../../core/StorageClient'; import { HuaweiObsConfig } from '../../config/types'; // 华为 OBS Node.js SDK(CommonJS) const ObsClient = require('esdk-obs-nodejs'); /** * 华为 OBS 的 StorageClient 实现(Adapter)。 * 通过 SDK 的 getObjectMetadata 查元数据判断是否存在,putObject 上传对象。 */ export class HuaweiObsClient implements StorageClient { private readonly client: InstanceType; private readonly whiteList: string[]; constructor(private readonly config: HuaweiObsConfig) { this.client = new ObsClient({ access_key_id: config.accessKey, secret_access_key: config.secretKey, server: config.endpoint, }); this.whiteList = JSON.parse(process.env.FILE_RE_WHITE_LIST as string) } /** * 按前缀列举对象 key(分页拉全),用于上传前一次拉取、内存判断存在,避免每个文件都 head。 */ async listObjectKeys(bucket: string, prefix: string): Promise { const keys: string[] = []; let marker: string | undefined; const pageSize = 1000; do { const param: Record = { Bucket: bucket, MaxKeys: pageSize, Prefix: prefix || undefined, }; if (marker) param.Marker = marker; const result = await this.client.listObjects(param); if (result.CommonMsg.Status > 300) { throw new Error( `OBS listObjects 失败: ${result.CommonMsg.Status} ${result.CommonMsg.Code} ${result.CommonMsg.Message}`, ); } const contents = result.InterfaceResult?.Contents ?? []; for (const item of contents) { if (item.Key) keys.push(item.Key); } const truncated = result.InterfaceResult?.IsTruncated === 'true'; marker = truncated ? result.InterfaceResult?.NextMarker : undefined; } while (marker); return keys; } /** * 调用 OBS getObjectMetadata 查元数据,判断对象是否存在(同路径即跳过上传)。 * 当使用 listObjectKeys 时,上传流程会优先用内存 Set 判断,不再逐文件调用本方法。 */ async headObject(bucket: string, key: string): Promise { try { if(this.whiteList.some(item => key.includes(item))) { return { exists: false }; } const result = await this.client.getObjectMetadata({ Bucket: bucket, Key: key, }); if (result.CommonMsg.Status <= 300) { return { exists: true }; } // 对象不存在 if (result.CommonMsg.Status === 404 || result.CommonMsg.Code === 'NoSuchKey' || result.CommonMsg.Code === 'NotFound') { return { exists: false }; } throw new Error(`OBS getObjectMetadata 失败: ${result.CommonMsg.Status} ${result.CommonMsg.Code} ${result.CommonMsg.Message}`); } catch (err: unknown) { const msg = err && typeof (err as any).message === 'string' ? (err as Error).message : String(err); const code = (err as any)?.Code ?? (err as any)?.code; if (code === 'NoSuchKey' || code === 'NotFound' || msg.includes('404') || msg.includes('NoSuchKey')) { return { exists: false }; } throw err; } } /** 调用 OBS putObject 上传对象(支持 Body 或 SourceFile 本地路径,SourceFile 由 SDK 直接读文件避免 Buffer 问题) */ async putObject(options: PutObjectOptions): Promise { const { bucket, key, contentType } = options; const param: Record = { Bucket: bucket, Key: key, ContentType: contentType, }; if (options.sourceFile) { param.SourceFile = options.sourceFile; } else if (options.body) { param.Body = options.body; } else { throw new Error('putObject 需要 body 或 sourceFile'); } const result = await this.client.putObject(param); if (result.CommonMsg.Status > 300) { const { Status, Code, Message, RequestId } = result.CommonMsg; const detail = [Status, Code, Message, RequestId].filter(Boolean).join(' '); throw new Error( `OBS putObject 失败 (${detail || '无详情'}). ` + '请检查: 1) 桶名是否正确且已创建 2) OBS_ENDPOINT 是否与该桶所在区域一致,如 https://obs.xx-xx-1.myhuaweicloud.com' ); } } }