@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.
- package/.env +2 -0
- package/dist/cli.cjs +1 -1
- package/dist/core/DirectoryUploadFlow.d.ts +5 -13
- package/dist/core/DirectoryUploadFlow.d.ts.map +1 -1
- package/dist/core/StorageClient.d.ts +7 -2
- package/dist/core/StorageClient.d.ts.map +1 -1
- package/dist/core/UploadFlowTemplate.d.ts +14 -10
- package/dist/core/UploadFlowTemplate.d.ts.map +1 -1
- package/dist/core/UploadService.d.ts +42 -0
- package/dist/core/UploadService.d.ts.map +1 -0
- package/dist/core/reporter.d.ts +9 -5
- package/dist/core/reporter.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +23 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/middleware/OssService.d.ts +10 -17
- package/dist/middleware/OssService.d.ts.map +1 -1
- package/dist/providers/huawei/HuaweiObsClient.d.ts +7 -1
- package/dist/providers/huawei/HuaweiObsClient.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/cli.ts +4 -10
- package/src/core/DirectoryUploadFlow.ts +12 -91
- package/src/core/StorageClient.ts +7 -2
- package/src/core/UploadService.ts +239 -0
- package/src/core/reporter.ts +35 -25
- package/src/index.ts +28 -59
- package/src/middleware/OssService.ts +51 -39
- package/src/providers/huawei/HuaweiObsClient.ts +50 -9
- package/src/core/UploadFlowTemplate.ts +0 -119
|
@@ -1,96 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { UploadFlowTemplate, UploadContext, LocalFile } from './UploadFlowTemplate';
|
|
3
|
-
import { UploadSummary } from './reporter';
|
|
4
|
-
import { buildCdnAccessUrl } from './urlHelper';
|
|
1
|
+
import { UploadService, type UploadDirectoryOptions } from './UploadService';
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 目录上传的具体实现:继承 UploadService,按目录遍历上传,云端已存在则跳过。
|
|
5
|
+
* 未覆盖任何方法时行为与基类一致;子类可覆盖 collectFiles、uploadOneFile 等。
|
|
9
6
|
*/
|
|
10
|
-
export class DirectoryUploadFlow extends
|
|
11
|
-
constructor(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 逐个文件上传:先 head 判断是否存在,存在则跳过,否则读取本地文件并 putObject。
|
|
22
|
-
* 每个文件的结果通过 reportFileResult 交给 Reporter。
|
|
23
|
-
*/
|
|
24
|
-
protected async uploadFiles(files: LocalFile[]): Promise<UploadSummary> {
|
|
25
|
-
const { bucket, storageClient, cdnBaseUrl } = this.context;
|
|
26
|
-
|
|
27
|
-
const summary: UploadSummary = {
|
|
28
|
-
total: files.length,
|
|
29
|
-
success: 0,
|
|
30
|
-
failed: 0,
|
|
31
|
-
skipped: 0,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
for (const file of files) {
|
|
35
|
-
const key = this.buildKey(file.relativePath);
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const head = await storageClient.headObject(bucket, key);
|
|
39
|
-
|
|
40
|
-
// 云端已有同名对象则跳过,不比较内容
|
|
41
|
-
if (head.exists) {
|
|
42
|
-
summary.skipped += 1;
|
|
43
|
-
await this.reportFileResult({
|
|
44
|
-
localPath: file.absolutePath,
|
|
45
|
-
relativePath: file.relativePath,
|
|
46
|
-
bucket,
|
|
47
|
-
key,
|
|
48
|
-
status: 'skipped',
|
|
49
|
-
...(cdnBaseUrl && { accessUrl: buildCdnAccessUrl(cdnBaseUrl, key) }),
|
|
50
|
-
});
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const data = await fs.readFile(file.absolutePath);
|
|
55
|
-
|
|
56
|
-
const { buffer, contentType } = this.context.fileProcessor
|
|
57
|
-
? await this.context.fileProcessor.process({
|
|
58
|
-
localPath: file.absolutePath,
|
|
59
|
-
relativePath: file.relativePath,
|
|
60
|
-
buffer: data,
|
|
61
|
-
})
|
|
62
|
-
: { buffer: data, contentType: undefined as string | undefined };
|
|
63
|
-
|
|
64
|
-
await storageClient.putObject({
|
|
65
|
-
bucket,
|
|
66
|
-
key,
|
|
67
|
-
body: buffer,
|
|
68
|
-
contentType,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
summary.success += 1;
|
|
72
|
-
await this.reportFileResult({
|
|
73
|
-
localPath: file.absolutePath,
|
|
74
|
-
relativePath: file.relativePath,
|
|
75
|
-
bucket,
|
|
76
|
-
key,
|
|
77
|
-
status: 'success',
|
|
78
|
-
...(cdnBaseUrl && { accessUrl: buildCdnAccessUrl(cdnBaseUrl, key) }),
|
|
79
|
-
});
|
|
80
|
-
} catch (err: any) {
|
|
81
|
-
summary.failed += 1;
|
|
82
|
-
await this.reportFileResult({
|
|
83
|
-
localPath: file.absolutePath,
|
|
84
|
-
relativePath: file.relativePath,
|
|
85
|
-
bucket,
|
|
86
|
-
key,
|
|
87
|
-
status: 'failed',
|
|
88
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return summary;
|
|
7
|
+
export class DirectoryUploadFlow extends UploadService {
|
|
8
|
+
constructor(
|
|
9
|
+
storageClient: UploadService['storageClient'],
|
|
10
|
+
basePrefix: string,
|
|
11
|
+
cdnBaseUrl?: string,
|
|
12
|
+
reporter?: UploadService['reporter'],
|
|
13
|
+
fileProcessor?: UploadService['fileProcessor'],
|
|
14
|
+
) {
|
|
15
|
+
super(storageClient, basePrefix, cdnBaseUrl, reporter, fileProcessor);
|
|
94
16
|
}
|
|
95
17
|
}
|
|
96
|
-
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
* 实现类负责将 putObject/headObject 翻译为具体云厂商的 API 调用。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/** 上传单个对象时的参数(body 与 sourceFile 二选一:sourceFile 为本地路径时由 SDK 直接读文件上传,避免先读成 Buffer) */
|
|
7
7
|
export interface PutObjectOptions {
|
|
8
8
|
bucket: string;
|
|
9
9
|
key: string;
|
|
10
|
-
|
|
10
|
+
/** 文件内容,与 sourceFile 二选一 */
|
|
11
|
+
body?: Buffer | NodeJS.ReadableStream;
|
|
12
|
+
/** 本地文件路径,与 body 二选一;设置时由实现方用 SDK 的 SourceFile 等方式直接上传 */
|
|
13
|
+
sourceFile?: string;
|
|
11
14
|
contentType?: string;
|
|
12
15
|
headers?: Record<string, string>;
|
|
13
16
|
}
|
|
@@ -21,5 +24,7 @@ export interface HeadObjectResult {
|
|
|
21
24
|
export interface StorageClient {
|
|
22
25
|
putObject(options: PutObjectOptions): Promise<void>;
|
|
23
26
|
headObject(bucket: string, key: string): Promise<HeadObjectResult>;
|
|
27
|
+
/** 可选:按前缀列举 key,用于一次拉取后内存判断存在,避免每个文件都 head */
|
|
28
|
+
listObjectKeys?(bucket: string, prefix: string): Promise<string[]>;
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -0,0 +1,239 @@
|
|
|
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 { buildCdnAccessUrl } from './urlHelper';
|
|
7
|
+
import type { FileProcessor } from './fileProcessor';
|
|
8
|
+
|
|
9
|
+
/** 常见扩展名 -> Content-Type,避免图片等以错误类型上传导致损坏或无法预览 */
|
|
10
|
+
const EXT_TO_MIME: Record<string, string> = {
|
|
11
|
+
png: 'image/png',
|
|
12
|
+
jpg: 'image/jpeg',
|
|
13
|
+
jpeg: 'image/jpeg',
|
|
14
|
+
gif: 'image/gif',
|
|
15
|
+
webp: 'image/webp',
|
|
16
|
+
svg: 'image/svg+xml',
|
|
17
|
+
ico: 'image/x-icon',
|
|
18
|
+
bmp: 'image/bmp',
|
|
19
|
+
tiff: 'image/tiff',
|
|
20
|
+
tif: 'image/tiff',
|
|
21
|
+
js: 'application/javascript',
|
|
22
|
+
mjs: 'application/javascript',
|
|
23
|
+
json: 'application/json',
|
|
24
|
+
css: 'text/css',
|
|
25
|
+
html: 'text/html',
|
|
26
|
+
htm: 'text/html',
|
|
27
|
+
txt: 'text/plain',
|
|
28
|
+
woff: 'font/woff',
|
|
29
|
+
woff2: 'font/woff2',
|
|
30
|
+
ttf: 'font/ttf',
|
|
31
|
+
eot: 'application/vnd.ms-fontobject',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function getContentType(relativePath: string): string | undefined {
|
|
35
|
+
const ext = path.extname(relativePath).slice(1).toLowerCase();
|
|
36
|
+
return ext ? EXT_TO_MIME[ext] : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 单次上传的目录参数 */
|
|
40
|
+
export interface UploadDirectoryOptions {
|
|
41
|
+
bucket: string;
|
|
42
|
+
localDir: string;
|
|
43
|
+
env?: string;
|
|
44
|
+
pathPrefix?: string;
|
|
45
|
+
include?: string[];
|
|
46
|
+
exclude?: string[];
|
|
47
|
+
/** 匹配到的 key 强制上传(不跳过),如从 FILE_RE_WHITE_LIST 解析的列表 */
|
|
48
|
+
forceUploadPatterns?: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LocalFile {
|
|
52
|
+
absolutePath: string;
|
|
53
|
+
relativePath: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 上传流程基类:封装「收集文件 → 过滤 → 逐个上传 → 上报」。
|
|
58
|
+
* 子类可覆盖 collectFiles、uploadOneFile 等扩展行为。
|
|
59
|
+
*/
|
|
60
|
+
export abstract class UploadService {
|
|
61
|
+
protected constructor(
|
|
62
|
+
protected readonly storageClient: StorageClient,
|
|
63
|
+
protected readonly basePrefix: string,
|
|
64
|
+
protected readonly cdnBaseUrl?: string,
|
|
65
|
+
protected readonly reporter?: Reporter,
|
|
66
|
+
protected readonly fileProcessor?: FileProcessor,
|
|
67
|
+
) {}
|
|
68
|
+
|
|
69
|
+
async uploadDirectory(options: UploadDirectoryOptions): Promise<UploadSummary> {
|
|
70
|
+
const files = await this.collectFiles(
|
|
71
|
+
options.localDir,
|
|
72
|
+
options.include,
|
|
73
|
+
options.exclude,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const summary: UploadSummary = {
|
|
77
|
+
total: files.length,
|
|
78
|
+
success: [],
|
|
79
|
+
failed: [],
|
|
80
|
+
skipped: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// 若有 listObjectKeys:一次拉取前缀下已有 key,用 Set 判断存在,避免每个文件都 head
|
|
84
|
+
const prefix = this.buildKey(this.basePrefix, options.env, options.pathPrefix, '');
|
|
85
|
+
let existingKeysSet: Set<string> | undefined;
|
|
86
|
+
if (typeof this.storageClient.listObjectKeys === 'function') {
|
|
87
|
+
existingKeysSet = new Set(await this.storageClient.listObjectKeys(options.bucket, prefix));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.reporter?.onStart) {
|
|
91
|
+
await this.reporter.onStart({
|
|
92
|
+
env: options.env,
|
|
93
|
+
bucket: options.bucket,
|
|
94
|
+
basePrefix: this.basePrefix,
|
|
95
|
+
pathPrefix: options.pathPrefix,
|
|
96
|
+
localDir: options.localDir,
|
|
97
|
+
cdnBaseUrl: this.cdnBaseUrl,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const result = await this.uploadOneFile(file, options, existingKeysSet);
|
|
103
|
+
summary[result.status].push(result.key);
|
|
104
|
+
const current = summary.success.length + summary.failed.length + summary.skipped.length;
|
|
105
|
+
if (this.reporter?.onProgress) {
|
|
106
|
+
await this.reporter.onProgress(current, files.length);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.reporter?.onComplete) {
|
|
111
|
+
await this.reporter.onComplete(summary);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return summary;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 收集并过滤要上传的文件,子类可覆盖 */
|
|
118
|
+
protected async collectFiles(
|
|
119
|
+
localDir: string,
|
|
120
|
+
include?: string[],
|
|
121
|
+
exclude?: string[],
|
|
122
|
+
): Promise<LocalFile[]> {
|
|
123
|
+
const files = await this.walkDir(localDir);
|
|
124
|
+
return files.filter((f) => shouldInclude(f.relativePath, include, exclude));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 上传单个文件:有 existingKeysSet 时用 Set 判断存在,否则 head;存在则跳过,否则 put;子类可覆盖 */
|
|
128
|
+
protected async uploadOneFile(
|
|
129
|
+
file: LocalFile,
|
|
130
|
+
options: UploadDirectoryOptions,
|
|
131
|
+
existingKeysSet?: Set<string>,
|
|
132
|
+
): Promise<{ status: 'success' | 'skipped' | 'failed'; key: string }> {
|
|
133
|
+
const key = this.buildKey(
|
|
134
|
+
this.basePrefix,
|
|
135
|
+
options.env,
|
|
136
|
+
options.pathPrefix,
|
|
137
|
+
file.relativePath,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const forceUpload = options.forceUploadPatterns?.some((p) => key.includes(p));
|
|
142
|
+
if (existingKeysSet !== undefined) {
|
|
143
|
+
if (!forceUpload && existingKeysSet.has(key)) {
|
|
144
|
+
await this.reportFile(file, options.bucket, key, 'skipped');
|
|
145
|
+
return { status: 'skipped', key };
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
const head = await this.storageClient.headObject(options.bucket, key);
|
|
149
|
+
if (head.exists) {
|
|
150
|
+
await this.reportFile(file, options.bucket, key, 'skipped');
|
|
151
|
+
return { status: 'skipped', key };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this.fileProcessor) {
|
|
156
|
+
const data = await fs.readFile(file.absolutePath);
|
|
157
|
+
const { buffer, contentType } = await this.fileProcessor.process({
|
|
158
|
+
localPath: file.absolutePath,
|
|
159
|
+
relativePath: file.relativePath,
|
|
160
|
+
buffer: data,
|
|
161
|
+
});
|
|
162
|
+
await this.storageClient.putObject({
|
|
163
|
+
bucket: options.bucket,
|
|
164
|
+
key,
|
|
165
|
+
body: buffer,
|
|
166
|
+
contentType,
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
await this.storageClient.putObject({
|
|
170
|
+
bucket: options.bucket,
|
|
171
|
+
key,
|
|
172
|
+
sourceFile: file.absolutePath,
|
|
173
|
+
contentType: getContentType(file.relativePath),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await this.reportFile(file, options.bucket, key, 'success');
|
|
178
|
+
return { status: 'success', key };
|
|
179
|
+
} catch (err: any) {
|
|
180
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
181
|
+
await this.reportFile(file, options.bucket, key, 'failed', error);
|
|
182
|
+
return { status: 'failed', key };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
protected buildKey(
|
|
187
|
+
basePrefix: string,
|
|
188
|
+
env?: string,
|
|
189
|
+
pathPrefix?: string,
|
|
190
|
+
relativePath?: string,
|
|
191
|
+
): string {
|
|
192
|
+
const parts: string[] = [];
|
|
193
|
+
if (basePrefix) parts.push(basePrefix);
|
|
194
|
+
if (env) parts.push(env);
|
|
195
|
+
if (pathPrefix) parts.push(pathPrefix);
|
|
196
|
+
parts.push((relativePath ?? '').replace(/\\/g, '/'));
|
|
197
|
+
return parts.join('/');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
protected async walkDir(rootDir: string, currentDir = ''): Promise<LocalFile[]> {
|
|
201
|
+
const dirPath = path.join(rootDir, currentDir);
|
|
202
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
203
|
+
const files: LocalFile[] = [];
|
|
204
|
+
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const rel = path.join(currentDir, entry.name);
|
|
207
|
+
const abs = path.join(rootDir, rel);
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
files.push(...(await this.walkDir(rootDir, rel)));
|
|
210
|
+
} else if (entry.isFile()) {
|
|
211
|
+
files.push({
|
|
212
|
+
absolutePath: abs,
|
|
213
|
+
relativePath: rel.replace(/\\/g, '/'),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return files;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
protected async reportFile(
|
|
221
|
+
file: LocalFile,
|
|
222
|
+
bucket: string,
|
|
223
|
+
key: string,
|
|
224
|
+
status: UploadFileResult['status'],
|
|
225
|
+
error?: Error,
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
if (!this.reporter?.onFileResult) return;
|
|
228
|
+
const result: UploadFileResult = {
|
|
229
|
+
localPath: file.absolutePath,
|
|
230
|
+
relativePath: file.relativePath,
|
|
231
|
+
bucket,
|
|
232
|
+
key,
|
|
233
|
+
status,
|
|
234
|
+
...(this.cdnBaseUrl && { accessUrl: buildCdnAccessUrl(this.cdnBaseUrl, key) }),
|
|
235
|
+
...(error && { error }),
|
|
236
|
+
};
|
|
237
|
+
await this.reporter.onFileResult(result);
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/core/reporter.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* 默认实现为控制台输出,可扩展为企业微信、钉钉等通知。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { SingleBar, Presets } from 'cli-progress';
|
|
7
|
+
|
|
6
8
|
export type UploadStatus = 'success' | 'failed' | 'skipped';
|
|
7
9
|
|
|
8
10
|
/** 单个文件的上传结果,供 Reporter 使用 */
|
|
@@ -20,12 +22,12 @@ export interface UploadFileResult {
|
|
|
20
22
|
/** 整次上传的汇总统计 */
|
|
21
23
|
export interface UploadSummary {
|
|
22
24
|
total: number;
|
|
23
|
-
success:
|
|
24
|
-
failed:
|
|
25
|
-
skipped:
|
|
25
|
+
success: string[];
|
|
26
|
+
failed: string[];
|
|
27
|
+
skipped: string[];
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
/** Reporter
|
|
30
|
+
/** Reporter 接口:上传开始、进度、每个文件结果、全部完成时回调 */
|
|
29
31
|
export interface Reporter {
|
|
30
32
|
onStart?(context: {
|
|
31
33
|
env?: string;
|
|
@@ -36,6 +38,9 @@ export interface Reporter {
|
|
|
36
38
|
cdnBaseUrl?: string;
|
|
37
39
|
}): void | Promise<void>;
|
|
38
40
|
|
|
41
|
+
/** 每完成一个文件后调用,便于显示进度(当前数、总数) */
|
|
42
|
+
onProgress?(current: number, total: number): void | Promise<void>;
|
|
43
|
+
|
|
39
44
|
onFileResult?(result: UploadFileResult): void | Promise<void>;
|
|
40
45
|
|
|
41
46
|
onComplete?(summary: UploadSummary): void | Promise<void>;
|
|
@@ -43,6 +48,8 @@ export interface Reporter {
|
|
|
43
48
|
|
|
44
49
|
/** 默认实现:将上传进度与结果输出到控制台 */
|
|
45
50
|
export class ConsoleReporter implements Reporter {
|
|
51
|
+
private progressBar: SingleBar | null = null;
|
|
52
|
+
|
|
46
53
|
onStart(context: {
|
|
47
54
|
env?: string;
|
|
48
55
|
bucket: string;
|
|
@@ -51,38 +58,41 @@ export class ConsoleReporter implements Reporter {
|
|
|
51
58
|
localDir: string;
|
|
52
59
|
cdnBaseUrl?: string;
|
|
53
60
|
}): void {
|
|
54
|
-
const {
|
|
55
|
-
// console.log('开始上传目录:');
|
|
56
|
-
// console.log(` 环境: ${env ?? '未指定'}`);
|
|
57
|
-
// console.log(` Bucket: ${bucket}`);
|
|
58
|
-
// console.log(` Base 前缀: ${basePrefix || '(空)'}`);
|
|
59
|
-
// console.log(` 业务前缀(pathPrefix): ${pathPrefix || '(空)'}`);
|
|
60
|
-
// console.log(` 本地目录: ${localDir}`);
|
|
61
|
+
const { cdnBaseUrl } = context;
|
|
61
62
|
if (cdnBaseUrl) {
|
|
62
63
|
console.log(` CDN 根地址: ${cdnBaseUrl}`);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
onProgress(current: number, total: number): void {
|
|
68
|
+
if (this.progressBar === null) {
|
|
69
|
+
this.progressBar = new SingleBar(
|
|
70
|
+
{
|
|
71
|
+
format: ' 上传进度 |{bar}| {percentage}% | {value}/{total} 文件',
|
|
72
|
+
barCompleteChar: '\u2588',
|
|
73
|
+
barIncompleteChar: '\u2591',
|
|
74
|
+
hideCursor: true,
|
|
75
|
+
},
|
|
76
|
+
Presets.shades_classic,
|
|
77
|
+
);
|
|
78
|
+
this.progressBar.start(total, 0);
|
|
79
|
+
}
|
|
80
|
+
this.progressBar.update(current);
|
|
81
|
+
if (current >= total) {
|
|
82
|
+
this.progressBar.stop();
|
|
83
|
+
this.progressBar = null;
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
onFileResult(_result: UploadFileResult): void {}
|
|
88
|
+
|
|
80
89
|
onComplete(summary: UploadSummary): void {
|
|
81
90
|
console.log('上传完成:');
|
|
82
91
|
console.log(` 总数: ${summary.total}`);
|
|
83
|
-
console.log(
|
|
84
|
-
console.log(`
|
|
85
|
-
console.log(
|
|
92
|
+
console.log(`\n 跳过: ${summary.skipped.length}`);
|
|
93
|
+
console.log(` 成功:\n ${summary.success.join('\n')}`);
|
|
94
|
+
console.log(`\n\n\n 失败:\n ${summary.failed.join('\n')}`);
|
|
95
|
+
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
98
|
|
package/src/index.ts
CHANGED
|
@@ -1,63 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 包入口:仅暴露上传方法及参数、返回值类型,内部实现固定。
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from './core/fileProcessor';
|
|
27
|
-
|
|
28
|
-
export {
|
|
29
|
-
UploadFlowTemplate,
|
|
30
|
-
type UploadContext,
|
|
31
|
-
type LocalFile,
|
|
32
|
-
} from './core/UploadFlowTemplate';
|
|
33
|
-
|
|
34
|
-
export {
|
|
35
|
-
DirectoryUploadFlow,
|
|
36
|
-
} from './core/DirectoryUploadFlow';
|
|
37
|
-
|
|
38
|
-
export {
|
|
39
|
-
createStorageClient,
|
|
40
|
-
} from './core/StorageFactory';
|
|
41
|
-
|
|
42
|
-
export {
|
|
43
|
-
OssService,
|
|
44
|
-
type UploadDirectoryInput,
|
|
45
|
-
} from './middleware/OssService';
|
|
46
|
-
|
|
47
|
-
export {
|
|
48
|
-
type HuaweiObsConfig,
|
|
49
|
-
type ProviderConfig,
|
|
50
|
-
type ProviderType,
|
|
51
|
-
type EnvConfigResolver,
|
|
52
|
-
type ResolvedProviderConfig,
|
|
53
|
-
type ProviderConfigResolver,
|
|
54
|
-
} from './config/types';
|
|
55
|
-
|
|
56
|
-
export {
|
|
57
|
-
EnvConfigResolverImpl,
|
|
58
|
-
} from './config/EnvConfigResolverImpl';
|
|
59
|
-
|
|
60
|
-
export {
|
|
61
|
-
ProviderConfigResolverImpl,
|
|
62
|
-
} from './config/ProviderConfigResolverImpl';
|
|
5
|
+
import { uploadDirectory } from './middleware/OssService';
|
|
6
|
+
import type { UploadSummary } from './core/reporter';
|
|
7
|
+
|
|
8
|
+
/** 上传参数 */
|
|
9
|
+
export type UploadOptions = {
|
|
10
|
+
/** 本地目录路径 */
|
|
11
|
+
localDir: string;
|
|
12
|
+
/** Bucket 名称 */
|
|
13
|
+
bucket: string;
|
|
14
|
+
/** 环境标识,如 dev / test / prod */
|
|
15
|
+
env?: string;
|
|
16
|
+
/** 业务路径前缀 */
|
|
17
|
+
pathPrefix?: string;
|
|
18
|
+
/** 包含的文件模式 */
|
|
19
|
+
include?: string[];
|
|
20
|
+
/** 排除的文件模式 */
|
|
21
|
+
exclude?: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** 上传结果:总数、成功/失败/跳过的 key 列表 */
|
|
25
|
+
export type UploadResult = UploadSummary;
|
|
63
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 上传本地目录到 OSS(华为 OBS)。配置从环境变量/配置文件读取,进度输出到控制台。
|
|
29
|
+
*/
|
|
30
|
+
export async function upload(options: UploadOptions): Promise<UploadResult> {
|
|
31
|
+
return uploadDirectory(options);
|
|
32
|
+
}
|
|
@@ -1,54 +1,66 @@
|
|
|
1
|
-
import { Reporter, UploadSummary } from '../core/reporter';
|
|
2
|
-
import { ProviderConfigResolver } from '../config/types';
|
|
1
|
+
import { Reporter, UploadSummary, ConsoleReporter } from '../core/reporter';
|
|
3
2
|
import { createStorageClient } from '../core/StorageFactory';
|
|
4
|
-
import { DirectoryUploadFlow } from '../core/DirectoryUploadFlow';
|
|
5
3
|
import type { FileProcessor } from '../core/fileProcessor';
|
|
4
|
+
import { ProviderConfigResolverImpl } from '../config/ProviderConfigResolverImpl';
|
|
5
|
+
import { type UploadDirectoryOptions as BaseOptions } from '../core/UploadService';
|
|
6
|
+
import { DirectoryUploadFlow } from '../core/DirectoryUploadFlow';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
localDir: string;
|
|
12
|
-
pathPrefix?: string;
|
|
13
|
-
include?: string[];
|
|
14
|
-
exclude?: string[];
|
|
8
|
+
// ============ 对外类型与门面 ============
|
|
9
|
+
|
|
10
|
+
/** 上传目录入参:含可选 reporter / fileProcessor,不传则用默认控制台上报 */
|
|
11
|
+
export interface UploadDirectoryOptions extends BaseOptions {
|
|
15
12
|
reporter?: Reporter;
|
|
16
|
-
/** 上传前对文件内容的处理器(如图片压缩),未配置则直接上传原内容 */
|
|
17
13
|
fileProcessor?: FileProcessor;
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
* 一行调用即可上传:内部使用默认配置解析器 + 控制台 Reporter。
|
|
18
|
+
*/
|
|
19
|
+
export async function uploadDirectory(
|
|
20
|
+
options: UploadDirectoryOptions,
|
|
21
|
+
): Promise<UploadSummary> {
|
|
22
|
+
return new OssService().uploadDirectory(options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============ 具体服务类:默认装配 ============
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* OSS 上传服务:解析配置并委托 DirectoryUploadFlow(继承 UploadService)执行上传。
|
|
24
29
|
*/
|
|
25
30
|
export class OssService {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const flow = new DirectoryUploadFlow({
|
|
38
|
-
env,
|
|
39
|
-
bucket,
|
|
40
|
-
localDir,
|
|
41
|
-
pathPrefix,
|
|
42
|
-
include,
|
|
43
|
-
exclude,
|
|
44
|
-
basePrefix,
|
|
45
|
-
storageClient: client,
|
|
31
|
+
private readonly resolver = new ProviderConfigResolverImpl();
|
|
32
|
+
|
|
33
|
+
async uploadDirectory(options: UploadDirectoryOptions): Promise<UploadSummary> {
|
|
34
|
+
const resolved = this.resolver.resolve();
|
|
35
|
+
const client = createStorageClient(resolved.providerConfig);
|
|
36
|
+
const reporter = options.reporter ?? new ConsoleReporter();
|
|
37
|
+
|
|
38
|
+
const flow = new DirectoryUploadFlow(
|
|
39
|
+
client,
|
|
40
|
+
resolved.basePrefix,
|
|
41
|
+
resolved.cdnBaseUrl,
|
|
46
42
|
reporter,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
});
|
|
43
|
+
options.fileProcessor,
|
|
44
|
+
);
|
|
50
45
|
|
|
51
|
-
return flow.
|
|
46
|
+
return flow.uploadDirectory({
|
|
47
|
+
bucket: options.bucket,
|
|
48
|
+
localDir: options.localDir,
|
|
49
|
+
env: options.env,
|
|
50
|
+
pathPrefix: options.pathPrefix,
|
|
51
|
+
include: options.include,
|
|
52
|
+
exclude: options.exclude,
|
|
53
|
+
forceUploadPatterns: getForceUploadPatterns(),
|
|
54
|
+
});
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
function getForceUploadPatterns(): string[] {
|
|
59
|
+
try {
|
|
60
|
+
const raw = process.env.FILE_RE_WHITE_LIST;
|
|
61
|
+
if (!raw) return [];
|
|
62
|
+
return JSON.parse(raw);
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|