@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.
Files changed (36) hide show
  1. package/README.md +152 -1
  2. package/dist/{chunk-OU5TKLHV.cjs → chunk-V75PCJHT.cjs} +116 -24
  3. package/dist/chunk-V75PCJHT.cjs.map +1 -0
  4. package/dist/{chunk-RBSFEEJJ.js → chunk-XUPDNN2U.js} +115 -25
  5. package/dist/chunk-XUPDNN2U.js.map +1 -0
  6. package/dist/client/file-chooser.d.ts +1 -0
  7. package/dist/client/file-chooser.d.ts.map +1 -1
  8. package/dist/client/hooks/use-upload.d.ts +1 -1
  9. package/dist/client/hooks/use-upload.d.ts.map +1 -1
  10. package/dist/client/index.cjs +88 -42
  11. package/dist/client/index.cjs.map +1 -1
  12. package/dist/client/index.js +87 -41
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/server/actions/drive.d.ts.map +1 -1
  15. package/dist/server/config.d.ts.map +1 -1
  16. package/dist/server/controllers/drive.d.ts +26 -0
  17. package/dist/server/controllers/drive.d.ts.map +1 -1
  18. package/dist/server/database/mongoose/schema/drive.d.ts +1 -0
  19. package/dist/server/database/mongoose/schema/drive.d.ts.map +1 -1
  20. package/dist/server/express.cjs +11 -11
  21. package/dist/server/express.js +2 -2
  22. package/dist/server/hono.cjs +11 -11
  23. package/dist/server/hono.js +2 -2
  24. package/dist/server/index.cjs +24 -16
  25. package/dist/server/index.d.ts +1 -1
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +1 -1
  28. package/dist/server/zod/schemas.d.ts +5 -0
  29. package/dist/server/zod/schemas.d.ts.map +1 -1
  30. package/dist/types/lib/database/drive.d.ts +1 -0
  31. package/dist/types/lib/database/drive.d.ts.map +1 -1
  32. package/dist/types/server/config.d.ts +17 -0
  33. package/dist/types/server/config.d.ts.map +1 -1
  34. package/package.json +2 -1
  35. package/dist/chunk-OU5TKLHV.cjs.map +0 -1
  36. 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 (fileType && config.security) {
2244
- if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
2270
+ if (unauthenticated) {
2271
+ const unauth = config.security?.unauthenticated;
2272
+ if (!unauth?.enabled) {
2245
2273
  cleanupTempFiles(files);
2246
- return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
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: you have run out of storage space" });
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-RBSFEEJJ.js.map
2636
- //# sourceMappingURL=chunk-RBSFEEJJ.js.map
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