@muhgholy/next-drive 4.23.7 → 4.23.10
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/README.md +152 -1
- package/dist/{chunk-LAKT7IJJ.cjs → chunk-V75PCJHT.cjs} +962 -773
- package/dist/chunk-V75PCJHT.cjs.map +1 -0
- package/dist/{chunk-MVYNW56R.js → chunk-XUPDNN2U.js} +957 -770
- package/dist/chunk-XUPDNN2U.js.map +1 -0
- package/dist/client/components/drive/{RenameAccountDialog.d.ts → account/rename.d.ts} +2 -2
- package/dist/client/components/drive/account/rename.d.ts.map +1 -0
- package/dist/client/components/drive/{dnd-provider.d.ts → dnd/context.d.ts} +1 -1
- package/dist/client/components/drive/dnd/context.d.ts.map +1 -0
- package/dist/client/components/drive/{CreateFolderDialog.d.ts → folder/create.d.ts} +2 -2
- package/dist/client/components/drive/folder/create.d.ts.map +1 -0
- package/dist/client/components/drive/{RenameDialog.d.ts → item/rename.d.ts} +3 -3
- package/dist/client/components/drive/item/rename.d.ts.map +1 -0
- package/dist/client/components/{dialog.d.ts → shared/confirm.d.ts} +2 -2
- package/dist/client/components/shared/confirm.d.ts.map +1 -0
- package/dist/client/components/ui/{alert-dialog.d.ts → alert-modal.d.ts} +1 -1
- package/dist/client/components/ui/alert-modal.d.ts.map +1 -0
- package/dist/client/components/ui/{dialog-fullscreen.d.ts → fullscreen.d.ts} +1 -1
- package/dist/client/components/ui/fullscreen.d.ts.map +1 -0
- package/dist/client/components/ui/{dialog.d.ts → modal.d.ts} +1 -1
- package/dist/client/components/ui/modal.d.ts.map +1 -0
- package/dist/client/context.d.ts.map +1 -1
- package/dist/client/file-chooser.d.ts +1 -0
- package/dist/client/file-chooser.d.ts.map +1 -1
- package/dist/client/hooks/{useUpload.d.ts → use-upload.d.ts} +2 -2
- package/dist/client/hooks/use-upload.d.ts.map +1 -0
- package/dist/client/index.cjs +315 -206
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.ts +12 -11
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +314 -205
- package/dist/client/index.js.map +1 -1
- package/dist/server/actions/auth.d.ts +4 -0
- package/dist/server/actions/auth.d.ts.map +1 -0
- package/dist/server/actions/cors.d.ts +4 -0
- package/dist/server/actions/cors.d.ts.map +1 -0
- package/dist/server/actions/drive.d.ts +18 -0
- package/dist/server/actions/drive.d.ts.map +1 -0
- package/dist/server/actions/public.d.ts +4 -0
- package/dist/server/actions/public.d.ts.map +1 -0
- package/dist/server/actions/shared.d.ts +14 -0
- package/dist/server/actions/shared.d.ts.map +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/controllers/drive.d.ts +26 -0
- package/dist/server/controllers/drive.d.ts.map +1 -1
- package/dist/server/database/mongoose/schema/drive.d.ts +1 -0
- package/dist/server/database/mongoose/schema/drive.d.ts.map +1 -1
- package/dist/server/express.cjs +11 -11
- package/dist/server/express.js +2 -2
- package/dist/server/hono.cjs +11 -11
- package/dist/server/hono.js +2 -2
- package/dist/server/index.cjs +24 -16
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/security/{cryptoUtils.d.ts → crypto-utils.d.ts} +1 -1
- package/dist/server/security/crypto-utils.d.ts.map +1 -0
- package/dist/server/security/{mimeFilter.d.ts → mime-filter.d.ts} +1 -1
- package/dist/server/security/mime-filter.d.ts.map +1 -0
- package/dist/server/storage-adapters/google.d.ts.map +1 -0
- package/dist/server/storage-adapters/local.d.ts.map +1 -0
- package/dist/server/utils/{folderValidation.d.ts → folder-validation.d.ts} +1 -1
- package/dist/server/utils/folder-validation.d.ts.map +1 -0
- package/dist/server/utils/{imageConvert.d.ts → image-convert.d.ts} +1 -1
- package/dist/server/utils/image-convert.d.ts.map +1 -0
- package/dist/server/zod/schemas.d.ts +5 -0
- package/dist/server/zod/schemas.d.ts.map +1 -1
- package/dist/types/lib/database/drive.d.ts +1 -0
- package/dist/types/lib/database/drive.d.ts.map +1 -1
- package/dist/types/lib/database/index.d.ts +2 -2
- package/dist/types/lib/database/index.d.ts.map +1 -1
- package/dist/types/server/config.d.ts +17 -0
- package/dist/types/server/config.d.ts.map +1 -1
- package/dist/types/server/index.d.ts +5 -5
- package/dist/types/server/index.d.ts.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-LAKT7IJJ.cjs.map +0 -1
- package/dist/chunk-MVYNW56R.js.map +0 -1
- package/dist/client/components/dialog.d.ts.map +0 -1
- package/dist/client/components/drive/CreateFolderDialog.d.ts.map +0 -1
- package/dist/client/components/drive/RenameAccountDialog.d.ts.map +0 -1
- package/dist/client/components/drive/RenameDialog.d.ts.map +0 -1
- package/dist/client/components/drive/dnd-provider.d.ts.map +0 -1
- package/dist/client/components/ui/alert-dialog.d.ts.map +0 -1
- package/dist/client/components/ui/dialog-fullscreen.d.ts.map +0 -1
- package/dist/client/components/ui/dialog.d.ts.map +0 -1
- package/dist/client/hooks/useUpload.d.ts.map +0 -1
- package/dist/server/providers/google.d.ts.map +0 -1
- package/dist/server/providers/local.d.ts.map +0 -1
- package/dist/server/security/cryptoUtils.d.ts.map +0 -1
- package/dist/server/security/mimeFilter.d.ts.map +0 -1
- package/dist/server/utils/folderValidation.d.ts.map +0 -1
- package/dist/server/utils/imageConvert.d.ts.map +0 -1
- /package/dist/server/{providers → storage-adapters}/google.d.ts +0 -0
- /package/dist/server/{providers → storage-adapters}/local.d.ts +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import mongoose, { Schema, isValidObjectId } from 'mongoose';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
3
|
import os2 from 'os';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import crypto3 from 'crypto';
|
|
7
6
|
import sharp2 from 'sharp';
|
|
8
7
|
import ffmpeg from 'fluent-ffmpeg';
|
|
9
8
|
import { google } from 'googleapis';
|
|
9
|
+
import formidable from 'formidable';
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
|
|
12
|
-
// src/server/
|
|
12
|
+
// src/server/config.ts
|
|
13
13
|
var informationSchema = new Schema({
|
|
14
14
|
type: { type: String, enum: ["FILE", "FOLDER"], required: true },
|
|
15
15
|
sizeInBytes: { type: Number, default: 0 },
|
|
@@ -36,6 +36,7 @@ var DriveSchema = new Schema(
|
|
|
36
36
|
information: { type: informationSchema, required: true },
|
|
37
37
|
status: { type: String, enum: ["READY", "PROCESSING", "UPLOADING", "FAILED"], default: "PROCESSING" },
|
|
38
38
|
trashedAt: { type: Date, default: null },
|
|
39
|
+
expiresAt: { type: Date, default: null },
|
|
39
40
|
meta: { type: Schema.Types.Mixed, default: {} },
|
|
40
41
|
createdAt: { type: Date, default: Date.now }
|
|
41
42
|
},
|
|
@@ -48,6 +49,7 @@ DriveSchema.index({ owner: 1, trashedAt: 1 });
|
|
|
48
49
|
DriveSchema.index({ owner: 1, "information.hash": 1 });
|
|
49
50
|
DriveSchema.index({ owner: 1, name: "text" });
|
|
50
51
|
DriveSchema.index({ owner: 1, "provider.type": 1 });
|
|
52
|
+
DriveSchema.index({ expiresAt: 1 });
|
|
51
53
|
DriveSchema.method("toClient", async function() {
|
|
52
54
|
const data = this.toJSON();
|
|
53
55
|
return {
|
|
@@ -60,6 +62,7 @@ DriveSchema.method("toClient", async function() {
|
|
|
60
62
|
information: data.information,
|
|
61
63
|
status: data.status,
|
|
62
64
|
trashedAt: data.trashedAt,
|
|
65
|
+
expiresAt: data.expiresAt,
|
|
63
66
|
meta: data.meta,
|
|
64
67
|
createdAt: data.createdAt
|
|
65
68
|
};
|
|
@@ -246,7 +249,8 @@ var getGlobal = () => {
|
|
|
246
249
|
globalThis[GLOBAL_KEY] = {
|
|
247
250
|
config: null,
|
|
248
251
|
migrationPromise: null,
|
|
249
|
-
initialized: false
|
|
252
|
+
initialized: false,
|
|
253
|
+
abuse: { ipHits: /* @__PURE__ */ new Map(), concurrent: 0 }
|
|
250
254
|
};
|
|
251
255
|
}
|
|
252
256
|
return globalThis[GLOBAL_KEY];
|
|
@@ -275,7 +279,8 @@ var driveConfiguration = async (config) => {
|
|
|
275
279
|
// 10GB default for ROOT
|
|
276
280
|
allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
|
|
277
281
|
signedUrls: config.security?.signedUrls,
|
|
278
|
-
trash: config.security?.trash
|
|
282
|
+
trash: config.security?.trash,
|
|
283
|
+
unauthenticated: config.security?.unauthenticated
|
|
279
284
|
}
|
|
280
285
|
};
|
|
281
286
|
} else {
|
|
@@ -293,7 +298,8 @@ var driveConfiguration = async (config) => {
|
|
|
293
298
|
maxUploadSizeInBytes: config.security?.maxUploadSizeInBytes ?? 10 * 1024 * 1024,
|
|
294
299
|
allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
|
|
295
300
|
signedUrls: config.security?.signedUrls,
|
|
296
|
-
trash: config.security?.trash
|
|
301
|
+
trash: config.security?.trash,
|
|
302
|
+
unauthenticated: config.security?.unauthenticated
|
|
297
303
|
},
|
|
298
304
|
information: config.information
|
|
299
305
|
};
|
|
@@ -324,6 +330,53 @@ var getDriveInformation = async (input) => {
|
|
|
324
330
|
}
|
|
325
331
|
return config.information(input);
|
|
326
332
|
};
|
|
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
|
+
};
|
|
327
380
|
var validateMimeType = (mime, allowedTypes) => {
|
|
328
381
|
if (allowedTypes.includes("*/*")) return true;
|
|
329
382
|
return allowedTypes.some((pattern) => {
|
|
@@ -336,7 +389,7 @@ var validateMimeType = (mime, allowedTypes) => {
|
|
|
336
389
|
});
|
|
337
390
|
};
|
|
338
391
|
var computeFileHash = (filePath) => new Promise((resolve, reject) => {
|
|
339
|
-
const hash =
|
|
392
|
+
const hash = crypto3.createHash("sha256");
|
|
340
393
|
const stream = fs.createReadStream(filePath);
|
|
341
394
|
stream.on("data", (data) => hash.update(data));
|
|
342
395
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -487,6 +540,12 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size, fit, posi
|
|
|
487
540
|
...resolvedPosition && { position: resolvedPosition }
|
|
488
541
|
};
|
|
489
542
|
};
|
|
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
|
+
}
|
|
490
549
|
var generatePlaceholderThumbnail = async (outputPath, mimeType) => {
|
|
491
550
|
const typeParts = mimeType.split("/");
|
|
492
551
|
const subtype = typeParts[1] || "file";
|
|
@@ -713,7 +772,7 @@ StorageAccountSchema.method("toClient", async function() {
|
|
|
713
772
|
var StorageAccount = mongoose.models.StorageAccount || mongoose.model("StorageAccount", StorageAccountSchema);
|
|
714
773
|
var account_default = StorageAccount;
|
|
715
774
|
|
|
716
|
-
// src/server/
|
|
775
|
+
// src/server/storage-adapters/google.ts
|
|
717
776
|
var createAuthClient = async (owner, accountId) => {
|
|
718
777
|
const query = { owner, "metadata.provider": "GOOGLE" };
|
|
719
778
|
if (accountId) query._id = accountId;
|
|
@@ -1181,7 +1240,368 @@ var GoogleDriveProvider = {
|
|
|
1181
1240
|
}
|
|
1182
1241
|
};
|
|
1183
1242
|
|
|
1184
|
-
// src/server/
|
|
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
|
+
unauthenticated: z.coerce.boolean().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
|
+
});
|
|
1185
1605
|
var getNextOrderValue = async (owner) => {
|
|
1186
1606
|
const lastItem = await drive_default.findOne({ owner }, {}, { sort: { order: -1 } });
|
|
1187
1607
|
return lastItem ? lastItem.order + 1 : 0;
|
|
@@ -1200,7 +1620,7 @@ var driveGetUrl = (fileId, options) => {
|
|
|
1200
1620
|
} else {
|
|
1201
1621
|
expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
1202
1622
|
}
|
|
1203
|
-
const signature =
|
|
1623
|
+
const signature = crypto3.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
|
|
1204
1624
|
const token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
|
|
1205
1625
|
return `${config.apiUrl || "/api/drive"}?action=serve&id=${fileId}&token=${token}`;
|
|
1206
1626
|
};
|
|
@@ -1209,7 +1629,7 @@ var driveAddSignedUrlToken = (item, config) => {
|
|
|
1209
1629
|
if (config.security?.signedUrls?.enabled && config.security.signedUrls.secret) {
|
|
1210
1630
|
const { secret, expiresIn } = config.security.signedUrls;
|
|
1211
1631
|
const expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
1212
|
-
const signature =
|
|
1632
|
+
const signature = crypto3.createHmac("sha256", secret).update(`${item.id}:${expiryTimestamp}`).digest("hex");
|
|
1213
1633
|
token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
|
|
1214
1634
|
}
|
|
1215
1635
|
const apiUrl = config.apiUrl || "/api/drive";
|
|
@@ -1528,7 +1948,7 @@ var driveUpload = async (source, key, options) => {
|
|
|
1528
1948
|
if (!fs.existsSync(tempDir)) {
|
|
1529
1949
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
1530
1950
|
}
|
|
1531
|
-
tempFilePath = path.join(tempDir, `upload-${
|
|
1951
|
+
tempFilePath = path.join(tempDir, `upload-${crypto3.randomUUID()}.tmp`);
|
|
1532
1952
|
fs.writeFileSync(tempFilePath, source);
|
|
1533
1953
|
sourceFilePath = tempFilePath;
|
|
1534
1954
|
fileSize = source.length;
|
|
@@ -1537,7 +1957,7 @@ var driveUpload = async (source, key, options) => {
|
|
|
1537
1957
|
if (!fs.existsSync(tempDir)) {
|
|
1538
1958
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
1539
1959
|
}
|
|
1540
|
-
tempFilePath = path.join(tempDir, `upload-${
|
|
1960
|
+
tempFilePath = path.join(tempDir, `upload-${crypto3.randomUUID()}.tmp`);
|
|
1541
1961
|
const writeStream = fs.createWriteStream(tempFilePath);
|
|
1542
1962
|
await new Promise((resolve, reject) => {
|
|
1543
1963
|
source.pipe(writeStream);
|
|
@@ -1708,98 +2128,29 @@ var driveCleanup = async () => {
|
|
|
1708
2128
|
}
|
|
1709
2129
|
return { removed, totalFreedInBytes };
|
|
1710
2130
|
};
|
|
1711
|
-
var
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
var sanitizeFilename = (name) => {
|
|
1715
|
-
return name.replace(/[<>:"|?*\x00-\x1F]/g, "").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\.\.\//g, "").replace(/\.\.+/g, "").split("/").pop() || "".trim().slice(0, 255);
|
|
2131
|
+
var driveConfirm = async (id) => {
|
|
2132
|
+
const result = await drive_default.updateOne({ _id: id }, { $set: { expiresAt: null } });
|
|
2133
|
+
return result.matchedCount > 0;
|
|
1716
2134
|
};
|
|
1717
|
-
var
|
|
1718
|
-
|
|
2135
|
+
var drivePurgeExpired = async () => {
|
|
2136
|
+
const expired = await drive_default.find({ expiresAt: { $ne: null, $lt: /* @__PURE__ */ new Date() } });
|
|
2137
|
+
const removed = [];
|
|
2138
|
+
let totalFreedInBytes = 0;
|
|
2139
|
+
for (const drive of expired) {
|
|
2140
|
+
const id = String(drive._id);
|
|
2141
|
+
try {
|
|
2142
|
+
await driveDelete(drive);
|
|
2143
|
+
totalFreedInBytes += drive.information.type === "FILE" ? drive.information.sizeInBytes : 0;
|
|
2144
|
+
removed.push(id);
|
|
2145
|
+
} catch (e) {
|
|
2146
|
+
console.error(`[next-drive] Failed to purge expired file ${id}:`, e);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return { removed, totalFreedInBytes };
|
|
1719
2150
|
};
|
|
1720
|
-
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" });
|
|
1721
|
-
var uploadChunkSchema = z.object({
|
|
1722
|
-
chunkIndex: z.number().int().min(0).max(1e4),
|
|
1723
|
-
totalChunks: z.number().int().min(1).max(1e4),
|
|
1724
|
-
driveId: z.string().optional(),
|
|
1725
|
-
fileName: nameSchema,
|
|
1726
|
-
fileSize: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
1727
|
-
fileType: z.string().min(1).max(255),
|
|
1728
|
-
folderId: z.string().optional()
|
|
1729
|
-
}).refine((data) => data.chunkIndex < data.totalChunks, {
|
|
1730
|
-
message: "Chunk index must be less than total chunks"
|
|
1731
|
-
});
|
|
1732
|
-
var listQuerySchema = z.object({
|
|
1733
|
-
folderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]),
|
|
1734
|
-
limit: z.string().optional().transform((val) => {
|
|
1735
|
-
const num = parseInt(val || "50", 10);
|
|
1736
|
-
return Math.min(Math.max(1, num), 100);
|
|
1737
|
-
}),
|
|
1738
|
-
afterId: objectIdSchema.optional()
|
|
1739
|
-
});
|
|
1740
|
-
z.object({
|
|
1741
|
-
id: objectIdSchema,
|
|
1742
|
-
token: z.string().optional()
|
|
1743
|
-
});
|
|
1744
|
-
z.object({
|
|
1745
|
-
id: objectIdSchema,
|
|
1746
|
-
size: z.enum(["small", "medium", "large"]).optional().default("medium"),
|
|
1747
|
-
token: z.string().optional()
|
|
1748
|
-
});
|
|
1749
|
-
var renameBodySchema = z.object({
|
|
1750
|
-
id: objectIdSchema,
|
|
1751
|
-
newName: nameSchema
|
|
1752
|
-
});
|
|
1753
|
-
var deleteQuerySchema = z.object({
|
|
1754
|
-
id: objectIdSchema
|
|
1755
|
-
});
|
|
1756
|
-
z.object({
|
|
1757
|
-
ids: z.array(objectIdSchema).min(1).max(1e3)
|
|
1758
|
-
});
|
|
1759
|
-
var createFolderBodySchema = z.object({
|
|
1760
|
-
name: nameSchema,
|
|
1761
|
-
parentId: z.union([z.literal("root"), objectIdSchema, z.string().length(0), z.undefined()]).optional()
|
|
1762
|
-
});
|
|
1763
|
-
var moveBodySchema = z.object({
|
|
1764
|
-
ids: z.array(objectIdSchema).min(1).max(1e3),
|
|
1765
|
-
targetFolderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]).optional()
|
|
1766
|
-
});
|
|
1767
|
-
z.object({
|
|
1768
|
-
ids: z.array(objectIdSchema).min(1).max(1e3)
|
|
1769
|
-
});
|
|
1770
|
-
var searchQuerySchema = z.object({
|
|
1771
|
-
q: z.string().min(1).max(100).transform(sanitizeRegexInput),
|
|
1772
|
-
folderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]).optional(),
|
|
1773
|
-
limit: z.string().optional().transform((val) => {
|
|
1774
|
-
const num = parseInt(val || "50", 10);
|
|
1775
|
-
return Math.min(Math.max(1, num), 100);
|
|
1776
|
-
}),
|
|
1777
|
-
trashed: z.string().optional().transform((val) => val === "true")
|
|
1778
|
-
});
|
|
1779
|
-
z.object({
|
|
1780
|
-
id: objectIdSchema
|
|
1781
|
-
});
|
|
1782
|
-
var cancelQuerySchema = z.object({
|
|
1783
|
-
id: z.string().uuid()
|
|
1784
|
-
});
|
|
1785
|
-
z.object({
|
|
1786
|
-
days: z.number().int().min(1).max(365).optional()
|
|
1787
|
-
});
|
|
1788
|
-
var driveFileSchemaZod = z.object({
|
|
1789
|
-
id: z.string(),
|
|
1790
|
-
file: z.object({
|
|
1791
|
-
name: z.string(),
|
|
1792
|
-
mime: z.string(),
|
|
1793
|
-
size: z.number()
|
|
1794
|
-
})
|
|
1795
|
-
});
|
|
1796
2151
|
|
|
1797
|
-
// src/server/
|
|
1798
|
-
|
|
1799
|
-
const basename = filename.replace(/^.*[\\\/]/, "");
|
|
1800
|
-
return basename.replace(/["\r\n]/g, "").replace(/[^\x20-\x7E]/g, "").slice(0, 255);
|
|
1801
|
-
}
|
|
1802
|
-
var getProvider = async (req, owner) => {
|
|
2152
|
+
// src/server/actions/shared.ts
|
|
2153
|
+
var resolveProvider = async (req, owner) => {
|
|
1803
2154
|
const accountId = req.headers["x-drive-account"];
|
|
1804
2155
|
if (!accountId || accountId === "LOCAL") {
|
|
1805
2156
|
return { provider: LocalStorageProvider };
|
|
@@ -1808,732 +2159,568 @@ var getProvider = async (req, owner) => {
|
|
|
1808
2159
|
if (!account) {
|
|
1809
2160
|
throw new Error("Storage account not found or access denied");
|
|
1810
2161
|
}
|
|
1811
|
-
if (account.metadata.provider === "GOOGLE")
|
|
2162
|
+
if (account.metadata.provider === "GOOGLE") {
|
|
2163
|
+
return { provider: GoogleDriveProvider, accountId: account._id.toString() };
|
|
2164
|
+
}
|
|
1812
2165
|
return { provider: LocalStorageProvider };
|
|
1813
2166
|
};
|
|
1814
|
-
var
|
|
2167
|
+
var withSignedUrl = (item, config) => {
|
|
1815
2168
|
return driveAddSignedUrlToken(item, config);
|
|
1816
2169
|
};
|
|
1817
|
-
var
|
|
2170
|
+
var withSignedUrls = (items, config) => {
|
|
1818
2171
|
return driveAddSignedUrlTokens(items, config);
|
|
1819
2172
|
};
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
} else if (Array.isArray(allowedOrigins)) {
|
|
1835
|
-
if (allowedOrigins.includes(origin)) {
|
|
1836
|
-
allowOrigin = origin;
|
|
2173
|
+
|
|
2174
|
+
// src/server/actions/drive.ts
|
|
2175
|
+
var handleDriveAction = async (ctx) => {
|
|
2176
|
+
const { req, res, action, config, owner, isRootMode, 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);
|
|
1837
2187
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
res.status(403).end();
|
|
1847
|
-
return true;
|
|
1848
|
-
}
|
|
1849
|
-
return false;
|
|
1850
|
-
}
|
|
1851
|
-
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
1852
|
-
res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
|
|
1853
|
-
res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
|
|
1854
|
-
res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(", "));
|
|
1855
|
-
res.setHeader("Access-Control-Max-Age", maxAge.toString());
|
|
1856
|
-
if (credentials) {
|
|
1857
|
-
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
1858
|
-
}
|
|
1859
|
-
if (req.method === "OPTIONS") {
|
|
1860
|
-
res.status(204).end();
|
|
1861
|
-
return true;
|
|
1862
|
-
}
|
|
1863
|
-
return false;
|
|
1864
|
-
};
|
|
1865
|
-
var driveAPIHandler = async (req, res) => {
|
|
1866
|
-
const action = req.query.action || (req.query.code && req.query.state ? "callback" : void 0);
|
|
1867
|
-
let config;
|
|
1868
|
-
try {
|
|
1869
|
-
config = getDriveConfig();
|
|
1870
|
-
} catch (error) {
|
|
1871
|
-
console.error("[next-drive] Configuration error:", error);
|
|
1872
|
-
res.status(500).json({ status: 500, message: "Drive is not ready: failed to initialize configuration" });
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
1875
|
-
const isPreflightHandled = applyCorsHeaders(req, res, config);
|
|
1876
|
-
if (isPreflightHandled) return;
|
|
1877
|
-
if (!action) {
|
|
1878
|
-
res.status(400).json({ status: 400, message: 'Missing "action" parameter in request' });
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
|
-
if (action === "serve" || action === "thumbnail") {
|
|
1882
|
-
try {
|
|
1883
|
-
const { id, token } = req.query;
|
|
1884
|
-
if (!id || typeof id !== "string") {
|
|
1885
|
-
return res.status(400).json({ status: 400, message: "Could not open file: missing or invalid file ID" });
|
|
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;
|
|
1886
2196
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
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) {
|
|
1893
2208
|
try {
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
if (Date.now() / 1e3 > expiry) {
|
|
1898
|
-
return res.status(401).json({ status: 401, message: "Access denied: this link has expired" });
|
|
1899
|
-
}
|
|
1900
|
-
const { secret } = config.security.signedUrls;
|
|
1901
|
-
const expectedSignature = crypto2.createHmac("sha256", secret).update(`${id}:${expiry}`).digest("hex");
|
|
1902
|
-
if (signature !== expectedSignature) {
|
|
1903
|
-
return res.status(401).json({ status: 401, message: "Access denied: this link's access token is invalid" });
|
|
1904
|
-
}
|
|
1905
|
-
} catch (err) {
|
|
1906
|
-
return res.status(401).json({ status: 401, message: "Access denied: this link's access token is malformed" });
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
1910
|
-
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
1911
|
-
if (action === "thumbnail") {
|
|
1912
|
-
const stream = await itemProvider.getThumbnail(drive, itemAccountId);
|
|
1913
|
-
res.setHeader("Content-Type", "image/webp");
|
|
1914
|
-
if (config.cors?.enabled) {
|
|
1915
|
-
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
2209
|
+
await provider.search(q, owner, accountId);
|
|
2210
|
+
} catch (e) {
|
|
2211
|
+
console.error("Search sync failed", e);
|
|
1916
2212
|
}
|
|
1917
|
-
stream.pipe(res);
|
|
1918
|
-
return;
|
|
1919
2213
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
const position = req.query.position;
|
|
1929
|
-
const isImage = mime.startsWith("image/");
|
|
1930
|
-
const shouldTransform = isImage && (format || quality || display || sizePreset || fit);
|
|
1931
|
-
res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
|
|
1932
|
-
if (config.cors?.enabled) {
|
|
1933
|
-
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1934
|
-
}
|
|
1935
|
-
if (shouldTransform) {
|
|
1936
|
-
try {
|
|
1937
|
-
const settings = getImageSettings(fileSize, quality, display, sizePreset, fit, position);
|
|
1938
|
-
let targetFormat = format || mime.split("/")[1];
|
|
1939
|
-
if (targetFormat === "jpg") targetFormat = "jpeg";
|
|
1940
|
-
if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
|
|
1941
|
-
targetFormat = format || "webp";
|
|
1942
|
-
}
|
|
1943
|
-
const cacheDir = path.join(config.storage.path, "file", drive._id.toString(), "cache");
|
|
1944
|
-
const cacheKey = [
|
|
1945
|
-
"opt",
|
|
1946
|
-
`q${settings.quality}`,
|
|
1947
|
-
`e${settings.effort}`,
|
|
1948
|
-
settings.width ? `${settings.width}x${settings.height}` : "orig",
|
|
1949
|
-
settings.fit || "none",
|
|
1950
|
-
settings.position || "c",
|
|
1951
|
-
targetFormat
|
|
1952
|
-
].join("_");
|
|
1953
|
-
const cachePath = path.join(cacheDir, `${cacheKey}.bin`);
|
|
1954
|
-
if (fs.existsSync(cachePath)) {
|
|
1955
|
-
const cacheStat = fs.statSync(cachePath);
|
|
1956
|
-
res.setHeader("Content-Type", `image/${targetFormat}`);
|
|
1957
|
-
res.setHeader("Content-Length", cacheStat.size);
|
|
1958
|
-
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1959
|
-
if (config.cors?.enabled) {
|
|
1960
|
-
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1961
|
-
}
|
|
1962
|
-
if ("destroy" in stream) stream.destroy();
|
|
1963
|
-
fs.createReadStream(cachePath).pipe(res);
|
|
1964
|
-
return;
|
|
1965
|
-
}
|
|
1966
|
-
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
1967
|
-
let pipeline = sharp2();
|
|
1968
|
-
if (settings.width && settings.height) {
|
|
1969
|
-
pipeline = pipeline.resize(settings.width, settings.height, {
|
|
1970
|
-
fit: settings.fit || "inside",
|
|
1971
|
-
position: settings.position || "center",
|
|
1972
|
-
withoutEnlargement: true,
|
|
1973
|
-
// Use transparent background for 'contain' fit to preserve transparency
|
|
1974
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
1975
|
-
});
|
|
1976
|
-
}
|
|
1977
|
-
if (targetFormat === "jpeg") {
|
|
1978
|
-
pipeline = pipeline.jpeg({ quality: settings.quality, mozjpeg: true });
|
|
1979
|
-
res.setHeader("Content-Type", "image/jpeg");
|
|
1980
|
-
} else if (targetFormat === "png") {
|
|
1981
|
-
pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
|
|
1982
|
-
res.setHeader("Content-Type", "image/png");
|
|
1983
|
-
} else if (targetFormat === "webp") {
|
|
1984
|
-
const webpEffort = Math.min(settings.effort, 6);
|
|
1985
|
-
pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
|
|
1986
|
-
res.setHeader("Content-Type", "image/webp");
|
|
1987
|
-
} else if (targetFormat === "avif") {
|
|
1988
|
-
pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
|
|
1989
|
-
res.setHeader("Content-Type", "image/avif");
|
|
1990
|
-
}
|
|
1991
|
-
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1992
|
-
pipeline.on("error", (err) => {
|
|
1993
|
-
console.error("[next-drive] Pipeline error:", err);
|
|
1994
|
-
});
|
|
1995
|
-
stream.pipe(pipeline);
|
|
1996
|
-
pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
|
|
1997
|
-
pipeline.clone().pipe(res);
|
|
1998
|
-
return;
|
|
1999
|
-
} catch (e) {
|
|
2000
|
-
console.error("[next-drive] Image transformation failed:", e);
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
res.setHeader("Content-Type", mime);
|
|
2004
|
-
if (fileSize) res.setHeader("Content-Length", fileSize);
|
|
2005
|
-
stream.pipe(res);
|
|
2006
|
-
return;
|
|
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;
|
|
2007
2222
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
|
2013
2238
|
});
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
const { key: owner } = information;
|
|
2020
|
-
const STORAGE_PATH = config.storage.path;
|
|
2021
|
-
const isRootMode = mode === "ROOT";
|
|
2022
|
-
if (action === "information") {
|
|
2023
|
-
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
2024
|
-
const googleConfigured = !!(clientId && clientSecret && redirectUri);
|
|
2025
|
-
return res.status(200).json({
|
|
2026
|
-
status: 200,
|
|
2027
|
-
message: "Information retrieved",
|
|
2028
|
-
data: {
|
|
2029
|
-
providers: {
|
|
2030
|
-
google: googleConfigured
|
|
2031
|
-
},
|
|
2032
|
-
mode
|
|
2033
|
-
}
|
|
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
|
+
});
|
|
2034
2244
|
});
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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
|
+
});
|
|
2261
|
+
if (!uploadData.success) {
|
|
2262
|
+
cleanupTempFiles(files);
|
|
2263
|
+
return void res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
|
|
2264
|
+
}
|
|
2265
|
+
const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId, unauthenticated } = uploadData.data;
|
|
2266
|
+
let currentUploadId = driveId;
|
|
2267
|
+
const tempBaseDir = path.join(os2.tmpdir(), "next-drive-uploads");
|
|
2268
|
+
if (!currentUploadId) {
|
|
2269
|
+
if (chunkIndex !== 0) return void res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
|
|
2270
|
+
if (unauthenticated) {
|
|
2271
|
+
const unauth = config.security?.unauthenticated;
|
|
2272
|
+
if (!unauth?.enabled) {
|
|
2273
|
+
cleanupTempFiles(files);
|
|
2274
|
+
return void res.status(403).json({ status: 403, message: "Anonymous uploads are not enabled" });
|
|
2055
2275
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
const { code, state } = req.query;
|
|
2060
|
-
if (!code) return res.status(400).json({ status: 400, message: "Google sign-in failed: authorization code missing" });
|
|
2061
|
-
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
2062
|
-
if (!clientId || !clientSecret || !redirectUri) return res.status(500).json({ status: 500, message: "Google Drive sign-in is not configured on the server" });
|
|
2063
|
-
const callbackUri = new URL(redirectUri);
|
|
2064
|
-
callbackUri.searchParams.set("action", "callback");
|
|
2065
|
-
const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
|
|
2066
|
-
const { tokens } = await oAuth2Client.getToken(code);
|
|
2067
|
-
oAuth2Client.setCredentials(tokens);
|
|
2068
|
-
const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client });
|
|
2069
|
-
const userInfo = await oauth2.userinfo.get();
|
|
2070
|
-
const existing = await account_default.findOne({ owner, "metadata.google.email": userInfo.data.email, "metadata.provider": "GOOGLE" });
|
|
2071
|
-
if (existing) {
|
|
2072
|
-
existing.metadata.google.credentials = tokens;
|
|
2073
|
-
existing.markModified("metadata");
|
|
2074
|
-
await existing.save();
|
|
2075
|
-
} else {
|
|
2076
|
-
const newAccount = new account_default({
|
|
2077
|
-
owner,
|
|
2078
|
-
name: userInfo.data.name || "Google Drive",
|
|
2079
|
-
metadata: {
|
|
2080
|
-
provider: "GOOGLE",
|
|
2081
|
-
google: {
|
|
2082
|
-
email: userInfo.data.email,
|
|
2083
|
-
credentials: tokens
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
});
|
|
2087
|
-
await newAccount.save();
|
|
2276
|
+
if (fileSizeInBytes > unauth.maxUploadSizeInBytes) {
|
|
2277
|
+
cleanupTempFiles(files);
|
|
2278
|
+
return void res.status(413).json({ status: 413, message: "Could not upload: file exceeds the maximum allowed size" });
|
|
2088
2279
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
(
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
// Method 2: localStorage event for new tabs (macOS fullscreen mode)
|
|
2104
|
-
try {
|
|
2105
|
-
localStorage.setItem('next-drive-oauth-success', Date.now().toString());
|
|
2106
|
-
localStorage.removeItem('next-drive-oauth-success');
|
|
2107
|
-
} catch (e) {}
|
|
2108
|
-
// Close the window/tab
|
|
2109
|
-
window.close();
|
|
2110
|
-
// Fallback: If window.close() doesn't work (some browsers block it),
|
|
2111
|
-
// show a message to manually close
|
|
2112
|
-
setTimeout(function() {
|
|
2113
|
-
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>';
|
|
2114
|
-
}, 500);
|
|
2115
|
-
})();
|
|
2116
|
-
</script>
|
|
2117
|
-
</body>
|
|
2118
|
-
</html>`);
|
|
2119
|
-
}
|
|
2120
|
-
case "listAccounts": {
|
|
2121
|
-
const accounts = await account_default.find({ owner });
|
|
2122
|
-
return res.status(200).json({
|
|
2123
|
-
status: 200,
|
|
2124
|
-
data: {
|
|
2125
|
-
accounts: accounts.map((a) => ({
|
|
2126
|
-
id: a._id.toString(),
|
|
2127
|
-
name: a.name,
|
|
2128
|
-
email: a.metadata.google?.email || "",
|
|
2129
|
-
provider: a.metadata.provider
|
|
2130
|
-
}))
|
|
2280
|
+
if (fileType && !validateMimeType(fileType, unauth.allowedMimeTypes)) {
|
|
2281
|
+
cleanupTempFiles(files);
|
|
2282
|
+
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2283
|
+
}
|
|
2284
|
+
const abuse = unauth.abuse;
|
|
2285
|
+
if (abuse) {
|
|
2286
|
+
const store = globalThis.__nextDrive.abuse;
|
|
2287
|
+
const now = Date.now();
|
|
2288
|
+
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";
|
|
2289
|
+
const hits = (store.ipHits.get(ip) ?? []).filter((t) => now - t < 36e5);
|
|
2290
|
+
const perIp = abuse.perIp;
|
|
2291
|
+
if (perIp && hits.filter((t) => now - t < perIp.windowMinutes * 6e4).length >= perIp.max) {
|
|
2292
|
+
cleanupTempFiles(files);
|
|
2293
|
+
return void res.status(429).json({ status: 429, message: "Too many uploads, please try again later" });
|
|
2131
2294
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
const { id } = req.query;
|
|
2136
|
-
const account = await account_default.findOne({ _id: id, owner });
|
|
2137
|
-
if (!account) return res.status(404).json({ status: 404, message: "Could not disconnect: account not found" });
|
|
2138
|
-
if (account.metadata.provider === "GOOGLE") {
|
|
2139
|
-
try {
|
|
2140
|
-
await GoogleDriveProvider.revokeToken(owner, account._id.toString());
|
|
2141
|
-
} catch (e) {
|
|
2142
|
-
console.error("Failed to revoke Google token:", e);
|
|
2295
|
+
if (abuse.hourlyPerIp && hits.length >= abuse.hourlyPerIp) {
|
|
2296
|
+
cleanupTempFiles(files);
|
|
2297
|
+
return void res.status(429).json({ status: 429, message: "Hourly upload limit reached, please try again later" });
|
|
2143
2298
|
}
|
|
2299
|
+
if (abuse.maxConcurrent && store.concurrent >= abuse.maxConcurrent) {
|
|
2300
|
+
cleanupTempFiles(files);
|
|
2301
|
+
return void res.status(429).json({ status: 429, message: "Server is busy with uploads, please try again later" });
|
|
2302
|
+
}
|
|
2303
|
+
if (abuse.maxLiveBytes) {
|
|
2304
|
+
const [agg] = await drive_default.aggregate([{ $match: { expiresAt: { $ne: null } } }, { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }]);
|
|
2305
|
+
if ((agg?.total ?? 0) + fileSizeInBytes > abuse.maxLiveBytes) {
|
|
2306
|
+
cleanupTempFiles(files);
|
|
2307
|
+
return void res.status(429).json({ status: 429, message: "Temporary storage is full, please try again later" });
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
hits.push(now);
|
|
2311
|
+
store.ipHits.set(ip, hits);
|
|
2312
|
+
store.concurrent++;
|
|
2144
2313
|
}
|
|
2145
|
-
|
|
2146
|
-
await drive_default.deleteMany({ owner, storageAccountId: id });
|
|
2147
|
-
return res.status(200).json({ status: 200, message: "Account removed" });
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
const { provider, accountId } = await getProvider(req, owner);
|
|
2152
|
-
switch (action) {
|
|
2153
|
-
// ** 1. LIST **
|
|
2154
|
-
case "list": {
|
|
2155
|
-
if (req.method !== "GET") return res.status(405).json({ status: 405, message: "Listing files requires a GET request" });
|
|
2156
|
-
const listQuery = listQuerySchema.safeParse(req.query);
|
|
2157
|
-
if (!listQuery.success) return res.status(400).json({ status: 400, message: "Could not list files: invalid request parameters" });
|
|
2158
|
-
const { folderId, limit, afterId } = listQuery.data;
|
|
2159
|
-
try {
|
|
2160
|
-
await provider.sync(folderId || "root", owner, accountId);
|
|
2161
|
-
} catch (e) {
|
|
2162
|
-
console.error("Sync failed", e);
|
|
2163
|
-
}
|
|
2164
|
-
const query = {
|
|
2165
|
-
"provider.type": provider.name,
|
|
2166
|
-
storageAccountId: accountId || null,
|
|
2167
|
-
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
2168
|
-
trashedAt: null
|
|
2169
|
-
};
|
|
2170
|
-
if (!isRootMode) {
|
|
2171
|
-
query.owner = owner;
|
|
2172
|
-
}
|
|
2173
|
-
if (afterId) query._id = { $lt: afterId };
|
|
2174
|
-
const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
|
|
2175
|
-
const plainItems = addSignedUrlTokens(await Promise.all(items.map((item) => item.toClient())), config);
|
|
2176
|
-
res.status(200).json({ status: 200, message: "Items retrieved", data: { items: plainItems, hasMore: items.length === limit } });
|
|
2177
|
-
return;
|
|
2178
|
-
}
|
|
2179
|
-
// ** 2. SEARCH **
|
|
2180
|
-
case "search": {
|
|
2181
|
-
const searchData = searchQuerySchema.safeParse(req.query);
|
|
2182
|
-
if (!searchData.success) return res.status(400).json({ status: 400, message: "Could not search: invalid request parameters" });
|
|
2183
|
-
const { q, folderId, limit, trashed } = searchData.data;
|
|
2184
|
-
if (!trashed) {
|
|
2185
|
-
try {
|
|
2186
|
-
await provider.search(q, owner, accountId);
|
|
2187
|
-
} catch (e) {
|
|
2188
|
-
console.error("Search sync failed", e);
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
const query = {
|
|
2192
|
-
"provider.type": provider.name,
|
|
2193
|
-
storageAccountId: accountId || null,
|
|
2194
|
-
trashedAt: trashed ? { $ne: null } : null,
|
|
2195
|
-
name: { $regex: q, $options: "i" }
|
|
2196
|
-
};
|
|
2197
|
-
if (!isRootMode) {
|
|
2198
|
-
query.owner = owner;
|
|
2199
|
-
}
|
|
2200
|
-
if (folderId && folderId !== "root") query.parentId = folderId;
|
|
2201
|
-
const items = await drive_default.find(query, {}, { limit, sort: { createdAt: -1 } });
|
|
2202
|
-
const plainItems = addSignedUrlTokens(await Promise.all(items.map((i) => i.toClient())), config);
|
|
2203
|
-
return res.status(200).json({ status: 200, message: "Results", data: { items: plainItems } });
|
|
2204
|
-
}
|
|
2205
|
-
// ** 3. UPLOAD **
|
|
2206
|
-
case "upload": {
|
|
2207
|
-
if (req.method !== "POST") return res.status(405).json({ status: 405, message: "Uploading requires a POST request" });
|
|
2208
|
-
const systemTmpDir = path.join(os2.tmpdir(), "next-drive-uploads");
|
|
2209
|
-
if (!fs.existsSync(systemTmpDir)) fs.mkdirSync(systemTmpDir, { recursive: true });
|
|
2210
|
-
const form = formidable({
|
|
2211
|
-
multiples: false,
|
|
2212
|
-
maxFileSize: (config.security?.maxUploadSizeInBytes ?? 1024 * 1024 * 1024) * 2,
|
|
2213
|
-
uploadDir: systemTmpDir,
|
|
2214
|
-
keepExtensions: true
|
|
2215
|
-
});
|
|
2216
|
-
const [fields, files] = await new Promise((resolve, reject) => {
|
|
2217
|
-
form.parse(req, (err, fields2, files2) => {
|
|
2218
|
-
if (err) reject(err);
|
|
2219
|
-
else resolve([fields2, files2]);
|
|
2220
|
-
});
|
|
2221
|
-
});
|
|
2222
|
-
const cleanupTempFiles = (files2) => {
|
|
2223
|
-
Object.values(files2).flat().forEach((file) => {
|
|
2224
|
-
if (file && fs.existsSync(file.filepath)) fs.rmSync(file.filepath, { force: true });
|
|
2225
|
-
});
|
|
2226
|
-
};
|
|
2227
|
-
const getString = (f) => Array.isArray(f) ? f[0] : f || "";
|
|
2228
|
-
const getInt = (f) => parseInt(getString(f) || "0", 10);
|
|
2229
|
-
const uploadData = uploadChunkSchema.safeParse({
|
|
2230
|
-
chunkIndex: getInt(fields.chunkIndex),
|
|
2231
|
-
totalChunks: getInt(fields.totalChunks),
|
|
2232
|
-
driveId: getString(fields.driveId) || void 0,
|
|
2233
|
-
fileName: getString(fields.fileName),
|
|
2234
|
-
fileSize: getInt(fields.fileSize),
|
|
2235
|
-
fileType: getString(fields.fileType),
|
|
2236
|
-
folderId: getString(fields.folderId) || void 0
|
|
2237
|
-
});
|
|
2238
|
-
if (!uploadData.success) {
|
|
2239
|
-
cleanupTempFiles(files);
|
|
2240
|
-
return res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
|
|
2241
|
-
}
|
|
2242
|
-
const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId } = uploadData.data;
|
|
2243
|
-
let currentUploadId = driveId;
|
|
2244
|
-
const tempBaseDir = path.join(os2.tmpdir(), "next-drive-uploads");
|
|
2245
|
-
if (!currentUploadId) {
|
|
2246
|
-
if (chunkIndex !== 0) return res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
|
|
2314
|
+
} else {
|
|
2247
2315
|
if (fileType && config.security) {
|
|
2248
2316
|
if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
|
|
2249
2317
|
cleanupTempFiles(files);
|
|
2250
|
-
return res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2318
|
+
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2251
2319
|
}
|
|
2252
2320
|
}
|
|
2253
2321
|
if (!isRootMode) {
|
|
2254
2322
|
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2255
2323
|
if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
|
|
2256
2324
|
cleanupTempFiles(files);
|
|
2257
|
-
return res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
|
|
2325
|
+
return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
|
|
2258
2326
|
}
|
|
2259
2327
|
}
|
|
2260
|
-
currentUploadId = crypto2.randomUUID();
|
|
2261
|
-
const uploadDir = path.join(tempBaseDir, currentUploadId);
|
|
2262
|
-
fs.mkdirSync(uploadDir, { recursive: true });
|
|
2263
|
-
const metadata = {
|
|
2264
|
-
owner,
|
|
2265
|
-
accountId,
|
|
2266
|
-
providerName: provider.name,
|
|
2267
|
-
name: fileName,
|
|
2268
|
-
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
2269
|
-
fileSize: fileSizeInBytes,
|
|
2270
|
-
mimeType: fileType,
|
|
2271
|
-
totalChunks
|
|
2272
|
-
};
|
|
2273
|
-
fs.writeFileSync(path.join(uploadDir, "metadata.json"), JSON.stringify(metadata));
|
|
2274
2328
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2329
|
+
currentUploadId = crypto3.randomUUID();
|
|
2330
|
+
const uploadDir2 = path.join(tempBaseDir, currentUploadId);
|
|
2331
|
+
fs.mkdirSync(uploadDir2, { recursive: true });
|
|
2332
|
+
const metadata = {
|
|
2333
|
+
owner: unauthenticated ? null : owner,
|
|
2334
|
+
accountId: unauthenticated ? null : accountId,
|
|
2335
|
+
providerName: provider.name,
|
|
2336
|
+
name: fileName,
|
|
2337
|
+
parentId: unauthenticated || folderId === "root" || !folderId ? null : folderId,
|
|
2338
|
+
fileSize: fileSizeInBytes,
|
|
2339
|
+
mimeType: fileType,
|
|
2340
|
+
totalChunks,
|
|
2341
|
+
unauthenticated: !!unauthenticated
|
|
2342
|
+
};
|
|
2343
|
+
fs.writeFileSync(path.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
|
|
2344
|
+
}
|
|
2345
|
+
if (!currentUploadId) {
|
|
2346
|
+
cleanupTempFiles(files);
|
|
2347
|
+
return void res.status(400).json({ status: 400, message: "Could not upload: invalid upload request" });
|
|
2348
|
+
}
|
|
2349
|
+
const uploadDir = path.join(tempBaseDir, currentUploadId);
|
|
2350
|
+
if (!fs.existsSync(uploadDir)) {
|
|
2351
|
+
cleanupTempFiles(files);
|
|
2352
|
+
return void res.status(404).json({ status: 404, message: "Could not upload: this upload session was not found or has expired" });
|
|
2353
|
+
}
|
|
2354
|
+
try {
|
|
2355
|
+
const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
|
|
2356
|
+
if (!chunkFile) throw new Error("Could not upload: no file chunk was received");
|
|
2357
|
+
const partPath = path.join(uploadDir, `part_${chunkIndex}`);
|
|
2358
|
+
try {
|
|
2359
|
+
fs.renameSync(chunkFile.filepath, partPath);
|
|
2360
|
+
} catch (err) {
|
|
2361
|
+
if (err instanceof Error && "code" in err && err.code === "EXDEV") {
|
|
2362
|
+
fs.copyFileSync(chunkFile.filepath, partPath);
|
|
2363
|
+
fs.unlinkSync(chunkFile.filepath);
|
|
2364
|
+
} else {
|
|
2365
|
+
throw err;
|
|
2280
2366
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2367
|
+
}
|
|
2368
|
+
const uploadedParts = fs.readdirSync(uploadDir).filter((f) => f.startsWith("part_"));
|
|
2369
|
+
if (uploadedParts.length === totalChunks) {
|
|
2370
|
+
const metaPath = path.join(uploadDir, "metadata.json");
|
|
2371
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
2372
|
+
const finalTempPath = path.join(uploadDir, "final.bin");
|
|
2373
|
+
const writeStream = fs.createWriteStream(finalTempPath);
|
|
2374
|
+
let streamError = null;
|
|
2375
|
+
writeStream.on("error", (err) => {
|
|
2376
|
+
streamError = err;
|
|
2377
|
+
});
|
|
2378
|
+
await new Promise((resolve, reject) => {
|
|
2379
|
+
writeStream.on("open", () => resolve());
|
|
2380
|
+
writeStream.once("error", reject);
|
|
2381
|
+
});
|
|
2382
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
2383
|
+
if (streamError) {
|
|
2384
|
+
writeStream.destroy();
|
|
2385
|
+
throw streamError;
|
|
2294
2386
|
}
|
|
2295
|
-
const
|
|
2296
|
-
if (
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
streamError = err;
|
|
2304
|
-
});
|
|
2305
|
-
await new Promise((resolve, reject) => {
|
|
2306
|
-
writeStream.on("open", () => resolve());
|
|
2307
|
-
writeStream.once("error", reject);
|
|
2308
|
-
});
|
|
2309
|
-
for (let i = 0; i < totalChunks; i++) {
|
|
2310
|
-
if (streamError) {
|
|
2311
|
-
writeStream.destroy();
|
|
2312
|
-
throw streamError;
|
|
2313
|
-
}
|
|
2314
|
-
const pPath = path.join(uploadDir, `part_${i}`);
|
|
2315
|
-
if (!fs.existsSync(pPath)) {
|
|
2316
|
-
writeStream.destroy();
|
|
2317
|
-
throw new Error(`Could not finish upload: chunk ${i} is missing`);
|
|
2318
|
-
}
|
|
2319
|
-
const data = fs.readFileSync(pPath);
|
|
2320
|
-
const canContinue = writeStream.write(data);
|
|
2321
|
-
if (!canContinue) {
|
|
2322
|
-
await new Promise((resolve, reject) => {
|
|
2323
|
-
writeStream.once("drain", resolve);
|
|
2324
|
-
writeStream.once("error", reject);
|
|
2325
|
-
});
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2387
|
+
const pPath = path.join(uploadDir, `part_${i}`);
|
|
2388
|
+
if (!fs.existsSync(pPath)) {
|
|
2389
|
+
writeStream.destroy();
|
|
2390
|
+
throw new Error(`Could not finish upload: chunk ${i} is missing`);
|
|
2391
|
+
}
|
|
2392
|
+
const data = fs.readFileSync(pPath);
|
|
2393
|
+
const canContinue = writeStream.write(data);
|
|
2394
|
+
if (!canContinue) {
|
|
2328
2395
|
await new Promise((resolve, reject) => {
|
|
2329
|
-
|
|
2330
|
-
reject(streamError);
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
writeStream.end();
|
|
2334
|
-
writeStream.on("finish", resolve);
|
|
2396
|
+
writeStream.once("drain", resolve);
|
|
2335
2397
|
writeStream.once("error", reject);
|
|
2336
2398
|
});
|
|
2337
|
-
if (!fs.existsSync(finalTempPath)) {
|
|
2338
|
-
throw new Error("Could not finish upload: failed to assemble the file");
|
|
2339
|
-
}
|
|
2340
|
-
const finalStats = fs.statSync(finalTempPath);
|
|
2341
|
-
if (finalStats.size !== meta.fileSize) {
|
|
2342
|
-
throw new Error("Could not finish upload: the assembled file is incomplete (size mismatch)");
|
|
2343
|
-
}
|
|
2344
|
-
const drive = new drive_default({
|
|
2345
|
-
owner: meta.owner,
|
|
2346
|
-
storageAccountId: meta.accountId || null,
|
|
2347
|
-
provider: { type: meta.providerName },
|
|
2348
|
-
name: meta.name,
|
|
2349
|
-
parentId: meta.parentId,
|
|
2350
|
-
order: 0,
|
|
2351
|
-
information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
|
|
2352
|
-
// path set by provider
|
|
2353
|
-
status: "UPLOADING",
|
|
2354
|
-
currentChunk: totalChunks,
|
|
2355
|
-
totalChunks
|
|
2356
|
-
});
|
|
2357
|
-
if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
|
|
2358
|
-
drive.information.path = path.join("file", String(drive._id), "data.bin");
|
|
2359
|
-
}
|
|
2360
|
-
await drive.save();
|
|
2361
|
-
try {
|
|
2362
|
-
const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
|
|
2363
|
-
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
2364
|
-
const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
|
|
2365
|
-
res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item: addSignedUrlToken(item, config) }, statistic: { storage: newQuota } });
|
|
2366
|
-
} catch (err) {
|
|
2367
|
-
await drive_default.deleteOne({ _id: drive._id });
|
|
2368
|
-
throw err;
|
|
2369
|
-
}
|
|
2370
|
-
} else {
|
|
2371
|
-
const newQuota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2372
|
-
if (chunkIndex === 0) {
|
|
2373
|
-
res.status(200).json({ status: 200, message: "Upload started", data: { type: "UPLOAD_STARTED", driveId: currentUploadId }, statistic: { storage: newQuota } });
|
|
2374
|
-
} else {
|
|
2375
|
-
res.status(200).json({ status: 200, message: "Chunk received", data: { type: "CHUNK_RECEIVED", driveId: currentUploadId, chunkIndex }, statistic: { storage: newQuota } });
|
|
2376
|
-
}
|
|
2377
2399
|
}
|
|
2378
|
-
} catch (e) {
|
|
2379
|
-
cleanupTempFiles(files);
|
|
2380
|
-
throw e;
|
|
2381
2400
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2401
|
+
await new Promise((resolve, reject) => {
|
|
2402
|
+
if (streamError) {
|
|
2403
|
+
reject(streamError);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
writeStream.end();
|
|
2407
|
+
writeStream.on("finish", resolve);
|
|
2408
|
+
writeStream.once("error", reject);
|
|
2409
|
+
});
|
|
2410
|
+
if (!fs.existsSync(finalTempPath)) {
|
|
2411
|
+
throw new Error("Could not finish upload: failed to assemble the file");
|
|
2412
|
+
}
|
|
2413
|
+
const finalStats = fs.statSync(finalTempPath);
|
|
2414
|
+
if (finalStats.size !== meta.fileSize) {
|
|
2415
|
+
throw new Error("Could not finish upload: the assembled file is incomplete (size mismatch)");
|
|
2416
|
+
}
|
|
2417
|
+
const drive = new drive_default({
|
|
2418
|
+
owner: meta.owner,
|
|
2419
|
+
storageAccountId: meta.accountId || null,
|
|
2420
|
+
provider: { type: meta.providerName },
|
|
2421
|
+
name: meta.name,
|
|
2422
|
+
parentId: meta.parentId,
|
|
2423
|
+
order: 0,
|
|
2424
|
+
information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
|
|
2425
|
+
status: "UPLOADING",
|
|
2426
|
+
currentChunk: totalChunks,
|
|
2427
|
+
totalChunks,
|
|
2428
|
+
expiresAt: meta.unauthenticated ? new Date(Date.now() + (config.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4) : null
|
|
2429
|
+
});
|
|
2430
|
+
if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
|
|
2431
|
+
drive.information.path = path.join("file", String(drive._id), "data.bin");
|
|
2432
|
+
}
|
|
2433
|
+
await drive.save();
|
|
2394
2434
|
try {
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2435
|
+
const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
|
|
2436
|
+
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
2437
|
+
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2438
|
+
const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
|
|
2439
|
+
res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item: withSignedUrl(item, config) }, statistic: { storage: newQuota } });
|
|
2440
|
+
} catch (err) {
|
|
2441
|
+
await drive_default.deleteOne({ _id: drive._id });
|
|
2442
|
+
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2443
|
+
throw err;
|
|
2444
|
+
}
|
|
2445
|
+
} else {
|
|
2446
|
+
const newQuota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2447
|
+
if (chunkIndex === 0) {
|
|
2448
|
+
res.status(200).json({ status: 200, message: "Upload started", data: { type: "UPLOAD_STARTED", driveId: currentUploadId }, statistic: { storage: newQuota } });
|
|
2449
|
+
} else {
|
|
2450
|
+
res.status(200).json({ status: 200, message: "Chunk received", data: { type: "CHUNK_RECEIVED", driveId: currentUploadId, chunkIndex }, statistic: { storage: newQuota } });
|
|
2398
2451
|
}
|
|
2399
2452
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
case "createFolder": {
|
|
2404
|
-
const folderData = createFolderBodySchema.safeParse(req.body);
|
|
2405
|
-
if (!folderData.success) return res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
|
|
2406
|
-
const { name, parentId } = folderData.data;
|
|
2407
|
-
const item = addSignedUrlToken(await provider.createFolder(name, parentId ?? null, owner, accountId), config);
|
|
2408
|
-
return res.status(201).json({ status: 201, message: "Folder created", data: { item } });
|
|
2453
|
+
} catch (e) {
|
|
2454
|
+
cleanupTempFiles(files);
|
|
2455
|
+
throw e;
|
|
2409
2456
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
case "cancel": {
|
|
2460
|
+
const cancelData = cancelQuerySchema.safeParse(req.query);
|
|
2461
|
+
if (!cancelData.success) return void res.status(400).json({ status: 400, message: "Could not cancel upload: invalid ID" });
|
|
2462
|
+
const { id } = cancelData.data;
|
|
2463
|
+
const tempUploadDir = path.join(os2.tmpdir(), "next-drive-uploads", id);
|
|
2464
|
+
if (fs.existsSync(tempUploadDir)) {
|
|
2419
2465
|
try {
|
|
2420
|
-
|
|
2466
|
+
const metaPath = path.join(tempUploadDir, "metadata.json");
|
|
2467
|
+
if (fs.existsSync(metaPath) && JSON.parse(fs.readFileSync(metaPath, "utf-8")).unauthenticated) {
|
|
2468
|
+
globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2469
|
+
}
|
|
2470
|
+
fs.rmSync(tempUploadDir, { recursive: true, force: true });
|
|
2421
2471
|
} catch (e) {
|
|
2422
|
-
console.error("
|
|
2472
|
+
console.error("Failed to cleanup temp upload:", e);
|
|
2423
2473
|
}
|
|
2424
|
-
drive.trashedAt = /* @__PURE__ */ new Date();
|
|
2425
|
-
await drive.save();
|
|
2426
|
-
return res.status(200).json({ status: 200, message: "Moved to trash", data: null });
|
|
2427
2474
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2475
|
+
res.status(200).json({ status: 200, message: "Upload cancelled", data: null });
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
case "createFolder": {
|
|
2479
|
+
const folderData = createFolderBodySchema.safeParse(req.body);
|
|
2480
|
+
if (!folderData.success) return void res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
|
|
2481
|
+
const { name, parentId } = folderData.data;
|
|
2482
|
+
const item = withSignedUrl(await provider.createFolder(name, parentId ?? null, owner, accountId), config);
|
|
2483
|
+
res.status(201).json({ status: 201, message: "Folder created", data: { item } });
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
case "delete": {
|
|
2487
|
+
const deleteData = deleteQuerySchema.safeParse(req.query);
|
|
2488
|
+
if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not move to trash: invalid ID" });
|
|
2489
|
+
const { id } = deleteData.data;
|
|
2490
|
+
const drive = await drive_default.findById(id);
|
|
2491
|
+
if (!drive) return void res.status(404).json({ status: 404, message: "Could not move to trash: item not found" });
|
|
2492
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
2493
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
2494
|
+
try {
|
|
2495
|
+
await itemProvider.trash([id], owner, itemAccountId);
|
|
2496
|
+
} catch (e) {
|
|
2497
|
+
console.error("Provider trash failed:", e);
|
|
2436
2498
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2499
|
+
drive.trashedAt = /* @__PURE__ */ new Date();
|
|
2500
|
+
await drive.save();
|
|
2501
|
+
res.status(200).json({ status: 200, message: "Moved to trash", data: null });
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
case "deletePermanent": {
|
|
2505
|
+
const deleteData = deleteQuerySchema.safeParse(req.query);
|
|
2506
|
+
if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not delete: invalid ID" });
|
|
2507
|
+
const { id } = deleteData.data;
|
|
2508
|
+
await provider.delete([id], owner, accountId);
|
|
2509
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2510
|
+
res.status(200).json({ status: 200, message: "Deleted", statistic: { storage: quota } });
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
case "quota": {
|
|
2514
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2515
|
+
res.status(200).json({
|
|
2516
|
+
status: 200,
|
|
2517
|
+
message: "Quota retrieved",
|
|
2518
|
+
data: {
|
|
2519
|
+
usedInBytes: quota.usedInBytes,
|
|
2520
|
+
totalInBytes: quota.quotaInBytes,
|
|
2521
|
+
availableInBytes: Math.max(0, quota.quotaInBytes - quota.usedInBytes),
|
|
2522
|
+
percentage: quota.quotaInBytes > 0 ? Math.round(quota.usedInBytes / quota.quotaInBytes * 100) : 0
|
|
2523
|
+
},
|
|
2524
|
+
statistic: { storage: quota }
|
|
2525
|
+
});
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
case "trash": {
|
|
2529
|
+
try {
|
|
2530
|
+
const { provider: trashProvider, accountId: trashAccountId } = await resolveProvider(req, owner);
|
|
2531
|
+
await trashProvider.syncTrash(owner, trashAccountId);
|
|
2532
|
+
} catch (e) {
|
|
2533
|
+
console.error("Trash sync failed", e);
|
|
2446
2534
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2535
|
+
const query = {
|
|
2536
|
+
owner,
|
|
2537
|
+
"provider.type": provider.name,
|
|
2538
|
+
storageAccountId: accountId || null,
|
|
2539
|
+
trashedAt: { $ne: null }
|
|
2540
|
+
};
|
|
2541
|
+
const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
|
|
2542
|
+
const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
|
|
2543
|
+
res.status(200).json({ status: 200, message: "Trash items", data: { items: plainItems, hasMore: false } });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
case "restore": {
|
|
2547
|
+
const restoreData = deleteQuerySchema.safeParse(req.query);
|
|
2548
|
+
if (!restoreData.success) return void res.status(400).json({ status: 400, message: "Could not restore: invalid ID" });
|
|
2549
|
+
const { id } = restoreData.data;
|
|
2550
|
+
const drive = await drive_default.findById(id);
|
|
2551
|
+
if (!drive) return void res.status(404).json({ status: 404, message: "Could not restore: item not found" });
|
|
2552
|
+
let targetParentId = drive.parentId;
|
|
2553
|
+
if (targetParentId) {
|
|
2554
|
+
const parent = await drive_default.findById(targetParentId);
|
|
2555
|
+
if (parent?.trashedAt) {
|
|
2556
|
+
targetParentId = null;
|
|
2454
2557
|
}
|
|
2455
|
-
const query = {
|
|
2456
|
-
owner,
|
|
2457
|
-
"provider.type": provider.name,
|
|
2458
|
-
storageAccountId: accountId || null,
|
|
2459
|
-
trashedAt: { $ne: null }
|
|
2460
|
-
};
|
|
2461
|
-
const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
|
|
2462
|
-
const plainItems = addSignedUrlTokens(await Promise.all(items.map((item) => item.toClient())), config);
|
|
2463
|
-
return res.status(200).json({
|
|
2464
|
-
status: 200,
|
|
2465
|
-
message: "Trash items",
|
|
2466
|
-
data: { items: plainItems, hasMore: false }
|
|
2467
|
-
});
|
|
2468
2558
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
if (!drive) return res.status(404).json({ status: 404, message: "Could not restore: item not found" });
|
|
2476
|
-
let targetParentId = drive.parentId;
|
|
2477
|
-
if (targetParentId) {
|
|
2478
|
-
const parent = await drive_default.findById(targetParentId);
|
|
2479
|
-
if (parent?.trashedAt) {
|
|
2480
|
-
targetParentId = null;
|
|
2481
|
-
}
|
|
2559
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
2560
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
2561
|
+
try {
|
|
2562
|
+
await itemProvider.untrash([id], owner, itemAccountId);
|
|
2563
|
+
if (targetParentId !== drive.parentId) {
|
|
2564
|
+
await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
|
|
2482
2565
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2566
|
+
} catch (e) {
|
|
2567
|
+
console.error("Provider restore failed:", e);
|
|
2568
|
+
}
|
|
2569
|
+
drive.trashedAt = null;
|
|
2570
|
+
drive.parentId = targetParentId;
|
|
2571
|
+
await drive.save();
|
|
2572
|
+
res.status(200).json({
|
|
2573
|
+
status: 200,
|
|
2574
|
+
message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
|
|
2575
|
+
data: null
|
|
2576
|
+
});
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
case "move": {
|
|
2580
|
+
const moveData = moveBodySchema.safeParse(req.body);
|
|
2581
|
+
if (!moveData.success) return void res.status(400).json({ status: 400, message: "Could not move: invalid request data" });
|
|
2582
|
+
const { ids, targetFolderId } = moveData.data;
|
|
2583
|
+
const items = [];
|
|
2584
|
+
const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
|
|
2585
|
+
for (const id of ids) {
|
|
2485
2586
|
try {
|
|
2486
|
-
await
|
|
2487
|
-
|
|
2488
|
-
await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
|
|
2489
|
-
}
|
|
2587
|
+
const item = await provider.move(id, effectiveTargetId, owner, accountId);
|
|
2588
|
+
items.push(item);
|
|
2490
2589
|
} catch (e) {
|
|
2491
|
-
console.error(
|
|
2590
|
+
console.error(`Failed to move item ${id}`, e);
|
|
2492
2591
|
}
|
|
2493
|
-
drive.trashedAt = null;
|
|
2494
|
-
drive.parentId = targetParentId;
|
|
2495
|
-
await drive.save();
|
|
2496
|
-
return res.status(200).json({
|
|
2497
|
-
status: 200,
|
|
2498
|
-
message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
|
|
2499
|
-
data: null
|
|
2500
|
-
});
|
|
2501
2592
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
|
|
2509
|
-
for (const id of ids) {
|
|
2510
|
-
try {
|
|
2511
|
-
const item = await provider.move(id, effectiveTargetId, owner, accountId);
|
|
2512
|
-
items.push(item);
|
|
2513
|
-
} catch (e) {
|
|
2514
|
-
console.error(`Failed to move item ${id}`, e);
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
return res.status(200).json({ status: 200, message: "Moved", data: { items: addSignedUrlTokens(items, config) } });
|
|
2593
|
+
res.status(200).json({ status: 200, message: "Moved", data: { items: withSignedUrls(items, config) } });
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
case "reorder": {
|
|
2597
|
+
if (req.method !== "POST") {
|
|
2598
|
+
return void res.status(405).json({ status: 405, message: "Reordering requires a POST request" });
|
|
2518
2599
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
if (!renameData.success) return res.status(400).json({ status: 400, message: "Could not rename: invalid request data" });
|
|
2523
|
-
const { id, newName } = renameData.data;
|
|
2524
|
-
const item = addSignedUrlToken(await provider.rename(id, newName, owner, accountId), config);
|
|
2525
|
-
return res.status(200).json({ status: 200, message: "Renamed", data: { item } });
|
|
2600
|
+
const reorderData = reorderBodySchema.safeParse(req.body);
|
|
2601
|
+
if (!reorderData.success) {
|
|
2602
|
+
return void res.status(400).json({ status: 400, message: "Could not reorder: invalid request data" });
|
|
2526
2603
|
}
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2604
|
+
const { ids } = reorderData.data;
|
|
2605
|
+
const query = {
|
|
2606
|
+
_id: { $in: ids },
|
|
2607
|
+
"provider.type": provider.name,
|
|
2608
|
+
storageAccountId: accountId || null,
|
|
2609
|
+
trashedAt: null
|
|
2610
|
+
};
|
|
2611
|
+
if (!isRootMode) {
|
|
2612
|
+
query.owner = owner;
|
|
2613
|
+
}
|
|
2614
|
+
const existingItems = await drive_default.find(query, { _id: 1, parentId: 1 });
|
|
2615
|
+
if (existingItems.length !== ids.length) {
|
|
2616
|
+
return void res.status(404).json({ status: 404, message: "Could not reorder: one or more items were not found" });
|
|
2617
|
+
}
|
|
2618
|
+
const parentIds = new Set(existingItems.map((item) => item.parentId ? item.parentId.toString() : "root"));
|
|
2619
|
+
if (parentIds.size > 1) {
|
|
2620
|
+
return void res.status(400).json({ status: 400, message: "Could not reorder: all items must be in the same folder" });
|
|
2621
|
+
}
|
|
2622
|
+
const operations = ids.map((id, order) => ({
|
|
2623
|
+
updateOne: {
|
|
2624
|
+
filter: {
|
|
2625
|
+
_id: id,
|
|
2626
|
+
"provider.type": provider.name,
|
|
2627
|
+
storageAccountId: accountId || null,
|
|
2628
|
+
trashedAt: null,
|
|
2629
|
+
...isRootMode ? {} : { owner }
|
|
2630
|
+
},
|
|
2631
|
+
update: { $set: { order } }
|
|
2632
|
+
}
|
|
2633
|
+
}));
|
|
2634
|
+
await drive_default.bulkWrite(operations);
|
|
2635
|
+
const updatedItems = await drive_default.find(query, {}, { sort: { order: 1 } });
|
|
2636
|
+
const plainItems = withSignedUrls(await Promise.all(updatedItems.map((item) => item.toClient())), config);
|
|
2637
|
+
res.status(200).json({ status: 200, message: "Reordered", data: { items: plainItems } });
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
case "rename": {
|
|
2641
|
+
const renameData = renameBodySchema.safeParse({ id: req.query.id, ...req.body });
|
|
2642
|
+
if (!renameData.success) return void res.status(400).json({ status: 400, message: "Could not rename: invalid request data" });
|
|
2643
|
+
const { id, newName } = renameData.data;
|
|
2644
|
+
const item = withSignedUrl(await provider.rename(id, newName, owner, accountId), config);
|
|
2645
|
+
res.status(200).json({ status: 200, message: "Renamed", data: { item } });
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
default: {
|
|
2649
|
+
res.status(400).json({ status: 400, message: `Unknown action requested: "${action}"` });
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
|
|
2655
|
+
// src/server/index.ts
|
|
2656
|
+
var driveAPIHandler = async (req, res) => {
|
|
2657
|
+
const action = req.query.action || (req.query.code && req.query.state ? "callback" : void 0);
|
|
2658
|
+
let config;
|
|
2659
|
+
try {
|
|
2660
|
+
config = getDriveConfig();
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
console.error("[next-drive] Configuration error:", error);
|
|
2663
|
+
res.status(500).json({ status: 500, message: "Drive is not ready: failed to initialize configuration" });
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
const isPreflightHandled = applyCorsHeaders(req, res, config);
|
|
2667
|
+
if (isPreflightHandled) return;
|
|
2668
|
+
if (!action) {
|
|
2669
|
+
res.status(400).json({ status: 400, message: 'Missing "action" parameter in request' });
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const wasPublicHandled = await handlePublicAction(req, res, action, config);
|
|
2673
|
+
if (wasPublicHandled) return;
|
|
2674
|
+
try {
|
|
2675
|
+
const mode = config.mode || "NORMAL";
|
|
2676
|
+
if (action === "information") {
|
|
2677
|
+
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
2678
|
+
const googleConfigured = !!(clientId && clientSecret && redirectUri);
|
|
2679
|
+
let authenticated = false;
|
|
2680
|
+
try {
|
|
2681
|
+
await getDriveInformation({ method: "REQUEST", req });
|
|
2682
|
+
authenticated = true;
|
|
2683
|
+
} catch {
|
|
2684
|
+
authenticated = false;
|
|
2685
|
+
}
|
|
2686
|
+
res.status(200).json({
|
|
2687
|
+
status: 200,
|
|
2688
|
+
message: "Information retrieved",
|
|
2689
|
+
data: {
|
|
2690
|
+
providers: {
|
|
2691
|
+
google: googleConfigured
|
|
2692
|
+
},
|
|
2693
|
+
mode,
|
|
2694
|
+
authenticated,
|
|
2695
|
+
unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
|
|
2696
|
+
}
|
|
2697
|
+
});
|
|
2698
|
+
return;
|
|
2530
2699
|
}
|
|
2700
|
+
const information = await getDriveInformation({ method: "REQUEST", req });
|
|
2701
|
+
const { key: owner } = information;
|
|
2702
|
+
const isRootMode = mode === "ROOT";
|
|
2703
|
+
const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
|
|
2704
|
+
if (wasAuthHandled) return;
|
|
2705
|
+
const { provider, accountId } = await resolveProvider(req, owner);
|
|
2706
|
+
await handleDriveAction({
|
|
2707
|
+
req,
|
|
2708
|
+
res,
|
|
2709
|
+
action,
|
|
2710
|
+
config,
|
|
2711
|
+
owner,
|
|
2712
|
+
isRootMode,
|
|
2713
|
+
information,
|
|
2714
|
+
provider,
|
|
2715
|
+
accountId
|
|
2716
|
+
});
|
|
2531
2717
|
} catch (error) {
|
|
2532
2718
|
console.error(`[next-drive] Error handling action ${action}:`, error);
|
|
2533
|
-
|
|
2719
|
+
const detail = error instanceof Error ? error.message : "Something went wrong while processing your request";
|
|
2720
|
+
res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
|
|
2534
2721
|
}
|
|
2535
2722
|
};
|
|
2536
2723
|
|
|
2537
|
-
export { driveAPIHandler, driveCleanup, driveConfiguration, driveDelete, driveFilePath, driveFileSchemaZod, driveGetUrl, driveInfo, driveList, driveListFiles, driveReadFile, driveUpload, drive_default, getDriveConfig, getDriveInformation };
|
|
2538
|
-
//# sourceMappingURL=chunk-
|
|
2539
|
-
//# sourceMappingURL=chunk-
|
|
2724
|
+
export { driveAPIHandler, driveCleanup, driveConfiguration, driveConfirm, driveDelete, driveFilePath, driveFileSchemaZod, driveGetUrl, driveInfo, driveList, driveListFiles, drivePurgeExpired, driveReadFile, driveUpload, drive_default, getDriveConfig, getDriveInformation };
|
|
2725
|
+
//# sourceMappingURL=chunk-XUPDNN2U.js.map
|
|
2726
|
+
//# sourceMappingURL=chunk-XUPDNN2U.js.map
|