@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.
- package/dist/{chunk-IVSBLEMY.cjs → chunk-45FZNILV.cjs} +170 -1031
- package/dist/chunk-45FZNILV.cjs.map +1 -0
- package/dist/{chunk-CPMMHIQM.js → chunk-VBFXHZBY.js} +159 -1026
- package/dist/chunk-VBFXHZBY.js.map +1 -0
- package/dist/chunk-VTCBYFAK.cjs +765 -0
- package/dist/chunk-VTCBYFAK.cjs.map +1 -0
- package/dist/chunk-ZNWS7VJP.js +755 -0
- package/dist/chunk-ZNWS7VJP.js.map +1 -0
- package/dist/client/components/drive/dnd/context.d.ts.map +1 -1
- package/dist/client/components/drive/explorer.d.ts.map +1 -1
- package/dist/client/components/drive/file-grid.d.ts.map +1 -1
- package/dist/client/hooks/use-marquee.d.ts +14 -0
- package/dist/client/hooks/use-marquee.d.ts.map +1 -0
- package/dist/client/index.cjs +170 -150
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.js +169 -150
- package/dist/client/index.js.map +1 -1
- package/dist/client/upload.d.ts +9 -4
- package/dist/client/upload.d.ts.map +1 -1
- package/dist/server/actions/drive.d.ts.map +1 -1
- package/dist/server/controllers/drive.d.ts.map +1 -1
- package/dist/server/express.cjs +15 -14
- package/dist/server/express.cjs.map +1 -1
- package/dist/server/express.js +4 -2
- package/dist/server/express.js.map +1 -1
- package/dist/server/hono.cjs +15 -14
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.js +4 -2
- package/dist/server/hono.js.map +1 -1
- package/dist/server/index.cjs +28 -27
- package/dist/server/index.js +2 -1
- package/dist/server/tus.d.ts +17 -0
- package/dist/server/tus.d.ts.map +1 -0
- package/dist/server/zod/schemas.d.ts +0 -53
- package/dist/server/zod/schemas.d.ts.map +1 -1
- package/dist/tus-6OAOA454.js +3 -0
- package/dist/tus-6OAOA454.js.map +1 -0
- package/dist/tus-P67YULWB.cjs +16 -0
- package/dist/tus-P67YULWB.cjs.map +1 -0
- package/dist/types/server/api.d.ts +1 -1
- package/dist/types/server/api.d.ts.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-CPMMHIQM.js.map +0 -1
- package/dist/chunk-IVSBLEMY.cjs.map +0 -1
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import
|
|
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
|
|
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/
|
|
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 =
|
|
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/
|
|
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 =
|
|
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 =
|
|
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-${
|
|
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-${
|
|
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
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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/
|
|
2175
|
-
var
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
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
|
-
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
const
|
|
2497
|
-
const
|
|
2498
|
-
|
|
2499
|
-
return;
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const {
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
|
2551
|
-
owner,
|
|
2552
|
-
"provider.type": provider.name,
|
|
1849
|
+
const drive = new drive_default({
|
|
1850
|
+
owner: ownerValue,
|
|
2553
1851
|
storageAccountId: accountId || null,
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
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
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
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
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
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 {
|
|
2777
|
-
//# sourceMappingURL=chunk-
|
|
2778
|
-
//# sourceMappingURL=chunk-
|
|
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
|