@muhgholy/next-drive 4.23.8 → 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-OU5TKLHV.cjs → chunk-V75PCJHT.cjs} +116 -24
- package/dist/chunk-V75PCJHT.cjs.map +1 -0
- package/dist/{chunk-RBSFEEJJ.js → chunk-XUPDNN2U.js} +115 -25
- package/dist/chunk-XUPDNN2U.js.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.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
|
@@ -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
|
};
|
|
@@ -1527,7 +1533,8 @@ var uploadChunkSchema = z.object({
|
|
|
1527
1533
|
fileName: nameSchema,
|
|
1528
1534
|
fileSize: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
1529
1535
|
fileType: z.string().min(1).max(255),
|
|
1530
|
-
folderId: z.string().optional()
|
|
1536
|
+
folderId: z.string().optional(),
|
|
1537
|
+
unauthenticated: z.coerce.boolean().optional()
|
|
1531
1538
|
}).refine((data) => data.chunkIndex < data.totalChunks, {
|
|
1532
1539
|
message: "Chunk index must be less than total chunks"
|
|
1533
1540
|
});
|
|
@@ -2121,6 +2128,26 @@ var driveCleanup = async () => {
|
|
|
2121
2128
|
}
|
|
2122
2129
|
return { removed, totalFreedInBytes };
|
|
2123
2130
|
};
|
|
2131
|
+
var driveConfirm = async (id) => {
|
|
2132
|
+
const result = await drive_default.updateOne({ _id: id }, { $set: { expiresAt: null } });
|
|
2133
|
+
return result.matchedCount > 0;
|
|
2134
|
+
};
|
|
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 };
|
|
2150
|
+
};
|
|
2124
2151
|
|
|
2125
2152
|
// src/server/actions/shared.ts
|
|
2126
2153
|
var resolveProvider = async (req, owner) => {
|
|
@@ -2235,36 +2262,83 @@ var handleDriveAction = async (ctx) => {
|
|
|
2235
2262
|
cleanupTempFiles(files);
|
|
2236
2263
|
return void res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
|
|
2237
2264
|
}
|
|
2238
|
-
const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId } = uploadData.data;
|
|
2265
|
+
const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId, unauthenticated } = uploadData.data;
|
|
2239
2266
|
let currentUploadId = driveId;
|
|
2240
2267
|
const tempBaseDir = path.join(os2.tmpdir(), "next-drive-uploads");
|
|
2241
2268
|
if (!currentUploadId) {
|
|
2242
2269
|
if (chunkIndex !== 0) return void res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
|
|
2243
|
-
if (
|
|
2244
|
-
|
|
2270
|
+
if (unauthenticated) {
|
|
2271
|
+
const unauth = config.security?.unauthenticated;
|
|
2272
|
+
if (!unauth?.enabled) {
|
|
2245
2273
|
cleanupTempFiles(files);
|
|
2246
|
-
return void res.status(
|
|
2274
|
+
return void res.status(403).json({ status: 403, message: "Anonymous uploads are not enabled" });
|
|
2247
2275
|
}
|
|
2248
|
-
|
|
2249
|
-
if (!isRootMode) {
|
|
2250
|
-
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2251
|
-
if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
|
|
2276
|
+
if (fileSizeInBytes > unauth.maxUploadSizeInBytes) {
|
|
2252
2277
|
cleanupTempFiles(files);
|
|
2253
|
-
return void res.status(413).json({ status: 413, message: "Could not upload:
|
|
2278
|
+
return void res.status(413).json({ status: 413, message: "Could not upload: file exceeds the maximum allowed size" });
|
|
2279
|
+
}
|
|
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" });
|
|
2294
|
+
}
|
|
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" });
|
|
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++;
|
|
2313
|
+
}
|
|
2314
|
+
} else {
|
|
2315
|
+
if (fileType && config.security) {
|
|
2316
|
+
if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
|
|
2317
|
+
cleanupTempFiles(files);
|
|
2318
|
+
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (!isRootMode) {
|
|
2322
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2323
|
+
if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
|
|
2324
|
+
cleanupTempFiles(files);
|
|
2325
|
+
return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
|
|
2326
|
+
}
|
|
2254
2327
|
}
|
|
2255
2328
|
}
|
|
2256
2329
|
currentUploadId = crypto3.randomUUID();
|
|
2257
2330
|
const uploadDir2 = path.join(tempBaseDir, currentUploadId);
|
|
2258
2331
|
fs.mkdirSync(uploadDir2, { recursive: true });
|
|
2259
2332
|
const metadata = {
|
|
2260
|
-
owner,
|
|
2261
|
-
accountId,
|
|
2333
|
+
owner: unauthenticated ? null : owner,
|
|
2334
|
+
accountId: unauthenticated ? null : accountId,
|
|
2262
2335
|
providerName: provider.name,
|
|
2263
2336
|
name: fileName,
|
|
2264
|
-
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
2337
|
+
parentId: unauthenticated || folderId === "root" || !folderId ? null : folderId,
|
|
2265
2338
|
fileSize: fileSizeInBytes,
|
|
2266
2339
|
mimeType: fileType,
|
|
2267
|
-
totalChunks
|
|
2340
|
+
totalChunks,
|
|
2341
|
+
unauthenticated: !!unauthenticated
|
|
2268
2342
|
};
|
|
2269
2343
|
fs.writeFileSync(path.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
|
|
2270
2344
|
}
|
|
@@ -2350,7 +2424,8 @@ var handleDriveAction = async (ctx) => {
|
|
|
2350
2424
|
information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
|
|
2351
2425
|
status: "UPLOADING",
|
|
2352
2426
|
currentChunk: totalChunks,
|
|
2353
|
-
totalChunks
|
|
2427
|
+
totalChunks,
|
|
2428
|
+
expiresAt: meta.unauthenticated ? new Date(Date.now() + (config.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4) : null
|
|
2354
2429
|
});
|
|
2355
2430
|
if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
|
|
2356
2431
|
drive.information.path = path.join("file", String(drive._id), "data.bin");
|
|
@@ -2359,10 +2434,12 @@ var handleDriveAction = async (ctx) => {
|
|
|
2359
2434
|
try {
|
|
2360
2435
|
const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
|
|
2361
2436
|
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
2437
|
+
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2362
2438
|
const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
|
|
2363
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 } });
|
|
2364
2440
|
} catch (err) {
|
|
2365
2441
|
await drive_default.deleteOne({ _id: drive._id });
|
|
2442
|
+
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2366
2443
|
throw err;
|
|
2367
2444
|
}
|
|
2368
2445
|
} else {
|
|
@@ -2386,6 +2463,10 @@ var handleDriveAction = async (ctx) => {
|
|
|
2386
2463
|
const tempUploadDir = path.join(os2.tmpdir(), "next-drive-uploads", id);
|
|
2387
2464
|
if (fs.existsSync(tempUploadDir)) {
|
|
2388
2465
|
try {
|
|
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
|
+
}
|
|
2389
2470
|
fs.rmSync(tempUploadDir, { recursive: true, force: true });
|
|
2390
2471
|
} catch (e) {
|
|
2391
2472
|
console.error("Failed to cleanup temp upload:", e);
|
|
@@ -2592,12 +2673,16 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2592
2673
|
if (wasPublicHandled) return;
|
|
2593
2674
|
try {
|
|
2594
2675
|
const mode = config.mode || "NORMAL";
|
|
2595
|
-
const information = await getDriveInformation({ method: "REQUEST", req });
|
|
2596
|
-
const { key: owner } = information;
|
|
2597
|
-
const isRootMode = mode === "ROOT";
|
|
2598
2676
|
if (action === "information") {
|
|
2599
2677
|
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
2600
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
|
+
}
|
|
2601
2686
|
res.status(200).json({
|
|
2602
2687
|
status: 200,
|
|
2603
2688
|
message: "Information retrieved",
|
|
@@ -2605,11 +2690,16 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2605
2690
|
providers: {
|
|
2606
2691
|
google: googleConfigured
|
|
2607
2692
|
},
|
|
2608
|
-
mode
|
|
2693
|
+
mode,
|
|
2694
|
+
authenticated,
|
|
2695
|
+
unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
|
|
2609
2696
|
}
|
|
2610
2697
|
});
|
|
2611
2698
|
return;
|
|
2612
2699
|
}
|
|
2700
|
+
const information = await getDriveInformation({ method: "REQUEST", req });
|
|
2701
|
+
const { key: owner } = information;
|
|
2702
|
+
const isRootMode = mode === "ROOT";
|
|
2613
2703
|
const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
|
|
2614
2704
|
if (wasAuthHandled) return;
|
|
2615
2705
|
const { provider, accountId } = await resolveProvider(req, owner);
|
|
@@ -2631,6 +2721,6 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2631
2721
|
}
|
|
2632
2722
|
};
|
|
2633
2723
|
|
|
2634
|
-
export { driveAPIHandler, driveCleanup, driveConfiguration, driveDelete, driveFilePath, driveFileSchemaZod, driveGetUrl, driveInfo, driveList, driveListFiles, driveReadFile, driveUpload, drive_default, getDriveConfig, getDriveInformation };
|
|
2635
|
-
//# sourceMappingURL=chunk-
|
|
2636
|
-
//# 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
|