next-file-manager 0.1.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.
@@ -0,0 +1,1413 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/server/index.ts
9
+ import formidable from "formidable";
10
+ import path4 from "path";
11
+ import fs5 from "fs";
12
+
13
+ // src/server/config.ts
14
+ import mongoose from "mongoose";
15
+ var globalConfig = null;
16
+ var driveConfiguration = (config) => {
17
+ if (mongoose.connection.readyState !== 1) {
18
+ throw new Error("Database not connected. Please connect to Mongoose before initializing next-file-manager.");
19
+ }
20
+ const mergedConfig = {
21
+ ...config,
22
+ security: {
23
+ maxUploadSizeInBytes: config.security?.maxUploadSizeInBytes ?? 10 * 1024 * 1024,
24
+ // Default to 10MB
25
+ allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
26
+ signedUrls: config.security?.signedUrls,
27
+ trash: config.security?.trash
28
+ },
29
+ information: config.information ?? (async (req) => {
30
+ return {
31
+ key: { id: "default-user" },
32
+ storage: { quotaInBytes: 10 * 1024 * 1024 * 1024 }
33
+ // Default to 10GB
34
+ };
35
+ })
36
+ };
37
+ globalConfig = mergedConfig;
38
+ return mergedConfig;
39
+ };
40
+ var getDriveConfig = () => {
41
+ if (!globalConfig) throw new Error("Drive configuration not initialized");
42
+ return globalConfig;
43
+ };
44
+ var getDriveInformation = async (req) => {
45
+ const config = getDriveConfig();
46
+ return config.information(req);
47
+ };
48
+
49
+ // src/server/database/mongoose/schema/drive.ts
50
+ import mongoose2, { Schema } from "mongoose";
51
+ var informationSchema = new Schema({
52
+ type: { type: String, enum: ["FILE", "FOLDER"], required: true },
53
+ sizeInBytes: { type: Number, default: 0 },
54
+ mime: { type: String },
55
+ path: { type: String },
56
+ width: { type: Number },
57
+ height: { type: Number },
58
+ duration: { type: Number },
59
+ hash: { type: String }
60
+ }, { _id: false });
61
+ var providerSchema = new Schema({
62
+ type: { type: String, enum: ["LOCAL", "GOOGLE"], required: true, default: "LOCAL" },
63
+ google: { type: Schema.Types.Mixed }
64
+ }, { _id: false });
65
+ var DriveSchema = new Schema(
66
+ {
67
+ owner: { type: Schema.Types.Mixed, default: null },
68
+ storageAccountId: { type: Schema.Types.ObjectId, ref: "StorageAccount", default: null },
69
+ name: { type: String, required: true },
70
+ parentId: { type: Schema.Types.ObjectId, ref: "Drive", default: null },
71
+ order: { type: Number, default: 0 },
72
+ provider: { type: providerSchema, default: () => ({ type: "LOCAL" }) },
73
+ metadata: { type: Schema.Types.Mixed, default: {} },
74
+ information: { type: informationSchema, required: true },
75
+ status: { type: String, enum: ["READY", "PROCESSING", "UPLOADING", "FAILED"], default: "PROCESSING" },
76
+ trashedAt: { type: Date, default: null },
77
+ createdAt: { type: Date, default: Date.now }
78
+ },
79
+ { minimize: false }
80
+ );
81
+ DriveSchema.index({ owner: 1, "information.type": 1 });
82
+ DriveSchema.index({ owner: 1, "provider.type": 1, "provider.google.id": 1 });
83
+ DriveSchema.index({ owner: 1, storageAccountId: 1 });
84
+ DriveSchema.index({ owner: 1, trashedAt: 1 });
85
+ DriveSchema.index({ owner: 1, "information.hash": 1 });
86
+ DriveSchema.index({ owner: 1, name: "text" });
87
+ DriveSchema.index({ owner: 1, "provider.type": 1 });
88
+ DriveSchema.method("toClient", async function() {
89
+ const data = this.toJSON();
90
+ return {
91
+ id: String(data._id),
92
+ name: data.name,
93
+ parentId: data.parentId ? String(data.parentId) : null,
94
+ order: data.order,
95
+ provider: data.provider,
96
+ metadata: data.metadata,
97
+ information: data.information,
98
+ status: data.status,
99
+ trashedAt: data.trashedAt,
100
+ createdAt: data.createdAt
101
+ };
102
+ });
103
+ var Drive = mongoose2.models.Drive || mongoose2.model("Drive", DriveSchema);
104
+ var drive_default = Drive;
105
+
106
+ // src/server/database/mongoose/schema/storage/account.ts
107
+ import mongoose3, { Schema as Schema2 } from "mongoose";
108
+ var StorageAccountSchema = new Schema2(
109
+ {
110
+ owner: { type: Schema2.Types.Mixed, default: null },
111
+ name: { type: String, required: true },
112
+ metadata: {
113
+ provider: { type: String, enum: ["GOOGLE"], required: true },
114
+ google: {
115
+ email: { type: String, required: true },
116
+ credentials: { type: Schema2.Types.Mixed, required: true }
117
+ }
118
+ },
119
+ createdAt: { type: Date, default: Date.now }
120
+ },
121
+ { minimize: false }
122
+ );
123
+ StorageAccountSchema.index({ owner: 1, "metadata.provider": 1 });
124
+ StorageAccountSchema.index({ owner: 1, "metadata.google.email": 1 });
125
+ StorageAccountSchema.method("toClient", async function() {
126
+ const data = this.toJSON();
127
+ return {
128
+ id: String(data._id),
129
+ owner: data.owner,
130
+ name: data.name,
131
+ metadata: data.metadata,
132
+ createdAt: data.createdAt
133
+ };
134
+ });
135
+ var StorageAccount = mongoose3.models.StorageAccount || mongoose3.model("StorageAccount", StorageAccountSchema);
136
+ var account_default = StorageAccount;
137
+
138
+ // src/server/utils.ts
139
+ import fs from "fs";
140
+ import crypto2 from "crypto";
141
+ import sharp from "sharp";
142
+ var validateMimeType = (mime, allowedTypes) => {
143
+ if (allowedTypes.includes("*/*")) return true;
144
+ return allowedTypes.some((pattern) => {
145
+ if (pattern === mime) return true;
146
+ if (pattern.endsWith("/*")) {
147
+ const prefix = pattern.slice(0, -2);
148
+ return mime.startsWith(`${prefix}/`);
149
+ }
150
+ return false;
151
+ });
152
+ };
153
+ var computeFileHash = (filePath) => new Promise((resolve, reject) => {
154
+ const hash = crypto2.createHash("sha256");
155
+ const stream = fs.createReadStream(filePath);
156
+ stream.on("data", (data) => hash.update(data));
157
+ stream.on("end", () => resolve(hash.digest("hex")));
158
+ stream.on("error", reject);
159
+ });
160
+ var extractImageMetadata = async (filePath) => {
161
+ try {
162
+ const { width = 0, height = 0, exif } = await sharp(filePath).metadata();
163
+ return { width, height, ...exif && { exif: { raw: exif.toString("base64") } } };
164
+ } catch {
165
+ return null;
166
+ }
167
+ };
168
+
169
+ // src/server/zod/schemas.ts
170
+ import { z } from "zod";
171
+ import { isValidObjectId } from "mongoose";
172
+ var objectIdSchema = z.string().refine((val) => isValidObjectId(val), {
173
+ message: "Invalid ObjectId format"
174
+ });
175
+ var sanitizeFilename = (name) => {
176
+ return name.replace(/[<>:"|?*\x00-\x1F]/g, "").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\.\.\//g, "").replace(/\.\.+/g, "").split("/").pop() || "".trim().slice(0, 255);
177
+ };
178
+ var sanitizeRegexInput = (input) => {
179
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 100);
180
+ };
181
+ var nameSchema = z.string().min(1, "Name is required").max(255, "Name too long").transform(sanitizeFilename).refine((val) => val.length > 0, { message: "Invalid name after sanitization" });
182
+ var uploadChunkSchema = z.object({
183
+ chunkIndex: z.number().int().min(0).max(1e4),
184
+ totalChunks: z.number().int().min(1).max(1e4),
185
+ driveId: z.string().optional(),
186
+ fileName: nameSchema,
187
+ fileSize: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
188
+ fileType: z.string().min(1).max(255),
189
+ folderId: z.string().optional()
190
+ }).refine((data) => data.chunkIndex < data.totalChunks, {
191
+ message: "Chunk index must be less than total chunks"
192
+ });
193
+ var listQuerySchema = z.object({
194
+ folderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]),
195
+ limit: z.string().optional().transform((val) => {
196
+ const num = parseInt(val || "50", 10);
197
+ return Math.min(Math.max(1, num), 100);
198
+ }),
199
+ afterId: objectIdSchema.optional()
200
+ });
201
+ var serveQuerySchema = z.object({
202
+ id: objectIdSchema,
203
+ token: z.string().optional(),
204
+ q: z.enum(["ultralow", "low", "medium", "high", "normal"]).optional(),
205
+ format: z.enum(["webp", "jpeg", "png"]).optional()
206
+ });
207
+ var thumbnailQuerySchema = z.object({
208
+ id: objectIdSchema,
209
+ size: z.enum(["small", "medium", "large"]).optional().default("medium"),
210
+ token: z.string().optional()
211
+ });
212
+ var renameBodySchema = z.object({
213
+ id: objectIdSchema,
214
+ newName: nameSchema
215
+ });
216
+ var deleteQuerySchema = z.object({
217
+ id: objectIdSchema
218
+ });
219
+ var deleteManyBodySchema = z.object({
220
+ ids: z.array(objectIdSchema).min(1).max(1e3)
221
+ });
222
+ var createFolderBodySchema = z.object({
223
+ name: nameSchema,
224
+ parentId: z.union([z.literal("root"), objectIdSchema, z.string().length(0), z.undefined()]).optional()
225
+ });
226
+ var moveBodySchema = z.object({
227
+ ids: z.array(objectIdSchema).min(1).max(1e3),
228
+ targetFolderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]).optional()
229
+ });
230
+ var reorderBodySchema = z.object({
231
+ ids: z.array(objectIdSchema).min(1).max(1e3)
232
+ });
233
+ var searchQuerySchema = z.object({
234
+ q: z.string().min(1).max(100).transform(sanitizeRegexInput),
235
+ folderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]).optional(),
236
+ limit: z.string().optional().transform((val) => {
237
+ const num = parseInt(val || "50", 10);
238
+ return Math.min(Math.max(1, num), 100);
239
+ }),
240
+ trashed: z.string().optional().transform((val) => val === "true")
241
+ });
242
+ var restoreQuerySchema = z.object({
243
+ id: objectIdSchema
244
+ });
245
+ var cancelQuerySchema = z.object({
246
+ id: objectIdSchema
247
+ });
248
+ var purgeTrashQuerySchema = z.object({
249
+ days: z.number().int().min(1).max(365).optional()
250
+ });
251
+ var driveFileSchemaZod = z.object({
252
+ id: z.string(),
253
+ file: z.object({
254
+ name: z.string(),
255
+ mime: z.string(),
256
+ size: z.number()
257
+ })
258
+ });
259
+
260
+ // src/server/security/cryptoUtils.ts
261
+ function sanitizeContentDispositionFilename(filename) {
262
+ const basename = filename.replace(/^.*[\\\/]/, "");
263
+ return basename.replace(/["\r\n]/g, "").replace(/[^\x20-\x7E]/g, "").slice(0, 255);
264
+ }
265
+
266
+ // src/server/providers/local.ts
267
+ import fs2 from "fs";
268
+ import path from "path";
269
+ import mongoose4 from "mongoose";
270
+ import ffmpeg from "fluent-ffmpeg";
271
+ var STORAGE_PATH = path.join(process.cwd(), "storage");
272
+ var LocalStorageProvider = {
273
+ name: "LOCAL",
274
+ sync: async (folderId, owner, accountId) => {
275
+ },
276
+ search: async (query, owner, accountId) => {
277
+ },
278
+ getQuota: async (owner, accountId) => {
279
+ const result = await drive_default.aggregate([
280
+ { $match: { owner, "information.type": "FILE", trashedAt: null } },
281
+ { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }
282
+ ]);
283
+ const usedInBytes = result[0]?.total || 0;
284
+ const config = getDriveConfig();
285
+ return { usedInBytes, quotaInBytes: 10 * 1024 * 1024 * 1024 };
286
+ },
287
+ openStream: async (item, accountId) => {
288
+ if (item.information.type !== "FILE") throw new Error("Cannot stream folder");
289
+ const filePath = path.join(STORAGE_PATH, item.information.path);
290
+ if (!fs2.existsSync(filePath)) {
291
+ throw new Error("File not found on disk");
292
+ }
293
+ const stat = fs2.statSync(filePath);
294
+ const stream = fs2.createReadStream(filePath);
295
+ return {
296
+ stream,
297
+ mime: item.information.mime,
298
+ size: stat.size
299
+ };
300
+ },
301
+ getThumbnail: async (item, accountId) => {
302
+ if (item.information.type !== "FILE") throw new Error("No thumbnail for folder");
303
+ const originalPath = path.join(STORAGE_PATH, item.information.path);
304
+ const thumbPath = path.join(STORAGE_PATH, "cache", "thumbnails", `${item._id.toString()}.webp`);
305
+ if (!fs2.existsSync(originalPath)) throw new Error("Original file not found");
306
+ if (fs2.existsSync(thumbPath)) {
307
+ return fs2.createReadStream(thumbPath);
308
+ }
309
+ if (!fs2.existsSync(path.dirname(thumbPath))) fs2.mkdirSync(path.dirname(thumbPath), { recursive: true });
310
+ if (item.information.mime.startsWith("image/")) {
311
+ const sharp2 = __require("sharp");
312
+ await sharp2(originalPath).resize(300, 300, { fit: "inside" }).toFormat("webp", { quality: 80 }).toFile(thumbPath);
313
+ } else if (item.information.mime.startsWith("video/")) {
314
+ await new Promise((resolve, reject) => {
315
+ ffmpeg(originalPath).screenshots({
316
+ count: 1,
317
+ folder: path.dirname(thumbPath),
318
+ filename: path.basename(thumbPath),
319
+ size: "300x?"
320
+ }).on("end", resolve).on("error", reject);
321
+ });
322
+ } else {
323
+ throw new Error("Unsupported mime type for thumbnail");
324
+ }
325
+ return fs2.createReadStream(thumbPath);
326
+ },
327
+ createFolder: async (name, parentId, owner, accountId) => {
328
+ const getNextOrderValue = async (owner2) => {
329
+ const lastItem = await drive_default.findOne({ owner: owner2 }, {}, { sort: { order: -1 } });
330
+ return lastItem ? lastItem.order + 1 : 0;
331
+ };
332
+ const folder = new drive_default({
333
+ owner,
334
+ name,
335
+ parentId: parentId === "root" || !parentId ? null : parentId,
336
+ order: await getNextOrderValue(owner),
337
+ provider: { type: "LOCAL" },
338
+ information: { type: "FOLDER" },
339
+ status: "READY"
340
+ });
341
+ await folder.save();
342
+ return folder.toClient();
343
+ },
344
+ uploadFile: async (drive, filePath, accountId) => {
345
+ if (drive.information.type !== "FILE") throw new Error("Invalid drive type");
346
+ const destPath = path.join(STORAGE_PATH, drive.information.path);
347
+ const dirPath = path.dirname(destPath);
348
+ if (!fs2.existsSync(dirPath)) fs2.mkdirSync(dirPath, { recursive: true });
349
+ fs2.renameSync(filePath, destPath);
350
+ drive.status = "READY";
351
+ drive.information.hash = await computeFileHash(destPath);
352
+ if (drive.information.mime.startsWith("image/")) {
353
+ const meta = await extractImageMetadata(destPath);
354
+ if (meta) {
355
+ drive.information.width = meta.width;
356
+ drive.information.height = meta.height;
357
+ }
358
+ }
359
+ await drive.save();
360
+ return drive.toClient();
361
+ },
362
+ delete: async (ids, owner, accountId) => {
363
+ const items = await drive_default.find({ _id: { $in: ids }, owner }).lean();
364
+ const getAllChildren = async (folderIds2) => {
365
+ const children = await drive_default.find({ parentId: { $in: folderIds2 }, owner }).lean();
366
+ if (children.length === 0) return [];
367
+ const subFolderIds = children.filter((c) => c.information.type === "FOLDER").map((c) => c._id.toString());
368
+ const subChildren = await getAllChildren(subFolderIds);
369
+ return [...children, ...subChildren];
370
+ };
371
+ const folderIds = items.filter((i) => i.information.type === "FOLDER").map((i) => i._id.toString());
372
+ const allChildren = await getAllChildren(folderIds);
373
+ const allItemsToDelete = [...items, ...allChildren];
374
+ for (const item of allItemsToDelete) {
375
+ if (item.information.type === "FILE" && item.information.path) {
376
+ const fullPath = path.join(STORAGE_PATH, item.information.path);
377
+ const dirPath = path.dirname(fullPath);
378
+ if (fs2.existsSync(dirPath)) {
379
+ fs2.rmSync(dirPath, { recursive: true, force: true });
380
+ }
381
+ }
382
+ }
383
+ await drive_default.deleteMany({ _id: { $in: allItemsToDelete.map((i) => i._id) } });
384
+ },
385
+ trash: async (ids, owner, accountId) => {
386
+ },
387
+ syncTrash: async (owner, accountId) => {
388
+ },
389
+ untrash: async (ids, owner, accountId) => {
390
+ },
391
+ rename: async (id, newName, owner, accountId) => {
392
+ const item = await drive_default.findOneAndUpdate(
393
+ { _id: id, owner },
394
+ { name: newName },
395
+ { new: true }
396
+ );
397
+ if (!item) throw new Error("Item not found");
398
+ return item.toClient();
399
+ },
400
+ move: async (id, newParentId, owner, accountId) => {
401
+ const item = await drive_default.findOne({ _id: id, owner });
402
+ if (!item) throw new Error("Item not found");
403
+ const oldParentId = item.parentId;
404
+ item.parentId = newParentId === "root" || !newParentId ? null : new mongoose4.Types.ObjectId(newParentId);
405
+ await item.save();
406
+ return item.toClient();
407
+ },
408
+ revokeToken: async (owner, accountId) => {
409
+ }
410
+ };
411
+
412
+ // src/server/providers/google.ts
413
+ import fs3 from "fs";
414
+ import path2 from "path";
415
+ import { google } from "googleapis";
416
+ import mongoose5 from "mongoose";
417
+ var STORAGE_PATH2 = path2.join(process.cwd(), "storage");
418
+ var createAuthClient = async (owner, accountId) => {
419
+ const query = { owner, "metadata.provider": "GOOGLE" };
420
+ if (accountId) query._id = accountId;
421
+ const account = await account_default.findOne(query);
422
+ if (!account) throw new Error("Google Drive account not connected");
423
+ const config = getDriveConfig();
424
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
425
+ if (!clientId || !clientSecret) throw new Error("Google credentials not configured on server");
426
+ const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
427
+ if (account.metadata.provider !== "GOOGLE" || !account.metadata.google) {
428
+ throw new Error("Invalid Google Account Metadata");
429
+ }
430
+ oAuth2Client.setCredentials(account.metadata.google.credentials);
431
+ oAuth2Client.on("tokens", async (tokens) => {
432
+ if (tokens.refresh_token) {
433
+ account.metadata.google.credentials = { ...account.metadata.google.credentials, ...tokens };
434
+ account.markModified("metadata");
435
+ await account.save();
436
+ }
437
+ });
438
+ return { client: oAuth2Client, accountId: account._id };
439
+ };
440
+ var GoogleDriveProvider = {
441
+ name: "GOOGLE",
442
+ sync: async (folderId, owner, accountId) => {
443
+ const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
444
+ const drive = google.drive({ version: "v3", auth: client });
445
+ let googleParentId = "root";
446
+ if (folderId && folderId !== "root") {
447
+ const folder = await drive_default.findOne({ _id: folderId, owner });
448
+ if (folder && folder.provider?.google?.id) {
449
+ googleParentId = folder.provider.google.id;
450
+ } else {
451
+ return;
452
+ }
453
+ }
454
+ let nextPageToken = void 0;
455
+ const allSyncedGoogleIds = /* @__PURE__ */ new Set();
456
+ do {
457
+ const res = await drive.files.list({
458
+ q: `'${googleParentId}' in parents and trashed = false`,
459
+ fields: "nextPageToken, files(id, name, mimeType, size, webViewLink, iconLink, thumbnailLink)",
460
+ pageSize: 1e3,
461
+ pageToken: nextPageToken
462
+ });
463
+ nextPageToken = res.data.nextPageToken || void 0;
464
+ const files = res.data.files || [];
465
+ for (const file of files) {
466
+ if (!file.id || !file.name || !file.mimeType) continue;
467
+ allSyncedGoogleIds.add(file.id);
468
+ const isFolder = file.mimeType === "application/vnd.google-apps.folder";
469
+ const sizeInBytes = file.size ? parseInt(file.size) : 0;
470
+ const updateData = {
471
+ name: file.name,
472
+ storageAccountId: foundAccountId,
473
+ parentId: folderId === "root" ? null : folderId,
474
+ information: {
475
+ type: isFolder ? "FOLDER" : "FILE",
476
+ sizeInBytes,
477
+ mime: file.mimeType,
478
+ path: ""
479
+ },
480
+ provider: {
481
+ type: "GOOGLE",
482
+ google: {
483
+ id: file.id,
484
+ webViewLink: file.webViewLink,
485
+ iconLink: file.iconLink,
486
+ thumbnailLink: file.thumbnailLink
487
+ }
488
+ },
489
+ status: "READY",
490
+ trashedAt: null
491
+ };
492
+ await drive_default.findOneAndUpdate(
493
+ {
494
+ owner,
495
+ "provider.google.id": file.id,
496
+ "provider.type": "GOOGLE"
497
+ },
498
+ { $set: updateData },
499
+ { upsert: true, new: true, setDefaultsOnInsert: true }
500
+ );
501
+ }
502
+ } while (nextPageToken);
503
+ const dbItems = await drive_default.find({
504
+ owner,
505
+ storageAccountId: foundAccountId,
506
+ parentId: folderId === "root" ? null : folderId,
507
+ "provider.type": "GOOGLE"
508
+ });
509
+ for (const item of dbItems) {
510
+ if (item.provider?.google?.id && !allSyncedGoogleIds.has(item.provider.google.id)) {
511
+ item.trashedAt = /* @__PURE__ */ new Date();
512
+ await item.save();
513
+ }
514
+ }
515
+ },
516
+ syncTrash: async (owner, accountId) => {
517
+ const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
518
+ const drive = google.drive({ version: "v3", auth: client });
519
+ let nextPageToken = void 0;
520
+ do {
521
+ const res = await drive.files.list({
522
+ q: "trashed = true",
523
+ fields: "nextPageToken, files(id, name, mimeType, size, webViewLink, iconLink, thumbnailLink)",
524
+ pageSize: 100,
525
+ // Limit sync for performance
526
+ pageToken: nextPageToken
527
+ });
528
+ nextPageToken = res.data.nextPageToken || void 0;
529
+ const files = res.data.files || [];
530
+ for (const file of files) {
531
+ if (!file.id || !file.name || !file.mimeType) continue;
532
+ const isFolder = file.mimeType === "application/vnd.google-apps.folder";
533
+ const sizeInBytes = file.size ? parseInt(file.size) : 0;
534
+ await drive_default.findOneAndUpdate(
535
+ { owner, "provider.google.id": file.id, "provider.type": "GOOGLE" },
536
+ {
537
+ $set: {
538
+ name: file.name,
539
+ storageAccountId: foundAccountId,
540
+ information: {
541
+ type: isFolder ? "FOLDER" : "FILE",
542
+ sizeInBytes,
543
+ mime: file.mimeType,
544
+ path: ""
545
+ },
546
+ provider: {
547
+ type: "GOOGLE",
548
+ google: {
549
+ id: file.id,
550
+ webViewLink: file.webViewLink,
551
+ iconLink: file.iconLink,
552
+ thumbnailLink: file.thumbnailLink
553
+ }
554
+ },
555
+ trashedAt: /* @__PURE__ */ new Date()
556
+ }
557
+ },
558
+ { upsert: true, setDefaultsOnInsert: true }
559
+ );
560
+ }
561
+ } while (nextPageToken);
562
+ },
563
+ search: async (query, owner, accountId) => {
564
+ const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
565
+ const drive = google.drive({ version: "v3", auth: client });
566
+ const res = await drive.files.list({
567
+ q: `name contains '${query}' and trashed = false`,
568
+ fields: "files(id, name, mimeType, size, parents, webViewLink, iconLink, thumbnailLink)",
569
+ pageSize: 50
570
+ });
571
+ const files = res.data.files || [];
572
+ for (const file of files) {
573
+ if (!file.id || !file.name) continue;
574
+ const isFolder = file.mimeType === "application/vnd.google-apps.folder";
575
+ if (!isFolder && file.mimeType?.startsWith("application/vnd.google-apps.")) continue;
576
+ const sizeInBytes = file.size ? parseInt(file.size) : 0;
577
+ await drive_default.findOneAndUpdate(
578
+ { owner, "provider.google.id": file.id, "metadata.type": "GOOGLE" },
579
+ {
580
+ $set: {
581
+ name: file.name,
582
+ storageAccountId: foundAccountId,
583
+ information: {
584
+ type: isFolder ? "FOLDER" : "FILE",
585
+ sizeInBytes,
586
+ mime: file.mimeType,
587
+ path: ""
588
+ },
589
+ metadata: {
590
+ type: "GOOGLE"
591
+ },
592
+ provider: {
593
+ google: {
594
+ id: file.id,
595
+ webViewLink: file.webViewLink,
596
+ iconLink: file.iconLink,
597
+ thumbnailLink: file.thumbnailLink
598
+ }
599
+ }
600
+ // Don't overwrite parentId if it exists.
601
+ // New items will default to null (Root) via schema default
602
+ }
603
+ },
604
+ { upsert: true, setDefaultsOnInsert: true }
605
+ );
606
+ }
607
+ },
608
+ getQuota: async (owner, accountId) => {
609
+ try {
610
+ const { client } = await createAuthClient(owner, accountId);
611
+ const drive = google.drive({ version: "v3", auth: client });
612
+ const res = await drive.about.get({ fields: "storageQuota" });
613
+ return {
614
+ usedInBytes: parseInt(res.data.storageQuota?.usage || "0"),
615
+ quotaInBytes: parseInt(res.data.storageQuota?.limit || "0")
616
+ };
617
+ } catch {
618
+ return { usedInBytes: 0, quotaInBytes: 0 };
619
+ }
620
+ },
621
+ openStream: async (item, accountId) => {
622
+ const { client } = await createAuthClient(item.owner, accountId || item.storageAccountId?.toString());
623
+ const drive = google.drive({ version: "v3", auth: client });
624
+ if (!item.provider?.google?.id) throw new Error("Missing Google File ID");
625
+ if (item.information.type === "FOLDER") throw new Error("Cannot stream folder");
626
+ const res = await drive.files.get(
627
+ { fileId: item.provider.google.id, alt: "media" },
628
+ { responseType: "stream" }
629
+ );
630
+ return {
631
+ stream: res.data,
632
+ mime: item.information.mime,
633
+ size: item.information.sizeInBytes
634
+ };
635
+ },
636
+ getThumbnail: async (item, accountId) => {
637
+ const { client } = await createAuthClient(item.owner, accountId || item.storageAccountId?.toString());
638
+ if (!item.provider?.google?.thumbnailLink) throw new Error("No thumbnail available");
639
+ const res = await client.request({ url: item.provider.google.thumbnailLink, responseType: "stream" });
640
+ return res.data;
641
+ },
642
+ createFolder: async (name, parentId, owner, accountId) => {
643
+ const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
644
+ const drive = google.drive({ version: "v3", auth: client });
645
+ let googleParentId = "root";
646
+ if (parentId && parentId !== "root") {
647
+ const parent = await drive_default.findOne({ _id: parentId, owner });
648
+ if (parent?.provider?.google?.id) googleParentId = parent.provider.google.id;
649
+ }
650
+ const res = await drive.files.create({
651
+ requestBody: {
652
+ name,
653
+ mimeType: "application/vnd.google-apps.folder",
654
+ parents: [googleParentId]
655
+ },
656
+ fields: "id, name, mimeType, webViewLink, iconLink"
657
+ });
658
+ const file = res.data;
659
+ if (!file.id) throw new Error("Failed to create folder on Google Drive");
660
+ const folder = new drive_default({
661
+ owner,
662
+ name: file.name,
663
+ parentId: parentId === "root" || !parentId ? null : parentId,
664
+ provider: {
665
+ type: "GOOGLE",
666
+ google: {
667
+ id: file.id,
668
+ webViewLink: file.webViewLink,
669
+ iconLink: file.iconLink
670
+ }
671
+ },
672
+ storageAccountId: foundAccountId,
673
+ information: { type: "FOLDER" },
674
+ status: "READY"
675
+ });
676
+ await folder.save();
677
+ return folder.toClient();
678
+ },
679
+ uploadFile: async (drive, filePath, accountId) => {
680
+ if (drive.information.type !== "FILE") throw new Error("Invalid drive type");
681
+ const { client } = await createAuthClient(drive.owner, accountId || drive.storageAccountId?.toString());
682
+ const googleDrive = google.drive({ version: "v3", auth: client });
683
+ let googleParentId = "root";
684
+ if (drive.parentId) {
685
+ const parent = await drive_default.findById(drive.parentId);
686
+ if (parent?.provider?.google?.id) googleParentId = parent.provider.google.id;
687
+ }
688
+ try {
689
+ const res = await googleDrive.files.create({
690
+ requestBody: {
691
+ name: drive.name,
692
+ parents: [googleParentId],
693
+ mimeType: drive.information.mime
694
+ },
695
+ media: {
696
+ mimeType: drive.information.mime,
697
+ body: fs3.createReadStream(filePath)
698
+ },
699
+ fields: "id, name, mimeType, webViewLink, iconLink, thumbnailLink, size"
700
+ });
701
+ const gFile = res.data;
702
+ if (!gFile.id) throw new Error("Upload to Google Drive failed");
703
+ drive.status = "READY";
704
+ drive.provider = {
705
+ type: "GOOGLE",
706
+ google: {
707
+ id: gFile.id,
708
+ webViewLink: gFile.webViewLink || void 0,
709
+ iconLink: gFile.iconLink || void 0,
710
+ thumbnailLink: gFile.thumbnailLink || void 0
711
+ }
712
+ };
713
+ } catch (error) {
714
+ drive.status = "FAILED";
715
+ console.error("Google Upload Error:", error);
716
+ throw error;
717
+ }
718
+ await drive.save();
719
+ return drive.toClient();
720
+ },
721
+ delete: async (ids, owner, accountId) => {
722
+ const { client } = await createAuthClient(owner, accountId);
723
+ const drive = google.drive({ version: "v3", auth: client });
724
+ const items = await drive_default.find({ _id: { $in: ids }, owner });
725
+ for (const item of items) {
726
+ if (item.provider?.google?.id) {
727
+ try {
728
+ await drive.files.delete({ fileId: item.provider.google.id });
729
+ } catch (e) {
730
+ console.error("Failed to delete Google file", e);
731
+ }
732
+ }
733
+ }
734
+ await drive_default.deleteMany({ _id: { $in: ids } });
735
+ },
736
+ trash: async (ids, owner, accountId) => {
737
+ const { client } = await createAuthClient(owner, accountId);
738
+ const drive = google.drive({ version: "v3", auth: client });
739
+ const items = await drive_default.find({ _id: { $in: ids }, owner });
740
+ for (const item of items) {
741
+ if (item.provider?.google?.id) {
742
+ try {
743
+ await drive.files.update({
744
+ fileId: item.provider.google.id,
745
+ requestBody: { trashed: true }
746
+ });
747
+ } catch (e) {
748
+ console.error("Failed to trash Google file", e);
749
+ }
750
+ }
751
+ }
752
+ },
753
+ untrash: async (ids, owner, accountId) => {
754
+ const { client } = await createAuthClient(owner, accountId);
755
+ const drive = google.drive({ version: "v3", auth: client });
756
+ const items = await drive_default.find({ _id: { $in: ids }, owner });
757
+ for (const item of items) {
758
+ if (item.provider?.google?.id) {
759
+ try {
760
+ await drive.files.update({
761
+ fileId: item.provider.google.id,
762
+ requestBody: { trashed: false }
763
+ });
764
+ } catch (e) {
765
+ console.error("Failed to restore Google file", e);
766
+ }
767
+ }
768
+ }
769
+ },
770
+ rename: async (id, newName, owner, accountId) => {
771
+ const { client } = await createAuthClient(owner, accountId);
772
+ const drive = google.drive({ version: "v3", auth: client });
773
+ const item = await drive_default.findOne({ _id: id, owner });
774
+ if (!item || !item.provider?.google?.id) throw new Error("Item not found");
775
+ await drive.files.update({
776
+ fileId: item.provider.google.id,
777
+ requestBody: { name: newName }
778
+ });
779
+ item.name = newName;
780
+ await item.save();
781
+ return item.toClient();
782
+ },
783
+ move: async (id, newParentId, owner, accountId) => {
784
+ const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
785
+ const drive = google.drive({ version: "v3", auth: client });
786
+ const item = await drive_default.findOne({ _id: id, owner });
787
+ if (!item || !item.provider?.google?.id) throw new Error("Item not found or not synced");
788
+ let previousGoogleParentId = void 0;
789
+ if (item.parentId) {
790
+ const oldParent = await drive_default.findOne({ _id: item.parentId, owner });
791
+ if (oldParent && oldParent.provider?.google?.id) {
792
+ previousGoogleParentId = oldParent.provider.google.id;
793
+ }
794
+ } else {
795
+ try {
796
+ const gFile = await drive.files.get({ fileId: item.provider.google.id, fields: "parents" });
797
+ if (gFile.data.parents && gFile.data.parents.length > 0) {
798
+ previousGoogleParentId = gFile.data.parents.join(",");
799
+ }
800
+ } catch (e) {
801
+ console.warn("Could not fetch parents for move", e);
802
+ }
803
+ }
804
+ let newGoogleParentId = "root";
805
+ if (newParentId && newParentId !== "root") {
806
+ const newParent = await drive_default.findOne({ _id: newParentId, owner });
807
+ if (!newParent || !newParent.provider?.google?.id) throw new Error("Target folder not found in Google Drive");
808
+ newGoogleParentId = newParent.provider.google.id;
809
+ }
810
+ await drive.files.update({
811
+ fileId: item.provider.google.id,
812
+ addParents: newGoogleParentId,
813
+ removeParents: previousGoogleParentId,
814
+ fields: "id, parents"
815
+ });
816
+ item.parentId = newParentId === "root" || !newParentId ? null : new mongoose5.Types.ObjectId(newParentId);
817
+ await item.save();
818
+ return item.toClient();
819
+ },
820
+ revokeToken: async (owner, accountId) => {
821
+ if (!accountId) return;
822
+ const { client } = await createAuthClient(owner, accountId);
823
+ const account = await account_default.findById(accountId);
824
+ if (account?.metadata?.provider === "GOOGLE" && account.metadata.google?.credentials) {
825
+ const creds = account.metadata.google.credentials;
826
+ if (typeof creds === "object" && "access_token" in creds) {
827
+ await client.revokeToken(creds.access_token);
828
+ }
829
+ }
830
+ }
831
+ };
832
+
833
+ // src/server/controllers/drive.ts
834
+ import fs4 from "fs";
835
+ import path3 from "path";
836
+ import crypto3 from "crypto";
837
+ var driveGetUrl = (fileId, options) => {
838
+ const config = getDriveConfig();
839
+ if (!config.security.signedUrls?.enabled) {
840
+ return `/api/drive?action=serve&id=${fileId}`;
841
+ }
842
+ const { secret, expiresIn } = config.security.signedUrls;
843
+ let expiryTimestamp;
844
+ if (options?.expiry instanceof Date) {
845
+ expiryTimestamp = Math.floor(options.expiry.getTime() / 1e3);
846
+ } else if (typeof options?.expiry === "number") {
847
+ expiryTimestamp = Math.floor(Date.now() / 1e3) + options.expiry;
848
+ } else {
849
+ expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
850
+ }
851
+ const signature = crypto3.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
852
+ const token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
853
+ return `/api/drive?action=serve&id=${fileId}&token=${token}`;
854
+ };
855
+ var driveReadFile = async (file) => {
856
+ let drive;
857
+ if (typeof file === "string") {
858
+ const doc = await drive_default.findById(file);
859
+ if (!doc) throw new Error(`File not found: ${file}`);
860
+ drive = doc;
861
+ } else if ("toClient" in file) {
862
+ drive = file;
863
+ } else {
864
+ throw new Error("Invalid file parameter provided");
865
+ }
866
+ if (drive.information.type !== "FILE") {
867
+ throw new Error("Cannot read a folder");
868
+ }
869
+ const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
870
+ const accountId = drive.storageAccountId?.toString();
871
+ return await provider.openStream(drive, accountId);
872
+ };
873
+ var driveFilePath = async (file) => {
874
+ let drive;
875
+ if (typeof file === "string") {
876
+ const doc = await drive_default.findById(file);
877
+ if (!doc) throw new Error(`File not found: ${file}`);
878
+ drive = doc;
879
+ } else if ("toClient" in file) {
880
+ drive = file;
881
+ } else {
882
+ throw new Error("Invalid file parameter provided");
883
+ }
884
+ if (drive.information.type !== "FILE") {
885
+ throw new Error("Cannot get path for a folder");
886
+ }
887
+ const config = getDriveConfig();
888
+ const STORAGE_PATH3 = config.storage.path;
889
+ const providerType = drive.provider?.type || "LOCAL";
890
+ if (providerType === "LOCAL") {
891
+ const filePath = path3.join(STORAGE_PATH3, drive.information.path);
892
+ if (!fs4.existsSync(filePath)) {
893
+ throw new Error(`Local file not found on disk: ${filePath}`);
894
+ }
895
+ return Object.freeze({
896
+ path: filePath,
897
+ name: drive.name,
898
+ mime: drive.information.mime,
899
+ size: drive.information.sizeInBytes,
900
+ provider: "LOCAL"
901
+ });
902
+ }
903
+ if (providerType === "GOOGLE") {
904
+ const libraryDir = path3.join(STORAGE_PATH3, "library", "google");
905
+ const fileName = `${drive._id}${path3.extname(drive.name)}`;
906
+ const cachedFilePath = path3.join(libraryDir, fileName);
907
+ if (fs4.existsSync(cachedFilePath)) {
908
+ const stats = fs4.statSync(cachedFilePath);
909
+ if (stats.size === drive.information.sizeInBytes) {
910
+ return Object.freeze({
911
+ path: cachedFilePath,
912
+ name: drive.name,
913
+ mime: drive.information.mime,
914
+ size: drive.information.sizeInBytes,
915
+ provider: "GOOGLE"
916
+ });
917
+ }
918
+ fs4.unlinkSync(cachedFilePath);
919
+ }
920
+ const accountId = drive.storageAccountId?.toString();
921
+ const { stream } = await GoogleDriveProvider.openStream(drive, accountId);
922
+ if (!fs4.existsSync(libraryDir)) {
923
+ fs4.mkdirSync(libraryDir, { recursive: true });
924
+ }
925
+ const tempPath = `${cachedFilePath}.tmp`;
926
+ const writeStream = fs4.createWriteStream(tempPath);
927
+ await new Promise((resolve, reject) => {
928
+ stream.pipe(writeStream);
929
+ writeStream.on("finish", resolve);
930
+ writeStream.on("error", reject);
931
+ stream.on("error", reject);
932
+ });
933
+ fs4.renameSync(tempPath, cachedFilePath);
934
+ return Object.freeze({
935
+ path: cachedFilePath,
936
+ name: drive.name,
937
+ mime: drive.information.mime,
938
+ size: drive.information.sizeInBytes,
939
+ provider: "GOOGLE"
940
+ });
941
+ }
942
+ throw new Error(`Unsupported provider: ${providerType}`);
943
+ };
944
+
945
+ // src/server/index.ts
946
+ var getProvider = async (req, owner) => {
947
+ const accountId = req.headers["x-drive-account"];
948
+ if (!accountId || accountId === "LOCAL") {
949
+ return { provider: LocalStorageProvider };
950
+ }
951
+ const account = await account_default.findOne({ _id: accountId, owner });
952
+ if (!account) {
953
+ throw new Error("Invalid Storage Account");
954
+ }
955
+ if (account.metadata.provider === "GOOGLE") return { provider: GoogleDriveProvider, accountId: account._id.toString() };
956
+ return { provider: LocalStorageProvider };
957
+ };
958
+ var driveAPIHandler = async (req, res) => {
959
+ const action = req.query.action;
960
+ try {
961
+ getDriveConfig();
962
+ } catch (error) {
963
+ console.error("[next-file-manager] Configuration error:", error);
964
+ res.status(500).json({ status: 500, message: "Failed to initialize drive configuration" });
965
+ return;
966
+ }
967
+ if (!action) {
968
+ res.status(400).json({ status: 400, message: "Missing action query parameter" });
969
+ return;
970
+ }
971
+ try {
972
+ const config = getDriveConfig();
973
+ const information = await getDriveInformation(req);
974
+ const { key: owner } = information;
975
+ const STORAGE_PATH3 = config.storage.path;
976
+ if (["getAuthUrl", "callback", "listAccounts", "removeAccount"].includes(action)) {
977
+ switch (action) {
978
+ case "getAuthUrl": {
979
+ const { provider: provider2 } = req.query;
980
+ if (provider2 === "GOOGLE") {
981
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
982
+ if (!clientId || !clientSecret) return res.status(500).json({ status: 500, message: "Google not configured" });
983
+ const { google: google2 } = __require("googleapis");
984
+ const oAuth2Client = new google2.auth.OAuth2(clientId, clientSecret, redirectUri);
985
+ const state = Buffer.from(JSON.stringify({ owner })).toString("base64");
986
+ const url = oAuth2Client.generateAuthUrl({
987
+ access_type: "offline",
988
+ scope: ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email"],
989
+ state,
990
+ prompt: "consent"
991
+ // force refresh token
992
+ });
993
+ return res.status(200).json({ status: 200, message: "Auth URL generated", data: { url } });
994
+ }
995
+ return res.status(400).json({ status: 400, message: "Unknown provider" });
996
+ }
997
+ case "callback": {
998
+ const { code, state } = req.query;
999
+ if (!code) return res.status(400).json({ status: 400, message: "Missing code" });
1000
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
1001
+ const { google: google2 } = __require("googleapis");
1002
+ const oAuth2Client = new google2.auth.OAuth2(clientId, clientSecret, redirectUri);
1003
+ const { tokens } = await oAuth2Client.getToken(code);
1004
+ oAuth2Client.setCredentials(tokens);
1005
+ const oauth2 = google2.oauth2({ version: "v2", auth: oAuth2Client });
1006
+ const userInfo = await oauth2.userinfo.get();
1007
+ const existing = await account_default.findOne({ owner, "metadata.google.email": userInfo.data.email, "metadata.provider": "GOOGLE" });
1008
+ if (existing) {
1009
+ existing.metadata.google.credentials = tokens;
1010
+ existing.markModified("metadata");
1011
+ await existing.save();
1012
+ } else {
1013
+ await account_default.create({
1014
+ owner,
1015
+ name: userInfo.data.name || "Google Drive",
1016
+ metadata: {
1017
+ provider: "GOOGLE",
1018
+ google: {
1019
+ email: userInfo.data.email,
1020
+ credentials: tokens
1021
+ }
1022
+ }
1023
+ });
1024
+ }
1025
+ res.setHeader("Content-Type", "text/html");
1026
+ return res.send('<script>window.opener.postMessage("oauth-success", "*"); window.close();</script>');
1027
+ }
1028
+ case "listAccounts": {
1029
+ const accounts = await account_default.find({ owner });
1030
+ return res.status(200).json({
1031
+ status: 200,
1032
+ data: {
1033
+ accounts: accounts.map((a) => ({
1034
+ id: a._id.toString(),
1035
+ name: a.name,
1036
+ email: a.metadata.google?.email || "",
1037
+ provider: a.metadata.provider
1038
+ }))
1039
+ }
1040
+ });
1041
+ }
1042
+ case "removeAccount": {
1043
+ const { id } = req.query;
1044
+ const account = await account_default.findOne({ _id: id, owner });
1045
+ if (!account) return res.status(404).json({ status: 404, message: "Account not found" });
1046
+ if (account.metadata.provider === "GOOGLE") {
1047
+ try {
1048
+ await GoogleDriveProvider.revokeToken(owner, account._id.toString());
1049
+ } catch (e) {
1050
+ console.error("Failed to revoke Google token:", e);
1051
+ }
1052
+ }
1053
+ await account_default.deleteOne({ _id: id, owner });
1054
+ await drive_default.deleteMany({ owner, storageAccountId: id });
1055
+ return res.status(200).json({ status: 200, message: "Account removed" });
1056
+ }
1057
+ }
1058
+ }
1059
+ const { provider, accountId } = await getProvider(req, owner);
1060
+ switch (action) {
1061
+ // ** 1. LIST **
1062
+ case "list": {
1063
+ if (req.method !== "GET") return res.status(405).json({ status: 405, message: "Only GET allowed" });
1064
+ const listQuery = listQuerySchema.safeParse(req.query);
1065
+ if (!listQuery.success) return res.status(400).json({ status: 400, message: "Invalid parameters" });
1066
+ const { folderId, limit, afterId } = listQuery.data;
1067
+ try {
1068
+ await provider.sync(folderId || "root", owner, accountId);
1069
+ } catch (e) {
1070
+ console.error("Sync failed", e);
1071
+ }
1072
+ const query = {
1073
+ owner,
1074
+ "provider.type": provider.name,
1075
+ storageAccountId: accountId || null,
1076
+ parentId: folderId === "root" || !folderId ? null : folderId,
1077
+ trashedAt: null
1078
+ };
1079
+ if (afterId) query._id = { $lt: afterId };
1080
+ const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
1081
+ const plainItems = await Promise.all(items.map((item) => item.toClient()));
1082
+ res.status(200).json({ status: 200, message: "Items retrieved", data: { items: plainItems, hasMore: items.length === limit } });
1083
+ return;
1084
+ }
1085
+ // ** 2. SEARCH **
1086
+ case "search": {
1087
+ const searchData = searchQuerySchema.safeParse(req.query);
1088
+ if (!searchData.success) return res.status(400).json({ status: 400, message: "Invalid params" });
1089
+ const { q, folderId, limit, trashed } = searchData.data;
1090
+ if (!trashed) {
1091
+ try {
1092
+ await provider.search(q, owner, accountId);
1093
+ } catch (e) {
1094
+ console.error("Search sync failed", e);
1095
+ }
1096
+ }
1097
+ const query = {
1098
+ owner,
1099
+ "provider.type": provider.name,
1100
+ storageAccountId: accountId || null,
1101
+ trashedAt: trashed ? { $ne: null } : null,
1102
+ name: { $regex: q, $options: "i" }
1103
+ };
1104
+ if (folderId && folderId !== "root") query.parentId = folderId;
1105
+ const items = await drive_default.find(query, {}, { limit, sort: { createdAt: -1 } });
1106
+ const plainItems = await Promise.all(items.map((i) => i.toClient()));
1107
+ return res.status(200).json({ status: 200, message: "Results", data: { items: plainItems } });
1108
+ }
1109
+ // ** 3. UPLOAD **
1110
+ case "upload": {
1111
+ if (req.method !== "POST") return res.status(405).json({ status: 405, message: "Only POST allowed" });
1112
+ const form = formidable({
1113
+ multiples: false,
1114
+ maxFileSize: config.security.maxUploadSizeInBytes * 2,
1115
+ uploadDir: path4.join(STORAGE_PATH3, "temp"),
1116
+ keepExtensions: true
1117
+ });
1118
+ if (!fs5.existsSync(path4.join(STORAGE_PATH3, "temp"))) fs5.mkdirSync(path4.join(STORAGE_PATH3, "temp"), { recursive: true });
1119
+ const [fields, files] = await new Promise((resolve, reject) => {
1120
+ form.parse(req, (err, fields2, files2) => {
1121
+ if (err) reject(err);
1122
+ else resolve([fields2, files2]);
1123
+ });
1124
+ });
1125
+ const cleanupTempFiles = (files2) => {
1126
+ Object.values(files2).flat().forEach((file) => {
1127
+ if (file && fs5.existsSync(file.filepath)) fs5.rmSync(file.filepath, { force: true });
1128
+ });
1129
+ };
1130
+ const getString = (f) => Array.isArray(f) ? f[0] : f || "";
1131
+ const getInt = (f) => parseInt(getString(f) || "0", 10);
1132
+ const uploadData = uploadChunkSchema.safeParse({
1133
+ chunkIndex: getInt(fields.chunkIndex),
1134
+ totalChunks: getInt(fields.totalChunks),
1135
+ driveId: getString(fields.driveId) || void 0,
1136
+ fileName: getString(fields.fileName),
1137
+ fileSize: getInt(fields.fileSize),
1138
+ fileType: getString(fields.fileType),
1139
+ folderId: getString(fields.folderId) || void 0
1140
+ });
1141
+ if (!uploadData.success) {
1142
+ cleanupTempFiles(files);
1143
+ return res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
1144
+ }
1145
+ const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId } = uploadData.data;
1146
+ let currentUploadId = driveId;
1147
+ const tempBaseDir = path4.join(STORAGE_PATH3, "temp", "uploads");
1148
+ if (!currentUploadId) {
1149
+ if (chunkIndex !== 0) return res.status(400).json({ message: "Missing upload ID for non-zero chunk" });
1150
+ if (fileType && !validateMimeType(fileType, config.security.allowedMimeTypes)) {
1151
+ cleanupTempFiles(files);
1152
+ return res.status(400).json({ status: 400, message: `File type ${fileType} not allowed` });
1153
+ }
1154
+ const quota = await provider.getQuota(owner, accountId);
1155
+ if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
1156
+ cleanupTempFiles(files);
1157
+ return res.status(413).json({ status: 413, message: "Storage quota exceeded" });
1158
+ }
1159
+ currentUploadId = crypto.randomUUID();
1160
+ const uploadDir = path4.join(tempBaseDir, currentUploadId);
1161
+ fs5.mkdirSync(uploadDir, { recursive: true });
1162
+ const metadata = {
1163
+ owner,
1164
+ accountId,
1165
+ providerName: provider.name,
1166
+ name: fileName,
1167
+ parentId: folderId === "root" || !folderId ? null : folderId,
1168
+ fileSize: fileSizeInBytes,
1169
+ mimeType: fileType,
1170
+ totalChunks
1171
+ };
1172
+ fs5.writeFileSync(path4.join(uploadDir, "metadata.json"), JSON.stringify(metadata));
1173
+ }
1174
+ if (currentUploadId) {
1175
+ const uploadDir = path4.join(tempBaseDir, currentUploadId);
1176
+ if (!fs5.existsSync(uploadDir)) {
1177
+ cleanupTempFiles(files);
1178
+ return res.status(404).json({ status: 404, message: "Upload session not found or expired" });
1179
+ }
1180
+ try {
1181
+ const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
1182
+ if (!chunkFile) throw new Error("No chunk file received");
1183
+ const partPath = path4.join(uploadDir, `part_${chunkIndex}`);
1184
+ fs5.renameSync(chunkFile.filepath, partPath);
1185
+ const uploadedParts = fs5.readdirSync(uploadDir).filter((f) => f.startsWith("part_"));
1186
+ if (uploadedParts.length === totalChunks) {
1187
+ const metaPath = path4.join(uploadDir, "metadata.json");
1188
+ const meta = JSON.parse(fs5.readFileSync(metaPath, "utf-8"));
1189
+ const finalTempPath = path4.join(uploadDir, "final.bin");
1190
+ const writeStream = fs5.createWriteStream(finalTempPath);
1191
+ for (let i = 0; i < totalChunks; i++) {
1192
+ const pPath = path4.join(uploadDir, `part_${i}`);
1193
+ const data = fs5.readFileSync(pPath);
1194
+ writeStream.write(data);
1195
+ }
1196
+ writeStream.end();
1197
+ const drive = new drive_default({
1198
+ owner: meta.owner,
1199
+ storageAccountId: meta.accountId || null,
1200
+ provider: { type: meta.providerName },
1201
+ name: meta.name,
1202
+ parentId: meta.parentId,
1203
+ order: 0,
1204
+ information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
1205
+ // path set by provider
1206
+ status: "UPLOADING",
1207
+ currentChunk: totalChunks,
1208
+ totalChunks
1209
+ });
1210
+ if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
1211
+ drive.information.path = path4.join("drive", String(drive._id), "data.bin");
1212
+ }
1213
+ await drive.save();
1214
+ try {
1215
+ const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
1216
+ fs5.rmSync(uploadDir, { recursive: true, force: true });
1217
+ const newQuota = await provider.getQuota(meta.owner, meta.accountId);
1218
+ res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item }, statistic: { storage: newQuota } });
1219
+ } catch (err) {
1220
+ await drive_default.deleteOne({ _id: drive._id });
1221
+ throw err;
1222
+ }
1223
+ } else {
1224
+ const newQuota = await provider.getQuota(owner, accountId);
1225
+ if (chunkIndex === 0) {
1226
+ res.status(200).json({ status: 200, message: "Upload started", data: { type: "UPLOAD_STARTED", driveId: currentUploadId }, statistic: { storage: newQuota } });
1227
+ } else {
1228
+ res.status(200).json({ status: 200, message: "Chunk received", data: { type: "CHUNK_RECEIVED", driveId: currentUploadId, chunkIndex }, statistic: { storage: newQuota } });
1229
+ }
1230
+ }
1231
+ } catch (e) {
1232
+ cleanupTempFiles(files);
1233
+ throw e;
1234
+ }
1235
+ return;
1236
+ }
1237
+ cleanupTempFiles(files);
1238
+ return res.status(400).json({ status: 400, message: "Invalid upload request" });
1239
+ }
1240
+ // ** 4. CREATE FOLDER **
1241
+ case "createFolder": {
1242
+ const folderData = createFolderBodySchema.safeParse(req.body);
1243
+ if (!folderData.success) return res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
1244
+ const { name, parentId } = folderData.data;
1245
+ const item = await provider.createFolder(name, parentId ?? null, owner, accountId);
1246
+ return res.status(201).json({ status: 201, message: "Folder created", data: { item } });
1247
+ }
1248
+ // ** 5. DELETE **
1249
+ case "delete": {
1250
+ const deleteData = deleteQuerySchema.safeParse(req.query);
1251
+ if (!deleteData.success) return res.status(400).json({ status: 400, message: "Invalid ID" });
1252
+ const { id } = deleteData.data;
1253
+ const drive = await drive_default.findById(id);
1254
+ if (!drive) return res.status(404).json({ status: 404, message: "Not found" });
1255
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1256
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
1257
+ try {
1258
+ await itemProvider.trash([id], owner, itemAccountId);
1259
+ } catch (e) {
1260
+ console.error("Provider trash failed:", e);
1261
+ }
1262
+ drive.trashedAt = /* @__PURE__ */ new Date();
1263
+ await drive.save();
1264
+ return res.status(200).json({ status: 200, message: "Moved to trash", data: null });
1265
+ }
1266
+ // ** 6. HARD DELETE **
1267
+ case "deletePermanent": {
1268
+ const deleteData = deleteQuerySchema.safeParse(req.query);
1269
+ if (!deleteData.success) return res.status(400).json({ status: 400, message: "Invalid ID" });
1270
+ const { id } = deleteData.data;
1271
+ await provider.delete([id], owner, accountId);
1272
+ const quota = await provider.getQuota(owner, accountId);
1273
+ return res.status(200).json({ status: 200, message: "Deleted", statistic: { storage: quota } });
1274
+ }
1275
+ // ** 7. QUOTA **
1276
+ case "quota": {
1277
+ const quota = await provider.getQuota(owner, accountId);
1278
+ return res.status(200).json({
1279
+ status: 200,
1280
+ message: "Quota retrieved",
1281
+ data: { usedInBytes: quota.usedInBytes, totalInBytes: quota.quotaInBytes, availableInBytes: Math.max(0, quota.quotaInBytes - quota.usedInBytes), percentage: quota.quotaInBytes > 0 ? Math.round(quota.usedInBytes / quota.quotaInBytes * 100) : 0 },
1282
+ statistic: { storage: quota }
1283
+ });
1284
+ }
1285
+ // ** 7B. TRASH **
1286
+ case "trash": {
1287
+ try {
1288
+ const { provider: trashProvider, accountId: trashAccountId } = await getProvider(req, owner);
1289
+ await trashProvider.syncTrash(owner, trashAccountId);
1290
+ } catch (e) {
1291
+ console.error("Trash sync failed", e);
1292
+ }
1293
+ const query = {
1294
+ owner,
1295
+ "provider.type": provider.name,
1296
+ storageAccountId: accountId || null,
1297
+ trashedAt: { $ne: null }
1298
+ };
1299
+ const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
1300
+ const plainItems = await Promise.all(items.map((item) => item.toClient()));
1301
+ return res.status(200).json({
1302
+ status: 200,
1303
+ message: "Trash items",
1304
+ data: { items: plainItems, hasMore: false }
1305
+ });
1306
+ }
1307
+ // ** 7C. RESTORE **
1308
+ case "restore": {
1309
+ const restoreData = deleteQuerySchema.safeParse(req.query);
1310
+ if (!restoreData.success) return res.status(400).json({ status: 400, message: "Invalid ID" });
1311
+ const { id } = restoreData.data;
1312
+ const drive = await drive_default.findById(id);
1313
+ if (!drive) return res.status(404).json({ status: 404, message: "Not found" });
1314
+ let targetParentId = drive.parentId;
1315
+ if (targetParentId) {
1316
+ const parent = await drive_default.findById(targetParentId);
1317
+ if (parent?.trashedAt) {
1318
+ targetParentId = null;
1319
+ }
1320
+ }
1321
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1322
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
1323
+ try {
1324
+ await itemProvider.untrash([id], owner, itemAccountId);
1325
+ if (targetParentId !== drive.parentId) {
1326
+ await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
1327
+ }
1328
+ } catch (e) {
1329
+ console.error("Provider restore failed:", e);
1330
+ }
1331
+ drive.trashedAt = null;
1332
+ drive.parentId = targetParentId;
1333
+ await drive.save();
1334
+ return res.status(200).json({
1335
+ status: 200,
1336
+ message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
1337
+ data: null
1338
+ });
1339
+ }
1340
+ // ** 7D. MOVE **
1341
+ case "move": {
1342
+ const moveData = moveBodySchema.safeParse(req.body);
1343
+ if (!moveData.success) return res.status(400).json({ status: 400, message: "Invalid data" });
1344
+ const { ids, targetFolderId } = moveData.data;
1345
+ const items = [];
1346
+ const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
1347
+ for (const id of ids) {
1348
+ try {
1349
+ const item = await provider.move(id, effectiveTargetId, owner, accountId);
1350
+ items.push(item);
1351
+ } catch (e) {
1352
+ console.error(`Failed to move item ${id}`, e);
1353
+ }
1354
+ }
1355
+ return res.status(200).json({ status: 200, message: "Moved", data: { items } });
1356
+ }
1357
+ // ** 8. RENAME **
1358
+ case "rename": {
1359
+ const renameData = renameBodySchema.safeParse({ id: req.query.id, ...req.body });
1360
+ if (!renameData.success) return res.status(400).json({ status: 400, message: "Invalid data" });
1361
+ const { id, newName } = renameData.data;
1362
+ const item = await provider.rename(id, newName, owner, accountId);
1363
+ return res.status(200).json({ status: 200, message: "Renamed", data: { item } });
1364
+ }
1365
+ // ** 9. THUMBNAIL **
1366
+ case "thumbnail": {
1367
+ const thumbQuery = thumbnailQuerySchema.safeParse(req.query);
1368
+ if (!thumbQuery.success) return res.status(400).json({ status: 400, message: "Invalid params" });
1369
+ const { id } = thumbQuery.data;
1370
+ const drive = await drive_default.findById(id);
1371
+ if (!drive) return res.status(404).json({ status: 404, message: "Not found" });
1372
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1373
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
1374
+ const stream = await itemProvider.getThumbnail(drive, itemAccountId);
1375
+ res.setHeader("Content-Type", "image/webp");
1376
+ stream.pipe(res);
1377
+ return;
1378
+ }
1379
+ // ** 10. SERVE / DOWNLOAD **
1380
+ case "serve": {
1381
+ const serveQuery = serveQuerySchema.safeParse(req.query);
1382
+ if (!serveQuery.success) return res.status(400).json({ status: 400, message: "Invalid params" });
1383
+ const { id } = serveQuery.data;
1384
+ const drive = await drive_default.findById(id);
1385
+ if (!drive) return res.status(404).json({ status: 404, message: "Not found" });
1386
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1387
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
1388
+ const { stream, mime, size } = await itemProvider.openStream(drive, itemAccountId);
1389
+ const safeFilename = sanitizeContentDispositionFilename(drive.name);
1390
+ res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
1391
+ res.setHeader("Content-Type", mime);
1392
+ if (size) res.setHeader("Content-Length", size);
1393
+ stream.pipe(res);
1394
+ return;
1395
+ }
1396
+ default:
1397
+ res.status(400).json({ status: 400, message: `Unknown action: ${action}` });
1398
+ }
1399
+ } catch (error) {
1400
+ console.error(`[next-file-manager] Error handling action ${action}:`, error);
1401
+ res.status(500).json({ status: 500, message: error instanceof Error ? error.message : "Unknown error" });
1402
+ }
1403
+ };
1404
+ export {
1405
+ driveAPIHandler,
1406
+ driveConfiguration,
1407
+ driveFilePath,
1408
+ driveGetUrl,
1409
+ driveReadFile,
1410
+ getDriveConfig,
1411
+ getDriveInformation
1412
+ };
1413
+ //# sourceMappingURL=index.js.map