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