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