@muhgholy/next-drive 4.23.6 → 4.23.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-TA6L5FYG.cjs → chunk-HQTC3554.cjs} +47 -2
- package/dist/chunk-HQTC3554.cjs.map +1 -0
- package/dist/{chunk-TMSG5WJZ.js → chunk-OSYRIHH4.js} +47 -3
- package/dist/chunk-OSYRIHH4.js.map +1 -0
- package/dist/{chunk-VIB7R4JN.cjs → chunk-OU5TKLHV.cjs} +929 -832
- package/dist/chunk-OU5TKLHV.cjs.map +1 -0
- package/dist/{chunk-GV4HB2G6.js → chunk-RBSFEEJJ.js} +926 -829
- package/dist/chunk-RBSFEEJJ.js.map +1 -0
- package/dist/client/components/drive/{RenameAccountDialog.d.ts → account/rename.d.ts} +2 -2
- package/dist/client/components/drive/account/rename.d.ts.map +1 -0
- package/dist/client/components/drive/{dnd-provider.d.ts → dnd/context.d.ts} +1 -1
- package/dist/client/components/drive/dnd/context.d.ts.map +1 -0
- package/dist/client/components/drive/{CreateFolderDialog.d.ts → folder/create.d.ts} +2 -2
- package/dist/client/components/drive/folder/create.d.ts.map +1 -0
- package/dist/client/components/drive/{RenameDialog.d.ts → item/rename.d.ts} +3 -3
- package/dist/client/components/drive/item/rename.d.ts.map +1 -0
- package/dist/client/components/{dialog.d.ts → shared/confirm.d.ts} +2 -2
- package/dist/client/components/shared/confirm.d.ts.map +1 -0
- package/dist/client/components/ui/{alert-dialog.d.ts → alert-modal.d.ts} +1 -1
- package/dist/client/components/ui/alert-modal.d.ts.map +1 -0
- package/dist/client/components/ui/{dialog-fullscreen.d.ts → fullscreen.d.ts} +1 -1
- package/dist/client/components/ui/fullscreen.d.ts.map +1 -0
- package/dist/client/components/ui/{dialog.d.ts → modal.d.ts} +1 -1
- package/dist/client/components/ui/modal.d.ts.map +1 -0
- package/dist/client/context.d.ts.map +1 -1
- package/dist/client/file-chooser.d.ts.map +1 -1
- package/dist/client/hooks/{useUpload.d.ts → use-upload.d.ts} +1 -1
- package/dist/client/hooks/use-upload.d.ts.map +1 -0
- package/dist/client/index.cjs +351 -279
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.css +1 -1
- package/dist/client/index.d.ts +12 -11
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +245 -173
- package/dist/client/index.js.map +1 -1
- package/dist/client/utils.d.ts +1 -0
- package/dist/client/utils.d.ts.map +1 -1
- package/dist/server/actions/auth.d.ts +4 -0
- package/dist/server/actions/auth.d.ts.map +1 -0
- package/dist/server/actions/cors.d.ts +4 -0
- package/dist/server/actions/cors.d.ts.map +1 -0
- package/dist/server/actions/drive.d.ts +18 -0
- package/dist/server/actions/drive.d.ts.map +1 -0
- package/dist/server/actions/public.d.ts +4 -0
- package/dist/server/actions/public.d.ts.map +1 -0
- package/dist/server/actions/shared.d.ts +14 -0
- package/dist/server/actions/shared.d.ts.map +1 -0
- package/dist/server/express.cjs +12 -12
- package/dist/server/express.js +3 -3
- package/dist/server/hono.cjs +12 -12
- package/dist/server/hono.js +3 -3
- package/dist/server/index.cjs +18 -18
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/security/{cryptoUtils.d.ts → crypto-utils.d.ts} +1 -1
- package/dist/server/security/crypto-utils.d.ts.map +1 -0
- package/dist/server/security/{mimeFilter.d.ts → mime-filter.d.ts} +1 -1
- package/dist/server/security/mime-filter.d.ts.map +1 -0
- package/dist/server/storage-adapters/google.d.ts.map +1 -0
- package/dist/server/storage-adapters/local.d.ts.map +1 -0
- package/dist/server/utils/{folderValidation.d.ts → folder-validation.d.ts} +1 -1
- package/dist/server/utils/folder-validation.d.ts.map +1 -0
- package/dist/server/utils/{imageConvert.d.ts → image-convert.d.ts} +1 -1
- package/dist/server/utils/image-convert.d.ts.map +1 -0
- package/dist/types/lib/database/index.d.ts +2 -2
- package/dist/types/lib/database/index.d.ts.map +1 -1
- package/dist/types/server/index.d.ts +5 -5
- package/dist/types/server/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GV4HB2G6.js.map +0 -1
- package/dist/chunk-TA6L5FYG.cjs.map +0 -1
- package/dist/chunk-TMSG5WJZ.js.map +0 -1
- package/dist/chunk-VIB7R4JN.cjs.map +0 -1
- package/dist/client/components/dialog.d.ts.map +0 -1
- package/dist/client/components/drive/CreateFolderDialog.d.ts.map +0 -1
- package/dist/client/components/drive/RenameAccountDialog.d.ts.map +0 -1
- package/dist/client/components/drive/RenameDialog.d.ts.map +0 -1
- package/dist/client/components/drive/dnd-provider.d.ts.map +0 -1
- package/dist/client/components/ui/alert-dialog.d.ts.map +0 -1
- package/dist/client/components/ui/dialog-fullscreen.d.ts.map +0 -1
- package/dist/client/components/ui/dialog.d.ts.map +0 -1
- package/dist/client/hooks/useUpload.d.ts.map +0 -1
- package/dist/server/providers/google.d.ts.map +0 -1
- package/dist/server/providers/local.d.ts.map +0 -1
- package/dist/server/security/cryptoUtils.d.ts.map +0 -1
- package/dist/server/security/mimeFilter.d.ts.map +0 -1
- package/dist/server/utils/folderValidation.d.ts.map +0 -1
- package/dist/server/utils/imageConvert.d.ts.map +0 -1
- /package/dist/server/{providers → storage-adapters}/google.d.ts +0 -0
- /package/dist/server/{providers → storage-adapters}/local.d.ts +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import
|
|
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
|
|
6
|
-
import
|
|
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/
|
|
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 =
|
|
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";
|
|
@@ -529,11 +582,11 @@ var LocalStorageProvider = {
|
|
|
529
582
|
return { usedInBytes, quotaInBytes: configuredQuotaInBytes ?? 0 };
|
|
530
583
|
},
|
|
531
584
|
openStream: async (item, accountId) => {
|
|
532
|
-
if (item.information.type !== "FILE") throw new Error("
|
|
585
|
+
if (item.information.type !== "FILE") throw new Error("Could not open local file: folders cannot be streamed");
|
|
533
586
|
const storagePath = getDriveConfig().storage.path;
|
|
534
587
|
const filePath = path.join(storagePath, "file", item._id.toString(), "data.bin");
|
|
535
588
|
if (!fs.existsSync(filePath)) {
|
|
536
|
-
throw new Error("
|
|
589
|
+
throw new Error("Could not open local file: it is missing from disk");
|
|
537
590
|
}
|
|
538
591
|
const stat = fs.statSync(filePath);
|
|
539
592
|
const stream = fs.createReadStream(filePath);
|
|
@@ -544,13 +597,13 @@ var LocalStorageProvider = {
|
|
|
544
597
|
};
|
|
545
598
|
},
|
|
546
599
|
getThumbnail: async (item, accountId) => {
|
|
547
|
-
if (item.information.type !== "FILE") throw new Error("No
|
|
600
|
+
if (item.information.type !== "FILE") throw new Error("No preview available: folders do not have thumbnails");
|
|
548
601
|
const storagePath = getDriveConfig().storage.path;
|
|
549
602
|
const fileDir = path.join(storagePath, "file", item._id.toString());
|
|
550
603
|
const originalPath = path.join(fileDir, "data.bin");
|
|
551
604
|
const thumbDir = path.join(fileDir, "cache");
|
|
552
605
|
const thumbPath = path.join(thumbDir, "thumbnail.webp");
|
|
553
|
-
if (!fs.existsSync(originalPath)) throw new Error("
|
|
606
|
+
if (!fs.existsSync(originalPath)) throw new Error("Could not generate preview: the original file is missing");
|
|
554
607
|
if (fs.existsSync(thumbPath)) {
|
|
555
608
|
return fs.createReadStream(thumbPath);
|
|
556
609
|
}
|
|
@@ -599,12 +652,12 @@ var LocalStorageProvider = {
|
|
|
599
652
|
return folder.toClient();
|
|
600
653
|
},
|
|
601
654
|
uploadFile: async (drive, filePath, accountId) => {
|
|
602
|
-
if (drive.information.type !== "FILE") throw new Error("
|
|
655
|
+
if (drive.information.type !== "FILE") throw new Error("Could not save local file: invalid file record");
|
|
603
656
|
const storagePath = getDriveConfig().storage.path;
|
|
604
657
|
const destDir = path.join(storagePath, "file", String(drive._id));
|
|
605
658
|
const destPath = path.join(destDir, "data.bin");
|
|
606
659
|
if (!fs.existsSync(filePath)) {
|
|
607
|
-
throw new Error("
|
|
660
|
+
throw new Error("Could not save local file: the uploaded data is missing");
|
|
608
661
|
}
|
|
609
662
|
if (!fs.existsSync(destDir)) {
|
|
610
663
|
fs.mkdirSync(destDir, { recursive: true });
|
|
@@ -620,12 +673,12 @@ var LocalStorageProvider = {
|
|
|
620
673
|
}
|
|
621
674
|
}
|
|
622
675
|
if (!fs.existsSync(destPath)) {
|
|
623
|
-
throw new Error("
|
|
676
|
+
throw new Error("Could not save local file: writing to storage failed");
|
|
624
677
|
}
|
|
625
678
|
const destStats = fs.statSync(destPath);
|
|
626
679
|
if (destStats.size !== drive.information.sizeInBytes) {
|
|
627
680
|
fs.unlinkSync(destPath);
|
|
628
|
-
throw new Error(
|
|
681
|
+
throw new Error("Could not save local file: the stored data was incomplete (size mismatch)");
|
|
629
682
|
}
|
|
630
683
|
drive.status = "READY";
|
|
631
684
|
drive.information.path = path.join("file", String(drive._id), "data.bin");
|
|
@@ -670,12 +723,12 @@ var LocalStorageProvider = {
|
|
|
670
723
|
},
|
|
671
724
|
rename: async (id, newName, owner, accountId) => {
|
|
672
725
|
const item = await drive_default.findOneAndUpdate({ _id: id, owner }, { name: newName }, { new: true });
|
|
673
|
-
if (!item) throw new Error("
|
|
726
|
+
if (!item) throw new Error("Could not rename: the item no longer exists");
|
|
674
727
|
return item.toClient();
|
|
675
728
|
},
|
|
676
729
|
move: async (id, newParentId, owner, accountId) => {
|
|
677
730
|
const item = await drive_default.findOne({ _id: id, owner });
|
|
678
|
-
if (!item) throw new Error("
|
|
731
|
+
if (!item) throw new Error("Could not move: the item no longer exists");
|
|
679
732
|
item.parentId = newParentId === "root" || !newParentId ? null : new mongoose.Types.ObjectId(newParentId);
|
|
680
733
|
await item.save();
|
|
681
734
|
return item.toClient();
|
|
@@ -713,18 +766,18 @@ 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/
|
|
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;
|
|
720
773
|
const account = await account_default.findOne(query);
|
|
721
|
-
if (!account) throw new Error("Google Drive account not connected");
|
|
774
|
+
if (!account) throw new Error("Could not reach Google Drive: account not connected");
|
|
722
775
|
const config = getDriveConfig();
|
|
723
776
|
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
724
|
-
if (!clientId || !clientSecret) throw new Error("Google credentials not configured on server");
|
|
777
|
+
if (!clientId || !clientSecret) throw new Error("Could not reach Google Drive: Google credentials are not configured on the server");
|
|
725
778
|
const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
|
726
779
|
if (account.metadata.provider !== "GOOGLE" || !account.metadata.google) {
|
|
727
|
-
throw new Error("
|
|
780
|
+
throw new Error("Could not reach Google Drive: account data is invalid, please reconnect");
|
|
728
781
|
}
|
|
729
782
|
oAuth2Client.setCredentials(account.metadata.google.credentials);
|
|
730
783
|
oAuth2Client.on("tokens", async (tokens) => {
|
|
@@ -946,8 +999,8 @@ var GoogleDriveProvider = {
|
|
|
946
999
|
openStream: async (item, accountId) => {
|
|
947
1000
|
const { client } = await createAuthClient(item.owner, accountId || item.storageAccountId?.toString());
|
|
948
1001
|
const drive = google.drive({ version: "v3", auth: client });
|
|
949
|
-
if (!item.provider?.google?.id) throw new Error("
|
|
950
|
-
if (item.information.type === "FOLDER") throw new Error("
|
|
1002
|
+
if (!item.provider?.google?.id) throw new Error("Could not open Google Drive file: its Google file ID is missing");
|
|
1003
|
+
if (item.information.type === "FOLDER") throw new Error("Could not open Google Drive file: folders cannot be streamed");
|
|
951
1004
|
const res = await drive.files.get(
|
|
952
1005
|
{ fileId: item.provider.google.id, alt: "media" },
|
|
953
1006
|
{ responseType: "stream" }
|
|
@@ -967,7 +1020,7 @@ var GoogleDriveProvider = {
|
|
|
967
1020
|
return fs.createReadStream(thumbPath);
|
|
968
1021
|
}
|
|
969
1022
|
const { client } = await createAuthClient(item.owner, accountId || item.storageAccountId?.toString());
|
|
970
|
-
if (!item.provider?.google?.thumbnailLink) throw new Error("No
|
|
1023
|
+
if (!item.provider?.google?.thumbnailLink) throw new Error("No preview available for this Google Drive file");
|
|
971
1024
|
const res = await client.request({ url: item.provider.google.thumbnailLink, responseType: "stream" });
|
|
972
1025
|
if (!fs.existsSync(thumbDir)) {
|
|
973
1026
|
fs.mkdirSync(thumbDir, { recursive: true });
|
|
@@ -1007,7 +1060,7 @@ var GoogleDriveProvider = {
|
|
|
1007
1060
|
fields: "id, name, mimeType, webViewLink, iconLink"
|
|
1008
1061
|
});
|
|
1009
1062
|
const file = res.data;
|
|
1010
|
-
if (!file.id) throw new Error("
|
|
1063
|
+
if (!file.id) throw new Error("Could not create folder on Google Drive");
|
|
1011
1064
|
const folder = new drive_default({
|
|
1012
1065
|
owner,
|
|
1013
1066
|
name: file.name,
|
|
@@ -1028,7 +1081,7 @@ var GoogleDriveProvider = {
|
|
|
1028
1081
|
return folder.toClient();
|
|
1029
1082
|
},
|
|
1030
1083
|
uploadFile: async (drive, filePath, accountId) => {
|
|
1031
|
-
if (drive.information.type !== "FILE") throw new Error("
|
|
1084
|
+
if (drive.information.type !== "FILE") throw new Error("Could not upload to Google Drive: invalid file record");
|
|
1032
1085
|
const { client } = await createAuthClient(drive.owner, accountId || drive.storageAccountId?.toString());
|
|
1033
1086
|
const googleDrive = google.drive({ version: "v3", auth: client });
|
|
1034
1087
|
let googleParentId = "root";
|
|
@@ -1050,7 +1103,7 @@ var GoogleDriveProvider = {
|
|
|
1050
1103
|
fields: "id, name, mimeType, webViewLink, iconLink, thumbnailLink, size"
|
|
1051
1104
|
});
|
|
1052
1105
|
const gFile = res.data;
|
|
1053
|
-
if (!gFile.id) throw new Error("
|
|
1106
|
+
if (!gFile.id) throw new Error("Could not upload to Google Drive: no file was created");
|
|
1054
1107
|
drive.status = "READY";
|
|
1055
1108
|
drive.provider = {
|
|
1056
1109
|
type: "GOOGLE",
|
|
@@ -1122,7 +1175,7 @@ var GoogleDriveProvider = {
|
|
|
1122
1175
|
const { client } = await createAuthClient(owner, accountId);
|
|
1123
1176
|
const drive = google.drive({ version: "v3", auth: client });
|
|
1124
1177
|
const item = await drive_default.findOne({ _id: id, owner });
|
|
1125
|
-
if (!item || !item.provider?.google?.id) throw new Error("
|
|
1178
|
+
if (!item || !item.provider?.google?.id) throw new Error("Could not rename on Google Drive: item not found or not synced");
|
|
1126
1179
|
await drive.files.update({
|
|
1127
1180
|
fileId: item.provider.google.id,
|
|
1128
1181
|
requestBody: { name: newName }
|
|
@@ -1135,7 +1188,7 @@ var GoogleDriveProvider = {
|
|
|
1135
1188
|
const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
|
|
1136
1189
|
const drive = google.drive({ version: "v3", auth: client });
|
|
1137
1190
|
const item = await drive_default.findOne({ _id: id, owner });
|
|
1138
|
-
if (!item || !item.provider?.google?.id) throw new Error("
|
|
1191
|
+
if (!item || !item.provider?.google?.id) throw new Error("Could not move on Google Drive: item not found or not synced");
|
|
1139
1192
|
let previousGoogleParentId = void 0;
|
|
1140
1193
|
if (item.parentId) {
|
|
1141
1194
|
const oldParent = await drive_default.findOne({ _id: item.parentId, owner });
|
|
@@ -1155,7 +1208,7 @@ var GoogleDriveProvider = {
|
|
|
1155
1208
|
let newGoogleParentId = "root";
|
|
1156
1209
|
if (newParentId && newParentId !== "root") {
|
|
1157
1210
|
const newParent = await drive_default.findOne({ _id: newParentId, owner });
|
|
1158
|
-
if (!newParent || !newParent.provider?.google?.id) throw new Error("
|
|
1211
|
+
if (!newParent || !newParent.provider?.google?.id) throw new Error("Could not move on Google Drive: target folder not found");
|
|
1159
1212
|
newGoogleParentId = newParent.provider.google.id;
|
|
1160
1213
|
}
|
|
1161
1214
|
await drive.files.update({
|
|
@@ -1181,7 +1234,367 @@ var GoogleDriveProvider = {
|
|
|
1181
1234
|
}
|
|
1182
1235
|
};
|
|
1183
1236
|
|
|
1184
|
-
// src/server/
|
|
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 =
|
|
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 =
|
|
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";
|
|
@@ -1223,15 +1636,15 @@ var driveReadFile = async (file) => {
|
|
|
1223
1636
|
let drive;
|
|
1224
1637
|
if (typeof file === "string") {
|
|
1225
1638
|
const doc = await drive_default.findById(file);
|
|
1226
|
-
if (!doc) throw new Error(
|
|
1639
|
+
if (!doc) throw new Error("Could not read file: the file no longer exists");
|
|
1227
1640
|
drive = doc;
|
|
1228
1641
|
} else if ("toClient" in file) {
|
|
1229
1642
|
drive = file;
|
|
1230
1643
|
} else {
|
|
1231
|
-
throw new Error("
|
|
1644
|
+
throw new Error("Could not read file: invalid file reference provided");
|
|
1232
1645
|
}
|
|
1233
1646
|
if (drive.information.type !== "FILE") {
|
|
1234
|
-
throw new Error("
|
|
1647
|
+
throw new Error("Could not read file: this item is a folder, not a file");
|
|
1235
1648
|
}
|
|
1236
1649
|
const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
1237
1650
|
const accountId = drive.storageAccountId?.toString();
|
|
@@ -1240,7 +1653,7 @@ var driveReadFile = async (file) => {
|
|
|
1240
1653
|
var driveInfo = async (source) => {
|
|
1241
1654
|
const fileId = typeof source === "string" ? source : source.id;
|
|
1242
1655
|
const drive = await drive_default.findById(fileId);
|
|
1243
|
-
if (!drive) throw new Error(
|
|
1656
|
+
if (!drive) throw new Error("Could not load file details: the file no longer exists");
|
|
1244
1657
|
let parentName;
|
|
1245
1658
|
if (drive.parentId) {
|
|
1246
1659
|
const parent = await drive_default.findById(drive.parentId);
|
|
@@ -1279,15 +1692,15 @@ var driveFilePath = async (file) => {
|
|
|
1279
1692
|
let drive;
|
|
1280
1693
|
if (typeof file === "string") {
|
|
1281
1694
|
const doc = await drive_default.findById(file);
|
|
1282
|
-
if (!doc) throw new Error(
|
|
1695
|
+
if (!doc) throw new Error("Could not locate file: the file no longer exists");
|
|
1283
1696
|
drive = doc;
|
|
1284
1697
|
} else if ("toClient" in file) {
|
|
1285
1698
|
drive = file;
|
|
1286
1699
|
} else {
|
|
1287
|
-
throw new Error("
|
|
1700
|
+
throw new Error("Could not locate file: invalid file reference provided");
|
|
1288
1701
|
}
|
|
1289
1702
|
if (drive.information.type !== "FILE") {
|
|
1290
|
-
throw new Error("
|
|
1703
|
+
throw new Error("Could not locate file: this item is a folder, not a file");
|
|
1291
1704
|
}
|
|
1292
1705
|
const config = getDriveConfig();
|
|
1293
1706
|
const STORAGE_PATH = config.storage.path;
|
|
@@ -1295,7 +1708,7 @@ var driveFilePath = async (file) => {
|
|
|
1295
1708
|
if (providerType === "LOCAL") {
|
|
1296
1709
|
const filePath = path.join(STORAGE_PATH, "file", String(drive._id), "data.bin");
|
|
1297
1710
|
if (!fs.existsSync(filePath)) {
|
|
1298
|
-
throw new Error(
|
|
1711
|
+
throw new Error("Could not locate file: the stored file is missing from disk");
|
|
1299
1712
|
}
|
|
1300
1713
|
return Object.freeze({
|
|
1301
1714
|
path: filePath,
|
|
@@ -1352,7 +1765,7 @@ var driveFilePath = async (file) => {
|
|
|
1352
1765
|
provider: "GOOGLE"
|
|
1353
1766
|
});
|
|
1354
1767
|
}
|
|
1355
|
-
throw new Error(`
|
|
1768
|
+
throw new Error(`Could not locate file: unsupported storage provider "${providerType}"`);
|
|
1356
1769
|
};
|
|
1357
1770
|
var driveList = async (options) => {
|
|
1358
1771
|
const { key, folderId, accountId, limit = 100, afterId } = options;
|
|
@@ -1360,7 +1773,7 @@ var driveList = async (options) => {
|
|
|
1360
1773
|
if (accountId && accountId !== "LOCAL") {
|
|
1361
1774
|
const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
|
|
1362
1775
|
if (!account) {
|
|
1363
|
-
throw new Error("
|
|
1776
|
+
throw new Error("Could not list files: storage account not found or access denied");
|
|
1364
1777
|
}
|
|
1365
1778
|
if (account.metadata.provider === "GOOGLE") {
|
|
1366
1779
|
providerName = "GOOGLE";
|
|
@@ -1388,7 +1801,7 @@ var driveListFiles = async (options) => {
|
|
|
1388
1801
|
if (accountId && accountId !== "LOCAL") {
|
|
1389
1802
|
const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
|
|
1390
1803
|
if (!account) {
|
|
1391
|
-
throw new Error("
|
|
1804
|
+
throw new Error("Could not load files: storage account not found or access denied");
|
|
1392
1805
|
}
|
|
1393
1806
|
if (account.metadata.provider === "GOOGLE") {
|
|
1394
1807
|
providerName = "GOOGLE";
|
|
@@ -1432,7 +1845,7 @@ var driveDelete = async (source, options) => {
|
|
|
1432
1845
|
let driveId;
|
|
1433
1846
|
if (typeof source === "string") {
|
|
1434
1847
|
const doc = await drive_default.findById(source);
|
|
1435
|
-
if (!doc) throw new Error(
|
|
1848
|
+
if (!doc) throw new Error("Could not delete: the file no longer exists");
|
|
1436
1849
|
drive = doc;
|
|
1437
1850
|
driveId = source;
|
|
1438
1851
|
} else if ("toClient" in source) {
|
|
@@ -1440,7 +1853,7 @@ var driveDelete = async (source, options) => {
|
|
|
1440
1853
|
driveId = String(drive._id);
|
|
1441
1854
|
} else {
|
|
1442
1855
|
const doc = await drive_default.findById(source.id);
|
|
1443
|
-
if (!doc) throw new Error(
|
|
1856
|
+
if (!doc) throw new Error("Could not delete: the selected file no longer exists");
|
|
1444
1857
|
drive = doc;
|
|
1445
1858
|
driveId = source.id;
|
|
1446
1859
|
}
|
|
@@ -1452,7 +1865,7 @@ var driveDelete = async (source, options) => {
|
|
|
1452
1865
|
trashedAt: null
|
|
1453
1866
|
});
|
|
1454
1867
|
if (childCount > 0) {
|
|
1455
|
-
throw new Error(`
|
|
1868
|
+
throw new Error(`Could not delete folder: it still contains ${childCount} item(s). Enable recursive delete to remove the folder and everything inside it.`);
|
|
1456
1869
|
}
|
|
1457
1870
|
}
|
|
1458
1871
|
const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
@@ -1463,17 +1876,17 @@ var driveDelete = async (source, options) => {
|
|
|
1463
1876
|
var resolveFolderByPath = async (folderPath, owner, accountId) => {
|
|
1464
1877
|
const normalizedPath = folderPath.replace(/^\/+|\/+$/g, "");
|
|
1465
1878
|
if (!normalizedPath) {
|
|
1466
|
-
throw new Error("
|
|
1879
|
+
throw new Error("Could not resolve folder: the folder path is empty");
|
|
1467
1880
|
}
|
|
1468
1881
|
const segments = normalizedPath.split("/").filter((s) => s.length > 0);
|
|
1469
1882
|
if (segments.length === 0) {
|
|
1470
|
-
throw new Error("
|
|
1883
|
+
throw new Error("Could not resolve folder: the folder path is invalid");
|
|
1471
1884
|
}
|
|
1472
1885
|
let providerName = "LOCAL";
|
|
1473
1886
|
if (accountId && accountId !== "LOCAL") {
|
|
1474
1887
|
const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner });
|
|
1475
1888
|
if (!account) {
|
|
1476
|
-
throw new Error("
|
|
1889
|
+
throw new Error("Could not resolve folder: storage account not found or access denied");
|
|
1477
1890
|
}
|
|
1478
1891
|
if (account.metadata.provider === "GOOGLE") {
|
|
1479
1892
|
providerName = "GOOGLE";
|
|
@@ -1507,7 +1920,7 @@ var driveUpload = async (source, key, options) => {
|
|
|
1507
1920
|
if (accountId && accountId !== "LOCAL") {
|
|
1508
1921
|
const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
|
|
1509
1922
|
if (!account) {
|
|
1510
|
-
throw new Error("
|
|
1923
|
+
throw new Error("Could not upload: storage account not found or access denied");
|
|
1511
1924
|
}
|
|
1512
1925
|
if (account.metadata.provider === "GOOGLE") {
|
|
1513
1926
|
provider = GoogleDriveProvider;
|
|
@@ -1518,7 +1931,7 @@ var driveUpload = async (source, key, options) => {
|
|
|
1518
1931
|
let fileSize;
|
|
1519
1932
|
if (typeof source === "string") {
|
|
1520
1933
|
if (!fs.existsSync(source)) {
|
|
1521
|
-
throw new Error(
|
|
1934
|
+
throw new Error("Could not upload: source file not found");
|
|
1522
1935
|
}
|
|
1523
1936
|
sourceFilePath = source;
|
|
1524
1937
|
const stats = fs.statSync(source);
|
|
@@ -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-${
|
|
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-${
|
|
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);
|
|
@@ -1577,17 +1990,17 @@ var driveUpload = async (source, key, options) => {
|
|
|
1577
1990
|
mimeType = mimeTypes[ext] || "application/octet-stream";
|
|
1578
1991
|
}
|
|
1579
1992
|
if (config.security && !validateMimeType(mimeType, config.security.allowedMimeTypes)) {
|
|
1580
|
-
throw new Error(`
|
|
1993
|
+
throw new Error(`Could not upload: file type "${mimeType}" is not allowed`);
|
|
1581
1994
|
}
|
|
1582
1995
|
if (config.security && fileSize > config.security.maxUploadSizeInBytes) {
|
|
1583
|
-
throw new Error(
|
|
1996
|
+
throw new Error("Could not upload: file is larger than the maximum allowed size");
|
|
1584
1997
|
}
|
|
1585
1998
|
const isRootMode = config.mode === "ROOT";
|
|
1586
1999
|
if (!options.enforce && !isRootMode) {
|
|
1587
2000
|
const information = await getDriveInformation({ method: "KEY", key });
|
|
1588
2001
|
const quota = await provider.getQuota(key, accountId, information.storage.quotaInBytes);
|
|
1589
2002
|
if (quota.usedInBytes + fileSize > quota.quotaInBytes) {
|
|
1590
|
-
throw new Error("
|
|
2003
|
+
throw new Error("Could not upload: you have run out of storage space");
|
|
1591
2004
|
}
|
|
1592
2005
|
}
|
|
1593
2006
|
let resolvedParentId = null;
|
|
@@ -1708,832 +2121,516 @@ var driveCleanup = async () => {
|
|
|
1708
2121
|
}
|
|
1709
2122
|
return { removed, totalFreedInBytes };
|
|
1710
2123
|
};
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
|
1718
|
-
return
|
|
2140
|
+
var withSignedUrl = (item, config) => {
|
|
2141
|
+
return driveAddSignedUrlToken(item, config);
|
|
1719
2142
|
};
|
|
1720
|
-
var
|
|
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("Invalid Storage Account");
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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: "Failed to initialize drive 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 query parameter" });
|
|
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: "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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
if (Date.now() / 1e3 > expiry) {
|
|
1898
|
-
return res.status(401).json({ status: 401, message: "Token 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: "Invalid token" });
|
|
1904
|
-
}
|
|
1905
|
-
} catch (err) {
|
|
1906
|
-
return res.status(401).json({ status: 401, message: "Invalid token format" });
|
|
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
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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` });
|
|
2055
2247
|
}
|
|
2056
|
-
return res.status(400).json({ status: 400, message: "Unknown provider" });
|
|
2057
2248
|
}
|
|
2058
|
-
|
|
2059
|
-
const
|
|
2060
|
-
if (
|
|
2061
|
-
|
|
2062
|
-
|
|
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();
|
|
2088
|
-
}
|
|
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
|
-
}
|
|
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: "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
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
if (!listQuery.success) return res.status(400).json({ status: 400, message: "Invalid 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
|
-
|
|
2265
|
+
fileSize: fileSizeInBytes,
|
|
2266
|
+
mimeType: fileType,
|
|
2267
|
+
totalChunks
|
|
2169
2268
|
};
|
|
2170
|
-
|
|
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
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
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
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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: "Only POST allowed" });
|
|
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
|
-
|
|
2223
|
-
|
|
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
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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: "Missing upload ID for non-zero 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: `File type ${fileType} not allowed` });
|
|
2251
|
-
}
|
|
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: "Storage quota exceeded" });
|
|
2308
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
2309
|
+
if (streamError) {
|
|
2310
|
+
writeStream.destroy();
|
|
2311
|
+
throw streamError;
|
|
2258
2312
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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: "Upload session not found or expired" });
|
|
2280
|
-
}
|
|
2281
|
-
try {
|
|
2282
|
-
const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
|
|
2283
|
-
if (!chunkFile) throw new Error("No chunk file 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
|
-
}
|
|
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`);
|
|
2294
2317
|
}
|
|
2295
|
-
const
|
|
2296
|
-
|
|
2297
|
-
|
|
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(`Missing chunk part: ${i}`);
|
|
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
|
-
|
|
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("Failed to create merged file");
|
|
2339
|
-
}
|
|
2340
|
-
const finalStats = fs.statSync(finalTempPath);
|
|
2341
|
-
if (finalStats.size !== meta.fileSize) {
|
|
2342
|
-
throw new Error(`File size mismatch: expected ${meta.fileSize}, got ${finalStats.size}`);
|
|
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
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
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
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
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
|
-
|
|
2389
|
+
fs.rmSync(tempUploadDir, { recursive: true, force: true });
|
|
2421
2390
|
} catch (e) {
|
|
2422
|
-
console.error("
|
|
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
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
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
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
if (!drive) return res.status(404).json({ status: 404, message: "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
|
-
|
|
2484
|
-
|
|
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
|
|
2487
|
-
|
|
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(
|
|
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
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
if (!renameData.success) return res.status(400).json({ status: 400, message: "Invalid 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
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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
|
-
|
|
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-
|
|
2539
|
-
//# sourceMappingURL=chunk-
|
|
2635
|
+
//# sourceMappingURL=chunk-RBSFEEJJ.js.map
|
|
2636
|
+
//# sourceMappingURL=chunk-RBSFEEJJ.js.map
|