@muhgholy/next-drive 4.23.6 → 4.23.8

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 (91) hide show
  1. package/dist/{chunk-TA6L5FYG.cjs → chunk-HQTC3554.cjs} +47 -2
  2. package/dist/chunk-HQTC3554.cjs.map +1 -0
  3. package/dist/{chunk-TMSG5WJZ.js → chunk-OSYRIHH4.js} +47 -3
  4. package/dist/chunk-OSYRIHH4.js.map +1 -0
  5. package/dist/{chunk-VIB7R4JN.cjs → chunk-OU5TKLHV.cjs} +929 -832
  6. package/dist/chunk-OU5TKLHV.cjs.map +1 -0
  7. package/dist/{chunk-GV4HB2G6.js → chunk-RBSFEEJJ.js} +926 -829
  8. package/dist/chunk-RBSFEEJJ.js.map +1 -0
  9. package/dist/client/components/drive/{RenameAccountDialog.d.ts → account/rename.d.ts} +2 -2
  10. package/dist/client/components/drive/account/rename.d.ts.map +1 -0
  11. package/dist/client/components/drive/{dnd-provider.d.ts → dnd/context.d.ts} +1 -1
  12. package/dist/client/components/drive/dnd/context.d.ts.map +1 -0
  13. package/dist/client/components/drive/{CreateFolderDialog.d.ts → folder/create.d.ts} +2 -2
  14. package/dist/client/components/drive/folder/create.d.ts.map +1 -0
  15. package/dist/client/components/drive/{RenameDialog.d.ts → item/rename.d.ts} +3 -3
  16. package/dist/client/components/drive/item/rename.d.ts.map +1 -0
  17. package/dist/client/components/{dialog.d.ts → shared/confirm.d.ts} +2 -2
  18. package/dist/client/components/shared/confirm.d.ts.map +1 -0
  19. package/dist/client/components/ui/{alert-dialog.d.ts → alert-modal.d.ts} +1 -1
  20. package/dist/client/components/ui/alert-modal.d.ts.map +1 -0
  21. package/dist/client/components/ui/{dialog-fullscreen.d.ts → fullscreen.d.ts} +1 -1
  22. package/dist/client/components/ui/fullscreen.d.ts.map +1 -0
  23. package/dist/client/components/ui/{dialog.d.ts → modal.d.ts} +1 -1
  24. package/dist/client/components/ui/modal.d.ts.map +1 -0
  25. package/dist/client/context.d.ts.map +1 -1
  26. package/dist/client/file-chooser.d.ts.map +1 -1
  27. package/dist/client/hooks/{useUpload.d.ts → use-upload.d.ts} +1 -1
  28. package/dist/client/hooks/use-upload.d.ts.map +1 -0
  29. package/dist/client/index.cjs +351 -279
  30. package/dist/client/index.cjs.map +1 -1
  31. package/dist/client/index.css +1 -1
  32. package/dist/client/index.d.ts +12 -11
  33. package/dist/client/index.d.ts.map +1 -1
  34. package/dist/client/index.js +245 -173
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/utils.d.ts +1 -0
  37. package/dist/client/utils.d.ts.map +1 -1
  38. package/dist/server/actions/auth.d.ts +4 -0
  39. package/dist/server/actions/auth.d.ts.map +1 -0
  40. package/dist/server/actions/cors.d.ts +4 -0
  41. package/dist/server/actions/cors.d.ts.map +1 -0
  42. package/dist/server/actions/drive.d.ts +18 -0
  43. package/dist/server/actions/drive.d.ts.map +1 -0
  44. package/dist/server/actions/public.d.ts +4 -0
  45. package/dist/server/actions/public.d.ts.map +1 -0
  46. package/dist/server/actions/shared.d.ts +14 -0
  47. package/dist/server/actions/shared.d.ts.map +1 -0
  48. package/dist/server/express.cjs +12 -12
  49. package/dist/server/express.js +3 -3
  50. package/dist/server/hono.cjs +12 -12
  51. package/dist/server/hono.js +3 -3
  52. package/dist/server/index.cjs +18 -18
  53. package/dist/server/index.d.ts +1 -1
  54. package/dist/server/index.d.ts.map +1 -1
  55. package/dist/server/index.js +2 -2
  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/types/lib/database/index.d.ts +2 -2
  67. package/dist/types/lib/database/index.d.ts.map +1 -1
  68. package/dist/types/server/index.d.ts +5 -5
  69. package/dist/types/server/index.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/dist/chunk-GV4HB2G6.js.map +0 -1
  72. package/dist/chunk-TA6L5FYG.cjs.map +0 -1
  73. package/dist/chunk-TMSG5WJZ.js.map +0 -1
  74. package/dist/chunk-VIB7R4JN.cjs.map +0 -1
  75. package/dist/client/components/dialog.d.ts.map +0 -1
  76. package/dist/client/components/drive/CreateFolderDialog.d.ts.map +0 -1
  77. package/dist/client/components/drive/RenameAccountDialog.d.ts.map +0 -1
  78. package/dist/client/components/drive/RenameDialog.d.ts.map +0 -1
  79. package/dist/client/components/drive/dnd-provider.d.ts.map +0 -1
  80. package/dist/client/components/ui/alert-dialog.d.ts.map +0 -1
  81. package/dist/client/components/ui/dialog-fullscreen.d.ts.map +0 -1
  82. package/dist/client/components/ui/dialog.d.ts.map +0 -1
  83. package/dist/client/hooks/useUpload.d.ts.map +0 -1
  84. package/dist/server/providers/google.d.ts.map +0 -1
  85. package/dist/server/providers/local.d.ts.map +0 -1
  86. package/dist/server/security/cryptoUtils.d.ts.map +0 -1
  87. package/dist/server/security/mimeFilter.d.ts.map +0 -1
  88. package/dist/server/utils/folderValidation.d.ts.map +0 -1
  89. package/dist/server/utils/imageConvert.d.ts.map +0 -1
  90. /package/dist/server/{providers → storage-adapters}/google.d.ts +0 -0
  91. /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 },
@@ -337,6 +337,53 @@ var getDriveInformation = async (input) => {
337
337
  }
338
338
  return config.information(input);
339
339
  };
340
+
341
+ // src/server/actions/cors.ts
342
+ var applyCorsHeaders = (req, res, config) => {
343
+ const cors = config.cors;
344
+ if (!cors?.enabled) return false;
345
+ const origin = req.headers.origin;
346
+ const allowedOrigins = cors.origins ?? "*";
347
+ const methods = cors.methods ?? ["GET", "POST", "PUT", "DELETE", "OPTIONS"];
348
+ const allowedHeaders = cors.allowedHeaders ?? ["Content-Type", "Authorization", "X-Drive-Account"];
349
+ const exposedHeaders = cors.exposedHeaders ?? ["Content-Length", "Content-Type", "Content-Disposition"];
350
+ const credentials = cors.credentials ?? false;
351
+ const maxAge = cors.maxAge ?? 86400;
352
+ let allowOrigin = null;
353
+ if (origin) {
354
+ if (allowedOrigins === "*") {
355
+ allowOrigin = origin;
356
+ } else if (Array.isArray(allowedOrigins)) {
357
+ if (allowedOrigins.includes(origin)) {
358
+ allowOrigin = origin;
359
+ }
360
+ } else if (allowedOrigins === origin) {
361
+ allowOrigin = origin;
362
+ }
363
+ } else if (allowedOrigins === "*") {
364
+ allowOrigin = "*";
365
+ }
366
+ if (!allowOrigin) {
367
+ if (req.method === "OPTIONS") {
368
+ res.status(403).end();
369
+ return true;
370
+ }
371
+ return false;
372
+ }
373
+ res.setHeader("Access-Control-Allow-Origin", allowOrigin);
374
+ res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
375
+ res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
376
+ res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(", "));
377
+ res.setHeader("Access-Control-Max-Age", maxAge.toString());
378
+ if (credentials) {
379
+ res.setHeader("Access-Control-Allow-Credentials", "true");
380
+ }
381
+ if (req.method === "OPTIONS") {
382
+ res.status(204).end();
383
+ return true;
384
+ }
385
+ return false;
386
+ };
340
387
  var validateMimeType = (mime, allowedTypes) => {
341
388
  if (allowedTypes.includes("*/*")) return true;
342
389
  return allowedTypes.some((pattern) => {
@@ -349,7 +396,7 @@ var validateMimeType = (mime, allowedTypes) => {
349
396
  });
350
397
  };
351
398
  var computeFileHash = (filePath) => new Promise((resolve, reject) => {
352
- const hash = crypto2__default.default.createHash("sha256");
399
+ const hash = crypto3__default.default.createHash("sha256");
353
400
  const stream = fs__default.default.createReadStream(filePath);
354
401
  stream.on("data", (data) => hash.update(data));
355
402
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -500,6 +547,12 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size, fit, posi
500
547
  ...resolvedPosition && { position: resolvedPosition }
501
548
  };
502
549
  };
550
+
551
+ // src/server/security/crypto-utils.ts
552
+ function sanitizeContentDispositionFilename(filename) {
553
+ const basename = filename.replace(/^.*[\\\/]/, "");
554
+ return basename.replace(/["\r\n]/g, "").replace(/[^\x20-\x7E]/g, "").slice(0, 255);
555
+ }
503
556
  var generatePlaceholderThumbnail = async (outputPath, mimeType) => {
504
557
  const typeParts = mimeType.split("/");
505
558
  const subtype = typeParts[1] || "file";
@@ -542,11 +595,11 @@ var LocalStorageProvider = {
542
595
  return { usedInBytes, quotaInBytes: configuredQuotaInBytes ?? 0 };
543
596
  },
544
597
  openStream: async (item, accountId) => {
545
- if (item.information.type !== "FILE") throw new Error("Cannot stream folder");
598
+ if (item.information.type !== "FILE") throw new Error("Could not open local file: folders cannot be streamed");
546
599
  const storagePath = getDriveConfig().storage.path;
547
600
  const filePath = path__default.default.join(storagePath, "file", item._id.toString(), "data.bin");
548
601
  if (!fs__default.default.existsSync(filePath)) {
549
- throw new Error("File not found on disk");
602
+ throw new Error("Could not open local file: it is missing from disk");
550
603
  }
551
604
  const stat = fs__default.default.statSync(filePath);
552
605
  const stream = fs__default.default.createReadStream(filePath);
@@ -557,13 +610,13 @@ var LocalStorageProvider = {
557
610
  };
558
611
  },
559
612
  getThumbnail: async (item, accountId) => {
560
- if (item.information.type !== "FILE") throw new Error("No thumbnail for folder");
613
+ if (item.information.type !== "FILE") throw new Error("No preview available: folders do not have thumbnails");
561
614
  const storagePath = getDriveConfig().storage.path;
562
615
  const fileDir = path__default.default.join(storagePath, "file", item._id.toString());
563
616
  const originalPath = path__default.default.join(fileDir, "data.bin");
564
617
  const thumbDir = path__default.default.join(fileDir, "cache");
565
618
  const thumbPath = path__default.default.join(thumbDir, "thumbnail.webp");
566
- if (!fs__default.default.existsSync(originalPath)) throw new Error("Original file not found");
619
+ if (!fs__default.default.existsSync(originalPath)) throw new Error("Could not generate preview: the original file is missing");
567
620
  if (fs__default.default.existsSync(thumbPath)) {
568
621
  return fs__default.default.createReadStream(thumbPath);
569
622
  }
@@ -612,12 +665,12 @@ var LocalStorageProvider = {
612
665
  return folder.toClient();
613
666
  },
614
667
  uploadFile: async (drive, filePath, accountId) => {
615
- if (drive.information.type !== "FILE") throw new Error("Invalid drive type");
668
+ if (drive.information.type !== "FILE") throw new Error("Could not save local file: invalid file record");
616
669
  const storagePath = getDriveConfig().storage.path;
617
670
  const destDir = path__default.default.join(storagePath, "file", String(drive._id));
618
671
  const destPath = path__default.default.join(destDir, "data.bin");
619
672
  if (!fs__default.default.existsSync(filePath)) {
620
- throw new Error("Source file not found");
673
+ throw new Error("Could not save local file: the uploaded data is missing");
621
674
  }
622
675
  if (!fs__default.default.existsSync(destDir)) {
623
676
  fs__default.default.mkdirSync(destDir, { recursive: true });
@@ -633,12 +686,12 @@ var LocalStorageProvider = {
633
686
  }
634
687
  }
635
688
  if (!fs__default.default.existsSync(destPath)) {
636
- throw new Error("Failed to write file to destination");
689
+ throw new Error("Could not save local file: writing to storage failed");
637
690
  }
638
691
  const destStats = fs__default.default.statSync(destPath);
639
692
  if (destStats.size !== drive.information.sizeInBytes) {
640
693
  fs__default.default.unlinkSync(destPath);
641
- throw new Error(`Destination file size mismatch: expected ${drive.information.sizeInBytes}, got ${destStats.size}`);
694
+ throw new Error("Could not save local file: the stored data was incomplete (size mismatch)");
642
695
  }
643
696
  drive.status = "READY";
644
697
  drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
@@ -683,12 +736,12 @@ var LocalStorageProvider = {
683
736
  },
684
737
  rename: async (id, newName, owner, accountId) => {
685
738
  const item = await drive_default.findOneAndUpdate({ _id: id, owner }, { name: newName }, { new: true });
686
- if (!item) throw new Error("Item not found");
739
+ if (!item) throw new Error("Could not rename: the item no longer exists");
687
740
  return item.toClient();
688
741
  },
689
742
  move: async (id, newParentId, owner, accountId) => {
690
743
  const item = await drive_default.findOne({ _id: id, owner });
691
- if (!item) throw new Error("Item not found");
744
+ if (!item) throw new Error("Could not move: the item no longer exists");
692
745
  item.parentId = newParentId === "root" || !newParentId ? null : new mongoose__default.default.Types.ObjectId(newParentId);
693
746
  await item.save();
694
747
  return item.toClient();
@@ -726,18 +779,18 @@ StorageAccountSchema.method("toClient", async function() {
726
779
  var StorageAccount = mongoose__default.default.models.StorageAccount || mongoose__default.default.model("StorageAccount", StorageAccountSchema);
727
780
  var account_default = StorageAccount;
728
781
 
729
- // src/server/providers/google.ts
782
+ // src/server/storage-adapters/google.ts
730
783
  var createAuthClient = async (owner, accountId) => {
731
784
  const query = { owner, "metadata.provider": "GOOGLE" };
732
785
  if (accountId) query._id = accountId;
733
786
  const account = await account_default.findOne(query);
734
- if (!account) throw new Error("Google Drive account not connected");
787
+ if (!account) throw new Error("Could not reach Google Drive: account not connected");
735
788
  const config = getDriveConfig();
736
789
  const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
737
- if (!clientId || !clientSecret) throw new Error("Google credentials not configured on server");
790
+ if (!clientId || !clientSecret) throw new Error("Could not reach Google Drive: Google credentials are not configured on the server");
738
791
  const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, redirectUri);
739
792
  if (account.metadata.provider !== "GOOGLE" || !account.metadata.google) {
740
- throw new Error("Invalid Google Account Metadata");
793
+ throw new Error("Could not reach Google Drive: account data is invalid, please reconnect");
741
794
  }
742
795
  oAuth2Client.setCredentials(account.metadata.google.credentials);
743
796
  oAuth2Client.on("tokens", async (tokens) => {
@@ -959,8 +1012,8 @@ var GoogleDriveProvider = {
959
1012
  openStream: async (item, accountId) => {
960
1013
  const { client } = await createAuthClient(item.owner, accountId || item.storageAccountId?.toString());
961
1014
  const drive = googleapis.google.drive({ version: "v3", auth: client });
962
- if (!item.provider?.google?.id) throw new Error("Missing Google File ID");
963
- if (item.information.type === "FOLDER") throw new Error("Cannot stream folder");
1015
+ if (!item.provider?.google?.id) throw new Error("Could not open Google Drive file: its Google file ID is missing");
1016
+ if (item.information.type === "FOLDER") throw new Error("Could not open Google Drive file: folders cannot be streamed");
964
1017
  const res = await drive.files.get(
965
1018
  { fileId: item.provider.google.id, alt: "media" },
966
1019
  { responseType: "stream" }
@@ -980,7 +1033,7 @@ var GoogleDriveProvider = {
980
1033
  return fs__default.default.createReadStream(thumbPath);
981
1034
  }
982
1035
  const { client } = await createAuthClient(item.owner, accountId || item.storageAccountId?.toString());
983
- if (!item.provider?.google?.thumbnailLink) throw new Error("No thumbnail available");
1036
+ if (!item.provider?.google?.thumbnailLink) throw new Error("No preview available for this Google Drive file");
984
1037
  const res = await client.request({ url: item.provider.google.thumbnailLink, responseType: "stream" });
985
1038
  if (!fs__default.default.existsSync(thumbDir)) {
986
1039
  fs__default.default.mkdirSync(thumbDir, { recursive: true });
@@ -1020,7 +1073,7 @@ var GoogleDriveProvider = {
1020
1073
  fields: "id, name, mimeType, webViewLink, iconLink"
1021
1074
  });
1022
1075
  const file = res.data;
1023
- if (!file.id) throw new Error("Failed to create folder on Google Drive");
1076
+ if (!file.id) throw new Error("Could not create folder on Google Drive");
1024
1077
  const folder = new drive_default({
1025
1078
  owner,
1026
1079
  name: file.name,
@@ -1041,7 +1094,7 @@ var GoogleDriveProvider = {
1041
1094
  return folder.toClient();
1042
1095
  },
1043
1096
  uploadFile: async (drive, filePath, accountId) => {
1044
- if (drive.information.type !== "FILE") throw new Error("Invalid drive type");
1097
+ if (drive.information.type !== "FILE") throw new Error("Could not upload to Google Drive: invalid file record");
1045
1098
  const { client } = await createAuthClient(drive.owner, accountId || drive.storageAccountId?.toString());
1046
1099
  const googleDrive = googleapis.google.drive({ version: "v3", auth: client });
1047
1100
  let googleParentId = "root";
@@ -1063,7 +1116,7 @@ var GoogleDriveProvider = {
1063
1116
  fields: "id, name, mimeType, webViewLink, iconLink, thumbnailLink, size"
1064
1117
  });
1065
1118
  const gFile = res.data;
1066
- if (!gFile.id) throw new Error("Upload to Google Drive failed");
1119
+ if (!gFile.id) throw new Error("Could not upload to Google Drive: no file was created");
1067
1120
  drive.status = "READY";
1068
1121
  drive.provider = {
1069
1122
  type: "GOOGLE",
@@ -1135,7 +1188,7 @@ var GoogleDriveProvider = {
1135
1188
  const { client } = await createAuthClient(owner, accountId);
1136
1189
  const drive = googleapis.google.drive({ version: "v3", auth: client });
1137
1190
  const item = await drive_default.findOne({ _id: id, owner });
1138
- if (!item || !item.provider?.google?.id) throw new Error("Item not found");
1191
+ if (!item || !item.provider?.google?.id) throw new Error("Could not rename on Google Drive: item not found or not synced");
1139
1192
  await drive.files.update({
1140
1193
  fileId: item.provider.google.id,
1141
1194
  requestBody: { name: newName }
@@ -1148,7 +1201,7 @@ var GoogleDriveProvider = {
1148
1201
  const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
1149
1202
  const drive = googleapis.google.drive({ version: "v3", auth: client });
1150
1203
  const item = await drive_default.findOne({ _id: id, owner });
1151
- if (!item || !item.provider?.google?.id) throw new Error("Item not found or not synced");
1204
+ if (!item || !item.provider?.google?.id) throw new Error("Could not move on Google Drive: item not found or not synced");
1152
1205
  let previousGoogleParentId = void 0;
1153
1206
  if (item.parentId) {
1154
1207
  const oldParent = await drive_default.findOne({ _id: item.parentId, owner });
@@ -1168,7 +1221,7 @@ var GoogleDriveProvider = {
1168
1221
  let newGoogleParentId = "root";
1169
1222
  if (newParentId && newParentId !== "root") {
1170
1223
  const newParent = await drive_default.findOne({ _id: newParentId, owner });
1171
- if (!newParent || !newParent.provider?.google?.id) throw new Error("Target folder not found in Google Drive");
1224
+ if (!newParent || !newParent.provider?.google?.id) throw new Error("Could not move on Google Drive: target folder not found");
1172
1225
  newGoogleParentId = newParent.provider.google.id;
1173
1226
  }
1174
1227
  await drive.files.update({
@@ -1194,7 +1247,367 @@ var GoogleDriveProvider = {
1194
1247
  }
1195
1248
  };
1196
1249
 
1197
- // src/server/controllers/drive.ts
1250
+ // src/server/actions/public.ts
1251
+ var handlePublicAction = async (req, res, action, config) => {
1252
+ if (action !== "serve" && action !== "thumbnail") {
1253
+ return false;
1254
+ }
1255
+ try {
1256
+ const { id, token } = req.query;
1257
+ if (!id || typeof id !== "string") {
1258
+ res.status(400).json({ status: 400, message: "Could not open file: missing or invalid file ID" });
1259
+ return true;
1260
+ }
1261
+ const drive = await drive_default.findById(id);
1262
+ if (!drive) {
1263
+ res.status(404).json({ status: 404, message: "File not found or no longer available" });
1264
+ return true;
1265
+ }
1266
+ if (config.security?.signedUrls?.enabled) {
1267
+ if (!token || typeof token !== "string") {
1268
+ res.status(401).json({ status: 401, message: "Access denied: this link is missing its access token" });
1269
+ return true;
1270
+ }
1271
+ try {
1272
+ const decoded = Buffer.from(token, "base64url").toString();
1273
+ const [expiryStr, signature] = decoded.split(":");
1274
+ const expiry = parseInt(expiryStr, 10);
1275
+ if (Date.now() / 1e3 > expiry) {
1276
+ res.status(401).json({ status: 401, message: "Access denied: this link has expired" });
1277
+ return true;
1278
+ }
1279
+ const { secret } = config.security.signedUrls;
1280
+ const expectedSignature = crypto3__default.default.createHmac("sha256", secret).update(`${id}:${expiry}`).digest("hex");
1281
+ if (signature !== expectedSignature) {
1282
+ res.status(401).json({ status: 401, message: "Access denied: this link's access token is invalid" });
1283
+ return true;
1284
+ }
1285
+ } catch {
1286
+ res.status(401).json({ status: 401, message: "Access denied: this link's access token is malformed" });
1287
+ return true;
1288
+ }
1289
+ }
1290
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1291
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
1292
+ if (action === "thumbnail") {
1293
+ const stream2 = await itemProvider.getThumbnail(drive, itemAccountId);
1294
+ res.setHeader("Content-Type", "image/webp");
1295
+ if (config.cors?.enabled) {
1296
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1297
+ }
1298
+ stream2.pipe(res);
1299
+ return true;
1300
+ }
1301
+ const { stream, mime, size: fileSize } = await itemProvider.openStream(drive, itemAccountId);
1302
+ const safeFilename = sanitizeContentDispositionFilename(drive.name);
1303
+ const format = req.query.format;
1304
+ const quality = req.query.quality;
1305
+ const display = req.query.display;
1306
+ const sizePreset = req.query.size;
1307
+ const fit = req.query.fit;
1308
+ const position = req.query.position;
1309
+ const isImage = mime.startsWith("image/");
1310
+ const shouldTransform = isImage && (format || quality || display || sizePreset || fit);
1311
+ res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
1312
+ if (config.cors?.enabled) {
1313
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1314
+ }
1315
+ if (shouldTransform) {
1316
+ try {
1317
+ const settings = getImageSettings(fileSize, quality, display, sizePreset, fit, position);
1318
+ let targetFormat = format || mime.split("/")[1];
1319
+ if (targetFormat === "jpg") targetFormat = "jpeg";
1320
+ if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
1321
+ targetFormat = format || "webp";
1322
+ }
1323
+ const cacheDir = path__default.default.join(config.storage.path, "file", drive._id.toString(), "cache");
1324
+ const cacheKey = [
1325
+ "opt",
1326
+ `q${settings.quality}`,
1327
+ `e${settings.effort}`,
1328
+ settings.width ? `${settings.width}x${settings.height}` : "orig",
1329
+ settings.fit || "none",
1330
+ settings.position || "c",
1331
+ targetFormat
1332
+ ].join("_");
1333
+ const cachePath = path__default.default.join(cacheDir, `${cacheKey}.bin`);
1334
+ if (fs__default.default.existsSync(cachePath)) {
1335
+ const cacheStat = fs__default.default.statSync(cachePath);
1336
+ res.setHeader("Content-Type", `image/${targetFormat}`);
1337
+ res.setHeader("Content-Length", cacheStat.size);
1338
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1339
+ if (config.cors?.enabled) {
1340
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1341
+ }
1342
+ if ("destroy" in stream) {
1343
+ stream.destroy();
1344
+ }
1345
+ fs__default.default.createReadStream(cachePath).pipe(res);
1346
+ return true;
1347
+ }
1348
+ if (!fs__default.default.existsSync(cacheDir)) fs__default.default.mkdirSync(cacheDir, { recursive: true });
1349
+ let pipeline = sharp2__default.default();
1350
+ if (settings.width && settings.height) {
1351
+ pipeline = pipeline.resize(settings.width, settings.height, {
1352
+ fit: settings.fit || "inside",
1353
+ position: settings.position || "center",
1354
+ withoutEnlargement: true,
1355
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
1356
+ });
1357
+ }
1358
+ if (targetFormat === "jpeg") {
1359
+ pipeline = pipeline.jpeg({ quality: settings.quality, mozjpeg: true });
1360
+ res.setHeader("Content-Type", "image/jpeg");
1361
+ } else if (targetFormat === "png") {
1362
+ pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
1363
+ res.setHeader("Content-Type", "image/png");
1364
+ } else if (targetFormat === "webp") {
1365
+ const webpEffort = Math.min(settings.effort, 6);
1366
+ pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
1367
+ res.setHeader("Content-Type", "image/webp");
1368
+ } else if (targetFormat === "avif") {
1369
+ pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
1370
+ res.setHeader("Content-Type", "image/avif");
1371
+ }
1372
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1373
+ pipeline.on("error", (err) => {
1374
+ console.error("[next-drive] Pipeline error:", err);
1375
+ });
1376
+ stream.pipe(pipeline);
1377
+ pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
1378
+ pipeline.clone().pipe(res);
1379
+ return true;
1380
+ } catch (e) {
1381
+ console.error("[next-drive] Image transformation failed:", e);
1382
+ }
1383
+ }
1384
+ res.setHeader("Content-Type", mime);
1385
+ if (fileSize) res.setHeader("Content-Length", fileSize);
1386
+ stream.pipe(res);
1387
+ return true;
1388
+ } catch (error) {
1389
+ console.error(`[next-drive] Error in ${action}:`, error);
1390
+ const detail = error instanceof Error ? error.message : "Something went wrong while serving the file";
1391
+ res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
1392
+ return true;
1393
+ }
1394
+ };
1395
+ var handleAuthAction = async (req, res, action, config, owner) => {
1396
+ if (!["getAuthUrl", "callback", "listAccounts", "removeAccount"].includes(action)) {
1397
+ return false;
1398
+ }
1399
+ switch (action) {
1400
+ case "getAuthUrl": {
1401
+ const { provider } = req.query;
1402
+ if (provider === "GOOGLE") {
1403
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
1404
+ if (!clientId || !clientSecret || !redirectUri) {
1405
+ res.status(500).json({ status: 500, message: "Google Drive is not configured on the server" });
1406
+ return true;
1407
+ }
1408
+ const callbackUri = new URL(redirectUri);
1409
+ callbackUri.searchParams.set("action", "callback");
1410
+ const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
1411
+ const state = Buffer.from(JSON.stringify({ owner })).toString("base64");
1412
+ const url = oAuth2Client.generateAuthUrl({
1413
+ access_type: "offline",
1414
+ scope: ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email"],
1415
+ state,
1416
+ prompt: "consent"
1417
+ });
1418
+ res.status(200).json({ status: 200, message: "Auth URL generated", data: { url } });
1419
+ return true;
1420
+ }
1421
+ res.status(400).json({ status: 400, message: "Unknown storage provider requested" });
1422
+ return true;
1423
+ }
1424
+ case "callback": {
1425
+ const { code } = req.query;
1426
+ if (!code) {
1427
+ res.status(400).json({ status: 400, message: "Google sign-in failed: authorization code missing" });
1428
+ return true;
1429
+ }
1430
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
1431
+ if (!clientId || !clientSecret || !redirectUri) {
1432
+ res.status(500).json({ status: 500, message: "Google Drive sign-in is not configured on the server" });
1433
+ return true;
1434
+ }
1435
+ const callbackUri = new URL(redirectUri);
1436
+ callbackUri.searchParams.set("action", "callback");
1437
+ const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
1438
+ const { tokens } = await oAuth2Client.getToken(code);
1439
+ oAuth2Client.setCredentials(tokens);
1440
+ const oauth2 = googleapis.google.oauth2({ version: "v2", auth: oAuth2Client });
1441
+ const userInfo = await oauth2.userinfo.get();
1442
+ const existing = await account_default.findOne({ owner, "metadata.google.email": userInfo.data.email, "metadata.provider": "GOOGLE" });
1443
+ if (existing) {
1444
+ existing.metadata.google.credentials = tokens;
1445
+ existing.markModified("metadata");
1446
+ await existing.save();
1447
+ } else {
1448
+ const newAccount = new account_default({
1449
+ owner,
1450
+ name: userInfo.data.name || "Google Drive",
1451
+ metadata: {
1452
+ provider: "GOOGLE",
1453
+ google: {
1454
+ email: userInfo.data.email,
1455
+ credentials: tokens
1456
+ }
1457
+ }
1458
+ });
1459
+ await newAccount.save();
1460
+ }
1461
+ res.setHeader("Content-Type", "text/html");
1462
+ res.send(`<!DOCTYPE html>
1463
+ <html>
1464
+ <head><title>Authentication Complete</title></head>
1465
+ <body>
1466
+ <p>Authentication successful! This window will close automatically.</p>
1467
+ <script>
1468
+ (function() {
1469
+ if (window.opener) {
1470
+ try {
1471
+ window.opener.postMessage('oauth-success', '*');
1472
+ } catch (e) {}
1473
+ }
1474
+ try {
1475
+ localStorage.setItem('next-drive-oauth-success', Date.now().toString());
1476
+ localStorage.removeItem('next-drive-oauth-success');
1477
+ } catch (e) {}
1478
+ window.close();
1479
+ setTimeout(function() {
1480
+ 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>';
1481
+ }, 500);
1482
+ })();
1483
+ </script>
1484
+ </body>
1485
+ </html>`);
1486
+ return true;
1487
+ }
1488
+ case "listAccounts": {
1489
+ const accounts = await account_default.find({ owner });
1490
+ res.status(200).json({
1491
+ status: 200,
1492
+ data: {
1493
+ accounts: accounts.map((a) => ({
1494
+ id: a._id.toString(),
1495
+ name: a.name,
1496
+ email: a.metadata.google?.email || "",
1497
+ provider: a.metadata.provider
1498
+ }))
1499
+ }
1500
+ });
1501
+ return true;
1502
+ }
1503
+ case "removeAccount": {
1504
+ const { id } = req.query;
1505
+ const account = await account_default.findOne({ _id: id, owner });
1506
+ if (!account) {
1507
+ res.status(404).json({ status: 404, message: "Could not disconnect: account not found" });
1508
+ return true;
1509
+ }
1510
+ if (account.metadata.provider === "GOOGLE") {
1511
+ try {
1512
+ await GoogleDriveProvider.revokeToken(owner, account._id.toString());
1513
+ } catch (e) {
1514
+ console.error("Failed to revoke Google token:", e);
1515
+ }
1516
+ }
1517
+ await account_default.deleteOne({ _id: id, owner });
1518
+ await drive_default.deleteMany({ owner, storageAccountId: id });
1519
+ res.status(200).json({ status: 200, message: "Account removed" });
1520
+ return true;
1521
+ }
1522
+ default:
1523
+ return false;
1524
+ }
1525
+ };
1526
+ var objectIdSchema = zod.z.string().refine((val) => mongoose.isValidObjectId(val), {
1527
+ message: "Invalid ObjectId format"
1528
+ });
1529
+ var sanitizeFilename = (name) => {
1530
+ return name.replace(/[<>:"|?*\x00-\x1F]/g, "").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\.\.\//g, "").replace(/\.\.+/g, "").split("/").pop() || "".trim().slice(0, 255);
1531
+ };
1532
+ var sanitizeRegexInput = (input) => {
1533
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 100);
1534
+ };
1535
+ 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" });
1536
+ var uploadChunkSchema = zod.z.object({
1537
+ chunkIndex: zod.z.number().int().min(0).max(1e4),
1538
+ totalChunks: zod.z.number().int().min(1).max(1e4),
1539
+ driveId: zod.z.string().optional(),
1540
+ fileName: nameSchema,
1541
+ fileSize: zod.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
1542
+ fileType: zod.z.string().min(1).max(255),
1543
+ folderId: zod.z.string().optional()
1544
+ }).refine((data) => data.chunkIndex < data.totalChunks, {
1545
+ message: "Chunk index must be less than total chunks"
1546
+ });
1547
+ var listQuerySchema = zod.z.object({
1548
+ folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]),
1549
+ limit: zod.z.string().optional().transform((val) => {
1550
+ const num = parseInt(val || "50", 10);
1551
+ return Math.min(Math.max(1, num), 100);
1552
+ }),
1553
+ afterId: objectIdSchema.optional()
1554
+ });
1555
+ zod.z.object({
1556
+ id: objectIdSchema,
1557
+ token: zod.z.string().optional()
1558
+ });
1559
+ zod.z.object({
1560
+ id: objectIdSchema,
1561
+ size: zod.z.enum(["small", "medium", "large"]).optional().default("medium"),
1562
+ token: zod.z.string().optional()
1563
+ });
1564
+ var renameBodySchema = zod.z.object({
1565
+ id: objectIdSchema,
1566
+ newName: nameSchema
1567
+ });
1568
+ var deleteQuerySchema = zod.z.object({
1569
+ id: objectIdSchema
1570
+ });
1571
+ zod.z.object({
1572
+ ids: zod.z.array(objectIdSchema).min(1).max(1e3)
1573
+ });
1574
+ var createFolderBodySchema = zod.z.object({
1575
+ name: nameSchema,
1576
+ parentId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.string().length(0), zod.z.undefined()]).optional()
1577
+ });
1578
+ var moveBodySchema = zod.z.object({
1579
+ ids: zod.z.array(objectIdSchema).min(1).max(1e3),
1580
+ targetFolderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional()
1581
+ });
1582
+ var reorderBodySchema = zod.z.object({
1583
+ ids: zod.z.array(objectIdSchema).min(1).max(1e3)
1584
+ });
1585
+ var searchQuerySchema = zod.z.object({
1586
+ q: zod.z.string().min(1).max(100).transform(sanitizeRegexInput),
1587
+ folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional(),
1588
+ limit: zod.z.string().optional().transform((val) => {
1589
+ const num = parseInt(val || "50", 10);
1590
+ return Math.min(Math.max(1, num), 100);
1591
+ }),
1592
+ trashed: zod.z.string().optional().transform((val) => val === "true")
1593
+ });
1594
+ zod.z.object({
1595
+ id: objectIdSchema
1596
+ });
1597
+ var cancelQuerySchema = zod.z.object({
1598
+ id: zod.z.string().uuid()
1599
+ });
1600
+ zod.z.object({
1601
+ days: zod.z.number().int().min(1).max(365).optional()
1602
+ });
1603
+ var driveFileSchemaZod = zod.z.object({
1604
+ id: zod.z.string(),
1605
+ file: zod.z.object({
1606
+ name: zod.z.string(),
1607
+ mime: zod.z.string(),
1608
+ size: zod.z.number()
1609
+ })
1610
+ });
1198
1611
  var getNextOrderValue = async (owner) => {
1199
1612
  const lastItem = await drive_default.findOne({ owner }, {}, { sort: { order: -1 } });
1200
1613
  return lastItem ? lastItem.order + 1 : 0;
@@ -1213,7 +1626,7 @@ var driveGetUrl = (fileId, options) => {
1213
1626
  } else {
1214
1627
  expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
1215
1628
  }
1216
- const signature = crypto2__default.default.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
1629
+ const signature = crypto3__default.default.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
1217
1630
  const token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
1218
1631
  return `${config.apiUrl || "/api/drive"}?action=serve&id=${fileId}&token=${token}`;
1219
1632
  };
@@ -1222,7 +1635,7 @@ var driveAddSignedUrlToken = (item, config) => {
1222
1635
  if (config.security?.signedUrls?.enabled && config.security.signedUrls.secret) {
1223
1636
  const { secret, expiresIn } = config.security.signedUrls;
1224
1637
  const expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
1225
- const signature = crypto2__default.default.createHmac("sha256", secret).update(`${item.id}:${expiryTimestamp}`).digest("hex");
1638
+ const signature = crypto3__default.default.createHmac("sha256", secret).update(`${item.id}:${expiryTimestamp}`).digest("hex");
1226
1639
  token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
1227
1640
  }
1228
1641
  const apiUrl = config.apiUrl || "/api/drive";
@@ -1236,15 +1649,15 @@ var driveReadFile = async (file) => {
1236
1649
  let drive;
1237
1650
  if (typeof file === "string") {
1238
1651
  const doc = await drive_default.findById(file);
1239
- if (!doc) throw new Error(`File not found: ${file}`);
1652
+ if (!doc) throw new Error("Could not read file: the file no longer exists");
1240
1653
  drive = doc;
1241
1654
  } else if ("toClient" in file) {
1242
1655
  drive = file;
1243
1656
  } else {
1244
- throw new Error("Invalid file parameter provided");
1657
+ throw new Error("Could not read file: invalid file reference provided");
1245
1658
  }
1246
1659
  if (drive.information.type !== "FILE") {
1247
- throw new Error("Cannot read a folder");
1660
+ throw new Error("Could not read file: this item is a folder, not a file");
1248
1661
  }
1249
1662
  const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1250
1663
  const accountId = drive.storageAccountId?.toString();
@@ -1253,7 +1666,7 @@ var driveReadFile = async (file) => {
1253
1666
  var driveInfo = async (source) => {
1254
1667
  const fileId = typeof source === "string" ? source : source.id;
1255
1668
  const drive = await drive_default.findById(fileId);
1256
- if (!drive) throw new Error(`File not found: ${fileId}`);
1669
+ if (!drive) throw new Error("Could not load file details: the file no longer exists");
1257
1670
  let parentName;
1258
1671
  if (drive.parentId) {
1259
1672
  const parent = await drive_default.findById(drive.parentId);
@@ -1292,15 +1705,15 @@ var driveFilePath = async (file) => {
1292
1705
  let drive;
1293
1706
  if (typeof file === "string") {
1294
1707
  const doc = await drive_default.findById(file);
1295
- if (!doc) throw new Error(`File not found: ${file}`);
1708
+ if (!doc) throw new Error("Could not locate file: the file no longer exists");
1296
1709
  drive = doc;
1297
1710
  } else if ("toClient" in file) {
1298
1711
  drive = file;
1299
1712
  } else {
1300
- throw new Error("Invalid file parameter provided");
1713
+ throw new Error("Could not locate file: invalid file reference provided");
1301
1714
  }
1302
1715
  if (drive.information.type !== "FILE") {
1303
- throw new Error("Cannot get path for a folder");
1716
+ throw new Error("Could not locate file: this item is a folder, not a file");
1304
1717
  }
1305
1718
  const config = getDriveConfig();
1306
1719
  const STORAGE_PATH = config.storage.path;
@@ -1308,7 +1721,7 @@ var driveFilePath = async (file) => {
1308
1721
  if (providerType === "LOCAL") {
1309
1722
  const filePath = path__default.default.join(STORAGE_PATH, "file", String(drive._id), "data.bin");
1310
1723
  if (!fs__default.default.existsSync(filePath)) {
1311
- throw new Error(`Local file not found on disk: ${filePath}`);
1724
+ throw new Error("Could not locate file: the stored file is missing from disk");
1312
1725
  }
1313
1726
  return Object.freeze({
1314
1727
  path: filePath,
@@ -1365,7 +1778,7 @@ var driveFilePath = async (file) => {
1365
1778
  provider: "GOOGLE"
1366
1779
  });
1367
1780
  }
1368
- throw new Error(`Unsupported provider: ${providerType}`);
1781
+ throw new Error(`Could not locate file: unsupported storage provider "${providerType}"`);
1369
1782
  };
1370
1783
  var driveList = async (options) => {
1371
1784
  const { key, folderId, accountId, limit = 100, afterId } = options;
@@ -1373,7 +1786,7 @@ var driveList = async (options) => {
1373
1786
  if (accountId && accountId !== "LOCAL") {
1374
1787
  const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
1375
1788
  if (!account) {
1376
- throw new Error("Invalid Storage Account");
1789
+ throw new Error("Could not list files: storage account not found or access denied");
1377
1790
  }
1378
1791
  if (account.metadata.provider === "GOOGLE") {
1379
1792
  providerName = "GOOGLE";
@@ -1401,7 +1814,7 @@ var driveListFiles = async (options) => {
1401
1814
  if (accountId && accountId !== "LOCAL") {
1402
1815
  const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
1403
1816
  if (!account) {
1404
- throw new Error("Invalid Storage Account");
1817
+ throw new Error("Could not load files: storage account not found or access denied");
1405
1818
  }
1406
1819
  if (account.metadata.provider === "GOOGLE") {
1407
1820
  providerName = "GOOGLE";
@@ -1445,7 +1858,7 @@ var driveDelete = async (source, options) => {
1445
1858
  let driveId;
1446
1859
  if (typeof source === "string") {
1447
1860
  const doc = await drive_default.findById(source);
1448
- if (!doc) throw new Error(`File not found: ${source}`);
1861
+ if (!doc) throw new Error("Could not delete: the file no longer exists");
1449
1862
  drive = doc;
1450
1863
  driveId = source;
1451
1864
  } else if ("toClient" in source) {
@@ -1453,7 +1866,7 @@ var driveDelete = async (source, options) => {
1453
1866
  driveId = String(drive._id);
1454
1867
  } else {
1455
1868
  const doc = await drive_default.findById(source.id);
1456
- if (!doc) throw new Error(`File not found: ${source.id}`);
1869
+ if (!doc) throw new Error("Could not delete: the selected file no longer exists");
1457
1870
  drive = doc;
1458
1871
  driveId = source.id;
1459
1872
  }
@@ -1465,7 +1878,7 @@ var driveDelete = async (source, options) => {
1465
1878
  trashedAt: null
1466
1879
  });
1467
1880
  if (childCount > 0) {
1468
- throw new Error(`Cannot delete folder: it contains ${childCount} item(s). Use recurse: true to delete folder and all its contents.`);
1881
+ throw new Error(`Could not delete folder: it still contains ${childCount} item(s). Enable recursive delete to remove the folder and everything inside it.`);
1469
1882
  }
1470
1883
  }
1471
1884
  const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
@@ -1476,17 +1889,17 @@ var driveDelete = async (source, options) => {
1476
1889
  var resolveFolderByPath = async (folderPath, owner, accountId) => {
1477
1890
  const normalizedPath = folderPath.replace(/^\/+|\/+$/g, "");
1478
1891
  if (!normalizedPath) {
1479
- throw new Error("Folder path cannot be empty");
1892
+ throw new Error("Could not resolve folder: the folder path is empty");
1480
1893
  }
1481
1894
  const segments = normalizedPath.split("/").filter((s) => s.length > 0);
1482
1895
  if (segments.length === 0) {
1483
- throw new Error("Invalid folder path");
1896
+ throw new Error("Could not resolve folder: the folder path is invalid");
1484
1897
  }
1485
1898
  let providerName = "LOCAL";
1486
1899
  if (accountId && accountId !== "LOCAL") {
1487
1900
  const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner });
1488
1901
  if (!account) {
1489
- throw new Error("Invalid Storage Account");
1902
+ throw new Error("Could not resolve folder: storage account not found or access denied");
1490
1903
  }
1491
1904
  if (account.metadata.provider === "GOOGLE") {
1492
1905
  providerName = "GOOGLE";
@@ -1520,7 +1933,7 @@ var driveUpload = async (source, key, options) => {
1520
1933
  if (accountId && accountId !== "LOCAL") {
1521
1934
  const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
1522
1935
  if (!account) {
1523
- throw new Error("Invalid Storage Account");
1936
+ throw new Error("Could not upload: storage account not found or access denied");
1524
1937
  }
1525
1938
  if (account.metadata.provider === "GOOGLE") {
1526
1939
  provider = GoogleDriveProvider;
@@ -1531,7 +1944,7 @@ var driveUpload = async (source, key, options) => {
1531
1944
  let fileSize;
1532
1945
  if (typeof source === "string") {
1533
1946
  if (!fs__default.default.existsSync(source)) {
1534
- throw new Error(`Source file not found: ${source}`);
1947
+ throw new Error("Could not upload: source file not found");
1535
1948
  }
1536
1949
  sourceFilePath = source;
1537
1950
  const stats = fs__default.default.statSync(source);
@@ -1541,7 +1954,7 @@ var driveUpload = async (source, key, options) => {
1541
1954
  if (!fs__default.default.existsSync(tempDir)) {
1542
1955
  fs__default.default.mkdirSync(tempDir, { recursive: true });
1543
1956
  }
1544
- tempFilePath = path__default.default.join(tempDir, `upload-${crypto2__default.default.randomUUID()}.tmp`);
1957
+ tempFilePath = path__default.default.join(tempDir, `upload-${crypto3__default.default.randomUUID()}.tmp`);
1545
1958
  fs__default.default.writeFileSync(tempFilePath, source);
1546
1959
  sourceFilePath = tempFilePath;
1547
1960
  fileSize = source.length;
@@ -1550,7 +1963,7 @@ var driveUpload = async (source, key, options) => {
1550
1963
  if (!fs__default.default.existsSync(tempDir)) {
1551
1964
  fs__default.default.mkdirSync(tempDir, { recursive: true });
1552
1965
  }
1553
- tempFilePath = path__default.default.join(tempDir, `upload-${crypto2__default.default.randomUUID()}.tmp`);
1966
+ tempFilePath = path__default.default.join(tempDir, `upload-${crypto3__default.default.randomUUID()}.tmp`);
1554
1967
  const writeStream = fs__default.default.createWriteStream(tempFilePath);
1555
1968
  await new Promise((resolve, reject) => {
1556
1969
  source.pipe(writeStream);
@@ -1590,17 +2003,17 @@ var driveUpload = async (source, key, options) => {
1590
2003
  mimeType = mimeTypes[ext] || "application/octet-stream";
1591
2004
  }
1592
2005
  if (config.security && !validateMimeType(mimeType, config.security.allowedMimeTypes)) {
1593
- throw new Error(`File type ${mimeType} not allowed`);
2006
+ throw new Error(`Could not upload: file type "${mimeType}" is not allowed`);
1594
2007
  }
1595
2008
  if (config.security && fileSize > config.security.maxUploadSizeInBytes) {
1596
- throw new Error(`File size ${fileSize} exceeds maximum allowed size ${config.security.maxUploadSizeInBytes}`);
2009
+ throw new Error("Could not upload: file is larger than the maximum allowed size");
1597
2010
  }
1598
2011
  const isRootMode = config.mode === "ROOT";
1599
2012
  if (!options.enforce && !isRootMode) {
1600
2013
  const information = await getDriveInformation({ method: "KEY", key });
1601
2014
  const quota = await provider.getQuota(key, accountId, information.storage.quotaInBytes);
1602
2015
  if (quota.usedInBytes + fileSize > quota.quotaInBytes) {
1603
- throw new Error("Storage quota exceeded");
2016
+ throw new Error("Could not upload: you have run out of storage space");
1604
2017
  }
1605
2018
  }
1606
2019
  let resolvedParentId = null;
@@ -1721,829 +2134,513 @@ var driveCleanup = async () => {
1721
2134
  }
1722
2135
  return { removed, totalFreedInBytes };
1723
2136
  };
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);
2137
+
2138
+ // src/server/actions/shared.ts
2139
+ var resolveProvider = async (req, owner) => {
2140
+ const accountId = req.headers["x-drive-account"];
2141
+ if (!accountId || accountId === "LOCAL") {
2142
+ return { provider: LocalStorageProvider };
2143
+ }
2144
+ const account = await account_default.findOne({ _id: accountId, owner });
2145
+ if (!account) {
2146
+ throw new Error("Storage account not found or access denied");
2147
+ }
2148
+ if (account.metadata.provider === "GOOGLE") {
2149
+ return { provider: GoogleDriveProvider, accountId: account._id.toString() };
2150
+ }
2151
+ return { provider: LocalStorageProvider };
1729
2152
  };
1730
- var sanitizeRegexInput = (input) => {
1731
- return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 100);
1732
- };
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
-
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) => {
1816
- const accountId = req.headers["x-drive-account"];
1817
- if (!accountId || accountId === "LOCAL") {
1818
- return { provider: LocalStorageProvider };
1819
- }
1820
- const account = await account_default.findOne({ _id: accountId, owner });
1821
- if (!account) {
1822
- throw new Error("Invalid Storage Account");
1823
- }
1824
- if (account.metadata.provider === "GOOGLE") return { provider: GoogleDriveProvider, accountId: account._id.toString() };
1825
- return { provider: LocalStorageProvider };
1826
- };
1827
- var addSignedUrlToken = (item, config) => {
2153
+ var withSignedUrl = (item, config) => {
1828
2154
  return driveAddSignedUrlToken(item, config);
1829
2155
  };
1830
- var addSignedUrlTokens = (items, config) => {
2156
+ var withSignedUrls = (items, config) => {
1831
2157
  return driveAddSignedUrlTokens(items, config);
1832
2158
  };
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;
2159
+
2160
+ // src/server/actions/drive.ts
2161
+ var handleDriveAction = async (ctx) => {
2162
+ const { req, res, action, config, owner, isRootMode, information, provider, accountId } = ctx;
2163
+ switch (action) {
2164
+ case "list": {
2165
+ if (req.method !== "GET") return void res.status(405).json({ status: 405, message: "Listing files requires a GET request" });
2166
+ const listQuery = listQuerySchema.safeParse(req.query);
2167
+ if (!listQuery.success) return void res.status(400).json({ status: 400, message: "Could not list files: invalid request parameters" });
2168
+ const { folderId, limit, afterId } = listQuery.data;
2169
+ try {
2170
+ await provider.sync(folderId || "root", owner, accountId);
2171
+ } catch (e) {
2172
+ console.error("Sync failed", e);
1850
2173
  }
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: "Failed to initialize drive 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 query parameter" });
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: "Missing or invalid file ID" });
2174
+ const query = {
2175
+ "provider.type": provider.name,
2176
+ storageAccountId: accountId || null,
2177
+ parentId: folderId === "root" || !folderId ? null : folderId,
2178
+ trashedAt: null
2179
+ };
2180
+ if (!isRootMode) {
2181
+ query.owner = owner;
1899
2182
  }
1900
- const drive = await drive_default.findById(id);
1901
- if (!drive) return res.status(404).json({ status: 404, message: "File not found" });
1902
- if (config.security?.signedUrls?.enabled) {
1903
- if (!token || typeof token !== "string") {
1904
- return res.status(401).json({ status: 401, message: "Missing or invalid token" });
1905
- }
2183
+ if (afterId) query._id = { $lt: afterId };
2184
+ const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
2185
+ const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
2186
+ res.status(200).json({ status: 200, message: "Items retrieved", data: { items: plainItems, hasMore: items.length === limit } });
2187
+ return;
2188
+ }
2189
+ case "search": {
2190
+ const searchData = searchQuerySchema.safeParse(req.query);
2191
+ if (!searchData.success) return void res.status(400).json({ status: 400, message: "Could not search: invalid request parameters" });
2192
+ const { q, folderId, limit, trashed } = searchData.data;
2193
+ if (!trashed) {
1906
2194
  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: "Token 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: "Invalid token" });
1917
- }
1918
- } catch (err) {
1919
- return res.status(401).json({ status: 401, message: "Invalid token format" });
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");
2195
+ await provider.search(q, owner, accountId);
2196
+ } catch (e) {
2197
+ console.error("Search sync failed", e);
1929
2198
  }
1930
- stream.pipe(res);
1931
- return;
1932
2199
  }
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;
2200
+ const query = {
2201
+ "provider.type": provider.name,
2202
+ storageAccountId: accountId || null,
2203
+ trashedAt: trashed ? { $ne: null } : null,
2204
+ name: { $regex: q, $options: "i" }
2205
+ };
2206
+ if (!isRootMode) {
2207
+ query.owner = owner;
2020
2208
  }
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 : "Unknown error"
2209
+ if (folderId && folderId !== "root") query.parentId = folderId;
2210
+ const items = await drive_default.find(query, {}, { limit, sort: { createdAt: -1 } });
2211
+ const plainItems = withSignedUrls(await Promise.all(items.map((i) => i.toClient())), config);
2212
+ res.status(200).json({ status: 200, message: "Results", data: { items: plainItems } });
2213
+ return;
2214
+ }
2215
+ case "upload": {
2216
+ if (req.method !== "POST") return void res.status(405).json({ status: 405, message: "Uploading requires a POST request" });
2217
+ const systemTmpDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
2218
+ if (!fs__default.default.existsSync(systemTmpDir)) fs__default.default.mkdirSync(systemTmpDir, { recursive: true });
2219
+ const form = formidable__default.default({
2220
+ multiples: false,
2221
+ maxFileSize: (config.security?.maxUploadSizeInBytes ?? 1024 * 1024 * 1024) * 2,
2222
+ uploadDir: systemTmpDir,
2223
+ keepExtensions: true
2026
2224
  });
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
- }
2225
+ const [fields, files] = await new Promise((resolve, reject) => {
2226
+ form.parse(req, (err, parsedFields, parsedFiles) => {
2227
+ if (err) reject(err);
2228
+ else resolve([parsedFields, parsedFiles]);
2229
+ });
2047
2230
  });
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 not configured" });
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 } });
2231
+ const cleanupTempFiles = (allFiles) => {
2232
+ Object.values(allFiles).flat().forEach((file) => {
2233
+ if (file && fs__default.default.existsSync(file.filepath)) fs__default.default.rmSync(file.filepath, { force: true });
2234
+ });
2235
+ };
2236
+ const getString = (f) => Array.isArray(f) ? f[0] : f || "";
2237
+ const getInt = (f) => parseInt(getString(f) || "0", 10);
2238
+ const uploadData = uploadChunkSchema.safeParse({
2239
+ chunkIndex: getInt(fields.chunkIndex),
2240
+ totalChunks: getInt(fields.totalChunks),
2241
+ driveId: getString(fields.driveId) || void 0,
2242
+ fileName: getString(fields.fileName),
2243
+ fileSize: getInt(fields.fileSize),
2244
+ fileType: getString(fields.fileType),
2245
+ folderId: getString(fields.folderId) || void 0
2246
+ });
2247
+ if (!uploadData.success) {
2248
+ cleanupTempFiles(files);
2249
+ return void res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
2250
+ }
2251
+ const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId } = uploadData.data;
2252
+ let currentUploadId = driveId;
2253
+ const tempBaseDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
2254
+ if (!currentUploadId) {
2255
+ if (chunkIndex !== 0) return void res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
2256
+ if (fileType && config.security) {
2257
+ if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
2258
+ cleanupTempFiles(files);
2259
+ return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2068
2260
  }
2069
- return res.status(400).json({ status: 400, message: "Unknown provider" });
2070
2261
  }
2071
- case "callback": {
2072
- const { code, state } = req.query;
2073
- if (!code) return res.status(400).json({ status: 400, message: "Missing code" });
2074
- const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
2075
- if (!clientId || !clientSecret || !redirectUri) return res.status(500).json({ status: 500, message: "Google not configured" });
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();
2101
- }
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
- }))
2144
- }
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: "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);
2156
- }
2262
+ if (!isRootMode) {
2263
+ const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2264
+ if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
2265
+ cleanupTempFiles(files);
2266
+ return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
2157
2267
  }
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: "Only GET allowed" });
2169
- const listQuery = listQuerySchema.safeParse(req.query);
2170
- if (!listQuery.success) return res.status(400).json({ status: 400, message: "Invalid 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
2268
  }
2177
- const query = {
2178
- "provider.type": provider.name,
2179
- storageAccountId: accountId || null,
2269
+ currentUploadId = crypto3__default.default.randomUUID();
2270
+ const uploadDir2 = path__default.default.join(tempBaseDir, currentUploadId);
2271
+ fs__default.default.mkdirSync(uploadDir2, { recursive: true });
2272
+ const metadata = {
2273
+ owner,
2274
+ accountId,
2275
+ providerName: provider.name,
2276
+ name: fileName,
2180
2277
  parentId: folderId === "root" || !folderId ? null : folderId,
2181
- trashedAt: null
2278
+ fileSize: fileSizeInBytes,
2279
+ mimeType: fileType,
2280
+ totalChunks
2182
2281
  };
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;
2282
+ fs__default.default.writeFileSync(path__default.default.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
2191
2283
  }
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: "Invalid params" });
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);
2284
+ if (!currentUploadId) {
2285
+ cleanupTempFiles(files);
2286
+ return void res.status(400).json({ status: 400, message: "Could not upload: invalid upload request" });
2287
+ }
2288
+ const uploadDir = path__default.default.join(tempBaseDir, currentUploadId);
2289
+ if (!fs__default.default.existsSync(uploadDir)) {
2290
+ cleanupTempFiles(files);
2291
+ return void res.status(404).json({ status: 404, message: "Could not upload: this upload session was not found or has expired" });
2292
+ }
2293
+ try {
2294
+ const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
2295
+ if (!chunkFile) throw new Error("Could not upload: no file chunk was received");
2296
+ const partPath = path__default.default.join(uploadDir, `part_${chunkIndex}`);
2297
+ try {
2298
+ fs__default.default.renameSync(chunkFile.filepath, partPath);
2299
+ } catch (err) {
2300
+ if (err instanceof Error && "code" in err && err.code === "EXDEV") {
2301
+ fs__default.default.copyFileSync(chunkFile.filepath, partPath);
2302
+ fs__default.default.unlinkSync(chunkFile.filepath);
2303
+ } else {
2304
+ throw err;
2202
2305
  }
2203
2306
  }
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: "Only POST allowed" });
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]);
2307
+ const uploadedParts = fs__default.default.readdirSync(uploadDir).filter((f) => f.startsWith("part_"));
2308
+ if (uploadedParts.length === totalChunks) {
2309
+ const metaPath = path__default.default.join(uploadDir, "metadata.json");
2310
+ const meta = JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8"));
2311
+ const finalTempPath = path__default.default.join(uploadDir, "final.bin");
2312
+ const writeStream = fs__default.default.createWriteStream(finalTempPath);
2313
+ let streamError = null;
2314
+ writeStream.on("error", (err) => {
2315
+ streamError = err;
2233
2316
  });
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 });
2317
+ await new Promise((resolve, reject) => {
2318
+ writeStream.on("open", () => resolve());
2319
+ writeStream.once("error", reject);
2238
2320
  });
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: "Missing upload ID for non-zero chunk" });
2260
- if (fileType && config.security) {
2261
- if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
2262
- cleanupTempFiles(files);
2263
- return res.status(400).json({ status: 400, message: `File type ${fileType} not allowed` });
2321
+ for (let i = 0; i < totalChunks; i++) {
2322
+ if (streamError) {
2323
+ writeStream.destroy();
2324
+ throw streamError;
2264
2325
  }
2265
- }
2266
- if (!isRootMode) {
2267
- const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2268
- if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
2269
- cleanupTempFiles(files);
2270
- return res.status(413).json({ status: 413, message: "Storage quota exceeded" });
2326
+ const pPath = path__default.default.join(uploadDir, `part_${i}`);
2327
+ if (!fs__default.default.existsSync(pPath)) {
2328
+ writeStream.destroy();
2329
+ throw new Error(`Could not finish upload: chunk ${i} is missing`);
2271
2330
  }
2272
- }
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
- }
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: "Upload session not found or expired" });
2293
- }
2294
- try {
2295
- const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
2296
- if (!chunkFile) throw new Error("No chunk file 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
- }
2307
- }
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(`Missing chunk part: ${i}`);
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
- }
2331
+ const data = fs__default.default.readFileSync(pPath);
2332
+ const canContinue = writeStream.write(data);
2333
+ if (!canContinue) {
2341
2334
  await new Promise((resolve, reject) => {
2342
- if (streamError) {
2343
- reject(streamError);
2344
- return;
2345
- }
2346
- writeStream.end();
2347
- writeStream.on("finish", resolve);
2335
+ writeStream.once("drain", resolve);
2348
2336
  writeStream.once("error", reject);
2349
2337
  });
2350
- if (!fs__default.default.existsSync(finalTempPath)) {
2351
- throw new Error("Failed to create merged file");
2352
- }
2353
- const finalStats = fs__default.default.statSync(finalTempPath);
2354
- if (finalStats.size !== meta.fileSize) {
2355
- throw new Error(`File size mismatch: expected ${meta.fileSize}, got ${finalStats.size}`);
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
2338
  }
2391
- } catch (e) {
2392
- cleanupTempFiles(files);
2393
- throw e;
2394
2339
  }
2395
- return;
2396
- }
2397
- cleanupTempFiles(files);
2398
- return res.status(400).json({ status: 400, message: "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: "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)) {
2340
+ await new Promise((resolve, reject) => {
2341
+ if (streamError) {
2342
+ reject(streamError);
2343
+ return;
2344
+ }
2345
+ writeStream.end();
2346
+ writeStream.on("finish", resolve);
2347
+ writeStream.once("error", reject);
2348
+ });
2349
+ if (!fs__default.default.existsSync(finalTempPath)) {
2350
+ throw new Error("Could not finish upload: failed to assemble the file");
2351
+ }
2352
+ const finalStats = fs__default.default.statSync(finalTempPath);
2353
+ if (finalStats.size !== meta.fileSize) {
2354
+ throw new Error("Could not finish upload: the assembled file is incomplete (size mismatch)");
2355
+ }
2356
+ const drive = new drive_default({
2357
+ owner: meta.owner,
2358
+ storageAccountId: meta.accountId || null,
2359
+ provider: { type: meta.providerName },
2360
+ name: meta.name,
2361
+ parentId: meta.parentId,
2362
+ order: 0,
2363
+ information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
2364
+ status: "UPLOADING",
2365
+ currentChunk: totalChunks,
2366
+ totalChunks
2367
+ });
2368
+ if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
2369
+ drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
2370
+ }
2371
+ await drive.save();
2407
2372
  try {
2408
- fs__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
2409
- } catch (e) {
2410
- console.error("Failed to cleanup temp upload:", e);
2373
+ const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
2374
+ fs__default.default.rmSync(uploadDir, { recursive: true, force: true });
2375
+ const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
2376
+ res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item: withSignedUrl(item, config) }, statistic: { storage: newQuota } });
2377
+ } catch (err) {
2378
+ await drive_default.deleteOne({ _id: drive._id });
2379
+ throw err;
2380
+ }
2381
+ } else {
2382
+ const newQuota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2383
+ if (chunkIndex === 0) {
2384
+ res.status(200).json({ status: 200, message: "Upload started", data: { type: "UPLOAD_STARTED", driveId: currentUploadId }, statistic: { storage: newQuota } });
2385
+ } else {
2386
+ res.status(200).json({ status: 200, message: "Chunk received", data: { type: "CHUNK_RECEIVED", driveId: currentUploadId, chunkIndex }, statistic: { storage: newQuota } });
2411
2387
  }
2412
2388
  }
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 } });
2389
+ } catch (e) {
2390
+ cleanupTempFiles(files);
2391
+ throw e;
2422
2392
  }
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: "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: "Not found" });
2430
- const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
2431
- const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
2393
+ return;
2394
+ }
2395
+ case "cancel": {
2396
+ const cancelData = cancelQuerySchema.safeParse(req.query);
2397
+ if (!cancelData.success) return void res.status(400).json({ status: 400, message: "Could not cancel upload: invalid ID" });
2398
+ const { id } = cancelData.data;
2399
+ const tempUploadDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads", id);
2400
+ if (fs__default.default.existsSync(tempUploadDir)) {
2432
2401
  try {
2433
- await itemProvider.trash([id], owner, itemAccountId);
2402
+ fs__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
2434
2403
  } catch (e) {
2435
- console.error("Provider trash failed:", e);
2404
+ console.error("Failed to cleanup temp upload:", e);
2436
2405
  }
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
2406
  }
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: "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 } });
2407
+ res.status(200).json({ status: 200, message: "Upload cancelled", data: null });
2408
+ return;
2409
+ }
2410
+ case "createFolder": {
2411
+ const folderData = createFolderBodySchema.safeParse(req.body);
2412
+ if (!folderData.success) return void res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
2413
+ const { name, parentId } = folderData.data;
2414
+ const item = withSignedUrl(await provider.createFolder(name, parentId ?? null, owner, accountId), config);
2415
+ res.status(201).json({ status: 201, message: "Folder created", data: { item } });
2416
+ return;
2417
+ }
2418
+ case "delete": {
2419
+ const deleteData = deleteQuerySchema.safeParse(req.query);
2420
+ if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not move to trash: invalid ID" });
2421
+ const { id } = deleteData.data;
2422
+ const drive = await drive_default.findById(id);
2423
+ if (!drive) return void res.status(404).json({ status: 404, message: "Could not move to trash: item not found" });
2424
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
2425
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
2426
+ try {
2427
+ await itemProvider.trash([id], owner, itemAccountId);
2428
+ } catch (e) {
2429
+ console.error("Provider trash failed:", e);
2449
2430
  }
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
- });
2431
+ drive.trashedAt = /* @__PURE__ */ new Date();
2432
+ await drive.save();
2433
+ res.status(200).json({ status: 200, message: "Moved to trash", data: null });
2434
+ return;
2435
+ }
2436
+ case "deletePermanent": {
2437
+ const deleteData = deleteQuerySchema.safeParse(req.query);
2438
+ if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not delete: invalid ID" });
2439
+ const { id } = deleteData.data;
2440
+ await provider.delete([id], owner, accountId);
2441
+ const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2442
+ res.status(200).json({ status: 200, message: "Deleted", statistic: { storage: quota } });
2443
+ return;
2444
+ }
2445
+ case "quota": {
2446
+ const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2447
+ res.status(200).json({
2448
+ status: 200,
2449
+ message: "Quota retrieved",
2450
+ data: {
2451
+ usedInBytes: quota.usedInBytes,
2452
+ totalInBytes: quota.quotaInBytes,
2453
+ availableInBytes: Math.max(0, quota.quotaInBytes - quota.usedInBytes),
2454
+ percentage: quota.quotaInBytes > 0 ? Math.round(quota.usedInBytes / quota.quotaInBytes * 100) : 0
2455
+ },
2456
+ statistic: { storage: quota }
2457
+ });
2458
+ return;
2459
+ }
2460
+ case "trash": {
2461
+ try {
2462
+ const { provider: trashProvider, accountId: trashAccountId } = await resolveProvider(req, owner);
2463
+ await trashProvider.syncTrash(owner, trashAccountId);
2464
+ } catch (e) {
2465
+ console.error("Trash sync failed", e);
2459
2466
  }
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);
2467
+ const query = {
2468
+ owner,
2469
+ "provider.type": provider.name,
2470
+ storageAccountId: accountId || null,
2471
+ trashedAt: { $ne: null }
2472
+ };
2473
+ const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
2474
+ const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
2475
+ res.status(200).json({ status: 200, message: "Trash items", data: { items: plainItems, hasMore: false } });
2476
+ return;
2477
+ }
2478
+ case "restore": {
2479
+ const restoreData = deleteQuerySchema.safeParse(req.query);
2480
+ if (!restoreData.success) return void res.status(400).json({ status: 400, message: "Could not restore: invalid ID" });
2481
+ const { id } = restoreData.data;
2482
+ const drive = await drive_default.findById(id);
2483
+ if (!drive) return void res.status(404).json({ status: 404, message: "Could not restore: item not found" });
2484
+ let targetParentId = drive.parentId;
2485
+ if (targetParentId) {
2486
+ const parent = await drive_default.findById(targetParentId);
2487
+ if (parent?.trashedAt) {
2488
+ targetParentId = null;
2467
2489
  }
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
2490
  }
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: "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: "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
- }
2491
+ const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
2492
+ const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
2493
+ try {
2494
+ await itemProvider.untrash([id], owner, itemAccountId);
2495
+ if (targetParentId !== drive.parentId) {
2496
+ await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
2495
2497
  }
2496
- const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
2497
- const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
2498
+ } catch (e) {
2499
+ console.error("Provider restore failed:", e);
2500
+ }
2501
+ drive.trashedAt = null;
2502
+ drive.parentId = targetParentId;
2503
+ await drive.save();
2504
+ res.status(200).json({
2505
+ status: 200,
2506
+ message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
2507
+ data: null
2508
+ });
2509
+ return;
2510
+ }
2511
+ case "move": {
2512
+ const moveData = moveBodySchema.safeParse(req.body);
2513
+ if (!moveData.success) return void res.status(400).json({ status: 400, message: "Could not move: invalid request data" });
2514
+ const { ids, targetFolderId } = moveData.data;
2515
+ const items = [];
2516
+ const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
2517
+ for (const id of ids) {
2498
2518
  try {
2499
- await itemProvider.untrash([id], owner, itemAccountId);
2500
- if (targetParentId !== drive.parentId) {
2501
- await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
2502
- }
2519
+ const item = await provider.move(id, effectiveTargetId, owner, accountId);
2520
+ items.push(item);
2503
2521
  } catch (e) {
2504
- console.error("Provider restore failed:", e);
2522
+ console.error(`Failed to move item ${id}`, e);
2505
2523
  }
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
2524
  }
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: "Invalid 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) } });
2525
+ res.status(200).json({ status: 200, message: "Moved", data: { items: withSignedUrls(items, config) } });
2526
+ return;
2527
+ }
2528
+ case "reorder": {
2529
+ if (req.method !== "POST") {
2530
+ return void res.status(405).json({ status: 405, message: "Reordering requires a POST request" });
2531
+ }
2532
+ const reorderData = reorderBodySchema.safeParse(req.body);
2533
+ if (!reorderData.success) {
2534
+ return void res.status(400).json({ status: 400, message: "Could not reorder: invalid request data" });
2531
2535
  }
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: "Invalid 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 } });
2536
+ const { ids } = reorderData.data;
2537
+ const query = {
2538
+ _id: { $in: ids },
2539
+ "provider.type": provider.name,
2540
+ storageAccountId: accountId || null,
2541
+ trashedAt: null
2542
+ };
2543
+ if (!isRootMode) {
2544
+ query.owner = owner;
2539
2545
  }
2540
- // ** 9. THUMBNAIL **
2541
- default:
2542
- res.status(400).json({ status: 400, message: `Unknown action: ${action}` });
2546
+ const existingItems = await drive_default.find(query, { _id: 1, parentId: 1 });
2547
+ if (existingItems.length !== ids.length) {
2548
+ return void res.status(404).json({ status: 404, message: "Could not reorder: one or more items were not found" });
2549
+ }
2550
+ const parentIds = new Set(existingItems.map((item) => item.parentId ? item.parentId.toString() : "root"));
2551
+ if (parentIds.size > 1) {
2552
+ return void res.status(400).json({ status: 400, message: "Could not reorder: all items must be in the same folder" });
2553
+ }
2554
+ const operations = ids.map((id, order) => ({
2555
+ updateOne: {
2556
+ filter: {
2557
+ _id: id,
2558
+ "provider.type": provider.name,
2559
+ storageAccountId: accountId || null,
2560
+ trashedAt: null,
2561
+ ...isRootMode ? {} : { owner }
2562
+ },
2563
+ update: { $set: { order } }
2564
+ }
2565
+ }));
2566
+ await drive_default.bulkWrite(operations);
2567
+ const updatedItems = await drive_default.find(query, {}, { sort: { order: 1 } });
2568
+ const plainItems = withSignedUrls(await Promise.all(updatedItems.map((item) => item.toClient())), config);
2569
+ res.status(200).json({ status: 200, message: "Reordered", data: { items: plainItems } });
2570
+ return;
2571
+ }
2572
+ case "rename": {
2573
+ const renameData = renameBodySchema.safeParse({ id: req.query.id, ...req.body });
2574
+ if (!renameData.success) return void res.status(400).json({ status: 400, message: "Could not rename: invalid request data" });
2575
+ const { id, newName } = renameData.data;
2576
+ const item = withSignedUrl(await provider.rename(id, newName, owner, accountId), config);
2577
+ res.status(200).json({ status: 200, message: "Renamed", data: { item } });
2578
+ return;
2579
+ }
2580
+ default: {
2581
+ res.status(400).json({ status: 400, message: `Unknown action requested: "${action}"` });
2582
+ return;
2543
2583
  }
2584
+ }
2585
+ };
2586
+
2587
+ // src/server/index.ts
2588
+ var driveAPIHandler = async (req, res) => {
2589
+ const action = req.query.action || (req.query.code && req.query.state ? "callback" : void 0);
2590
+ let config;
2591
+ try {
2592
+ config = getDriveConfig();
2593
+ } catch (error) {
2594
+ console.error("[next-drive] Configuration error:", error);
2595
+ res.status(500).json({ status: 500, message: "Drive is not ready: failed to initialize configuration" });
2596
+ return;
2597
+ }
2598
+ const isPreflightHandled = applyCorsHeaders(req, res, config);
2599
+ if (isPreflightHandled) return;
2600
+ if (!action) {
2601
+ res.status(400).json({ status: 400, message: 'Missing "action" parameter in request' });
2602
+ return;
2603
+ }
2604
+ const wasPublicHandled = await handlePublicAction(req, res, action, config);
2605
+ if (wasPublicHandled) return;
2606
+ try {
2607
+ const mode = config.mode || "NORMAL";
2608
+ const information = await getDriveInformation({ method: "REQUEST", req });
2609
+ const { key: owner } = information;
2610
+ const isRootMode = mode === "ROOT";
2611
+ if (action === "information") {
2612
+ const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
2613
+ const googleConfigured = !!(clientId && clientSecret && redirectUri);
2614
+ res.status(200).json({
2615
+ status: 200,
2616
+ message: "Information retrieved",
2617
+ data: {
2618
+ providers: {
2619
+ google: googleConfigured
2620
+ },
2621
+ mode
2622
+ }
2623
+ });
2624
+ return;
2625
+ }
2626
+ const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
2627
+ if (wasAuthHandled) return;
2628
+ const { provider, accountId } = await resolveProvider(req, owner);
2629
+ await handleDriveAction({
2630
+ req,
2631
+ res,
2632
+ action,
2633
+ config,
2634
+ owner,
2635
+ isRootMode,
2636
+ information,
2637
+ provider,
2638
+ accountId
2639
+ });
2544
2640
  } catch (error) {
2545
2641
  console.error(`[next-drive] Error handling action ${action}:`, error);
2546
- res.status(500).json({ status: 500, message: error instanceof Error ? error.message : "Unknown error" });
2642
+ const detail = error instanceof Error ? error.message : "Something went wrong while processing your request";
2643
+ res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
2547
2644
  }
2548
2645
  };
2549
2646
 
@@ -2562,5 +2659,5 @@ exports.driveUpload = driveUpload;
2562
2659
  exports.drive_default = drive_default;
2563
2660
  exports.getDriveConfig = getDriveConfig;
2564
2661
  exports.getDriveInformation = getDriveInformation;
2565
- //# sourceMappingURL=chunk-VIB7R4JN.cjs.map
2566
- //# sourceMappingURL=chunk-VIB7R4JN.cjs.map
2662
+ //# sourceMappingURL=chunk-OU5TKLHV.cjs.map
2663
+ //# sourceMappingURL=chunk-OU5TKLHV.cjs.map