@kevisual/oss 0.0.13 → 0.0.14
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/index.d.ts +1 -1
- package/dist/index.js +11 -2049
- package/dist/services/config.d.ts +1 -1
- package/dist/services/config.js +36 -2049
- package/dist/services/index.d.ts +20 -2
- package/dist/services/index.js +38 -2049
- package/package.json +11 -12
- package/src/core/copy-object.ts +26 -0
- package/src/core/type.ts +87 -0
- package/src/index.ts +244 -0
- package/src/s3/copy-object.ts +46 -0
- package/src/s3/core.ts +425 -0
- package/src/s3/type.ts +157 -0
- package/src/services/config.ts +67 -0
- package/src/services/index.ts +9 -0
- package/src/test/common.ts +103 -0
- package/src/test/config-admin.ts +84 -0
- package/src/test/test-s3.ts +23 -0
- package/src/util/download.ts +73 -0
- package/src/util/extract-standard-headers.ts +44 -0
- package/src/util/get-content-type.ts +49 -0
- package/src/util/hash.ts +26 -0
- package/src/util/index.ts +5 -0
package/src/s3/core.ts
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import {
|
|
2
|
+
S3Client,
|
|
3
|
+
GetObjectCommand,
|
|
4
|
+
PutObjectCommand,
|
|
5
|
+
DeleteObjectCommand,
|
|
6
|
+
ListObjectsV2Command,
|
|
7
|
+
HeadObjectCommand,
|
|
8
|
+
CopyObjectCommand,
|
|
9
|
+
type GetObjectCommandOutput,
|
|
10
|
+
} from '@aws-sdk/client-s3';
|
|
11
|
+
import type { Readable } from 'node:stream';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import { omit } from 'es-toolkit';
|
|
14
|
+
import {
|
|
15
|
+
OssBaseOperation,
|
|
16
|
+
ItemBucketMetadata,
|
|
17
|
+
UploadedObjectInfo,
|
|
18
|
+
StatObjectResult,
|
|
19
|
+
ListObjectResult,
|
|
20
|
+
ListFileObject,
|
|
21
|
+
} from './type.ts';
|
|
22
|
+
import { hash } from '../util/hash.ts';
|
|
23
|
+
import { getContentType } from '../util/get-content-type.ts';
|
|
24
|
+
import { extractStandardHeaders } from '../util/extract-standard-headers.ts';
|
|
25
|
+
|
|
26
|
+
export type OssBaseOptions<T = { [key: string]: any }> = {
|
|
27
|
+
/**
|
|
28
|
+
* 已经初始化好的 S3Client
|
|
29
|
+
*/
|
|
30
|
+
client: S3Client;
|
|
31
|
+
/**
|
|
32
|
+
* 桶名
|
|
33
|
+
*/
|
|
34
|
+
bucketName: string;
|
|
35
|
+
/**
|
|
36
|
+
* 前缀
|
|
37
|
+
*/
|
|
38
|
+
prefix?: string;
|
|
39
|
+
} & T;
|
|
40
|
+
|
|
41
|
+
export class OssBase implements OssBaseOperation {
|
|
42
|
+
client: S3Client;
|
|
43
|
+
bucketName: string;
|
|
44
|
+
prefix = '';
|
|
45
|
+
/**
|
|
46
|
+
* 计算字符串或者对象的的md5值
|
|
47
|
+
*/
|
|
48
|
+
hash = hash;
|
|
49
|
+
|
|
50
|
+
constructor(opts: OssBaseOptions) {
|
|
51
|
+
if (!opts.client) {
|
|
52
|
+
throw new Error('client is required');
|
|
53
|
+
}
|
|
54
|
+
this.bucketName = opts.bucketName;
|
|
55
|
+
this.client = opts.client;
|
|
56
|
+
this.prefix = opts?.prefix ?? '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setPrefix(prefix: string) {
|
|
60
|
+
this.prefix = prefix;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 获取对象
|
|
65
|
+
* @param objectName 对象名
|
|
66
|
+
*/
|
|
67
|
+
async getObject(objectName: string): Promise<GetObjectCommandOutput> {
|
|
68
|
+
const command = new GetObjectCommand({
|
|
69
|
+
Bucket: this.bucketName,
|
|
70
|
+
Key: `${this.prefix}${objectName}`,
|
|
71
|
+
});
|
|
72
|
+
const response = await this.client.send(command);
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 获取对象内容为字符串
|
|
78
|
+
* @param objectName 对象名
|
|
79
|
+
*/
|
|
80
|
+
async getObjectAsString(objectName: string): Promise<string> {
|
|
81
|
+
const response = await this.getObject(objectName);
|
|
82
|
+
if (response.Body) {
|
|
83
|
+
return await response.Body.transformToString();
|
|
84
|
+
}
|
|
85
|
+
throw new Error('Object body is empty');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 获取对象内容为 JSON
|
|
90
|
+
* @param objectName 对象名
|
|
91
|
+
*/
|
|
92
|
+
async getJson(objectName: string): Promise<Record<string, any>> {
|
|
93
|
+
const str = await this.getObjectAsString(objectName);
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(str);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new Error('Failed to parse JSON');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 上传对象
|
|
103
|
+
* @param objectName 对象名
|
|
104
|
+
* @param data 数据
|
|
105
|
+
* @param metaData 元数据
|
|
106
|
+
* @param opts 选项
|
|
107
|
+
*/
|
|
108
|
+
async putObject(
|
|
109
|
+
objectName: string,
|
|
110
|
+
data: Buffer | string | Object | Readable,
|
|
111
|
+
metaData: ItemBucketMetadata = {},
|
|
112
|
+
opts?: { check?: boolean; isStream?: boolean; size?: number; contentType?: string },
|
|
113
|
+
): Promise<UploadedObjectInfo> {
|
|
114
|
+
let putData: Buffer | string | Readable;
|
|
115
|
+
let contentLength: number | undefined = opts?.size;
|
|
116
|
+
const isStream = opts?.isStream;
|
|
117
|
+
|
|
118
|
+
if (!isStream) {
|
|
119
|
+
if (typeof data === 'string') {
|
|
120
|
+
putData = data;
|
|
121
|
+
contentLength = Buffer.byteLength(data);
|
|
122
|
+
} else if (Buffer.isBuffer(data)) {
|
|
123
|
+
putData = data;
|
|
124
|
+
contentLength = data.length;
|
|
125
|
+
} else {
|
|
126
|
+
putData = JSON.stringify(data);
|
|
127
|
+
contentLength = Buffer.byteLength(putData);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
putData = data as Readable;
|
|
131
|
+
if (!contentLength) {
|
|
132
|
+
throw new Error('Stream upload requires size parameter');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 检查现有对象并合并元数据
|
|
137
|
+
if (opts?.check) {
|
|
138
|
+
const obj = await this.statObject(objectName, true);
|
|
139
|
+
if (obj) {
|
|
140
|
+
const omitMeta = ['size', 'content-type', 'cache-control', 'app-source'];
|
|
141
|
+
const objMeta = JSON.parse(JSON.stringify(omit(obj.metaData, omitMeta)));
|
|
142
|
+
metaData = {
|
|
143
|
+
...objMeta,
|
|
144
|
+
...metaData,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { standardHeaders, customMetadata } = extractStandardHeaders(metaData);
|
|
150
|
+
|
|
151
|
+
const command = new PutObjectCommand({
|
|
152
|
+
Bucket: this.bucketName,
|
|
153
|
+
Key: `${this.prefix}${objectName}`,
|
|
154
|
+
Body: putData,
|
|
155
|
+
ContentLength: contentLength,
|
|
156
|
+
ContentType: opts?.contentType || standardHeaders.ContentType || getContentType(objectName),
|
|
157
|
+
CacheControl: standardHeaders.CacheControl,
|
|
158
|
+
ContentDisposition: standardHeaders.ContentDisposition,
|
|
159
|
+
ContentEncoding: standardHeaders.ContentEncoding,
|
|
160
|
+
ContentLanguage: standardHeaders.ContentLanguage,
|
|
161
|
+
Expires: standardHeaders.Expires,
|
|
162
|
+
Metadata: customMetadata,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const response = await this.client.send(command);
|
|
166
|
+
return {
|
|
167
|
+
etag: response.ETag?.replace(/"/g, '') || '',
|
|
168
|
+
versionId: response.VersionId || '',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 上传文件
|
|
174
|
+
* @param objectName 对象名
|
|
175
|
+
* @param filePath 文件路径
|
|
176
|
+
* @param metaData 元数据
|
|
177
|
+
*/
|
|
178
|
+
async fPutObject(
|
|
179
|
+
objectName: string,
|
|
180
|
+
filePath: string,
|
|
181
|
+
metaData?: ItemBucketMetadata,
|
|
182
|
+
): Promise<UploadedObjectInfo> {
|
|
183
|
+
const fileStream = fs.createReadStream(filePath);
|
|
184
|
+
const stat = fs.statSync(filePath);
|
|
185
|
+
|
|
186
|
+
const { standardHeaders, customMetadata } = extractStandardHeaders(metaData || {});
|
|
187
|
+
|
|
188
|
+
const command = new PutObjectCommand({
|
|
189
|
+
Bucket: this.bucketName,
|
|
190
|
+
Key: `${this.prefix}${objectName}`,
|
|
191
|
+
Body: fileStream,
|
|
192
|
+
ContentLength: stat.size,
|
|
193
|
+
ContentType: standardHeaders.ContentType || getContentType(filePath),
|
|
194
|
+
CacheControl: standardHeaders.CacheControl,
|
|
195
|
+
ContentDisposition: standardHeaders.ContentDisposition,
|
|
196
|
+
ContentEncoding: standardHeaders.ContentEncoding,
|
|
197
|
+
ContentLanguage: standardHeaders.ContentLanguage,
|
|
198
|
+
Expires: standardHeaders.Expires,
|
|
199
|
+
Metadata: customMetadata,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const response = await this.client.send(command);
|
|
203
|
+
return {
|
|
204
|
+
etag: response.ETag?.replace(/"/g, '') || '',
|
|
205
|
+
versionId: response.VersionId || '',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 删除对象
|
|
211
|
+
* @param objectName 对象名
|
|
212
|
+
*/
|
|
213
|
+
async deleteObject(objectName: string): Promise<void> {
|
|
214
|
+
const command = new DeleteObjectCommand({
|
|
215
|
+
Bucket: this.bucketName,
|
|
216
|
+
Key: `${this.prefix}${objectName}`,
|
|
217
|
+
});
|
|
218
|
+
await this.client.send(command);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 列出对象
|
|
223
|
+
* @param objectName 前缀
|
|
224
|
+
* @param opts 选项
|
|
225
|
+
*/
|
|
226
|
+
async listObjects<IS_FILE = false>(
|
|
227
|
+
objectName: string,
|
|
228
|
+
opts?: { recursive?: boolean; startAfter?: string; maxKeys?: number },
|
|
229
|
+
): Promise<IS_FILE extends true ? ListFileObject[] : ListObjectResult[]> {
|
|
230
|
+
const prefix = `${this.prefix}${objectName}`;
|
|
231
|
+
const results: ListObjectResult[] = [];
|
|
232
|
+
let continuationToken: string | undefined;
|
|
233
|
+
|
|
234
|
+
do {
|
|
235
|
+
const command = new ListObjectsV2Command({
|
|
236
|
+
Bucket: this.bucketName,
|
|
237
|
+
Prefix: prefix,
|
|
238
|
+
Delimiter: opts?.recursive ? undefined : '/',
|
|
239
|
+
StartAfter: opts?.startAfter,
|
|
240
|
+
MaxKeys: opts?.maxKeys || 1000,
|
|
241
|
+
ContinuationToken: continuationToken,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const response = await this.client.send(command);
|
|
245
|
+
|
|
246
|
+
// 处理文件对象
|
|
247
|
+
if (response.Contents) {
|
|
248
|
+
for (const item of response.Contents) {
|
|
249
|
+
results.push({
|
|
250
|
+
name: item.Key || '',
|
|
251
|
+
size: item.Size || 0,
|
|
252
|
+
lastModified: item.LastModified || new Date(),
|
|
253
|
+
etag: item.ETag?.replace(/"/g, '') || '',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 处理目录(CommonPrefixes)
|
|
259
|
+
if (response.CommonPrefixes && !opts?.recursive) {
|
|
260
|
+
for (const prefix of response.CommonPrefixes) {
|
|
261
|
+
results.push({
|
|
262
|
+
prefix: prefix.Prefix || '',
|
|
263
|
+
size: 0,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
continuationToken = response.NextContinuationToken;
|
|
269
|
+
} while (continuationToken);
|
|
270
|
+
|
|
271
|
+
return results as IS_FILE extends true ? ListFileObject[] : ListObjectResult[];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 获取对象信息
|
|
276
|
+
* @param objectName 对象名
|
|
277
|
+
* @param checkFile 是否检查文件存在(不存在返回null而非抛错)
|
|
278
|
+
*/
|
|
279
|
+
async statObject(objectName: string, checkFile = true): Promise<StatObjectResult | null> {
|
|
280
|
+
try {
|
|
281
|
+
const command = new HeadObjectCommand({
|
|
282
|
+
Bucket: this.bucketName,
|
|
283
|
+
Key: `${this.prefix}${objectName}`,
|
|
284
|
+
});
|
|
285
|
+
const response = await this.client.send(command);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
size: response.ContentLength || 0,
|
|
289
|
+
etag: response.ETag?.replace(/"/g, '') || '',
|
|
290
|
+
lastModified: response.LastModified || new Date(),
|
|
291
|
+
metaData: (response.Metadata as ItemBucketMetadata) || {},
|
|
292
|
+
versionId: response.VersionId || null,
|
|
293
|
+
};
|
|
294
|
+
} catch (e: any) {
|
|
295
|
+
if (checkFile && (e.name === 'NotFound' || e.$metadata?.httpStatusCode === 404)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
throw e;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 获取完整的对象名称
|
|
304
|
+
* @param objectName 对象名
|
|
305
|
+
*/
|
|
306
|
+
getObjectName(objectName: string): string {
|
|
307
|
+
return `${this.prefix}${objectName}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 检查文件hash是否一致
|
|
312
|
+
* @param objectName 对象名
|
|
313
|
+
* @param hash hash值
|
|
314
|
+
* @param meta 元数据
|
|
315
|
+
*/
|
|
316
|
+
async checkObjectHash(
|
|
317
|
+
objectName: string,
|
|
318
|
+
hash: string,
|
|
319
|
+
meta?: ItemBucketMetadata,
|
|
320
|
+
): Promise<{ success: boolean; metaData: ItemBucketMetadata | null; obj: StatObjectResult | null; equalMeta?: boolean }> {
|
|
321
|
+
const obj = await this.statObject(objectName, true);
|
|
322
|
+
if (!obj) {
|
|
323
|
+
return { success: false, metaData: null, obj: null, equalMeta: false };
|
|
324
|
+
}
|
|
325
|
+
const omitMeta = ['content-type', 'cache-control', 'app-source'];
|
|
326
|
+
const metaData = omit(obj.metaData, omitMeta);
|
|
327
|
+
let equalMeta = false;
|
|
328
|
+
if (meta) {
|
|
329
|
+
equalMeta = JSON.stringify(metaData) === JSON.stringify(meta);
|
|
330
|
+
}
|
|
331
|
+
return { success: obj.etag === hash, metaData, obj, equalMeta };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 获取元数据
|
|
336
|
+
* @param pathname 路径名
|
|
337
|
+
* @param meta 元数据
|
|
338
|
+
*/
|
|
339
|
+
getMetadata(pathname: string, meta: ItemBucketMetadata = { 'app-source': 'user-app' }): ItemBucketMetadata {
|
|
340
|
+
const isHtml = pathname.endsWith('.html');
|
|
341
|
+
if (isHtml) {
|
|
342
|
+
meta = {
|
|
343
|
+
...meta,
|
|
344
|
+
'content-type': 'text/html; charset=utf-8',
|
|
345
|
+
'cache-control': 'no-cache',
|
|
346
|
+
};
|
|
347
|
+
} else {
|
|
348
|
+
meta = {
|
|
349
|
+
...meta,
|
|
350
|
+
'content-type': getContentType(pathname),
|
|
351
|
+
'cache-control': 'max-age=31536000, immutable',
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return meta;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 复制对象
|
|
359
|
+
* @param sourceObject 源对象
|
|
360
|
+
* @param targetObject 目标对象
|
|
361
|
+
*/
|
|
362
|
+
async copyObject(sourceObject: string, targetObject: string): Promise<any> {
|
|
363
|
+
const command = new CopyObjectCommand({
|
|
364
|
+
Bucket: this.bucketName,
|
|
365
|
+
CopySource: `${this.bucketName}/${this.prefix}${sourceObject}`,
|
|
366
|
+
Key: `${this.prefix}${targetObject}`,
|
|
367
|
+
});
|
|
368
|
+
const response = await this.client.send(command);
|
|
369
|
+
return response;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 替换对象元数据
|
|
374
|
+
* @param objectName 对象名
|
|
375
|
+
* @param meta 新元数据
|
|
376
|
+
*/
|
|
377
|
+
async replaceObject(objectName: string, meta: ItemBucketMetadata): Promise<any> {
|
|
378
|
+
const key = `${this.prefix}${objectName}`;
|
|
379
|
+
// 获取当前对象的元数据
|
|
380
|
+
const stat = await this.statObject(objectName, false);
|
|
381
|
+
const sourceMetadata = stat?.metaData || {};
|
|
382
|
+
|
|
383
|
+
const mergedMeta = { ...sourceMetadata, ...meta };
|
|
384
|
+
const { standardHeaders, customMetadata } = extractStandardHeaders(mergedMeta);
|
|
385
|
+
|
|
386
|
+
const command = new CopyObjectCommand({
|
|
387
|
+
Bucket: this.bucketName,
|
|
388
|
+
CopySource: `${this.bucketName}/${key}`,
|
|
389
|
+
Key: key,
|
|
390
|
+
ContentType: standardHeaders.ContentType,
|
|
391
|
+
CacheControl: standardHeaders.CacheControl,
|
|
392
|
+
ContentDisposition: standardHeaders.ContentDisposition,
|
|
393
|
+
ContentEncoding: standardHeaders.ContentEncoding,
|
|
394
|
+
ContentLanguage: standardHeaders.ContentLanguage,
|
|
395
|
+
Expires: standardHeaders.Expires,
|
|
396
|
+
Metadata: customMetadata,
|
|
397
|
+
MetadataDirective: 'REPLACE',
|
|
398
|
+
});
|
|
399
|
+
const response = await this.client.send(command);
|
|
400
|
+
return response;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 创建实例
|
|
405
|
+
*/
|
|
406
|
+
static create<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, opts: OssBaseOptions<U>): T {
|
|
407
|
+
return new this(opts);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 从已有实例创建
|
|
412
|
+
*/
|
|
413
|
+
static fromBase<T extends OssBase, U>(
|
|
414
|
+
this: new (opts: OssBaseOptions<U>) => T,
|
|
415
|
+
createOpts: { oss: OssBase; opts: Partial<OssBaseOptions<U>> },
|
|
416
|
+
): T {
|
|
417
|
+
const base = createOpts.oss;
|
|
418
|
+
const opts = createOpts.opts as any;
|
|
419
|
+
return new this({
|
|
420
|
+
client: base.client,
|
|
421
|
+
bucketName: base.bucketName,
|
|
422
|
+
...opts,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
package/src/s3/type.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { CopyObjectCommandOutput } from '@aws-sdk/client-s3';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
|
|
4
|
+
export type ItemBucketMetadata = Record<string, string>;
|
|
5
|
+
|
|
6
|
+
export type UploadedObjectInfo = {
|
|
7
|
+
etag: string;
|
|
8
|
+
lastModified?: Date;
|
|
9
|
+
size?: number;
|
|
10
|
+
versionId: string;
|
|
11
|
+
metadata?: ItemBucketMetadata;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type StatObjectResult = {
|
|
15
|
+
size: number;
|
|
16
|
+
etag: string;
|
|
17
|
+
lastModified: Date;
|
|
18
|
+
metaData: ItemBucketMetadata;
|
|
19
|
+
versionId?: string | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ListFileObject = {
|
|
23
|
+
name: string;
|
|
24
|
+
size: number;
|
|
25
|
+
lastModified: Date;
|
|
26
|
+
etag: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ListDirectoryObject = {
|
|
30
|
+
prefix: string;
|
|
31
|
+
size: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ListObjectResult = ListFileObject | ListDirectoryObject;
|
|
35
|
+
|
|
36
|
+
export interface OssBaseOperation {
|
|
37
|
+
prefix: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 设置前缀
|
|
41
|
+
* @param prefix 前缀
|
|
42
|
+
*/
|
|
43
|
+
setPrefix(prefix: string): void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 获取对象
|
|
47
|
+
* @param objectName 对象名
|
|
48
|
+
*/
|
|
49
|
+
getObject(objectName: string): Promise<any>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 获取对象内容为字符串
|
|
53
|
+
* @param objectName 对象名
|
|
54
|
+
*/
|
|
55
|
+
getObjectAsString?(objectName: string): Promise<string>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 获取对象内容为 JSON
|
|
59
|
+
* @param objectName 对象名
|
|
60
|
+
*/
|
|
61
|
+
getJson?(objectName: string): Promise<Record<string, any>>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 上传对象
|
|
65
|
+
* @param objectName 对象名
|
|
66
|
+
* @param data 数据
|
|
67
|
+
* @param metaData 元数据
|
|
68
|
+
* @param opts 选项
|
|
69
|
+
*/
|
|
70
|
+
putObject(
|
|
71
|
+
objectName: string,
|
|
72
|
+
data: Buffer | string | Object | Readable,
|
|
73
|
+
metaData?: ItemBucketMetadata,
|
|
74
|
+
opts?: { check?: boolean; isStream?: boolean; size?: number; contentType?: string },
|
|
75
|
+
): Promise<UploadedObjectInfo>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 上传文件
|
|
79
|
+
* @param objectName 对象名
|
|
80
|
+
* @param filePath 文件路径
|
|
81
|
+
* @param metaData 元数据
|
|
82
|
+
*/
|
|
83
|
+
fPutObject(objectName: string, filePath: string, metaData?: ItemBucketMetadata): Promise<UploadedObjectInfo>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 获取对象信息
|
|
87
|
+
* @param objectName 对象名
|
|
88
|
+
* @param checkFile 是否检查文件存在(不存在返回null而非抛错)
|
|
89
|
+
*/
|
|
90
|
+
statObject(objectName: string, checkFile?: boolean): Promise<StatObjectResult | null>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 删除对象
|
|
94
|
+
* @param objectName 对象名
|
|
95
|
+
*/
|
|
96
|
+
deleteObject(objectName: string): Promise<any>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 列出对象
|
|
100
|
+
* @param objectName 前缀
|
|
101
|
+
* @param opts 选项
|
|
102
|
+
*/
|
|
103
|
+
listObjects(
|
|
104
|
+
objectName: string,
|
|
105
|
+
opts?: {
|
|
106
|
+
/** 是否递归 */
|
|
107
|
+
recursive?: boolean;
|
|
108
|
+
/** 开始位置 */
|
|
109
|
+
startAfter?: string;
|
|
110
|
+
/** 最大返回数量 */
|
|
111
|
+
maxKeys?: number;
|
|
112
|
+
},
|
|
113
|
+
): Promise<ListObjectResult[]>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取完整的对象名称
|
|
117
|
+
* @param objectName 对象名
|
|
118
|
+
*/
|
|
119
|
+
getObjectName?(objectName: string): string;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 检查文件hash是否一致
|
|
123
|
+
* @param objectName 对象名
|
|
124
|
+
* @param hash hash值
|
|
125
|
+
* @param meta 元数据
|
|
126
|
+
*/
|
|
127
|
+
checkObjectHash?(
|
|
128
|
+
objectName: string,
|
|
129
|
+
hash: string,
|
|
130
|
+
meta?: ItemBucketMetadata,
|
|
131
|
+
): Promise<{ success: boolean; metaData: ItemBucketMetadata | null; obj: StatObjectResult | null; equalMeta?: boolean }>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 获取元数据
|
|
135
|
+
* @param pathname 路径名
|
|
136
|
+
* @param meta 元数据
|
|
137
|
+
*/
|
|
138
|
+
getMetadata?(pathname: string, meta?: ItemBucketMetadata): ItemBucketMetadata;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 复制对象
|
|
142
|
+
* @param sourceObject 源对象
|
|
143
|
+
* @param targetObject 目标对象
|
|
144
|
+
*/
|
|
145
|
+
copyObject(sourceObject: string, targetObject: string): Promise<CopyObjectCommandOutput>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 替换对象元数据
|
|
149
|
+
* @param objectName 对象名
|
|
150
|
+
* @param meta 新元数据
|
|
151
|
+
*/
|
|
152
|
+
replaceObject?(objectName: string, meta: ItemBucketMetadata): Promise<any>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface OssService extends OssBaseOperation {
|
|
156
|
+
owner: string;
|
|
157
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { OssBase, OssBaseOptions } from '../index.ts';
|
|
2
|
+
import { OssService } from '../core/type.ts';
|
|
3
|
+
import * as util from '../util/index.ts';
|
|
4
|
+
|
|
5
|
+
export class ConfigOssService extends OssBase implements OssService {
|
|
6
|
+
owner: string;
|
|
7
|
+
constructor(opts: OssBaseOptions<{ owner: string }>) {
|
|
8
|
+
super(opts);
|
|
9
|
+
this.owner = opts.owner;
|
|
10
|
+
this.setPrefix(`${this.owner}/config/1.0.0/`);
|
|
11
|
+
}
|
|
12
|
+
async listAllFile() {
|
|
13
|
+
const list = await this.listObjects<true>('');
|
|
14
|
+
return list.filter((item) => item.size > 0 && this.isEndWithJson(item.name));
|
|
15
|
+
}
|
|
16
|
+
async listAll() {
|
|
17
|
+
return await this.listObjects<false>('');
|
|
18
|
+
}
|
|
19
|
+
configMap = new Map<string, any>();
|
|
20
|
+
keys: string[] = [];
|
|
21
|
+
async getAllConfigJson() {
|
|
22
|
+
const list = await this.listAllFile();
|
|
23
|
+
return list;
|
|
24
|
+
}
|
|
25
|
+
isEndWithJson(string: string) {
|
|
26
|
+
return string?.endsWith?.(`.json`);
|
|
27
|
+
}
|
|
28
|
+
putJsonObject(key: string, data: any) {
|
|
29
|
+
const json = util.hashSringify(data);
|
|
30
|
+
return this.putObject(key, json, {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'app-source': 'user-config-app',
|
|
33
|
+
'Cache-Control': 'max-age=31536000, immutable',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async getObjectList(objectNameList: string[]) {
|
|
37
|
+
const jsonMap = new Map<string, Record<string, any>>();
|
|
38
|
+
for (const objectName of objectNameList) {
|
|
39
|
+
try {
|
|
40
|
+
const json = await this.getJson(objectName);
|
|
41
|
+
jsonMap.set(objectName, json);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(error);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return jsonMap;
|
|
48
|
+
}
|
|
49
|
+
async getList() {
|
|
50
|
+
const list = await this.listAllFile();
|
|
51
|
+
/**
|
|
52
|
+
* key -> etag
|
|
53
|
+
*/
|
|
54
|
+
const keyEtagMap = new Map<string, Etag>();
|
|
55
|
+
const listKeys = list.map((item) => {
|
|
56
|
+
const key = item.name.replace(this.prefix, '');
|
|
57
|
+
keyEtagMap.set(key, item.etag);
|
|
58
|
+
return {
|
|
59
|
+
...item,
|
|
60
|
+
key,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
type Etag = string;
|
|
64
|
+
const keys = Array.from(keyEtagMap.keys());
|
|
65
|
+
return { list: listKeys, keys, keyEtagMap };
|
|
66
|
+
}
|
|
67
|
+
}
|