@dd-code/oss-uploader 0.1.0 → 0.1.2

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.
@@ -10,25 +10,59 @@ const ObsClient = require('esdk-obs-nodejs');
10
10
  */
11
11
  export class HuaweiObsClient implements StorageClient {
12
12
  private readonly client: InstanceType<typeof ObsClient>;
13
-
13
+ private readonly whiteList: string[];
14
14
  constructor(private readonly config: HuaweiObsConfig) {
15
15
  this.client = new ObsClient({
16
16
  access_key_id: config.accessKey,
17
17
  secret_access_key: config.secretKey,
18
18
  server: config.endpoint,
19
19
  });
20
+ this.whiteList = JSON.parse(process.env.FILE_RE_WHITE_LIST as string)
21
+ }
22
+
23
+ /**
24
+ * 按前缀列举对象 key(分页拉全),用于上传前一次拉取、内存判断存在,避免每个文件都 head。
25
+ */
26
+ async listObjectKeys(bucket: string, prefix: string): Promise<string[]> {
27
+ const keys: string[] = [];
28
+ let marker: string | undefined;
29
+ const pageSize = 1000;
30
+ do {
31
+ const param: Record<string, unknown> = {
32
+ Bucket: bucket,
33
+ MaxKeys: pageSize,
34
+ Prefix: prefix || undefined,
35
+ };
36
+ if (marker) param.Marker = marker;
37
+ const result = await this.client.listObjects(param);
38
+ if (result.CommonMsg.Status > 300) {
39
+ throw new Error(
40
+ `OBS listObjects 失败: ${result.CommonMsg.Status} ${result.CommonMsg.Code} ${result.CommonMsg.Message}`,
41
+ );
42
+ }
43
+ const contents = result.InterfaceResult?.Contents ?? [];
44
+ for (const item of contents) {
45
+ if (item.Key) keys.push(item.Key);
46
+ }
47
+ const truncated = result.InterfaceResult?.IsTruncated === 'true';
48
+ marker = truncated ? result.InterfaceResult?.NextMarker : undefined;
49
+ } while (marker);
50
+ return keys;
20
51
  }
21
52
 
22
53
  /**
23
54
  * 调用 OBS getObjectMetadata 查元数据,判断对象是否存在(同路径即跳过上传)。
55
+ * 当使用 listObjectKeys 时,上传流程会优先用内存 Set 判断,不再逐文件调用本方法。
24
56
  */
25
57
  async headObject(bucket: string, key: string): Promise<HeadObjectResult> {
26
58
  try {
59
+ if(this.whiteList.some(item => key.includes(item))) {
60
+ return { exists: false };
61
+ }
27
62
  const result = await this.client.getObjectMetadata({
28
63
  Bucket: bucket,
29
64
  Key: key,
30
65
  });
31
-
32
66
  if (result.CommonMsg.Status <= 300) {
33
67
  return { exists: true };
34
68
  }
@@ -49,16 +83,23 @@ export class HuaweiObsClient implements StorageClient {
49
83
  }
50
84
  }
51
85
 
52
- /** 调用 OBS putObject 上传对象 */
86
+ /** 调用 OBS putObject 上传对象(支持 Body 或 SourceFile 本地路径,SourceFile 由 SDK 直接读文件避免 Buffer 问题) */
53
87
  async putObject(options: PutObjectOptions): Promise<void> {
54
- const { bucket, key, body, contentType } = options;
55
- // console.log('putObject', { bucket, key, endpoint: this.config.endpoint, accessKey: this.config.accessKey, secretKey: this.config.secretKey })
56
- const result = await this.client.putObject({
88
+ const { bucket, key, contentType } = options;
89
+ const param: Record<string, unknown> = {
57
90
  Bucket: bucket,
58
91
  Key: key,
59
- Body: body,
60
- // ContentType: contentType,
61
- });
92
+ ContentType: contentType,
93
+ };
94
+ if (options.sourceFile) {
95
+ param.SourceFile = options.sourceFile;
96
+ } else if (options.body) {
97
+ param.Body = options.body;
98
+ } else {
99
+ throw new Error('putObject 需要 body 或 sourceFile');
100
+ }
101
+
102
+ const result = await this.client.putObject(param);
62
103
 
63
104
  if (result.CommonMsg.Status > 300) {
64
105
  const { Status, Code, Message, RequestId } = result.CommonMsg;
@@ -1,119 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { Reporter, UploadSummary, UploadFileResult } from './reporter';
4
- import { StorageClient } from './StorageClient';
5
- import { shouldInclude } from './filters';
6
- import type { FileProcessor } from './fileProcessor';
7
-
8
- /** 上传流程的上下文:环境、bucket、前缀、本地目录、存储客户端、Reporter、CDN 根地址、上传前处理器等 */
9
- export interface UploadContext {
10
- env?: string;
11
- bucket: string;
12
- basePrefix: string;
13
- pathPrefix?: string;
14
- localDir: string;
15
- include?: string[];
16
- exclude?: string[];
17
- storageClient: StorageClient;
18
- reporter?: Reporter;
19
- /** CDN 加速根地址,配置后可为每个文件生成 accessUrl */
20
- cdnBaseUrl?: string;
21
- /** 上传前对文件内容的处理器(如图片压缩),未配置则直接上传原内容 */
22
- fileProcessor?: FileProcessor;
23
- }
24
-
25
- /** 本地文件信息:绝对路径与相对路径 */
26
- export interface LocalFile {
27
- absolutePath: string;
28
- relativePath: string;
29
- }
30
-
31
- /**
32
- * 上传流程模板(Template Method)。
33
- * 定义固定步骤:onStart -> collectFiles -> filterFiles -> uploadFiles -> onComplete,
34
- * 子类实现 collectFiles 与 uploadFiles,其余步骤可复用或重写。
35
- */
36
- export abstract class UploadFlowTemplate {
37
- protected constructor(protected readonly context: UploadContext) {}
38
-
39
- /** 执行完整上传流程 */
40
- async execute(): Promise<UploadSummary> {
41
- const { reporter } = this.context;
42
-
43
- if (reporter?.onStart) {
44
- await reporter.onStart({
45
- env: this.context.env,
46
- bucket: this.context.bucket,
47
- basePrefix: this.context.basePrefix,
48
- pathPrefix: this.context.pathPrefix,
49
- localDir: this.context.localDir,
50
- cdnBaseUrl: this.context.cdnBaseUrl,
51
- });
52
- }
53
-
54
- const files = await this.collectFiles();
55
- const filtered = this.filterFiles(files);
56
- const summary = await this.uploadFiles(filtered);
57
-
58
- if (reporter?.onComplete) {
59
- await reporter.onComplete(summary);
60
- }
61
-
62
- return summary;
63
- }
64
-
65
- protected abstract collectFiles(): Promise<LocalFile[]>;
66
- protected abstract uploadFiles(files: LocalFile[]): Promise<UploadSummary>;
67
-
68
- /** 使用 include/exclude 过滤文件列表 */
69
- protected filterFiles(files: LocalFile[]): LocalFile[] {
70
- const { include, exclude } = this.context;
71
- return files.filter((f) => shouldInclude(f.relativePath, include, exclude));
72
- }
73
-
74
- /**
75
- * 构造远端 key:basePrefix / env / pathPrefix / relativePath。
76
- * 保持目录结构,且 base 前缀来自 config,不对外暴露。
77
- */
78
- protected buildKey(relativePath: string): string {
79
- const parts: string[] = [];
80
- if (this.context.basePrefix) parts.push(this.context.basePrefix);
81
- if (this.context.env) parts.push(this.context.env);
82
- if (this.context.pathPrefix) parts.push(this.context.pathPrefix);
83
-
84
- parts.push(relativePath.replace(/\\/g, '/'));
85
- return parts.join('/');
86
- }
87
-
88
- /** 递归遍历目录,收集所有文件的绝对路径与相对路径 */
89
- protected async walkDir(rootDir: string, currentDir = ''): Promise<LocalFile[]> {
90
- const dirPath = path.join(rootDir, currentDir);
91
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
92
- const files: LocalFile[] = [];
93
-
94
- for (const entry of entries) {
95
- const rel = path.join(currentDir, entry.name);
96
- const abs = path.join(rootDir, rel);
97
-
98
- if (entry.isDirectory()) {
99
- files.push(...(await this.walkDir(rootDir, rel)));
100
- } else if (entry.isFile()) {
101
- files.push({
102
- absolutePath: abs,
103
- relativePath: rel.replace(/\\/g, '/'),
104
- });
105
- }
106
- }
107
-
108
- return files;
109
- }
110
-
111
- /** 将单个文件的上传结果交给 Reporter */
112
- protected async reportFileResult(result: UploadFileResult): Promise<void> {
113
- const { reporter } = this.context;
114
- if (reporter?.onFileResult) {
115
- await reporter.onFileResult(result);
116
- }
117
- }
118
- }
119
-