@muhgholy/next-drive 4.23.7 → 4.23.10

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.
Files changed (95) hide show
  1. package/README.md +152 -1
  2. package/dist/{chunk-LAKT7IJJ.cjs → chunk-V75PCJHT.cjs} +962 -773
  3. package/dist/chunk-V75PCJHT.cjs.map +1 -0
  4. package/dist/{chunk-MVYNW56R.js → chunk-XUPDNN2U.js} +957 -770
  5. package/dist/chunk-XUPDNN2U.js.map +1 -0
  6. package/dist/client/components/drive/{RenameAccountDialog.d.ts → account/rename.d.ts} +2 -2
  7. package/dist/client/components/drive/account/rename.d.ts.map +1 -0
  8. package/dist/client/components/drive/{dnd-provider.d.ts → dnd/context.d.ts} +1 -1
  9. package/dist/client/components/drive/dnd/context.d.ts.map +1 -0
  10. package/dist/client/components/drive/{CreateFolderDialog.d.ts → folder/create.d.ts} +2 -2
  11. package/dist/client/components/drive/folder/create.d.ts.map +1 -0
  12. package/dist/client/components/drive/{RenameDialog.d.ts → item/rename.d.ts} +3 -3
  13. package/dist/client/components/drive/item/rename.d.ts.map +1 -0
  14. package/dist/client/components/{dialog.d.ts → shared/confirm.d.ts} +2 -2
  15. package/dist/client/components/shared/confirm.d.ts.map +1 -0
  16. package/dist/client/components/ui/{alert-dialog.d.ts → alert-modal.d.ts} +1 -1
  17. package/dist/client/components/ui/alert-modal.d.ts.map +1 -0
  18. package/dist/client/components/ui/{dialog-fullscreen.d.ts → fullscreen.d.ts} +1 -1
  19. package/dist/client/components/ui/fullscreen.d.ts.map +1 -0
  20. package/dist/client/components/ui/{dialog.d.ts → modal.d.ts} +1 -1
  21. package/dist/client/components/ui/modal.d.ts.map +1 -0
  22. package/dist/client/context.d.ts.map +1 -1
  23. package/dist/client/file-chooser.d.ts +1 -0
  24. package/dist/client/file-chooser.d.ts.map +1 -1
  25. package/dist/client/hooks/{useUpload.d.ts → use-upload.d.ts} +2 -2
  26. package/dist/client/hooks/use-upload.d.ts.map +1 -0
  27. package/dist/client/index.cjs +315 -206
  28. package/dist/client/index.cjs.map +1 -1
  29. package/dist/client/index.d.ts +12 -11
  30. package/dist/client/index.d.ts.map +1 -1
  31. package/dist/client/index.js +314 -205
  32. package/dist/client/index.js.map +1 -1
  33. package/dist/server/actions/auth.d.ts +4 -0
  34. package/dist/server/actions/auth.d.ts.map +1 -0
  35. package/dist/server/actions/cors.d.ts +4 -0
  36. package/dist/server/actions/cors.d.ts.map +1 -0
  37. package/dist/server/actions/drive.d.ts +18 -0
  38. package/dist/server/actions/drive.d.ts.map +1 -0
  39. package/dist/server/actions/public.d.ts +4 -0
  40. package/dist/server/actions/public.d.ts.map +1 -0
  41. package/dist/server/actions/shared.d.ts +14 -0
  42. package/dist/server/actions/shared.d.ts.map +1 -0
  43. package/dist/server/config.d.ts.map +1 -1
  44. package/dist/server/controllers/drive.d.ts +26 -0
  45. package/dist/server/controllers/drive.d.ts.map +1 -1
  46. package/dist/server/database/mongoose/schema/drive.d.ts +1 -0
  47. package/dist/server/database/mongoose/schema/drive.d.ts.map +1 -1
  48. package/dist/server/express.cjs +11 -11
  49. package/dist/server/express.js +2 -2
  50. package/dist/server/hono.cjs +11 -11
  51. package/dist/server/hono.js +2 -2
  52. package/dist/server/index.cjs +24 -16
  53. package/dist/server/index.d.ts +2 -2
  54. package/dist/server/index.d.ts.map +1 -1
  55. package/dist/server/index.js +1 -1
  56. package/dist/server/security/{cryptoUtils.d.ts → crypto-utils.d.ts} +1 -1
  57. package/dist/server/security/crypto-utils.d.ts.map +1 -0
  58. package/dist/server/security/{mimeFilter.d.ts → mime-filter.d.ts} +1 -1
  59. package/dist/server/security/mime-filter.d.ts.map +1 -0
  60. package/dist/server/storage-adapters/google.d.ts.map +1 -0
  61. package/dist/server/storage-adapters/local.d.ts.map +1 -0
  62. package/dist/server/utils/{folderValidation.d.ts → folder-validation.d.ts} +1 -1
  63. package/dist/server/utils/folder-validation.d.ts.map +1 -0
  64. package/dist/server/utils/{imageConvert.d.ts → image-convert.d.ts} +1 -1
  65. package/dist/server/utils/image-convert.d.ts.map +1 -0
  66. package/dist/server/zod/schemas.d.ts +5 -0
  67. package/dist/server/zod/schemas.d.ts.map +1 -1
  68. package/dist/types/lib/database/drive.d.ts +1 -0
  69. package/dist/types/lib/database/drive.d.ts.map +1 -1
  70. package/dist/types/lib/database/index.d.ts +2 -2
  71. package/dist/types/lib/database/index.d.ts.map +1 -1
  72. package/dist/types/server/config.d.ts +17 -0
  73. package/dist/types/server/config.d.ts.map +1 -1
  74. package/dist/types/server/index.d.ts +5 -5
  75. package/dist/types/server/index.d.ts.map +1 -1
  76. package/package.json +2 -1
  77. package/dist/chunk-LAKT7IJJ.cjs.map +0 -1
  78. package/dist/chunk-MVYNW56R.js.map +0 -1
  79. package/dist/client/components/dialog.d.ts.map +0 -1
  80. package/dist/client/components/drive/CreateFolderDialog.d.ts.map +0 -1
  81. package/dist/client/components/drive/RenameAccountDialog.d.ts.map +0 -1
  82. package/dist/client/components/drive/RenameDialog.d.ts.map +0 -1
  83. package/dist/client/components/drive/dnd-provider.d.ts.map +0 -1
  84. package/dist/client/components/ui/alert-dialog.d.ts.map +0 -1
  85. package/dist/client/components/ui/dialog-fullscreen.d.ts.map +0 -1
  86. package/dist/client/components/ui/dialog.d.ts.map +0 -1
  87. package/dist/client/hooks/useUpload.d.ts.map +0 -1
  88. package/dist/server/providers/google.d.ts.map +0 -1
  89. package/dist/server/providers/local.d.ts.map +0 -1
  90. package/dist/server/security/cryptoUtils.d.ts.map +0 -1
  91. package/dist/server/security/mimeFilter.d.ts.map +0 -1
  92. package/dist/server/utils/folderValidation.d.ts.map +0 -1
  93. package/dist/server/utils/imageConvert.d.ts.map +0 -1
  94. /package/dist/server/{providers → storage-adapters}/google.d.ts +0 -0
  95. /package/dist/server/{providers → storage-adapters}/local.d.ts +0 -0
@@ -1,28 +1,28 @@
1
1
  'use strict';
2
2
 
3
- var formidable = require('formidable');
3
+ var mongoose = require('mongoose');
4
4
  var path = require('path');
5
- var fs = require('fs');
6
5
  var os2 = require('os');
7
- var crypto2 = require('crypto');
8
- var mongoose = require('mongoose');
6
+ var fs = require('fs');
7
+ var crypto3 = require('crypto');
9
8
  var sharp2 = require('sharp');
10
9
  var ffmpeg = require('fluent-ffmpeg');
11
10
  var googleapis = require('googleapis');
11
+ var formidable = require('formidable');
12
12
  var zod = require('zod');
13
13
 
14
14
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
15
 
16
- var formidable__default = /*#__PURE__*/_interopDefault(formidable);
16
+ var mongoose__default = /*#__PURE__*/_interopDefault(mongoose);
17
17
  var path__default = /*#__PURE__*/_interopDefault(path);
18
- var fs__default = /*#__PURE__*/_interopDefault(fs);
19
18
  var os2__default = /*#__PURE__*/_interopDefault(os2);
20
- var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
21
- var mongoose__default = /*#__PURE__*/_interopDefault(mongoose);
19
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
20
+ var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
22
21
  var sharp2__default = /*#__PURE__*/_interopDefault(sharp2);
23
22
  var ffmpeg__default = /*#__PURE__*/_interopDefault(ffmpeg);
23
+ var formidable__default = /*#__PURE__*/_interopDefault(formidable);
24
24
 
25
- // src/server/index.ts
25
+ // src/server/config.ts
26
26
  var informationSchema = new mongoose.Schema({
27
27
  type: { type: String, enum: ["FILE", "FOLDER"], required: true },
28
28
  sizeInBytes: { type: Number, default: 0 },
@@ -49,6 +49,7 @@ var DriveSchema = new mongoose.Schema(
49
49
  information: { type: informationSchema, required: true },
50
50
  status: { type: String, enum: ["READY", "PROCESSING", "UPLOADING", "FAILED"], default: "PROCESSING" },
51
51
  trashedAt: { type: Date, default: null },
52
+ expiresAt: { type: Date, default: null },
52
53
  meta: { type: mongoose.Schema.Types.Mixed, default: {} },
53
54
  createdAt: { type: Date, default: Date.now }
54
55
  },
@@ -61,6 +62,7 @@ DriveSchema.index({ owner: 1, trashedAt: 1 });
61
62
  DriveSchema.index({ owner: 1, "information.hash": 1 });
62
63
  DriveSchema.index({ owner: 1, name: "text" });
63
64
  DriveSchema.index({ owner: 1, "provider.type": 1 });
65
+ DriveSchema.index({ expiresAt: 1 });
64
66
  DriveSchema.method("toClient", async function() {
65
67
  const data = this.toJSON();
66
68
  return {
@@ -73,6 +75,7 @@ DriveSchema.method("toClient", async function() {
73
75
  information: data.information,
74
76
  status: data.status,
75
77
  trashedAt: data.trashedAt,
78
+ expiresAt: data.expiresAt,
76
79
  meta: data.meta,
77
80
  createdAt: data.createdAt
78
81
  };
@@ -259,7 +262,8 @@ var getGlobal = () => {
259
262
  globalThis[GLOBAL_KEY] = {
260
263
  config: null,
261
264
  migrationPromise: null,
262
- initialized: false
265
+ initialized: false,
266
+ abuse: { ipHits: /* @__PURE__ */ new Map(), concurrent: 0 }
263
267
  };
264
268
  }
265
269
  return globalThis[GLOBAL_KEY];
@@ -288,7 +292,8 @@ var driveConfiguration = async (config) => {
288
292
  // 10GB default for ROOT
289
293
  allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
290
294
  signedUrls: config.security?.signedUrls,
291
- trash: config.security?.trash
295
+ trash: config.security?.trash,
296
+ unauthenticated: config.security?.unauthenticated
292
297
  }
293
298
  };
294
299
  } else {
@@ -306,7 +311,8 @@ var driveConfiguration = async (config) => {
306
311
  maxUploadSizeInBytes: config.security?.maxUploadSizeInBytes ?? 10 * 1024 * 1024,
307
312
  allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
308
313
  signedUrls: config.security?.signedUrls,
309
- trash: config.security?.trash
314
+ trash: config.security?.trash,
315
+ unauthenticated: config.security?.unauthenticated
310
316
  },
311
317
  information: config.information
312
318
  };
@@ -337,6 +343,53 @@ var getDriveInformation = async (input) => {
337
343
  }
338
344
  return config.information(input);
339
345
  };
346
+
347
+ // src/server/actions/cors.ts
348
+ var applyCorsHeaders = (req, res, config) => {
349
+ const cors = config.cors;
350
+ if (!cors?.enabled) return false;
351
+ const origin = req.headers.origin;
352
+ const allowedOrigins = cors.origins ?? "*";
353
+ const methods = cors.methods ?? ["GET", "POST", "PUT", "DELETE", "OPTIONS"];
354
+ const allowedHeaders = cors.allowedHeaders ?? ["Content-Type", "Authorization", "X-Drive-Account"];
355
+ const exposedHeaders = cors.exposedHeaders ?? ["Content-Length", "Content-Type", "Content-Disposition"];
356
+ const credentials = cors.credentials ?? false;
357
+ const maxAge = cors.maxAge ?? 86400;
358
+ let allowOrigin = null;
359
+ if (origin) {
360
+ if (allowedOrigins === "*") {
361
+ allowOrigin = origin;
362
+ } else if (Array.isArray(allowedOrigins)) {
363
+ if (allowedOrigins.includes(origin)) {
364
+ allowOrigin = origin;
365
+ }
366
+ } else if (allowedOrigins === origin) {
367
+ allowOrigin = origin;
368
+ }
369
+ } else if (allowedOrigins === "*") {
370
+ allowOrigin = "*";
371
+ }
372
+ if (!allowOrigin) {
373
+ if (req.method === "OPTIONS") {
374
+ res.status(403).end();
375
+ return true;
376
+ }
377
+ return false;
378
+ }
379
+ res.setHeader("Access-Control-Allow-Origin", allowOrigin);
380
+ res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
381
+ res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
382
+ res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(", "));
383
+ res.setHeader("Access-Control-Max-Age", maxAge.toString());
384
+ if (credentials) {
385
+ res.setHeader("Access-Control-Allow-Credentials", "true");
386
+ }
387
+ if (req.method === "OPTIONS") {
388
+ res.status(204).end();
389
+ return true;
390
+ }
391
+ return false;
392
+ };
340
393
  var validateMimeType = (mime, allowedTypes) => {
341
394
  if (allowedTypes.includes("*/*")) return true;
342
395
  return allowedTypes.some((pattern) => {
@@ -349,7 +402,7 @@ var validateMimeType = (mime, allowedTypes) => {
349
402
  });
350
403
  };
351
404
  var computeFileHash = (filePath) => new Promise((resolve, reject) => {
352
- const hash = crypto2__default.default.createHash("sha256");
405
+ const hash = crypto3__default.default.createHash("sha256");
353
406
  const stream = fs__default.default.createReadStream(filePath);
354
407
  stream.on("data", (data) => hash.update(data));
355
408
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -500,6 +553,12 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size, fit, posi
500
553
  ...resolvedPosition && { position: resolvedPosition }
501
554
  };
502
555
  };
556
+
557
+ // src/server/security/crypto-utils.ts
558
+ function sanitizeContentDispositionFilename(filename) {
559
+ const basename = filename.replace(/^.*[\\\/]/, "");
560
+ return basename.replace(/["\r\n]/g, "").replace(/[^\x20-\x7E]/g, "").slice(0, 255);
561
+ }
503
562
  var generatePlaceholderThumbnail = async (outputPath, mimeType) => {
504
563
  const typeParts = mimeType.split("/");
505
564
  const subtype = typeParts[1] || "file";
@@ -726,7 +785,7 @@ StorageAccountSchema.method("toClient", async function() {
726
785
  var StorageAccount = mongoose__default.default.models.StorageAccount || mongoose__default.default.model("StorageAccount", StorageAccountSchema);
727
786
  var account_default = StorageAccount;
728
787
 
729
- // src/server/providers/google.ts
788
+ // src/server/storage-adapters/google.ts
730
789
  var createAuthClient = async (owner, accountId) => {
731
790
  const query = { owner, "metadata.provider": "GOOGLE" };
732
791
  if (accountId) query._id = accountId;
@@ -1194,7 +1253,368 @@ var GoogleDriveProvider = {
1194
1253
  }
1195
1254
  };
1196
1255
 
1197
- // src/server/controllers/drive.ts
1256
+ // src/server/actions/public.ts
1257
+ var handlePublicAction = async (req, res, action, config) => {
1258
+ if (action !== "serve" && action !== "thumbnail") {
1259
+ return false;
1260
+ }
1261
+ try {
1262
+ const { id, token } = req.query;
1263
+ if (!id || typeof id !== "string") {
1264
+ res.status(400).json({ status: 400, message: "Could not open file: missing or invalid file ID" });
1265
+ return true;
1266
+ }
1267
+ const drive = await drive_default.findById(id);
1268
+ if (!drive) {
1269
+ res.status(404).json({ status: 404, message: "File not found or no longer available" });
1270
+ return true;
1271
+ }
1272
+ if (config.security?.signedUrls?.enabled) {
1273
+ if (!token || typeof token !== "string") {
1274
+ res.status(401).json({ status: 401, message: "Access denied: this link is missing its access token" });
1275
+ return true;
1276
+ }
1277
+ try {
1278
+ const decoded = Buffer.from(token, "base64url").toString();
1279
+ const [expiryStr, signature] = decoded.split(":");
1280
+ const expiry = parseInt(expiryStr, 10);
1281
+ if (Date.now() / 1e3 > expiry) {
1282
+ res.status(401).json({ status: 401, message: "Access denied: this link has expired" });
1283
+ return true;
1284
+ }
1285
+ const { secret } = config.security.signedUrls;
1286
+ const expectedSignature = crypto3__default.default.createHmac("sha256", secret).update(`${id}:${expiry}`).digest("hex");
1287
+ if (signature !== expectedSignature) {
1288
+ res.status(401).json({ status: 401, message: "Access denied: this link's access token is invalid" });
1289
+ return true;
1290
+ }
1291
+ } catch {
1292
+ res.status(401).json({ status: 401, message: "Access denied: this link's access token is malformed" });
1293
+ return true;
1294
+ }
1295
+ }
1296
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1297
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
1298
+ if (action === "thumbnail") {
1299
+ const stream2 = await itemProvider.getThumbnail(drive, itemAccountId);
1300
+ res.setHeader("Content-Type", "image/webp");
1301
+ if (config.cors?.enabled) {
1302
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1303
+ }
1304
+ stream2.pipe(res);
1305
+ return true;
1306
+ }
1307
+ const { stream, mime, size: fileSize } = await itemProvider.openStream(drive, itemAccountId);
1308
+ const safeFilename = sanitizeContentDispositionFilename(drive.name);
1309
+ const format = req.query.format;
1310
+ const quality = req.query.quality;
1311
+ const display = req.query.display;
1312
+ const sizePreset = req.query.size;
1313
+ const fit = req.query.fit;
1314
+ const position = req.query.position;
1315
+ const isImage = mime.startsWith("image/");
1316
+ const shouldTransform = isImage && (format || quality || display || sizePreset || fit);
1317
+ res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
1318
+ if (config.cors?.enabled) {
1319
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1320
+ }
1321
+ if (shouldTransform) {
1322
+ try {
1323
+ const settings = getImageSettings(fileSize, quality, display, sizePreset, fit, position);
1324
+ let targetFormat = format || mime.split("/")[1];
1325
+ if (targetFormat === "jpg") targetFormat = "jpeg";
1326
+ if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
1327
+ targetFormat = format || "webp";
1328
+ }
1329
+ const cacheDir = path__default.default.join(config.storage.path, "file", drive._id.toString(), "cache");
1330
+ const cacheKey = [
1331
+ "opt",
1332
+ `q${settings.quality}`,
1333
+ `e${settings.effort}`,
1334
+ settings.width ? `${settings.width}x${settings.height}` : "orig",
1335
+ settings.fit || "none",
1336
+ settings.position || "c",
1337
+ targetFormat
1338
+ ].join("_");
1339
+ const cachePath = path__default.default.join(cacheDir, `${cacheKey}.bin`);
1340
+ if (fs__default.default.existsSync(cachePath)) {
1341
+ const cacheStat = fs__default.default.statSync(cachePath);
1342
+ res.setHeader("Content-Type", `image/${targetFormat}`);
1343
+ res.setHeader("Content-Length", cacheStat.size);
1344
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1345
+ if (config.cors?.enabled) {
1346
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1347
+ }
1348
+ if ("destroy" in stream) {
1349
+ stream.destroy();
1350
+ }
1351
+ fs__default.default.createReadStream(cachePath).pipe(res);
1352
+ return true;
1353
+ }
1354
+ if (!fs__default.default.existsSync(cacheDir)) fs__default.default.mkdirSync(cacheDir, { recursive: true });
1355
+ let pipeline = sharp2__default.default();
1356
+ if (settings.width && settings.height) {
1357
+ pipeline = pipeline.resize(settings.width, settings.height, {
1358
+ fit: settings.fit || "inside",
1359
+ position: settings.position || "center",
1360
+ withoutEnlargement: true,
1361
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
1362
+ });
1363
+ }
1364
+ if (targetFormat === "jpeg") {
1365
+ pipeline = pipeline.jpeg({ quality: settings.quality, mozjpeg: true });
1366
+ res.setHeader("Content-Type", "image/jpeg");
1367
+ } else if (targetFormat === "png") {
1368
+ pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
1369
+ res.setHeader("Content-Type", "image/png");
1370
+ } else if (targetFormat === "webp") {
1371
+ const webpEffort = Math.min(settings.effort, 6);
1372
+ pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
1373
+ res.setHeader("Content-Type", "image/webp");
1374
+ } else if (targetFormat === "avif") {
1375
+ pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
1376
+ res.setHeader("Content-Type", "image/avif");
1377
+ }
1378
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1379
+ pipeline.on("error", (err) => {
1380
+ console.error("[next-drive] Pipeline error:", err);
1381
+ });
1382
+ stream.pipe(pipeline);
1383
+ pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
1384
+ pipeline.clone().pipe(res);
1385
+ return true;
1386
+ } catch (e) {
1387
+ console.error("[next-drive] Image transformation failed:", e);
1388
+ }
1389
+ }
1390
+ res.setHeader("Content-Type", mime);
1391
+ if (fileSize) res.setHeader("Content-Length", fileSize);
1392
+ stream.pipe(res);
1393
+ return true;
1394
+ } catch (error) {
1395
+ console.error(`[next-drive] Error in ${action}:`, error);
1396
+ const detail = error instanceof Error ? error.message : "Something went wrong while serving the file";
1397
+ res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
1398
+ return true;
1399
+ }
1400
+ };
1401
+ var handleAuthAction = async (req, res, action, config, owner) => {
1402
+ if (!["getAuthUrl", "callback", "listAccounts", "removeAccount"].includes(action)) {
1403
+ return false;
1404
+ }
1405
+ switch (action) {
1406
+ case "getAuthUrl": {
1407
+ const { provider } = req.query;
1408
+ if (provider === "GOOGLE") {
1409
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
1410
+ if (!clientId || !clientSecret || !redirectUri) {
1411
+ res.status(500).json({ status: 500, message: "Google Drive is not configured on the server" });
1412
+ return true;
1413
+ }
1414
+ const callbackUri = new URL(redirectUri);
1415
+ callbackUri.searchParams.set("action", "callback");
1416
+ const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
1417
+ const state = Buffer.from(JSON.stringify({ owner })).toString("base64");
1418
+ const url = oAuth2Client.generateAuthUrl({
1419
+ access_type: "offline",
1420
+ scope: ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email"],
1421
+ state,
1422
+ prompt: "consent"
1423
+ });
1424
+ res.status(200).json({ status: 200, message: "Auth URL generated", data: { url } });
1425
+ return true;
1426
+ }
1427
+ res.status(400).json({ status: 400, message: "Unknown storage provider requested" });
1428
+ return true;
1429
+ }
1430
+ case "callback": {
1431
+ const { code } = req.query;
1432
+ if (!code) {
1433
+ res.status(400).json({ status: 400, message: "Google sign-in failed: authorization code missing" });
1434
+ return true;
1435
+ }
1436
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
1437
+ if (!clientId || !clientSecret || !redirectUri) {
1438
+ res.status(500).json({ status: 500, message: "Google Drive sign-in is not configured on the server" });
1439
+ return true;
1440
+ }
1441
+ const callbackUri = new URL(redirectUri);
1442
+ callbackUri.searchParams.set("action", "callback");
1443
+ const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
1444
+ const { tokens } = await oAuth2Client.getToken(code);
1445
+ oAuth2Client.setCredentials(tokens);
1446
+ const oauth2 = googleapis.google.oauth2({ version: "v2", auth: oAuth2Client });
1447
+ const userInfo = await oauth2.userinfo.get();
1448
+ const existing = await account_default.findOne({ owner, "metadata.google.email": userInfo.data.email, "metadata.provider": "GOOGLE" });
1449
+ if (existing) {
1450
+ existing.metadata.google.credentials = tokens;
1451
+ existing.markModified("metadata");
1452
+ await existing.save();
1453
+ } else {
1454
+ const newAccount = new account_default({
1455
+ owner,
1456
+ name: userInfo.data.name || "Google Drive",
1457
+ metadata: {
1458
+ provider: "GOOGLE",
1459
+ google: {
1460
+ email: userInfo.data.email,
1461
+ credentials: tokens
1462
+ }
1463
+ }
1464
+ });
1465
+ await newAccount.save();
1466
+ }
1467
+ res.setHeader("Content-Type", "text/html");
1468
+ res.send(`<!DOCTYPE html>
1469
+ <html>
1470
+ <head><title>Authentication Complete</title></head>
1471
+ <body>
1472
+ <p>Authentication successful! This window will close automatically.</p>
1473
+ <script>
1474
+ (function() {
1475
+ if (window.opener) {
1476
+ try {
1477
+ window.opener.postMessage('oauth-success', '*');
1478
+ } catch (e) {}
1479
+ }
1480
+ try {
1481
+ localStorage.setItem('next-drive-oauth-success', Date.now().toString());
1482
+ localStorage.removeItem('next-drive-oauth-success');
1483
+ } catch (e) {}
1484
+ window.close();
1485
+ setTimeout(function() {
1486
+ document.body.innerHTML = '<p style="font-family: system-ui; text-align: center; margin-top: 50px;">Authentication successful!<br>You can close this tab now.</p>';
1487
+ }, 500);
1488
+ })();
1489
+ </script>
1490
+ </body>
1491
+ </html>`);
1492
+ return true;
1493
+ }
1494
+ case "listAccounts": {
1495
+ const accounts = await account_default.find({ owner });
1496
+ res.status(200).json({
1497
+ status: 200,
1498
+ data: {
1499
+ accounts: accounts.map((a) => ({
1500
+ id: a._id.toString(),
1501
+ name: a.name,
1502
+ email: a.metadata.google?.email || "",
1503
+ provider: a.metadata.provider
1504
+ }))
1505
+ }
1506
+ });
1507
+ return true;
1508
+ }
1509
+ case "removeAccount": {
1510
+ const { id } = req.query;
1511
+ const account = await account_default.findOne({ _id: id, owner });
1512
+ if (!account) {
1513
+ res.status(404).json({ status: 404, message: "Could not disconnect: account not found" });
1514
+ return true;
1515
+ }
1516
+ if (account.metadata.provider === "GOOGLE") {
1517
+ try {
1518
+ await GoogleDriveProvider.revokeToken(owner, account._id.toString());
1519
+ } catch (e) {
1520
+ console.error("Failed to revoke Google token:", e);
1521
+ }
1522
+ }
1523
+ await account_default.deleteOne({ _id: id, owner });
1524
+ await drive_default.deleteMany({ owner, storageAccountId: id });
1525
+ res.status(200).json({ status: 200, message: "Account removed" });
1526
+ return true;
1527
+ }
1528
+ default:
1529
+ return false;
1530
+ }
1531
+ };
1532
+ var objectIdSchema = zod.z.string().refine((val) => mongoose.isValidObjectId(val), {
1533
+ message: "Invalid ObjectId format"
1534
+ });
1535
+ var sanitizeFilename = (name) => {
1536
+ return name.replace(/[<>:"|?*\x00-\x1F]/g, "").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\.\.\//g, "").replace(/\.\.+/g, "").split("/").pop() || "".trim().slice(0, 255);
1537
+ };
1538
+ var sanitizeRegexInput = (input) => {
1539
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 100);
1540
+ };
1541
+ var nameSchema = zod.z.string().min(1, "Name is required").max(255, "Name too long").transform(sanitizeFilename).refine((val) => val.length > 0, { message: "Invalid name after sanitization" });
1542
+ var uploadChunkSchema = zod.z.object({
1543
+ chunkIndex: zod.z.number().int().min(0).max(1e4),
1544
+ totalChunks: zod.z.number().int().min(1).max(1e4),
1545
+ driveId: zod.z.string().optional(),
1546
+ fileName: nameSchema,
1547
+ fileSize: zod.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
1548
+ fileType: zod.z.string().min(1).max(255),
1549
+ folderId: zod.z.string().optional(),
1550
+ unauthenticated: zod.z.coerce.boolean().optional()
1551
+ }).refine((data) => data.chunkIndex < data.totalChunks, {
1552
+ message: "Chunk index must be less than total chunks"
1553
+ });
1554
+ var listQuerySchema = zod.z.object({
1555
+ folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]),
1556
+ limit: zod.z.string().optional().transform((val) => {
1557
+ const num = parseInt(val || "50", 10);
1558
+ return Math.min(Math.max(1, num), 100);
1559
+ }),
1560
+ afterId: objectIdSchema.optional()
1561
+ });
1562
+ zod.z.object({
1563
+ id: objectIdSchema,
1564
+ token: zod.z.string().optional()
1565
+ });
1566
+ zod.z.object({
1567
+ id: objectIdSchema,
1568
+ size: zod.z.enum(["small", "medium", "large"]).optional().default("medium"),
1569
+ token: zod.z.string().optional()
1570
+ });
1571
+ var renameBodySchema = zod.z.object({
1572
+ id: objectIdSchema,
1573
+ newName: nameSchema
1574
+ });
1575
+ var deleteQuerySchema = zod.z.object({
1576
+ id: objectIdSchema
1577
+ });
1578
+ zod.z.object({
1579
+ ids: zod.z.array(objectIdSchema).min(1).max(1e3)
1580
+ });
1581
+ var createFolderBodySchema = zod.z.object({
1582
+ name: nameSchema,
1583
+ parentId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.string().length(0), zod.z.undefined()]).optional()
1584
+ });
1585
+ var moveBodySchema = zod.z.object({
1586
+ ids: zod.z.array(objectIdSchema).min(1).max(1e3),
1587
+ targetFolderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional()
1588
+ });
1589
+ var reorderBodySchema = zod.z.object({
1590
+ ids: zod.z.array(objectIdSchema).min(1).max(1e3)
1591
+ });
1592
+ var searchQuerySchema = zod.z.object({
1593
+ q: zod.z.string().min(1).max(100).transform(sanitizeRegexInput),
1594
+ folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional(),
1595
+ limit: zod.z.string().optional().transform((val) => {
1596
+ const num = parseInt(val || "50", 10);
1597
+ return Math.min(Math.max(1, num), 100);
1598
+ }),
1599
+ trashed: zod.z.string().optional().transform((val) => val === "true")
1600
+ });
1601
+ zod.z.object({
1602
+ id: objectIdSchema
1603
+ });
1604
+ var cancelQuerySchema = zod.z.object({
1605
+ id: zod.z.string().uuid()
1606
+ });
1607
+ zod.z.object({
1608
+ days: zod.z.number().int().min(1).max(365).optional()
1609
+ });
1610
+ var driveFileSchemaZod = zod.z.object({
1611
+ id: zod.z.string(),
1612
+ file: zod.z.object({
1613
+ name: zod.z.string(),
1614
+ mime: zod.z.string(),
1615
+ size: zod.z.number()
1616
+ })
1617
+ });
1198
1618
  var getNextOrderValue = async (owner) => {
1199
1619
  const lastItem = await drive_default.findOne({ owner }, {}, { sort: { order: -1 } });
1200
1620
  return lastItem ? lastItem.order + 1 : 0;
@@ -1213,7 +1633,7 @@ var driveGetUrl = (fileId, options) => {
1213
1633
  } else {
1214
1634
  expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
1215
1635
  }
1216
- const signature = crypto2__default.default.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
1636
+ const signature = crypto3__default.default.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
1217
1637
  const token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
1218
1638
  return `${config.apiUrl || "/api/drive"}?action=serve&id=${fileId}&token=${token}`;
1219
1639
  };
@@ -1222,7 +1642,7 @@ var driveAddSignedUrlToken = (item, config) => {
1222
1642
  if (config.security?.signedUrls?.enabled && config.security.signedUrls.secret) {
1223
1643
  const { secret, expiresIn } = config.security.signedUrls;
1224
1644
  const expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
1225
- const signature = crypto2__default.default.createHmac("sha256", secret).update(`${item.id}:${expiryTimestamp}`).digest("hex");
1645
+ const signature = crypto3__default.default.createHmac("sha256", secret).update(`${item.id}:${expiryTimestamp}`).digest("hex");
1226
1646
  token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
1227
1647
  }
1228
1648
  const apiUrl = config.apiUrl || "/api/drive";
@@ -1541,7 +1961,7 @@ var driveUpload = async (source, key, options) => {
1541
1961
  if (!fs__default.default.existsSync(tempDir)) {
1542
1962
  fs__default.default.mkdirSync(tempDir, { recursive: true });
1543
1963
  }
1544
- tempFilePath = path__default.default.join(tempDir, `upload-${crypto2__default.default.randomUUID()}.tmp`);
1964
+ tempFilePath = path__default.default.join(tempDir, `upload-${crypto3__default.default.randomUUID()}.tmp`);
1545
1965
  fs__default.default.writeFileSync(tempFilePath, source);
1546
1966
  sourceFilePath = tempFilePath;
1547
1967
  fileSize = source.length;
@@ -1550,7 +1970,7 @@ var driveUpload = async (source, key, options) => {
1550
1970
  if (!fs__default.default.existsSync(tempDir)) {
1551
1971
  fs__default.default.mkdirSync(tempDir, { recursive: true });
1552
1972
  }
1553
- tempFilePath = path__default.default.join(tempDir, `upload-${crypto2__default.default.randomUUID()}.tmp`);
1973
+ tempFilePath = path__default.default.join(tempDir, `upload-${crypto3__default.default.randomUUID()}.tmp`);
1554
1974
  const writeStream = fs__default.default.createWriteStream(tempFilePath);
1555
1975
  await new Promise((resolve, reject) => {
1556
1976
  source.pipe(writeStream);
@@ -1721,98 +2141,29 @@ var driveCleanup = async () => {
1721
2141
  }
1722
2142
  return { removed, totalFreedInBytes };
1723
2143
  };
1724
- var objectIdSchema = zod.z.string().refine((val) => mongoose.isValidObjectId(val), {
1725
- message: "Invalid ObjectId format"
1726
- });
1727
- var sanitizeFilename = (name) => {
1728
- return name.replace(/[<>:"|?*\x00-\x1F]/g, "").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\.\.\//g, "").replace(/\.\.+/g, "").split("/").pop() || "".trim().slice(0, 255);
2144
+ var driveConfirm = async (id) => {
2145
+ const result = await drive_default.updateOne({ _id: id }, { $set: { expiresAt: null } });
2146
+ return result.matchedCount > 0;
1729
2147
  };
1730
- var sanitizeRegexInput = (input) => {
1731
- return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 100);
2148
+ var drivePurgeExpired = async () => {
2149
+ const expired = await drive_default.find({ expiresAt: { $ne: null, $lt: /* @__PURE__ */ new Date() } });
2150
+ const removed = [];
2151
+ let totalFreedInBytes = 0;
2152
+ for (const drive of expired) {
2153
+ const id = String(drive._id);
2154
+ try {
2155
+ await driveDelete(drive);
2156
+ totalFreedInBytes += drive.information.type === "FILE" ? drive.information.sizeInBytes : 0;
2157
+ removed.push(id);
2158
+ } catch (e) {
2159
+ console.error(`[next-drive] Failed to purge expired file ${id}:`, e);
2160
+ }
2161
+ }
2162
+ return { removed, totalFreedInBytes };
1732
2163
  };
1733
- var nameSchema = zod.z.string().min(1, "Name is required").max(255, "Name too long").transform(sanitizeFilename).refine((val) => val.length > 0, { message: "Invalid name after sanitization" });
1734
- var uploadChunkSchema = zod.z.object({
1735
- chunkIndex: zod.z.number().int().min(0).max(1e4),
1736
- totalChunks: zod.z.number().int().min(1).max(1e4),
1737
- driveId: zod.z.string().optional(),
1738
- fileName: nameSchema,
1739
- fileSize: zod.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
1740
- fileType: zod.z.string().min(1).max(255),
1741
- folderId: zod.z.string().optional()
1742
- }).refine((data) => data.chunkIndex < data.totalChunks, {
1743
- message: "Chunk index must be less than total chunks"
1744
- });
1745
- var listQuerySchema = zod.z.object({
1746
- folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]),
1747
- limit: zod.z.string().optional().transform((val) => {
1748
- const num = parseInt(val || "50", 10);
1749
- return Math.min(Math.max(1, num), 100);
1750
- }),
1751
- afterId: objectIdSchema.optional()
1752
- });
1753
- zod.z.object({
1754
- id: objectIdSchema,
1755
- token: zod.z.string().optional()
1756
- });
1757
- zod.z.object({
1758
- id: objectIdSchema,
1759
- size: zod.z.enum(["small", "medium", "large"]).optional().default("medium"),
1760
- token: zod.z.string().optional()
1761
- });
1762
- var renameBodySchema = zod.z.object({
1763
- id: objectIdSchema,
1764
- newName: nameSchema
1765
- });
1766
- var deleteQuerySchema = zod.z.object({
1767
- id: objectIdSchema
1768
- });
1769
- zod.z.object({
1770
- ids: zod.z.array(objectIdSchema).min(1).max(1e3)
1771
- });
1772
- var createFolderBodySchema = zod.z.object({
1773
- name: nameSchema,
1774
- parentId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.string().length(0), zod.z.undefined()]).optional()
1775
- });
1776
- var moveBodySchema = zod.z.object({
1777
- ids: zod.z.array(objectIdSchema).min(1).max(1e3),
1778
- targetFolderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional()
1779
- });
1780
- zod.z.object({
1781
- ids: zod.z.array(objectIdSchema).min(1).max(1e3)
1782
- });
1783
- var searchQuerySchema = zod.z.object({
1784
- q: zod.z.string().min(1).max(100).transform(sanitizeRegexInput),
1785
- folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional(),
1786
- limit: zod.z.string().optional().transform((val) => {
1787
- const num = parseInt(val || "50", 10);
1788
- return Math.min(Math.max(1, num), 100);
1789
- }),
1790
- trashed: zod.z.string().optional().transform((val) => val === "true")
1791
- });
1792
- zod.z.object({
1793
- id: objectIdSchema
1794
- });
1795
- var cancelQuerySchema = zod.z.object({
1796
- id: zod.z.string().uuid()
1797
- });
1798
- zod.z.object({
1799
- days: zod.z.number().int().min(1).max(365).optional()
1800
- });
1801
- var driveFileSchemaZod = zod.z.object({
1802
- id: zod.z.string(),
1803
- file: zod.z.object({
1804
- name: zod.z.string(),
1805
- mime: zod.z.string(),
1806
- size: zod.z.number()
1807
- })
1808
- });
1809
2164
 
1810
- // src/server/security/cryptoUtils.ts
1811
- function sanitizeContentDispositionFilename(filename) {
1812
- const basename = filename.replace(/^.*[\\\/]/, "");
1813
- return basename.replace(/["\r\n]/g, "").replace(/[^\x20-\x7E]/g, "").slice(0, 255);
1814
- }
1815
- var getProvider = async (req, owner) => {
2165
+ // src/server/actions/shared.ts
2166
+ var resolveProvider = async (req, owner) => {
1816
2167
  const accountId = req.headers["x-drive-account"];
1817
2168
  if (!accountId || accountId === "LOCAL") {
1818
2169
  return { provider: LocalStorageProvider };
@@ -1821,735 +2172,572 @@ var getProvider = async (req, owner) => {
1821
2172
  if (!account) {
1822
2173
  throw new Error("Storage account not found or access denied");
1823
2174
  }
1824
- if (account.metadata.provider === "GOOGLE") return { provider: GoogleDriveProvider, accountId: account._id.toString() };
2175
+ if (account.metadata.provider === "GOOGLE") {
2176
+ return { provider: GoogleDriveProvider, accountId: account._id.toString() };
2177
+ }
1825
2178
  return { provider: LocalStorageProvider };
1826
2179
  };
1827
- var addSignedUrlToken = (item, config) => {
2180
+ var withSignedUrl = (item, config) => {
1828
2181
  return driveAddSignedUrlToken(item, config);
1829
2182
  };
1830
- var addSignedUrlTokens = (items, config) => {
2183
+ var withSignedUrls = (items, config) => {
1831
2184
  return driveAddSignedUrlTokens(items, config);
1832
2185
  };
1833
- var applyCorsHeaders = (req, res, config) => {
1834
- const cors = config.cors;
1835
- if (!cors?.enabled) return false;
1836
- const origin = req.headers.origin;
1837
- const allowedOrigins = cors.origins ?? "*";
1838
- const methods = cors.methods ?? ["GET", "POST", "PUT", "DELETE", "OPTIONS"];
1839
- const allowedHeaders = cors.allowedHeaders ?? ["Content-Type", "Authorization", "X-Drive-Account"];
1840
- const exposedHeaders = cors.exposedHeaders ?? ["Content-Length", "Content-Type", "Content-Disposition"];
1841
- const credentials = cors.credentials ?? false;
1842
- const maxAge = cors.maxAge ?? 86400;
1843
- let allowOrigin = null;
1844
- if (origin) {
1845
- if (allowedOrigins === "*") {
1846
- allowOrigin = origin;
1847
- } else if (Array.isArray(allowedOrigins)) {
1848
- if (allowedOrigins.includes(origin)) {
1849
- allowOrigin = origin;
2186
+
2187
+ // src/server/actions/drive.ts
2188
+ var handleDriveAction = async (ctx) => {
2189
+ const { req, res, action, config, owner, isRootMode, information, provider, accountId } = ctx;
2190
+ switch (action) {
2191
+ case "list": {
2192
+ if (req.method !== "GET") return void res.status(405).json({ status: 405, message: "Listing files requires a GET request" });
2193
+ const listQuery = listQuerySchema.safeParse(req.query);
2194
+ if (!listQuery.success) return void res.status(400).json({ status: 400, message: "Could not list files: invalid request parameters" });
2195
+ const { folderId, limit, afterId } = listQuery.data;
2196
+ try {
2197
+ await provider.sync(folderId || "root", owner, accountId);
2198
+ } catch (e) {
2199
+ console.error("Sync failed", e);
1850
2200
  }
1851
- } else if (allowedOrigins === origin) {
1852
- allowOrigin = origin;
1853
- }
1854
- } else if (allowedOrigins === "*") {
1855
- allowOrigin = "*";
1856
- }
1857
- if (!allowOrigin) {
1858
- if (req.method === "OPTIONS") {
1859
- res.status(403).end();
1860
- return true;
1861
- }
1862
- return false;
1863
- }
1864
- res.setHeader("Access-Control-Allow-Origin", allowOrigin);
1865
- res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
1866
- res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
1867
- res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(", "));
1868
- res.setHeader("Access-Control-Max-Age", maxAge.toString());
1869
- if (credentials) {
1870
- res.setHeader("Access-Control-Allow-Credentials", "true");
1871
- }
1872
- if (req.method === "OPTIONS") {
1873
- res.status(204).end();
1874
- return true;
1875
- }
1876
- return false;
1877
- };
1878
- var driveAPIHandler = async (req, res) => {
1879
- const action = req.query.action || (req.query.code && req.query.state ? "callback" : void 0);
1880
- let config;
1881
- try {
1882
- config = getDriveConfig();
1883
- } catch (error) {
1884
- console.error("[next-drive] Configuration error:", error);
1885
- res.status(500).json({ status: 500, message: "Drive is not ready: failed to initialize configuration" });
1886
- return;
1887
- }
1888
- const isPreflightHandled = applyCorsHeaders(req, res, config);
1889
- if (isPreflightHandled) return;
1890
- if (!action) {
1891
- res.status(400).json({ status: 400, message: 'Missing "action" parameter in request' });
1892
- return;
1893
- }
1894
- if (action === "serve" || action === "thumbnail") {
1895
- try {
1896
- const { id, token } = req.query;
1897
- if (!id || typeof id !== "string") {
1898
- return res.status(400).json({ status: 400, message: "Could not open file: missing or invalid file ID" });
2201
+ const query = {
2202
+ "provider.type": provider.name,
2203
+ storageAccountId: accountId || null,
2204
+ parentId: folderId === "root" || !folderId ? null : folderId,
2205
+ trashedAt: null
2206
+ };
2207
+ if (!isRootMode) {
2208
+ query.owner = owner;
1899
2209
  }
1900
- const drive = await drive_default.findById(id);
1901
- if (!drive) return res.status(404).json({ status: 404, message: "File not found or no longer available" });
1902
- if (config.security?.signedUrls?.enabled) {
1903
- if (!token || typeof token !== "string") {
1904
- return res.status(401).json({ status: 401, message: "Access denied: this link is missing its access token" });
1905
- }
2210
+ if (afterId) query._id = { $lt: afterId };
2211
+ const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
2212
+ const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
2213
+ res.status(200).json({ status: 200, message: "Items retrieved", data: { items: plainItems, hasMore: items.length === limit } });
2214
+ return;
2215
+ }
2216
+ case "search": {
2217
+ const searchData = searchQuerySchema.safeParse(req.query);
2218
+ if (!searchData.success) return void res.status(400).json({ status: 400, message: "Could not search: invalid request parameters" });
2219
+ const { q, folderId, limit, trashed } = searchData.data;
2220
+ if (!trashed) {
1906
2221
  try {
1907
- const decoded = Buffer.from(token, "base64url").toString();
1908
- const [expiryStr, signature] = decoded.split(":");
1909
- const expiry = parseInt(expiryStr, 10);
1910
- if (Date.now() / 1e3 > expiry) {
1911
- return res.status(401).json({ status: 401, message: "Access denied: this link has expired" });
1912
- }
1913
- const { secret } = config.security.signedUrls;
1914
- const expectedSignature = crypto2__default.default.createHmac("sha256", secret).update(`${id}:${expiry}`).digest("hex");
1915
- if (signature !== expectedSignature) {
1916
- return res.status(401).json({ status: 401, message: "Access denied: this link's access token is invalid" });
1917
- }
1918
- } catch (err) {
1919
- return res.status(401).json({ status: 401, message: "Access denied: this link's access token is malformed" });
1920
- }
1921
- }
1922
- const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1923
- const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
1924
- if (action === "thumbnail") {
1925
- const stream = await itemProvider.getThumbnail(drive, itemAccountId);
1926
- res.setHeader("Content-Type", "image/webp");
1927
- if (config.cors?.enabled) {
1928
- res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
2222
+ await provider.search(q, owner, accountId);
2223
+ } catch (e) {
2224
+ console.error("Search sync failed", e);
1929
2225
  }
1930
- stream.pipe(res);
1931
- return;
1932
2226
  }
1933
- if (action === "serve") {
1934
- const { stream, mime, size: fileSize } = await itemProvider.openStream(drive, itemAccountId);
1935
- const safeFilename = sanitizeContentDispositionFilename(drive.name);
1936
- const format = req.query.format;
1937
- const quality = req.query.quality;
1938
- const display = req.query.display;
1939
- const sizePreset = req.query.size;
1940
- const fit = req.query.fit;
1941
- const position = req.query.position;
1942
- const isImage = mime.startsWith("image/");
1943
- const shouldTransform = isImage && (format || quality || display || sizePreset || fit);
1944
- res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
1945
- if (config.cors?.enabled) {
1946
- res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1947
- }
1948
- if (shouldTransform) {
1949
- try {
1950
- const settings = getImageSettings(fileSize, quality, display, sizePreset, fit, position);
1951
- let targetFormat = format || mime.split("/")[1];
1952
- if (targetFormat === "jpg") targetFormat = "jpeg";
1953
- if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
1954
- targetFormat = format || "webp";
1955
- }
1956
- const cacheDir = path__default.default.join(config.storage.path, "file", drive._id.toString(), "cache");
1957
- const cacheKey = [
1958
- "opt",
1959
- `q${settings.quality}`,
1960
- `e${settings.effort}`,
1961
- settings.width ? `${settings.width}x${settings.height}` : "orig",
1962
- settings.fit || "none",
1963
- settings.position || "c",
1964
- targetFormat
1965
- ].join("_");
1966
- const cachePath = path__default.default.join(cacheDir, `${cacheKey}.bin`);
1967
- if (fs__default.default.existsSync(cachePath)) {
1968
- const cacheStat = fs__default.default.statSync(cachePath);
1969
- res.setHeader("Content-Type", `image/${targetFormat}`);
1970
- res.setHeader("Content-Length", cacheStat.size);
1971
- res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1972
- if (config.cors?.enabled) {
1973
- res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1974
- }
1975
- if ("destroy" in stream) stream.destroy();
1976
- fs__default.default.createReadStream(cachePath).pipe(res);
1977
- return;
1978
- }
1979
- if (!fs__default.default.existsSync(cacheDir)) fs__default.default.mkdirSync(cacheDir, { recursive: true });
1980
- let pipeline = sharp2__default.default();
1981
- if (settings.width && settings.height) {
1982
- pipeline = pipeline.resize(settings.width, settings.height, {
1983
- fit: settings.fit || "inside",
1984
- position: settings.position || "center",
1985
- withoutEnlargement: true,
1986
- // Use transparent background for 'contain' fit to preserve transparency
1987
- background: { r: 0, g: 0, b: 0, alpha: 0 }
1988
- });
1989
- }
1990
- if (targetFormat === "jpeg") {
1991
- pipeline = pipeline.jpeg({ quality: settings.quality, mozjpeg: true });
1992
- res.setHeader("Content-Type", "image/jpeg");
1993
- } else if (targetFormat === "png") {
1994
- pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
1995
- res.setHeader("Content-Type", "image/png");
1996
- } else if (targetFormat === "webp") {
1997
- const webpEffort = Math.min(settings.effort, 6);
1998
- pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
1999
- res.setHeader("Content-Type", "image/webp");
2000
- } else if (targetFormat === "avif") {
2001
- pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
2002
- res.setHeader("Content-Type", "image/avif");
2003
- }
2004
- res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
2005
- pipeline.on("error", (err) => {
2006
- console.error("[next-drive] Pipeline error:", err);
2007
- });
2008
- stream.pipe(pipeline);
2009
- pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
2010
- pipeline.clone().pipe(res);
2011
- return;
2012
- } catch (e) {
2013
- console.error("[next-drive] Image transformation failed:", e);
2014
- }
2015
- }
2016
- res.setHeader("Content-Type", mime);
2017
- if (fileSize) res.setHeader("Content-Length", fileSize);
2018
- stream.pipe(res);
2019
- return;
2227
+ const query = {
2228
+ "provider.type": provider.name,
2229
+ storageAccountId: accountId || null,
2230
+ trashedAt: trashed ? { $ne: null } : null,
2231
+ name: { $regex: q, $options: "i" }
2232
+ };
2233
+ if (!isRootMode) {
2234
+ query.owner = owner;
2020
2235
  }
2021
- } catch (error) {
2022
- console.error(`[next-drive] Error in ${action}:`, error);
2023
- return res.status(500).json({
2024
- status: 500,
2025
- message: error instanceof Error ? error.message : "Something went wrong while serving the file"
2236
+ if (folderId && folderId !== "root") query.parentId = folderId;
2237
+ const items = await drive_default.find(query, {}, { limit, sort: { createdAt: -1 } });
2238
+ const plainItems = withSignedUrls(await Promise.all(items.map((i) => i.toClient())), config);
2239
+ res.status(200).json({ status: 200, message: "Results", data: { items: plainItems } });
2240
+ return;
2241
+ }
2242
+ case "upload": {
2243
+ if (req.method !== "POST") return void res.status(405).json({ status: 405, message: "Uploading requires a POST request" });
2244
+ const systemTmpDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
2245
+ if (!fs__default.default.existsSync(systemTmpDir)) fs__default.default.mkdirSync(systemTmpDir, { recursive: true });
2246
+ const form = formidable__default.default({
2247
+ multiples: false,
2248
+ maxFileSize: (config.security?.maxUploadSizeInBytes ?? 1024 * 1024 * 1024) * 2,
2249
+ uploadDir: systemTmpDir,
2250
+ keepExtensions: true
2026
2251
  });
2027
- }
2028
- }
2029
- try {
2030
- const mode = config.mode || "NORMAL";
2031
- const information = await getDriveInformation({ method: "REQUEST", req });
2032
- const { key: owner } = information;
2033
- const STORAGE_PATH = config.storage.path;
2034
- const isRootMode = mode === "ROOT";
2035
- if (action === "information") {
2036
- const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
2037
- const googleConfigured = !!(clientId && clientSecret && redirectUri);
2038
- return res.status(200).json({
2039
- status: 200,
2040
- message: "Information retrieved",
2041
- data: {
2042
- providers: {
2043
- google: googleConfigured
2044
- },
2045
- mode
2046
- }
2252
+ const [fields, files] = await new Promise((resolve, reject) => {
2253
+ form.parse(req, (err, parsedFields, parsedFiles) => {
2254
+ if (err) reject(err);
2255
+ else resolve([parsedFields, parsedFiles]);
2256
+ });
2047
2257
  });
2048
- }
2049
- if (["getAuthUrl", "callback", "listAccounts", "removeAccount"].includes(action)) {
2050
- switch (action) {
2051
- case "getAuthUrl": {
2052
- const { provider: provider2 } = req.query;
2053
- if (provider2 === "GOOGLE") {
2054
- const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
2055
- if (!clientId || !clientSecret || !redirectUri) return res.status(500).json({ status: 500, message: "Google Drive is not configured on the server" });
2056
- const callbackUri = new URL(redirectUri);
2057
- callbackUri.searchParams.set("action", "callback");
2058
- const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
2059
- const state = Buffer.from(JSON.stringify({ owner })).toString("base64");
2060
- const url = oAuth2Client.generateAuthUrl({
2061
- access_type: "offline",
2062
- scope: ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email"],
2063
- state,
2064
- prompt: "consent"
2065
- // force refresh token
2066
- });
2067
- return res.status(200).json({ status: 200, message: "Auth URL generated", data: { url } });
2258
+ const cleanupTempFiles = (allFiles) => {
2259
+ Object.values(allFiles).flat().forEach((file) => {
2260
+ if (file && fs__default.default.existsSync(file.filepath)) fs__default.default.rmSync(file.filepath, { force: true });
2261
+ });
2262
+ };
2263
+ const getString = (f) => Array.isArray(f) ? f[0] : f || "";
2264
+ const getInt = (f) => parseInt(getString(f) || "0", 10);
2265
+ const uploadData = uploadChunkSchema.safeParse({
2266
+ chunkIndex: getInt(fields.chunkIndex),
2267
+ totalChunks: getInt(fields.totalChunks),
2268
+ driveId: getString(fields.driveId) || void 0,
2269
+ fileName: getString(fields.fileName),
2270
+ fileSize: getInt(fields.fileSize),
2271
+ fileType: getString(fields.fileType),
2272
+ folderId: getString(fields.folderId) || void 0
2273
+ });
2274
+ if (!uploadData.success) {
2275
+ cleanupTempFiles(files);
2276
+ return void res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
2277
+ }
2278
+ const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId, unauthenticated } = uploadData.data;
2279
+ let currentUploadId = driveId;
2280
+ const tempBaseDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
2281
+ if (!currentUploadId) {
2282
+ if (chunkIndex !== 0) return void res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
2283
+ if (unauthenticated) {
2284
+ const unauth = config.security?.unauthenticated;
2285
+ if (!unauth?.enabled) {
2286
+ cleanupTempFiles(files);
2287
+ return void res.status(403).json({ status: 403, message: "Anonymous uploads are not enabled" });
2068
2288
  }
2069
- return res.status(400).json({ status: 400, message: "Unknown storage provider requested" });
2070
- }
2071
- case "callback": {
2072
- const { code, state } = req.query;
2073
- if (!code) return res.status(400).json({ status: 400, message: "Google sign-in failed: authorization code missing" });
2074
- const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
2075
- if (!clientId || !clientSecret || !redirectUri) return res.status(500).json({ status: 500, message: "Google Drive sign-in is not configured on the server" });
2076
- const callbackUri = new URL(redirectUri);
2077
- callbackUri.searchParams.set("action", "callback");
2078
- const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
2079
- const { tokens } = await oAuth2Client.getToken(code);
2080
- oAuth2Client.setCredentials(tokens);
2081
- const oauth2 = googleapis.google.oauth2({ version: "v2", auth: oAuth2Client });
2082
- const userInfo = await oauth2.userinfo.get();
2083
- const existing = await account_default.findOne({ owner, "metadata.google.email": userInfo.data.email, "metadata.provider": "GOOGLE" });
2084
- if (existing) {
2085
- existing.metadata.google.credentials = tokens;
2086
- existing.markModified("metadata");
2087
- await existing.save();
2088
- } else {
2089
- const newAccount = new account_default({
2090
- owner,
2091
- name: userInfo.data.name || "Google Drive",
2092
- metadata: {
2093
- provider: "GOOGLE",
2094
- google: {
2095
- email: userInfo.data.email,
2096
- credentials: tokens
2097
- }
2098
- }
2099
- });
2100
- await newAccount.save();
2289
+ if (fileSizeInBytes > unauth.maxUploadSizeInBytes) {
2290
+ cleanupTempFiles(files);
2291
+ return void res.status(413).json({ status: 413, message: "Could not upload: file exceeds the maximum allowed size" });
2101
2292
  }
2102
- res.setHeader("Content-Type", "text/html");
2103
- return res.send(`<!DOCTYPE html>
2104
- <html>
2105
- <head><title>Authentication Complete</title></head>
2106
- <body>
2107
- <p>Authentication successful! This window will close automatically.</p>
2108
- <script>
2109
- (function() {
2110
- // Method 1: postMessage for popup windows
2111
- if (window.opener) {
2112
- try {
2113
- window.opener.postMessage('oauth-success', '*');
2114
- } catch (e) {}
2115
- }
2116
- // Method 2: localStorage event for new tabs (macOS fullscreen mode)
2117
- try {
2118
- localStorage.setItem('next-drive-oauth-success', Date.now().toString());
2119
- localStorage.removeItem('next-drive-oauth-success');
2120
- } catch (e) {}
2121
- // Close the window/tab
2122
- window.close();
2123
- // Fallback: If window.close() doesn't work (some browsers block it),
2124
- // show a message to manually close
2125
- setTimeout(function() {
2126
- document.body.innerHTML = '<p style="font-family: system-ui; text-align: center; margin-top: 50px;">Authentication successful!<br>You can close this tab now.</p>';
2127
- }, 500);
2128
- })();
2129
- </script>
2130
- </body>
2131
- </html>`);
2132
- }
2133
- case "listAccounts": {
2134
- const accounts = await account_default.find({ owner });
2135
- return res.status(200).json({
2136
- status: 200,
2137
- data: {
2138
- accounts: accounts.map((a) => ({
2139
- id: a._id.toString(),
2140
- name: a.name,
2141
- email: a.metadata.google?.email || "",
2142
- provider: a.metadata.provider
2143
- }))
2293
+ if (fileType && !validateMimeType(fileType, unauth.allowedMimeTypes)) {
2294
+ cleanupTempFiles(files);
2295
+ return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2296
+ }
2297
+ const abuse = unauth.abuse;
2298
+ if (abuse) {
2299
+ const store = globalThis.__nextDrive.abuse;
2300
+ const now = Date.now();
2301
+ const ip = abuse.clientId?.(req) ?? (abuse.trustedHeaders ?? ["cf-connecting-ip", "x-forwarded-for"]).map((h) => req.headers[h]).find(Boolean)?.split(",")[0].trim() ?? req.socket.remoteAddress ?? "unknown";
2302
+ const hits = (store.ipHits.get(ip) ?? []).filter((t) => now - t < 36e5);
2303
+ const perIp = abuse.perIp;
2304
+ if (perIp && hits.filter((t) => now - t < perIp.windowMinutes * 6e4).length >= perIp.max) {
2305
+ cleanupTempFiles(files);
2306
+ return void res.status(429).json({ status: 429, message: "Too many uploads, please try again later" });
2144
2307
  }
2145
- });
2146
- }
2147
- case "removeAccount": {
2148
- const { id } = req.query;
2149
- const account = await account_default.findOne({ _id: id, owner });
2150
- if (!account) return res.status(404).json({ status: 404, message: "Could not disconnect: account not found" });
2151
- if (account.metadata.provider === "GOOGLE") {
2152
- try {
2153
- await GoogleDriveProvider.revokeToken(owner, account._id.toString());
2154
- } catch (e) {
2155
- console.error("Failed to revoke Google token:", e);
2308
+ if (abuse.hourlyPerIp && hits.length >= abuse.hourlyPerIp) {
2309
+ cleanupTempFiles(files);
2310
+ return void res.status(429).json({ status: 429, message: "Hourly upload limit reached, please try again later" });
2156
2311
  }
2312
+ if (abuse.maxConcurrent && store.concurrent >= abuse.maxConcurrent) {
2313
+ cleanupTempFiles(files);
2314
+ return void res.status(429).json({ status: 429, message: "Server is busy with uploads, please try again later" });
2315
+ }
2316
+ if (abuse.maxLiveBytes) {
2317
+ const [agg] = await drive_default.aggregate([{ $match: { expiresAt: { $ne: null } } }, { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }]);
2318
+ if ((agg?.total ?? 0) + fileSizeInBytes > abuse.maxLiveBytes) {
2319
+ cleanupTempFiles(files);
2320
+ return void res.status(429).json({ status: 429, message: "Temporary storage is full, please try again later" });
2321
+ }
2322
+ }
2323
+ hits.push(now);
2324
+ store.ipHits.set(ip, hits);
2325
+ store.concurrent++;
2157
2326
  }
2158
- await account_default.deleteOne({ _id: id, owner });
2159
- await drive_default.deleteMany({ owner, storageAccountId: id });
2160
- return res.status(200).json({ status: 200, message: "Account removed" });
2161
- }
2162
- }
2163
- }
2164
- const { provider, accountId } = await getProvider(req, owner);
2165
- switch (action) {
2166
- // ** 1. LIST **
2167
- case "list": {
2168
- if (req.method !== "GET") return res.status(405).json({ status: 405, message: "Listing files requires a GET request" });
2169
- const listQuery = listQuerySchema.safeParse(req.query);
2170
- if (!listQuery.success) return res.status(400).json({ status: 400, message: "Could not list files: invalid request parameters" });
2171
- const { folderId, limit, afterId } = listQuery.data;
2172
- try {
2173
- await provider.sync(folderId || "root", owner, accountId);
2174
- } catch (e) {
2175
- console.error("Sync failed", e);
2176
- }
2177
- const query = {
2178
- "provider.type": provider.name,
2179
- storageAccountId: accountId || null,
2180
- parentId: folderId === "root" || !folderId ? null : folderId,
2181
- trashedAt: null
2182
- };
2183
- if (!isRootMode) {
2184
- query.owner = owner;
2185
- }
2186
- if (afterId) query._id = { $lt: afterId };
2187
- const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
2188
- const plainItems = addSignedUrlTokens(await Promise.all(items.map((item) => item.toClient())), config);
2189
- res.status(200).json({ status: 200, message: "Items retrieved", data: { items: plainItems, hasMore: items.length === limit } });
2190
- return;
2191
- }
2192
- // ** 2. SEARCH **
2193
- case "search": {
2194
- const searchData = searchQuerySchema.safeParse(req.query);
2195
- if (!searchData.success) return res.status(400).json({ status: 400, message: "Could not search: invalid request parameters" });
2196
- const { q, folderId, limit, trashed } = searchData.data;
2197
- if (!trashed) {
2198
- try {
2199
- await provider.search(q, owner, accountId);
2200
- } catch (e) {
2201
- console.error("Search sync failed", e);
2202
- }
2203
- }
2204
- const query = {
2205
- "provider.type": provider.name,
2206
- storageAccountId: accountId || null,
2207
- trashedAt: trashed ? { $ne: null } : null,
2208
- name: { $regex: q, $options: "i" }
2209
- };
2210
- if (!isRootMode) {
2211
- query.owner = owner;
2212
- }
2213
- if (folderId && folderId !== "root") query.parentId = folderId;
2214
- const items = await drive_default.find(query, {}, { limit, sort: { createdAt: -1 } });
2215
- const plainItems = addSignedUrlTokens(await Promise.all(items.map((i) => i.toClient())), config);
2216
- return res.status(200).json({ status: 200, message: "Results", data: { items: plainItems } });
2217
- }
2218
- // ** 3. UPLOAD **
2219
- case "upload": {
2220
- if (req.method !== "POST") return res.status(405).json({ status: 405, message: "Uploading requires a POST request" });
2221
- const systemTmpDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
2222
- if (!fs__default.default.existsSync(systemTmpDir)) fs__default.default.mkdirSync(systemTmpDir, { recursive: true });
2223
- const form = formidable__default.default({
2224
- multiples: false,
2225
- maxFileSize: (config.security?.maxUploadSizeInBytes ?? 1024 * 1024 * 1024) * 2,
2226
- uploadDir: systemTmpDir,
2227
- keepExtensions: true
2228
- });
2229
- const [fields, files] = await new Promise((resolve, reject) => {
2230
- form.parse(req, (err, fields2, files2) => {
2231
- if (err) reject(err);
2232
- else resolve([fields2, files2]);
2233
- });
2234
- });
2235
- const cleanupTempFiles = (files2) => {
2236
- Object.values(files2).flat().forEach((file) => {
2237
- if (file && fs__default.default.existsSync(file.filepath)) fs__default.default.rmSync(file.filepath, { force: true });
2238
- });
2239
- };
2240
- const getString = (f) => Array.isArray(f) ? f[0] : f || "";
2241
- const getInt = (f) => parseInt(getString(f) || "0", 10);
2242
- const uploadData = uploadChunkSchema.safeParse({
2243
- chunkIndex: getInt(fields.chunkIndex),
2244
- totalChunks: getInt(fields.totalChunks),
2245
- driveId: getString(fields.driveId) || void 0,
2246
- fileName: getString(fields.fileName),
2247
- fileSize: getInt(fields.fileSize),
2248
- fileType: getString(fields.fileType),
2249
- folderId: getString(fields.folderId) || void 0
2250
- });
2251
- if (!uploadData.success) {
2252
- cleanupTempFiles(files);
2253
- return res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
2254
- }
2255
- const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId } = uploadData.data;
2256
- let currentUploadId = driveId;
2257
- const tempBaseDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
2258
- if (!currentUploadId) {
2259
- if (chunkIndex !== 0) return res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
2327
+ } else {
2260
2328
  if (fileType && config.security) {
2261
2329
  if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
2262
2330
  cleanupTempFiles(files);
2263
- return res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2331
+ return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2264
2332
  }
2265
2333
  }
2266
2334
  if (!isRootMode) {
2267
2335
  const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2268
2336
  if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
2269
2337
  cleanupTempFiles(files);
2270
- return res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
2338
+ return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
2271
2339
  }
2272
2340
  }
2273
- currentUploadId = crypto2__default.default.randomUUID();
2274
- const uploadDir = path__default.default.join(tempBaseDir, currentUploadId);
2275
- fs__default.default.mkdirSync(uploadDir, { recursive: true });
2276
- const metadata = {
2277
- owner,
2278
- accountId,
2279
- providerName: provider.name,
2280
- name: fileName,
2281
- parentId: folderId === "root" || !folderId ? null : folderId,
2282
- fileSize: fileSizeInBytes,
2283
- mimeType: fileType,
2284
- totalChunks
2285
- };
2286
- fs__default.default.writeFileSync(path__default.default.join(uploadDir, "metadata.json"), JSON.stringify(metadata));
2287
2341
  }
2288
- if (currentUploadId) {
2289
- const uploadDir = path__default.default.join(tempBaseDir, currentUploadId);
2290
- if (!fs__default.default.existsSync(uploadDir)) {
2291
- cleanupTempFiles(files);
2292
- return res.status(404).json({ status: 404, message: "Could not upload: this upload session was not found or has expired" });
2342
+ currentUploadId = crypto3__default.default.randomUUID();
2343
+ const uploadDir2 = path__default.default.join(tempBaseDir, currentUploadId);
2344
+ fs__default.default.mkdirSync(uploadDir2, { recursive: true });
2345
+ const metadata = {
2346
+ owner: unauthenticated ? null : owner,
2347
+ accountId: unauthenticated ? null : accountId,
2348
+ providerName: provider.name,
2349
+ name: fileName,
2350
+ parentId: unauthenticated || folderId === "root" || !folderId ? null : folderId,
2351
+ fileSize: fileSizeInBytes,
2352
+ mimeType: fileType,
2353
+ totalChunks,
2354
+ unauthenticated: !!unauthenticated
2355
+ };
2356
+ fs__default.default.writeFileSync(path__default.default.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
2357
+ }
2358
+ if (!currentUploadId) {
2359
+ cleanupTempFiles(files);
2360
+ return void res.status(400).json({ status: 400, message: "Could not upload: invalid upload request" });
2361
+ }
2362
+ const uploadDir = path__default.default.join(tempBaseDir, currentUploadId);
2363
+ if (!fs__default.default.existsSync(uploadDir)) {
2364
+ cleanupTempFiles(files);
2365
+ return void res.status(404).json({ status: 404, message: "Could not upload: this upload session was not found or has expired" });
2366
+ }
2367
+ try {
2368
+ const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
2369
+ if (!chunkFile) throw new Error("Could not upload: no file chunk was received");
2370
+ const partPath = path__default.default.join(uploadDir, `part_${chunkIndex}`);
2371
+ try {
2372
+ fs__default.default.renameSync(chunkFile.filepath, partPath);
2373
+ } catch (err) {
2374
+ if (err instanceof Error && "code" in err && err.code === "EXDEV") {
2375
+ fs__default.default.copyFileSync(chunkFile.filepath, partPath);
2376
+ fs__default.default.unlinkSync(chunkFile.filepath);
2377
+ } else {
2378
+ throw err;
2293
2379
  }
2294
- try {
2295
- const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
2296
- if (!chunkFile) throw new Error("Could not upload: no file chunk was received");
2297
- const partPath = path__default.default.join(uploadDir, `part_${chunkIndex}`);
2298
- try {
2299
- fs__default.default.renameSync(chunkFile.filepath, partPath);
2300
- } catch (err) {
2301
- if (err instanceof Error && "code" in err && err.code === "EXDEV") {
2302
- fs__default.default.copyFileSync(chunkFile.filepath, partPath);
2303
- fs__default.default.unlinkSync(chunkFile.filepath);
2304
- } else {
2305
- throw err;
2306
- }
2380
+ }
2381
+ const uploadedParts = fs__default.default.readdirSync(uploadDir).filter((f) => f.startsWith("part_"));
2382
+ if (uploadedParts.length === totalChunks) {
2383
+ const metaPath = path__default.default.join(uploadDir, "metadata.json");
2384
+ const meta = JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8"));
2385
+ const finalTempPath = path__default.default.join(uploadDir, "final.bin");
2386
+ const writeStream = fs__default.default.createWriteStream(finalTempPath);
2387
+ let streamError = null;
2388
+ writeStream.on("error", (err) => {
2389
+ streamError = err;
2390
+ });
2391
+ await new Promise((resolve, reject) => {
2392
+ writeStream.on("open", () => resolve());
2393
+ writeStream.once("error", reject);
2394
+ });
2395
+ for (let i = 0; i < totalChunks; i++) {
2396
+ if (streamError) {
2397
+ writeStream.destroy();
2398
+ throw streamError;
2307
2399
  }
2308
- const uploadedParts = fs__default.default.readdirSync(uploadDir).filter((f) => f.startsWith("part_"));
2309
- if (uploadedParts.length === totalChunks) {
2310
- const metaPath = path__default.default.join(uploadDir, "metadata.json");
2311
- const meta = JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8"));
2312
- const finalTempPath = path__default.default.join(uploadDir, "final.bin");
2313
- const writeStream = fs__default.default.createWriteStream(finalTempPath);
2314
- let streamError = null;
2315
- writeStream.on("error", (err) => {
2316
- streamError = err;
2317
- });
2318
- await new Promise((resolve, reject) => {
2319
- writeStream.on("open", () => resolve());
2320
- writeStream.once("error", reject);
2321
- });
2322
- for (let i = 0; i < totalChunks; i++) {
2323
- if (streamError) {
2324
- writeStream.destroy();
2325
- throw streamError;
2326
- }
2327
- const pPath = path__default.default.join(uploadDir, `part_${i}`);
2328
- if (!fs__default.default.existsSync(pPath)) {
2329
- writeStream.destroy();
2330
- throw new Error(`Could not finish upload: chunk ${i} is missing`);
2331
- }
2332
- const data = fs__default.default.readFileSync(pPath);
2333
- const canContinue = writeStream.write(data);
2334
- if (!canContinue) {
2335
- await new Promise((resolve, reject) => {
2336
- writeStream.once("drain", resolve);
2337
- writeStream.once("error", reject);
2338
- });
2339
- }
2340
- }
2400
+ const pPath = path__default.default.join(uploadDir, `part_${i}`);
2401
+ if (!fs__default.default.existsSync(pPath)) {
2402
+ writeStream.destroy();
2403
+ throw new Error(`Could not finish upload: chunk ${i} is missing`);
2404
+ }
2405
+ const data = fs__default.default.readFileSync(pPath);
2406
+ const canContinue = writeStream.write(data);
2407
+ if (!canContinue) {
2341
2408
  await new Promise((resolve, reject) => {
2342
- if (streamError) {
2343
- reject(streamError);
2344
- return;
2345
- }
2346
- writeStream.end();
2347
- writeStream.on("finish", resolve);
2409
+ writeStream.once("drain", resolve);
2348
2410
  writeStream.once("error", reject);
2349
2411
  });
2350
- if (!fs__default.default.existsSync(finalTempPath)) {
2351
- throw new Error("Could not finish upload: failed to assemble the file");
2352
- }
2353
- const finalStats = fs__default.default.statSync(finalTempPath);
2354
- if (finalStats.size !== meta.fileSize) {
2355
- throw new Error("Could not finish upload: the assembled file is incomplete (size mismatch)");
2356
- }
2357
- const drive = new drive_default({
2358
- owner: meta.owner,
2359
- storageAccountId: meta.accountId || null,
2360
- provider: { type: meta.providerName },
2361
- name: meta.name,
2362
- parentId: meta.parentId,
2363
- order: 0,
2364
- information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
2365
- // path set by provider
2366
- status: "UPLOADING",
2367
- currentChunk: totalChunks,
2368
- totalChunks
2369
- });
2370
- if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
2371
- drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
2372
- }
2373
- await drive.save();
2374
- try {
2375
- const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
2376
- fs__default.default.rmSync(uploadDir, { recursive: true, force: true });
2377
- const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
2378
- res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item: addSignedUrlToken(item, config) }, statistic: { storage: newQuota } });
2379
- } catch (err) {
2380
- await drive_default.deleteOne({ _id: drive._id });
2381
- throw err;
2382
- }
2383
- } else {
2384
- const newQuota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2385
- if (chunkIndex === 0) {
2386
- res.status(200).json({ status: 200, message: "Upload started", data: { type: "UPLOAD_STARTED", driveId: currentUploadId }, statistic: { storage: newQuota } });
2387
- } else {
2388
- res.status(200).json({ status: 200, message: "Chunk received", data: { type: "CHUNK_RECEIVED", driveId: currentUploadId, chunkIndex }, statistic: { storage: newQuota } });
2389
- }
2390
2412
  }
2391
- } catch (e) {
2392
- cleanupTempFiles(files);
2393
- throw e;
2394
2413
  }
2395
- return;
2396
- }
2397
- cleanupTempFiles(files);
2398
- return res.status(400).json({ status: 400, message: "Could not upload: invalid upload request" });
2399
- }
2400
- // ** 4. CANCEL UPLOAD **
2401
- case "cancel": {
2402
- const cancelData = cancelQuerySchema.safeParse(req.query);
2403
- if (!cancelData.success) return res.status(400).json({ status: 400, message: "Could not cancel upload: invalid ID" });
2404
- const { id } = cancelData.data;
2405
- const tempUploadDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads", id);
2406
- if (fs__default.default.existsSync(tempUploadDir)) {
2414
+ await new Promise((resolve, reject) => {
2415
+ if (streamError) {
2416
+ reject(streamError);
2417
+ return;
2418
+ }
2419
+ writeStream.end();
2420
+ writeStream.on("finish", resolve);
2421
+ writeStream.once("error", reject);
2422
+ });
2423
+ if (!fs__default.default.existsSync(finalTempPath)) {
2424
+ throw new Error("Could not finish upload: failed to assemble the file");
2425
+ }
2426
+ const finalStats = fs__default.default.statSync(finalTempPath);
2427
+ if (finalStats.size !== meta.fileSize) {
2428
+ throw new Error("Could not finish upload: the assembled file is incomplete (size mismatch)");
2429
+ }
2430
+ const drive = new drive_default({
2431
+ owner: meta.owner,
2432
+ storageAccountId: meta.accountId || null,
2433
+ provider: { type: meta.providerName },
2434
+ name: meta.name,
2435
+ parentId: meta.parentId,
2436
+ order: 0,
2437
+ information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
2438
+ status: "UPLOADING",
2439
+ currentChunk: totalChunks,
2440
+ totalChunks,
2441
+ expiresAt: meta.unauthenticated ? new Date(Date.now() + (config.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4) : null
2442
+ });
2443
+ if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
2444
+ drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
2445
+ }
2446
+ await drive.save();
2407
2447
  try {
2408
- fs__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
2409
- } catch (e) {
2410
- console.error("Failed to cleanup temp upload:", e);
2448
+ const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
2449
+ fs__default.default.rmSync(uploadDir, { recursive: true, force: true });
2450
+ if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2451
+ const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
2452
+ res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item: withSignedUrl(item, config) }, statistic: { storage: newQuota } });
2453
+ } catch (err) {
2454
+ await drive_default.deleteOne({ _id: drive._id });
2455
+ if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2456
+ throw err;
2457
+ }
2458
+ } else {
2459
+ const newQuota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2460
+ if (chunkIndex === 0) {
2461
+ res.status(200).json({ status: 200, message: "Upload started", data: { type: "UPLOAD_STARTED", driveId: currentUploadId }, statistic: { storage: newQuota } });
2462
+ } else {
2463
+ res.status(200).json({ status: 200, message: "Chunk received", data: { type: "CHUNK_RECEIVED", driveId: currentUploadId, chunkIndex }, statistic: { storage: newQuota } });
2411
2464
  }
2412
2465
  }
2413
- return res.status(200).json({ status: 200, message: "Upload cancelled", data: null });
2414
- }
2415
- // ** 5. CREATE FOLDER **
2416
- case "createFolder": {
2417
- const folderData = createFolderBodySchema.safeParse(req.body);
2418
- if (!folderData.success) return res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
2419
- const { name, parentId } = folderData.data;
2420
- const item = addSignedUrlToken(await provider.createFolder(name, parentId ?? null, owner, accountId), config);
2421
- return res.status(201).json({ status: 201, message: "Folder created", data: { item } });
2466
+ } catch (e) {
2467
+ cleanupTempFiles(files);
2468
+ throw e;
2422
2469
  }
2423
- // ** 5. DELETE **
2424
- case "delete": {
2425
- const deleteData = deleteQuerySchema.safeParse(req.query);
2426
- if (!deleteData.success) return res.status(400).json({ status: 400, message: "Could not move to trash: invalid ID" });
2427
- const { id } = deleteData.data;
2428
- const drive = await drive_default.findById(id);
2429
- if (!drive) return res.status(404).json({ status: 404, message: "Could not move to trash: item not found" });
2430
- const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
2431
- const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
2470
+ return;
2471
+ }
2472
+ case "cancel": {
2473
+ const cancelData = cancelQuerySchema.safeParse(req.query);
2474
+ if (!cancelData.success) return void res.status(400).json({ status: 400, message: "Could not cancel upload: invalid ID" });
2475
+ const { id } = cancelData.data;
2476
+ const tempUploadDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads", id);
2477
+ if (fs__default.default.existsSync(tempUploadDir)) {
2432
2478
  try {
2433
- await itemProvider.trash([id], owner, itemAccountId);
2479
+ const metaPath = path__default.default.join(tempUploadDir, "metadata.json");
2480
+ if (fs__default.default.existsSync(metaPath) && JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8")).unauthenticated) {
2481
+ globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2482
+ }
2483
+ fs__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
2434
2484
  } catch (e) {
2435
- console.error("Provider trash failed:", e);
2485
+ console.error("Failed to cleanup temp upload:", e);
2436
2486
  }
2437
- drive.trashedAt = /* @__PURE__ */ new Date();
2438
- await drive.save();
2439
- return res.status(200).json({ status: 200, message: "Moved to trash", data: null });
2440
2487
  }
2441
- // ** 6. HARD DELETE **
2442
- case "deletePermanent": {
2443
- const deleteData = deleteQuerySchema.safeParse(req.query);
2444
- if (!deleteData.success) return res.status(400).json({ status: 400, message: "Could not delete: invalid ID" });
2445
- const { id } = deleteData.data;
2446
- await provider.delete([id], owner, accountId);
2447
- const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2448
- return res.status(200).json({ status: 200, message: "Deleted", statistic: { storage: quota } });
2488
+ res.status(200).json({ status: 200, message: "Upload cancelled", data: null });
2489
+ return;
2490
+ }
2491
+ case "createFolder": {
2492
+ const folderData = createFolderBodySchema.safeParse(req.body);
2493
+ if (!folderData.success) return void res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
2494
+ const { name, parentId } = folderData.data;
2495
+ const item = withSignedUrl(await provider.createFolder(name, parentId ?? null, owner, accountId), config);
2496
+ res.status(201).json({ status: 201, message: "Folder created", data: { item } });
2497
+ return;
2498
+ }
2499
+ case "delete": {
2500
+ const deleteData = deleteQuerySchema.safeParse(req.query);
2501
+ if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not move to trash: invalid ID" });
2502
+ const { id } = deleteData.data;
2503
+ const drive = await drive_default.findById(id);
2504
+ if (!drive) return void res.status(404).json({ status: 404, message: "Could not move to trash: item not found" });
2505
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
2506
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
2507
+ try {
2508
+ await itemProvider.trash([id], owner, itemAccountId);
2509
+ } catch (e) {
2510
+ console.error("Provider trash failed:", e);
2449
2511
  }
2450
- // ** 7. QUOTA **
2451
- case "quota": {
2452
- const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2453
- return res.status(200).json({
2454
- status: 200,
2455
- message: "Quota retrieved",
2456
- 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 },
2457
- statistic: { storage: quota }
2458
- });
2512
+ drive.trashedAt = /* @__PURE__ */ new Date();
2513
+ await drive.save();
2514
+ res.status(200).json({ status: 200, message: "Moved to trash", data: null });
2515
+ return;
2516
+ }
2517
+ case "deletePermanent": {
2518
+ const deleteData = deleteQuerySchema.safeParse(req.query);
2519
+ if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not delete: invalid ID" });
2520
+ const { id } = deleteData.data;
2521
+ await provider.delete([id], owner, accountId);
2522
+ const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2523
+ res.status(200).json({ status: 200, message: "Deleted", statistic: { storage: quota } });
2524
+ return;
2525
+ }
2526
+ case "quota": {
2527
+ const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2528
+ res.status(200).json({
2529
+ status: 200,
2530
+ message: "Quota retrieved",
2531
+ data: {
2532
+ usedInBytes: quota.usedInBytes,
2533
+ totalInBytes: quota.quotaInBytes,
2534
+ availableInBytes: Math.max(0, quota.quotaInBytes - quota.usedInBytes),
2535
+ percentage: quota.quotaInBytes > 0 ? Math.round(quota.usedInBytes / quota.quotaInBytes * 100) : 0
2536
+ },
2537
+ statistic: { storage: quota }
2538
+ });
2539
+ return;
2540
+ }
2541
+ case "trash": {
2542
+ try {
2543
+ const { provider: trashProvider, accountId: trashAccountId } = await resolveProvider(req, owner);
2544
+ await trashProvider.syncTrash(owner, trashAccountId);
2545
+ } catch (e) {
2546
+ console.error("Trash sync failed", e);
2459
2547
  }
2460
- // ** 7B. TRASH **
2461
- case "trash": {
2462
- try {
2463
- const { provider: trashProvider, accountId: trashAccountId } = await getProvider(req, owner);
2464
- await trashProvider.syncTrash(owner, trashAccountId);
2465
- } catch (e) {
2466
- console.error("Trash sync failed", e);
2548
+ const query = {
2549
+ owner,
2550
+ "provider.type": provider.name,
2551
+ storageAccountId: accountId || null,
2552
+ trashedAt: { $ne: null }
2553
+ };
2554
+ const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
2555
+ const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
2556
+ res.status(200).json({ status: 200, message: "Trash items", data: { items: plainItems, hasMore: false } });
2557
+ return;
2558
+ }
2559
+ case "restore": {
2560
+ const restoreData = deleteQuerySchema.safeParse(req.query);
2561
+ if (!restoreData.success) return void res.status(400).json({ status: 400, message: "Could not restore: invalid ID" });
2562
+ const { id } = restoreData.data;
2563
+ const drive = await drive_default.findById(id);
2564
+ if (!drive) return void res.status(404).json({ status: 404, message: "Could not restore: item not found" });
2565
+ let targetParentId = drive.parentId;
2566
+ if (targetParentId) {
2567
+ const parent = await drive_default.findById(targetParentId);
2568
+ if (parent?.trashedAt) {
2569
+ targetParentId = null;
2467
2570
  }
2468
- const query = {
2469
- owner,
2470
- "provider.type": provider.name,
2471
- storageAccountId: accountId || null,
2472
- trashedAt: { $ne: null }
2473
- };
2474
- const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
2475
- const plainItems = addSignedUrlTokens(await Promise.all(items.map((item) => item.toClient())), config);
2476
- return res.status(200).json({
2477
- status: 200,
2478
- message: "Trash items",
2479
- data: { items: plainItems, hasMore: false }
2480
- });
2481
2571
  }
2482
- // ** 7C. RESTORE **
2483
- case "restore": {
2484
- const restoreData = deleteQuerySchema.safeParse(req.query);
2485
- if (!restoreData.success) return res.status(400).json({ status: 400, message: "Could not restore: invalid ID" });
2486
- const { id } = restoreData.data;
2487
- const drive = await drive_default.findById(id);
2488
- if (!drive) return res.status(404).json({ status: 404, message: "Could not restore: item not found" });
2489
- let targetParentId = drive.parentId;
2490
- if (targetParentId) {
2491
- const parent = await drive_default.findById(targetParentId);
2492
- if (parent?.trashedAt) {
2493
- targetParentId = null;
2494
- }
2572
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
2573
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
2574
+ try {
2575
+ await itemProvider.untrash([id], owner, itemAccountId);
2576
+ if (targetParentId !== drive.parentId) {
2577
+ await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
2495
2578
  }
2496
- const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
2497
- const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
2579
+ } catch (e) {
2580
+ console.error("Provider restore failed:", e);
2581
+ }
2582
+ drive.trashedAt = null;
2583
+ drive.parentId = targetParentId;
2584
+ await drive.save();
2585
+ res.status(200).json({
2586
+ status: 200,
2587
+ message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
2588
+ data: null
2589
+ });
2590
+ return;
2591
+ }
2592
+ case "move": {
2593
+ const moveData = moveBodySchema.safeParse(req.body);
2594
+ if (!moveData.success) return void res.status(400).json({ status: 400, message: "Could not move: invalid request data" });
2595
+ const { ids, targetFolderId } = moveData.data;
2596
+ const items = [];
2597
+ const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
2598
+ for (const id of ids) {
2498
2599
  try {
2499
- await itemProvider.untrash([id], owner, itemAccountId);
2500
- if (targetParentId !== drive.parentId) {
2501
- await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
2502
- }
2600
+ const item = await provider.move(id, effectiveTargetId, owner, accountId);
2601
+ items.push(item);
2503
2602
  } catch (e) {
2504
- console.error("Provider restore failed:", e);
2603
+ console.error(`Failed to move item ${id}`, e);
2505
2604
  }
2506
- drive.trashedAt = null;
2507
- drive.parentId = targetParentId;
2508
- await drive.save();
2509
- return res.status(200).json({
2510
- status: 200,
2511
- message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
2512
- data: null
2513
- });
2514
2605
  }
2515
- // ** 7D. MOVE **
2516
- case "move": {
2517
- const moveData = moveBodySchema.safeParse(req.body);
2518
- if (!moveData.success) return res.status(400).json({ status: 400, message: "Could not move: invalid request data" });
2519
- const { ids, targetFolderId } = moveData.data;
2520
- const items = [];
2521
- const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
2522
- for (const id of ids) {
2523
- try {
2524
- const item = await provider.move(id, effectiveTargetId, owner, accountId);
2525
- items.push(item);
2526
- } catch (e) {
2527
- console.error(`Failed to move item ${id}`, e);
2528
- }
2529
- }
2530
- return res.status(200).json({ status: 200, message: "Moved", data: { items: addSignedUrlTokens(items, config) } });
2606
+ res.status(200).json({ status: 200, message: "Moved", data: { items: withSignedUrls(items, config) } });
2607
+ return;
2608
+ }
2609
+ case "reorder": {
2610
+ if (req.method !== "POST") {
2611
+ return void res.status(405).json({ status: 405, message: "Reordering requires a POST request" });
2531
2612
  }
2532
- // ** 8. RENAME **
2533
- case "rename": {
2534
- const renameData = renameBodySchema.safeParse({ id: req.query.id, ...req.body });
2535
- if (!renameData.success) return res.status(400).json({ status: 400, message: "Could not rename: invalid request data" });
2536
- const { id, newName } = renameData.data;
2537
- const item = addSignedUrlToken(await provider.rename(id, newName, owner, accountId), config);
2538
- return res.status(200).json({ status: 200, message: "Renamed", data: { item } });
2613
+ const reorderData = reorderBodySchema.safeParse(req.body);
2614
+ if (!reorderData.success) {
2615
+ return void res.status(400).json({ status: 400, message: "Could not reorder: invalid request data" });
2539
2616
  }
2540
- // ** 9. THUMBNAIL **
2541
- default:
2542
- res.status(400).json({ status: 400, message: `Unknown action requested: "${action}"` });
2617
+ const { ids } = reorderData.data;
2618
+ const query = {
2619
+ _id: { $in: ids },
2620
+ "provider.type": provider.name,
2621
+ storageAccountId: accountId || null,
2622
+ trashedAt: null
2623
+ };
2624
+ if (!isRootMode) {
2625
+ query.owner = owner;
2626
+ }
2627
+ const existingItems = await drive_default.find(query, { _id: 1, parentId: 1 });
2628
+ if (existingItems.length !== ids.length) {
2629
+ return void res.status(404).json({ status: 404, message: "Could not reorder: one or more items were not found" });
2630
+ }
2631
+ const parentIds = new Set(existingItems.map((item) => item.parentId ? item.parentId.toString() : "root"));
2632
+ if (parentIds.size > 1) {
2633
+ return void res.status(400).json({ status: 400, message: "Could not reorder: all items must be in the same folder" });
2634
+ }
2635
+ const operations = ids.map((id, order) => ({
2636
+ updateOne: {
2637
+ filter: {
2638
+ _id: id,
2639
+ "provider.type": provider.name,
2640
+ storageAccountId: accountId || null,
2641
+ trashedAt: null,
2642
+ ...isRootMode ? {} : { owner }
2643
+ },
2644
+ update: { $set: { order } }
2645
+ }
2646
+ }));
2647
+ await drive_default.bulkWrite(operations);
2648
+ const updatedItems = await drive_default.find(query, {}, { sort: { order: 1 } });
2649
+ const plainItems = withSignedUrls(await Promise.all(updatedItems.map((item) => item.toClient())), config);
2650
+ res.status(200).json({ status: 200, message: "Reordered", data: { items: plainItems } });
2651
+ return;
2652
+ }
2653
+ case "rename": {
2654
+ const renameData = renameBodySchema.safeParse({ id: req.query.id, ...req.body });
2655
+ if (!renameData.success) return void res.status(400).json({ status: 400, message: "Could not rename: invalid request data" });
2656
+ const { id, newName } = renameData.data;
2657
+ const item = withSignedUrl(await provider.rename(id, newName, owner, accountId), config);
2658
+ res.status(200).json({ status: 200, message: "Renamed", data: { item } });
2659
+ return;
2660
+ }
2661
+ default: {
2662
+ res.status(400).json({ status: 400, message: `Unknown action requested: "${action}"` });
2663
+ return;
2664
+ }
2665
+ }
2666
+ };
2667
+
2668
+ // src/server/index.ts
2669
+ var driveAPIHandler = async (req, res) => {
2670
+ const action = req.query.action || (req.query.code && req.query.state ? "callback" : void 0);
2671
+ let config;
2672
+ try {
2673
+ config = getDriveConfig();
2674
+ } catch (error) {
2675
+ console.error("[next-drive] Configuration error:", error);
2676
+ res.status(500).json({ status: 500, message: "Drive is not ready: failed to initialize configuration" });
2677
+ return;
2678
+ }
2679
+ const isPreflightHandled = applyCorsHeaders(req, res, config);
2680
+ if (isPreflightHandled) return;
2681
+ if (!action) {
2682
+ res.status(400).json({ status: 400, message: 'Missing "action" parameter in request' });
2683
+ return;
2684
+ }
2685
+ const wasPublicHandled = await handlePublicAction(req, res, action, config);
2686
+ if (wasPublicHandled) return;
2687
+ try {
2688
+ const mode = config.mode || "NORMAL";
2689
+ if (action === "information") {
2690
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
2691
+ const googleConfigured = !!(clientId && clientSecret && redirectUri);
2692
+ let authenticated = false;
2693
+ try {
2694
+ await getDriveInformation({ method: "REQUEST", req });
2695
+ authenticated = true;
2696
+ } catch {
2697
+ authenticated = false;
2698
+ }
2699
+ res.status(200).json({
2700
+ status: 200,
2701
+ message: "Information retrieved",
2702
+ data: {
2703
+ providers: {
2704
+ google: googleConfigured
2705
+ },
2706
+ mode,
2707
+ authenticated,
2708
+ unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
2709
+ }
2710
+ });
2711
+ return;
2543
2712
  }
2713
+ const information = await getDriveInformation({ method: "REQUEST", req });
2714
+ const { key: owner } = information;
2715
+ const isRootMode = mode === "ROOT";
2716
+ const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
2717
+ if (wasAuthHandled) return;
2718
+ const { provider, accountId } = await resolveProvider(req, owner);
2719
+ await handleDriveAction({
2720
+ req,
2721
+ res,
2722
+ action,
2723
+ config,
2724
+ owner,
2725
+ isRootMode,
2726
+ information,
2727
+ provider,
2728
+ accountId
2729
+ });
2544
2730
  } catch (error) {
2545
2731
  console.error(`[next-drive] Error handling action ${action}:`, error);
2546
- res.status(500).json({ status: 500, message: error instanceof Error ? error.message : "Something went wrong while processing your request" });
2732
+ const detail = error instanceof Error ? error.message : "Something went wrong while processing your request";
2733
+ res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
2547
2734
  }
2548
2735
  };
2549
2736
 
2550
2737
  exports.driveAPIHandler = driveAPIHandler;
2551
2738
  exports.driveCleanup = driveCleanup;
2552
2739
  exports.driveConfiguration = driveConfiguration;
2740
+ exports.driveConfirm = driveConfirm;
2553
2741
  exports.driveDelete = driveDelete;
2554
2742
  exports.driveFilePath = driveFilePath;
2555
2743
  exports.driveFileSchemaZod = driveFileSchemaZod;
@@ -2557,10 +2745,11 @@ exports.driveGetUrl = driveGetUrl;
2557
2745
  exports.driveInfo = driveInfo;
2558
2746
  exports.driveList = driveList;
2559
2747
  exports.driveListFiles = driveListFiles;
2748
+ exports.drivePurgeExpired = drivePurgeExpired;
2560
2749
  exports.driveReadFile = driveReadFile;
2561
2750
  exports.driveUpload = driveUpload;
2562
2751
  exports.drive_default = drive_default;
2563
2752
  exports.getDriveConfig = getDriveConfig;
2564
2753
  exports.getDriveInformation = getDriveInformation;
2565
- //# sourceMappingURL=chunk-LAKT7IJJ.cjs.map
2566
- //# sourceMappingURL=chunk-LAKT7IJJ.cjs.map
2754
+ //# sourceMappingURL=chunk-V75PCJHT.cjs.map
2755
+ //# sourceMappingURL=chunk-V75PCJHT.cjs.map