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