@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
package/README.md CHANGED
@@ -7,7 +7,8 @@ File storage and management for Next.js and Express apps. Includes a responsive
7
7
  - 📁 **File Management** – Upload, rename, move, organize files and folders
8
8
  - 🔍 **Search** – Search active files or trash with real-time filtering
9
9
  - 🗑️ **Trash System** – Soft delete, restore, and empty trash
10
- - 📱 **Responsive UI** – Optimized for desktop and mobile
10
+ - **Anonymous Uploads** – One-time uploads with auto-expiry, confirmation, and abuse prevention
11
+ - �📱 **Responsive UI** – Optimized for desktop and mobile
11
12
  - 🎬 **Video Thumbnails** – Auto-generated thumbnails (requires FFmpeg)
12
13
  - 🔐 **Security** – Signed URLs and configurable upload limits
13
14
  - 📊 **View Modes** – Grid/List views with sorting and grouping
@@ -188,6 +189,8 @@ function MyForm() {
188
189
  }
189
190
  ```
190
191
 
192
+ > For unauthenticated, one-time uploads, add the `allowUnauthenticated` prop. See [Anonymous (One-Time) Uploads](#anonymous-one-time-uploads).
193
+
191
194
  ---
192
195
 
193
196
  ## Express Integration
@@ -582,6 +585,128 @@ await driveDelete(items[0]);
582
585
 
583
586
  ---
584
587
 
588
+ ## Anonymous (One-Time) Uploads
589
+
590
+ Let **unauthenticated** users upload a file once — without giving them access to a drive. The user picks a file, it uploads with progress (just like the drive explorer), and you receive a normal `TDriveFile` (with a real `drive.id`). The file is **temporary**: it is automatically deleted after a TTL (default **60 minutes**) unless your backend explicitly confirms it.
591
+
592
+ The same `DriveFileChooser` adapts to the visitor automatically: **logged-in users get the full drive explorer** (browse + pick existing files or upload to their own drive, stored permanently), while **anonymous visitors get a one-time upload** (temporary, auto-expiring). You opt in with a single `allowUnauthenticated` prop.
593
+
594
+ This is useful for forms where you need a `drive.id` for an anonymous visitor (e.g. a contact form attachment, a job application, or a guest submission) but don't want to expose your authenticated drive.
595
+
596
+ **Lifecycle:** `pick file → upload → returns TDriveFile (temporary)` → your backend saves `drive.id` → `driveConfirm(id)` keeps it → otherwise `drivePurgeExpired()` deletes it after the TTL.
597
+
598
+ ### 1. Enable in your server configuration
599
+
600
+ Add an `unauthenticated` block to `security`. It is **completely separate** from your authenticated limits (it has its own size and MIME allow-list). The feature is disabled unless `enabled: true`.
601
+
602
+ ```typescript
603
+ security: {
604
+ // ...your normal authenticated limits
605
+ maxUploadSizeInBytes: 50 * 1024 * 1024,
606
+ allowedMimeTypes: ["image/*", "video/*", "application/pdf"],
607
+
608
+ unauthenticated: {
609
+ enabled: true,
610
+ ttlMinutes: 60, // Lifetime before auto-delete (default 60)
611
+ maxUploadSizeInBytes: 25 * 1024 * 1024, // 25MB (separate from authenticated limit)
612
+ allowedMimeTypes: ["image/*", "application/pdf"],
613
+
614
+ // Optional abuse prevention (all fields optional)
615
+ abuse: {
616
+ perIp: { windowMinutes: 10, max: 20 }, // Max 20 uploads / 10 min per IP
617
+ hourlyPerIp: 60, // Max 60 uploads / hour per IP
618
+ maxConcurrent: 10, // Max simultaneous anonymous uploads (global)
619
+ maxLiveBytes: 2 * 1024 * 1024 * 1024, // Max total live temporary storage (global)
620
+ },
621
+ },
622
+ }
623
+ ```
624
+
625
+ > Anonymous uploads do **not** count against any user's quota and are stored with no owner.
626
+
627
+ ### 2. Client component
628
+
629
+ Pass the `allowUnauthenticated` prop to `DriveFileChooser`. It checks the visitor's auth status once on mount:
630
+
631
+ - **Logged in** → opens the normal drive explorer (browse and pick existing files or upload to their own drive).
632
+ - **Not logged in** → opens the native file picker and uploads anonymously, returning the uploaded `TDriveFile`.
633
+
634
+ ```tsx
635
+ import { useState } from "react";
636
+ import { DriveFileChooser } from "@muhgholy/next-drive/client";
637
+ import type { TDriveFile } from "@muhgholy/next-drive/client";
638
+
639
+ function GuestForm() {
640
+ const [file, setFile] = useState<TDriveFile | null>(null);
641
+
642
+ return (
643
+ <DriveFileChooser
644
+ allowUnauthenticated
645
+ value={file}
646
+ onChange={setFile}
647
+ accept="image/*"
648
+ placeholder="Upload an attachment"
649
+ />
650
+ );
651
+ }
652
+ ```
653
+
654
+ > `DriveFileChooser` must be inside a `DriveProvider` (it reads `apiEndpoint` and resolves auth from context). The `multiple` and `accept` props work the same as the standard chooser.
655
+
656
+ ### 3. Confirm to keep the file
657
+
658
+ After you persist the returned `drive.id` (e.g. attach it to a submitted form record), call `driveConfirm` to clear its expiry so it is never auto-deleted:
659
+
660
+ ```typescript
661
+ import { driveConfirm } from "@muhgholy/next-drive/server";
662
+
663
+ const kept = await driveConfirm(driveFile.id);
664
+ // kept === true if the file exists
665
+ ```
666
+
667
+ **Returns:** `Promise<boolean>` — `true` if the file exists. Safe to call on files uploaded while logged in (they are already permanent, so it is a harmless no-op) — so your form handler can always call `driveConfirm` regardless of whether the visitor was authenticated.
668
+
669
+ ### 4. Purge expired (unconfirmed) files
670
+
671
+ Run `drivePurgeExpired` on a schedule (cron job or interval) to permanently delete temporary files whose TTL has passed and were never confirmed:
672
+
673
+ ```typescript
674
+ import { drivePurgeExpired } from "@muhgholy/next-drive/server";
675
+
676
+ // e.g. in a cron route or a setInterval on your server
677
+ const result = await drivePurgeExpired();
678
+ console.log(`Purged ${result.removed.length} expired files, freed ${result.totalFreedInBytes} bytes`);
679
+ ```
680
+
681
+ **Returns:** `{ removed: string[], totalFreedInBytes: number }`
682
+
683
+ > [!IMPORTANT]
684
+ > Confirmed files are never purged. Only files that are both expired **and** unconfirmed are removed. If you never call `drivePurgeExpired`, temporary files will remain — schedule it (e.g. hourly).
685
+
686
+ ### Determining the client IP (behind a proxy)
687
+
688
+ Per-IP limits resolve the client IP from `cf-connecting-ip`, then the first entry of `x-forwarded-for`, then the socket address. Override the trusted headers or supply your own resolver:
689
+
690
+ ```typescript
691
+ unauthenticated: {
692
+ enabled: true,
693
+ maxUploadSizeInBytes: 25 * 1024 * 1024,
694
+ allowedMimeTypes: ["image/*"],
695
+ abuse: {
696
+ perIp: { windowMinutes: 10, max: 20 },
697
+ trustedHeaders: ["cf-connecting-ip", "x-forwarded-for"], // checked in order
698
+ clientId: (req) => (req.headers["cf-connecting-ip"] as string) ?? "unknown", // full override
699
+ },
700
+ }
701
+ ```
702
+
703
+ > [!WARNING]
704
+ > Only trust forwarded IP headers if every request reaches your origin through a proxy you control (e.g. Cloudflare). If clients can reach the origin directly, these headers can be spoofed — lock your origin down to your proxy's IPs.
705
+ >
706
+ > Abuse counters are kept in-memory per process. In a multi-instance deployment, each instance enforces limits independently; use a shared store (e.g. Redis) if you need cross-instance limits.
707
+
708
+ ---
709
+
585
710
  ## Configuration Options
586
711
 
587
712
  ### Security
@@ -596,9 +721,35 @@ security: {
596
721
  expiresIn: 3600, // seconds
597
722
  },
598
723
  trash: { retentionDays: 30 },
724
+ // Anonymous one-time uploads (see "Anonymous (One-Time) Uploads" section)
725
+ unauthenticated: {
726
+ enabled: true,
727
+ ttlMinutes: 60,
728
+ maxUploadSizeInBytes: 25 * 1024 * 1024,
729
+ allowedMimeTypes: ['image/*', 'application/pdf'],
730
+ abuse: {
731
+ perIp: { windowMinutes: 10, max: 20 },
732
+ hourlyPerIp: 60,
733
+ maxConcurrent: 10,
734
+ maxLiveBytes: 2 * 1024 * 1024 * 1024,
735
+ },
736
+ },
599
737
  }
600
738
  ```
601
739
 
740
+ | Option | Type | Default | Description |
741
+ | ------------------------------------- | ---------- | ----------- | ---------------------------------------------------------------- |
742
+ | `unauthenticated.enabled` | `boolean` | `false` | Enable anonymous one-time uploads |
743
+ | `unauthenticated.ttlMinutes` | `number` | `60` | Minutes before an unconfirmed upload becomes eligible for purge |
744
+ | `unauthenticated.maxUploadSizeInBytes`| `number` | — | Max size per anonymous upload (separate from authenticated) |
745
+ | `unauthenticated.allowedMimeTypes` | `string[]` | — | Allowed MIME types for anonymous uploads |
746
+ | `unauthenticated.abuse.perIp` | `{ windowMinutes, max }` | — | Sliding-window per-IP upload limit |
747
+ | `unauthenticated.abuse.hourlyPerIp` | `number` | — | Max anonymous uploads per IP per hour |
748
+ | `unauthenticated.abuse.maxConcurrent` | `number` | — | Max simultaneous anonymous uploads (global, per process) |
749
+ | `unauthenticated.abuse.maxLiveBytes` | `number` | — | Max total live temporary storage in bytes (global) |
750
+ | `unauthenticated.abuse.trustedHeaders`| `string[]` | `['cf-connecting-ip', 'x-forwarded-for']` | Headers checked in order to resolve client IP |
751
+ | `unauthenticated.abuse.clientId` | `(req) => string` | — | Full override for resolving the client identifier |
752
+
602
753
  ### CORS (Cross-Origin)
603
754
 
604
755
  Required when client and API are on different domains:
@@ -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) => {
@@ -2146,7 +2173,7 @@ var withSignedUrls = (items, config) => {
2146
2173
 
2147
2174
  // src/server/actions/drive.ts
2148
2175
  var handleDriveAction = async (ctx) => {
2149
- const { req, res, action, config, owner, isRootMode, information, provider, accountId } = ctx;
2176
+ const { req, res, action, config, owner, isRootMode, authenticated, information, provider, accountId } = ctx;
2150
2177
  switch (action) {
2151
2178
  case "list": {
2152
2179
  if (req.method !== "GET") return void res.status(405).json({ status: 405, message: "Listing files requires a GET request" });
@@ -2235,36 +2262,87 @@ 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) {
2273
+ cleanupTempFiles(files);
2274
+ return void res.status(403).json({ status: 403, message: "Anonymous uploads are not enabled" });
2275
+ }
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" });
2279
+ }
2280
+ if (fileType && !validateMimeType(fileType, unauth.allowedMimeTypes)) {
2245
2281
  cleanupTempFiles(files);
2246
2282
  return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2247
2283
  }
2248
- }
2249
- if (!isRootMode) {
2250
- const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2251
- if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
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 (!authenticated) {
2252
2316
  cleanupTempFiles(files);
2253
- return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
2317
+ return void res.status(401).json({ status: 401, message: "Authentication required to upload" });
2318
+ }
2319
+ if (fileType && config.security) {
2320
+ if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
2321
+ cleanupTempFiles(files);
2322
+ return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2323
+ }
2324
+ }
2325
+ if (!isRootMode) {
2326
+ const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2327
+ if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
2328
+ cleanupTempFiles(files);
2329
+ return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
2330
+ }
2254
2331
  }
2255
2332
  }
2256
2333
  currentUploadId = crypto3.randomUUID();
2257
2334
  const uploadDir2 = path.join(tempBaseDir, currentUploadId);
2258
2335
  fs.mkdirSync(uploadDir2, { recursive: true });
2259
2336
  const metadata = {
2260
- owner,
2261
- accountId,
2337
+ owner: unauthenticated ? null : owner,
2338
+ accountId: unauthenticated ? null : accountId,
2262
2339
  providerName: provider.name,
2263
2340
  name: fileName,
2264
- parentId: folderId === "root" || !folderId ? null : folderId,
2341
+ parentId: unauthenticated || folderId === "root" || !folderId ? null : folderId,
2265
2342
  fileSize: fileSizeInBytes,
2266
2343
  mimeType: fileType,
2267
- totalChunks
2344
+ totalChunks,
2345
+ unauthenticated: !!unauthenticated
2268
2346
  };
2269
2347
  fs.writeFileSync(path.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
2270
2348
  }
@@ -2350,7 +2428,8 @@ var handleDriveAction = async (ctx) => {
2350
2428
  information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
2351
2429
  status: "UPLOADING",
2352
2430
  currentChunk: totalChunks,
2353
- totalChunks
2431
+ totalChunks,
2432
+ expiresAt: meta.unauthenticated ? new Date(Date.now() + (config.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4) : null
2354
2433
  });
2355
2434
  if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
2356
2435
  drive.information.path = path.join("file", String(drive._id), "data.bin");
@@ -2359,10 +2438,12 @@ var handleDriveAction = async (ctx) => {
2359
2438
  try {
2360
2439
  const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
2361
2440
  fs.rmSync(uploadDir, { recursive: true, force: true });
2441
+ if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2362
2442
  const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
2363
2443
  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
2444
  } catch (err) {
2365
2445
  await drive_default.deleteOne({ _id: drive._id });
2446
+ if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2366
2447
  throw err;
2367
2448
  }
2368
2449
  } else {
@@ -2386,6 +2467,10 @@ var handleDriveAction = async (ctx) => {
2386
2467
  const tempUploadDir = path.join(os2.tmpdir(), "next-drive-uploads", id);
2387
2468
  if (fs.existsSync(tempUploadDir)) {
2388
2469
  try {
2470
+ const metaPath = path.join(tempUploadDir, "metadata.json");
2471
+ if (fs.existsSync(metaPath) && JSON.parse(fs.readFileSync(metaPath, "utf-8")).unauthenticated) {
2472
+ globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2473
+ }
2389
2474
  fs.rmSync(tempUploadDir, { recursive: true, force: true });
2390
2475
  } catch (e) {
2391
2476
  console.error("Failed to cleanup temp upload:", e);
@@ -2592,12 +2677,16 @@ var driveAPIHandler = async (req, res) => {
2592
2677
  if (wasPublicHandled) return;
2593
2678
  try {
2594
2679
  const mode = config.mode || "NORMAL";
2595
- const information = await getDriveInformation({ method: "REQUEST", req });
2596
- const { key: owner } = information;
2597
- const isRootMode = mode === "ROOT";
2598
2680
  if (action === "information") {
2599
2681
  const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
2600
2682
  const googleConfigured = !!(clientId && clientSecret && redirectUri);
2683
+ let authenticated2 = false;
2684
+ try {
2685
+ await getDriveInformation({ method: "REQUEST", req });
2686
+ authenticated2 = true;
2687
+ } catch {
2688
+ authenticated2 = false;
2689
+ }
2601
2690
  res.status(200).json({
2602
2691
  status: 200,
2603
2692
  message: "Information retrieved",
@@ -2605,11 +2694,27 @@ var driveAPIHandler = async (req, res) => {
2605
2694
  providers: {
2606
2695
  google: googleConfigured
2607
2696
  },
2608
- mode
2697
+ mode,
2698
+ authenticated: authenticated2,
2699
+ unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
2609
2700
  }
2610
2701
  });
2611
2702
  return;
2612
2703
  }
2704
+ const isRootMode = mode === "ROOT";
2705
+ let information;
2706
+ let authenticated = true;
2707
+ try {
2708
+ information = await getDriveInformation({ method: "REQUEST", req });
2709
+ } catch (err) {
2710
+ if ((action === "upload" || action === "cancel") && config.security?.unauthenticated?.enabled) {
2711
+ information = { key: null, storage: { quotaInBytes: 0 } };
2712
+ authenticated = false;
2713
+ } else {
2714
+ throw err;
2715
+ }
2716
+ }
2717
+ const { key: owner } = information;
2613
2718
  const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
2614
2719
  if (wasAuthHandled) return;
2615
2720
  const { provider, accountId } = await resolveProvider(req, owner);
@@ -2620,6 +2725,7 @@ var driveAPIHandler = async (req, res) => {
2620
2725
  config,
2621
2726
  owner,
2622
2727
  isRootMode,
2728
+ authenticated,
2623
2729
  information,
2624
2730
  provider,
2625
2731
  accountId
@@ -2631,6 +2737,6 @@ var driveAPIHandler = async (req, res) => {
2631
2737
  }
2632
2738
  };
2633
2739
 
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
2740
+ export { driveAPIHandler, driveCleanup, driveConfiguration, driveConfirm, driveDelete, driveFilePath, driveFileSchemaZod, driveGetUrl, driveInfo, driveList, driveListFiles, drivePurgeExpired, driveReadFile, driveUpload, drive_default, getDriveConfig, getDriveInformation };
2741
+ //# sourceMappingURL=chunk-26KZWPCF.js.map
2742
+ //# sourceMappingURL=chunk-26KZWPCF.js.map