@muhgholy/next-drive 4.23.32 → 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.
- package/README.md +79 -3
- package/dist/{chunk-HWNIRA6Y.cjs → chunk-DHIHCER6.cjs} +220 -71
- package/dist/chunk-DHIHCER6.cjs.map +1 -0
- package/dist/{chunk-2DCZENJ2.js → chunk-E72MTT6W.js} +3 -3
- package/dist/{chunk-2DCZENJ2.js.map → chunk-E72MTT6W.js.map} +1 -1
- package/dist/{chunk-WUG4ATLH.cjs → chunk-PKNXKHGS.cjs} +37 -37
- package/dist/{chunk-WUG4ATLH.cjs.map → chunk-PKNXKHGS.cjs.map} +1 -1
- package/dist/{chunk-IWV6G7HQ.js → chunk-QMPV4YSZ.js} +221 -73
- package/dist/chunk-QMPV4YSZ.js.map +1 -0
- package/dist/server/controllers/drive.d.ts +33 -2
- package/dist/server/controllers/drive.d.ts.map +1 -1
- package/dist/server/errors.d.ts +12 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/express.cjs +16 -12
- package/dist/server/express.cjs.map +1 -1
- package/dist/server/express.d.ts +3 -0
- package/dist/server/express.d.ts.map +1 -1
- package/dist/server/express.js +4 -4
- package/dist/server/express.js.map +1 -1
- package/dist/server/hono.cjs +82 -13
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.d.ts +4 -1
- package/dist/server/hono.d.ts.map +1 -1
- package/dist/server/hono.js +72 -7
- package/dist/server/hono.js.map +1 -1
- package/dist/server/index.cjs +23 -19
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/storage-adapters/google.d.ts.map +1 -1
- package/dist/server/tus.d.ts.map +1 -1
- package/dist/tus-EQ2IXSRE.cjs +20 -0
- package/dist/tus-EQ2IXSRE.cjs.map +1 -0
- package/dist/tus-ODCMFEBZ.js +3 -0
- package/dist/tus-ODCMFEBZ.js.map +1 -0
- package/dist/types/server/storage.d.ts +4 -1
- package/dist/types/server/storage.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-HWNIRA6Y.cjs.map +0 -1
- package/dist/chunk-IWV6G7HQ.js.map +0 -1
- package/dist/tus-5LLFMVQK.cjs +0 -20
- package/dist/tus-5LLFMVQK.cjs.map +0 -1
- package/dist/tus-C4T5ZDTG.js +0 -3
- 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
|
-
|
|
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
|
-
|
|
994
|
-
if (!
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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-
|
|
2025
|
-
//# sourceMappingURL=chunk-
|
|
2173
|
+
//# sourceMappingURL=chunk-DHIHCER6.cjs.map
|
|
2174
|
+
//# sourceMappingURL=chunk-DHIHCER6.cjs.map
|