@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.
Files changed (39) hide show
  1. package/dist/{chunk-IVSBLEMY.cjs → chunk-45FZNILV.cjs} +170 -1031
  2. package/dist/chunk-45FZNILV.cjs.map +1 -0
  3. package/dist/{chunk-CPMMHIQM.js → chunk-VBFXHZBY.js} +159 -1026
  4. package/dist/chunk-VBFXHZBY.js.map +1 -0
  5. package/dist/chunk-VTCBYFAK.cjs +765 -0
  6. package/dist/chunk-VTCBYFAK.cjs.map +1 -0
  7. package/dist/chunk-ZNWS7VJP.js +755 -0
  8. package/dist/chunk-ZNWS7VJP.js.map +1 -0
  9. package/dist/client/index.cjs +71 -140
  10. package/dist/client/index.cjs.map +1 -1
  11. package/dist/client/index.js +70 -140
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/client/upload.d.ts +9 -4
  14. package/dist/client/upload.d.ts.map +1 -1
  15. package/dist/server/actions/drive.d.ts.map +1 -1
  16. package/dist/server/controllers/drive.d.ts.map +1 -1
  17. package/dist/server/express.cjs +15 -14
  18. package/dist/server/express.cjs.map +1 -1
  19. package/dist/server/express.js +4 -2
  20. package/dist/server/express.js.map +1 -1
  21. package/dist/server/hono.cjs +15 -14
  22. package/dist/server/hono.cjs.map +1 -1
  23. package/dist/server/hono.js +4 -2
  24. package/dist/server/hono.js.map +1 -1
  25. package/dist/server/index.cjs +28 -27
  26. package/dist/server/index.js +2 -1
  27. package/dist/server/tus.d.ts +17 -0
  28. package/dist/server/tus.d.ts.map +1 -0
  29. package/dist/server/zod/schemas.d.ts +0 -53
  30. package/dist/server/zod/schemas.d.ts.map +1 -1
  31. package/dist/tus-6OAOA454.js +3 -0
  32. package/dist/tus-6OAOA454.js.map +1 -0
  33. package/dist/tus-P67YULWB.cjs +16 -0
  34. package/dist/tus-P67YULWB.cjs.map +1 -0
  35. package/dist/types/server/api.d.ts +1 -1
  36. package/dist/types/server/api.d.ts.map +1 -1
  37. package/package.json +4 -1
  38. package/dist/chunk-CPMMHIQM.js.map +0 -1
  39. package/dist/chunk-IVSBLEMY.cjs.map +0 -1
@@ -1,28 +1,28 @@
1
1
  'use strict';
2
2
 
3
- var mongoose = require('mongoose');
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 fs = require('fs');
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 mongoose__default = /*#__PURE__*/_interopDefault(mongoose);
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 fs__default = /*#__PURE__*/_interopDefault(fs);
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/config.ts
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 = crypto3__default.default.createHash("sha256");
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/actions/public.ts
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 = crypto3__default.default.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
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 = crypto3__default.default.createHmac("sha256", secret).update(`${item.id}:${expiryTimestamp}`).digest("hex");
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-${crypto3__default.default.randomUUID()}.tmp`);
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-${crypto3__default.default.randomUUID()}.tmp`);
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
- const systemTmpDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
2134
- if (fs__default.default.existsSync(systemTmpDir)) {
2135
- try {
2136
- totalFreedInBytes += getDirSize(systemTmpDir);
2137
- fs__default.default.rmSync(systemTmpDir, { recursive: true, force: true });
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/actions/drive.ts
2188
- var handleDriveAction = async (ctx) => {
2189
- const { req, res, action, config, owner, isRootMode, authenticated, information, provider, accountId } = ctx;
2190
- switch (action) {
2191
- case "list": {
2192
- if (req.method !== "GET") return void res.status(405).json({ status: 405, message: "Listing files requires a GET request" });
2193
- const listQuery = listQuerySchema.safeParse(req.query);
2194
- if (!listQuery.success) return void res.status(400).json({ status: 400, message: "Could not list files: invalid request parameters" });
2195
- const { folderId, limit, afterId } = listQuery.data;
2196
- try {
2197
- await provider.sync(folderId || "root", owner, accountId);
2198
- } catch (e) {
2199
- console.error("Sync failed", e);
2200
- }
2201
- const query = {
2202
- "provider.type": provider.name,
2203
- storageAccountId: accountId || null,
2204
- parentId: folderId === "root" || !folderId ? null : folderId,
2205
- trashedAt: null
2206
- };
2207
- if (!isRootMode) {
2208
- query.owner = owner;
2209
- }
2210
- if (afterId) query._id = { $lt: afterId };
2211
- const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
2212
- const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
2213
- res.status(200).json({ status: 200, message: "Items retrieved", data: { items: plainItems, hasMore: items.length === limit } });
2214
- return;
2215
- }
2216
- case "search": {
2217
- const searchData = searchQuerySchema.safeParse(req.query);
2218
- if (!searchData.success) return void res.status(400).json({ status: 400, message: "Could not search: invalid request parameters" });
2219
- const { q, folderId, limit, trashed } = searchData.data;
2220
- if (!trashed) {
2221
- try {
2222
- await provider.search(q, owner, accountId);
2223
- } catch (e) {
2224
- console.error("Search sync failed", e);
2225
- }
2226
- }
2227
- const query = {
2228
- "provider.type": provider.name,
2229
- storageAccountId: accountId || null,
2230
- trashedAt: trashed ? { $ne: null } : null,
2231
- name: { $regex: q, $options: "i" }
2232
- };
2233
- if (!isRootMode) {
2234
- query.owner = owner;
2235
- }
2236
- if (folderId && folderId !== "root") query.parentId = folderId;
2237
- const items = await drive_default.find(query, {}, { limit, sort: { createdAt: -1 } });
2238
- const plainItems = withSignedUrls(await Promise.all(items.map((i) => i.toClient())), config);
2239
- res.status(200).json({ status: 200, message: "Results", data: { items: plainItems } });
2240
- return;
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
- } catch (e) {
2482
- cleanupTempFiles(files);
2483
- throw e;
2484
- }
2485
- return;
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
- res.status(200).json({ status: 200, message: "Upload cancelled", data: null });
2504
- return;
2505
- }
2506
- case "createFolder": {
2507
- const folderData = createFolderBodySchema.safeParse(req.body);
2508
- if (!folderData.success) return void res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
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);
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 query = {
2564
- owner,
2565
- "provider.type": provider.name,
1861
+ const drive = new drive_default({
1862
+ owner: ownerValue,
2566
1863
  storageAccountId: accountId || null,
2567
- trashedAt: { $ne: null }
2568
- };
2569
- const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
2570
- const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
2571
- res.status(200).json({ status: 200, message: "Trash items", data: { items: plainItems, hasMore: false } });
2572
- return;
2573
- }
2574
- case "restore": {
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
- return;
2606
- }
2607
- case "move": {
2608
- const moveData = moveBodySchema.safeParse(req.body);
2609
- if (!moveData.success) return void res.status(400).json({ status: 400, message: "Could not move: invalid request data" });
2610
- const { ids, targetFolderId } = moveData.data;
2611
- const items = [];
2612
- const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
2613
- for (const id of ids) {
2614
- try {
2615
- const item = await provider.move(id, effectiveTargetId, owner, accountId);
2616
- items.push(item);
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" });
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 driveAPIHandler = async (req, res) => {
2705
- if (req.body === void 0 && typeof req.on === "function" && (req.headers["content-type"] || "").includes("application/json")) {
2706
- req.body = await parseJsonBody(req);
2707
- } else if (req.body === void 0) {
2708
- req.body = {};
2709
- }
2710
- const action = req.query.action || (req.query.code && req.query.state ? "callback" : void 0);
2711
- let config;
2712
- try {
2713
- config = getDriveConfig();
2714
- } catch (error) {
2715
- console.error("[next-drive] Configuration error:", error);
2716
- res.status(500).json({ status: 500, message: "Drive is not ready: failed to initialize configuration" });
2717
- return;
2718
- }
2719
- const isPreflightHandled = applyCorsHeaders(req, res, config);
2720
- if (isPreflightHandled) return;
2721
- if (!action) {
2722
- res.status(400).json({ status: 400, message: 'Missing "action" parameter in request' });
2723
- return;
2724
- }
2725
- const wasPublicHandled = await handlePublicAction(req, res, action, config);
2726
- if (wasPublicHandled) return;
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.driveAPIHandler = driveAPIHandler;
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
- //# sourceMappingURL=chunk-IVSBLEMY.cjs.map
2807
- //# sourceMappingURL=chunk-IVSBLEMY.cjs.map
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