@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
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
|
-
-
|
|
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 (
|
|
2244
|
-
|
|
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
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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(
|
|
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-
|
|
2636
|
-
//# sourceMappingURL=chunk-
|
|
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
|