@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.
Files changed (37) hide show
  1. package/README.md +152 -1
  2. package/dist/{chunk-RBSFEEJJ.js → chunk-26KZWPCF.js} +131 -25
  3. package/dist/chunk-26KZWPCF.js.map +1 -0
  4. package/dist/{chunk-OU5TKLHV.cjs → chunk-NDHVF2IB.cjs} +132 -24
  5. package/dist/chunk-NDHVF2IB.cjs.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 +1 -0
  15. package/dist/server/actions/drive.d.ts.map +1 -1
  16. package/dist/server/config.d.ts.map +1 -1
  17. package/dist/server/controllers/drive.d.ts +26 -0
  18. package/dist/server/controllers/drive.d.ts.map +1 -1
  19. package/dist/server/database/mongoose/schema/drive.d.ts +1 -0
  20. package/dist/server/database/mongoose/schema/drive.d.ts.map +1 -1
  21. package/dist/server/express.cjs +11 -11
  22. package/dist/server/express.js +2 -2
  23. package/dist/server/hono.cjs +11 -11
  24. package/dist/server/hono.js +2 -2
  25. package/dist/server/index.cjs +24 -16
  26. package/dist/server/index.d.ts +1 -1
  27. package/dist/server/index.d.ts.map +1 -1
  28. package/dist/server/index.js +1 -1
  29. package/dist/server/zod/schemas.d.ts +5 -0
  30. package/dist/server/zod/schemas.d.ts.map +1 -1
  31. package/dist/types/lib/database/drive.d.ts +1 -0
  32. package/dist/types/lib/database/drive.d.ts.map +1 -1
  33. package/dist/types/server/config.d.ts +17 -0
  34. package/dist/types/server/config.d.ts.map +1 -1
  35. package/package.json +2 -1
  36. package/dist/chunk-OU5TKLHV.cjs.map +0 -1
  37. 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 (fileType && config.security) {
2257
- if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
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
- if (!isRootMode) {
2263
- const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2264
- if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
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(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
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-OU5TKLHV.cjs.map
2663
- //# sourceMappingURL=chunk-OU5TKLHV.cjs.map
2770
+ //# sourceMappingURL=chunk-NDHVF2IB.cjs.map
2771
+ //# sourceMappingURL=chunk-NDHVF2IB.cjs.map