@h-ai/storage 0.1.0-alpha5
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/LICENSE +202 -0
- package/README.md +151 -0
- package/dist/api/index.d.ts +167 -0
- package/dist/api/index.js +105 -0
- package/dist/api/index.js.map +1 -0
- package/dist/browser.d.ts +506 -0
- package/dist/browser.js +4 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-KHZOR6CR.js +76 -0
- package/dist/chunk-KHZOR6CR.js.map +1 -0
- package/dist/chunk-U427BQIQ.js +266 -0
- package/dist/chunk-U427BQIQ.js.map +1 -0
- package/dist/client/index.d.ts +172 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/node.d.ts +37 -0
- package/dist/node.js +959 -0
- package/dist/node.js.map +1 -0
- package/package.json +56 -0
package/dist/node.js
ADDED
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
import { HaiStorageError, StorageConfigSchema } from './chunk-KHZOR6CR.js';
|
|
2
|
+
export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageTypeSchema } from './chunk-KHZOR6CR.js';
|
|
3
|
+
import { storageM, MIME_TYPES, MIME_TYPE_DEFAULT } from './chunk-U427BQIQ.js';
|
|
4
|
+
export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-U427BQIQ.js';
|
|
5
|
+
import { core, err, ok } from '@h-ai/core';
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as fsp from 'fs/promises';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, CopyObjectCommand, DeleteObjectsCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
|
12
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
13
|
+
|
|
14
|
+
var logger = core.logger.child({ module: "storage", scope: "provider-local" });
|
|
15
|
+
function toStorageError(error, key) {
|
|
16
|
+
const e = error;
|
|
17
|
+
if (e.code === "ENOENT") {
|
|
18
|
+
return {
|
|
19
|
+
...HaiStorageError.NOT_FOUND,
|
|
20
|
+
message: storageM("storage_fileNotFound", { params: { key: key ?? "" } }),
|
|
21
|
+
cause: error
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (e.code === "EACCES" || e.code === "EPERM") {
|
|
25
|
+
return {
|
|
26
|
+
...HaiStorageError.PERMISSION_DENIED,
|
|
27
|
+
message: storageM("storage_permissionDenied", { params: { key: key ?? "" } }),
|
|
28
|
+
cause: error
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (e.code === "ENOSPC") {
|
|
32
|
+
return {
|
|
33
|
+
...HaiStorageError.QUOTA_EXCEEDED,
|
|
34
|
+
message: storageM("storage_diskSpaceInsufficient"),
|
|
35
|
+
cause: error
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (e.code === "EISDIR") {
|
|
39
|
+
return {
|
|
40
|
+
...HaiStorageError.INVALID_PATH,
|
|
41
|
+
message: storageM("storage_pathIsDir", { params: { key: key ?? "" } }),
|
|
42
|
+
cause: error
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (e.code === "EINVAL") {
|
|
46
|
+
return {
|
|
47
|
+
...HaiStorageError.INVALID_PATH,
|
|
48
|
+
message: e.message || storageM("storage_pathTraversal"),
|
|
49
|
+
cause: error
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
code: HaiStorageError.IO_ERROR.code,
|
|
54
|
+
message: storageM("storage_ioError", { params: { error: e.message ?? "" } }),
|
|
55
|
+
cause: error
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function getMimeType2(filePath) {
|
|
59
|
+
const ext = path.extname(filePath).toLowerCase().replace(".", "");
|
|
60
|
+
return MIME_TYPES[ext] || MIME_TYPE_DEFAULT;
|
|
61
|
+
}
|
|
62
|
+
function calculateEtag(stat2) {
|
|
63
|
+
return `"${stat2.size.toString(16)}-${stat2.mtimeMs.toString(16)}"`;
|
|
64
|
+
}
|
|
65
|
+
function isMetaFile(key) {
|
|
66
|
+
return key.endsWith(".meta.json");
|
|
67
|
+
}
|
|
68
|
+
function createInvalidPathError() {
|
|
69
|
+
const error = new Error(storageM("storage_pathTraversal"));
|
|
70
|
+
error.code = "EINVAL";
|
|
71
|
+
return error;
|
|
72
|
+
}
|
|
73
|
+
function hasPathTraversalSegment(key) {
|
|
74
|
+
const segments = key.split(/[\\/]+/).filter(Boolean);
|
|
75
|
+
return segments.includes("..");
|
|
76
|
+
}
|
|
77
|
+
function safePath(root, key) {
|
|
78
|
+
if (path.isAbsolute(key)) {
|
|
79
|
+
throw createInvalidPathError();
|
|
80
|
+
}
|
|
81
|
+
if (key.includes("\0") || hasPathTraversalSegment(key)) {
|
|
82
|
+
throw createInvalidPathError();
|
|
83
|
+
}
|
|
84
|
+
const normalized = path.normalize(key);
|
|
85
|
+
const fullPath = path.resolve(root, normalized);
|
|
86
|
+
const realRoot = path.resolve(root);
|
|
87
|
+
const inRoot = fullPath === realRoot || fullPath.startsWith(`${realRoot}${path.sep}`);
|
|
88
|
+
if (!inRoot) {
|
|
89
|
+
throw createInvalidPathError();
|
|
90
|
+
}
|
|
91
|
+
return fullPath;
|
|
92
|
+
}
|
|
93
|
+
function createLocalProvider() {
|
|
94
|
+
let config = null;
|
|
95
|
+
let connected = false;
|
|
96
|
+
function getConfig() {
|
|
97
|
+
if (!config) {
|
|
98
|
+
throw new Error(storageM("storage_localNotInitialized"));
|
|
99
|
+
}
|
|
100
|
+
return config;
|
|
101
|
+
}
|
|
102
|
+
function fullPath(key) {
|
|
103
|
+
return safePath(getConfig().root, key);
|
|
104
|
+
}
|
|
105
|
+
function toBuffer(data) {
|
|
106
|
+
if (typeof data === "string") {
|
|
107
|
+
return Buffer.from(data);
|
|
108
|
+
}
|
|
109
|
+
if (data instanceof Buffer) {
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
return Buffer.from(data);
|
|
113
|
+
}
|
|
114
|
+
const file = {
|
|
115
|
+
async put(key, data, options = {}) {
|
|
116
|
+
if (isMetaFile(key)) {
|
|
117
|
+
return err(
|
|
118
|
+
HaiStorageError.INVALID_PATH,
|
|
119
|
+
storageM("storage_metaFileAccess", { params: { key } })
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const filePath = fullPath(key);
|
|
124
|
+
const buffer = toBuffer(data);
|
|
125
|
+
logger.debug("Putting file", { key, size: buffer.length });
|
|
126
|
+
const dir2 = path.dirname(filePath);
|
|
127
|
+
await fsp.mkdir(dir2, { recursive: true, mode: getConfig().directoryMode });
|
|
128
|
+
await fsp.writeFile(filePath, buffer, { mode: getConfig().fileMode });
|
|
129
|
+
const stat2 = await fsp.stat(filePath);
|
|
130
|
+
const metadata = {
|
|
131
|
+
key,
|
|
132
|
+
size: stat2.size,
|
|
133
|
+
contentType: options.contentType || getMimeType2(key),
|
|
134
|
+
lastModified: stat2.mtime,
|
|
135
|
+
etag: calculateEtag(stat2),
|
|
136
|
+
metadata: options.metadata
|
|
137
|
+
};
|
|
138
|
+
if (options.metadata) {
|
|
139
|
+
const metaPath = `${filePath}.meta.json`;
|
|
140
|
+
await fsp.writeFile(metaPath, JSON.stringify({
|
|
141
|
+
contentType: options.contentType,
|
|
142
|
+
metadata: options.metadata
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
return ok(metadata);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.warn("Put file failed", { key, error });
|
|
148
|
+
return err(toStorageError(error, key));
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async get(key, options = {}) {
|
|
152
|
+
if (isMetaFile(key)) {
|
|
153
|
+
return err(
|
|
154
|
+
HaiStorageError.INVALID_PATH,
|
|
155
|
+
storageM("storage_metaFileAccess", { params: { key } })
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const filePath = fullPath(key);
|
|
160
|
+
if (options.rangeStart !== void 0 || options.rangeEnd !== void 0) {
|
|
161
|
+
const stat2 = await fsp.stat(filePath);
|
|
162
|
+
const start = options.rangeStart ?? 0;
|
|
163
|
+
const end = options.rangeEnd !== void 0 ? options.rangeEnd : stat2.size - 1;
|
|
164
|
+
return new Promise((resolve2) => {
|
|
165
|
+
const chunks = [];
|
|
166
|
+
const stream = fs.createReadStream(filePath, { start, end });
|
|
167
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
168
|
+
stream.on("end", () => resolve2(ok(Buffer.concat(chunks))));
|
|
169
|
+
stream.on("error", (error) => resolve2(err(toStorageError(error, key))));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const data = await fsp.readFile(filePath);
|
|
173
|
+
return ok(data);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return err(toStorageError(error, key));
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
async head(key) {
|
|
179
|
+
if (isMetaFile(key)) {
|
|
180
|
+
return err(
|
|
181
|
+
HaiStorageError.INVALID_PATH,
|
|
182
|
+
storageM("storage_metaFileAccess", { params: { key } })
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const filePath = fullPath(key);
|
|
187
|
+
const stat2 = await fsp.stat(filePath);
|
|
188
|
+
if (stat2.isDirectory()) {
|
|
189
|
+
return err(
|
|
190
|
+
HaiStorageError.INVALID_PATH,
|
|
191
|
+
storageM("storage_pathIsDir", { params: { key } })
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
let customMetadata = {};
|
|
195
|
+
try {
|
|
196
|
+
const metaPath = `${filePath}.meta.json`;
|
|
197
|
+
const metaContent = await fsp.readFile(metaPath, "utf-8");
|
|
198
|
+
customMetadata = JSON.parse(metaContent);
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
const etag = `"${stat2.size.toString(16)}-${stat2.mtimeMs.toString(16)}"`;
|
|
202
|
+
return ok({
|
|
203
|
+
key,
|
|
204
|
+
size: stat2.size,
|
|
205
|
+
contentType: customMetadata.contentType || getMimeType2(key),
|
|
206
|
+
lastModified: stat2.mtime,
|
|
207
|
+
etag,
|
|
208
|
+
metadata: customMetadata.metadata
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return err(toStorageError(error, key));
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
async exists(key) {
|
|
215
|
+
if (isMetaFile(key)) {
|
|
216
|
+
return err(
|
|
217
|
+
HaiStorageError.INVALID_PATH,
|
|
218
|
+
storageM("storage_metaFileAccess", { params: { key } })
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const filePath = fullPath(key);
|
|
223
|
+
await fsp.access(filePath, fs.constants.F_OK);
|
|
224
|
+
return ok(true);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error.code === "ENOENT") {
|
|
227
|
+
return ok(false);
|
|
228
|
+
}
|
|
229
|
+
return err(toStorageError(error, key));
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
async delete(key) {
|
|
233
|
+
if (isMetaFile(key)) {
|
|
234
|
+
return err(
|
|
235
|
+
HaiStorageError.INVALID_PATH,
|
|
236
|
+
storageM("storage_metaFileAccess", { params: { key } })
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const filePath = fullPath(key);
|
|
241
|
+
logger.debug("Deleting file", { key });
|
|
242
|
+
await fsp.unlink(filePath);
|
|
243
|
+
try {
|
|
244
|
+
await fsp.unlink(`${filePath}.meta.json`);
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
return ok(void 0);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error.code === "ENOENT") {
|
|
250
|
+
return ok(void 0);
|
|
251
|
+
}
|
|
252
|
+
logger.warn("Delete file failed", { key, error });
|
|
253
|
+
return err(toStorageError(error, key));
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
async deleteMany(keys) {
|
|
257
|
+
const results = await Promise.allSettled(keys.map((key) => file.delete(key)));
|
|
258
|
+
const errors = [];
|
|
259
|
+
for (const result of results) {
|
|
260
|
+
if (result.status === "fulfilled" && !result.value.success) {
|
|
261
|
+
errors.push(result.value.error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (errors.length > 0) {
|
|
265
|
+
return err(
|
|
266
|
+
HaiStorageError.OPERATION_FAILED,
|
|
267
|
+
storageM("storage_deleteManyFailed", { params: { count: errors.length } }),
|
|
268
|
+
errors
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return ok(void 0);
|
|
272
|
+
},
|
|
273
|
+
async copy(sourceKey, destKey, options = {}) {
|
|
274
|
+
if (isMetaFile(sourceKey) || isMetaFile(destKey)) {
|
|
275
|
+
return err(
|
|
276
|
+
HaiStorageError.INVALID_PATH,
|
|
277
|
+
storageM("storage_metaFileAccess", { params: { key: sourceKey } }),
|
|
278
|
+
sourceKey
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const sourcePath = fullPath(sourceKey);
|
|
283
|
+
const destPath = fullPath(destKey);
|
|
284
|
+
logger.debug("Copying file", { sourceKey, destKey });
|
|
285
|
+
await fsp.mkdir(path.dirname(destPath), { recursive: true, mode: getConfig().directoryMode });
|
|
286
|
+
await fsp.copyFile(sourcePath, destPath);
|
|
287
|
+
const stat2 = await fsp.stat(destPath);
|
|
288
|
+
const metadata = {
|
|
289
|
+
key: destKey,
|
|
290
|
+
size: stat2.size,
|
|
291
|
+
contentType: options.contentType || getMimeType2(destKey),
|
|
292
|
+
lastModified: stat2.mtime,
|
|
293
|
+
etag: calculateEtag(stat2),
|
|
294
|
+
metadata: options.metadata
|
|
295
|
+
};
|
|
296
|
+
if (options.contentType || options.metadata) {
|
|
297
|
+
const metaPath = `${destPath}.meta.json`;
|
|
298
|
+
await fsp.writeFile(metaPath, JSON.stringify({
|
|
299
|
+
contentType: options.contentType,
|
|
300
|
+
metadata: options.metadata
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
return ok(metadata);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return err(toStorageError(error, sourceKey));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const dir = {
|
|
310
|
+
async list(options = {}) {
|
|
311
|
+
try {
|
|
312
|
+
const cfg = getConfig();
|
|
313
|
+
const prefix = options.prefix || "";
|
|
314
|
+
const delimiter = options.delimiter || "";
|
|
315
|
+
const maxKeys = options.maxKeys || 1e3;
|
|
316
|
+
const basePath = safePath(cfg.root, prefix);
|
|
317
|
+
const files = [];
|
|
318
|
+
const commonPrefixes = /* @__PURE__ */ new Set();
|
|
319
|
+
async function readDir(dirPath) {
|
|
320
|
+
let entries;
|
|
321
|
+
try {
|
|
322
|
+
entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
323
|
+
} catch {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (files.length >= maxKeys)
|
|
328
|
+
return;
|
|
329
|
+
const fullEntryPath = path.join(dirPath, entry.name);
|
|
330
|
+
const key = path.relative(cfg.root, fullEntryPath).replace(/\\/g, "/");
|
|
331
|
+
if (entry.name.endsWith(".meta.json"))
|
|
332
|
+
continue;
|
|
333
|
+
if (entry.isDirectory()) {
|
|
334
|
+
if (delimiter) {
|
|
335
|
+
commonPrefixes.add(`${key}/`);
|
|
336
|
+
} else {
|
|
337
|
+
await readDir(fullEntryPath);
|
|
338
|
+
}
|
|
339
|
+
} else if (entry.isFile()) {
|
|
340
|
+
const stat2 = await fsp.stat(fullEntryPath);
|
|
341
|
+
files.push({
|
|
342
|
+
key,
|
|
343
|
+
size: stat2.size,
|
|
344
|
+
contentType: getMimeType2(key),
|
|
345
|
+
lastModified: stat2.mtime
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
await readDir(basePath);
|
|
351
|
+
return ok({
|
|
352
|
+
files,
|
|
353
|
+
commonPrefixes: Array.from(commonPrefixes),
|
|
354
|
+
isTruncated: files.length >= maxKeys
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return err(toStorageError(error));
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
async delete(prefix) {
|
|
361
|
+
try {
|
|
362
|
+
const dirPath = fullPath(prefix);
|
|
363
|
+
await fsp.rm(dirPath, { recursive: true, force: true });
|
|
364
|
+
return ok(void 0);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (error.code === "ENOENT") {
|
|
367
|
+
return ok(void 0);
|
|
368
|
+
}
|
|
369
|
+
return err(toStorageError(error, prefix));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const presign = {
|
|
374
|
+
async getUrl(key, options) {
|
|
375
|
+
const expiresIn = options?.expiresIn || 3600;
|
|
376
|
+
const expires = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
377
|
+
const signature = crypto.createHash("sha256").update(`${key}:${expires}`).digest("hex").slice(0, 16);
|
|
378
|
+
return ok(`local://${key}?expires=${expires}&signature=${signature}`);
|
|
379
|
+
},
|
|
380
|
+
async putUrl(key, options) {
|
|
381
|
+
const expiresIn = options?.expiresIn || 3600;
|
|
382
|
+
const expires = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
383
|
+
const signature = crypto.createHash("sha256").update(`put:${key}:${expires}`).digest("hex").slice(0, 16);
|
|
384
|
+
return ok(`local://${key}?action=put&expires=${expires}&signature=${signature}`);
|
|
385
|
+
},
|
|
386
|
+
publicUrl(_key) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
return {
|
|
391
|
+
name: "local",
|
|
392
|
+
file,
|
|
393
|
+
dir,
|
|
394
|
+
presign,
|
|
395
|
+
async connect(cfg) {
|
|
396
|
+
if (cfg.type !== "local") {
|
|
397
|
+
return err(
|
|
398
|
+
HaiStorageError.CONFIG_ERROR,
|
|
399
|
+
storageM("storage_localConfigTypeError")
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
config = cfg;
|
|
404
|
+
logger.info("Connecting local provider", { root: cfg.root });
|
|
405
|
+
await fsp.mkdir(cfg.root, { recursive: true, mode: cfg.directoryMode });
|
|
406
|
+
connected = true;
|
|
407
|
+
logger.info("Local provider connected", { root: cfg.root });
|
|
408
|
+
return ok(void 0);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
logger.error("Local provider connect failed", { error });
|
|
411
|
+
config = null;
|
|
412
|
+
return err(toStorageError(error));
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
async close() {
|
|
416
|
+
logger.info("Local provider disconnected");
|
|
417
|
+
config = null;
|
|
418
|
+
connected = false;
|
|
419
|
+
},
|
|
420
|
+
isConnected() {
|
|
421
|
+
return connected;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
var logger2 = core.logger.child({ module: "storage", scope: "provider-s3" });
|
|
426
|
+
function sanitizeEndpointUrl(url) {
|
|
427
|
+
try {
|
|
428
|
+
const parsed = new URL(url);
|
|
429
|
+
if (parsed.username)
|
|
430
|
+
parsed.username = "***";
|
|
431
|
+
if (parsed.password)
|
|
432
|
+
parsed.password = "***";
|
|
433
|
+
return parsed.toString();
|
|
434
|
+
} catch {
|
|
435
|
+
return "(invalid url)";
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function toStorageError2(error, key) {
|
|
439
|
+
const e = error;
|
|
440
|
+
if (e.name === "NoSuchKey" || e.Code === "NoSuchKey" || e.name === "NotFound" || e.Code === "NotFound" || e.name === "404" || e.$metadata?.httpStatusCode === 404) {
|
|
441
|
+
return {
|
|
442
|
+
...HaiStorageError.NOT_FOUND,
|
|
443
|
+
message: storageM("storage_fileNotFound", { params: { key: key ?? "" } }),
|
|
444
|
+
cause: error
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (e.name === "AccessDenied" || e.Code === "AccessDenied") {
|
|
448
|
+
return {
|
|
449
|
+
...HaiStorageError.PERMISSION_DENIED,
|
|
450
|
+
message: storageM("storage_permissionDenied", { params: { key: key ?? "" } }),
|
|
451
|
+
cause: error
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (e.name === "NoSuchBucket" || e.Code === "NoSuchBucket") {
|
|
455
|
+
return {
|
|
456
|
+
...HaiStorageError.CONFIG_ERROR,
|
|
457
|
+
message: storageM("storage_bucketNotExist"),
|
|
458
|
+
cause: error
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (e.name === "NetworkError" || e.name?.includes("ECONNREFUSED")) {
|
|
462
|
+
return {
|
|
463
|
+
...HaiStorageError.NETWORK_ERROR,
|
|
464
|
+
message: storageM("storage_networkError"),
|
|
465
|
+
cause: error
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
...HaiStorageError.OPERATION_FAILED,
|
|
470
|
+
message: storageM("storage_operationFailed", { params: { error: e.message ?? "" } }),
|
|
471
|
+
cause: error
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function withPrefix(key, prefix) {
|
|
475
|
+
if (!prefix)
|
|
476
|
+
return key;
|
|
477
|
+
return `${prefix.replace(/\/+$/, "")}/${key.replace(/^\/+/, "")}`;
|
|
478
|
+
}
|
|
479
|
+
function withoutPrefix(key, prefix) {
|
|
480
|
+
if (!prefix)
|
|
481
|
+
return key;
|
|
482
|
+
const normalizedPrefix = `${prefix.replace(/\/+$/, "")}/`;
|
|
483
|
+
if (key.startsWith(normalizedPrefix)) {
|
|
484
|
+
return key.slice(normalizedPrefix.length);
|
|
485
|
+
}
|
|
486
|
+
return key;
|
|
487
|
+
}
|
|
488
|
+
function createS3Provider() {
|
|
489
|
+
let client = null;
|
|
490
|
+
let config = null;
|
|
491
|
+
function getClient() {
|
|
492
|
+
if (!client) {
|
|
493
|
+
throw new Error(storageM("storage_s3ClientNotInitialized"));
|
|
494
|
+
}
|
|
495
|
+
return client;
|
|
496
|
+
}
|
|
497
|
+
function getConfig() {
|
|
498
|
+
if (!config) {
|
|
499
|
+
throw new Error(storageM("storage_s3ConfigNotInitialized"));
|
|
500
|
+
}
|
|
501
|
+
return config;
|
|
502
|
+
}
|
|
503
|
+
function fullKey(key) {
|
|
504
|
+
return withPrefix(key, getConfig().prefix);
|
|
505
|
+
}
|
|
506
|
+
const file = {
|
|
507
|
+
async put(key, data, options = {}) {
|
|
508
|
+
try {
|
|
509
|
+
const s3Client = getClient();
|
|
510
|
+
const s3Config = getConfig();
|
|
511
|
+
const fullPath = fullKey(key);
|
|
512
|
+
const body = typeof data === "string" ? Buffer.from(data) : data;
|
|
513
|
+
await s3Client.send(new PutObjectCommand({
|
|
514
|
+
Bucket: s3Config.bucket,
|
|
515
|
+
Key: fullPath,
|
|
516
|
+
Body: body,
|
|
517
|
+
ContentType: options.contentType,
|
|
518
|
+
Metadata: options.metadata,
|
|
519
|
+
CacheControl: options.cacheControl,
|
|
520
|
+
ContentDisposition: options.contentDisposition
|
|
521
|
+
}));
|
|
522
|
+
const headResult = await file.head(key);
|
|
523
|
+
if (headResult.success) {
|
|
524
|
+
return headResult;
|
|
525
|
+
}
|
|
526
|
+
return ok({
|
|
527
|
+
key,
|
|
528
|
+
size: body.length,
|
|
529
|
+
contentType: options.contentType || "application/octet-stream",
|
|
530
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
531
|
+
});
|
|
532
|
+
} catch (error) {
|
|
533
|
+
return err(toStorageError2(error, key));
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
async get(key, options = {}) {
|
|
537
|
+
try {
|
|
538
|
+
const s3Client = getClient();
|
|
539
|
+
const s3Config = getConfig();
|
|
540
|
+
const fullPath = fullKey(key);
|
|
541
|
+
let Range;
|
|
542
|
+
if (options.rangeStart !== void 0 || options.rangeEnd !== void 0) {
|
|
543
|
+
const start = options.rangeStart ?? 0;
|
|
544
|
+
const end = options.rangeEnd !== void 0 ? options.rangeEnd : "";
|
|
545
|
+
Range = `bytes=${start}-${end}`;
|
|
546
|
+
}
|
|
547
|
+
const response = await s3Client.send(new GetObjectCommand({
|
|
548
|
+
Bucket: s3Config.bucket,
|
|
549
|
+
Key: fullPath,
|
|
550
|
+
Range
|
|
551
|
+
}));
|
|
552
|
+
if (!response.Body) {
|
|
553
|
+
return err(
|
|
554
|
+
HaiStorageError.OPERATION_FAILED,
|
|
555
|
+
storageM("storage_responseBodyEmpty")
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const chunks = [];
|
|
559
|
+
const stream = response.Body;
|
|
560
|
+
for await (const chunk of stream) {
|
|
561
|
+
chunks.push(chunk);
|
|
562
|
+
}
|
|
563
|
+
return ok(Buffer.concat(chunks));
|
|
564
|
+
} catch (error) {
|
|
565
|
+
return err(toStorageError2(error, key));
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
async head(key) {
|
|
569
|
+
try {
|
|
570
|
+
const s3Client = getClient();
|
|
571
|
+
const s3Config = getConfig();
|
|
572
|
+
const fullPath = fullKey(key);
|
|
573
|
+
const response = await s3Client.send(new HeadObjectCommand({
|
|
574
|
+
Bucket: s3Config.bucket,
|
|
575
|
+
Key: fullPath
|
|
576
|
+
}));
|
|
577
|
+
return ok({
|
|
578
|
+
key,
|
|
579
|
+
size: response.ContentLength || 0,
|
|
580
|
+
contentType: response.ContentType || "application/octet-stream",
|
|
581
|
+
lastModified: response.LastModified || /* @__PURE__ */ new Date(),
|
|
582
|
+
etag: response.ETag,
|
|
583
|
+
metadata: response.Metadata
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
return err(toStorageError2(error, key));
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
async exists(key) {
|
|
590
|
+
const result = await file.head(key);
|
|
591
|
+
if (result.success) {
|
|
592
|
+
return ok(true);
|
|
593
|
+
}
|
|
594
|
+
if (result.error.code === HaiStorageError.NOT_FOUND.code) {
|
|
595
|
+
return ok(false);
|
|
596
|
+
}
|
|
597
|
+
return err(result.error);
|
|
598
|
+
},
|
|
599
|
+
async delete(key) {
|
|
600
|
+
try {
|
|
601
|
+
const s3Client = getClient();
|
|
602
|
+
const s3Config = getConfig();
|
|
603
|
+
const fullPath = fullKey(key);
|
|
604
|
+
await s3Client.send(new DeleteObjectCommand({
|
|
605
|
+
Bucket: s3Config.bucket,
|
|
606
|
+
Key: fullPath
|
|
607
|
+
}));
|
|
608
|
+
return ok(void 0);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
return err(toStorageError2(error, key));
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
async deleteMany(keys) {
|
|
614
|
+
try {
|
|
615
|
+
const s3Client = getClient();
|
|
616
|
+
const s3Config = getConfig();
|
|
617
|
+
if (keys.length === 0) {
|
|
618
|
+
return ok(void 0);
|
|
619
|
+
}
|
|
620
|
+
await s3Client.send(new DeleteObjectsCommand({
|
|
621
|
+
Bucket: s3Config.bucket,
|
|
622
|
+
Delete: {
|
|
623
|
+
Objects: keys.map((key) => ({ Key: fullKey(key) }))
|
|
624
|
+
}
|
|
625
|
+
}));
|
|
626
|
+
return ok(void 0);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
return err(toStorageError2(error));
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
async copy(sourceKey, destKey, options = {}) {
|
|
632
|
+
try {
|
|
633
|
+
const s3Client = getClient();
|
|
634
|
+
const s3Config = getConfig();
|
|
635
|
+
const sourceFullPath = fullKey(sourceKey);
|
|
636
|
+
const destFullPath = fullKey(destKey);
|
|
637
|
+
await s3Client.send(new CopyObjectCommand({
|
|
638
|
+
Bucket: s3Config.bucket,
|
|
639
|
+
CopySource: `${s3Config.bucket}/${sourceFullPath}`,
|
|
640
|
+
Key: destFullPath,
|
|
641
|
+
ContentType: options.contentType,
|
|
642
|
+
Metadata: options.metadata,
|
|
643
|
+
MetadataDirective: options.metadata || options.contentType ? "REPLACE" : "COPY"
|
|
644
|
+
}));
|
|
645
|
+
return file.head(destKey);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
return err(toStorageError2(error, sourceKey));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const dir = {
|
|
652
|
+
async list(options = {}) {
|
|
653
|
+
try {
|
|
654
|
+
const s3Client = getClient();
|
|
655
|
+
const s3Config = getConfig();
|
|
656
|
+
let prefix = s3Config.prefix || "";
|
|
657
|
+
if (options.prefix) {
|
|
658
|
+
prefix = withPrefix(options.prefix, s3Config.prefix);
|
|
659
|
+
}
|
|
660
|
+
const response = await s3Client.send(new ListObjectsV2Command({
|
|
661
|
+
Bucket: s3Config.bucket,
|
|
662
|
+
Prefix: prefix || void 0,
|
|
663
|
+
Delimiter: options.delimiter,
|
|
664
|
+
MaxKeys: options.maxKeys,
|
|
665
|
+
ContinuationToken: options.continuationToken
|
|
666
|
+
}));
|
|
667
|
+
const files = (response.Contents || []).map((item) => ({
|
|
668
|
+
key: withoutPrefix(item.Key || "", s3Config.prefix),
|
|
669
|
+
size: item.Size || 0,
|
|
670
|
+
contentType: "application/octet-stream",
|
|
671
|
+
// ListObjects 不返回 ContentType
|
|
672
|
+
lastModified: item.LastModified || /* @__PURE__ */ new Date(),
|
|
673
|
+
etag: item.ETag
|
|
674
|
+
}));
|
|
675
|
+
const commonPrefixes = (response.CommonPrefixes || []).map((p) => withoutPrefix(p.Prefix || "", s3Config.prefix)).filter((p) => p.length > 0);
|
|
676
|
+
return ok({
|
|
677
|
+
files,
|
|
678
|
+
commonPrefixes,
|
|
679
|
+
nextContinuationToken: response.NextContinuationToken,
|
|
680
|
+
isTruncated: response.IsTruncated || false
|
|
681
|
+
});
|
|
682
|
+
} catch (error) {
|
|
683
|
+
return err(toStorageError2(error));
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
async delete(prefix) {
|
|
687
|
+
try {
|
|
688
|
+
let continuationToken;
|
|
689
|
+
do {
|
|
690
|
+
const listResult = await dir.list({
|
|
691
|
+
prefix,
|
|
692
|
+
continuationToken,
|
|
693
|
+
maxKeys: 1e3
|
|
694
|
+
});
|
|
695
|
+
if (!listResult.success) {
|
|
696
|
+
return err(listResult.error);
|
|
697
|
+
}
|
|
698
|
+
const keys = listResult.data.files.map((f) => f.key);
|
|
699
|
+
if (keys.length > 0) {
|
|
700
|
+
const deleteResult = await file.deleteMany(keys);
|
|
701
|
+
if (!deleteResult.success) {
|
|
702
|
+
return deleteResult;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
continuationToken = listResult.data.nextContinuationToken;
|
|
706
|
+
} while (continuationToken);
|
|
707
|
+
return ok(void 0);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
return err(toStorageError2(error));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
const presign = {
|
|
714
|
+
async getUrl(key, options) {
|
|
715
|
+
try {
|
|
716
|
+
const s3Client = getClient();
|
|
717
|
+
const s3Config = getConfig();
|
|
718
|
+
const fullPath = fullKey(key);
|
|
719
|
+
const command = new GetObjectCommand({
|
|
720
|
+
Bucket: s3Config.bucket,
|
|
721
|
+
Key: fullPath,
|
|
722
|
+
ResponseContentType: options?.responseContentType,
|
|
723
|
+
ResponseContentDisposition: options?.responseContentDisposition
|
|
724
|
+
});
|
|
725
|
+
const url = await getSignedUrl(s3Client, command, {
|
|
726
|
+
expiresIn: options?.expiresIn || 3600
|
|
727
|
+
});
|
|
728
|
+
return ok(url);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
return err(
|
|
731
|
+
HaiStorageError.PRESIGN_FAILED,
|
|
732
|
+
storageM("storage_presignUrlFailed"),
|
|
733
|
+
error
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
async putUrl(key, options) {
|
|
738
|
+
try {
|
|
739
|
+
const s3Client = getClient();
|
|
740
|
+
const s3Config = getConfig();
|
|
741
|
+
const fullPath = fullKey(key);
|
|
742
|
+
const command = new PutObjectCommand({
|
|
743
|
+
Bucket: s3Config.bucket,
|
|
744
|
+
Key: fullPath,
|
|
745
|
+
ContentType: options?.contentType || "application/octet-stream"
|
|
746
|
+
});
|
|
747
|
+
const url = await getSignedUrl(s3Client, command, {
|
|
748
|
+
expiresIn: options?.expiresIn || 3600
|
|
749
|
+
});
|
|
750
|
+
return ok(url);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
return err(
|
|
753
|
+
HaiStorageError.PRESIGN_FAILED,
|
|
754
|
+
storageM("storage_presignUploadUrlFailed"),
|
|
755
|
+
error
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
publicUrl(key) {
|
|
760
|
+
const s3Config = getConfig();
|
|
761
|
+
if (!s3Config.publicUrl) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const fullPath = fullKey(key);
|
|
765
|
+
return `${s3Config.publicUrl.replace(/\/+$/, "")}/${fullPath}`;
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
return {
|
|
769
|
+
name: "s3",
|
|
770
|
+
file,
|
|
771
|
+
dir,
|
|
772
|
+
presign,
|
|
773
|
+
async connect(cfg) {
|
|
774
|
+
if (cfg.type !== "s3") {
|
|
775
|
+
return err(
|
|
776
|
+
HaiStorageError.CONFIG_ERROR,
|
|
777
|
+
storageM("storage_s3ConfigTypeError")
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
config = cfg;
|
|
782
|
+
logger2.info("Connecting S3 provider", {
|
|
783
|
+
bucket: cfg.bucket,
|
|
784
|
+
region: cfg.region,
|
|
785
|
+
endpoint: cfg.endpoint ? sanitizeEndpointUrl(cfg.endpoint) : void 0
|
|
786
|
+
});
|
|
787
|
+
client = new S3Client({
|
|
788
|
+
region: cfg.region,
|
|
789
|
+
endpoint: cfg.endpoint,
|
|
790
|
+
forcePathStyle: cfg.forcePathStyle,
|
|
791
|
+
credentials: {
|
|
792
|
+
accessKeyId: cfg.accessKeyId,
|
|
793
|
+
secretAccessKey: cfg.secretAccessKey
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
await client.send(new ListObjectsV2Command({
|
|
797
|
+
Bucket: cfg.bucket,
|
|
798
|
+
MaxKeys: 1
|
|
799
|
+
}));
|
|
800
|
+
logger2.info("S3 provider connected", { bucket: cfg.bucket });
|
|
801
|
+
return ok(void 0);
|
|
802
|
+
} catch (error) {
|
|
803
|
+
logger2.error("S3 provider connect failed", { error });
|
|
804
|
+
config = null;
|
|
805
|
+
client = null;
|
|
806
|
+
return err(toStorageError2(error));
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
async close() {
|
|
810
|
+
if (client) {
|
|
811
|
+
client.destroy();
|
|
812
|
+
client = null;
|
|
813
|
+
config = null;
|
|
814
|
+
logger2.info("S3 provider disconnected");
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
isConnected() {
|
|
818
|
+
return client !== null && config !== null;
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/storage-main.ts
|
|
824
|
+
var logger3 = core.logger.child({ module: "storage", scope: "main" });
|
|
825
|
+
var currentProvider = null;
|
|
826
|
+
var currentConfig = null;
|
|
827
|
+
var initInProgress = false;
|
|
828
|
+
function createProvider(config) {
|
|
829
|
+
switch (config.type) {
|
|
830
|
+
case "s3":
|
|
831
|
+
return createS3Provider();
|
|
832
|
+
case "local":
|
|
833
|
+
return createLocalProvider();
|
|
834
|
+
default:
|
|
835
|
+
throw new Error(storageM("storage_unsupportedType", { params: { type: config.type } }));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
var notInitialized = core.module.createNotInitializedKit(
|
|
839
|
+
HaiStorageError.NOT_INITIALIZED,
|
|
840
|
+
() => storageM("storage_notInitialized")
|
|
841
|
+
);
|
|
842
|
+
var notInitializedFile = notInitialized.proxy();
|
|
843
|
+
var notInitializedDir = notInitialized.proxy();
|
|
844
|
+
var notInitializedPresignBase = notInitialized.proxy();
|
|
845
|
+
var notInitializedPresign = new Proxy(
|
|
846
|
+
notInitializedPresignBase,
|
|
847
|
+
{
|
|
848
|
+
get: (target, prop, receiver) => prop === "publicUrl" ? () => null : Reflect.get(target, prop, receiver)
|
|
849
|
+
}
|
|
850
|
+
);
|
|
851
|
+
var storage = {
|
|
852
|
+
/**
|
|
853
|
+
* 初始化存储连接。
|
|
854
|
+
*
|
|
855
|
+
* 如果当前已有活跃连接,会先自动 close 再重新初始化。
|
|
856
|
+
* 配置会通过 Zod Schema 校验;校验失败或连接异常会返回 `CONNECTION_FAILED`。
|
|
857
|
+
*
|
|
858
|
+
* @param config 存储配置(S3 或本地),支持省略带默认值的字段。
|
|
859
|
+
* @returns 成功时返回 ok(undefined);失败时返回包含错误码和消息的 err。
|
|
860
|
+
*/
|
|
861
|
+
async init(config) {
|
|
862
|
+
if (initInProgress) {
|
|
863
|
+
logger3.warn("Storage init already in progress, skipping concurrent call");
|
|
864
|
+
return err(
|
|
865
|
+
HaiStorageError.OPERATION_FAILED,
|
|
866
|
+
storageM("storage_operationFailed", { params: { error: "Concurrent initialization detected" } })
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
initInProgress = true;
|
|
870
|
+
try {
|
|
871
|
+
if (currentProvider) {
|
|
872
|
+
logger3.warn("Storage module is already initialized, reinitializing");
|
|
873
|
+
await storage.close();
|
|
874
|
+
}
|
|
875
|
+
logger3.info("Initializing storage module");
|
|
876
|
+
const parseResult = StorageConfigSchema.safeParse(config);
|
|
877
|
+
if (!parseResult.success) {
|
|
878
|
+
logger3.error("Storage config validation failed", { error: parseResult.error.message });
|
|
879
|
+
return err(
|
|
880
|
+
HaiStorageError.CONFIG_ERROR,
|
|
881
|
+
storageM("storage_configError", { params: { error: parseResult.error.message } }),
|
|
882
|
+
parseResult.error
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const parsed = parseResult.data;
|
|
886
|
+
const provider = createProvider(parsed);
|
|
887
|
+
const connectResult = await provider.connect(parsed);
|
|
888
|
+
if (!connectResult.success) {
|
|
889
|
+
logger3.error("Storage module initialization failed", {
|
|
890
|
+
code: connectResult.error.code,
|
|
891
|
+
message: connectResult.error.message
|
|
892
|
+
});
|
|
893
|
+
return connectResult;
|
|
894
|
+
}
|
|
895
|
+
currentProvider = provider;
|
|
896
|
+
currentConfig = parsed;
|
|
897
|
+
logger3.info("Storage module initialized");
|
|
898
|
+
return ok(void 0);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
logger3.error("Storage module initialization failed", { error });
|
|
901
|
+
return err(
|
|
902
|
+
HaiStorageError.CONNECTION_FAILED,
|
|
903
|
+
storageM("storage_operationFailed", {
|
|
904
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
905
|
+
}),
|
|
906
|
+
error
|
|
907
|
+
);
|
|
908
|
+
} finally {
|
|
909
|
+
initInProgress = false;
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
/** 文件操作接口。未初始化时所有方法返回 NOT_INITIALIZED 错误 */
|
|
913
|
+
get file() {
|
|
914
|
+
return currentProvider?.file ?? notInitializedFile;
|
|
915
|
+
},
|
|
916
|
+
/** 目录操作接口。未初始化时所有方法返回 NOT_INITIALIZED 错误 */
|
|
917
|
+
get dir() {
|
|
918
|
+
return currentProvider?.dir ?? notInitializedDir;
|
|
919
|
+
},
|
|
920
|
+
/** 签名 URL 操作接口。未初始化时 publicUrl 返回 null,其余返回错误 */
|
|
921
|
+
get presign() {
|
|
922
|
+
return currentProvider?.presign ?? notInitializedPresign;
|
|
923
|
+
},
|
|
924
|
+
/** 当前解析后的存储配置;未初始化或已关闭时为 null */
|
|
925
|
+
get config() {
|
|
926
|
+
return currentConfig;
|
|
927
|
+
},
|
|
928
|
+
/** 是否已完成初始化 */
|
|
929
|
+
get isInitialized() {
|
|
930
|
+
return currentProvider !== null;
|
|
931
|
+
},
|
|
932
|
+
/**
|
|
933
|
+
* 关闭存储连接并释放资源
|
|
934
|
+
*
|
|
935
|
+
* 关闭后 file/dir/presign 操作将返回 NOT_INITIALIZED 错误。
|
|
936
|
+
* 重复调用不会报错。
|
|
937
|
+
*/
|
|
938
|
+
async close() {
|
|
939
|
+
if (!currentProvider) {
|
|
940
|
+
currentConfig = null;
|
|
941
|
+
logger3.info("Storage module already closed, skipping");
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
logger3.info("Closing storage module");
|
|
945
|
+
try {
|
|
946
|
+
await currentProvider.close();
|
|
947
|
+
logger3.info("Storage module closed");
|
|
948
|
+
} catch (error) {
|
|
949
|
+
logger3.error("Storage module close failed", { error });
|
|
950
|
+
} finally {
|
|
951
|
+
currentProvider = null;
|
|
952
|
+
currentConfig = null;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
export { storage };
|
|
958
|
+
//# sourceMappingURL=node.js.map
|
|
959
|
+
//# sourceMappingURL=node.js.map
|