@digibuffer/file-manager 1.0.1
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/README.md +213 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client-DTkToHgY.js +347 -0
- package/dist/index-CHmsNSsb.d.ts +1 -0
- package/dist/index-pUAG6PPj.d.ts +132 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/router-CD_RQnlx.d.ts +159 -0
- package/dist/server/adapters/next.d.ts +20 -0
- package/dist/server/adapters/next.js +63 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +3 -0
- package/dist/server-CZrPOP0h.js +490 -0
- package/dist/types-Brk9aR7c.d.ts +177 -0
- package/package.json +70 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { deleteObject } from "@digibuffer/upload-lib-server/helpers";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/server/file-manager.ts
|
|
5
|
+
/**
|
|
6
|
+
* S3 List Objects V2 XML response parser
|
|
7
|
+
*/
|
|
8
|
+
function parseListObjectsV2(xml) {
|
|
9
|
+
const objects = [];
|
|
10
|
+
const contentRegex = /<Contents>([\s\S]*?)<\/Contents>/g;
|
|
11
|
+
let match;
|
|
12
|
+
while ((match = contentRegex.exec(xml)) !== null) {
|
|
13
|
+
const content = match[1];
|
|
14
|
+
const key = content.match(/<Key>(.*?)<\/Key>/)?.[1];
|
|
15
|
+
const size = content.match(/<Size>(.*?)<\/Size>/)?.[1];
|
|
16
|
+
const lastModified = content.match(/<LastModified>(.*?)<\/LastModified>/)?.[1];
|
|
17
|
+
const etag = content.match(/<ETag>(.*?)<\/ETag>/)?.[1];
|
|
18
|
+
const storageClass = content.match(/<StorageClass>(.*?)<\/StorageClass>/)?.[1];
|
|
19
|
+
if (key && size && lastModified && etag) objects.push({
|
|
20
|
+
key,
|
|
21
|
+
size: parseInt(size, 10),
|
|
22
|
+
lastModified,
|
|
23
|
+
etag: etag.replace(/"/g, ""),
|
|
24
|
+
storageClass
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
objects,
|
|
29
|
+
isTruncated: xml.includes("<IsTruncated>true</IsTruncated>"),
|
|
30
|
+
nextContinuationToken: xml.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/)?.[1]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* File Manager for managing files in R2/S3
|
|
35
|
+
*/
|
|
36
|
+
var FileManager = class {
|
|
37
|
+
client;
|
|
38
|
+
bucketName;
|
|
39
|
+
database;
|
|
40
|
+
defaultLimit;
|
|
41
|
+
maxLimit;
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.client = config.client;
|
|
44
|
+
this.bucketName = config.bucketName;
|
|
45
|
+
this.database = config.database;
|
|
46
|
+
this.defaultLimit = config.defaultLimit ?? 50;
|
|
47
|
+
this.maxLimit = config.maxLimit ?? 1e3;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* List files from storage with pagination
|
|
51
|
+
*/
|
|
52
|
+
async listFromStorage(options = {}) {
|
|
53
|
+
const limit = Math.min(options.limit ?? this.defaultLimit, this.maxLimit);
|
|
54
|
+
const params = new URLSearchParams({
|
|
55
|
+
"list-type": "2",
|
|
56
|
+
"max-keys": limit.toString()
|
|
57
|
+
});
|
|
58
|
+
if (options.cursor) params.append("continuation-token", options.cursor);
|
|
59
|
+
if (options.prefix) params.append("prefix", options.prefix);
|
|
60
|
+
if (options.delimiter) params.append("delimiter", options.delimiter);
|
|
61
|
+
const url = this.buildUrl(void 0, params);
|
|
62
|
+
try {
|
|
63
|
+
const response = await this.client.fetch(url, { method: "GET" });
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
throw this.createError("Failed to list files from storage", "STORAGE_LIST_ERROR", {
|
|
67
|
+
status: response.status,
|
|
68
|
+
statusText: response.statusText,
|
|
69
|
+
body: text
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const parsed = parseListObjectsV2(await response.text());
|
|
73
|
+
const items = parsed.objects.map((obj) => ({
|
|
74
|
+
key: obj.key,
|
|
75
|
+
size: obj.size,
|
|
76
|
+
lastModified: new Date(obj.lastModified),
|
|
77
|
+
etag: obj.etag,
|
|
78
|
+
storageClass: obj.storageClass
|
|
79
|
+
}));
|
|
80
|
+
if (options.sortBy) this.sortFiles(items, options.sortBy, options.sortOrder);
|
|
81
|
+
return {
|
|
82
|
+
items,
|
|
83
|
+
nextCursor: parsed.nextContinuationToken,
|
|
84
|
+
hasMore: parsed.isTruncated
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof Error && error.name === "FileManagerError") throw error;
|
|
88
|
+
throw this.createError("Failed to list files", "LIST_ERROR", { originalError: error });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* List files from database with pagination
|
|
93
|
+
*/
|
|
94
|
+
async listFromDatabase(options = {}) {
|
|
95
|
+
if (!this.database?.getFiles) throw this.createError("Database getFiles callback not configured", "DATABASE_NOT_CONFIGURED");
|
|
96
|
+
try {
|
|
97
|
+
const limit = Math.min(options.limit ?? this.defaultLimit, this.maxLimit);
|
|
98
|
+
const result = await this.database.getFiles({
|
|
99
|
+
limit,
|
|
100
|
+
cursor: options.cursor,
|
|
101
|
+
prefix: options.prefix,
|
|
102
|
+
delimiter: options.delimiter
|
|
103
|
+
});
|
|
104
|
+
if (options.sortBy && result.items.length > 0) this.sortFiles(result.items, options.sortBy, options.sortOrder);
|
|
105
|
+
return result;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw this.createError("Failed to list files from database", "DATABASE_LIST_ERROR", { originalError: error });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* List files from both database and storage, merging results
|
|
112
|
+
*/
|
|
113
|
+
async listFromBoth(options = {}) {
|
|
114
|
+
const [dbResult, storageResult] = await Promise.all([this.listFromDatabase(options).catch(() => ({
|
|
115
|
+
items: [],
|
|
116
|
+
hasMore: false
|
|
117
|
+
})), this.listFromStorage(options).catch(() => ({
|
|
118
|
+
items: [],
|
|
119
|
+
hasMore: false
|
|
120
|
+
}))]);
|
|
121
|
+
const storageMap = new Map(storageResult.items.map((file) => [file.key, file]));
|
|
122
|
+
return {
|
|
123
|
+
items: dbResult.items.map((dbFile) => {
|
|
124
|
+
const storageFile = storageMap.get(dbFile.key);
|
|
125
|
+
if (storageFile) return {
|
|
126
|
+
...dbFile,
|
|
127
|
+
...storageFile,
|
|
128
|
+
id: dbFile.id
|
|
129
|
+
};
|
|
130
|
+
return dbFile;
|
|
131
|
+
}),
|
|
132
|
+
nextCursor: dbResult.nextCursor,
|
|
133
|
+
hasMore: dbResult.hasMore,
|
|
134
|
+
totalCount: dbResult.totalCount
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* List files with automatic source selection
|
|
139
|
+
*/
|
|
140
|
+
async list(options = {}) {
|
|
141
|
+
const source = options.source ?? (this.database?.getFiles ? "database" : "storage");
|
|
142
|
+
switch (source) {
|
|
143
|
+
case "database": return this.listFromDatabase(options);
|
|
144
|
+
case "storage": return this.listFromStorage(options);
|
|
145
|
+
case "both": return this.listFromBoth(options);
|
|
146
|
+
default: throw this.createError("Invalid source option", "INVALID_SOURCE", { source });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Delete a single file
|
|
151
|
+
*/
|
|
152
|
+
async delete(key) {
|
|
153
|
+
let file;
|
|
154
|
+
try {
|
|
155
|
+
if (this.database?.getFileByKey) file = await this.database.getFileByKey(key);
|
|
156
|
+
if (this.database?.onBeforeDelete) {
|
|
157
|
+
if (!await this.database.onBeforeDelete({
|
|
158
|
+
key,
|
|
159
|
+
file: file ?? void 0
|
|
160
|
+
})) return {
|
|
161
|
+
success: false,
|
|
162
|
+
key,
|
|
163
|
+
error: "Delete prevented by onBeforeDelete hook"
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
await deleteObject(this.client, {
|
|
167
|
+
bucket: this.bucketName,
|
|
168
|
+
key
|
|
169
|
+
});
|
|
170
|
+
if (this.database?.onAfterDelete) await this.database.onAfterDelete({
|
|
171
|
+
key,
|
|
172
|
+
file: file ?? void 0,
|
|
173
|
+
success: true
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
success: true,
|
|
177
|
+
key
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
181
|
+
if (this.database?.onAfterDelete) await this.database.onAfterDelete({
|
|
182
|
+
key,
|
|
183
|
+
file: file ?? void 0,
|
|
184
|
+
success: false,
|
|
185
|
+
error: errorMessage
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
key,
|
|
190
|
+
error: errorMessage
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Delete multiple files in batch
|
|
196
|
+
*/
|
|
197
|
+
async deleteBatch(keys) {
|
|
198
|
+
if (keys.length === 0) return {
|
|
199
|
+
deleted: [],
|
|
200
|
+
errors: []
|
|
201
|
+
};
|
|
202
|
+
const results = await Promise.allSettled(keys.map((key) => this.delete(key)));
|
|
203
|
+
const deleted = [];
|
|
204
|
+
const errors = [];
|
|
205
|
+
results.forEach((result, index) => {
|
|
206
|
+
const key = keys[index];
|
|
207
|
+
if (result.status === "fulfilled" && result.value.success) deleted.push(key);
|
|
208
|
+
else {
|
|
209
|
+
const error = result.status === "fulfilled" ? result.value.error ?? "Unknown error" : result.reason instanceof Error ? result.reason.message : "Unknown error";
|
|
210
|
+
errors.push({
|
|
211
|
+
key,
|
|
212
|
+
error
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
if (this.database?.onAfterBatchDelete) await this.database.onAfterBatchDelete({
|
|
217
|
+
deleted,
|
|
218
|
+
errors
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
deleted,
|
|
222
|
+
errors
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Generate a presigned download URL
|
|
227
|
+
*/
|
|
228
|
+
async getDownloadUrl(key, options = {}) {
|
|
229
|
+
const expiresIn = options.expiresIn ?? 3600;
|
|
230
|
+
const params = new URLSearchParams();
|
|
231
|
+
if (options.filename || options.disposition) {
|
|
232
|
+
const disposition = options.disposition ?? "attachment";
|
|
233
|
+
const filename = options.filename ?? key.split("/").pop();
|
|
234
|
+
params.append("response-content-disposition", `${disposition}; filename="${filename}"`);
|
|
235
|
+
}
|
|
236
|
+
const url = this.buildUrl(key, params.toString() ? params : void 0);
|
|
237
|
+
Math.floor(Date.now() / 1e3) + expiresIn;
|
|
238
|
+
const request = new Request(url, { method: "GET" });
|
|
239
|
+
return (await this.client.sign(request, { aws: {
|
|
240
|
+
signQuery: true,
|
|
241
|
+
allHeaders: true
|
|
242
|
+
} })).url;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Check if a file exists
|
|
246
|
+
*/
|
|
247
|
+
async exists(key) {
|
|
248
|
+
try {
|
|
249
|
+
const url = this.buildUrl(key);
|
|
250
|
+
return (await this.client.fetch(url, { method: "HEAD" })).ok;
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get file metadata from storage
|
|
257
|
+
*/
|
|
258
|
+
async getMetadata(key) {
|
|
259
|
+
try {
|
|
260
|
+
const url = this.buildUrl(key);
|
|
261
|
+
const response = await this.client.fetch(url, { method: "HEAD" });
|
|
262
|
+
if (!response.ok) return null;
|
|
263
|
+
return {
|
|
264
|
+
key,
|
|
265
|
+
size: parseInt(response.headers.get("content-length") ?? "0", 10),
|
|
266
|
+
lastModified: new Date(response.headers.get("last-modified") ?? Date.now()),
|
|
267
|
+
etag: response.headers.get("etag")?.replace(/"/g, "") ?? "",
|
|
268
|
+
contentType: response.headers.get("content-type") ?? void 0
|
|
269
|
+
};
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Build URL helper - supports both path-style and virtual-hosted-style
|
|
276
|
+
*/
|
|
277
|
+
buildUrl(key, params) {
|
|
278
|
+
const baseHost = this.client.host.startsWith("http") ? this.client.host : `https://${this.client.host}`;
|
|
279
|
+
let url;
|
|
280
|
+
if (key) url = this.bucketName ? `${baseHost}/${this.bucketName}/${key}` : `${baseHost}/${key}`;
|
|
281
|
+
else url = this.bucketName ? `${baseHost}/${this.bucketName}` : baseHost;
|
|
282
|
+
if (params) url += `?${params.toString()}`;
|
|
283
|
+
return url;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Sort files helper
|
|
287
|
+
*/
|
|
288
|
+
sortFiles(files, sortBy, sortOrder = "desc") {
|
|
289
|
+
files.sort((a, b) => {
|
|
290
|
+
let comparison = 0;
|
|
291
|
+
switch (sortBy) {
|
|
292
|
+
case "lastModified":
|
|
293
|
+
comparison = a.lastModified.getTime() - b.lastModified.getTime();
|
|
294
|
+
break;
|
|
295
|
+
case "size":
|
|
296
|
+
comparison = a.size - b.size;
|
|
297
|
+
break;
|
|
298
|
+
case "name":
|
|
299
|
+
comparison = a.key.localeCompare(b.key);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
return sortOrder === "asc" ? comparison : -comparison;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Create a FileManagerError
|
|
307
|
+
*/
|
|
308
|
+
createError(message, code, details) {
|
|
309
|
+
const error = new Error(message);
|
|
310
|
+
error.name = "FileManagerError";
|
|
311
|
+
error.code = code;
|
|
312
|
+
error.details = details;
|
|
313
|
+
return error;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/server/router.ts
|
|
319
|
+
/**
|
|
320
|
+
* Request schemas
|
|
321
|
+
*/
|
|
322
|
+
const ListRequestSchema = z.object({
|
|
323
|
+
action: z.literal("list"),
|
|
324
|
+
limit: z.number().int().positive().optional(),
|
|
325
|
+
cursor: z.string().optional(),
|
|
326
|
+
prefix: z.string().optional(),
|
|
327
|
+
delimiter: z.string().optional(),
|
|
328
|
+
source: z.enum([
|
|
329
|
+
"storage",
|
|
330
|
+
"database",
|
|
331
|
+
"both"
|
|
332
|
+
]).optional(),
|
|
333
|
+
sortBy: z.enum([
|
|
334
|
+
"lastModified",
|
|
335
|
+
"size",
|
|
336
|
+
"name"
|
|
337
|
+
]).optional(),
|
|
338
|
+
sortOrder: z.enum(["asc", "desc"]).optional()
|
|
339
|
+
});
|
|
340
|
+
const DeleteRequestSchema = z.object({
|
|
341
|
+
action: z.literal("delete"),
|
|
342
|
+
key: z.string().min(1)
|
|
343
|
+
});
|
|
344
|
+
const BatchDeleteRequestSchema = z.object({
|
|
345
|
+
action: z.literal("batchDelete"),
|
|
346
|
+
keys: z.array(z.string().min(1)).min(1)
|
|
347
|
+
});
|
|
348
|
+
const GetUrlRequestSchema = z.object({
|
|
349
|
+
action: z.literal("getUrl"),
|
|
350
|
+
key: z.string().min(1),
|
|
351
|
+
expiresIn: z.number().int().positive().optional(),
|
|
352
|
+
filename: z.string().optional(),
|
|
353
|
+
disposition: z.enum(["attachment", "inline"]).optional()
|
|
354
|
+
});
|
|
355
|
+
const ExistsRequestSchema = z.object({
|
|
356
|
+
action: z.literal("exists"),
|
|
357
|
+
key: z.string().min(1)
|
|
358
|
+
});
|
|
359
|
+
const GetMetadataRequestSchema = z.object({
|
|
360
|
+
action: z.literal("getMetadata"),
|
|
361
|
+
key: z.string().min(1)
|
|
362
|
+
});
|
|
363
|
+
const FileManagerRequestSchema = z.discriminatedUnion("action", [
|
|
364
|
+
ListRequestSchema,
|
|
365
|
+
DeleteRequestSchema,
|
|
366
|
+
BatchDeleteRequestSchema,
|
|
367
|
+
GetUrlRequestSchema,
|
|
368
|
+
ExistsRequestSchema,
|
|
369
|
+
GetMetadataRequestSchema
|
|
370
|
+
]);
|
|
371
|
+
/**
|
|
372
|
+
* Router for file management operations
|
|
373
|
+
*/
|
|
374
|
+
var FileManagerRouter = class {
|
|
375
|
+
routes = /* @__PURE__ */ new Map();
|
|
376
|
+
/**
|
|
377
|
+
* Register a route
|
|
378
|
+
*/
|
|
379
|
+
route(path, config) {
|
|
380
|
+
this.routes.set(path, config);
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Handle a request
|
|
385
|
+
*/
|
|
386
|
+
async handle(path, body, context) {
|
|
387
|
+
const route = this.routes.get(path);
|
|
388
|
+
if (!route) return this.errorResponse("Route not found", 404);
|
|
389
|
+
try {
|
|
390
|
+
const request = FileManagerRequestSchema.parse(body);
|
|
391
|
+
if (route.onAuthorize) {
|
|
392
|
+
if (!await route.onAuthorize(request, context)) return this.errorResponse("Unauthorized", 403);
|
|
393
|
+
}
|
|
394
|
+
switch (request.action) {
|
|
395
|
+
case "list": {
|
|
396
|
+
let options = {
|
|
397
|
+
limit: request.limit,
|
|
398
|
+
cursor: request.cursor,
|
|
399
|
+
prefix: request.prefix,
|
|
400
|
+
delimiter: request.delimiter,
|
|
401
|
+
source: request.source,
|
|
402
|
+
sortBy: request.sortBy,
|
|
403
|
+
sortOrder: request.sortOrder
|
|
404
|
+
};
|
|
405
|
+
if (route.onBeforeList) {
|
|
406
|
+
const result$1 = await route.onBeforeList(options, context);
|
|
407
|
+
if (result$1 === false) return this.errorResponse("List operation denied", 403);
|
|
408
|
+
options = result$1;
|
|
409
|
+
}
|
|
410
|
+
const result = await route.fileManager.list(options);
|
|
411
|
+
return this.successResponse(result);
|
|
412
|
+
}
|
|
413
|
+
case "delete": {
|
|
414
|
+
if (route.onBeforeDelete) {
|
|
415
|
+
if (!await route.onBeforeDelete(request.key, context)) return this.errorResponse("Delete operation denied", 403);
|
|
416
|
+
}
|
|
417
|
+
const result = await route.fileManager.delete(request.key);
|
|
418
|
+
return this.successResponse(result);
|
|
419
|
+
}
|
|
420
|
+
case "batchDelete": {
|
|
421
|
+
let keys = request.keys;
|
|
422
|
+
if (route.onBeforeBatchDelete) {
|
|
423
|
+
const result$1 = await route.onBeforeBatchDelete(keys, context);
|
|
424
|
+
if (result$1 === false) return this.errorResponse("Batch delete operation denied", 403);
|
|
425
|
+
keys = result$1;
|
|
426
|
+
}
|
|
427
|
+
const result = await route.fileManager.deleteBatch(keys);
|
|
428
|
+
return this.successResponse(result);
|
|
429
|
+
}
|
|
430
|
+
case "getUrl": {
|
|
431
|
+
const url = await route.fileManager.getDownloadUrl(request.key, {
|
|
432
|
+
expiresIn: request.expiresIn,
|
|
433
|
+
filename: request.filename,
|
|
434
|
+
disposition: request.disposition
|
|
435
|
+
});
|
|
436
|
+
return this.successResponse({ url });
|
|
437
|
+
}
|
|
438
|
+
case "exists": {
|
|
439
|
+
const exists = await route.fileManager.exists(request.key);
|
|
440
|
+
return this.successResponse({ exists });
|
|
441
|
+
}
|
|
442
|
+
case "getMetadata": {
|
|
443
|
+
const metadata = await route.fileManager.getMetadata(request.key);
|
|
444
|
+
return this.successResponse({ metadata });
|
|
445
|
+
}
|
|
446
|
+
default: return this.errorResponse("Invalid action", 400);
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error instanceof z.ZodError) return this.errorResponse("Invalid request", 400, { errors: error.errors });
|
|
450
|
+
if (error instanceof Error) return this.errorResponse(error.message, 500, { name: error.name });
|
|
451
|
+
return this.errorResponse("Internal server error", 500);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Success response helper
|
|
456
|
+
*/
|
|
457
|
+
successResponse(data) {
|
|
458
|
+
return new Response(JSON.stringify({
|
|
459
|
+
success: true,
|
|
460
|
+
data
|
|
461
|
+
}), {
|
|
462
|
+
status: 200,
|
|
463
|
+
headers: { "Content-Type": "application/json" }
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Error response helper
|
|
468
|
+
*/
|
|
469
|
+
errorResponse(message, status, details) {
|
|
470
|
+
return new Response(JSON.stringify({
|
|
471
|
+
success: false,
|
|
472
|
+
error: {
|
|
473
|
+
message,
|
|
474
|
+
details
|
|
475
|
+
}
|
|
476
|
+
}), {
|
|
477
|
+
status,
|
|
478
|
+
headers: { "Content-Type": "application/json" }
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* Create a file manager router
|
|
484
|
+
*/
|
|
485
|
+
function createFileManagerRouter() {
|
|
486
|
+
return new FileManagerRouter();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
//#endregion
|
|
490
|
+
export { createFileManagerRouter as n, FileManager as r, FileManagerRouter as t };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Client } from "@digibuffer/upload-lib-server/clients";
|
|
2
|
+
|
|
3
|
+
//#region src/shared/types.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pagination options for listing files
|
|
7
|
+
*/
|
|
8
|
+
type PaginationOptions = {
|
|
9
|
+
/** Maximum number of items to return per page */
|
|
10
|
+
limit?: number;
|
|
11
|
+
/** Pagination cursor (continuation token from previous response) */
|
|
12
|
+
cursor?: string;
|
|
13
|
+
/** Optional prefix to filter files by key prefix */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
/** Optional delimiter for grouping keys (commonly '/') */
|
|
16
|
+
delimiter?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Paginated response wrapper
|
|
20
|
+
*/
|
|
21
|
+
type PaginatedResponse<T> = {
|
|
22
|
+
/** Array of items for current page */
|
|
23
|
+
items: T[];
|
|
24
|
+
/** Cursor for next page, undefined if no more pages */
|
|
25
|
+
nextCursor?: string;
|
|
26
|
+
/** Whether there are more pages available */
|
|
27
|
+
hasMore: boolean;
|
|
28
|
+
/** Total count if available (may not be accurate for large datasets) */
|
|
29
|
+
totalCount?: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* File metadata from storage
|
|
33
|
+
*/
|
|
34
|
+
type StorageFile = {
|
|
35
|
+
/** Object key in storage */
|
|
36
|
+
key: string;
|
|
37
|
+
/** File size in bytes */
|
|
38
|
+
size: number;
|
|
39
|
+
/** Last modified timestamp */
|
|
40
|
+
lastModified: Date;
|
|
41
|
+
/** ETag for the object */
|
|
42
|
+
etag: string;
|
|
43
|
+
/** Content type */
|
|
44
|
+
contentType?: string;
|
|
45
|
+
/** Storage class */
|
|
46
|
+
storageClass?: string;
|
|
47
|
+
/** Custom metadata */
|
|
48
|
+
metadata?: Record<string, string>;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* File with database information
|
|
52
|
+
*/
|
|
53
|
+
type ManagedFile = StorageFile & {
|
|
54
|
+
/** Database ID */
|
|
55
|
+
id: string;
|
|
56
|
+
/** User ID who owns the file */
|
|
57
|
+
userId?: string;
|
|
58
|
+
/** Original filename */
|
|
59
|
+
originalName?: string;
|
|
60
|
+
/** Additional database fields */
|
|
61
|
+
[key: string]: any;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Delete operation result
|
|
65
|
+
*/
|
|
66
|
+
type DeleteResult = {
|
|
67
|
+
/** Whether the delete was successful */
|
|
68
|
+
success: boolean;
|
|
69
|
+
/** The key that was deleted */
|
|
70
|
+
key: string;
|
|
71
|
+
/** Error message if failed */
|
|
72
|
+
error?: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Batch delete result
|
|
76
|
+
*/
|
|
77
|
+
type BatchDeleteResult = {
|
|
78
|
+
/** Successfully deleted keys */
|
|
79
|
+
deleted: string[];
|
|
80
|
+
/** Failed deletions with errors */
|
|
81
|
+
errors: Array<{
|
|
82
|
+
key: string;
|
|
83
|
+
error: string;
|
|
84
|
+
}>;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Download URL options
|
|
88
|
+
*/
|
|
89
|
+
type DownloadUrlOptions = {
|
|
90
|
+
/** Expiration time in seconds (default: 3600) */
|
|
91
|
+
expiresIn?: number;
|
|
92
|
+
/** Optional filename for Content-Disposition header */
|
|
93
|
+
filename?: string;
|
|
94
|
+
/** Whether to force download (attachment) vs inline */
|
|
95
|
+
disposition?: 'attachment' | 'inline';
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Database callbacks for file operations
|
|
99
|
+
*/
|
|
100
|
+
type DatabaseCallbacks<TFile = ManagedFile> = {
|
|
101
|
+
/**
|
|
102
|
+
* Called after a file is deleted from storage
|
|
103
|
+
* Use this to update your database
|
|
104
|
+
*/
|
|
105
|
+
onAfterDelete?: (params: {
|
|
106
|
+
key: string;
|
|
107
|
+
file?: TFile;
|
|
108
|
+
success: boolean;
|
|
109
|
+
error?: string;
|
|
110
|
+
}) => Promise<void> | void;
|
|
111
|
+
/**
|
|
112
|
+
* Called before a file is deleted from storage
|
|
113
|
+
* Return false to prevent deletion
|
|
114
|
+
*/
|
|
115
|
+
onBeforeDelete?: (params: {
|
|
116
|
+
key: string;
|
|
117
|
+
file?: TFile;
|
|
118
|
+
}) => Promise<boolean> | boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Called after batch delete operation
|
|
121
|
+
*/
|
|
122
|
+
onAfterBatchDelete?: (params: {
|
|
123
|
+
deleted: string[];
|
|
124
|
+
errors: Array<{
|
|
125
|
+
key: string;
|
|
126
|
+
error: string;
|
|
127
|
+
}>;
|
|
128
|
+
}) => Promise<void> | void;
|
|
129
|
+
/**
|
|
130
|
+
* Fetch file metadata from database by key
|
|
131
|
+
*/
|
|
132
|
+
getFileByKey?: (key: string) => Promise<TFile | null> | TFile | null;
|
|
133
|
+
/**
|
|
134
|
+
* Fetch files from database with pagination
|
|
135
|
+
*/
|
|
136
|
+
getFiles?: (options: PaginationOptions) => Promise<PaginatedResponse<TFile>> | PaginatedResponse<TFile>;
|
|
137
|
+
/**
|
|
138
|
+
* Update file metadata in database
|
|
139
|
+
*/
|
|
140
|
+
updateFile?: (key: string, data: Partial<TFile>) => Promise<TFile> | TFile;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* File manager configuration
|
|
144
|
+
*/
|
|
145
|
+
type FileManagerConfig<TFile = ManagedFile> = {
|
|
146
|
+
/** S3 client instance */
|
|
147
|
+
client: Client;
|
|
148
|
+
/** Default bucket name */
|
|
149
|
+
bucketName: string;
|
|
150
|
+
/** Database callbacks */
|
|
151
|
+
database?: DatabaseCallbacks<TFile>;
|
|
152
|
+
/** Default pagination limit */
|
|
153
|
+
defaultLimit?: number;
|
|
154
|
+
/** Maximum pagination limit */
|
|
155
|
+
maxLimit?: number;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* List files options
|
|
159
|
+
*/
|
|
160
|
+
type ListFilesOptions = PaginationOptions & {
|
|
161
|
+
/** Whether to fetch from database or storage */
|
|
162
|
+
source?: 'storage' | 'database' | 'both';
|
|
163
|
+
/** Sort order */
|
|
164
|
+
sortBy?: 'lastModified' | 'size' | 'name';
|
|
165
|
+
/** Sort direction */
|
|
166
|
+
sortOrder?: 'asc' | 'desc';
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* File operation error
|
|
170
|
+
*/
|
|
171
|
+
declare class FileManagerError extends Error {
|
|
172
|
+
code: string;
|
|
173
|
+
details?: Record<string, any> | undefined;
|
|
174
|
+
constructor(message: string, code: string, details?: Record<string, any> | undefined);
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
export { FileManagerConfig as a, ManagedFile as c, StorageFile as d, DownloadUrlOptions as i, PaginatedResponse as l, DatabaseCallbacks as n, FileManagerError as o, DeleteResult as r, ListFilesOptions as s, BatchDeleteResult as t, PaginationOptions as u };
|