@muhgholy/next-drive 4.23.24 → 4.23.25
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-IVSBLEMY.cjs → chunk-45FZNILV.cjs} +170 -1031
- package/dist/chunk-45FZNILV.cjs.map +1 -0
- package/dist/{chunk-CPMMHIQM.js → chunk-VBFXHZBY.js} +159 -1026
- package/dist/chunk-VBFXHZBY.js.map +1 -0
- package/dist/chunk-VTCBYFAK.cjs +765 -0
- package/dist/chunk-VTCBYFAK.cjs.map +1 -0
- package/dist/chunk-ZNWS7VJP.js +755 -0
- package/dist/chunk-ZNWS7VJP.js.map +1 -0
- package/dist/client/index.cjs +71 -140
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.js +70 -140
- package/dist/client/index.js.map +1 -1
- package/dist/client/upload.d.ts +9 -4
- package/dist/client/upload.d.ts.map +1 -1
- package/dist/server/actions/drive.d.ts.map +1 -1
- package/dist/server/controllers/drive.d.ts.map +1 -1
- package/dist/server/express.cjs +15 -14
- package/dist/server/express.cjs.map +1 -1
- package/dist/server/express.js +4 -2
- package/dist/server/express.js.map +1 -1
- package/dist/server/hono.cjs +15 -14
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.js +4 -2
- package/dist/server/hono.js.map +1 -1
- package/dist/server/index.cjs +28 -27
- package/dist/server/index.js +2 -1
- package/dist/server/tus.d.ts +17 -0
- package/dist/server/tus.d.ts.map +1 -0
- package/dist/server/zod/schemas.d.ts +0 -53
- package/dist/server/zod/schemas.d.ts.map +1 -1
- package/dist/tus-6OAOA454.js +3 -0
- package/dist/tus-6OAOA454.js.map +1 -0
- package/dist/tus-P67YULWB.cjs +16 -0
- package/dist/tus-P67YULWB.cjs.map +1 -0
- package/dist/types/server/api.d.ts +1 -1
- package/dist/types/server/api.d.ts.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-CPMMHIQM.js.map +0 -1
- package/dist/chunk-IVSBLEMY.cjs.map +0 -1
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var fs = require('fs');
|
|
4
4
|
var path = require('path');
|
|
5
|
+
var stream = require('stream');
|
|
6
|
+
var server = require('@tus/server');
|
|
7
|
+
var fileStore = require('@tus/file-store');
|
|
8
|
+
var mongoose = require('mongoose');
|
|
5
9
|
var os2 = require('os');
|
|
6
|
-
var
|
|
7
|
-
var crypto3 = require('crypto');
|
|
10
|
+
var crypto2 = require('crypto');
|
|
8
11
|
var sharp2 = require('sharp');
|
|
9
12
|
var ffmpeg = require('fluent-ffmpeg');
|
|
10
13
|
var googleapis = require('googleapis');
|
|
11
|
-
var formidable = require('formidable');
|
|
12
|
-
var zod = require('zod');
|
|
13
14
|
|
|
14
15
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
16
|
|
|
16
|
-
var
|
|
17
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
17
18
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
19
|
+
var mongoose__default = /*#__PURE__*/_interopDefault(mongoose);
|
|
18
20
|
var os2__default = /*#__PURE__*/_interopDefault(os2);
|
|
19
|
-
var
|
|
20
|
-
var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
|
|
21
|
+
var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
|
|
21
22
|
var sharp2__default = /*#__PURE__*/_interopDefault(sharp2);
|
|
22
23
|
var ffmpeg__default = /*#__PURE__*/_interopDefault(ffmpeg);
|
|
23
|
-
var formidable__default = /*#__PURE__*/_interopDefault(formidable);
|
|
24
24
|
|
|
25
|
-
// src/server/
|
|
25
|
+
// src/server/tus.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 },
|
|
@@ -343,53 +343,6 @@ var getDriveInformation = async (input) => {
|
|
|
343
343
|
}
|
|
344
344
|
return config.information(input);
|
|
345
345
|
};
|
|
346
|
-
|
|
347
|
-
// src/server/actions/cors.ts
|
|
348
|
-
var applyCorsHeaders = (req, res, config) => {
|
|
349
|
-
const cors = config.cors;
|
|
350
|
-
if (!cors?.enabled) return false;
|
|
351
|
-
const origin = req.headers.origin;
|
|
352
|
-
const allowedOrigins = cors.origins ?? "*";
|
|
353
|
-
const methods = cors.methods ?? ["GET", "POST", "PUT", "DELETE", "OPTIONS"];
|
|
354
|
-
const allowedHeaders = cors.allowedHeaders ?? ["Content-Type", "Authorization", "X-Drive-Account"];
|
|
355
|
-
const exposedHeaders = cors.exposedHeaders ?? ["Content-Length", "Content-Type", "Content-Disposition"];
|
|
356
|
-
const credentials = cors.credentials ?? false;
|
|
357
|
-
const maxAge = cors.maxAge ?? 86400;
|
|
358
|
-
let allowOrigin = null;
|
|
359
|
-
if (origin) {
|
|
360
|
-
if (allowedOrigins === "*") {
|
|
361
|
-
allowOrigin = origin;
|
|
362
|
-
} else if (Array.isArray(allowedOrigins)) {
|
|
363
|
-
if (allowedOrigins.includes(origin)) {
|
|
364
|
-
allowOrigin = origin;
|
|
365
|
-
}
|
|
366
|
-
} else if (allowedOrigins === origin) {
|
|
367
|
-
allowOrigin = origin;
|
|
368
|
-
}
|
|
369
|
-
} else if (allowedOrigins === "*") {
|
|
370
|
-
allowOrigin = "*";
|
|
371
|
-
}
|
|
372
|
-
if (!allowOrigin) {
|
|
373
|
-
if (req.method === "OPTIONS") {
|
|
374
|
-
res.status(403).end();
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
380
|
-
res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
|
|
381
|
-
res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
|
|
382
|
-
res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(", "));
|
|
383
|
-
res.setHeader("Access-Control-Max-Age", maxAge.toString());
|
|
384
|
-
if (credentials) {
|
|
385
|
-
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
386
|
-
}
|
|
387
|
-
if (req.method === "OPTIONS") {
|
|
388
|
-
res.status(204).end();
|
|
389
|
-
return true;
|
|
390
|
-
}
|
|
391
|
-
return false;
|
|
392
|
-
};
|
|
393
346
|
var validateMimeType = (mime, allowedTypes) => {
|
|
394
347
|
if (allowedTypes.includes("*/*")) return true;
|
|
395
348
|
return allowedTypes.some((pattern) => {
|
|
@@ -402,7 +355,7 @@ var validateMimeType = (mime, allowedTypes) => {
|
|
|
402
355
|
});
|
|
403
356
|
};
|
|
404
357
|
var computeFileHash = (filePath) => new Promise((resolve, reject) => {
|
|
405
|
-
const hash =
|
|
358
|
+
const hash = crypto2__default.default.createHash("sha256");
|
|
406
359
|
const stream = fs__default.default.createReadStream(filePath);
|
|
407
360
|
stream.on("data", (data) => hash.update(data));
|
|
408
361
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -553,12 +506,6 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size, fit, posi
|
|
|
553
506
|
...resolvedPosition && { position: resolvedPosition }
|
|
554
507
|
};
|
|
555
508
|
};
|
|
556
|
-
|
|
557
|
-
// src/server/security/crypto-utils.ts
|
|
558
|
-
function sanitizeContentDispositionFilename(filename) {
|
|
559
|
-
const basename = filename.replace(/^.*[\\\/]/, "");
|
|
560
|
-
return basename.replace(/["\r\n]/g, "").replace(/[^\x20-\x7E]/g, "").slice(0, 255);
|
|
561
|
-
}
|
|
562
509
|
var generatePlaceholderThumbnail = async (outputPath, mimeType) => {
|
|
563
510
|
const typeParts = mimeType.split("/");
|
|
564
511
|
const subtype = typeParts[1] || "file";
|
|
@@ -1253,368 +1200,7 @@ var GoogleDriveProvider = {
|
|
|
1253
1200
|
}
|
|
1254
1201
|
};
|
|
1255
1202
|
|
|
1256
|
-
// src/server/
|
|
1257
|
-
var handlePublicAction = async (req, res, action, config) => {
|
|
1258
|
-
if (action !== "serve" && action !== "thumbnail") {
|
|
1259
|
-
return false;
|
|
1260
|
-
}
|
|
1261
|
-
try {
|
|
1262
|
-
const { id, token } = req.query;
|
|
1263
|
-
if (!id || typeof id !== "string") {
|
|
1264
|
-
res.status(400).json({ status: 400, message: "Could not open file: missing or invalid file ID" });
|
|
1265
|
-
return true;
|
|
1266
|
-
}
|
|
1267
|
-
const drive = await drive_default.findById(id);
|
|
1268
|
-
if (!drive) {
|
|
1269
|
-
res.status(404).json({ status: 404, message: "File not found or no longer available" });
|
|
1270
|
-
return true;
|
|
1271
|
-
}
|
|
1272
|
-
if (config.security?.signedUrls?.enabled) {
|
|
1273
|
-
if (!token || typeof token !== "string") {
|
|
1274
|
-
res.status(401).json({ status: 401, message: "Access denied: this link is missing its access token" });
|
|
1275
|
-
return true;
|
|
1276
|
-
}
|
|
1277
|
-
try {
|
|
1278
|
-
const decoded = Buffer.from(token, "base64url").toString();
|
|
1279
|
-
const [expiryStr, signature] = decoded.split(":");
|
|
1280
|
-
const expiry = parseInt(expiryStr, 10);
|
|
1281
|
-
if (Date.now() / 1e3 > expiry) {
|
|
1282
|
-
res.status(401).json({ status: 401, message: "Access denied: this link has expired" });
|
|
1283
|
-
return true;
|
|
1284
|
-
}
|
|
1285
|
-
const { secret } = config.security.signedUrls;
|
|
1286
|
-
const expectedSignature = crypto3__default.default.createHmac("sha256", secret).update(`${id}:${expiry}`).digest("hex");
|
|
1287
|
-
if (signature !== expectedSignature) {
|
|
1288
|
-
res.status(401).json({ status: 401, message: "Access denied: this link's access token is invalid" });
|
|
1289
|
-
return true;
|
|
1290
|
-
}
|
|
1291
|
-
} catch {
|
|
1292
|
-
res.status(401).json({ status: 401, message: "Access denied: this link's access token is malformed" });
|
|
1293
|
-
return true;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
1297
|
-
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
1298
|
-
if (action === "thumbnail") {
|
|
1299
|
-
const stream2 = await itemProvider.getThumbnail(drive, itemAccountId);
|
|
1300
|
-
res.setHeader("Content-Type", "image/webp");
|
|
1301
|
-
if (config.cors?.enabled) {
|
|
1302
|
-
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1303
|
-
}
|
|
1304
|
-
stream2.pipe(res);
|
|
1305
|
-
return true;
|
|
1306
|
-
}
|
|
1307
|
-
const { stream, mime, size: fileSize } = await itemProvider.openStream(drive, itemAccountId);
|
|
1308
|
-
const safeFilename = sanitizeContentDispositionFilename(drive.name);
|
|
1309
|
-
const format = req.query.format;
|
|
1310
|
-
const quality = req.query.quality;
|
|
1311
|
-
const display = req.query.display;
|
|
1312
|
-
const sizePreset = req.query.size;
|
|
1313
|
-
const fit = req.query.fit;
|
|
1314
|
-
const position = req.query.position;
|
|
1315
|
-
const isImage = mime.startsWith("image/");
|
|
1316
|
-
const shouldTransform = isImage && (format || quality || display || sizePreset || fit);
|
|
1317
|
-
res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
|
|
1318
|
-
if (config.cors?.enabled) {
|
|
1319
|
-
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1320
|
-
}
|
|
1321
|
-
if (shouldTransform) {
|
|
1322
|
-
try {
|
|
1323
|
-
const settings = getImageSettings(fileSize, quality, display, sizePreset, fit, position);
|
|
1324
|
-
let targetFormat = format || mime.split("/")[1];
|
|
1325
|
-
if (targetFormat === "jpg") targetFormat = "jpeg";
|
|
1326
|
-
if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
|
|
1327
|
-
targetFormat = format || "webp";
|
|
1328
|
-
}
|
|
1329
|
-
const cacheDir = path__default.default.join(config.storage.path, "file", drive._id.toString(), "cache");
|
|
1330
|
-
const cacheKey = [
|
|
1331
|
-
"opt",
|
|
1332
|
-
`q${settings.quality}`,
|
|
1333
|
-
`e${settings.effort}`,
|
|
1334
|
-
settings.width ? `${settings.width}x${settings.height}` : "orig",
|
|
1335
|
-
settings.fit || "none",
|
|
1336
|
-
settings.position || "c",
|
|
1337
|
-
targetFormat
|
|
1338
|
-
].join("_");
|
|
1339
|
-
const cachePath = path__default.default.join(cacheDir, `${cacheKey}.bin`);
|
|
1340
|
-
if (fs__default.default.existsSync(cachePath)) {
|
|
1341
|
-
const cacheStat = fs__default.default.statSync(cachePath);
|
|
1342
|
-
res.setHeader("Content-Type", `image/${targetFormat}`);
|
|
1343
|
-
res.setHeader("Content-Length", cacheStat.size);
|
|
1344
|
-
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1345
|
-
if (config.cors?.enabled) {
|
|
1346
|
-
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1347
|
-
}
|
|
1348
|
-
if ("destroy" in stream) {
|
|
1349
|
-
stream.destroy();
|
|
1350
|
-
}
|
|
1351
|
-
fs__default.default.createReadStream(cachePath).pipe(res);
|
|
1352
|
-
return true;
|
|
1353
|
-
}
|
|
1354
|
-
if (!fs__default.default.existsSync(cacheDir)) fs__default.default.mkdirSync(cacheDir, { recursive: true });
|
|
1355
|
-
let pipeline = sharp2__default.default();
|
|
1356
|
-
if (settings.width && settings.height) {
|
|
1357
|
-
pipeline = pipeline.resize(settings.width, settings.height, {
|
|
1358
|
-
fit: settings.fit || "inside",
|
|
1359
|
-
position: settings.position || "center",
|
|
1360
|
-
withoutEnlargement: true,
|
|
1361
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
1362
|
-
});
|
|
1363
|
-
}
|
|
1364
|
-
if (targetFormat === "jpeg") {
|
|
1365
|
-
pipeline = pipeline.jpeg({ quality: settings.quality, mozjpeg: true });
|
|
1366
|
-
res.setHeader("Content-Type", "image/jpeg");
|
|
1367
|
-
} else if (targetFormat === "png") {
|
|
1368
|
-
pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
|
|
1369
|
-
res.setHeader("Content-Type", "image/png");
|
|
1370
|
-
} else if (targetFormat === "webp") {
|
|
1371
|
-
const webpEffort = Math.min(settings.effort, 6);
|
|
1372
|
-
pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
|
|
1373
|
-
res.setHeader("Content-Type", "image/webp");
|
|
1374
|
-
} else if (targetFormat === "avif") {
|
|
1375
|
-
pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
|
|
1376
|
-
res.setHeader("Content-Type", "image/avif");
|
|
1377
|
-
}
|
|
1378
|
-
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1379
|
-
pipeline.on("error", (err) => {
|
|
1380
|
-
console.error("[next-drive] Pipeline error:", err);
|
|
1381
|
-
});
|
|
1382
|
-
stream.pipe(pipeline);
|
|
1383
|
-
pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
|
|
1384
|
-
pipeline.clone().pipe(res);
|
|
1385
|
-
return true;
|
|
1386
|
-
} catch (e) {
|
|
1387
|
-
console.error("[next-drive] Image transformation failed:", e);
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
res.setHeader("Content-Type", mime);
|
|
1391
|
-
if (fileSize) res.setHeader("Content-Length", fileSize);
|
|
1392
|
-
stream.pipe(res);
|
|
1393
|
-
return true;
|
|
1394
|
-
} catch (error) {
|
|
1395
|
-
console.error(`[next-drive] Error in ${action}:`, error);
|
|
1396
|
-
const detail = error instanceof Error ? error.message : "Something went wrong while serving the file";
|
|
1397
|
-
res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
|
|
1398
|
-
return true;
|
|
1399
|
-
}
|
|
1400
|
-
};
|
|
1401
|
-
var handleAuthAction = async (req, res, action, config, owner) => {
|
|
1402
|
-
if (!["getAuthUrl", "callback", "listAccounts", "removeAccount"].includes(action)) {
|
|
1403
|
-
return false;
|
|
1404
|
-
}
|
|
1405
|
-
switch (action) {
|
|
1406
|
-
case "getAuthUrl": {
|
|
1407
|
-
const { provider } = req.query;
|
|
1408
|
-
if (provider === "GOOGLE") {
|
|
1409
|
-
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
1410
|
-
if (!clientId || !clientSecret || !redirectUri) {
|
|
1411
|
-
res.status(500).json({ status: 500, message: "Google Drive is not configured on the server" });
|
|
1412
|
-
return true;
|
|
1413
|
-
}
|
|
1414
|
-
const callbackUri = new URL(redirectUri);
|
|
1415
|
-
callbackUri.searchParams.set("action", "callback");
|
|
1416
|
-
const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
|
|
1417
|
-
const state = Buffer.from(JSON.stringify({ owner })).toString("base64");
|
|
1418
|
-
const url = oAuth2Client.generateAuthUrl({
|
|
1419
|
-
access_type: "offline",
|
|
1420
|
-
scope: ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email"],
|
|
1421
|
-
state,
|
|
1422
|
-
prompt: "consent"
|
|
1423
|
-
});
|
|
1424
|
-
res.status(200).json({ status: 200, message: "Auth URL generated", data: { url } });
|
|
1425
|
-
return true;
|
|
1426
|
-
}
|
|
1427
|
-
res.status(400).json({ status: 400, message: "Unknown storage provider requested" });
|
|
1428
|
-
return true;
|
|
1429
|
-
}
|
|
1430
|
-
case "callback": {
|
|
1431
|
-
const { code } = req.query;
|
|
1432
|
-
if (!code) {
|
|
1433
|
-
res.status(400).json({ status: 400, message: "Google sign-in failed: authorization code missing" });
|
|
1434
|
-
return true;
|
|
1435
|
-
}
|
|
1436
|
-
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
1437
|
-
if (!clientId || !clientSecret || !redirectUri) {
|
|
1438
|
-
res.status(500).json({ status: 500, message: "Google Drive sign-in is not configured on the server" });
|
|
1439
|
-
return true;
|
|
1440
|
-
}
|
|
1441
|
-
const callbackUri = new URL(redirectUri);
|
|
1442
|
-
callbackUri.searchParams.set("action", "callback");
|
|
1443
|
-
const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
|
|
1444
|
-
const { tokens } = await oAuth2Client.getToken(code);
|
|
1445
|
-
oAuth2Client.setCredentials(tokens);
|
|
1446
|
-
const oauth2 = googleapis.google.oauth2({ version: "v2", auth: oAuth2Client });
|
|
1447
|
-
const userInfo = await oauth2.userinfo.get();
|
|
1448
|
-
const existing = await account_default.findOne({ owner, "metadata.google.email": userInfo.data.email, "metadata.provider": "GOOGLE" });
|
|
1449
|
-
if (existing) {
|
|
1450
|
-
existing.metadata.google.credentials = tokens;
|
|
1451
|
-
existing.markModified("metadata");
|
|
1452
|
-
await existing.save();
|
|
1453
|
-
} else {
|
|
1454
|
-
const newAccount = new account_default({
|
|
1455
|
-
owner,
|
|
1456
|
-
name: userInfo.data.name || "Google Drive",
|
|
1457
|
-
metadata: {
|
|
1458
|
-
provider: "GOOGLE",
|
|
1459
|
-
google: {
|
|
1460
|
-
email: userInfo.data.email,
|
|
1461
|
-
credentials: tokens
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
});
|
|
1465
|
-
await newAccount.save();
|
|
1466
|
-
}
|
|
1467
|
-
res.setHeader("Content-Type", "text/html");
|
|
1468
|
-
res.send(`<!DOCTYPE html>
|
|
1469
|
-
<html>
|
|
1470
|
-
<head><title>Authentication Complete</title></head>
|
|
1471
|
-
<body>
|
|
1472
|
-
<p>Authentication successful! This window will close automatically.</p>
|
|
1473
|
-
<script>
|
|
1474
|
-
(function() {
|
|
1475
|
-
if (window.opener) {
|
|
1476
|
-
try {
|
|
1477
|
-
window.opener.postMessage('oauth-success', '*');
|
|
1478
|
-
} catch (e) {}
|
|
1479
|
-
}
|
|
1480
|
-
try {
|
|
1481
|
-
localStorage.setItem('next-drive-oauth-success', Date.now().toString());
|
|
1482
|
-
localStorage.removeItem('next-drive-oauth-success');
|
|
1483
|
-
} catch (e) {}
|
|
1484
|
-
window.close();
|
|
1485
|
-
setTimeout(function() {
|
|
1486
|
-
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>';
|
|
1487
|
-
}, 500);
|
|
1488
|
-
})();
|
|
1489
|
-
</script>
|
|
1490
|
-
</body>
|
|
1491
|
-
</html>`);
|
|
1492
|
-
return true;
|
|
1493
|
-
}
|
|
1494
|
-
case "listAccounts": {
|
|
1495
|
-
const accounts = await account_default.find({ owner });
|
|
1496
|
-
res.status(200).json({
|
|
1497
|
-
status: 200,
|
|
1498
|
-
data: {
|
|
1499
|
-
accounts: accounts.map((a) => ({
|
|
1500
|
-
id: a._id.toString(),
|
|
1501
|
-
name: a.name,
|
|
1502
|
-
email: a.metadata.google?.email || "",
|
|
1503
|
-
provider: a.metadata.provider
|
|
1504
|
-
}))
|
|
1505
|
-
}
|
|
1506
|
-
});
|
|
1507
|
-
return true;
|
|
1508
|
-
}
|
|
1509
|
-
case "removeAccount": {
|
|
1510
|
-
const { id } = req.query;
|
|
1511
|
-
const account = await account_default.findOne({ _id: id, owner });
|
|
1512
|
-
if (!account) {
|
|
1513
|
-
res.status(404).json({ status: 404, message: "Could not disconnect: account not found" });
|
|
1514
|
-
return true;
|
|
1515
|
-
}
|
|
1516
|
-
if (account.metadata.provider === "GOOGLE") {
|
|
1517
|
-
try {
|
|
1518
|
-
await GoogleDriveProvider.revokeToken(owner, account._id.toString());
|
|
1519
|
-
} catch (e) {
|
|
1520
|
-
console.error("Failed to revoke Google token:", e);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
await account_default.deleteOne({ _id: id, owner });
|
|
1524
|
-
await drive_default.deleteMany({ owner, storageAccountId: id });
|
|
1525
|
-
res.status(200).json({ status: 200, message: "Account removed" });
|
|
1526
|
-
return true;
|
|
1527
|
-
}
|
|
1528
|
-
default:
|
|
1529
|
-
return false;
|
|
1530
|
-
}
|
|
1531
|
-
};
|
|
1532
|
-
var objectIdSchema = zod.z.string().refine((val) => mongoose.isValidObjectId(val), {
|
|
1533
|
-
message: "Invalid ObjectId format"
|
|
1534
|
-
});
|
|
1535
|
-
var sanitizeFilename = (name) => {
|
|
1536
|
-
return name.replace(/[<>:"|?*\x00-\x1F]/g, "").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\.\.\//g, "").replace(/\.\.+/g, "").split("/").pop() || "".trim().slice(0, 255);
|
|
1537
|
-
};
|
|
1538
|
-
var sanitizeRegexInput = (input) => {
|
|
1539
|
-
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 100);
|
|
1540
|
-
};
|
|
1541
|
-
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" });
|
|
1542
|
-
var uploadChunkSchema = zod.z.object({
|
|
1543
|
-
chunkIndex: zod.z.number().int().min(0).max(1e4),
|
|
1544
|
-
totalChunks: zod.z.number().int().min(1).max(1e4),
|
|
1545
|
-
driveId: zod.z.string().optional(),
|
|
1546
|
-
fileName: nameSchema,
|
|
1547
|
-
fileSize: zod.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
1548
|
-
fileType: zod.z.string().min(1).max(255),
|
|
1549
|
-
folderId: zod.z.string().optional(),
|
|
1550
|
-
conflictAction: zod.z.enum(["rename", "replace"]).optional()
|
|
1551
|
-
}).refine((data) => data.chunkIndex < data.totalChunks, {
|
|
1552
|
-
message: "Chunk index must be less than total chunks"
|
|
1553
|
-
});
|
|
1554
|
-
var listQuerySchema = zod.z.object({
|
|
1555
|
-
folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]),
|
|
1556
|
-
limit: zod.z.string().optional().transform((val) => {
|
|
1557
|
-
const num = parseInt(val || "50", 10);
|
|
1558
|
-
return Math.min(Math.max(1, num), 100);
|
|
1559
|
-
}),
|
|
1560
|
-
afterId: objectIdSchema.optional()
|
|
1561
|
-
});
|
|
1562
|
-
zod.z.object({
|
|
1563
|
-
id: objectIdSchema,
|
|
1564
|
-
token: zod.z.string().optional()
|
|
1565
|
-
});
|
|
1566
|
-
zod.z.object({
|
|
1567
|
-
id: objectIdSchema,
|
|
1568
|
-
size: zod.z.enum(["small", "medium", "large"]).optional().default("medium"),
|
|
1569
|
-
token: zod.z.string().optional()
|
|
1570
|
-
});
|
|
1571
|
-
var renameBodySchema = zod.z.object({
|
|
1572
|
-
id: objectIdSchema,
|
|
1573
|
-
newName: nameSchema
|
|
1574
|
-
});
|
|
1575
|
-
var deleteQuerySchema = zod.z.object({
|
|
1576
|
-
id: objectIdSchema
|
|
1577
|
-
});
|
|
1578
|
-
zod.z.object({
|
|
1579
|
-
ids: zod.z.array(objectIdSchema).min(1).max(1e3)
|
|
1580
|
-
});
|
|
1581
|
-
var createFolderBodySchema = zod.z.object({
|
|
1582
|
-
name: nameSchema,
|
|
1583
|
-
parentId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.string().length(0), zod.z.undefined()]).optional()
|
|
1584
|
-
});
|
|
1585
|
-
var moveBodySchema = zod.z.object({
|
|
1586
|
-
ids: zod.z.array(objectIdSchema).min(1).max(1e3),
|
|
1587
|
-
targetFolderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional()
|
|
1588
|
-
});
|
|
1589
|
-
var reorderBodySchema = zod.z.object({
|
|
1590
|
-
ids: zod.z.array(objectIdSchema).min(1).max(1e3)
|
|
1591
|
-
});
|
|
1592
|
-
var searchQuerySchema = zod.z.object({
|
|
1593
|
-
q: zod.z.string().min(1).max(100).transform(sanitizeRegexInput),
|
|
1594
|
-
folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional(),
|
|
1595
|
-
limit: zod.z.string().optional().transform((val) => {
|
|
1596
|
-
const num = parseInt(val || "50", 10);
|
|
1597
|
-
return Math.min(Math.max(1, num), 100);
|
|
1598
|
-
}),
|
|
1599
|
-
trashed: zod.z.string().optional().transform((val) => val === "true")
|
|
1600
|
-
});
|
|
1601
|
-
zod.z.object({
|
|
1602
|
-
id: objectIdSchema
|
|
1603
|
-
});
|
|
1604
|
-
var cancelQuerySchema = zod.z.object({
|
|
1605
|
-
id: zod.z.string().uuid()
|
|
1606
|
-
});
|
|
1607
|
-
zod.z.object({
|
|
1608
|
-
days: zod.z.number().int().min(1).max(365).optional()
|
|
1609
|
-
});
|
|
1610
|
-
var driveFileSchemaZod = zod.z.object({
|
|
1611
|
-
id: zod.z.string(),
|
|
1612
|
-
file: zod.z.object({
|
|
1613
|
-
name: zod.z.string(),
|
|
1614
|
-
mime: zod.z.string(),
|
|
1615
|
-
size: zod.z.number()
|
|
1616
|
-
})
|
|
1617
|
-
});
|
|
1203
|
+
// src/server/controllers/drive.ts
|
|
1618
1204
|
var getNextOrderValue = async (owner) => {
|
|
1619
1205
|
const lastItem = await drive_default.findOne({ owner }, {}, { sort: { order: -1 } });
|
|
1620
1206
|
return lastItem ? lastItem.order + 1 : 0;
|
|
@@ -1633,7 +1219,7 @@ var driveGetUrl = (fileId, options) => {
|
|
|
1633
1219
|
} else {
|
|
1634
1220
|
expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
1635
1221
|
}
|
|
1636
|
-
const signature =
|
|
1222
|
+
const signature = crypto2__default.default.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
|
|
1637
1223
|
const token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
|
|
1638
1224
|
return `${config.apiUrl || "/api/drive"}?action=serve&id=${fileId}&token=${token}`;
|
|
1639
1225
|
};
|
|
@@ -1642,7 +1228,7 @@ var driveAddSignedUrlToken = (item, config) => {
|
|
|
1642
1228
|
if (config.security?.signedUrls?.enabled && config.security.signedUrls.secret) {
|
|
1643
1229
|
const { secret, expiresIn } = config.security.signedUrls;
|
|
1644
1230
|
const expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
1645
|
-
const signature =
|
|
1231
|
+
const signature = crypto2__default.default.createHmac("sha256", secret).update(`${item.id}:${expiryTimestamp}`).digest("hex");
|
|
1646
1232
|
token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
|
|
1647
1233
|
}
|
|
1648
1234
|
const apiUrl = config.apiUrl || "/api/drive";
|
|
@@ -1961,7 +1547,7 @@ var driveUpload = async (source, key, options) => {
|
|
|
1961
1547
|
if (!fs__default.default.existsSync(tempDir)) {
|
|
1962
1548
|
fs__default.default.mkdirSync(tempDir, { recursive: true });
|
|
1963
1549
|
}
|
|
1964
|
-
tempFilePath = path__default.default.join(tempDir, `upload-${
|
|
1550
|
+
tempFilePath = path__default.default.join(tempDir, `upload-${crypto2__default.default.randomUUID()}.tmp`);
|
|
1965
1551
|
fs__default.default.writeFileSync(tempFilePath, source);
|
|
1966
1552
|
sourceFilePath = tempFilePath;
|
|
1967
1553
|
fileSize = source.length;
|
|
@@ -1970,7 +1556,7 @@ var driveUpload = async (source, key, options) => {
|
|
|
1970
1556
|
if (!fs__default.default.existsSync(tempDir)) {
|
|
1971
1557
|
fs__default.default.mkdirSync(tempDir, { recursive: true });
|
|
1972
1558
|
}
|
|
1973
|
-
tempFilePath = path__default.default.join(tempDir, `upload-${
|
|
1559
|
+
tempFilePath = path__default.default.join(tempDir, `upload-${crypto2__default.default.randomUUID()}.tmp`);
|
|
1974
1560
|
const writeStream = fs__default.default.createWriteStream(tempFilePath);
|
|
1975
1561
|
await new Promise((resolve, reject) => {
|
|
1976
1562
|
source.pipe(writeStream);
|
|
@@ -2130,14 +1716,11 @@ var driveCleanup = async () => {
|
|
|
2130
1716
|
console.error("[next-drive] Failed to remove temp directory:", e);
|
|
2131
1717
|
}
|
|
2132
1718
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
} catch (e) {
|
|
2139
|
-
console.error("[next-drive] Failed to remove system temp directory:", e);
|
|
2140
|
-
}
|
|
1719
|
+
try {
|
|
1720
|
+
const { getTusServer: getTusServer2 } = await import('./tus-P67YULWB.cjs');
|
|
1721
|
+
await getTusServer2().cleanUpExpiredUploads();
|
|
1722
|
+
} catch (e) {
|
|
1723
|
+
console.error("[next-drive] Failed to clean up expired tus uploads:", e);
|
|
2141
1724
|
}
|
|
2142
1725
|
return { removed, totalFreedInBytes };
|
|
2143
1726
|
};
|
|
@@ -2184,615 +1767,165 @@ var withSignedUrls = (items, config) => {
|
|
|
2184
1767
|
return driveAddSignedUrlTokens(items, config);
|
|
2185
1768
|
};
|
|
2186
1769
|
|
|
2187
|
-
// src/server/
|
|
2188
|
-
var
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
case "upload": {
|
|
2243
|
-
if (req.method !== "POST") return void res.status(405).json({ status: 405, message: "Uploading requires a POST request" });
|
|
2244
|
-
const systemTmpDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
|
|
2245
|
-
if (!fs__default.default.existsSync(systemTmpDir)) fs__default.default.mkdirSync(systemTmpDir, { recursive: true });
|
|
2246
|
-
const form = formidable__default.default({
|
|
2247
|
-
multiples: false,
|
|
2248
|
-
maxFileSize: (config.security?.maxUploadSizeInBytes ?? 1024 * 1024 * 1024) * 2,
|
|
2249
|
-
uploadDir: systemTmpDir,
|
|
2250
|
-
keepExtensions: true
|
|
2251
|
-
});
|
|
2252
|
-
const [fields, files] = await new Promise((resolve, reject) => {
|
|
2253
|
-
form.parse(req, (err, parsedFields, parsedFiles) => {
|
|
2254
|
-
if (err) reject(err);
|
|
2255
|
-
else resolve([parsedFields, parsedFiles]);
|
|
2256
|
-
});
|
|
2257
|
-
});
|
|
2258
|
-
const cleanupTempFiles = (allFiles) => {
|
|
2259
|
-
Object.values(allFiles).flat().forEach((file) => {
|
|
2260
|
-
if (file && fs__default.default.existsSync(file.filepath)) fs__default.default.rmSync(file.filepath, { force: true });
|
|
2261
|
-
});
|
|
2262
|
-
};
|
|
2263
|
-
const getString = (f) => Array.isArray(f) ? f[0] : f || "";
|
|
2264
|
-
const getInt = (f) => parseInt(getString(f) || "0", 10);
|
|
2265
|
-
const uploadData = uploadChunkSchema.safeParse({
|
|
2266
|
-
chunkIndex: getInt(fields.chunkIndex),
|
|
2267
|
-
totalChunks: getInt(fields.totalChunks),
|
|
2268
|
-
driveId: getString(fields.driveId) || void 0,
|
|
2269
|
-
fileName: getString(fields.fileName),
|
|
2270
|
-
fileSize: getInt(fields.fileSize),
|
|
2271
|
-
fileType: getString(fields.fileType),
|
|
2272
|
-
folderId: getString(fields.folderId) || void 0,
|
|
2273
|
-
conflictAction: getString(fields.conflictAction) || void 0
|
|
2274
|
-
});
|
|
2275
|
-
if (!uploadData.success) {
|
|
2276
|
-
cleanupTempFiles(files);
|
|
2277
|
-
return void res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
|
|
2278
|
-
}
|
|
2279
|
-
const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId, conflictAction } = uploadData.data;
|
|
2280
|
-
let currentUploadId = driveId;
|
|
2281
|
-
const tempBaseDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
|
|
2282
|
-
if (!currentUploadId) {
|
|
2283
|
-
if (chunkIndex !== 0) return void res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
|
|
2284
|
-
if (!authenticated) {
|
|
2285
|
-
const unauth = config.security?.unauthenticated;
|
|
2286
|
-
if (!unauth?.enabled) {
|
|
2287
|
-
cleanupTempFiles(files);
|
|
2288
|
-
return void res.status(401).json({ status: 401, message: "Authentication required to upload" });
|
|
2289
|
-
}
|
|
2290
|
-
if (fileSizeInBytes > unauth.maxUploadSizeInBytes) {
|
|
2291
|
-
cleanupTempFiles(files);
|
|
2292
|
-
return void res.status(413).json({ status: 413, message: "Could not upload: file exceeds the maximum allowed size" });
|
|
2293
|
-
}
|
|
2294
|
-
if (fileType && !validateMimeType(fileType, unauth.allowedMimeTypes)) {
|
|
2295
|
-
cleanupTempFiles(files);
|
|
2296
|
-
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2297
|
-
}
|
|
2298
|
-
const abuse = unauth.abuse;
|
|
2299
|
-
if (abuse) {
|
|
2300
|
-
const store = globalThis.__nextDrive.abuse;
|
|
2301
|
-
const now = Date.now();
|
|
2302
|
-
const ip = abuse.clientId?.(req) ?? (abuse.trustedHeaders ?? ["cf-connecting-ip", "x-forwarded-for"]).map((h) => req.headers[h]).find(Boolean)?.split(",")[0].trim() ?? req.socket.remoteAddress ?? "unknown";
|
|
2303
|
-
const hits = (store.ipHits.get(ip) ?? []).filter((t) => now - t < 36e5);
|
|
2304
|
-
const perIp = abuse.perIp;
|
|
2305
|
-
if (perIp && hits.filter((t) => now - t < perIp.windowMinutes * 6e4).length >= perIp.max) {
|
|
2306
|
-
cleanupTempFiles(files);
|
|
2307
|
-
return void res.status(429).json({ status: 429, message: "Too many uploads, please try again later" });
|
|
2308
|
-
}
|
|
2309
|
-
if (abuse.hourlyPerIp && hits.length >= abuse.hourlyPerIp) {
|
|
2310
|
-
cleanupTempFiles(files);
|
|
2311
|
-
return void res.status(429).json({ status: 429, message: "Hourly upload limit reached, please try again later" });
|
|
2312
|
-
}
|
|
2313
|
-
if (abuse.maxConcurrent && store.concurrent >= abuse.maxConcurrent) {
|
|
2314
|
-
cleanupTempFiles(files);
|
|
2315
|
-
return void res.status(429).json({ status: 429, message: "Server is busy with uploads, please try again later" });
|
|
2316
|
-
}
|
|
2317
|
-
if (abuse.maxLiveBytes) {
|
|
2318
|
-
const [agg] = await drive_default.aggregate([{ $match: { expiresAt: { $ne: null } } }, { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }]);
|
|
2319
|
-
if ((agg?.total ?? 0) + fileSizeInBytes > abuse.maxLiveBytes) {
|
|
2320
|
-
cleanupTempFiles(files);
|
|
2321
|
-
return void res.status(429).json({ status: 429, message: "Temporary storage is full, please try again later" });
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
hits.push(now);
|
|
2325
|
-
store.ipHits.set(ip, hits);
|
|
2326
|
-
store.concurrent++;
|
|
2327
|
-
}
|
|
2328
|
-
} else {
|
|
2329
|
-
if (fileType && config.security) {
|
|
2330
|
-
if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
|
|
2331
|
-
cleanupTempFiles(files);
|
|
2332
|
-
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
if (!isRootMode) {
|
|
2336
|
-
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2337
|
-
if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
|
|
2338
|
-
cleanupTempFiles(files);
|
|
2339
|
-
return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
currentUploadId = crypto3__default.default.randomUUID();
|
|
2344
|
-
const uploadDir2 = path__default.default.join(tempBaseDir, currentUploadId);
|
|
2345
|
-
fs__default.default.mkdirSync(uploadDir2, { recursive: true });
|
|
2346
|
-
const metadata = {
|
|
2347
|
-
owner: authenticated ? owner : null,
|
|
2348
|
-
accountId: authenticated ? accountId : null,
|
|
2349
|
-
providerName: provider.name,
|
|
2350
|
-
name: fileName,
|
|
2351
|
-
parentId: !authenticated || folderId === "root" || !folderId ? null : folderId,
|
|
2352
|
-
fileSize: fileSizeInBytes,
|
|
2353
|
-
mimeType: fileType,
|
|
2354
|
-
totalChunks,
|
|
2355
|
-
conflictAction: conflictAction ?? null,
|
|
2356
|
-
unauthenticated: !authenticated
|
|
2357
|
-
};
|
|
2358
|
-
fs__default.default.writeFileSync(path__default.default.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
|
|
2359
|
-
}
|
|
2360
|
-
if (!currentUploadId) {
|
|
2361
|
-
cleanupTempFiles(files);
|
|
2362
|
-
return void res.status(400).json({ status: 400, message: "Could not upload: invalid upload request" });
|
|
2363
|
-
}
|
|
2364
|
-
const uploadDir = path__default.default.join(tempBaseDir, currentUploadId);
|
|
2365
|
-
if (!fs__default.default.existsSync(uploadDir)) {
|
|
2366
|
-
cleanupTempFiles(files);
|
|
2367
|
-
return void res.status(404).json({ status: 404, message: "Could not upload: this upload session was not found or has expired" });
|
|
2368
|
-
}
|
|
2369
|
-
try {
|
|
2370
|
-
const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
|
|
2371
|
-
if (!chunkFile) throw new Error("Could not upload: no file chunk was received");
|
|
2372
|
-
const partPath = path__default.default.join(uploadDir, `part_${chunkIndex}`);
|
|
2373
|
-
try {
|
|
2374
|
-
fs__default.default.renameSync(chunkFile.filepath, partPath);
|
|
2375
|
-
} catch (err) {
|
|
2376
|
-
if (err instanceof Error && "code" in err && err.code === "EXDEV") {
|
|
2377
|
-
fs__default.default.copyFileSync(chunkFile.filepath, partPath);
|
|
2378
|
-
fs__default.default.unlinkSync(chunkFile.filepath);
|
|
2379
|
-
} else {
|
|
2380
|
-
throw err;
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
const uploadedParts = fs__default.default.readdirSync(uploadDir).filter((f) => f.startsWith("part_"));
|
|
2384
|
-
if (uploadedParts.length === totalChunks) {
|
|
2385
|
-
const metaPath = path__default.default.join(uploadDir, "metadata.json");
|
|
2386
|
-
const meta = JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8"));
|
|
2387
|
-
const finalTempPath = path__default.default.join(uploadDir, "final.bin");
|
|
2388
|
-
const writeStream = fs__default.default.createWriteStream(finalTempPath);
|
|
2389
|
-
let streamError = null;
|
|
2390
|
-
writeStream.on("error", (err) => {
|
|
2391
|
-
streamError = err;
|
|
2392
|
-
});
|
|
2393
|
-
await new Promise((resolve, reject) => {
|
|
2394
|
-
writeStream.on("open", () => resolve());
|
|
2395
|
-
writeStream.once("error", reject);
|
|
2396
|
-
});
|
|
2397
|
-
for (let i = 0; i < totalChunks; i++) {
|
|
2398
|
-
if (streamError) {
|
|
2399
|
-
writeStream.destroy();
|
|
2400
|
-
throw streamError;
|
|
2401
|
-
}
|
|
2402
|
-
const pPath = path__default.default.join(uploadDir, `part_${i}`);
|
|
2403
|
-
if (!fs__default.default.existsSync(pPath)) {
|
|
2404
|
-
writeStream.destroy();
|
|
2405
|
-
throw new Error(`Could not finish upload: chunk ${i} is missing`);
|
|
2406
|
-
}
|
|
2407
|
-
const data = fs__default.default.readFileSync(pPath);
|
|
2408
|
-
const canContinue = writeStream.write(data);
|
|
2409
|
-
if (!canContinue) {
|
|
2410
|
-
await new Promise((resolve, reject) => {
|
|
2411
|
-
writeStream.once("drain", resolve);
|
|
2412
|
-
writeStream.once("error", reject);
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
await new Promise((resolve, reject) => {
|
|
2417
|
-
if (streamError) {
|
|
2418
|
-
reject(streamError);
|
|
2419
|
-
return;
|
|
2420
|
-
}
|
|
2421
|
-
writeStream.end();
|
|
2422
|
-
writeStream.on("finish", resolve);
|
|
2423
|
-
writeStream.once("error", reject);
|
|
2424
|
-
});
|
|
2425
|
-
if (!fs__default.default.existsSync(finalTempPath)) {
|
|
2426
|
-
throw new Error("Could not finish upload: failed to assemble the file");
|
|
2427
|
-
}
|
|
2428
|
-
const finalStats = fs__default.default.statSync(finalTempPath);
|
|
2429
|
-
if (finalStats.size !== meta.fileSize) {
|
|
2430
|
-
throw new Error("Could not finish upload: the assembled file is incomplete (size mismatch)");
|
|
2431
|
-
}
|
|
2432
|
-
let replaceTargetId = null;
|
|
2433
|
-
if (meta.conflictAction === "replace") {
|
|
2434
|
-
const existing = await drive_default.findOne({ owner: meta.owner, storageAccountId: meta.accountId || null, "provider.type": meta.providerName, parentId: meta.parentId, name: meta.name, "information.type": "FILE", trashedAt: null });
|
|
2435
|
-
if (existing) replaceTargetId = String(existing._id);
|
|
2436
|
-
} else {
|
|
2437
|
-
const ext = path__default.default.extname(meta.name);
|
|
2438
|
-
const base = meta.name.slice(0, meta.name.length - ext.length);
|
|
2439
|
-
let n = 1;
|
|
2440
|
-
while (await drive_default.exists({ owner: meta.owner, storageAccountId: meta.accountId || null, "provider.type": meta.providerName, parentId: meta.parentId, name: meta.name, "information.type": "FILE", trashedAt: null })) {
|
|
2441
|
-
meta.name = `${base} (${n++})${ext}`;
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
const drive = new drive_default({
|
|
2445
|
-
owner: meta.owner,
|
|
2446
|
-
storageAccountId: meta.accountId || null,
|
|
2447
|
-
provider: { type: meta.providerName },
|
|
2448
|
-
name: meta.name,
|
|
2449
|
-
parentId: meta.parentId,
|
|
2450
|
-
order: 0,
|
|
2451
|
-
information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
|
|
2452
|
-
status: "UPLOADING",
|
|
2453
|
-
currentChunk: totalChunks,
|
|
2454
|
-
totalChunks,
|
|
2455
|
-
expiresAt: meta.unauthenticated ? new Date(Date.now() + (config.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4) : null
|
|
2456
|
-
});
|
|
2457
|
-
if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
|
|
2458
|
-
drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
|
|
2459
|
-
}
|
|
2460
|
-
await drive.save();
|
|
2461
|
-
try {
|
|
2462
|
-
const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
|
|
2463
|
-
if (replaceTargetId) await provider.delete([replaceTargetId], meta.owner, meta.accountId);
|
|
2464
|
-
fs__default.default.rmSync(uploadDir, { recursive: true, force: true });
|
|
2465
|
-
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2466
|
-
const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
|
|
2467
|
-
res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item: withSignedUrl(item, config) }, statistic: { storage: newQuota } });
|
|
2468
|
-
} catch (err) {
|
|
2469
|
-
await drive_default.deleteOne({ _id: drive._id });
|
|
2470
|
-
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2471
|
-
throw err;
|
|
2472
|
-
}
|
|
2473
|
-
} else {
|
|
2474
|
-
const newQuota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2475
|
-
if (chunkIndex === 0) {
|
|
2476
|
-
res.status(200).json({ status: 200, message: "Upload started", data: { type: "UPLOAD_STARTED", driveId: currentUploadId }, statistic: { storage: newQuota } });
|
|
2477
|
-
} else {
|
|
2478
|
-
res.status(200).json({ status: 200, message: "Chunk received", data: { type: "CHUNK_RECEIVED", driveId: currentUploadId, chunkIndex }, statistic: { storage: newQuota } });
|
|
1770
|
+
// src/server/tus.ts
|
|
1771
|
+
var UPLOAD_EXPIRATION_MS = 24 * 60 * 60 * 1e3;
|
|
1772
|
+
var getCtx = (req) => req.__driveCtx;
|
|
1773
|
+
var decConcurrent = () => {
|
|
1774
|
+
const abuse = globalThis.__nextDrive?.abuse;
|
|
1775
|
+
if (abuse) abuse.concurrent = Math.max(0, abuse.concurrent - 1);
|
|
1776
|
+
};
|
|
1777
|
+
var getTusServer = () => {
|
|
1778
|
+
const g = globalThis;
|
|
1779
|
+
g.__nextDrive = g.__nextDrive ?? {};
|
|
1780
|
+
if (g.__nextDrive.tus) return g.__nextDrive.tus;
|
|
1781
|
+
const config = getDriveConfig();
|
|
1782
|
+
const tusDir = path__default.default.join(config.storage.path, "tus");
|
|
1783
|
+
fs__default.default.mkdirSync(tusDir, { recursive: true });
|
|
1784
|
+
const apiPath = new URL(config.apiUrl || "/api/drive", "http://x").pathname;
|
|
1785
|
+
const server$1 = new server.Server({
|
|
1786
|
+
path: apiPath,
|
|
1787
|
+
datastore: new fileStore.FileStore({ directory: tusDir, expirationPeriodInMilliseconds: UPLOAD_EXPIRATION_MS }),
|
|
1788
|
+
// ** tus writes the response (and its CORS headers) last, so the custom account/item headers
|
|
1789
|
+
// ** must be declared here to survive cross-origin requests.
|
|
1790
|
+
allowedHeaders: ["X-Drive-Account"],
|
|
1791
|
+
exposedHeaders: ["X-Drive-Item"],
|
|
1792
|
+
maxSize: (req) => {
|
|
1793
|
+
const ctx = getCtx(req);
|
|
1794
|
+
if (!ctx) return Number.MAX_SAFE_INTEGER;
|
|
1795
|
+
return ctx.authenticated ? ctx.config.security?.maxUploadSizeInBytes ?? Number.MAX_SAFE_INTEGER : ctx.config.security?.unauthenticated?.maxUploadSizeInBytes ?? 0;
|
|
1796
|
+
},
|
|
1797
|
+
// ** Keep the upload id in the query string so every method stays on `?action=upload`.
|
|
1798
|
+
generateUrl: (_req, { path: p, id }) => `${p}?action=upload&id=${id}`,
|
|
1799
|
+
getFileIdFromRequest: (req) => new URL(req.url, "http://x").searchParams.get("id") ?? void 0,
|
|
1800
|
+
onUploadCreate: async (req, upload) => {
|
|
1801
|
+
const ctx = getCtx(req);
|
|
1802
|
+
if (!ctx) throw { status_code: 500, body: "Could not upload: request context is missing" };
|
|
1803
|
+
const { config: config2, authenticated, owner, provider, accountId, information, isRootMode } = ctx;
|
|
1804
|
+
const size = upload.size ?? 0;
|
|
1805
|
+
const fileType = upload.metadata?.filetype || "";
|
|
1806
|
+
if (!authenticated) {
|
|
1807
|
+
const unauth = config2.security?.unauthenticated;
|
|
1808
|
+
if (!unauth?.enabled) throw { status_code: 401, body: "Authentication required to upload" };
|
|
1809
|
+
if (size > unauth.maxUploadSizeInBytes) throw { status_code: 413, body: "Could not upload: file exceeds the maximum allowed size" };
|
|
1810
|
+
if (fileType && !validateMimeType(fileType, unauth.allowedMimeTypes)) throw { status_code: 400, body: `Could not upload: file type "${fileType}" is not allowed` };
|
|
1811
|
+
const abuse = unauth.abuse;
|
|
1812
|
+
if (abuse) {
|
|
1813
|
+
const store = globalThis.__nextDrive.abuse;
|
|
1814
|
+
const now = Date.now();
|
|
1815
|
+
const nreq = req.__nodeReq;
|
|
1816
|
+
const ip = (nreq && abuse.clientId?.(nreq)) ?? (abuse.trustedHeaders ?? ["cf-connecting-ip", "x-forwarded-for"]).map((h) => req.headers.get(h)).find(Boolean)?.split(",")[0].trim() ?? nreq?.socket?.remoteAddress ?? "unknown";
|
|
1817
|
+
const hits = (store.ipHits.get(ip) ?? []).filter((t) => now - t < 36e5);
|
|
1818
|
+
const perIp = abuse.perIp;
|
|
1819
|
+
if (perIp && hits.filter((t) => now - t < perIp.windowMinutes * 6e4).length >= perIp.max) throw { status_code: 429, body: "Too many uploads, please try again later" };
|
|
1820
|
+
if (abuse.hourlyPerIp && hits.length >= abuse.hourlyPerIp) throw { status_code: 429, body: "Hourly upload limit reached, please try again later" };
|
|
1821
|
+
if (abuse.maxConcurrent && store.concurrent >= abuse.maxConcurrent) throw { status_code: 429, body: "Server is busy with uploads, please try again later" };
|
|
1822
|
+
if (abuse.maxLiveBytes) {
|
|
1823
|
+
const [agg] = await drive_default.aggregate([{ $match: { expiresAt: { $ne: null } } }, { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }]);
|
|
1824
|
+
if ((agg?.total ?? 0) + size > abuse.maxLiveBytes) throw { status_code: 429, body: "Temporary storage is full, please try again later" };
|
|
2479
1825
|
}
|
|
1826
|
+
hits.push(now);
|
|
1827
|
+
store.ipHits.set(ip, hits);
|
|
1828
|
+
store.concurrent++;
|
|
2480
1829
|
}
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
}
|
|
2487
|
-
case "cancel": {
|
|
2488
|
-
const cancelData = cancelQuerySchema.safeParse(req.query);
|
|
2489
|
-
if (!cancelData.success) return void res.status(400).json({ status: 400, message: "Could not cancel upload: invalid ID" });
|
|
2490
|
-
const { id } = cancelData.data;
|
|
2491
|
-
const tempUploadDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads", id);
|
|
2492
|
-
if (fs__default.default.existsSync(tempUploadDir)) {
|
|
2493
|
-
try {
|
|
2494
|
-
const metaPath = path__default.default.join(tempUploadDir, "metadata.json");
|
|
2495
|
-
if (fs__default.default.existsSync(metaPath) && JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8")).unauthenticated) {
|
|
2496
|
-
globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2497
|
-
}
|
|
2498
|
-
fs__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
|
|
2499
|
-
} catch (e) {
|
|
2500
|
-
console.error("Failed to cleanup temp upload:", e);
|
|
1830
|
+
} else {
|
|
1831
|
+
if (fileType && config2.security && !validateMimeType(fileType, config2.security.allowedMimeTypes)) throw { status_code: 400, body: `Could not upload: file type "${fileType}" is not allowed` };
|
|
1832
|
+
if (!isRootMode) {
|
|
1833
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
1834
|
+
if (quota.usedInBytes + size > quota.quotaInBytes) throw { status_code: 413, body: "Could not upload: you have run out of storage space" };
|
|
2501
1835
|
}
|
|
2502
1836
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
return;
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
const {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
}
|
|
2527
|
-
drive.trashedAt = /* @__PURE__ */ new Date();
|
|
2528
|
-
await drive.save();
|
|
2529
|
-
res.status(200).json({ status: 200, message: "Moved to trash", data: null });
|
|
2530
|
-
return;
|
|
2531
|
-
}
|
|
2532
|
-
case "deletePermanent": {
|
|
2533
|
-
const deleteData = deleteQuerySchema.safeParse(req.query);
|
|
2534
|
-
if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not delete: invalid ID" });
|
|
2535
|
-
const { id } = deleteData.data;
|
|
2536
|
-
await provider.delete([id], owner, accountId);
|
|
2537
|
-
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2538
|
-
res.status(200).json({ status: 200, message: "Deleted", statistic: { storage: quota } });
|
|
2539
|
-
return;
|
|
2540
|
-
}
|
|
2541
|
-
case "quota": {
|
|
2542
|
-
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2543
|
-
res.status(200).json({
|
|
2544
|
-
status: 200,
|
|
2545
|
-
message: "Quota retrieved",
|
|
2546
|
-
data: {
|
|
2547
|
-
usedInBytes: quota.usedInBytes,
|
|
2548
|
-
totalInBytes: quota.quotaInBytes,
|
|
2549
|
-
availableInBytes: Math.max(0, quota.quotaInBytes - quota.usedInBytes),
|
|
2550
|
-
percentage: quota.quotaInBytes > 0 ? Math.round(quota.usedInBytes / quota.quotaInBytes * 100) : 0
|
|
2551
|
-
},
|
|
2552
|
-
statistic: { storage: quota }
|
|
2553
|
-
});
|
|
2554
|
-
return;
|
|
2555
|
-
}
|
|
2556
|
-
case "trash": {
|
|
2557
|
-
try {
|
|
2558
|
-
const { provider: trashProvider, accountId: trashAccountId } = await resolveProvider(req, owner);
|
|
2559
|
-
await trashProvider.syncTrash(owner, trashAccountId);
|
|
2560
|
-
} catch (e) {
|
|
2561
|
-
console.error("Trash sync failed", e);
|
|
1837
|
+
return {};
|
|
1838
|
+
},
|
|
1839
|
+
onUploadFinish: async (req, upload) => {
|
|
1840
|
+
const ctx = getCtx(req);
|
|
1841
|
+
if (!ctx) throw { status_code: 500, body: "Could not finish upload: request context is missing" };
|
|
1842
|
+
const { config: config2, authenticated, owner, provider, accountId } = ctx;
|
|
1843
|
+
const meta = upload.metadata ?? {};
|
|
1844
|
+
const filePath = upload.storage?.path ?? path__default.default.join(tusDir, upload.id);
|
|
1845
|
+
const already = await drive_default.findOne({ "meta.tusId": upload.id });
|
|
1846
|
+
if (already) return { headers: { "X-Drive-Item": encodeURIComponent(JSON.stringify(withSignedUrl(await already.toClient(), config2))) } };
|
|
1847
|
+
const ownerValue = authenticated ? owner : null;
|
|
1848
|
+
const parentId = !authenticated || !meta.folderId || meta.folderId === "root" ? null : meta.folderId;
|
|
1849
|
+
let name = meta.filename || "file";
|
|
1850
|
+
let replaceTargetId = null;
|
|
1851
|
+
const dupeQuery = { owner: ownerValue, storageAccountId: accountId || null, "provider.type": provider.name, parentId, "information.type": "FILE", trashedAt: null };
|
|
1852
|
+
if (meta.conflictAction === "replace") {
|
|
1853
|
+
const existing = await drive_default.findOne({ ...dupeQuery, name });
|
|
1854
|
+
if (existing) replaceTargetId = String(existing._id);
|
|
1855
|
+
} else {
|
|
1856
|
+
const ext = path__default.default.extname(name);
|
|
1857
|
+
const base = name.slice(0, name.length - ext.length);
|
|
1858
|
+
let n = 1;
|
|
1859
|
+
while (await drive_default.exists({ ...dupeQuery, name })) name = `${base} (${n++})${ext}`;
|
|
2562
1860
|
}
|
|
2563
|
-
const
|
|
2564
|
-
owner,
|
|
2565
|
-
"provider.type": provider.name,
|
|
1861
|
+
const drive = new drive_default({
|
|
1862
|
+
owner: ownerValue,
|
|
2566
1863
|
storageAccountId: accountId || null,
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
const restoreData = deleteQuerySchema.safeParse(req.query);
|
|
2576
|
-
if (!restoreData.success) return void res.status(400).json({ status: 400, message: "Could not restore: invalid ID" });
|
|
2577
|
-
const { id } = restoreData.data;
|
|
2578
|
-
const drive = await drive_default.findById(id);
|
|
2579
|
-
if (!drive) return void res.status(404).json({ status: 404, message: "Could not restore: item not found" });
|
|
2580
|
-
let targetParentId = drive.parentId;
|
|
2581
|
-
if (targetParentId) {
|
|
2582
|
-
const parent = await drive_default.findById(targetParentId);
|
|
2583
|
-
if (parent?.trashedAt) {
|
|
2584
|
-
targetParentId = null;
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
2588
|
-
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
2589
|
-
try {
|
|
2590
|
-
await itemProvider.untrash([id], owner, itemAccountId);
|
|
2591
|
-
if (targetParentId !== drive.parentId) {
|
|
2592
|
-
await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
|
|
2593
|
-
}
|
|
2594
|
-
} catch (e) {
|
|
2595
|
-
console.error("Provider restore failed:", e);
|
|
2596
|
-
}
|
|
2597
|
-
drive.trashedAt = null;
|
|
2598
|
-
drive.parentId = targetParentId;
|
|
2599
|
-
await drive.save();
|
|
2600
|
-
res.status(200).json({
|
|
2601
|
-
status: 200,
|
|
2602
|
-
message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
|
|
2603
|
-
data: null
|
|
1864
|
+
provider: { type: provider.name },
|
|
1865
|
+
name,
|
|
1866
|
+
parentId,
|
|
1867
|
+
order: 0,
|
|
1868
|
+
information: { type: "FILE", sizeInBytes: upload.size ?? 0, mime: meta.filetype || "application/octet-stream", path: "" },
|
|
1869
|
+
status: "UPLOADING",
|
|
1870
|
+
expiresAt: authenticated ? null : new Date(Date.now() + (config2.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4),
|
|
1871
|
+
meta: { tusId: upload.id }
|
|
2604
1872
|
});
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
console.error(`Failed to move item ${id}`, e);
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
res.status(200).json({ status: 200, message: "Moved", data: { items: withSignedUrls(items, config) } });
|
|
2622
|
-
return;
|
|
2623
|
-
}
|
|
2624
|
-
case "reorder": {
|
|
2625
|
-
if (req.method !== "POST") {
|
|
2626
|
-
return void res.status(405).json({ status: 405, message: "Reordering requires a POST request" });
|
|
2627
|
-
}
|
|
2628
|
-
const reorderData = reorderBodySchema.safeParse(req.body);
|
|
2629
|
-
if (!reorderData.success) {
|
|
2630
|
-
return void res.status(400).json({ status: 400, message: "Could not reorder: invalid request data" });
|
|
2631
|
-
}
|
|
2632
|
-
const { ids } = reorderData.data;
|
|
2633
|
-
const query = {
|
|
2634
|
-
_id: { $in: ids },
|
|
2635
|
-
"provider.type": provider.name,
|
|
2636
|
-
storageAccountId: accountId || null,
|
|
2637
|
-
trashedAt: null
|
|
2638
|
-
};
|
|
2639
|
-
if (!isRootMode) {
|
|
2640
|
-
query.owner = owner;
|
|
2641
|
-
}
|
|
2642
|
-
const existingItems = await drive_default.find(query, { _id: 1, parentId: 1 });
|
|
2643
|
-
if (existingItems.length !== ids.length) {
|
|
2644
|
-
return void res.status(404).json({ status: 404, message: "Could not reorder: one or more items were not found" });
|
|
2645
|
-
}
|
|
2646
|
-
const parentIds = new Set(existingItems.map((item) => item.parentId ? item.parentId.toString() : "root"));
|
|
2647
|
-
if (parentIds.size > 1) {
|
|
2648
|
-
return void res.status(400).json({ status: 400, message: "Could not reorder: all items must be in the same folder" });
|
|
1873
|
+
if (provider.name === "LOCAL" && drive.information.type === "FILE") drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
|
|
1874
|
+
await drive.save();
|
|
1875
|
+
try {
|
|
1876
|
+
const item = await provider.uploadFile(drive, filePath, accountId);
|
|
1877
|
+
if (replaceTargetId) await provider.delete([replaceTargetId], ownerValue, accountId);
|
|
1878
|
+
fs__default.default.rmSync(filePath, { force: true });
|
|
1879
|
+
fs__default.default.rmSync(`${filePath}.json`, { force: true });
|
|
1880
|
+
if (!authenticated) decConcurrent();
|
|
1881
|
+
return { headers: { "X-Drive-Item": encodeURIComponent(JSON.stringify(withSignedUrl(item, config2))) } };
|
|
1882
|
+
} catch (err) {
|
|
1883
|
+
await drive_default.deleteOne({ _id: drive._id });
|
|
1884
|
+
if (!authenticated) decConcurrent();
|
|
1885
|
+
throw { status_code: 500, body: err instanceof Error ? err.message : "Could not finish upload" };
|
|
2649
1886
|
}
|
|
2650
|
-
const operations = ids.map((id, order) => ({
|
|
2651
|
-
updateOne: {
|
|
2652
|
-
filter: {
|
|
2653
|
-
_id: id,
|
|
2654
|
-
"provider.type": provider.name,
|
|
2655
|
-
storageAccountId: accountId || null,
|
|
2656
|
-
trashedAt: null,
|
|
2657
|
-
...isRootMode ? {} : { owner }
|
|
2658
|
-
},
|
|
2659
|
-
update: { $set: { order } }
|
|
2660
|
-
}
|
|
2661
|
-
}));
|
|
2662
|
-
await drive_default.bulkWrite(operations);
|
|
2663
|
-
const updatedItems = await drive_default.find(query, {}, { sort: { order: 1 } });
|
|
2664
|
-
const plainItems = withSignedUrls(await Promise.all(updatedItems.map((item) => item.toClient())), config);
|
|
2665
|
-
res.status(200).json({ status: 200, message: "Reordered", data: { items: plainItems } });
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
case "rename": {
|
|
2669
|
-
const renameData = renameBodySchema.safeParse({ id: req.query.id, ...req.body });
|
|
2670
|
-
if (!renameData.success) return void res.status(400).json({ status: 400, message: "Could not rename: invalid request data" });
|
|
2671
|
-
const { id, newName } = renameData.data;
|
|
2672
|
-
const item = withSignedUrl(await provider.rename(id, newName, owner, accountId), config);
|
|
2673
|
-
res.status(200).json({ status: 200, message: "Renamed", data: { item } });
|
|
2674
|
-
return;
|
|
2675
|
-
}
|
|
2676
|
-
default: {
|
|
2677
|
-
res.status(400).json({ status: 400, message: `Unknown action requested: "${action}"` });
|
|
2678
|
-
return;
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
};
|
|
2682
|
-
|
|
2683
|
-
// src/server/index.ts
|
|
2684
|
-
var parseJsonBody = (req) => {
|
|
2685
|
-
return new Promise((resolve) => {
|
|
2686
|
-
try {
|
|
2687
|
-
const chunks = [];
|
|
2688
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
2689
|
-
req.on("end", () => {
|
|
2690
|
-
const raw = Buffer.concat(chunks).toString();
|
|
2691
|
-
if (!raw) return resolve({});
|
|
2692
|
-
try {
|
|
2693
|
-
resolve(JSON.parse(raw));
|
|
2694
|
-
} catch {
|
|
2695
|
-
resolve({});
|
|
2696
|
-
}
|
|
2697
|
-
});
|
|
2698
|
-
req.on("error", () => resolve({}));
|
|
2699
|
-
} catch {
|
|
2700
|
-
resolve({});
|
|
2701
1887
|
}
|
|
2702
1888
|
});
|
|
1889
|
+
server$1.on(server.EVENTS.POST_TERMINATE, (req) => {
|
|
1890
|
+
const ctx = getCtx(req);
|
|
1891
|
+
if (ctx && !ctx.authenticated) decConcurrent();
|
|
1892
|
+
});
|
|
1893
|
+
g.__nextDrive.tus = server$1;
|
|
1894
|
+
return server$1;
|
|
2703
1895
|
};
|
|
2704
|
-
var
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
try {
|
|
2728
|
-
const mode = config.mode || "NORMAL";
|
|
2729
|
-
if (action === "information") {
|
|
2730
|
-
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
2731
|
-
const googleConfigured = !!(clientId && clientSecret && redirectUri);
|
|
2732
|
-
let authenticated2 = false;
|
|
2733
|
-
try {
|
|
2734
|
-
await getDriveInformation({ method: "REQUEST", req });
|
|
2735
|
-
authenticated2 = true;
|
|
2736
|
-
} catch {
|
|
2737
|
-
authenticated2 = false;
|
|
2738
|
-
}
|
|
2739
|
-
res.status(200).json({
|
|
2740
|
-
status: 200,
|
|
2741
|
-
message: "Information retrieved",
|
|
2742
|
-
data: {
|
|
2743
|
-
providers: {
|
|
2744
|
-
google: googleConfigured
|
|
2745
|
-
},
|
|
2746
|
-
mode,
|
|
2747
|
-
authenticated: authenticated2,
|
|
2748
|
-
unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
|
|
2749
|
-
}
|
|
2750
|
-
});
|
|
2751
|
-
return;
|
|
2752
|
-
}
|
|
2753
|
-
const isRootMode = mode === "ROOT";
|
|
2754
|
-
let information;
|
|
2755
|
-
let authenticated = true;
|
|
2756
|
-
try {
|
|
2757
|
-
information = await getDriveInformation({ method: "REQUEST", req });
|
|
2758
|
-
} catch (err) {
|
|
2759
|
-
if ((action === "upload" || action === "cancel") && config.security?.unauthenticated?.enabled) {
|
|
2760
|
-
information = { key: null, storage: { quotaInBytes: 0 } };
|
|
2761
|
-
authenticated = false;
|
|
2762
|
-
} else {
|
|
2763
|
-
throw err;
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
const { key: owner } = information;
|
|
2767
|
-
const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
|
|
2768
|
-
if (wasAuthHandled) return;
|
|
2769
|
-
const { provider, accountId } = await resolveProvider(req, owner);
|
|
2770
|
-
await handleDriveAction({
|
|
2771
|
-
req,
|
|
2772
|
-
res,
|
|
2773
|
-
action,
|
|
2774
|
-
config,
|
|
2775
|
-
owner,
|
|
2776
|
-
isRootMode,
|
|
2777
|
-
authenticated,
|
|
2778
|
-
information,
|
|
2779
|
-
provider,
|
|
2780
|
-
accountId
|
|
2781
|
-
});
|
|
2782
|
-
} catch (error) {
|
|
2783
|
-
console.error(`[next-drive] Error handling action ${action}:`, error);
|
|
2784
|
-
const detail = error instanceof Error ? error.message : "Something went wrong while processing your request";
|
|
2785
|
-
res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
|
|
2786
|
-
}
|
|
1896
|
+
var handleUpload = async (req, res, ctx) => {
|
|
1897
|
+
const host = req.headers.host || "localhost";
|
|
1898
|
+
const headers = new Headers();
|
|
1899
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1900
|
+
if (Array.isArray(value)) headers.set(key, value.join(", "));
|
|
1901
|
+
else if (typeof value === "string") headers.set(key, value);
|
|
1902
|
+
}
|
|
1903
|
+
const hasBody = req.method === "POST" || req.method === "PATCH" || req.method === "PUT";
|
|
1904
|
+
const request = new Request(`http://${host}${req.url}`, {
|
|
1905
|
+
method: req.method,
|
|
1906
|
+
headers,
|
|
1907
|
+
body: hasBody ? stream.Readable.toWeb(req) : void 0,
|
|
1908
|
+
// ** Required by Node/undici when streaming a request body.
|
|
1909
|
+
duplex: "half"
|
|
1910
|
+
});
|
|
1911
|
+
request.__driveCtx = ctx;
|
|
1912
|
+
request.__nodeReq = req;
|
|
1913
|
+
const response = await getTusServer().handleWeb(request);
|
|
1914
|
+
res.statusCode = response.status;
|
|
1915
|
+
response.headers.forEach((value, key) => {
|
|
1916
|
+
if (value !== void 0 && value !== null) res.setHeader(key, value);
|
|
1917
|
+
});
|
|
1918
|
+
res.end(response.body ? Buffer.from(await response.arrayBuffer()) : void 0);
|
|
2787
1919
|
};
|
|
2788
1920
|
|
|
2789
|
-
exports.
|
|
1921
|
+
exports.GoogleDriveProvider = GoogleDriveProvider;
|
|
1922
|
+
exports.LocalStorageProvider = LocalStorageProvider;
|
|
1923
|
+
exports.account_default = account_default;
|
|
2790
1924
|
exports.driveCleanup = driveCleanup;
|
|
2791
1925
|
exports.driveConfiguration = driveConfiguration;
|
|
2792
1926
|
exports.driveConfirm = driveConfirm;
|
|
2793
1927
|
exports.driveDelete = driveDelete;
|
|
2794
1928
|
exports.driveFilePath = driveFilePath;
|
|
2795
|
-
exports.driveFileSchemaZod = driveFileSchemaZod;
|
|
2796
1929
|
exports.driveGetUrl = driveGetUrl;
|
|
2797
1930
|
exports.driveInfo = driveInfo;
|
|
2798
1931
|
exports.driveList = driveList;
|
|
@@ -2803,5 +1936,11 @@ exports.driveUpload = driveUpload;
|
|
|
2803
1936
|
exports.drive_default = drive_default;
|
|
2804
1937
|
exports.getDriveConfig = getDriveConfig;
|
|
2805
1938
|
exports.getDriveInformation = getDriveInformation;
|
|
2806
|
-
|
|
2807
|
-
|
|
1939
|
+
exports.getImageSettings = getImageSettings;
|
|
1940
|
+
exports.getTusServer = getTusServer;
|
|
1941
|
+
exports.handleUpload = handleUpload;
|
|
1942
|
+
exports.resolveProvider = resolveProvider;
|
|
1943
|
+
exports.withSignedUrl = withSignedUrl;
|
|
1944
|
+
exports.withSignedUrls = withSignedUrls;
|
|
1945
|
+
//# sourceMappingURL=chunk-45FZNILV.cjs.map
|
|
1946
|
+
//# sourceMappingURL=chunk-45FZNILV.cjs.map
|