@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
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:
|
|
@@ -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) => {
|
|
@@ -2248,36 +2275,83 @@ 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 (
|
|
2257
|
-
|
|
2283
|
+
if (unauthenticated) {
|
|
2284
|
+
const unauth = config.security?.unauthenticated;
|
|
2285
|
+
if (!unauth?.enabled) {
|
|
2258
2286
|
cleanupTempFiles(files);
|
|
2259
|
-
return void res.status(
|
|
2287
|
+
return void res.status(403).json({ status: 403, message: "Anonymous uploads are not enabled" });
|
|
2260
2288
|
}
|
|
2261
|
-
|
|
2262
|
-
if (!isRootMode) {
|
|
2263
|
-
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2264
|
-
if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
|
|
2289
|
+
if (fileSizeInBytes > unauth.maxUploadSizeInBytes) {
|
|
2265
2290
|
cleanupTempFiles(files);
|
|
2266
|
-
return void res.status(413).json({ status: 413, message: "Could not upload:
|
|
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)) {
|
|
2294
|
+
cleanupTempFiles(files);
|
|
2295
|
+
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2296
|
+
}
|
|
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 (fileType && config.security) {
|
|
2329
|
+
if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
|
|
2330
|
+
cleanupTempFiles(files);
|
|
2331
|
+
return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
if (!isRootMode) {
|
|
2335
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
2336
|
+
if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
|
|
2337
|
+
cleanupTempFiles(files);
|
|
2338
|
+
return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
|
|
2339
|
+
}
|
|
2267
2340
|
}
|
|
2268
2341
|
}
|
|
2269
2342
|
currentUploadId = crypto3__default.default.randomUUID();
|
|
2270
2343
|
const uploadDir2 = path__default.default.join(tempBaseDir, currentUploadId);
|
|
2271
2344
|
fs__default.default.mkdirSync(uploadDir2, { recursive: true });
|
|
2272
2345
|
const metadata = {
|
|
2273
|
-
owner,
|
|
2274
|
-
accountId,
|
|
2346
|
+
owner: unauthenticated ? null : owner,
|
|
2347
|
+
accountId: unauthenticated ? null : accountId,
|
|
2275
2348
|
providerName: provider.name,
|
|
2276
2349
|
name: fileName,
|
|
2277
|
-
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
2350
|
+
parentId: unauthenticated || folderId === "root" || !folderId ? null : folderId,
|
|
2278
2351
|
fileSize: fileSizeInBytes,
|
|
2279
2352
|
mimeType: fileType,
|
|
2280
|
-
totalChunks
|
|
2353
|
+
totalChunks,
|
|
2354
|
+
unauthenticated: !!unauthenticated
|
|
2281
2355
|
};
|
|
2282
2356
|
fs__default.default.writeFileSync(path__default.default.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
|
|
2283
2357
|
}
|
|
@@ -2363,7 +2437,8 @@ var handleDriveAction = async (ctx) => {
|
|
|
2363
2437
|
information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
|
|
2364
2438
|
status: "UPLOADING",
|
|
2365
2439
|
currentChunk: totalChunks,
|
|
2366
|
-
totalChunks
|
|
2440
|
+
totalChunks,
|
|
2441
|
+
expiresAt: meta.unauthenticated ? new Date(Date.now() + (config.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4) : null
|
|
2367
2442
|
});
|
|
2368
2443
|
if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
|
|
2369
2444
|
drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
|
|
@@ -2372,10 +2447,12 @@ var handleDriveAction = async (ctx) => {
|
|
|
2372
2447
|
try {
|
|
2373
2448
|
const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
|
|
2374
2449
|
fs__default.default.rmSync(uploadDir, { recursive: true, force: true });
|
|
2450
|
+
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2375
2451
|
const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
|
|
2376
2452
|
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
2453
|
} catch (err) {
|
|
2378
2454
|
await drive_default.deleteOne({ _id: drive._id });
|
|
2455
|
+
if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2379
2456
|
throw err;
|
|
2380
2457
|
}
|
|
2381
2458
|
} else {
|
|
@@ -2399,6 +2476,10 @@ var handleDriveAction = async (ctx) => {
|
|
|
2399
2476
|
const tempUploadDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads", id);
|
|
2400
2477
|
if (fs__default.default.existsSync(tempUploadDir)) {
|
|
2401
2478
|
try {
|
|
2479
|
+
const metaPath = path__default.default.join(tempUploadDir, "metadata.json");
|
|
2480
|
+
if (fs__default.default.existsSync(metaPath) && JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8")).unauthenticated) {
|
|
2481
|
+
globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
|
|
2482
|
+
}
|
|
2402
2483
|
fs__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
|
|
2403
2484
|
} catch (e) {
|
|
2404
2485
|
console.error("Failed to cleanup temp upload:", e);
|
|
@@ -2605,12 +2686,16 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2605
2686
|
if (wasPublicHandled) return;
|
|
2606
2687
|
try {
|
|
2607
2688
|
const mode = config.mode || "NORMAL";
|
|
2608
|
-
const information = await getDriveInformation({ method: "REQUEST", req });
|
|
2609
|
-
const { key: owner } = information;
|
|
2610
|
-
const isRootMode = mode === "ROOT";
|
|
2611
2689
|
if (action === "information") {
|
|
2612
2690
|
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
2613
2691
|
const googleConfigured = !!(clientId && clientSecret && redirectUri);
|
|
2692
|
+
let authenticated = false;
|
|
2693
|
+
try {
|
|
2694
|
+
await getDriveInformation({ method: "REQUEST", req });
|
|
2695
|
+
authenticated = true;
|
|
2696
|
+
} catch {
|
|
2697
|
+
authenticated = false;
|
|
2698
|
+
}
|
|
2614
2699
|
res.status(200).json({
|
|
2615
2700
|
status: 200,
|
|
2616
2701
|
message: "Information retrieved",
|
|
@@ -2618,11 +2703,16 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2618
2703
|
providers: {
|
|
2619
2704
|
google: googleConfigured
|
|
2620
2705
|
},
|
|
2621
|
-
mode
|
|
2706
|
+
mode,
|
|
2707
|
+
authenticated,
|
|
2708
|
+
unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
|
|
2622
2709
|
}
|
|
2623
2710
|
});
|
|
2624
2711
|
return;
|
|
2625
2712
|
}
|
|
2713
|
+
const information = await getDriveInformation({ method: "REQUEST", req });
|
|
2714
|
+
const { key: owner } = information;
|
|
2715
|
+
const isRootMode = mode === "ROOT";
|
|
2626
2716
|
const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
|
|
2627
2717
|
if (wasAuthHandled) return;
|
|
2628
2718
|
const { provider, accountId } = await resolveProvider(req, owner);
|
|
@@ -2647,6 +2737,7 @@ var driveAPIHandler = async (req, res) => {
|
|
|
2647
2737
|
exports.driveAPIHandler = driveAPIHandler;
|
|
2648
2738
|
exports.driveCleanup = driveCleanup;
|
|
2649
2739
|
exports.driveConfiguration = driveConfiguration;
|
|
2740
|
+
exports.driveConfirm = driveConfirm;
|
|
2650
2741
|
exports.driveDelete = driveDelete;
|
|
2651
2742
|
exports.driveFilePath = driveFilePath;
|
|
2652
2743
|
exports.driveFileSchemaZod = driveFileSchemaZod;
|
|
@@ -2654,10 +2745,11 @@ exports.driveGetUrl = driveGetUrl;
|
|
|
2654
2745
|
exports.driveInfo = driveInfo;
|
|
2655
2746
|
exports.driveList = driveList;
|
|
2656
2747
|
exports.driveListFiles = driveListFiles;
|
|
2748
|
+
exports.drivePurgeExpired = drivePurgeExpired;
|
|
2657
2749
|
exports.driveReadFile = driveReadFile;
|
|
2658
2750
|
exports.driveUpload = driveUpload;
|
|
2659
2751
|
exports.drive_default = drive_default;
|
|
2660
2752
|
exports.getDriveConfig = getDriveConfig;
|
|
2661
2753
|
exports.getDriveInformation = getDriveInformation;
|
|
2662
|
-
//# sourceMappingURL=chunk-
|
|
2663
|
-
//# sourceMappingURL=chunk-
|
|
2754
|
+
//# sourceMappingURL=chunk-V75PCJHT.cjs.map
|
|
2755
|
+
//# sourceMappingURL=chunk-V75PCJHT.cjs.map
|