@muhgholy/next-drive 4.23.24 → 4.23.26
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-CLF77PSE.js +755 -0
- package/dist/chunk-CLF77PSE.js.map +1 -0
- package/dist/{chunk-IVSBLEMY.cjs → chunk-DUTHZ5HA.cjs} +201 -1027
- package/dist/chunk-DUTHZ5HA.cjs.map +1 -0
- package/dist/chunk-PSJYCISB.cjs +765 -0
- package/dist/chunk-PSJYCISB.cjs.map +1 -0
- package/dist/{chunk-CPMMHIQM.js → chunk-W7PHCOE6.js} +190 -1022
- package/dist/chunk-W7PHCOE6.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/config.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-JZQ3PKBS.cjs +16 -0
- package/dist/tus-JZQ3PKBS.cjs.map +1 -0
- package/dist/tus-MNPA66VL.js +3 -0
- package/dist/tus-MNPA66VL.js.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 mongoose = require('mongoose');
|
|
7
|
+
var server = require('@tus/server');
|
|
8
|
+
var fileStore = require('@tus/file-store');
|
|
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 },
|
|
@@ -263,7 +263,7 @@ var getGlobal = () => {
|
|
|
263
263
|
config: null,
|
|
264
264
|
migrationPromise: null,
|
|
265
265
|
initialized: false,
|
|
266
|
-
abuse: { ipHits: /* @__PURE__ */ new Map(),
|
|
266
|
+
abuse: { ipHits: /* @__PURE__ */ new Map(), active: /* @__PURE__ */ new Map(), lastSweep: 0 }
|
|
267
267
|
};
|
|
268
268
|
}
|
|
269
269
|
return globalThis[GLOBAL_KEY];
|
|
@@ -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-JZQ3PKBS.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,200 @@ 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
|
-
return;
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
if (!
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
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" });
|
|
1770
|
+
// src/server/tus.ts
|
|
1771
|
+
var UPLOAD_EXPIRATION_MS = 24 * 60 * 60 * 1e3;
|
|
1772
|
+
var getCtx = (req) => req.__driveCtx;
|
|
1773
|
+
var abuseStore = () => globalThis.__nextDrive.abuse;
|
|
1774
|
+
var countActiveAnon = (store) => {
|
|
1775
|
+
const cutoff = Date.now() - UPLOAD_EXPIRATION_MS;
|
|
1776
|
+
for (const [id, ts] of store.active) if (ts < cutoff) store.active.delete(id);
|
|
1777
|
+
return store.active.size;
|
|
1778
|
+
};
|
|
1779
|
+
var releaseSlot = (id) => abuseStore()?.active.delete(id);
|
|
1780
|
+
var getTusServer = () => {
|
|
1781
|
+
const g = globalThis;
|
|
1782
|
+
g.__nextDrive = g.__nextDrive ?? {};
|
|
1783
|
+
if (g.__nextDrive.tus) return g.__nextDrive.tus;
|
|
1784
|
+
const config = getDriveConfig();
|
|
1785
|
+
const tusDir = path__default.default.join(config.storage.path, "tus");
|
|
1786
|
+
fs__default.default.mkdirSync(tusDir, { recursive: true });
|
|
1787
|
+
const apiPath = new URL(config.apiUrl || "/api/drive", "http://x").pathname;
|
|
1788
|
+
const server$1 = new server.Server({
|
|
1789
|
+
path: apiPath,
|
|
1790
|
+
datastore: new fileStore.FileStore({ directory: tusDir, expirationPeriodInMilliseconds: UPLOAD_EXPIRATION_MS }),
|
|
1791
|
+
// ** tus writes the response (and its CORS headers) last, so the custom account/item headers
|
|
1792
|
+
// ** must be declared here to survive cross-origin requests.
|
|
1793
|
+
allowedHeaders: ["X-Drive-Account"],
|
|
1794
|
+
exposedHeaders: ["X-Drive-Item"],
|
|
1795
|
+
maxSize: (req) => {
|
|
1796
|
+
const ctx = getCtx(req);
|
|
1797
|
+
if (!ctx) return Number.MAX_SAFE_INTEGER;
|
|
1798
|
+
return ctx.authenticated ? ctx.config.security?.maxUploadSizeInBytes ?? Number.MAX_SAFE_INTEGER : ctx.config.security?.unauthenticated?.maxUploadSizeInBytes ?? 0;
|
|
1799
|
+
},
|
|
1800
|
+
// ** Keep the upload id in the query string so every method stays on `?action=upload`.
|
|
1801
|
+
generateUrl: (_req, { path: p, id }) => `${p}?action=upload&id=${id}`,
|
|
1802
|
+
// ** SECURITY: the id becomes a filesystem path inside FileStore (fs.unlink/read/resolve), and it
|
|
1803
|
+
// ** arrives from the client-controlled query string. Only accept the exact format our
|
|
1804
|
+
// ** namingFunction produces (32 lowercase hex) so a crafted `?id=../../..` cannot traverse out
|
|
1805
|
+
// ** of the upload dir (which would allow arbitrary file read/delete). Anything else -> 404.
|
|
1806
|
+
getFileIdFromRequest: (req) => {
|
|
1807
|
+
const id = new URL(req.url, "http://x").searchParams.get("id");
|
|
1808
|
+
return id && /^[a-f0-9]{32}$/.test(id) ? id : void 0;
|
|
1809
|
+
},
|
|
1810
|
+
onUploadCreate: async (req, upload) => {
|
|
1811
|
+
const ctx = getCtx(req);
|
|
1812
|
+
if (!ctx) throw { status_code: 500, body: "Could not upload: request context is missing" };
|
|
1813
|
+
const { config: config2, authenticated, owner, provider, accountId, information, isRootMode } = ctx;
|
|
1814
|
+
const size = upload.size ?? 0;
|
|
1815
|
+
const fileType = upload.metadata?.filetype || "";
|
|
1816
|
+
if (!authenticated) {
|
|
1817
|
+
const unauth = config2.security?.unauthenticated;
|
|
1818
|
+
if (!unauth?.enabled) throw { status_code: 401, body: "Authentication required to upload" };
|
|
1819
|
+
if (size > unauth.maxUploadSizeInBytes) throw { status_code: 413, body: "Could not upload: file exceeds the maximum allowed size" };
|
|
1820
|
+
if (fileType && !validateMimeType(fileType, unauth.allowedMimeTypes)) throw { status_code: 400, body: `Could not upload: file type "${fileType}" is not allowed` };
|
|
1821
|
+
const abuse = unauth.abuse;
|
|
1822
|
+
if (abuse) {
|
|
1823
|
+
const store = abuseStore();
|
|
1824
|
+
const now = Date.now();
|
|
1825
|
+
if (now - store.lastSweep > 6e4) {
|
|
1826
|
+
store.lastSweep = now;
|
|
1827
|
+
for (const [seenIp, seenHits] of store.ipHits) {
|
|
1828
|
+
const fresh = seenHits.filter((t) => now - t < 36e5);
|
|
1829
|
+
if (fresh.length) store.ipHits.set(seenIp, fresh);
|
|
1830
|
+
else store.ipHits.delete(seenIp);
|
|
2340
1831
|
}
|
|
2341
1832
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
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;
|
|
1833
|
+
const nreq = req.__nodeReq;
|
|
1834
|
+
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";
|
|
1835
|
+
const hits = (store.ipHits.get(ip) ?? []).filter((t) => now - t < 36e5);
|
|
1836
|
+
const perIp = abuse.perIp;
|
|
1837
|
+
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" };
|
|
1838
|
+
if (abuse.hourlyPerIp && hits.length >= abuse.hourlyPerIp) throw { status_code: 429, body: "Hourly upload limit reached, please try again later" };
|
|
1839
|
+
if (abuse.maxConcurrent && countActiveAnon(store) >= abuse.maxConcurrent) throw { status_code: 429, body: "Server is busy with uploads, please try again later" };
|
|
1840
|
+
if (abuse.maxLiveBytes) {
|
|
1841
|
+
const [agg] = await drive_default.aggregate([{ $match: { expiresAt: { $ne: null } } }, { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }]);
|
|
1842
|
+
if ((agg?.total ?? 0) + size > abuse.maxLiveBytes) throw { status_code: 429, body: "Temporary storage is full, please try again later" };
|
|
2381
1843
|
}
|
|
1844
|
+
hits.push(now);
|
|
1845
|
+
store.ipHits.set(ip, hits);
|
|
1846
|
+
store.active.set(upload.id, now);
|
|
2382
1847
|
}
|
|
2383
|
-
|
|
2384
|
-
if (
|
|
2385
|
-
|
|
2386
|
-
const
|
|
2387
|
-
|
|
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 } });
|
|
2479
|
-
}
|
|
1848
|
+
} else {
|
|
1849
|
+
if (fileType && config2.security && !validateMimeType(fileType, config2.security.allowedMimeTypes)) throw { status_code: 400, body: `Could not upload: file type "${fileType}" is not allowed` };
|
|
1850
|
+
if (!isRootMode) {
|
|
1851
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
1852
|
+
if (quota.usedInBytes + size > quota.quotaInBytes) throw { status_code: 413, body: "Could not upload: you have run out of storage space" };
|
|
2480
1853
|
}
|
|
2481
|
-
} catch (e) {
|
|
2482
|
-
cleanupTempFiles(files);
|
|
2483
|
-
throw e;
|
|
2484
1854
|
}
|
|
2485
|
-
return;
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
const
|
|
2489
|
-
if (!
|
|
2490
|
-
const {
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
1855
|
+
return {};
|
|
1856
|
+
},
|
|
1857
|
+
onUploadFinish: async (req, upload) => {
|
|
1858
|
+
const ctx = getCtx(req);
|
|
1859
|
+
if (!ctx) throw { status_code: 500, body: "Could not finish upload: request context is missing" };
|
|
1860
|
+
const { config: config2, authenticated, owner, provider, accountId, information, isRootMode } = ctx;
|
|
1861
|
+
const meta = upload.metadata ?? {};
|
|
1862
|
+
const size = upload.size ?? 0;
|
|
1863
|
+
const filePath = upload.storage?.path ?? path__default.default.join(tusDir, upload.id);
|
|
1864
|
+
const ownerValue = authenticated ? owner : null;
|
|
1865
|
+
if (!authenticated) releaseSlot(upload.id);
|
|
1866
|
+
const already = await drive_default.findOne({ "meta.tusId": upload.id });
|
|
1867
|
+
if (already) return { headers: { "X-Drive-Item": encodeURIComponent(JSON.stringify(withSignedUrl(await already.toClient(), config2))) } };
|
|
1868
|
+
if (!authenticated) {
|
|
1869
|
+
const unauth = config2.security?.unauthenticated;
|
|
1870
|
+
if (!unauth?.enabled) throw { status_code: 401, body: "Authentication required to upload" };
|
|
1871
|
+
if (size > unauth.maxUploadSizeInBytes) throw { status_code: 413, body: "Could not upload: file exceeds the maximum allowed size" };
|
|
1872
|
+
if (unauth.abuse?.maxLiveBytes) {
|
|
1873
|
+
const [agg] = await drive_default.aggregate([{ $match: { expiresAt: { $ne: null } } }, { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }]);
|
|
1874
|
+
if ((agg?.total ?? 0) + size > unauth.abuse.maxLiveBytes) throw { status_code: 429, body: "Temporary storage is full, please try again later" };
|
|
2501
1875
|
}
|
|
1876
|
+
} else if (!isRootMode) {
|
|
1877
|
+
if (config2.security && size > config2.security.maxUploadSizeInBytes) throw { status_code: 413, body: "Could not upload: file exceeds the maximum allowed size" };
|
|
1878
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
1879
|
+
if (quota.usedInBytes + size > quota.quotaInBytes) throw { status_code: 413, body: "Could not upload: you have run out of storage space" };
|
|
2502
1880
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const { name, parentId } = folderData.data;
|
|
2510
|
-
const item = withSignedUrl(await provider.createFolder(name, parentId ?? null, owner, accountId), config);
|
|
2511
|
-
res.status(201).json({ status: 201, message: "Folder created", data: { item } });
|
|
2512
|
-
return;
|
|
2513
|
-
}
|
|
2514
|
-
case "delete": {
|
|
2515
|
-
const deleteData = deleteQuerySchema.safeParse(req.query);
|
|
2516
|
-
if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not move to trash: invalid ID" });
|
|
2517
|
-
const { id } = deleteData.data;
|
|
2518
|
-
const drive = await drive_default.findById(id);
|
|
2519
|
-
if (!drive) return void res.status(404).json({ status: 404, message: "Could not move to trash: item not found" });
|
|
2520
|
-
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
2521
|
-
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
2522
|
-
try {
|
|
2523
|
-
await itemProvider.trash([id], owner, itemAccountId);
|
|
2524
|
-
} catch (e) {
|
|
2525
|
-
console.error("Provider trash failed:", e);
|
|
1881
|
+
let parentId = null;
|
|
1882
|
+
if (authenticated && meta.folderId && meta.folderId !== "root") {
|
|
1883
|
+
if (!mongoose.isValidObjectId(meta.folderId)) throw { status_code: 400, body: "Could not upload: invalid target folder" };
|
|
1884
|
+
const folder = await drive_default.findOne({ _id: meta.folderId, owner: ownerValue, storageAccountId: accountId || null, "information.type": "FOLDER", trashedAt: null });
|
|
1885
|
+
if (!folder) throw { status_code: 404, body: "Could not upload: target folder was not found" };
|
|
1886
|
+
parentId = meta.folderId;
|
|
2526
1887
|
}
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
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);
|
|
1888
|
+
let name = (meta.filename || "file").replace(/[/\\]/g, "_").replace(/[\x00-\x1f<>:"|?*]/g, "").replace(/^\.+/, "").trim().slice(0, 255) || "file";
|
|
1889
|
+
let replaceTargetId = null;
|
|
1890
|
+
const dupeQuery = { owner: ownerValue, storageAccountId: accountId || null, "provider.type": provider.name, parentId, "information.type": "FILE", trashedAt: null };
|
|
1891
|
+
if (authenticated && meta.conflictAction === "replace") {
|
|
1892
|
+
const existing = await drive_default.findOne({ ...dupeQuery, name });
|
|
1893
|
+
if (existing) replaceTargetId = String(existing._id);
|
|
1894
|
+
} else {
|
|
1895
|
+
const ext = path__default.default.extname(name);
|
|
1896
|
+
const base = name.slice(0, name.length - ext.length);
|
|
1897
|
+
let n = 1;
|
|
1898
|
+
while (await drive_default.exists({ ...dupeQuery, name })) name = `${base} (${n++})${ext}`;
|
|
2562
1899
|
}
|
|
2563
|
-
const
|
|
2564
|
-
owner,
|
|
2565
|
-
"provider.type": provider.name,
|
|
1900
|
+
const drive = new drive_default({
|
|
1901
|
+
owner: ownerValue,
|
|
2566
1902
|
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
|
|
1903
|
+
provider: { type: provider.name },
|
|
1904
|
+
name,
|
|
1905
|
+
parentId,
|
|
1906
|
+
order: 0,
|
|
1907
|
+
information: { type: "FILE", sizeInBytes: size, mime: meta.filetype || "application/octet-stream", path: "" },
|
|
1908
|
+
status: "UPLOADING",
|
|
1909
|
+
expiresAt: authenticated ? null : new Date(Date.now() + (config2.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4),
|
|
1910
|
+
meta: { tusId: upload.id }
|
|
2604
1911
|
});
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
} catch (e) {
|
|
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" });
|
|
1912
|
+
if (provider.name === "LOCAL" && drive.information.type === "FILE") drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
|
|
1913
|
+
await drive.save();
|
|
1914
|
+
try {
|
|
1915
|
+
const item = await provider.uploadFile(drive, filePath, accountId);
|
|
1916
|
+
if (replaceTargetId) await provider.delete([replaceTargetId], ownerValue, accountId);
|
|
1917
|
+
fs__default.default.rmSync(filePath, { force: true });
|
|
1918
|
+
fs__default.default.rmSync(`${filePath}.json`, { force: true });
|
|
1919
|
+
return { headers: { "X-Drive-Item": encodeURIComponent(JSON.stringify(withSignedUrl(item, config2))) } };
|
|
1920
|
+
} catch (err) {
|
|
1921
|
+
await drive_default.deleteOne({ _id: drive._id });
|
|
1922
|
+
console.error("[next-drive] Upload finalize failed:", err);
|
|
1923
|
+
throw { status_code: 500, body: "Could not finish upload" };
|
|
2649
1924
|
}
|
|
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
1925
|
}
|
|
2702
1926
|
});
|
|
1927
|
+
server$1.on(server.EVENTS.POST_TERMINATE, (_req, _res, id) => releaseSlot(id));
|
|
1928
|
+
g.__nextDrive.tus = server$1;
|
|
1929
|
+
return server$1;
|
|
2703
1930
|
};
|
|
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
|
-
}
|
|
1931
|
+
var handleUpload = async (req, res, ctx) => {
|
|
1932
|
+
const host = req.headers.host || "localhost";
|
|
1933
|
+
const headers = new Headers();
|
|
1934
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1935
|
+
if (Array.isArray(value)) headers.set(key, value.join(", "));
|
|
1936
|
+
else if (typeof value === "string") headers.set(key, value);
|
|
1937
|
+
}
|
|
1938
|
+
const hasBody = req.method === "POST" || req.method === "PATCH" || req.method === "PUT";
|
|
1939
|
+
const request = new Request(`http://${host}${req.url}`, {
|
|
1940
|
+
method: req.method,
|
|
1941
|
+
headers,
|
|
1942
|
+
body: hasBody ? stream.Readable.toWeb(req) : void 0,
|
|
1943
|
+
// ** Required by Node/undici when streaming a request body.
|
|
1944
|
+
duplex: "half"
|
|
1945
|
+
});
|
|
1946
|
+
request.__driveCtx = ctx;
|
|
1947
|
+
request.__nodeReq = req;
|
|
1948
|
+
const response = await getTusServer().handleWeb(request);
|
|
1949
|
+
res.statusCode = response.status;
|
|
1950
|
+
response.headers.forEach((value, key) => {
|
|
1951
|
+
if (value !== void 0 && value !== null) res.setHeader(key, value);
|
|
1952
|
+
});
|
|
1953
|
+
res.end(response.body ? Buffer.from(await response.arrayBuffer()) : void 0);
|
|
2787
1954
|
};
|
|
2788
1955
|
|
|
2789
|
-
exports.
|
|
1956
|
+
exports.GoogleDriveProvider = GoogleDriveProvider;
|
|
1957
|
+
exports.LocalStorageProvider = LocalStorageProvider;
|
|
1958
|
+
exports.account_default = account_default;
|
|
2790
1959
|
exports.driveCleanup = driveCleanup;
|
|
2791
1960
|
exports.driveConfiguration = driveConfiguration;
|
|
2792
1961
|
exports.driveConfirm = driveConfirm;
|
|
2793
1962
|
exports.driveDelete = driveDelete;
|
|
2794
1963
|
exports.driveFilePath = driveFilePath;
|
|
2795
|
-
exports.driveFileSchemaZod = driveFileSchemaZod;
|
|
2796
1964
|
exports.driveGetUrl = driveGetUrl;
|
|
2797
1965
|
exports.driveInfo = driveInfo;
|
|
2798
1966
|
exports.driveList = driveList;
|
|
@@ -2803,5 +1971,11 @@ exports.driveUpload = driveUpload;
|
|
|
2803
1971
|
exports.drive_default = drive_default;
|
|
2804
1972
|
exports.getDriveConfig = getDriveConfig;
|
|
2805
1973
|
exports.getDriveInformation = getDriveInformation;
|
|
2806
|
-
|
|
2807
|
-
|
|
1974
|
+
exports.getImageSettings = getImageSettings;
|
|
1975
|
+
exports.getTusServer = getTusServer;
|
|
1976
|
+
exports.handleUpload = handleUpload;
|
|
1977
|
+
exports.resolveProvider = resolveProvider;
|
|
1978
|
+
exports.withSignedUrl = withSignedUrl;
|
|
1979
|
+
exports.withSignedUrls = withSignedUrls;
|
|
1980
|
+
//# sourceMappingURL=chunk-DUTHZ5HA.cjs.map
|
|
1981
|
+
//# sourceMappingURL=chunk-DUTHZ5HA.cjs.map
|