@digibuffer/file-manager-core 1.0.0
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 +240 -0
- package/dist/adapters/next.d.ts +24 -0
- package/dist/adapters/next.js +46 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +3 -0
- package/dist/router-CY6g7nWy.d.ts +339 -0
- package/dist/router-De_vaLLo.js +1031 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import { createAwsSignature, createSignedUrl } from "@digibuffer/upload-lib-server/helpers";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/types.ts
|
|
5
|
+
/**
|
|
6
|
+
* Custom error class for file manager operations
|
|
7
|
+
*/
|
|
8
|
+
var FileManagerError = class extends Error {
|
|
9
|
+
constructor(message, code, statusCode = 500, details) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
this.details = details;
|
|
14
|
+
this.name = "FileManagerError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/storage/s3-provider.ts
|
|
20
|
+
/**
|
|
21
|
+
* S3/R2 Storage Provider Implementation
|
|
22
|
+
*/
|
|
23
|
+
var S3StorageProvider = class {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* List files from S3/R2 storage
|
|
29
|
+
*/
|
|
30
|
+
async list(options = {}) {
|
|
31
|
+
const { prefix = "", delimiter = "", maxKeys = 1e3, cursor, sortBy = "lastModified", sortOrder = "desc" } = options;
|
|
32
|
+
const url = new URL(this.config.endpoint);
|
|
33
|
+
url.pathname = `/${this.config.bucket}`;
|
|
34
|
+
url.searchParams.set("list-type", "2");
|
|
35
|
+
url.searchParams.set("max-keys", maxKeys.toString());
|
|
36
|
+
if (prefix) url.searchParams.set("prefix", prefix);
|
|
37
|
+
if (delimiter) url.searchParams.set("delimiter", delimiter);
|
|
38
|
+
if (cursor?.continuationToken) url.searchParams.set("continuation-token", cursor.continuationToken);
|
|
39
|
+
const signature = await createAwsSignature({
|
|
40
|
+
method: "GET",
|
|
41
|
+
url: url.toString(),
|
|
42
|
+
region: this.config.region || "auto",
|
|
43
|
+
service: "s3",
|
|
44
|
+
accessKeyId: this.config.accessKeyId,
|
|
45
|
+
secretAccessKey: this.config.secretAccessKey
|
|
46
|
+
});
|
|
47
|
+
const response = await fetch(url.toString(), {
|
|
48
|
+
method: "GET",
|
|
49
|
+
headers: signature.headers
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) throw new FileManagerError(`Failed to list files: ${response.statusText}`, "LIST_ERROR", response.status);
|
|
52
|
+
const xmlText = await response.text();
|
|
53
|
+
const files = this.parseListResponse(xmlText);
|
|
54
|
+
const sortedFiles = this.sortFiles(files, sortBy, sortOrder);
|
|
55
|
+
const nextToken = this.extractContinuationToken(xmlText);
|
|
56
|
+
return {
|
|
57
|
+
data: sortedFiles,
|
|
58
|
+
nextCursor: nextToken ? { continuationToken: nextToken } : void 0,
|
|
59
|
+
hasMore: !!nextToken
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Delete a single file
|
|
64
|
+
*/
|
|
65
|
+
async delete(key) {
|
|
66
|
+
try {
|
|
67
|
+
const url = `${this.config.endpoint}/${this.config.bucket}/${encodeURIComponent(key)}`;
|
|
68
|
+
const signature = await createAwsSignature({
|
|
69
|
+
method: "DELETE",
|
|
70
|
+
url,
|
|
71
|
+
region: this.config.region || "auto",
|
|
72
|
+
service: "s3",
|
|
73
|
+
accessKeyId: this.config.accessKeyId,
|
|
74
|
+
secretAccessKey: this.config.secretAccessKey
|
|
75
|
+
});
|
|
76
|
+
const response = await fetch(url, {
|
|
77
|
+
method: "DELETE",
|
|
78
|
+
headers: signature.headers
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok && response.status !== 404) throw new Error(`Failed to delete: ${response.statusText}`);
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
key
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
key,
|
|
89
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Delete multiple files in batch
|
|
95
|
+
*/
|
|
96
|
+
async deleteBatch(keys) {
|
|
97
|
+
const results = await Promise.allSettled(keys.map((key) => this.delete(key)));
|
|
98
|
+
const succeeded = [];
|
|
99
|
+
const failed = [];
|
|
100
|
+
results.forEach((result, index) => {
|
|
101
|
+
if (result.status === "fulfilled" && result.value.success) succeeded.push(keys[index]);
|
|
102
|
+
else failed.push({
|
|
103
|
+
key: keys[index],
|
|
104
|
+
error: result.status === "rejected" ? result.reason : result.value.error || "Unknown error"
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
succeeded,
|
|
109
|
+
failed,
|
|
110
|
+
totalProcessed: keys.length
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if file exists
|
|
115
|
+
*/
|
|
116
|
+
async exists(key) {
|
|
117
|
+
try {
|
|
118
|
+
return await this.getMetadata(key) !== null;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get file metadata
|
|
125
|
+
*/
|
|
126
|
+
async getMetadata(key) {
|
|
127
|
+
try {
|
|
128
|
+
const url = `${this.config.endpoint}/${this.config.bucket}/${encodeURIComponent(key)}`;
|
|
129
|
+
const signature = await createAwsSignature({
|
|
130
|
+
method: "HEAD",
|
|
131
|
+
url,
|
|
132
|
+
region: this.config.region || "auto",
|
|
133
|
+
service: "s3",
|
|
134
|
+
accessKeyId: this.config.accessKeyId,
|
|
135
|
+
secretAccessKey: this.config.secretAccessKey
|
|
136
|
+
});
|
|
137
|
+
const response = await fetch(url, {
|
|
138
|
+
method: "HEAD",
|
|
139
|
+
headers: signature.headers
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
if (response.status === 404) return null;
|
|
143
|
+
throw new Error(`Failed to get metadata: ${response.statusText}`);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
key,
|
|
147
|
+
bucket: this.config.bucket,
|
|
148
|
+
size: parseInt(response.headers.get("content-length") || "0", 10),
|
|
149
|
+
lastModified: new Date(response.headers.get("last-modified") || Date.now()),
|
|
150
|
+
contentType: response.headers.get("content-type") || void 0,
|
|
151
|
+
etag: response.headers.get("etag")?.replace(/"/g, "") || void 0
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error(`Error getting metadata for ${key}:`, error);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Generate presigned download URL
|
|
160
|
+
*/
|
|
161
|
+
async getDownloadUrl(key, options = {}) {
|
|
162
|
+
const { expiresIn = 3600, disposition = "inline", filename } = options;
|
|
163
|
+
const url = new URL(`${this.config.endpoint}/${this.config.bucket}/${encodeURIComponent(key)}`);
|
|
164
|
+
if (disposition === "attachment") {
|
|
165
|
+
const downloadFilename = filename || key.split("/").pop() || "download";
|
|
166
|
+
url.searchParams.set("response-content-disposition", `attachment; filename="${downloadFilename}"`);
|
|
167
|
+
}
|
|
168
|
+
return await createSignedUrl({
|
|
169
|
+
url: url.toString(),
|
|
170
|
+
method: "GET",
|
|
171
|
+
expiresIn,
|
|
172
|
+
region: this.config.region || "auto",
|
|
173
|
+
service: "s3",
|
|
174
|
+
accessKeyId: this.config.accessKeyId,
|
|
175
|
+
secretAccessKey: this.config.secretAccessKey
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Move a file (copy + delete source)
|
|
180
|
+
*/
|
|
181
|
+
async move(options) {
|
|
182
|
+
const { sourceKey, destinationKey } = options;
|
|
183
|
+
try {
|
|
184
|
+
const copyResult = await this.copy({
|
|
185
|
+
sourceKey,
|
|
186
|
+
destinationKey
|
|
187
|
+
});
|
|
188
|
+
if (!copyResult.success) return copyResult;
|
|
189
|
+
const deleteResult = await this.delete(sourceKey);
|
|
190
|
+
if (!deleteResult.success) {
|
|
191
|
+
await this.delete(destinationKey);
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
key: sourceKey,
|
|
195
|
+
error: `Failed to delete source after copy: ${deleteResult.error}`
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
key: destinationKey
|
|
201
|
+
};
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
key: sourceKey,
|
|
206
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Copy a file to a new location
|
|
212
|
+
*/
|
|
213
|
+
async copy(options) {
|
|
214
|
+
const { sourceKey, destinationKey, metadata } = options;
|
|
215
|
+
try {
|
|
216
|
+
const url = `${this.config.endpoint}/${this.config.bucket}/${encodeURIComponent(destinationKey)}`;
|
|
217
|
+
const headers = { "x-amz-copy-source": `/${this.config.bucket}/${encodeURIComponent(sourceKey)}` };
|
|
218
|
+
if (metadata) {
|
|
219
|
+
headers["x-amz-metadata-directive"] = "REPLACE";
|
|
220
|
+
Object.entries(metadata).forEach(([key, value]) => {
|
|
221
|
+
headers[`x-amz-meta-${key}`] = value;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const signature = await createAwsSignature({
|
|
225
|
+
method: "PUT",
|
|
226
|
+
url,
|
|
227
|
+
region: this.config.region || "auto",
|
|
228
|
+
service: "s3",
|
|
229
|
+
accessKeyId: this.config.accessKeyId,
|
|
230
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
231
|
+
headers
|
|
232
|
+
});
|
|
233
|
+
const response = await fetch(url, {
|
|
234
|
+
method: "PUT",
|
|
235
|
+
headers: {
|
|
236
|
+
...signature.headers,
|
|
237
|
+
...headers
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) throw new Error(`Failed to copy: ${response.statusText}`);
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
key: destinationKey
|
|
244
|
+
};
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
key: sourceKey,
|
|
249
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Rename a file (same as move)
|
|
255
|
+
*/
|
|
256
|
+
async rename(oldKey, newKey) {
|
|
257
|
+
return this.move({
|
|
258
|
+
sourceKey: oldKey,
|
|
259
|
+
destinationKey: newKey
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Parse XML list response
|
|
264
|
+
*/
|
|
265
|
+
parseListResponse(xmlText) {
|
|
266
|
+
const files = [];
|
|
267
|
+
const contentsRegex = /<Contents>([\s\S]*?)<\/Contents>/g;
|
|
268
|
+
let match;
|
|
269
|
+
while ((match = contentsRegex.exec(xmlText)) !== null) {
|
|
270
|
+
const content = match[1];
|
|
271
|
+
const key = this.extractXmlValue(content, "Key");
|
|
272
|
+
const size = this.extractXmlValue(content, "Size");
|
|
273
|
+
const lastModified = this.extractXmlValue(content, "LastModified");
|
|
274
|
+
const etag = this.extractXmlValue(content, "ETag");
|
|
275
|
+
if (key) files.push({
|
|
276
|
+
key,
|
|
277
|
+
bucket: this.config.bucket,
|
|
278
|
+
size: parseInt(size || "0", 10),
|
|
279
|
+
lastModified: new Date(lastModified || Date.now()),
|
|
280
|
+
etag: etag?.replace(/"/g, "")
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return files;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Extract continuation token from XML
|
|
287
|
+
*/
|
|
288
|
+
extractContinuationToken(xmlText) {
|
|
289
|
+
return this.extractXmlValue(xmlText, "NextContinuationToken") || void 0;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Extract value from XML tag
|
|
293
|
+
*/
|
|
294
|
+
extractXmlValue(xml, tag) {
|
|
295
|
+
const regex = /* @__PURE__ */ new RegExp(`<${tag}>([^<]*)<\/${tag}>`);
|
|
296
|
+
const match = xml.match(regex);
|
|
297
|
+
return match ? match[1] : null;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Sort files by specified field
|
|
301
|
+
*/
|
|
302
|
+
sortFiles(files, sortBy, sortOrder) {
|
|
303
|
+
return files.sort((a, b) => {
|
|
304
|
+
let comparison = 0;
|
|
305
|
+
switch (sortBy) {
|
|
306
|
+
case "name":
|
|
307
|
+
comparison = a.key.localeCompare(b.key);
|
|
308
|
+
break;
|
|
309
|
+
case "size":
|
|
310
|
+
comparison = a.size - b.size;
|
|
311
|
+
break;
|
|
312
|
+
case "lastModified":
|
|
313
|
+
comparison = a.lastModified.getTime() - b.lastModified.getTime();
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
return sortOrder === "asc" ? comparison : -comparison;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/database/postgres-provider.ts
|
|
323
|
+
/**
|
|
324
|
+
* PostgreSQL Database Provider Implementation
|
|
325
|
+
*/
|
|
326
|
+
var PostgresDatabaseProvider = class {
|
|
327
|
+
sql;
|
|
328
|
+
tableName;
|
|
329
|
+
constructor(config) {
|
|
330
|
+
this.tableName = config.tableName || "files";
|
|
331
|
+
this.initializeConnection(config);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Initialize PostgreSQL connection
|
|
335
|
+
*/
|
|
336
|
+
async initializeConnection(config) {
|
|
337
|
+
try {
|
|
338
|
+
const postgres = await import("postgres");
|
|
339
|
+
if (config.connectionString) this.sql = postgres.default(config.connectionString);
|
|
340
|
+
else this.sql = postgres.default({
|
|
341
|
+
host: config.host,
|
|
342
|
+
port: config.port || 5432,
|
|
343
|
+
database: config.database,
|
|
344
|
+
user: config.user,
|
|
345
|
+
password: config.password
|
|
346
|
+
});
|
|
347
|
+
await this.ensureTableExists();
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error.code === "ERR_MODULE_NOT_FOUND") throw new FileManagerError("PostgreSQL dependency not found. Install \"postgres\" package to use database mode.", "MISSING_DEPENDENCY", 500);
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Create files table if it doesn't exist
|
|
355
|
+
*/
|
|
356
|
+
async ensureTableExists() {
|
|
357
|
+
await this.sql`
|
|
358
|
+
CREATE TABLE IF NOT EXISTS ${this.sql(this.tableName)} (
|
|
359
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
360
|
+
key TEXT NOT NULL UNIQUE,
|
|
361
|
+
size BIGINT NOT NULL,
|
|
362
|
+
content_type TEXT,
|
|
363
|
+
etag TEXT,
|
|
364
|
+
user_id TEXT,
|
|
365
|
+
custom_metadata JSONB,
|
|
366
|
+
last_modified TIMESTAMP NOT NULL,
|
|
367
|
+
uploaded_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
368
|
+
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
369
|
+
)
|
|
370
|
+
`;
|
|
371
|
+
await this.sql`
|
|
372
|
+
CREATE INDEX IF NOT EXISTS ${this.sql(`idx_${this.tableName}_key`)}
|
|
373
|
+
ON ${this.sql(this.tableName)} (key)
|
|
374
|
+
`;
|
|
375
|
+
await this.sql`
|
|
376
|
+
CREATE INDEX IF NOT EXISTS ${this.sql(`idx_${this.tableName}_user_id`)}
|
|
377
|
+
ON ${this.sql(this.tableName)} (user_id)
|
|
378
|
+
`;
|
|
379
|
+
await this.sql`
|
|
380
|
+
CREATE INDEX IF NOT EXISTS ${this.sql(`idx_${this.tableName}_uploaded_at`)}
|
|
381
|
+
ON ${this.sql(this.tableName)} (uploaded_at DESC)
|
|
382
|
+
`;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* List files from database
|
|
386
|
+
*/
|
|
387
|
+
async list(options = {}) {
|
|
388
|
+
const { prefix = "", maxKeys = 1e3, cursor, sortBy = "lastModified", sortOrder = "desc", userId } = options;
|
|
389
|
+
const offset = cursor?.offset || 0;
|
|
390
|
+
let query = this.sql`
|
|
391
|
+
SELECT
|
|
392
|
+
id::text,
|
|
393
|
+
key,
|
|
394
|
+
size,
|
|
395
|
+
content_type,
|
|
396
|
+
etag,
|
|
397
|
+
user_id,
|
|
398
|
+
custom_metadata,
|
|
399
|
+
last_modified,
|
|
400
|
+
uploaded_at,
|
|
401
|
+
updated_at
|
|
402
|
+
FROM ${this.sql(this.tableName)}
|
|
403
|
+
WHERE 1=1
|
|
404
|
+
`;
|
|
405
|
+
if (prefix) query = this.sql`${query} AND key LIKE ${prefix + "%"}`;
|
|
406
|
+
if (userId) query = this.sql`${query} AND user_id = ${userId}`;
|
|
407
|
+
const sortColumn = this.getSortColumn(sortBy);
|
|
408
|
+
const order = sortOrder.toUpperCase();
|
|
409
|
+
query = this.sql`${query} ORDER BY ${this.sql(sortColumn)} ${this.sql.unsafe(order)}`;
|
|
410
|
+
query = this.sql`${query} LIMIT ${maxKeys + 1} OFFSET ${offset}`;
|
|
411
|
+
const rows = await query;
|
|
412
|
+
const hasMore = rows.length > maxKeys;
|
|
413
|
+
return {
|
|
414
|
+
data: (hasMore ? rows.slice(0, maxKeys) : rows).map((row) => ({
|
|
415
|
+
id: row.id,
|
|
416
|
+
key: row.key,
|
|
417
|
+
size: Number(row.size),
|
|
418
|
+
contentType: row.content_type,
|
|
419
|
+
etag: row.etag,
|
|
420
|
+
userId: row.user_id,
|
|
421
|
+
customMetadata: row.custom_metadata,
|
|
422
|
+
lastModified: new Date(row.last_modified),
|
|
423
|
+
uploadedAt: new Date(row.uploaded_at),
|
|
424
|
+
updatedAt: new Date(row.updated_at)
|
|
425
|
+
})),
|
|
426
|
+
nextCursor: hasMore ? { offset: offset + maxKeys } : void 0,
|
|
427
|
+
hasMore
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get a single file by key
|
|
432
|
+
*/
|
|
433
|
+
async get(key) {
|
|
434
|
+
const rows = await this.sql`
|
|
435
|
+
SELECT
|
|
436
|
+
id::text,
|
|
437
|
+
key,
|
|
438
|
+
size,
|
|
439
|
+
content_type,
|
|
440
|
+
etag,
|
|
441
|
+
user_id,
|
|
442
|
+
custom_metadata,
|
|
443
|
+
last_modified,
|
|
444
|
+
uploaded_at,
|
|
445
|
+
updated_at
|
|
446
|
+
FROM ${this.sql(this.tableName)}
|
|
447
|
+
WHERE key = ${key}
|
|
448
|
+
LIMIT 1
|
|
449
|
+
`;
|
|
450
|
+
if (rows.length === 0) return null;
|
|
451
|
+
const row = rows[0];
|
|
452
|
+
return {
|
|
453
|
+
id: row.id,
|
|
454
|
+
key: row.key,
|
|
455
|
+
size: Number(row.size),
|
|
456
|
+
contentType: row.content_type,
|
|
457
|
+
etag: row.etag,
|
|
458
|
+
userId: row.user_id,
|
|
459
|
+
customMetadata: row.custom_metadata,
|
|
460
|
+
lastModified: new Date(row.last_modified),
|
|
461
|
+
uploadedAt: new Date(row.uploaded_at),
|
|
462
|
+
updatedAt: new Date(row.updated_at)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Create a new file record
|
|
467
|
+
*/
|
|
468
|
+
async create(file) {
|
|
469
|
+
const row = (await this.sql`
|
|
470
|
+
INSERT INTO ${this.sql(this.tableName)} (
|
|
471
|
+
key,
|
|
472
|
+
size,
|
|
473
|
+
content_type,
|
|
474
|
+
etag,
|
|
475
|
+
user_id,
|
|
476
|
+
custom_metadata,
|
|
477
|
+
last_modified
|
|
478
|
+
)
|
|
479
|
+
VALUES (
|
|
480
|
+
${file.key},
|
|
481
|
+
${file.size},
|
|
482
|
+
${file.contentType || null},
|
|
483
|
+
${file.etag || null},
|
|
484
|
+
${file.userId || null},
|
|
485
|
+
${file.customMetadata ? JSON.stringify(file.customMetadata) : null},
|
|
486
|
+
${file.lastModified}
|
|
487
|
+
)
|
|
488
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
489
|
+
size = EXCLUDED.size,
|
|
490
|
+
content_type = EXCLUDED.content_type,
|
|
491
|
+
etag = EXCLUDED.etag,
|
|
492
|
+
last_modified = EXCLUDED.last_modified,
|
|
493
|
+
updated_at = NOW()
|
|
494
|
+
RETURNING
|
|
495
|
+
id::text,
|
|
496
|
+
key,
|
|
497
|
+
size,
|
|
498
|
+
content_type,
|
|
499
|
+
etag,
|
|
500
|
+
user_id,
|
|
501
|
+
custom_metadata,
|
|
502
|
+
last_modified,
|
|
503
|
+
uploaded_at,
|
|
504
|
+
updated_at
|
|
505
|
+
`)[0];
|
|
506
|
+
return {
|
|
507
|
+
id: row.id,
|
|
508
|
+
key: row.key,
|
|
509
|
+
size: Number(row.size),
|
|
510
|
+
contentType: row.content_type,
|
|
511
|
+
etag: row.etag,
|
|
512
|
+
userId: row.user_id,
|
|
513
|
+
customMetadata: row.custom_metadata,
|
|
514
|
+
lastModified: new Date(row.last_modified),
|
|
515
|
+
uploadedAt: new Date(row.uploaded_at),
|
|
516
|
+
updatedAt: new Date(row.updated_at)
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Update file metadata
|
|
521
|
+
*/
|
|
522
|
+
async update(key, data) {
|
|
523
|
+
const updates = { updated_at: /* @__PURE__ */ new Date() };
|
|
524
|
+
if (data.size !== void 0) updates.size = data.size;
|
|
525
|
+
if (data.contentType !== void 0) updates.content_type = data.contentType;
|
|
526
|
+
if (data.etag !== void 0) updates.etag = data.etag;
|
|
527
|
+
if (data.userId !== void 0) updates.user_id = data.userId;
|
|
528
|
+
if (data.customMetadata !== void 0) updates.custom_metadata = JSON.stringify(data.customMetadata);
|
|
529
|
+
if (data.lastModified !== void 0) updates.last_modified = data.lastModified;
|
|
530
|
+
const rows = await this.sql`
|
|
531
|
+
UPDATE ${this.sql(this.tableName)}
|
|
532
|
+
SET ${this.sql(updates)}
|
|
533
|
+
WHERE key = ${key}
|
|
534
|
+
RETURNING
|
|
535
|
+
id::text,
|
|
536
|
+
key,
|
|
537
|
+
size,
|
|
538
|
+
content_type,
|
|
539
|
+
etag,
|
|
540
|
+
user_id,
|
|
541
|
+
custom_metadata,
|
|
542
|
+
last_modified,
|
|
543
|
+
uploaded_at,
|
|
544
|
+
updated_at
|
|
545
|
+
`;
|
|
546
|
+
if (rows.length === 0) return null;
|
|
547
|
+
const row = rows[0];
|
|
548
|
+
return {
|
|
549
|
+
id: row.id,
|
|
550
|
+
key: row.key,
|
|
551
|
+
size: Number(row.size),
|
|
552
|
+
contentType: row.content_type,
|
|
553
|
+
etag: row.etag,
|
|
554
|
+
userId: row.user_id,
|
|
555
|
+
customMetadata: row.custom_metadata,
|
|
556
|
+
lastModified: new Date(row.last_modified),
|
|
557
|
+
uploadedAt: new Date(row.uploaded_at),
|
|
558
|
+
updatedAt: new Date(row.updated_at)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Delete a file record
|
|
563
|
+
*/
|
|
564
|
+
async delete(key) {
|
|
565
|
+
try {
|
|
566
|
+
await this.sql`
|
|
567
|
+
DELETE FROM ${this.sql(this.tableName)}
|
|
568
|
+
WHERE key = ${key}
|
|
569
|
+
`;
|
|
570
|
+
return {
|
|
571
|
+
success: true,
|
|
572
|
+
key
|
|
573
|
+
};
|
|
574
|
+
} catch (error) {
|
|
575
|
+
return {
|
|
576
|
+
success: false,
|
|
577
|
+
key,
|
|
578
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Delete multiple file records
|
|
584
|
+
*/
|
|
585
|
+
async deleteBatch(keys) {
|
|
586
|
+
try {
|
|
587
|
+
const deleted = (await this.sql`
|
|
588
|
+
DELETE FROM ${this.sql(this.tableName)}
|
|
589
|
+
WHERE key = ANY(${keys})
|
|
590
|
+
RETURNING key
|
|
591
|
+
`).map((row) => row.key);
|
|
592
|
+
return {
|
|
593
|
+
succeeded: deleted,
|
|
594
|
+
failed: keys.filter((key) => !deleted.includes(key)).map((key) => ({
|
|
595
|
+
key,
|
|
596
|
+
error: "Not found"
|
|
597
|
+
})),
|
|
598
|
+
totalProcessed: keys.length
|
|
599
|
+
};
|
|
600
|
+
} catch (error) {
|
|
601
|
+
return {
|
|
602
|
+
succeeded: [],
|
|
603
|
+
failed: keys.map((key) => ({
|
|
604
|
+
key,
|
|
605
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
606
|
+
})),
|
|
607
|
+
totalProcessed: keys.length
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Check if file exists
|
|
613
|
+
*/
|
|
614
|
+
async exists(key) {
|
|
615
|
+
return (await this.sql`
|
|
616
|
+
SELECT 1 FROM ${this.sql(this.tableName)}
|
|
617
|
+
WHERE key = ${key}
|
|
618
|
+
LIMIT 1
|
|
619
|
+
`).length > 0;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Sync storage files to database
|
|
623
|
+
*/
|
|
624
|
+
async sync(storageFiles) {
|
|
625
|
+
if (storageFiles.length === 0) return;
|
|
626
|
+
const values = storageFiles.map((file) => ({
|
|
627
|
+
key: file.key,
|
|
628
|
+
size: file.size,
|
|
629
|
+
content_type: file.contentType || null,
|
|
630
|
+
etag: file.etag || null,
|
|
631
|
+
last_modified: file.lastModified
|
|
632
|
+
}));
|
|
633
|
+
await this.sql`
|
|
634
|
+
INSERT INTO ${this.sql(this.tableName)} ${this.sql(values)}
|
|
635
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
636
|
+
size = EXCLUDED.size,
|
|
637
|
+
content_type = EXCLUDED.content_type,
|
|
638
|
+
etag = EXCLUDED.etag,
|
|
639
|
+
last_modified = EXCLUDED.last_modified,
|
|
640
|
+
updated_at = NOW()
|
|
641
|
+
`;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Get sort column for SQL query
|
|
645
|
+
*/
|
|
646
|
+
getSortColumn(sortBy) {
|
|
647
|
+
switch (sortBy) {
|
|
648
|
+
case "name": return "key";
|
|
649
|
+
case "size": return "size";
|
|
650
|
+
case "lastModified": return "last_modified";
|
|
651
|
+
default: return "last_modified";
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Close database connection
|
|
656
|
+
*/
|
|
657
|
+
async close() {
|
|
658
|
+
if (this.sql) await this.sql.end();
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/file-manager.ts
|
|
664
|
+
/**
|
|
665
|
+
* Main FileManager class - Orchestrates file operations across storage and database
|
|
666
|
+
*/
|
|
667
|
+
var FileManager = class {
|
|
668
|
+
storage;
|
|
669
|
+
database;
|
|
670
|
+
defaultSource;
|
|
671
|
+
autoSync;
|
|
672
|
+
hooks;
|
|
673
|
+
constructor(config, hooks) {
|
|
674
|
+
this.storage = new S3StorageProvider(config.storage);
|
|
675
|
+
this.database = config.database ? new PostgresDatabaseProvider(config.database) : void 0;
|
|
676
|
+
this.defaultSource = config.defaultSource || (this.database ? "database" : "storage");
|
|
677
|
+
this.autoSync = config.autoSync ?? true;
|
|
678
|
+
this.hooks = hooks;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* List files from specified source
|
|
682
|
+
*/
|
|
683
|
+
async list(options = {}, source) {
|
|
684
|
+
const dataSource = source || this.defaultSource;
|
|
685
|
+
switch (dataSource) {
|
|
686
|
+
case "storage": return this.listFromStorage(options);
|
|
687
|
+
case "database":
|
|
688
|
+
if (!this.database) throw new FileManagerError("Database not configured", "DATABASE_NOT_CONFIGURED", 400);
|
|
689
|
+
return this.listFromDatabase(options);
|
|
690
|
+
case "both": return this.listFromBoth(options);
|
|
691
|
+
default: throw new FileManagerError(`Invalid data source: ${dataSource}`, "INVALID_DATA_SOURCE", 400);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* List files from storage only
|
|
696
|
+
*/
|
|
697
|
+
async listFromStorage(options = {}) {
|
|
698
|
+
return this.storage.list(options);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* List files from database only
|
|
702
|
+
*/
|
|
703
|
+
async listFromDatabase(options = {}) {
|
|
704
|
+
if (!this.database) throw new FileManagerError("Database not configured", "DATABASE_NOT_CONFIGURED", 400);
|
|
705
|
+
return this.database.list(options);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* List files from both storage and database, merged
|
|
709
|
+
*/
|
|
710
|
+
async listFromBoth(options = {}) {
|
|
711
|
+
if (!this.database) return this.listFromStorage(options);
|
|
712
|
+
const [storageResult, dbResult] = await Promise.all([this.storage.list(options), this.database.list(options)]);
|
|
713
|
+
const dbKeys = new Set(dbResult.data.map((f) => f.key));
|
|
714
|
+
return {
|
|
715
|
+
data: [...dbResult.data, ...storageResult.data.filter((f) => !dbKeys.has(f.key))],
|
|
716
|
+
nextCursor: dbResult.nextCursor || storageResult.nextCursor,
|
|
717
|
+
hasMore: dbResult.hasMore || storageResult.hasMore
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Get file metadata
|
|
722
|
+
*/
|
|
723
|
+
async getMetadata(key, source) {
|
|
724
|
+
if ((source || this.defaultSource) === "database" && this.database) return this.database.get(key);
|
|
725
|
+
return this.storage.getMetadata(key);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Check if file exists
|
|
729
|
+
*/
|
|
730
|
+
async exists(key, source) {
|
|
731
|
+
if ((source || this.defaultSource) === "database" && this.database) return this.database.exists(key);
|
|
732
|
+
return this.storage.exists(key);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Get download URL for a file
|
|
736
|
+
*/
|
|
737
|
+
async getDownloadUrl(key, options) {
|
|
738
|
+
return this.storage.getDownloadUrl(key, options);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Delete a single file
|
|
742
|
+
*/
|
|
743
|
+
async delete(key) {
|
|
744
|
+
if (this.hooks?.beforeDelete) await this.hooks.beforeDelete([key]);
|
|
745
|
+
const storageResult = await this.storage.delete(key);
|
|
746
|
+
if (this.database && storageResult.success) await this.database.delete(key);
|
|
747
|
+
if (this.hooks?.afterDelete) await this.hooks.afterDelete([key]);
|
|
748
|
+
return storageResult;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Delete multiple files in batch
|
|
752
|
+
*/
|
|
753
|
+
async deleteBatch(keys) {
|
|
754
|
+
if (this.hooks?.beforeDelete) await this.hooks.beforeDelete(keys);
|
|
755
|
+
const storageResult = await this.storage.deleteBatch(keys);
|
|
756
|
+
if (this.database && storageResult.succeeded.length > 0) await this.database.deleteBatch(storageResult.succeeded);
|
|
757
|
+
if (this.hooks?.afterDelete) await this.hooks.afterDelete(storageResult.succeeded);
|
|
758
|
+
return storageResult;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Move a file to a new location
|
|
762
|
+
*/
|
|
763
|
+
async move(options) {
|
|
764
|
+
if (this.hooks?.beforeMove) await this.hooks.beforeMove(options);
|
|
765
|
+
const result = await this.storage.move(options);
|
|
766
|
+
if (this.database && result.success) {
|
|
767
|
+
const dbFile = await this.database.get(options.sourceKey);
|
|
768
|
+
if (dbFile) {
|
|
769
|
+
await this.database.delete(options.sourceKey);
|
|
770
|
+
await this.database.create({
|
|
771
|
+
...dbFile,
|
|
772
|
+
key: options.destinationKey
|
|
773
|
+
});
|
|
774
|
+
} else if (this.autoSync) {
|
|
775
|
+
const metadata = await this.storage.getMetadata(options.destinationKey);
|
|
776
|
+
if (metadata) await this.database.create({
|
|
777
|
+
key: metadata.key,
|
|
778
|
+
size: metadata.size,
|
|
779
|
+
lastModified: metadata.lastModified,
|
|
780
|
+
contentType: metadata.contentType,
|
|
781
|
+
etag: metadata.etag
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (this.hooks?.afterMove) await this.hooks.afterMove(options);
|
|
786
|
+
return result;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Copy a file to a new location
|
|
790
|
+
*/
|
|
791
|
+
async copy(options) {
|
|
792
|
+
if (this.hooks?.beforeCopy) await this.hooks.beforeCopy(options);
|
|
793
|
+
const result = await this.storage.copy(options);
|
|
794
|
+
if (this.database && result.success) {
|
|
795
|
+
const sourceFile = await this.database.get(options.sourceKey);
|
|
796
|
+
if (sourceFile) await this.database.create({
|
|
797
|
+
...sourceFile,
|
|
798
|
+
key: options.destinationKey
|
|
799
|
+
});
|
|
800
|
+
else if (this.autoSync) {
|
|
801
|
+
const metadata = await this.storage.getMetadata(options.destinationKey);
|
|
802
|
+
if (metadata) await this.database.create({
|
|
803
|
+
key: metadata.key,
|
|
804
|
+
size: metadata.size,
|
|
805
|
+
lastModified: metadata.lastModified,
|
|
806
|
+
contentType: metadata.contentType,
|
|
807
|
+
etag: metadata.etag
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (this.hooks?.afterCopy) await this.hooks.afterCopy(options);
|
|
812
|
+
return result;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Rename a file (alias for move)
|
|
816
|
+
*/
|
|
817
|
+
async rename(oldKey, newKey) {
|
|
818
|
+
return this.move({
|
|
819
|
+
sourceKey: oldKey,
|
|
820
|
+
destinationKey: newKey
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Sync storage files to database
|
|
825
|
+
*/
|
|
826
|
+
async syncToDatabase(options = {}) {
|
|
827
|
+
if (!this.database) throw new FileManagerError("Database not configured", "DATABASE_NOT_CONFIGURED", 400);
|
|
828
|
+
let hasMore = true;
|
|
829
|
+
let cursor = options.cursor;
|
|
830
|
+
while (hasMore) {
|
|
831
|
+
const result = await this.storage.list({
|
|
832
|
+
...options,
|
|
833
|
+
cursor
|
|
834
|
+
});
|
|
835
|
+
await this.database.sync(result.data);
|
|
836
|
+
hasMore = result.hasMore;
|
|
837
|
+
cursor = result.nextCursor;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Create a file record in database (useful after upload)
|
|
842
|
+
*/
|
|
843
|
+
async createDatabaseRecord(file) {
|
|
844
|
+
if (!this.database) return null;
|
|
845
|
+
return this.database.create(file);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Update file metadata in database
|
|
849
|
+
*/
|
|
850
|
+
async updateDatabaseRecord(key, data) {
|
|
851
|
+
if (!this.database) return null;
|
|
852
|
+
return this.database.update(key, data);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/router.ts
|
|
858
|
+
/**
|
|
859
|
+
* Request validation schemas
|
|
860
|
+
*/
|
|
861
|
+
const ListOptionsSchema = z.object({
|
|
862
|
+
prefix: z.string().optional(),
|
|
863
|
+
delimiter: z.string().optional(),
|
|
864
|
+
maxKeys: z.number().min(1).max(1e4).optional(),
|
|
865
|
+
cursor: z.object({
|
|
866
|
+
continuationToken: z.string().optional(),
|
|
867
|
+
offset: z.number().optional()
|
|
868
|
+
}).optional(),
|
|
869
|
+
sortBy: z.enum([
|
|
870
|
+
"name",
|
|
871
|
+
"size",
|
|
872
|
+
"lastModified"
|
|
873
|
+
]).optional(),
|
|
874
|
+
sortOrder: z.enum(["asc", "desc"]).optional(),
|
|
875
|
+
userId: z.string().optional()
|
|
876
|
+
});
|
|
877
|
+
const DownloadUrlOptionsSchema = z.object({
|
|
878
|
+
expiresIn: z.number().optional(),
|
|
879
|
+
disposition: z.enum(["inline", "attachment"]).optional(),
|
|
880
|
+
filename: z.string().optional()
|
|
881
|
+
});
|
|
882
|
+
const MoveOptionsSchema = z.object({
|
|
883
|
+
sourceKey: z.string().min(1),
|
|
884
|
+
destinationKey: z.string().min(1),
|
|
885
|
+
deleteSource: z.boolean().optional()
|
|
886
|
+
});
|
|
887
|
+
const CopyOptionsSchema = z.object({
|
|
888
|
+
sourceKey: z.string().min(1),
|
|
889
|
+
destinationKey: z.string().min(1),
|
|
890
|
+
metadata: z.record(z.string()).optional()
|
|
891
|
+
});
|
|
892
|
+
const RenameOptionsSchema = z.object({ newKey: z.string().min(1) });
|
|
893
|
+
const FileOperationRequestSchema = z.object({
|
|
894
|
+
action: z.enum([
|
|
895
|
+
"list",
|
|
896
|
+
"delete",
|
|
897
|
+
"batchDelete",
|
|
898
|
+
"exists",
|
|
899
|
+
"getMetadata",
|
|
900
|
+
"getUrl",
|
|
901
|
+
"move",
|
|
902
|
+
"copy",
|
|
903
|
+
"rename",
|
|
904
|
+
"createFolder"
|
|
905
|
+
]),
|
|
906
|
+
key: z.string().optional(),
|
|
907
|
+
keys: z.array(z.string()).optional(),
|
|
908
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
909
|
+
source: z.enum([
|
|
910
|
+
"storage",
|
|
911
|
+
"database",
|
|
912
|
+
"both"
|
|
913
|
+
]).optional()
|
|
914
|
+
});
|
|
915
|
+
/**
|
|
916
|
+
* FileManager Router - Handles HTTP requests for file operations
|
|
917
|
+
*/
|
|
918
|
+
var FileManagerRouter = class {
|
|
919
|
+
fileManager;
|
|
920
|
+
authorize;
|
|
921
|
+
getAuthContext;
|
|
922
|
+
constructor(config) {
|
|
923
|
+
this.fileManager = config.fileManager;
|
|
924
|
+
this.authorize = config.authorize;
|
|
925
|
+
this.getAuthContext = config.getAuthContext;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Handle incoming request
|
|
929
|
+
*/
|
|
930
|
+
async handle(request) {
|
|
931
|
+
try {
|
|
932
|
+
const body = await request.json();
|
|
933
|
+
const validatedRequest = FileOperationRequestSchema.parse(body);
|
|
934
|
+
const authContext = this.getAuthContext ? await this.getAuthContext(request) : {};
|
|
935
|
+
if (this.authorize) {
|
|
936
|
+
if (!await this.authorize(authContext, validatedRequest.action, validatedRequest.key || "batch")) return this.errorResponse("Unauthorized", "UNAUTHORIZED", 403);
|
|
937
|
+
}
|
|
938
|
+
const result = await this.routeAction(validatedRequest, authContext);
|
|
939
|
+
return this.successResponse(result);
|
|
940
|
+
} catch (error) {
|
|
941
|
+
return this.handleError(error);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Route action to appropriate handler
|
|
946
|
+
*/
|
|
947
|
+
async routeAction(request, authContext) {
|
|
948
|
+
const { action, key, keys, options, source } = request;
|
|
949
|
+
switch (action) {
|
|
950
|
+
case "list": return this.fileManager.list(ListOptionsSchema.parse(options ?? {}), source);
|
|
951
|
+
case "delete":
|
|
952
|
+
if (!key) throw new FileManagerError("Key is required for delete", "MISSING_KEY", 400);
|
|
953
|
+
return this.fileManager.delete(key);
|
|
954
|
+
case "batchDelete":
|
|
955
|
+
if (!keys || keys.length === 0) throw new FileManagerError("Keys are required for batchDelete", "MISSING_KEYS", 400);
|
|
956
|
+
return this.fileManager.deleteBatch(keys);
|
|
957
|
+
case "exists":
|
|
958
|
+
if (!key) throw new FileManagerError("Key is required for exists", "MISSING_KEY", 400);
|
|
959
|
+
return { exists: await this.fileManager.exists(key, source) };
|
|
960
|
+
case "getMetadata":
|
|
961
|
+
if (!key) throw new FileManagerError("Key is required for getMetadata", "MISSING_KEY", 400);
|
|
962
|
+
return this.fileManager.getMetadata(key, source);
|
|
963
|
+
case "getUrl":
|
|
964
|
+
if (!key) throw new FileManagerError("Key is required for getUrl", "MISSING_KEY", 400);
|
|
965
|
+
return { url: await this.fileManager.getDownloadUrl(key, DownloadUrlOptionsSchema.parse(options ?? {})) };
|
|
966
|
+
case "move":
|
|
967
|
+
const moveOpts = MoveOptionsSchema.parse(options);
|
|
968
|
+
return this.fileManager.move(moveOpts);
|
|
969
|
+
case "copy":
|
|
970
|
+
const copyOpts = CopyOptionsSchema.parse(options);
|
|
971
|
+
return this.fileManager.copy(copyOpts);
|
|
972
|
+
case "rename":
|
|
973
|
+
if (!key) throw new FileManagerError("Key is required for rename", "MISSING_KEY", 400);
|
|
974
|
+
const renameOpts = RenameOptionsSchema.parse(options);
|
|
975
|
+
return this.fileManager.rename(key, renameOpts.newKey);
|
|
976
|
+
case "createFolder":
|
|
977
|
+
if (!key) throw new FileManagerError("Key is required for createFolder", "MISSING_KEY", 400);
|
|
978
|
+
return {
|
|
979
|
+
success: true,
|
|
980
|
+
key,
|
|
981
|
+
message: "Folder created"
|
|
982
|
+
};
|
|
983
|
+
default: throw new FileManagerError(`Unknown action: ${action}`, "UNKNOWN_ACTION", 400);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Success response
|
|
988
|
+
*/
|
|
989
|
+
successResponse(data) {
|
|
990
|
+
const response = {
|
|
991
|
+
success: true,
|
|
992
|
+
data
|
|
993
|
+
};
|
|
994
|
+
return new Response(JSON.stringify(response), {
|
|
995
|
+
status: 200,
|
|
996
|
+
headers: { "Content-Type": "application/json" }
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Error response
|
|
1001
|
+
*/
|
|
1002
|
+
errorResponse(message, code, status) {
|
|
1003
|
+
const response = {
|
|
1004
|
+
success: false,
|
|
1005
|
+
error: message,
|
|
1006
|
+
code
|
|
1007
|
+
};
|
|
1008
|
+
return new Response(JSON.stringify(response), {
|
|
1009
|
+
status,
|
|
1010
|
+
headers: { "Content-Type": "application/json" }
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Handle errors
|
|
1015
|
+
*/
|
|
1016
|
+
handleError(error) {
|
|
1017
|
+
console.error("FileManager Router Error:", error);
|
|
1018
|
+
if (error instanceof FileManagerError) return this.errorResponse(error.message, error.code, error.statusCode);
|
|
1019
|
+
if (error instanceof z.ZodError) return this.errorResponse(`Validation error: ${error.errors.map((e) => e.message).join(", ")}`, "VALIDATION_ERROR", 400);
|
|
1020
|
+
return this.errorResponse("Internal server error", "INTERNAL_ERROR", 500);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
/**
|
|
1024
|
+
* Create a new FileManager router instance
|
|
1025
|
+
*/
|
|
1026
|
+
function createFileManagerRouter(config) {
|
|
1027
|
+
return new FileManagerRouter(config);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
//#endregion
|
|
1031
|
+
export { S3StorageProvider as a, PostgresDatabaseProvider as i, createFileManagerRouter as n, FileManagerError as o, FileManager as r, FileManagerRouter as t };
|