@muhgholy/next-drive 4.23.8 → 4.23.11
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-RBSFEEJJ.js → chunk-26KZWPCF.js} +131 -25
- package/dist/chunk-26KZWPCF.js.map +1 -0
- package/dist/{chunk-OU5TKLHV.cjs → chunk-NDHVF2IB.cjs} +132 -24
- package/dist/chunk-NDHVF2IB.cjs.map +1 -0
- package/dist/client/file-chooser.d.ts +1 -0
- package/dist/client/file-chooser.d.ts.map +1 -1
- package/dist/client/hooks/use-upload.d.ts +1 -1
- package/dist/client/hooks/use-upload.d.ts.map +1 -1
- package/dist/client/index.cjs +88 -42
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.js +87 -41
- package/dist/client/index.js.map +1 -1
- package/dist/server/actions/drive.d.ts +1 -0
- package/dist/server/actions/drive.d.ts.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/controllers/drive.d.ts +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 +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- 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/server/config.d.ts +17 -0
- package/dist/types/server/config.d.ts.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-OU5TKLHV.cjs.map +0 -1
- package/dist/chunk-RBSFEEJJ.js.map +0 -1
|
@@ -49,6 +49,7 @@ var DriveSchema = new mongoose.Schema(
|
|
|
49
49
|
information: { type: informationSchema, required: true },
|
|
50
50
|
status: { type: String, enum: ["READY", "PROCESSING", "UPLOADING", "FAILED"], default: "PROCESSING" },
|
|
51
51
|
trashedAt: { type: Date, default: null },
|
|
52
|
+
expiresAt: { type: Date, default: null },
|
|
52
53
|
meta: { type: mongoose.Schema.Types.Mixed, default: {} },
|
|
53
54
|
createdAt: { type: Date, default: Date.now }
|
|
54
55
|
},
|
|
@@ -61,6 +62,7 @@ DriveSchema.index({ owner: 1, trashedAt: 1 });
|
|
|
61
62
|
DriveSchema.index({ owner: 1, "information.hash": 1 });
|
|
62
63
|
DriveSchema.index({ owner: 1, name: "text" });
|
|
63
64
|
DriveSchema.index({ owner: 1, "provider.type": 1 });
|
|
65
|
+
DriveSchema.index({ expiresAt: 1 });
|
|
64
66
|
DriveSchema.method("toClient", async function() {
|
|
65
67
|
const data = this.toJSON();
|
|
66
68
|
return {
|
|
@@ -73,6 +75,7 @@ DriveSchema.method("toClient", async function() {
|
|
|
73
75
|
information: data.information,
|
|
74
76
|
status: data.status,
|
|
75
77
|
trashedAt: data.trashedAt,
|
|
78
|
+
expiresAt: data.expiresAt,
|
|
76
79
|
meta: data.meta,
|
|
77
80
|
createdAt: data.createdAt
|
|
78
81
|
};
|
|
@@ -259,7 +262,8 @@ var getGlobal = () => {
|
|
|
259
262
|
globalThis[GLOBAL_KEY] = {
|
|
260
263
|
config: null,
|
|
261
264
|
migrationPromise: null,
|
|
262
|
-
initialized: false
|
|
265
|
+
initialized: false,
|
|
266
|
+
abuse: { ipHits: /* @__PURE__ */ new Map(), concurrent: 0 }
|
|
263
267
|
};
|
|
264
268
|
}
|
|
265
269
|
return globalThis[GLOBAL_KEY];
|
|
@@ -288,7 +292,8 @@ var driveConfiguration = async (config) => {
|
|
|
288
292
|
// 10GB default for ROOT
|
|
289
293
|
allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
|
|
290
294
|
signedUrls: config.security?.signedUrls,
|
|
291
|
-
trash: config.security?.trash
|
|
295
|
+
trash: config.security?.trash,
|
|
296
|
+
unauthenticated: config.security?.unauthenticated
|
|
292
297
|
}
|
|
293
298
|
};
|
|
294
299
|
} else {
|
|
@@ -306,7 +311,8 @@ var driveConfiguration = async (config) => {
|
|
|
306
311
|
maxUploadSizeInBytes: config.security?.maxUploadSizeInBytes ?? 10 * 1024 * 1024,
|
|
307
312
|
allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
|
|
308
313
|
signedUrls: config.security?.signedUrls,
|
|
309
|
-
trash: config.security?.trash
|
|
314
|
+
trash: config.security?.trash,
|
|
315
|
+
unauthenticated: config.security?.unauthenticated
|
|
310
316
|
},
|
|
311
317
|
information: config.information
|
|
312
318
|
};
|
|
@@ -1540,7 +1546,8 @@ var uploadChunkSchema = zod.z.object({
|
|
|
1540
1546
|
fileName: nameSchema,
|
|
1541
1547
|
fileSize: zod.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
1542
1548
|
fileType: zod.z.string().min(1).max(255),
|
|
1543
|
-
folderId: zod.z.string().optional()
|
|
1549
|
+
folderId: zod.z.string().optional(),
|
|
1550
|
+
unauthenticated: zod.z.coerce.boolean().optional()
|
|
1544
1551
|
}).refine((data) => data.chunkIndex < data.totalChunks, {
|
|
1545
1552
|
message: "Chunk index must be less than total chunks"
|
|
1546
1553
|
});
|
|
@@ -2134,6 +2141,26 @@ var driveCleanup = async () => {
|
|
|
2134
2141
|
}
|
|
2135
2142
|
return { removed, totalFreedInBytes };
|
|
2136
2143
|
};
|
|
2144
|
+
var driveConfirm = async (id) => {
|
|
2145
|
+
const result = await drive_default.updateOne({ _id: id }, { $set: { expiresAt: null } });
|
|
2146
|
+
return result.matchedCount > 0;
|
|
2147
|
+
};
|
|
2148
|
+
var drivePurgeExpired = async () => {
|
|
2149
|
+
const expired = await drive_default.find({ expiresAt: { $ne: null, $lt: /* @__PURE__ */ new Date() } });
|
|
2150
|
+
const removed = [];
|
|
2151
|
+
let totalFreedInBytes = 0;
|
|
2152
|
+
for (const drive of expired) {
|
|
2153
|
+
const id = String(drive._id);
|
|
2154
|
+
try {
|
|
2155
|
+
await driveDelete(drive);
|
|
2156
|
+
totalFreedInBytes += drive.information.type === "FILE" ? drive.information.sizeInBytes : 0;
|
|
2157
|
+
removed.push(id);
|
|
2158
|
+
} catch (e) {
|
|
2159
|
+
console.error(`[next-drive] Failed to purge expired file ${id}:`, e);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return { removed, totalFreedInBytes };
|
|
2163
|
+
};
|
|
2137
2164
|
|
|
2138
2165
|
// src/server/actions/shared.ts
|
|
2139
2166
|
var resolveProvider = async (req, owner) => {
|
|
@@ -2159,7 +2186,7 @@ var withSignedUrls = (items, config) => {
|
|
|
2159
2186
|
|
|
2160
2187
|
// src/server/actions/drive.ts
|
|
2161
2188
|
var handleDriveAction = async (ctx) => {
|
|
2162
|
-
const { req, res, action, config, owner, isRootMode, information, provider, accountId } = ctx;
|
|
2189
|
+
const { req, res, action, config, owner, isRootMode, authenticated, information, provider, accountId } = ctx;
|
|
2163
2190
|
switch (action) {
|
|
2164
2191
|
case "list": {
|
|
2165
2192
|
if (req.method !== "GET") return void res.status(405).json({ status: 405, message: "Listing files requires a GET request" });
|
|
@@ -2248,36 +2275,87 @@ var handleDriveAction = async (ctx) => {
|
|
|
2248
2275
|
cleanupTempFiles(files);
|
|
2249
2276
|
return void res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
|
|
2250
2277
|
}
|
|
2251
|
-
const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId } = uploadData.data;
|
|
2278
|
+
const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId, unauthenticated } = uploadData.data;
|
|
2252
2279
|
let currentUploadId = driveId;
|
|
2253
2280
|
const tempBaseDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
|
|
2254
2281
|
if (!currentUploadId) {
|
|
2255
2282
|
if (chunkIndex !== 0) return void res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
|
|
2256
|
-
if (
|
|
2257
|
-
|
|
2283
|
+
if (unauthenticated) {
|
|
2284
|
+
const unauth = config.security?.unauthenticated;
|
|
2285
|
+
if (!unauth?.enabled) {
|
|
2286
|
+
cleanupTempFiles(files);
|
|
2287
|
+
return void res.status(403).json({ status: 403, message: "Anonymous uploads are not enabled" });
|
|
2288
|
+
}
|
|
2289
|
+
if (fileSizeInBytes > unauth.maxUploadSizeInBytes) {
|
|
2290
|
+
cleanupTempFiles(files);
|
|
2291
|
+
return void res.status(413).json({ status: 413, message: "Could not upload: file exceeds the maximum allowed size" });
|
|
2292
|
+
}
|
|
2293
|
+
if (fileType && !validateMimeType(fileType, unauth.allowedMimeTypes)) {
|
|
2258
2294
|
cleanupTempFiles(files);
|
|
2259
2295
|
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2260
2296
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2297
|
+
const abuse = unauth.abuse;
|
|
2298
|
+
if (abuse) {
|
|
2299
|
+
const store = globalThis.__nextDrive.abuse;
|
|
2300
|
+
const now = Date.now();
|
|
2301
|
+
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";
|
|
2302
|
+
const hits = (store.ipHits.get(ip) ?? []).filter((t) => now - t < 36e5);
|
|
2303
|
+
const perIp = abuse.perIp;
|
|
2304
|
+
if (perIp && hits.filter((t) => now - t < perIp.windowMinutes * 6e4).length >= perIp.max) {
|
|
2305
|
+
cleanupTempFiles(files);
|
|
2306
|
+
return void res.status(429).json({ status: 429, message: "Too many uploads, please try again later" });
|
|
2307
|
+
}
|
|
2308
|
+
if (abuse.hourlyPerIp && hits.length >= abuse.hourlyPerIp) {
|
|
2309
|
+
cleanupTempFiles(files);
|
|
2310
|
+
return void res.status(429).json({ status: 429, message: "Hourly upload limit reached, please try again later" });
|
|
2311
|
+
}
|
|
2312
|
+
if (abuse.maxConcurrent && store.concurrent >= abuse.maxConcurrent) {
|
|
2313
|
+
cleanupTempFiles(files);
|
|
2314
|
+
return void res.status(429).json({ status: 429, message: "Server is busy with uploads, please try again later" });
|
|
2315
|
+
}
|
|
2316
|
+
if (abuse.maxLiveBytes) {
|
|
2317
|
+
const [agg] = await drive_default.aggregate([{ $match: { expiresAt: { $ne: null } } }, { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }]);
|
|
2318
|
+
if ((agg?.total ?? 0) + fileSizeInBytes > abuse.maxLiveBytes) {
|
|
2319
|
+
cleanupTempFiles(files);
|
|
2320
|
+
return void res.status(429).json({ status: 429, message: "Temporary storage is full, please try again later" });
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
hits.push(now);
|
|
2324
|
+
store.ipHits.set(ip, hits);
|
|
2325
|
+
store.concurrent++;
|
|
2326
|
+
}
|
|
2327
|
+
} else {
|
|
2328
|
+
if (!authenticated) {
|
|
2265
2329
|
cleanupTempFiles(files);
|
|
2266
|
-
return void res.status(
|
|
2330
|
+
return void res.status(401).json({ status: 401, message: "Authentication required to upload" });
|
|
2331
|
+
}
|
|
2332
|
+
if (fileType && config.security) {
|
|
2333
|
+
if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
|
|
2334
|
+
cleanupTempFiles(files);
|
|
2335
|
+
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (!isRootMode) {
|
|
2339
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2340
|
+
if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
|
|
2341
|
+
cleanupTempFiles(files);
|
|
2342
|
+
return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
|
|
2343
|
+
}
|
|
2267
2344
|
}
|
|
2268
2345
|
}
|
|
2269
2346
|
currentUploadId = crypto3__default.default.randomUUID();
|
|
2270
2347
|
const uploadDir2 = path__default.default.join(tempBaseDir, currentUploadId);
|
|
2271
2348
|
fs__default.default.mkdirSync(uploadDir2, { recursive: true });
|
|
2272
2349
|
const metadata = {
|
|
2273
|
-
owner,
|
|
2274
|
-
accountId,
|
|
2350
|
+
owner: unauthenticated ? null : owner,
|
|
2351
|
+
accountId: unauthenticated ? null : accountId,
|
|
2275
2352
|
providerName: provider.name,
|
|
2276
2353
|
name: fileName,
|
|
2277
|
-
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
2354
|
+
parentId: unauthenticated || folderId === "root" || !folderId ? null : folderId,
|
|
2278
2355
|
fileSize: fileSizeInBytes,
|
|
2279
2356
|
mimeType: fileType,
|
|
2280
|
-
totalChunks
|
|
2357
|
+
totalChunks,
|
|
2358
|
+
unauthenticated: !!unauthenticated
|
|
2281
2359
|
};
|
|
2282
2360
|
fs__default.default.writeFileSync(path__default.default.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
|
|
2283
2361
|
}
|
|
@@ -2363,7 +2441,8 @@ var handleDriveAction = async (ctx) => {
|
|
|
2363
2441
|
information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
|
|
2364
2442
|
status: "UPLOADING",
|
|
2365
2443
|
currentChunk: totalChunks,
|
|
2366
|
-
totalChunks
|
|
2444
|
+
totalChunks,
|
|
2445
|
+
expiresAt: meta.unauthenticated ? new Date(Date.now() + (config.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4) : null
|
|
2367
2446
|
});
|
|
2368
2447
|
if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
|
|
2369
2448
|
drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
|
|
@@ -2372,10 +2451,12 @@ var handleDriveAction = async (ctx) => {
|
|
|
2372
2451
|
try {
|
|
2373
2452
|
const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
|
|
2374
2453
|
fs__default.default.rmSync(uploadDir, { recursive: true, force: true });
|
|
2454
|
+
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2375
2455
|
const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
|
|
2376
2456
|
res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item: withSignedUrl(item, config) }, statistic: { storage: newQuota } });
|
|
2377
2457
|
} catch (err) {
|
|
2378
2458
|
await drive_default.deleteOne({ _id: drive._id });
|
|
2459
|
+
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2379
2460
|
throw err;
|
|
2380
2461
|
}
|
|
2381
2462
|
} else {
|
|
@@ -2399,6 +2480,10 @@ var handleDriveAction = async (ctx) => {
|
|
|
2399
2480
|
const tempUploadDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads", id);
|
|
2400
2481
|
if (fs__default.default.existsSync(tempUploadDir)) {
|
|
2401
2482
|
try {
|
|
2483
|
+
const metaPath = path__default.default.join(tempUploadDir, "metadata.json");
|
|
2484
|
+
if (fs__default.default.existsSync(metaPath) && JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8")).unauthenticated) {
|
|
2485
|
+
globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2486
|
+
}
|
|
2402
2487
|
fs__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
|
|
2403
2488
|
} catch (e) {
|
|
2404
2489
|
console.error("Failed to cleanup temp upload:", e);
|
|
@@ -2605,12 +2690,16 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2605
2690
|
if (wasPublicHandled) return;
|
|
2606
2691
|
try {
|
|
2607
2692
|
const mode = config.mode || "NORMAL";
|
|
2608
|
-
const information = await getDriveInformation({ method: "REQUEST", req });
|
|
2609
|
-
const { key: owner } = information;
|
|
2610
|
-
const isRootMode = mode === "ROOT";
|
|
2611
2693
|
if (action === "information") {
|
|
2612
2694
|
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
2613
2695
|
const googleConfigured = !!(clientId && clientSecret && redirectUri);
|
|
2696
|
+
let authenticated2 = false;
|
|
2697
|
+
try {
|
|
2698
|
+
await getDriveInformation({ method: "REQUEST", req });
|
|
2699
|
+
authenticated2 = true;
|
|
2700
|
+
} catch {
|
|
2701
|
+
authenticated2 = false;
|
|
2702
|
+
}
|
|
2614
2703
|
res.status(200).json({
|
|
2615
2704
|
status: 200,
|
|
2616
2705
|
message: "Information retrieved",
|
|
@@ -2618,11 +2707,27 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2618
2707
|
providers: {
|
|
2619
2708
|
google: googleConfigured
|
|
2620
2709
|
},
|
|
2621
|
-
mode
|
|
2710
|
+
mode,
|
|
2711
|
+
authenticated: authenticated2,
|
|
2712
|
+
unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
|
|
2622
2713
|
}
|
|
2623
2714
|
});
|
|
2624
2715
|
return;
|
|
2625
2716
|
}
|
|
2717
|
+
const isRootMode = mode === "ROOT";
|
|
2718
|
+
let information;
|
|
2719
|
+
let authenticated = true;
|
|
2720
|
+
try {
|
|
2721
|
+
information = await getDriveInformation({ method: "REQUEST", req });
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
if ((action === "upload" || action === "cancel") && config.security?.unauthenticated?.enabled) {
|
|
2724
|
+
information = { key: null, storage: { quotaInBytes: 0 } };
|
|
2725
|
+
authenticated = false;
|
|
2726
|
+
} else {
|
|
2727
|
+
throw err;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
const { key: owner } = information;
|
|
2626
2731
|
const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
|
|
2627
2732
|
if (wasAuthHandled) return;
|
|
2628
2733
|
const { provider, accountId } = await resolveProvider(req, owner);
|
|
@@ -2633,6 +2738,7 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2633
2738
|
config,
|
|
2634
2739
|
owner,
|
|
2635
2740
|
isRootMode,
|
|
2741
|
+
authenticated,
|
|
2636
2742
|
information,
|
|
2637
2743
|
provider,
|
|
2638
2744
|
accountId
|
|
@@ -2647,6 +2753,7 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2647
2753
|
exports.driveAPIHandler = driveAPIHandler;
|
|
2648
2754
|
exports.driveCleanup = driveCleanup;
|
|
2649
2755
|
exports.driveConfiguration = driveConfiguration;
|
|
2756
|
+
exports.driveConfirm = driveConfirm;
|
|
2650
2757
|
exports.driveDelete = driveDelete;
|
|
2651
2758
|
exports.driveFilePath = driveFilePath;
|
|
2652
2759
|
exports.driveFileSchemaZod = driveFileSchemaZod;
|
|
@@ -2654,10 +2761,11 @@ exports.driveGetUrl = driveGetUrl;
|
|
|
2654
2761
|
exports.driveInfo = driveInfo;
|
|
2655
2762
|
exports.driveList = driveList;
|
|
2656
2763
|
exports.driveListFiles = driveListFiles;
|
|
2764
|
+
exports.drivePurgeExpired = drivePurgeExpired;
|
|
2657
2765
|
exports.driveReadFile = driveReadFile;
|
|
2658
2766
|
exports.driveUpload = driveUpload;
|
|
2659
2767
|
exports.drive_default = drive_default;
|
|
2660
2768
|
exports.getDriveConfig = getDriveConfig;
|
|
2661
2769
|
exports.getDriveInformation = getDriveInformation;
|
|
2662
|
-
//# sourceMappingURL=chunk-
|
|
2663
|
-
//# sourceMappingURL=chunk-
|
|
2770
|
+
//# sourceMappingURL=chunk-NDHVF2IB.cjs.map
|
|
2771
|
+
//# sourceMappingURL=chunk-NDHVF2IB.cjs.map
|