@muhgholy/next-drive 4.23.33 → 4.23.34

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 (44) hide show
  1. package/README.md +79 -3
  2. package/dist/{chunk-HWNIRA6Y.cjs → chunk-DHIHCER6.cjs} +220 -71
  3. package/dist/chunk-DHIHCER6.cjs.map +1 -0
  4. package/dist/{chunk-2DCZENJ2.js → chunk-E72MTT6W.js} +3 -3
  5. package/dist/{chunk-2DCZENJ2.js.map → chunk-E72MTT6W.js.map} +1 -1
  6. package/dist/{chunk-WUG4ATLH.cjs → chunk-PKNXKHGS.cjs} +37 -37
  7. package/dist/{chunk-WUG4ATLH.cjs.map → chunk-PKNXKHGS.cjs.map} +1 -1
  8. package/dist/{chunk-IWV6G7HQ.js → chunk-QMPV4YSZ.js} +221 -73
  9. package/dist/chunk-QMPV4YSZ.js.map +1 -0
  10. package/dist/server/controllers/drive.d.ts +33 -2
  11. package/dist/server/controllers/drive.d.ts.map +1 -1
  12. package/dist/server/errors.d.ts +12 -0
  13. package/dist/server/errors.d.ts.map +1 -0
  14. package/dist/server/express.cjs +16 -12
  15. package/dist/server/express.cjs.map +1 -1
  16. package/dist/server/express.d.ts +3 -0
  17. package/dist/server/express.d.ts.map +1 -1
  18. package/dist/server/express.js +4 -4
  19. package/dist/server/express.js.map +1 -1
  20. package/dist/server/hono.cjs +16 -12
  21. package/dist/server/hono.cjs.map +1 -1
  22. package/dist/server/hono.d.ts +3 -0
  23. package/dist/server/hono.d.ts.map +1 -1
  24. package/dist/server/hono.js +4 -4
  25. package/dist/server/hono.js.map +1 -1
  26. package/dist/server/index.cjs +23 -19
  27. package/dist/server/index.d.ts +3 -0
  28. package/dist/server/index.d.ts.map +1 -1
  29. package/dist/server/index.js +2 -2
  30. package/dist/server/storage-adapters/google.d.ts.map +1 -1
  31. package/dist/server/tus.d.ts.map +1 -1
  32. package/dist/tus-EQ2IXSRE.cjs +20 -0
  33. package/dist/tus-EQ2IXSRE.cjs.map +1 -0
  34. package/dist/tus-ODCMFEBZ.js +3 -0
  35. package/dist/tus-ODCMFEBZ.js.map +1 -0
  36. package/dist/types/server/storage.d.ts +4 -1
  37. package/dist/types/server/storage.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-HWNIRA6Y.cjs.map +0 -1
  40. package/dist/chunk-IWV6G7HQ.js.map +0 -1
  41. package/dist/tus-5LLFMVQK.cjs +0 -20
  42. package/dist/tus-5LLFMVQK.cjs.map +0 -1
  43. package/dist/tus-C4T5ZDTG.js +0 -3
  44. package/dist/tus-C4T5ZDTG.js.map +0 -1
package/README.md CHANGED
@@ -446,7 +446,7 @@ const url = driveGetUrl(fileId, { expiry: new Date("2026-12-31") });
446
446
  ### Read File Stream
447
447
 
448
448
  ```typescript
449
- import { driveReadFile } from "@muhgholy/next-drive/server";
449
+ import { driveReadFile, DriveError } from "@muhgholy/next-drive/server";
450
450
 
451
451
  // Using file ID
452
452
  const { stream, mime, size } = await driveReadFile(fileId);
@@ -455,6 +455,27 @@ stream.pipe(response);
455
455
  // Using database document
456
456
  const drive = await Drive.findById(fileId);
457
457
  const { stream, mime, size } = await driveReadFile(drive);
458
+
459
+ // With progress and abort
460
+ const controller = new AbortController();
461
+ const { stream } = await driveReadFile(fileId, {
462
+ onProgress: ({ bytesDownloaded, totalBytes, percentage }) => {
463
+ console.log(`${percentage}% (${bytesDownloaded}/${totalBytes} bytes)`);
464
+ },
465
+ signal: controller.signal,
466
+ });
467
+
468
+ // onProgress fires for bytes piped through the returned stream - works the same way
469
+ // whether the file is local or on Google Drive, and whether it's already cached or not.
470
+ // Aborting destroys the stream and emits an 'error' event with a DriveError (code: 'ABORTED').
471
+ stream.on("error", (err) => {
472
+ if (err instanceof DriveError && err.code === "ABORTED") {
473
+ console.log("Read aborted");
474
+ }
475
+ });
476
+
477
+ // Somewhere else:
478
+ controller.abort();
458
479
  ```
459
480
 
460
481
  ### Get File/Folder Information
@@ -496,7 +517,7 @@ const info = await driveInfo(file);
496
517
  For libraries requiring file paths (Sharp, FFmpeg, etc.):
497
518
 
498
519
  ```typescript
499
- import { driveFilePath } from "@muhgholy/next-drive/server";
520
+ import { driveFilePath, DriveError } from "@muhgholy/next-drive/server";
500
521
 
501
522
  const { path, mime, size, provider } = await driveFilePath(fileId);
502
523
 
@@ -505,9 +526,64 @@ await sharp(path).resize(800, 600).toFile("output.jpg");
505
526
 
506
527
  // Use with FFmpeg
507
528
  await ffmpeg(path).format("mp4").save("output.mp4");
529
+
530
+ // With progress and abort (only relevant for a Google Drive cache-miss download - local files
531
+ // and already-cached Google Drive files resolve instantly and never call onProgress)
532
+ const controller = new AbortController();
533
+ try {
534
+ const result = await driveFilePath(fileId, {
535
+ onProgress: ({ bytesDownloaded, totalBytes, percentage }) => {
536
+ console.log(`${percentage}% (${bytesDownloaded}/${totalBytes} bytes)`);
537
+ },
538
+ signal: controller.signal,
539
+ });
540
+ } catch (err) {
541
+ if (err instanceof DriveError && err.code === "ABORTED") {
542
+ console.log("Download aborted");
543
+ }
544
+ }
545
+
546
+ // Somewhere else:
547
+ controller.abort();
548
+ ```
549
+
550
+ > Google Drive files are automatically downloaded to local cache. Concurrent calls for the same
551
+ > file are deduplicated - only one download happens, and every caller's `onProgress` receives the
552
+ > same progress events.
553
+
554
+ ### Error Handling
555
+
556
+ Every error thrown directly by the server controllers (`driveReadFile`, `driveFilePath`,
557
+ `driveList`, `driveUpload`, `driveDelete`, etc.) is a `DriveError` with a `code` you can match on
558
+ instead of parsing the message text:
559
+
560
+ ```typescript
561
+ import { DriveError } from "@muhgholy/next-drive/server";
562
+
563
+ try {
564
+ await driveFilePath(fileId);
565
+ } catch (err) {
566
+ if (err instanceof DriveError) {
567
+ switch (err.code) {
568
+ case "NOT_FOUND":
569
+ // file no longer exists
570
+ break;
571
+ case "ABORTED":
572
+ // caller-triggered abort via AbortSignal
573
+ break;
574
+ case "ACCESS_DENIED":
575
+ case "QUOTA_EXCEEDED":
576
+ case "FILE_TOO_LARGE":
577
+ // ...handle other codes
578
+ break;
579
+ }
580
+ }
581
+ }
508
582
  ```
509
583
 
510
- > Google Drive files are automatically downloaded to local cache.
584
+ `TDriveErrorCode` includes: `NOT_FOUND`, `INVALID_REFERENCE`, `INVALID_TYPE`, `FILE_MISSING`,
585
+ `UNSUPPORTED_PROVIDER`, `ACCESS_DENIED`, `FOLDER_NOT_EMPTY`, `INVALID_PATH`, `INVALID_MIME_TYPE`,
586
+ `FILE_TOO_LARGE`, `QUOTA_EXCEEDED`, `MAX_DEPTH_EXCEEDED`, `ABORTED`, `DOWNLOAD_FAILED`.
511
587
 
512
588
  ### List Files and Folders
513
589
 
@@ -517,6 +517,15 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size, fit, posi
517
517
  ...resolvedPosition && { position: resolvedPosition }
518
518
  };
519
519
  };
520
+
521
+ // src/server/errors.ts
522
+ var DriveError = class extends Error {
523
+ constructor(code, message) {
524
+ super(message);
525
+ this.name = "DriveError";
526
+ this.code = code;
527
+ }
528
+ };
520
529
  var generatePlaceholderThumbnail = async (outputPath, mimeType) => {
521
530
  const typeParts = mimeType.split("/");
522
531
  const subtype = typeParts[1] || "file";
@@ -973,7 +982,7 @@ var GoogleDriveProvider = {
973
982
  return { usedInBytes: 0, quotaInBytes: 0 };
974
983
  }
975
984
  },
976
- openStream: async (item, accountId) => {
985
+ openStream: async (item, accountId, options) => {
977
986
  if (item.information.type === "FOLDER") throw new Error("Could not open Google Drive file: folders cannot be streamed");
978
987
  const cachePath = path__default.default.join(getDriveConfig().storage.path, "file", item._id.toString(), "data.bin");
979
988
  if (fs__default.default.existsSync(cachePath)) {
@@ -990,24 +999,36 @@ var GoogleDriveProvider = {
990
999
  { fileId: item.provider.google.id, alt: "media" },
991
1000
  { responseType: "stream" }
992
1001
  );
993
- const fileDir = path__default.default.dirname(cachePath);
994
- if (!fs__default.default.existsSync(fileDir)) fs__default.default.mkdirSync(fileDir, { recursive: true });
995
- const tempPath = `${cachePath}.tmp`;
996
- const writeStream = fs__default.default.createWriteStream(tempPath);
997
- res.data.pipe(writeStream);
998
- writeStream.on("finish", () => {
999
- try {
1000
- fs__default.default.renameSync(tempPath, cachePath);
1001
- } catch {
1002
- fs__default.default.unlinkSync(tempPath);
1003
- }
1004
- });
1005
- writeStream.on("error", () => fs__default.default.unlink(tempPath, () => {
1006
- }));
1002
+ let cacheWritePromise;
1003
+ if (!options?.skipCache) {
1004
+ const fileDir = path__default.default.dirname(cachePath);
1005
+ if (!fs__default.default.existsSync(fileDir)) fs__default.default.mkdirSync(fileDir, { recursive: true });
1006
+ const tempPath = `${cachePath}.tmp`;
1007
+ const writeStream = fs__default.default.createWriteStream(tempPath);
1008
+ res.data.pipe(writeStream);
1009
+ cacheWritePromise = new Promise((resolve, reject) => {
1010
+ writeStream.on("finish", () => {
1011
+ try {
1012
+ fs__default.default.renameSync(tempPath, cachePath);
1013
+ resolve();
1014
+ } catch {
1015
+ fs__default.default.unlink(tempPath, () => {
1016
+ });
1017
+ reject(new Error("Could not cache Google Drive file: failed to finalize the cached file"));
1018
+ }
1019
+ });
1020
+ writeStream.on("error", (err) => {
1021
+ fs__default.default.unlink(tempPath, () => {
1022
+ });
1023
+ reject(err);
1024
+ });
1025
+ });
1026
+ }
1007
1027
  return {
1008
1028
  stream: res.data,
1009
1029
  mime: item.information.mime,
1010
- size: item.information.sizeInBytes
1030
+ size: item.information.sizeInBytes,
1031
+ cacheWritePromise
1011
1032
  };
1012
1033
  },
1013
1034
  getThumbnail: async (item, accountId) => {
@@ -1271,28 +1292,44 @@ var driveAddSignedUrlToken = (item, config) => {
1271
1292
  var driveAddSignedUrlTokens = (items, config) => {
1272
1293
  return items.map((item) => driveAddSignedUrlToken(item, config));
1273
1294
  };
1274
- var driveReadFile = async (file) => {
1295
+ var driveReadFile = async (file, options) => {
1275
1296
  let drive;
1276
1297
  if (typeof file === "string") {
1277
1298
  const doc = await drive_default.findById(file);
1278
- if (!doc) throw new Error("Could not read file: the file no longer exists");
1299
+ if (!doc) throw new DriveError("NOT_FOUND", "Could not read file: the file no longer exists");
1279
1300
  drive = doc;
1280
1301
  } else if ("toClient" in file) {
1281
1302
  drive = file;
1282
1303
  } else {
1283
- throw new Error("Could not read file: invalid file reference provided");
1304
+ throw new DriveError("INVALID_REFERENCE", "Could not read file: invalid file reference provided");
1284
1305
  }
1285
1306
  if (drive.information.type !== "FILE") {
1286
- throw new Error("Could not read file: this item is a folder, not a file");
1307
+ throw new DriveError("INVALID_TYPE", "Could not read file: this item is a folder, not a file");
1287
1308
  }
1288
1309
  const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1289
1310
  const accountId = drive.storageAccountId?.toString();
1290
- return await provider.openStream(drive, accountId);
1311
+ const driveId = String(drive._id);
1312
+ let result;
1313
+ if (provider === GoogleDriveProvider && cacheWriteLocks.has(driveId)) {
1314
+ result = await provider.openStream(drive, accountId, { skipCache: true });
1315
+ } else {
1316
+ result = await provider.openStream(drive, accountId);
1317
+ if (result.cacheWritePromise) {
1318
+ const cacheWritePromise = result.cacheWritePromise;
1319
+ cacheWritePromise.catch(() => {
1320
+ });
1321
+ cacheWriteLocks.set(driveId, cacheWritePromise);
1322
+ cacheWritePromise.finally(() => {
1323
+ if (cacheWriteLocks.get(driveId) === cacheWritePromise) cacheWriteLocks.delete(driveId);
1324
+ });
1325
+ }
1326
+ }
1327
+ return { ...result, stream: instrumentReadableForProgress(result.stream, result.size, options) };
1291
1328
  };
1292
1329
  var driveInfo = async (source) => {
1293
1330
  const fileId = typeof source === "string" ? source : source.id;
1294
1331
  const drive = await drive_default.findById(fileId);
1295
- if (!drive) throw new Error("Could not load file details: the file no longer exists");
1332
+ if (!drive) throw new DriveError("NOT_FOUND", "Could not load file details: the file no longer exists");
1296
1333
  let parentName;
1297
1334
  if (drive.parentId) {
1298
1335
  const parent = await drive_default.findById(drive.parentId);
@@ -1327,19 +1364,107 @@ var driveInfo = async (source) => {
1327
1364
  }
1328
1365
  return info;
1329
1366
  };
1330
- var driveFilePath = async (file) => {
1367
+ var computeTransferPercentage = (bytesDownloaded, totalBytes) => {
1368
+ if (!totalBytes) return 0;
1369
+ return Math.min(100, Math.round(bytesDownloaded / totalBytes * 100));
1370
+ };
1371
+ var cacheWriteLocks = /* @__PURE__ */ new Map();
1372
+ var inFlightGoogleDownloads = /* @__PURE__ */ new Map();
1373
+ var downloadGoogleFileToCache = async (drive, accountId, cachePath, totalBytes, listeners, controller) => {
1374
+ if (controller.signal.aborted) {
1375
+ throw new DriveError("ABORTED", "Could not download file: the download was aborted");
1376
+ }
1377
+ const { stream } = await GoogleDriveProvider.openStream(drive, accountId, { skipCache: true });
1378
+ if (controller.signal.aborted) {
1379
+ stream.destroy();
1380
+ throw new DriveError("ABORTED", "Could not download file: the download was aborted");
1381
+ }
1382
+ const fileDir = path__default.default.dirname(cachePath);
1383
+ if (!fs__default.default.existsSync(fileDir)) fs__default.default.mkdirSync(fileDir, { recursive: true });
1384
+ const tempPath = `${cachePath}.tmp`;
1385
+ const writeStream = fs__default.default.createWriteStream(tempPath);
1386
+ const cleanupTempFile = () => fs__default.default.unlink(tempPath, () => {
1387
+ });
1388
+ const onAbort = () => {
1389
+ const abortError = new DriveError("ABORTED", "Could not download file: the download was aborted");
1390
+ stream.destroy(abortError);
1391
+ writeStream.destroy(abortError);
1392
+ };
1393
+ controller.signal.addEventListener("abort", onAbort, { once: true });
1394
+ try {
1395
+ let bytesDownloaded = 0;
1396
+ await new Promise((resolve, reject) => {
1397
+ stream.on("data", (chunk) => {
1398
+ bytesDownloaded += chunk.length;
1399
+ const info = {
1400
+ bytesDownloaded,
1401
+ totalBytes,
1402
+ percentage: computeTransferPercentage(bytesDownloaded, totalBytes)
1403
+ };
1404
+ for (const listener of listeners) listener(info);
1405
+ });
1406
+ stream.pipe(writeStream);
1407
+ writeStream.on("finish", resolve);
1408
+ writeStream.on("error", reject);
1409
+ stream.on("error", reject);
1410
+ });
1411
+ } catch (err) {
1412
+ cleanupTempFile();
1413
+ if (controller.signal.aborted) {
1414
+ throw new DriveError("ABORTED", "Could not download file: the download was aborted");
1415
+ }
1416
+ throw new DriveError("DOWNLOAD_FAILED", `Could not download file: ${err instanceof Error ? err.message : "an unknown error occurred"}`);
1417
+ } finally {
1418
+ controller.signal.removeEventListener("abort", onAbort);
1419
+ }
1420
+ try {
1421
+ fs__default.default.renameSync(tempPath, cachePath);
1422
+ } catch (err) {
1423
+ if (err instanceof Error && "code" in err && err.code === "EXDEV") {
1424
+ fs__default.default.copyFileSync(tempPath, cachePath);
1425
+ fs__default.default.unlinkSync(tempPath);
1426
+ } else {
1427
+ cleanupTempFile();
1428
+ throw new DriveError("DOWNLOAD_FAILED", "Could not download file: failed to finalize the cached file");
1429
+ }
1430
+ }
1431
+ };
1432
+ var instrumentReadableForProgress = (source, totalBytes, options) => {
1433
+ if (!options?.onProgress && !options?.signal) return source;
1434
+ const passThrough = new stream.PassThrough();
1435
+ let bytesDownloaded = 0;
1436
+ source.on("data", (chunk) => {
1437
+ bytesDownloaded += chunk.length;
1438
+ options.onProgress?.({
1439
+ bytesDownloaded,
1440
+ totalBytes,
1441
+ percentage: computeTransferPercentage(bytesDownloaded, totalBytes)
1442
+ });
1443
+ });
1444
+ source.on("error", (err) => passThrough.destroy(err));
1445
+ source.pipe(passThrough);
1446
+ if (options.signal) {
1447
+ const onAbort = () => {
1448
+ source.destroy(new DriveError("ABORTED", "Could not read file: the read was aborted"));
1449
+ };
1450
+ if (options.signal.aborted) onAbort();
1451
+ else options.signal.addEventListener("abort", onAbort, { once: true });
1452
+ }
1453
+ return passThrough;
1454
+ };
1455
+ var driveFilePath = async (file, options) => {
1331
1456
  let drive;
1332
1457
  if (typeof file === "string") {
1333
1458
  const doc = await drive_default.findById(file);
1334
- if (!doc) throw new Error("Could not locate file: the file no longer exists");
1459
+ if (!doc) throw new DriveError("NOT_FOUND", "Could not locate file: the file no longer exists");
1335
1460
  drive = doc;
1336
1461
  } else if ("toClient" in file) {
1337
1462
  drive = file;
1338
1463
  } else {
1339
- throw new Error("Could not locate file: invalid file reference provided");
1464
+ throw new DriveError("INVALID_REFERENCE", "Could not locate file: invalid file reference provided");
1340
1465
  }
1341
1466
  if (drive.information.type !== "FILE") {
1342
- throw new Error("Could not locate file: this item is a folder, not a file");
1467
+ throw new DriveError("INVALID_TYPE", "Could not locate file: this item is a folder, not a file");
1343
1468
  }
1344
1469
  const config = getDriveConfig();
1345
1470
  const STORAGE_PATH = config.storage.path;
@@ -1347,7 +1472,7 @@ var driveFilePath = async (file) => {
1347
1472
  if (providerType === "LOCAL") {
1348
1473
  const filePath = path__default.default.join(STORAGE_PATH, "file", String(drive._id), "data.bin");
1349
1474
  if (!fs__default.default.existsSync(filePath)) {
1350
- throw new Error("Could not locate file: the stored file is missing from disk");
1475
+ throw new DriveError("FILE_MISSING", "Could not locate file: the stored file is missing from disk");
1351
1476
  }
1352
1477
  return Object.freeze({
1353
1478
  path: filePath,
@@ -1358,11 +1483,34 @@ var driveFilePath = async (file) => {
1358
1483
  });
1359
1484
  }
1360
1485
  if (providerType === "GOOGLE") {
1361
- const fileDir = path__default.default.join(STORAGE_PATH, "file", String(drive._id));
1486
+ const driveId = String(drive._id);
1487
+ const fileDir = path__default.default.join(STORAGE_PATH, "file", driveId);
1362
1488
  const cachedFilePath = path__default.default.join(fileDir, "data.bin");
1489
+ const expectedSize = drive.information.sizeInBytes;
1490
+ const isCached = () => {
1491
+ if (!fs__default.default.existsSync(cachedFilePath)) return false;
1492
+ return fs__default.default.statSync(cachedFilePath).size === expectedSize;
1493
+ };
1494
+ if (isCached()) {
1495
+ return Object.freeze({
1496
+ path: cachedFilePath,
1497
+ name: drive.name,
1498
+ mime: drive.information.mime,
1499
+ size: drive.information.sizeInBytes,
1500
+ provider: "GOOGLE"
1501
+ });
1502
+ }
1363
1503
  if (fs__default.default.existsSync(cachedFilePath)) {
1364
- const stats = fs__default.default.statSync(cachedFilePath);
1365
- if (stats.size === drive.information.sizeInBytes) {
1504
+ fs__default.default.unlinkSync(cachedFilePath);
1505
+ }
1506
+ const accountId = drive.storageAccountId?.toString();
1507
+ const existingLock = cacheWriteLocks.get(driveId);
1508
+ if (existingLock) {
1509
+ try {
1510
+ await existingLock;
1511
+ } catch {
1512
+ }
1513
+ if (isCached()) {
1366
1514
  return Object.freeze({
1367
1515
  path: cachedFilePath,
1368
1516
  name: drive.name,
@@ -1371,31 +1519,26 @@ var driveFilePath = async (file) => {
1371
1519
  provider: "GOOGLE"
1372
1520
  });
1373
1521
  }
1374
- fs__default.default.unlinkSync(cachedFilePath);
1375
1522
  }
1376
- const accountId = drive.storageAccountId?.toString();
1377
- const { stream } = await GoogleDriveProvider.openStream(drive, accountId);
1378
- if (!fs__default.default.existsSync(fileDir)) {
1379
- fs__default.default.mkdirSync(fileDir, { recursive: true });
1523
+ let entry = inFlightGoogleDownloads.get(driveId);
1524
+ if (!entry) {
1525
+ const controller = new AbortController();
1526
+ const listeners = /* @__PURE__ */ new Set();
1527
+ const promise = downloadGoogleFileToCache(drive, accountId, cachedFilePath, expectedSize, listeners, controller).finally(() => {
1528
+ inFlightGoogleDownloads.delete(driveId);
1529
+ if (cacheWriteLocks.get(driveId) === promise) cacheWriteLocks.delete(driveId);
1530
+ });
1531
+ entry = { promise, controller, listeners };
1532
+ inFlightGoogleDownloads.set(driveId, entry);
1533
+ cacheWriteLocks.set(driveId, promise);
1380
1534
  }
1381
- const tempPath = `${cachedFilePath}.tmp`;
1382
- const writeStream = fs__default.default.createWriteStream(tempPath);
1383
- await new Promise((resolve, reject) => {
1384
- stream.pipe(writeStream);
1385
- writeStream.on("finish", resolve);
1386
- writeStream.on("error", reject);
1387
- stream.on("error", reject);
1388
- });
1389
- try {
1390
- fs__default.default.renameSync(tempPath, cachedFilePath);
1391
- } catch (err) {
1392
- if (err instanceof Error && "code" in err && err.code === "EXDEV") {
1393
- fs__default.default.copyFileSync(tempPath, cachedFilePath);
1394
- fs__default.default.unlinkSync(tempPath);
1395
- } else {
1396
- throw err;
1397
- }
1535
+ const activeEntry = entry;
1536
+ if (options?.onProgress) activeEntry.listeners.add(options.onProgress);
1537
+ if (options?.signal) {
1538
+ if (options.signal.aborted) activeEntry.controller.abort();
1539
+ else options.signal.addEventListener("abort", () => activeEntry.controller.abort(), { once: true });
1398
1540
  }
1541
+ await activeEntry.promise;
1399
1542
  return Object.freeze({
1400
1543
  path: cachedFilePath,
1401
1544
  name: drive.name,
@@ -1404,7 +1547,7 @@ var driveFilePath = async (file) => {
1404
1547
  provider: "GOOGLE"
1405
1548
  });
1406
1549
  }
1407
- throw new Error(`Could not locate file: unsupported storage provider "${providerType}"`);
1550
+ throw new DriveError("UNSUPPORTED_PROVIDER", `Could not locate file: unsupported storage provider "${providerType}"`);
1408
1551
  };
1409
1552
  var driveList = async (options) => {
1410
1553
  const { key, folderId, accountId, limit = 100, afterId } = options;
@@ -1412,7 +1555,7 @@ var driveList = async (options) => {
1412
1555
  if (accountId && accountId !== "LOCAL") {
1413
1556
  const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
1414
1557
  if (!account) {
1415
- throw new Error("Could not list files: storage account not found or access denied");
1558
+ throw new DriveError("ACCESS_DENIED", "Could not list files: storage account not found or access denied");
1416
1559
  }
1417
1560
  if (account.metadata.provider === "GOOGLE") {
1418
1561
  providerName = "GOOGLE";
@@ -1440,7 +1583,7 @@ var driveListFiles = async (options) => {
1440
1583
  if (accountId && accountId !== "LOCAL") {
1441
1584
  const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
1442
1585
  if (!account) {
1443
- throw new Error("Could not load files: storage account not found or access denied");
1586
+ throw new DriveError("ACCESS_DENIED", "Could not load files: storage account not found or access denied");
1444
1587
  }
1445
1588
  if (account.metadata.provider === "GOOGLE") {
1446
1589
  providerName = "GOOGLE";
@@ -1484,7 +1627,7 @@ var driveDelete = async (source, options) => {
1484
1627
  let driveId;
1485
1628
  if (typeof source === "string") {
1486
1629
  const doc = await drive_default.findById(source);
1487
- if (!doc) throw new Error("Could not delete: the file no longer exists");
1630
+ if (!doc) throw new DriveError("NOT_FOUND", "Could not delete: the file no longer exists");
1488
1631
  drive = doc;
1489
1632
  driveId = source;
1490
1633
  } else if ("toClient" in source) {
@@ -1492,7 +1635,7 @@ var driveDelete = async (source, options) => {
1492
1635
  driveId = String(drive._id);
1493
1636
  } else {
1494
1637
  const doc = await drive_default.findById(source.id);
1495
- if (!doc) throw new Error("Could not delete: the selected file no longer exists");
1638
+ if (!doc) throw new DriveError("NOT_FOUND", "Could not delete: the selected file no longer exists");
1496
1639
  drive = doc;
1497
1640
  driveId = source.id;
1498
1641
  }
@@ -1504,7 +1647,7 @@ var driveDelete = async (source, options) => {
1504
1647
  trashedAt: null
1505
1648
  });
1506
1649
  if (childCount > 0) {
1507
- throw new Error(`Could not delete folder: it still contains ${childCount} item(s). Enable recursive delete to remove the folder and everything inside it.`);
1650
+ throw new DriveError("FOLDER_NOT_EMPTY", `Could not delete folder: it still contains ${childCount} item(s). Enable recursive delete to remove the folder and everything inside it.`);
1508
1651
  }
1509
1652
  }
1510
1653
  const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
@@ -1515,17 +1658,17 @@ var driveDelete = async (source, options) => {
1515
1658
  var resolveFolderByPath = async (folderPath, owner, accountId) => {
1516
1659
  const normalizedPath = folderPath.replace(/^\/+|\/+$/g, "");
1517
1660
  if (!normalizedPath) {
1518
- throw new Error("Could not resolve folder: the folder path is empty");
1661
+ throw new DriveError("INVALID_PATH", "Could not resolve folder: the folder path is empty");
1519
1662
  }
1520
1663
  const segments = normalizedPath.split("/").filter((s) => s.length > 0);
1521
1664
  if (segments.length === 0) {
1522
- throw new Error("Could not resolve folder: the folder path is invalid");
1665
+ throw new DriveError("INVALID_PATH", "Could not resolve folder: the folder path is invalid");
1523
1666
  }
1524
1667
  let providerName = "LOCAL";
1525
1668
  if (accountId && accountId !== "LOCAL") {
1526
1669
  const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner });
1527
1670
  if (!account) {
1528
- throw new Error("Could not resolve folder: storage account not found or access denied");
1671
+ throw new DriveError("ACCESS_DENIED", "Could not resolve folder: storage account not found or access denied");
1529
1672
  }
1530
1673
  if (account.metadata.provider === "GOOGLE") {
1531
1674
  providerName = "GOOGLE";
@@ -1559,7 +1702,7 @@ var driveUpload = async (source, key, options) => {
1559
1702
  if (accountId && accountId !== "LOCAL") {
1560
1703
  const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
1561
1704
  if (!account) {
1562
- throw new Error("Could not upload: storage account not found or access denied");
1705
+ throw new DriveError("ACCESS_DENIED", "Could not upload: storage account not found or access denied");
1563
1706
  }
1564
1707
  if (account.metadata.provider === "GOOGLE") {
1565
1708
  provider = GoogleDriveProvider;
@@ -1570,7 +1713,7 @@ var driveUpload = async (source, key, options) => {
1570
1713
  let fileSize;
1571
1714
  if (typeof source === "string") {
1572
1715
  if (!fs__default.default.existsSync(source)) {
1573
- throw new Error("Could not upload: source file not found");
1716
+ throw new DriveError("FILE_MISSING", "Could not upload: source file not found");
1574
1717
  }
1575
1718
  sourceFilePath = source;
1576
1719
  const stats = fs__default.default.statSync(source);
@@ -1629,17 +1772,17 @@ var driveUpload = async (source, key, options) => {
1629
1772
  mimeType = mimeTypes[ext] || "application/octet-stream";
1630
1773
  }
1631
1774
  if (config.security && !validateMimeType(mimeType, config.security.allowedMimeTypes)) {
1632
- throw new Error(`Could not upload: file type "${mimeType}" is not allowed`);
1775
+ throw new DriveError("INVALID_MIME_TYPE", `Could not upload: file type "${mimeType}" is not allowed`);
1633
1776
  }
1634
1777
  if (config.security && fileSize > config.security.maxUploadSizeInBytes) {
1635
- throw new Error("Could not upload: file is larger than the maximum allowed size");
1778
+ throw new DriveError("FILE_TOO_LARGE", "Could not upload: file is larger than the maximum allowed size");
1636
1779
  }
1637
1780
  const isRootMode = config.mode === "ROOT";
1638
1781
  if (!options.enforce && !isRootMode) {
1639
1782
  const information = await getDriveInformation({ method: "KEY", key });
1640
1783
  const quota = await provider.getQuota(key, accountId, information.storage.quotaInBytes);
1641
1784
  if (quota.usedInBytes + fileSize > quota.quotaInBytes) {
1642
- throw new Error("Could not upload: you have run out of storage space");
1785
+ throw new DriveError("QUOTA_EXCEEDED", "Could not upload: you have run out of storage space");
1643
1786
  }
1644
1787
  }
1645
1788
  let resolvedParentId = null;
@@ -1751,7 +1894,7 @@ var driveCleanup = async () => {
1751
1894
  }
1752
1895
  }
1753
1896
  try {
1754
- const { getTusServer: getTusServer2 } = await import('./tus-5LLFMVQK.cjs');
1897
+ const { getTusServer: getTusServer2 } = await import('./tus-EQ2IXSRE.cjs');
1755
1898
  await getTusServer2().cleanUpExpiredUploads();
1756
1899
  } catch (e) {
1757
1900
  console.error("[next-drive] Failed to clean up expired tus uploads:", e);
@@ -1840,8 +1983,13 @@ var getTusServer = () => {
1840
1983
  if (!ctx) return Number.MAX_SAFE_INTEGER;
1841
1984
  return ctx.authenticated ? ctx.config.security?.maxUploadSizeInBytes ?? Number.MAX_SAFE_INTEGER : ctx.config.security?.unauthenticated?.maxUploadSizeInBytes ?? 0;
1842
1985
  },
1843
- // ** Keep the upload id in the query string so every method stays on `?action=upload`.
1844
- generateUrl: (_req, { path: p, id }) => `${p}?action=upload&id=${id}`,
1986
+ // ** Keep the upload id in the query string so every method stays on `?action=upload`. Derive
1987
+ // ** the path from the ACTUAL incoming request rather than the static `apiPath` this server was
1988
+ // ** constructed with - the two can drift apart (config.apiUrl is frozen on `globalThis` for the
1989
+ // ** life of the process, see getTusServer() above, so it can go stale after a proxy/route change
1990
+ // ** without a full restart). Mirroring the request's own path makes the continuation URL always
1991
+ // ** match wherever the client actually is, regardless of what `apiUrl` was configured with.
1992
+ generateUrl: (req, { id }) => `${new URL(req.url).pathname}?action=upload&id=${id}`,
1845
1993
  // ** SECURITY: the id becomes a filesystem path inside FileStore (fs.unlink/read/resolve), and it
1846
1994
  // ** arrives from the client-controlled query string. Only accept the exact format our
1847
1995
  // ** namingFunction produces (32 lowercase hex) so a crafted `?id=../../..` cannot traverse out
@@ -1996,6 +2144,7 @@ var handleUpload = async (req, res, ctx) => {
1996
2144
  res.end(response.body ? Buffer.from(await response.arrayBuffer()) : void 0);
1997
2145
  };
1998
2146
 
2147
+ exports.DriveError = DriveError;
1999
2148
  exports.GoogleDriveProvider = GoogleDriveProvider;
2000
2149
  exports.LocalStorageProvider = LocalStorageProvider;
2001
2150
  exports.account_default = account_default;
@@ -2021,5 +2170,5 @@ exports.resolveProvider = resolveProvider;
2021
2170
  exports.resolveTusCorsOptions = resolveTusCorsOptions;
2022
2171
  exports.withSignedUrl = withSignedUrl;
2023
2172
  exports.withSignedUrls = withSignedUrls;
2024
- //# sourceMappingURL=chunk-HWNIRA6Y.cjs.map
2025
- //# sourceMappingURL=chunk-HWNIRA6Y.cjs.map
2173
+ //# sourceMappingURL=chunk-DHIHCER6.cjs.map
2174
+ //# sourceMappingURL=chunk-DHIHCER6.cjs.map