@kevisual/oss 0.0.16 → 0.0.17
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 +255 -152
- package/dist/index.js +36502 -111
- package/dist/services.d.ts +428 -0
- package/dist/services.js +36791 -0
- package/package.json +11 -15
- package/src/core/core.ts +244 -0
- package/src/index.ts +1 -244
- package/src/s3/core.ts +24 -8
- package/src/services/config.ts +2 -2
- package/src/test/common.ts +1 -1
- package/dist/services/config.d.ts +0 -190
- package/dist/services/config.js +0 -358
- package/dist/services/index.d.ts +0 -235
- package/dist/services/index.js +0 -421
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevisual/oss",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "bun run bun.config.ts"
|
|
@@ -14,32 +14,28 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"@
|
|
17
|
+
"@aws-sdk/client-s3": "^3.978.0",
|
|
18
|
+
"@kevisual/use-config": "^1.0.28",
|
|
19
|
+
"@types/bun": "^1.3.8",
|
|
20
|
+
"@types/node": "^25.1.0",
|
|
19
21
|
"bun-plugin-dts": "^0.3.0",
|
|
20
22
|
"dotenv": "^17.2.3",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"fast-glob": "^3.3.3"
|
|
23
|
+
"es-toolkit": "^1.44.0",
|
|
24
|
+
"fast-glob": "^3.3.3",
|
|
25
|
+
"minio": "^8.0.6"
|
|
25
26
|
},
|
|
26
27
|
"exports": {
|
|
27
28
|
".": {
|
|
28
29
|
"import": "./dist/index.js",
|
|
29
30
|
"types": "./dist/index.d.ts"
|
|
30
31
|
},
|
|
31
|
-
"./config": {
|
|
32
|
-
"import": "./dist/services/config.js",
|
|
33
|
-
"types": "./dist/services/config.d.ts"
|
|
34
|
-
},
|
|
35
32
|
"./services": {
|
|
36
|
-
"import": "./dist/services
|
|
37
|
-
"types": "./dist/services
|
|
33
|
+
"import": "./dist/services.js",
|
|
34
|
+
"types": "./dist/services.d.ts"
|
|
38
35
|
},
|
|
39
36
|
"./s3.ts": "./src/s3/core.ts"
|
|
40
37
|
},
|
|
41
38
|
"publishConfig": {
|
|
42
39
|
"access": "public"
|
|
43
|
-
}
|
|
44
|
-
"dependencies": {}
|
|
40
|
+
}
|
|
45
41
|
}
|
package/src/core/core.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Client, ItemBucketMetadata } from 'minio';
|
|
2
|
+
import { ListFileObject, ListObjectResult, OssBaseOperation } from './type.ts';
|
|
3
|
+
import { hash } from '../util/hash.ts';
|
|
4
|
+
import { copyObject } from './copy-object.ts';
|
|
5
|
+
import { getContentType } from '../util/get-content-type.ts';
|
|
6
|
+
import { omit } from 'es-toolkit'
|
|
7
|
+
export type OssBaseOptions<T = { [key: string]: any }> = {
|
|
8
|
+
/**
|
|
9
|
+
* 已经初始化好的minio client
|
|
10
|
+
*/
|
|
11
|
+
client: Client;
|
|
12
|
+
/**
|
|
13
|
+
* 桶名
|
|
14
|
+
*/
|
|
15
|
+
bucketName: string;
|
|
16
|
+
/**
|
|
17
|
+
* 前缀
|
|
18
|
+
*/
|
|
19
|
+
prefix?: string;
|
|
20
|
+
} & T;
|
|
21
|
+
|
|
22
|
+
export class OssBase implements OssBaseOperation {
|
|
23
|
+
client?: Client;
|
|
24
|
+
bucketName: string;
|
|
25
|
+
prefix = '';
|
|
26
|
+
/**
|
|
27
|
+
* 计算字符串或者对象的的md5值
|
|
28
|
+
*/
|
|
29
|
+
hash = hash;
|
|
30
|
+
constructor(opts: OssBaseOptions) {
|
|
31
|
+
if (!opts.client) {
|
|
32
|
+
throw new Error('client is required');
|
|
33
|
+
}
|
|
34
|
+
this.bucketName = opts.bucketName;
|
|
35
|
+
this.client = opts.client;
|
|
36
|
+
this.prefix = opts?.prefix ?? '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setPrefix(prefix: string) {
|
|
40
|
+
this.prefix = prefix;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getObject(objectName: string) {
|
|
44
|
+
const bucketName = this.bucketName;
|
|
45
|
+
const obj = await this.client.getObject(bucketName, `${this.prefix}${objectName}`);
|
|
46
|
+
return obj;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getJson(objectName: string): Promise<Record<string, any>> {
|
|
50
|
+
const obj = await this.getObject(objectName);
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
let data = '';
|
|
53
|
+
obj.on('data', (chunk) => {
|
|
54
|
+
data += chunk;
|
|
55
|
+
});
|
|
56
|
+
obj.on('end', () => {
|
|
57
|
+
try {
|
|
58
|
+
const jsonData = JSON.parse(data);
|
|
59
|
+
resolve(jsonData);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
reject(new Error('Failed to parse JSON'));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
obj.on('error', (err) => {
|
|
65
|
+
reject(err);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 上传文件, 当是流的时候,中断之后的etag会变,所以传递的时候不要嵌套async await,例如 busboy 监听文件流内部的时候,不要用check
|
|
71
|
+
* @param objectName
|
|
72
|
+
* @param data
|
|
73
|
+
* @param metaData
|
|
74
|
+
* @param options 如果文件本身存在,则复制原有的meta的内容
|
|
75
|
+
* @returns
|
|
76
|
+
*/
|
|
77
|
+
async putObject(
|
|
78
|
+
objectName: string,
|
|
79
|
+
data: Buffer | string | Object,
|
|
80
|
+
metaData: ItemBucketMetadata = {},
|
|
81
|
+
opts?: { check?: boolean; isStream?: boolean; size?: number },
|
|
82
|
+
) {
|
|
83
|
+
let putData: Buffer | string;
|
|
84
|
+
let size: number = opts?.size;
|
|
85
|
+
const isStream = opts?.isStream;
|
|
86
|
+
if (!isStream) {
|
|
87
|
+
if (typeof data === 'string') {
|
|
88
|
+
putData = data;
|
|
89
|
+
size = putData.length;
|
|
90
|
+
} else {
|
|
91
|
+
putData = JSON.stringify(data);
|
|
92
|
+
size = putData.length;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
putData = data as any;
|
|
96
|
+
// 对于流式上传,如果没有提供 size,会导致多部分上传,ETag 会是 ****-1 格式
|
|
97
|
+
// 必须提供准确的 size 才能得到标准的 MD5 格式 ETag
|
|
98
|
+
if (!size) {
|
|
99
|
+
throw new Error('Stream upload requires size parameter to avoid multipart upload and get standard MD5 ETag');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (opts?.check) {
|
|
103
|
+
const obj = await this.statObject(objectName, true);
|
|
104
|
+
if (obj) {
|
|
105
|
+
const omitMeda = ['size', 'content-type', 'cache-control', 'app-source'];
|
|
106
|
+
const objMeta = JSON.parse(JSON.stringify(omit(obj.metaData, omitMeda)));
|
|
107
|
+
metaData = {
|
|
108
|
+
...objMeta,
|
|
109
|
+
...metaData,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bucketName = this.bucketName;
|
|
115
|
+
const obj = await this.client.putObject(bucketName, `${this.prefix}${objectName}`, putData, size, metaData);
|
|
116
|
+
return obj;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async deleteObject(objectName: string) {
|
|
120
|
+
const bucketName = this.bucketName;
|
|
121
|
+
const obj = await this.client.removeObject(bucketName, `${this.prefix}${objectName}`);
|
|
122
|
+
return obj;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async listObjects<IS_FILE = false>(objectName: string, opts?: { recursive?: boolean; startAfter?: string }) {
|
|
126
|
+
const bucketName = this.bucketName;
|
|
127
|
+
const prefix = `${this.prefix}${objectName}`;
|
|
128
|
+
const res = await new Promise((resolve, reject) => {
|
|
129
|
+
let res: any[] = [];
|
|
130
|
+
let hasError = false;
|
|
131
|
+
this.client
|
|
132
|
+
.listObjectsV2(bucketName, prefix, opts?.recursive ?? false, opts?.startAfter)
|
|
133
|
+
.on('data', (data) => {
|
|
134
|
+
res.push(data);
|
|
135
|
+
})
|
|
136
|
+
.on('error', (err) => {
|
|
137
|
+
console.error('minio error', prefix, err);
|
|
138
|
+
hasError = true;
|
|
139
|
+
})
|
|
140
|
+
.on('end', () => {
|
|
141
|
+
if (hasError) {
|
|
142
|
+
reject();
|
|
143
|
+
return;
|
|
144
|
+
} else {
|
|
145
|
+
resolve(res);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
return res as IS_FILE extends true ? ListFileObject[] : ListObjectResult[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async fPutObject(objectName: string, filePath: string, metaData?: ItemBucketMetadata) {
|
|
153
|
+
const bucketName = this.bucketName;
|
|
154
|
+
const obj = await this.client.fPutObject(bucketName, `${this.prefix}${objectName}`, filePath, metaData);
|
|
155
|
+
return obj as any;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 获取完整的对象名称
|
|
159
|
+
* @param objectName
|
|
160
|
+
* @returns
|
|
161
|
+
*/
|
|
162
|
+
async getObjectName(objectName: string) {
|
|
163
|
+
return `${this.prefix}${objectName}`;
|
|
164
|
+
}
|
|
165
|
+
async statObject(objectName: string, checkFile = true) {
|
|
166
|
+
const bucketName = this.bucketName;
|
|
167
|
+
try {
|
|
168
|
+
const obj = await this.client.statObject(bucketName, `${this.prefix}${objectName}`);
|
|
169
|
+
return obj;
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (e.code === 'NotFound') {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
throw e;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 检查文件hash是否一致
|
|
179
|
+
* @param objectName
|
|
180
|
+
* @param hash
|
|
181
|
+
* @returns
|
|
182
|
+
*/
|
|
183
|
+
async checkObjectHash(
|
|
184
|
+
objectName: string,
|
|
185
|
+
hash: string,
|
|
186
|
+
meta?: ItemBucketMetadata,
|
|
187
|
+
): Promise<{ success: boolean; metaData: ItemBucketMetadata | null; obj: any; equalMeta?: boolean }> {
|
|
188
|
+
const obj = await this.statObject(`${this.prefix}${objectName}`, true);
|
|
189
|
+
if (!obj) {
|
|
190
|
+
return { success: false, metaData: null, obj: null, equalMeta: false };
|
|
191
|
+
}
|
|
192
|
+
let metaData: ItemBucketMetadata = {};
|
|
193
|
+
const omitMeda = ['content-type', 'cache-control', 'app-source'];
|
|
194
|
+
const objMeta = omit(obj.metaData, omitMeda);
|
|
195
|
+
metaData = {
|
|
196
|
+
...objMeta,
|
|
197
|
+
};
|
|
198
|
+
let equalMeta = false;
|
|
199
|
+
if (meta) {
|
|
200
|
+
equalMeta = JSON.stringify(metaData) === JSON.stringify(meta);
|
|
201
|
+
}
|
|
202
|
+
return { success: obj.etag === hash, metaData, obj, equalMeta };
|
|
203
|
+
}
|
|
204
|
+
getMetadata(pathname: string, meta: ItemBucketMetadata = { 'app-source': 'user-app' }) {
|
|
205
|
+
const isHtml = pathname.endsWith('.html');
|
|
206
|
+
if (isHtml) {
|
|
207
|
+
meta = {
|
|
208
|
+
...meta,
|
|
209
|
+
'content-type': 'text/html; charset=utf-8',
|
|
210
|
+
'cache-control': 'no-cache',
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
meta = {
|
|
214
|
+
...meta,
|
|
215
|
+
'content-type': getContentType(pathname),
|
|
216
|
+
'cache-control': 'max-age=31536000, immutable',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return meta;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async copyObject(sourceObject: any, targetObject: any) {
|
|
223
|
+
const bucketName = this.bucketName;
|
|
224
|
+
const obj = await this.client.copyObject(bucketName, sourceObject, targetObject);
|
|
225
|
+
return obj;
|
|
226
|
+
}
|
|
227
|
+
async replaceObject(objectName: string, meta: { [key: string]: string }) {
|
|
228
|
+
const { bucketName, client } = this;
|
|
229
|
+
return copyObject({ bucketName, client, objectName: `${this.prefix}${objectName}`, newMetadata: meta });
|
|
230
|
+
}
|
|
231
|
+
static create<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, opts: OssBaseOptions<U>): T {
|
|
232
|
+
return new this(opts);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static fromBase<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, createOpts: { oss: OssBase; opts: Partial<OssBaseOptions<U>> }): T {
|
|
236
|
+
const base = createOpts.oss;
|
|
237
|
+
const opts = createOpts.opts as any;
|
|
238
|
+
return new this({
|
|
239
|
+
client: base.client,
|
|
240
|
+
bucketName: base.bucketName,
|
|
241
|
+
...opts,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,244 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { ListFileObject, ListObjectResult, OssBaseOperation } from './core/type.ts';
|
|
3
|
-
import { hash } from './util/hash.ts';
|
|
4
|
-
import { copyObject } from './core/copy-object.ts';
|
|
5
|
-
import { getContentType } from './util/get-content-type.ts';
|
|
6
|
-
import { omit } from 'es-toolkit'
|
|
7
|
-
export type OssBaseOptions<T = { [key: string]: any }> = {
|
|
8
|
-
/**
|
|
9
|
-
* 已经初始化好的minio client
|
|
10
|
-
*/
|
|
11
|
-
client: Client;
|
|
12
|
-
/**
|
|
13
|
-
* 桶名
|
|
14
|
-
*/
|
|
15
|
-
bucketName: string;
|
|
16
|
-
/**
|
|
17
|
-
* 前缀
|
|
18
|
-
*/
|
|
19
|
-
prefix?: string;
|
|
20
|
-
} & T;
|
|
21
|
-
|
|
22
|
-
export class OssBase implements OssBaseOperation {
|
|
23
|
-
client?: Client;
|
|
24
|
-
bucketName: string;
|
|
25
|
-
prefix = '';
|
|
26
|
-
/**
|
|
27
|
-
* 计算字符串或者对象的的md5值
|
|
28
|
-
*/
|
|
29
|
-
hash = hash;
|
|
30
|
-
constructor(opts: OssBaseOptions) {
|
|
31
|
-
if (!opts.client) {
|
|
32
|
-
throw new Error('client is required');
|
|
33
|
-
}
|
|
34
|
-
this.bucketName = opts.bucketName;
|
|
35
|
-
this.client = opts.client;
|
|
36
|
-
this.prefix = opts?.prefix ?? '';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
setPrefix(prefix: string) {
|
|
40
|
-
this.prefix = prefix;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async getObject(objectName: string) {
|
|
44
|
-
const bucketName = this.bucketName;
|
|
45
|
-
const obj = await this.client.getObject(bucketName, `${this.prefix}${objectName}`);
|
|
46
|
-
return obj;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async getJson(objectName: string): Promise<Record<string, any>> {
|
|
50
|
-
const obj = await this.getObject(objectName);
|
|
51
|
-
return new Promise((resolve, reject) => {
|
|
52
|
-
let data = '';
|
|
53
|
-
obj.on('data', (chunk) => {
|
|
54
|
-
data += chunk;
|
|
55
|
-
});
|
|
56
|
-
obj.on('end', () => {
|
|
57
|
-
try {
|
|
58
|
-
const jsonData = JSON.parse(data);
|
|
59
|
-
resolve(jsonData);
|
|
60
|
-
} catch (error) {
|
|
61
|
-
reject(new Error('Failed to parse JSON'));
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
obj.on('error', (err) => {
|
|
65
|
-
reject(err);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* 上传文件, 当是流的时候,中断之后的etag会变,所以传递的时候不要嵌套async await,例如 busboy 监听文件流内部的时候,不要用check
|
|
71
|
-
* @param objectName
|
|
72
|
-
* @param data
|
|
73
|
-
* @param metaData
|
|
74
|
-
* @param options 如果文件本身存在,则复制原有的meta的内容
|
|
75
|
-
* @returns
|
|
76
|
-
*/
|
|
77
|
-
async putObject(
|
|
78
|
-
objectName: string,
|
|
79
|
-
data: Buffer | string | Object,
|
|
80
|
-
metaData: ItemBucketMetadata = {},
|
|
81
|
-
opts?: { check?: boolean; isStream?: boolean; size?: number },
|
|
82
|
-
) {
|
|
83
|
-
let putData: Buffer | string;
|
|
84
|
-
let size: number = opts?.size;
|
|
85
|
-
const isStream = opts?.isStream;
|
|
86
|
-
if (!isStream) {
|
|
87
|
-
if (typeof data === 'string') {
|
|
88
|
-
putData = data;
|
|
89
|
-
size = putData.length;
|
|
90
|
-
} else {
|
|
91
|
-
putData = JSON.stringify(data);
|
|
92
|
-
size = putData.length;
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
putData = data as any;
|
|
96
|
-
// 对于流式上传,如果没有提供 size,会导致多部分上传,ETag 会是 ****-1 格式
|
|
97
|
-
// 必须提供准确的 size 才能得到标准的 MD5 格式 ETag
|
|
98
|
-
if (!size) {
|
|
99
|
-
throw new Error('Stream upload requires size parameter to avoid multipart upload and get standard MD5 ETag');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (opts?.check) {
|
|
103
|
-
const obj = await this.statObject(objectName, true);
|
|
104
|
-
if (obj) {
|
|
105
|
-
const omitMeda = ['size', 'content-type', 'cache-control', 'app-source'];
|
|
106
|
-
const objMeta = JSON.parse(JSON.stringify(omit(obj.metaData, omitMeda)));
|
|
107
|
-
metaData = {
|
|
108
|
-
...objMeta,
|
|
109
|
-
...metaData,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const bucketName = this.bucketName;
|
|
115
|
-
const obj = await this.client.putObject(bucketName, `${this.prefix}${objectName}`, putData, size, metaData);
|
|
116
|
-
return obj;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async deleteObject(objectName: string) {
|
|
120
|
-
const bucketName = this.bucketName;
|
|
121
|
-
const obj = await this.client.removeObject(bucketName, `${this.prefix}${objectName}`);
|
|
122
|
-
return obj;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async listObjects<IS_FILE = false>(objectName: string, opts?: { recursive?: boolean; startAfter?: string }) {
|
|
126
|
-
const bucketName = this.bucketName;
|
|
127
|
-
const prefix = `${this.prefix}${objectName}`;
|
|
128
|
-
const res = await new Promise((resolve, reject) => {
|
|
129
|
-
let res: any[] = [];
|
|
130
|
-
let hasError = false;
|
|
131
|
-
this.client
|
|
132
|
-
.listObjectsV2(bucketName, prefix, opts?.recursive ?? false, opts?.startAfter)
|
|
133
|
-
.on('data', (data) => {
|
|
134
|
-
res.push(data);
|
|
135
|
-
})
|
|
136
|
-
.on('error', (err) => {
|
|
137
|
-
console.error('minio error', prefix, err);
|
|
138
|
-
hasError = true;
|
|
139
|
-
})
|
|
140
|
-
.on('end', () => {
|
|
141
|
-
if (hasError) {
|
|
142
|
-
reject();
|
|
143
|
-
return;
|
|
144
|
-
} else {
|
|
145
|
-
resolve(res);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
return res as IS_FILE extends true ? ListFileObject[] : ListObjectResult[];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async fPutObject(objectName: string, filePath: string, metaData?: ItemBucketMetadata) {
|
|
153
|
-
const bucketName = this.bucketName;
|
|
154
|
-
const obj = await this.client.fPutObject(bucketName, `${this.prefix}${objectName}`, filePath, metaData);
|
|
155
|
-
return obj as any;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* 获取完整的对象名称
|
|
159
|
-
* @param objectName
|
|
160
|
-
* @returns
|
|
161
|
-
*/
|
|
162
|
-
async getObjectName(objectName: string) {
|
|
163
|
-
return `${this.prefix}${objectName}`;
|
|
164
|
-
}
|
|
165
|
-
async statObject(objectName: string, checkFile = true) {
|
|
166
|
-
const bucketName = this.bucketName;
|
|
167
|
-
try {
|
|
168
|
-
const obj = await this.client.statObject(bucketName, `${this.prefix}${objectName}`);
|
|
169
|
-
return obj;
|
|
170
|
-
} catch (e) {
|
|
171
|
-
if (e.code === 'NotFound') {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
throw e;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* 检查文件hash是否一致
|
|
179
|
-
* @param objectName
|
|
180
|
-
* @param hash
|
|
181
|
-
* @returns
|
|
182
|
-
*/
|
|
183
|
-
async checkObjectHash(
|
|
184
|
-
objectName: string,
|
|
185
|
-
hash: string,
|
|
186
|
-
meta?: ItemBucketMetadata,
|
|
187
|
-
): Promise<{ success: boolean; metaData: ItemBucketMetadata | null; obj: any; equalMeta?: boolean }> {
|
|
188
|
-
const obj = await this.statObject(`${this.prefix}${objectName}`, true);
|
|
189
|
-
if (!obj) {
|
|
190
|
-
return { success: false, metaData: null, obj: null, equalMeta: false };
|
|
191
|
-
}
|
|
192
|
-
let metaData: ItemBucketMetadata = {};
|
|
193
|
-
const omitMeda = ['content-type', 'cache-control', 'app-source'];
|
|
194
|
-
const objMeta = omit(obj.metaData, omitMeda);
|
|
195
|
-
metaData = {
|
|
196
|
-
...objMeta,
|
|
197
|
-
};
|
|
198
|
-
let equalMeta = false;
|
|
199
|
-
if (meta) {
|
|
200
|
-
equalMeta = JSON.stringify(metaData) === JSON.stringify(meta);
|
|
201
|
-
}
|
|
202
|
-
return { success: obj.etag === hash, metaData, obj, equalMeta };
|
|
203
|
-
}
|
|
204
|
-
getMetadata(pathname: string, meta: ItemBucketMetadata = { 'app-source': 'user-app' }) {
|
|
205
|
-
const isHtml = pathname.endsWith('.html');
|
|
206
|
-
if (isHtml) {
|
|
207
|
-
meta = {
|
|
208
|
-
...meta,
|
|
209
|
-
'content-type': 'text/html; charset=utf-8',
|
|
210
|
-
'cache-control': 'no-cache',
|
|
211
|
-
};
|
|
212
|
-
} else {
|
|
213
|
-
meta = {
|
|
214
|
-
...meta,
|
|
215
|
-
'content-type': getContentType(pathname),
|
|
216
|
-
'cache-control': 'max-age=31536000, immutable',
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
return meta;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async copyObject(sourceObject: any, targetObject: any) {
|
|
223
|
-
const bucketName = this.bucketName;
|
|
224
|
-
const obj = await this.client.copyObject(bucketName, sourceObject, targetObject);
|
|
225
|
-
return obj;
|
|
226
|
-
}
|
|
227
|
-
async replaceObject(objectName: string, meta: { [key: string]: string }) {
|
|
228
|
-
const { bucketName, client } = this;
|
|
229
|
-
return copyObject({ bucketName, client, objectName: `${this.prefix}${objectName}`, newMetadata: meta });
|
|
230
|
-
}
|
|
231
|
-
static create<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, opts: OssBaseOptions<U>): T {
|
|
232
|
-
return new this(opts);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
static fromBase<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, createOpts: { oss: OssBase; opts: Partial<OssBaseOptions<U>> }): T {
|
|
236
|
-
const base = createOpts.oss;
|
|
237
|
-
const opts = createOpts.opts as any;
|
|
238
|
-
return new this({
|
|
239
|
-
client: base.client,
|
|
240
|
-
bucketName: base.bucketName,
|
|
241
|
-
...opts,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
1
|
+
export * from './s3/core.ts'
|
package/src/s3/core.ts
CHANGED
|
@@ -137,7 +137,7 @@ export class OssBase implements OssBaseOperation {
|
|
|
137
137
|
if (opts?.check) {
|
|
138
138
|
const obj = await this.statObject(objectName, true);
|
|
139
139
|
if (obj) {
|
|
140
|
-
const omitMeta = ['size', '
|
|
140
|
+
const omitMeta = ['size', 'Content-Type', 'Cache-Control', 'app-source'];
|
|
141
141
|
const objMeta = JSON.parse(JSON.stringify(omit(obj.metaData, omitMeta)));
|
|
142
142
|
metaData = {
|
|
143
143
|
...objMeta,
|
|
@@ -217,6 +217,11 @@ export class OssBase implements OssBaseOperation {
|
|
|
217
217
|
});
|
|
218
218
|
await this.client.send(command);
|
|
219
219
|
}
|
|
220
|
+
async deleteObjects(objectNameList: string[]): Promise<void> {
|
|
221
|
+
for (const objectName of objectNameList) {
|
|
222
|
+
await this.deleteObject(objectName);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
220
225
|
|
|
221
226
|
/**
|
|
222
227
|
* 列出对象
|
|
@@ -286,13 +291,24 @@ export class OssBase implements OssBaseOperation {
|
|
|
286
291
|
|
|
287
292
|
return {
|
|
288
293
|
size: response.ContentLength || 0,
|
|
289
|
-
etag: response.ETag?.replace(/"/g, '') || '',
|
|
294
|
+
etag: response.ETag?.replace?.(/"/g, '') || '',
|
|
290
295
|
lastModified: response.LastModified || new Date(),
|
|
291
296
|
metaData: (response.Metadata as ItemBucketMetadata) || {},
|
|
292
297
|
versionId: response.VersionId || null,
|
|
293
298
|
};
|
|
294
299
|
} catch (e: any) {
|
|
295
|
-
|
|
300
|
+
console.error('statObject error', e);
|
|
301
|
+
// 检查是否是 404 错误 - 支持多种 S3 兼容存储的错误格式
|
|
302
|
+
const isNotFound =
|
|
303
|
+
e.name === 'NotFound' ||
|
|
304
|
+
e.name === 'NoSuchBucket' ||
|
|
305
|
+
e.name === 'NoSuchKey' ||
|
|
306
|
+
e.code === 'NotFound' ||
|
|
307
|
+
e.code === 'NoSuchBucket' ||
|
|
308
|
+
e.code === 'NoSuchKey' ||
|
|
309
|
+
e.$metadata?.httpStatusCode === 404;
|
|
310
|
+
|
|
311
|
+
if (checkFile && isNotFound) {
|
|
296
312
|
return null;
|
|
297
313
|
}
|
|
298
314
|
throw e;
|
|
@@ -322,7 +338,7 @@ export class OssBase implements OssBaseOperation {
|
|
|
322
338
|
if (!obj) {
|
|
323
339
|
return { success: false, metaData: null, obj: null, equalMeta: false };
|
|
324
340
|
}
|
|
325
|
-
const omitMeta = ['
|
|
341
|
+
const omitMeta = ['Content-Type', 'Cache-Control', 'app-source'];
|
|
326
342
|
const metaData = omit(obj.metaData, omitMeta);
|
|
327
343
|
let equalMeta = false;
|
|
328
344
|
if (meta) {
|
|
@@ -341,14 +357,14 @@ export class OssBase implements OssBaseOperation {
|
|
|
341
357
|
if (isHtml) {
|
|
342
358
|
meta = {
|
|
343
359
|
...meta,
|
|
344
|
-
'
|
|
345
|
-
'
|
|
360
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
361
|
+
'Cache-Control': 'no-cache',
|
|
346
362
|
};
|
|
347
363
|
} else {
|
|
348
364
|
meta = {
|
|
349
365
|
...meta,
|
|
350
|
-
'
|
|
351
|
-
'
|
|
366
|
+
'Content-Type': getContentType(pathname),
|
|
367
|
+
'Cache-Control': 'max-age=31536000, immutable',
|
|
352
368
|
};
|
|
353
369
|
}
|
|
354
370
|
return meta;
|
package/src/services/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OssBase, OssBaseOptions } from '../
|
|
2
|
-
import { OssService } from '../
|
|
1
|
+
import { OssBase, OssBaseOptions } from '../s3/core.ts';
|
|
2
|
+
import { OssService } from '../s3/type.ts';
|
|
3
3
|
import * as util from '../util/index.ts';
|
|
4
4
|
|
|
5
5
|
export class ConfigOssService extends OssBase implements OssService {
|
package/src/test/common.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { S3Client, ListObjectsV2Command, GetBucketMetadataConfigurationCommand,
|
|
|
6
6
|
export const s3Client = new S3Client({
|
|
7
7
|
credentials: {
|
|
8
8
|
accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
|
|
9
|
-
secretAccessKey: process.env.
|
|
9
|
+
secretAccessKey: process.env.S3_ACCESS_KEY_SECRET || '',
|
|
10
10
|
},
|
|
11
11
|
region: process.env.S3_REGION,
|
|
12
12
|
endpoint: 'https://tos-s3-cn-shanghai.volces.com',
|