@muhgholy/next-drive 1.4.0 → 1.4.2

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