@muhgholy/next-drive 4.23.23 → 4.23.25
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/dist/{chunk-IVSBLEMY.cjs → chunk-45FZNILV.cjs} +170 -1031
- package/dist/chunk-45FZNILV.cjs.map +1 -0
- package/dist/{chunk-CPMMHIQM.js → chunk-VBFXHZBY.js} +159 -1026
- package/dist/chunk-VBFXHZBY.js.map +1 -0
- package/dist/chunk-VTCBYFAK.cjs +765 -0
- package/dist/chunk-VTCBYFAK.cjs.map +1 -0
- package/dist/chunk-ZNWS7VJP.js +755 -0
- package/dist/chunk-ZNWS7VJP.js.map +1 -0
- package/dist/client/components/drive/dnd/context.d.ts.map +1 -1
- package/dist/client/components/drive/explorer.d.ts.map +1 -1
- package/dist/client/components/drive/file-grid.d.ts.map +1 -1
- package/dist/client/hooks/use-marquee.d.ts +14 -0
- package/dist/client/hooks/use-marquee.d.ts.map +1 -0
- package/dist/client/index.cjs +170 -150
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.js +169 -150
- package/dist/client/index.js.map +1 -1
- package/dist/client/upload.d.ts +9 -4
- package/dist/client/upload.d.ts.map +1 -1
- package/dist/server/actions/drive.d.ts.map +1 -1
- package/dist/server/controllers/drive.d.ts.map +1 -1
- package/dist/server/express.cjs +15 -14
- package/dist/server/express.cjs.map +1 -1
- package/dist/server/express.js +4 -2
- package/dist/server/express.js.map +1 -1
- package/dist/server/hono.cjs +15 -14
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.js +4 -2
- package/dist/server/hono.js.map +1 -1
- package/dist/server/index.cjs +28 -27
- package/dist/server/index.js +2 -1
- package/dist/server/tus.d.ts +17 -0
- package/dist/server/tus.d.ts.map +1 -0
- package/dist/server/zod/schemas.d.ts +0 -53
- package/dist/server/zod/schemas.d.ts.map +1 -1
- package/dist/tus-6OAOA454.js +3 -0
- package/dist/tus-6OAOA454.js.map +1 -0
- package/dist/tus-P67YULWB.cjs +16 -0
- package/dist/tus-P67YULWB.cjs.map +1 -0
- package/dist/types/server/api.d.ts +1 -1
- package/dist/types/server/api.d.ts.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-CPMMHIQM.js.map +0 -1
- package/dist/chunk-IVSBLEMY.cjs.map +0 -1
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import { getDriveConfig, getDriveInformation, resolveProvider, drive_default, GoogleDriveProvider, LocalStorageProvider, getImageSettings, account_default, withSignedUrl, withSignedUrls, handleUpload } from './chunk-VBFXHZBY.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
import { google } from 'googleapis';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { isValidObjectId } from 'mongoose';
|
|
9
|
+
|
|
10
|
+
// src/server/actions/cors.ts
|
|
11
|
+
var applyCorsHeaders = (req, res, config) => {
|
|
12
|
+
const cors = config.cors;
|
|
13
|
+
if (!cors?.enabled) return false;
|
|
14
|
+
const origin = req.headers.origin;
|
|
15
|
+
const allowedOrigins = cors.origins ?? "*";
|
|
16
|
+
const methods = cors.methods ?? ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
17
|
+
const allowedHeaders = cors.allowedHeaders ?? ["Content-Type", "Authorization", "X-Drive-Account", "Tus-Resumable", "Upload-Length", "Upload-Offset", "Upload-Metadata", "Upload-Concat", "Upload-Defer-Length"];
|
|
18
|
+
const exposedHeaders = cors.exposedHeaders ?? ["Content-Length", "Content-Type", "Content-Disposition", "Location", "Tus-Resumable", "Upload-Length", "Upload-Offset", "Upload-Metadata", "Upload-Expires", "X-Drive-Item"];
|
|
19
|
+
const credentials = cors.credentials ?? false;
|
|
20
|
+
const maxAge = cors.maxAge ?? 86400;
|
|
21
|
+
let allowOrigin = null;
|
|
22
|
+
if (origin) {
|
|
23
|
+
if (allowedOrigins === "*") {
|
|
24
|
+
allowOrigin = origin;
|
|
25
|
+
} else if (Array.isArray(allowedOrigins)) {
|
|
26
|
+
if (allowedOrigins.includes(origin)) {
|
|
27
|
+
allowOrigin = origin;
|
|
28
|
+
}
|
|
29
|
+
} else if (allowedOrigins === origin) {
|
|
30
|
+
allowOrigin = origin;
|
|
31
|
+
}
|
|
32
|
+
} else if (allowedOrigins === "*") {
|
|
33
|
+
allowOrigin = "*";
|
|
34
|
+
}
|
|
35
|
+
if (!allowOrigin) {
|
|
36
|
+
if (req.method === "OPTIONS") {
|
|
37
|
+
res.status(403).end();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
43
|
+
res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
|
|
44
|
+
res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
|
|
45
|
+
res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(", "));
|
|
46
|
+
res.setHeader("Access-Control-Max-Age", maxAge.toString());
|
|
47
|
+
if (credentials) {
|
|
48
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
49
|
+
}
|
|
50
|
+
if (req.method === "OPTIONS") {
|
|
51
|
+
res.status(204).end();
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/server/security/crypto-utils.ts
|
|
58
|
+
function sanitizeContentDispositionFilename(filename) {
|
|
59
|
+
const basename = filename.replace(/^.*[\\\/]/, "");
|
|
60
|
+
return basename.replace(/["\r\n]/g, "").replace(/[^\x20-\x7E]/g, "").slice(0, 255);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/server/actions/public.ts
|
|
64
|
+
var handlePublicAction = async (req, res, action, config) => {
|
|
65
|
+
if (action !== "serve" && action !== "thumbnail") {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const { id, token } = req.query;
|
|
70
|
+
if (!id || typeof id !== "string") {
|
|
71
|
+
res.status(400).json({ status: 400, message: "Could not open file: missing or invalid file ID" });
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const drive = await drive_default.findById(id);
|
|
75
|
+
if (!drive) {
|
|
76
|
+
res.status(404).json({ status: 404, message: "File not found or no longer available" });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (config.security?.signedUrls?.enabled) {
|
|
80
|
+
if (!token || typeof token !== "string") {
|
|
81
|
+
res.status(401).json({ status: 401, message: "Access denied: this link is missing its access token" });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const decoded = Buffer.from(token, "base64url").toString();
|
|
86
|
+
const [expiryStr, signature] = decoded.split(":");
|
|
87
|
+
const expiry = parseInt(expiryStr, 10);
|
|
88
|
+
if (Date.now() / 1e3 > expiry) {
|
|
89
|
+
res.status(401).json({ status: 401, message: "Access denied: this link has expired" });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
const { secret } = config.security.signedUrls;
|
|
93
|
+
const expectedSignature = crypto.createHmac("sha256", secret).update(`${id}:${expiry}`).digest("hex");
|
|
94
|
+
if (signature !== expectedSignature) {
|
|
95
|
+
res.status(401).json({ status: 401, message: "Access denied: this link's access token is invalid" });
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
res.status(401).json({ status: 401, message: "Access denied: this link's access token is malformed" });
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
104
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
105
|
+
if (action === "thumbnail") {
|
|
106
|
+
const stream2 = await itemProvider.getThumbnail(drive, itemAccountId);
|
|
107
|
+
res.setHeader("Content-Type", "image/webp");
|
|
108
|
+
if (config.cors?.enabled) {
|
|
109
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
110
|
+
}
|
|
111
|
+
stream2.pipe(res);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const { stream, mime, size: fileSize } = await itemProvider.openStream(drive, itemAccountId);
|
|
115
|
+
const safeFilename = sanitizeContentDispositionFilename(drive.name);
|
|
116
|
+
const format = req.query.format;
|
|
117
|
+
const quality = req.query.quality;
|
|
118
|
+
const display = req.query.display;
|
|
119
|
+
const sizePreset = req.query.size;
|
|
120
|
+
const fit = req.query.fit;
|
|
121
|
+
const position = req.query.position;
|
|
122
|
+
const isImage = mime.startsWith("image/");
|
|
123
|
+
const shouldTransform = isImage && (format || quality || display || sizePreset || fit);
|
|
124
|
+
res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
|
|
125
|
+
if (config.cors?.enabled) {
|
|
126
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
127
|
+
}
|
|
128
|
+
if (shouldTransform) {
|
|
129
|
+
try {
|
|
130
|
+
const settings = getImageSettings(fileSize, quality, display, sizePreset, fit, position);
|
|
131
|
+
let targetFormat = format || mime.split("/")[1];
|
|
132
|
+
if (targetFormat === "jpg") targetFormat = "jpeg";
|
|
133
|
+
if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
|
|
134
|
+
targetFormat = format || "webp";
|
|
135
|
+
}
|
|
136
|
+
const cacheDir = path.join(config.storage.path, "file", drive._id.toString(), "cache");
|
|
137
|
+
const cacheKey = [
|
|
138
|
+
"opt",
|
|
139
|
+
`q${settings.quality}`,
|
|
140
|
+
`e${settings.effort}`,
|
|
141
|
+
settings.width ? `${settings.width}x${settings.height}` : "orig",
|
|
142
|
+
settings.fit || "none",
|
|
143
|
+
settings.position || "c",
|
|
144
|
+
targetFormat
|
|
145
|
+
].join("_");
|
|
146
|
+
const cachePath = path.join(cacheDir, `${cacheKey}.bin`);
|
|
147
|
+
if (fs.existsSync(cachePath)) {
|
|
148
|
+
const cacheStat = fs.statSync(cachePath);
|
|
149
|
+
res.setHeader("Content-Type", `image/${targetFormat}`);
|
|
150
|
+
res.setHeader("Content-Length", cacheStat.size);
|
|
151
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
152
|
+
if (config.cors?.enabled) {
|
|
153
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
154
|
+
}
|
|
155
|
+
if ("destroy" in stream) {
|
|
156
|
+
stream.destroy();
|
|
157
|
+
}
|
|
158
|
+
fs.createReadStream(cachePath).pipe(res);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
162
|
+
let pipeline = sharp();
|
|
163
|
+
if (settings.width && settings.height) {
|
|
164
|
+
pipeline = pipeline.resize(settings.width, settings.height, {
|
|
165
|
+
fit: settings.fit || "inside",
|
|
166
|
+
position: settings.position || "center",
|
|
167
|
+
withoutEnlargement: true,
|
|
168
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (targetFormat === "jpeg") {
|
|
172
|
+
pipeline = pipeline.jpeg({ quality: settings.quality, mozjpeg: true });
|
|
173
|
+
res.setHeader("Content-Type", "image/jpeg");
|
|
174
|
+
} else if (targetFormat === "png") {
|
|
175
|
+
pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
|
|
176
|
+
res.setHeader("Content-Type", "image/png");
|
|
177
|
+
} else if (targetFormat === "webp") {
|
|
178
|
+
const webpEffort = Math.min(settings.effort, 6);
|
|
179
|
+
pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
|
|
180
|
+
res.setHeader("Content-Type", "image/webp");
|
|
181
|
+
} else if (targetFormat === "avif") {
|
|
182
|
+
pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
|
|
183
|
+
res.setHeader("Content-Type", "image/avif");
|
|
184
|
+
}
|
|
185
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
186
|
+
pipeline.on("error", (err) => {
|
|
187
|
+
console.error("[next-drive] Pipeline error:", err);
|
|
188
|
+
});
|
|
189
|
+
stream.pipe(pipeline);
|
|
190
|
+
pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
|
|
191
|
+
pipeline.clone().pipe(res);
|
|
192
|
+
return true;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.error("[next-drive] Image transformation failed:", e);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
res.setHeader("Content-Type", mime);
|
|
198
|
+
if (fileSize) res.setHeader("Content-Length", fileSize);
|
|
199
|
+
stream.pipe(res);
|
|
200
|
+
return true;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(`[next-drive] Error in ${action}:`, error);
|
|
203
|
+
const detail = error instanceof Error ? error.message : "Something went wrong while serving the file";
|
|
204
|
+
res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var handleAuthAction = async (req, res, action, config, owner) => {
|
|
209
|
+
if (!["getAuthUrl", "callback", "listAccounts", "removeAccount"].includes(action)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
switch (action) {
|
|
213
|
+
case "getAuthUrl": {
|
|
214
|
+
const { provider } = req.query;
|
|
215
|
+
if (provider === "GOOGLE") {
|
|
216
|
+
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
217
|
+
if (!clientId || !clientSecret || !redirectUri) {
|
|
218
|
+
res.status(500).json({ status: 500, message: "Google Drive is not configured on the server" });
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
const callbackUri = new URL(redirectUri);
|
|
222
|
+
callbackUri.searchParams.set("action", "callback");
|
|
223
|
+
const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
|
|
224
|
+
const state = Buffer.from(JSON.stringify({ owner })).toString("base64");
|
|
225
|
+
const url = oAuth2Client.generateAuthUrl({
|
|
226
|
+
access_type: "offline",
|
|
227
|
+
scope: ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email"],
|
|
228
|
+
state,
|
|
229
|
+
prompt: "consent"
|
|
230
|
+
});
|
|
231
|
+
res.status(200).json({ status: 200, message: "Auth URL generated", data: { url } });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
res.status(400).json({ status: 400, message: "Unknown storage provider requested" });
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
case "callback": {
|
|
238
|
+
const { code } = req.query;
|
|
239
|
+
if (!code) {
|
|
240
|
+
res.status(400).json({ status: 400, message: "Google sign-in failed: authorization code missing" });
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
244
|
+
if (!clientId || !clientSecret || !redirectUri) {
|
|
245
|
+
res.status(500).json({ status: 500, message: "Google Drive sign-in is not configured on the server" });
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
const callbackUri = new URL(redirectUri);
|
|
249
|
+
callbackUri.searchParams.set("action", "callback");
|
|
250
|
+
const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
|
|
251
|
+
const { tokens } = await oAuth2Client.getToken(code);
|
|
252
|
+
oAuth2Client.setCredentials(tokens);
|
|
253
|
+
const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client });
|
|
254
|
+
const userInfo = await oauth2.userinfo.get();
|
|
255
|
+
const existing = await account_default.findOne({ owner, "metadata.google.email": userInfo.data.email, "metadata.provider": "GOOGLE" });
|
|
256
|
+
if (existing) {
|
|
257
|
+
existing.metadata.google.credentials = tokens;
|
|
258
|
+
existing.markModified("metadata");
|
|
259
|
+
await existing.save();
|
|
260
|
+
} else {
|
|
261
|
+
const newAccount = new account_default({
|
|
262
|
+
owner,
|
|
263
|
+
name: userInfo.data.name || "Google Drive",
|
|
264
|
+
metadata: {
|
|
265
|
+
provider: "GOOGLE",
|
|
266
|
+
google: {
|
|
267
|
+
email: userInfo.data.email,
|
|
268
|
+
credentials: tokens
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
await newAccount.save();
|
|
273
|
+
}
|
|
274
|
+
res.setHeader("Content-Type", "text/html");
|
|
275
|
+
res.send(`<!DOCTYPE html>
|
|
276
|
+
<html>
|
|
277
|
+
<head><title>Authentication Complete</title></head>
|
|
278
|
+
<body>
|
|
279
|
+
<p>Authentication successful! This window will close automatically.</p>
|
|
280
|
+
<script>
|
|
281
|
+
(function() {
|
|
282
|
+
if (window.opener) {
|
|
283
|
+
try {
|
|
284
|
+
window.opener.postMessage('oauth-success', '*');
|
|
285
|
+
} catch (e) {}
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
localStorage.setItem('next-drive-oauth-success', Date.now().toString());
|
|
289
|
+
localStorage.removeItem('next-drive-oauth-success');
|
|
290
|
+
} catch (e) {}
|
|
291
|
+
window.close();
|
|
292
|
+
setTimeout(function() {
|
|
293
|
+
document.body.innerHTML = '<p style="font-family: system-ui; text-align: center; margin-top: 50px;">Authentication successful!<br>You can close this tab now.</p>';
|
|
294
|
+
}, 500);
|
|
295
|
+
})();
|
|
296
|
+
</script>
|
|
297
|
+
</body>
|
|
298
|
+
</html>`);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
case "listAccounts": {
|
|
302
|
+
const accounts = await account_default.find({ owner });
|
|
303
|
+
res.status(200).json({
|
|
304
|
+
status: 200,
|
|
305
|
+
data: {
|
|
306
|
+
accounts: accounts.map((a) => ({
|
|
307
|
+
id: a._id.toString(),
|
|
308
|
+
name: a.name,
|
|
309
|
+
email: a.metadata.google?.email || "",
|
|
310
|
+
provider: a.metadata.provider
|
|
311
|
+
}))
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
case "removeAccount": {
|
|
317
|
+
const { id } = req.query;
|
|
318
|
+
const account = await account_default.findOne({ _id: id, owner });
|
|
319
|
+
if (!account) {
|
|
320
|
+
res.status(404).json({ status: 404, message: "Could not disconnect: account not found" });
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (account.metadata.provider === "GOOGLE") {
|
|
324
|
+
try {
|
|
325
|
+
await GoogleDriveProvider.revokeToken(owner, account._id.toString());
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.error("Failed to revoke Google token:", e);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
await account_default.deleteOne({ _id: id, owner });
|
|
331
|
+
await drive_default.deleteMany({ owner, storageAccountId: id });
|
|
332
|
+
res.status(200).json({ status: 200, message: "Account removed" });
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
default:
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
var objectIdSchema = z.string().refine((val) => isValidObjectId(val), {
|
|
340
|
+
message: "Invalid ObjectId format"
|
|
341
|
+
});
|
|
342
|
+
var sanitizeFilename = (name) => {
|
|
343
|
+
return name.replace(/[<>:"|?*\x00-\x1F]/g, "").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\.\.\//g, "").replace(/\.\.+/g, "").split("/").pop() || "".trim().slice(0, 255);
|
|
344
|
+
};
|
|
345
|
+
var sanitizeRegexInput = (input) => {
|
|
346
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 100);
|
|
347
|
+
};
|
|
348
|
+
var nameSchema = z.string().min(1, "Name is required").max(255, "Name too long").transform(sanitizeFilename).refine((val) => val.length > 0, { message: "Invalid name after sanitization" });
|
|
349
|
+
var listQuerySchema = z.object({
|
|
350
|
+
folderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]),
|
|
351
|
+
limit: z.string().optional().transform((val) => {
|
|
352
|
+
const num = parseInt(val || "50", 10);
|
|
353
|
+
return Math.min(Math.max(1, num), 100);
|
|
354
|
+
}),
|
|
355
|
+
afterId: objectIdSchema.optional()
|
|
356
|
+
});
|
|
357
|
+
z.object({
|
|
358
|
+
id: objectIdSchema,
|
|
359
|
+
token: z.string().optional()
|
|
360
|
+
});
|
|
361
|
+
z.object({
|
|
362
|
+
id: objectIdSchema,
|
|
363
|
+
size: z.enum(["small", "medium", "large"]).optional().default("medium"),
|
|
364
|
+
token: z.string().optional()
|
|
365
|
+
});
|
|
366
|
+
var renameBodySchema = z.object({
|
|
367
|
+
id: objectIdSchema,
|
|
368
|
+
newName: nameSchema
|
|
369
|
+
});
|
|
370
|
+
var deleteQuerySchema = z.object({
|
|
371
|
+
id: objectIdSchema
|
|
372
|
+
});
|
|
373
|
+
z.object({
|
|
374
|
+
ids: z.array(objectIdSchema).min(1).max(1e3)
|
|
375
|
+
});
|
|
376
|
+
var createFolderBodySchema = z.object({
|
|
377
|
+
name: nameSchema,
|
|
378
|
+
parentId: z.union([z.literal("root"), objectIdSchema, z.string().length(0), z.undefined()]).optional()
|
|
379
|
+
});
|
|
380
|
+
var moveBodySchema = z.object({
|
|
381
|
+
ids: z.array(objectIdSchema).min(1).max(1e3),
|
|
382
|
+
targetFolderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]).optional()
|
|
383
|
+
});
|
|
384
|
+
var reorderBodySchema = z.object({
|
|
385
|
+
ids: z.array(objectIdSchema).min(1).max(1e3)
|
|
386
|
+
});
|
|
387
|
+
var searchQuerySchema = z.object({
|
|
388
|
+
q: z.string().min(1).max(100).transform(sanitizeRegexInput),
|
|
389
|
+
folderId: z.union([z.literal("root"), objectIdSchema, z.undefined()]).optional(),
|
|
390
|
+
limit: z.string().optional().transform((val) => {
|
|
391
|
+
const num = parseInt(val || "50", 10);
|
|
392
|
+
return Math.min(Math.max(1, num), 100);
|
|
393
|
+
}),
|
|
394
|
+
trashed: z.string().optional().transform((val) => val === "true")
|
|
395
|
+
});
|
|
396
|
+
z.object({
|
|
397
|
+
id: objectIdSchema
|
|
398
|
+
});
|
|
399
|
+
z.object({
|
|
400
|
+
days: z.number().int().min(1).max(365).optional()
|
|
401
|
+
});
|
|
402
|
+
var driveFileSchemaZod = z.object({
|
|
403
|
+
id: z.string(),
|
|
404
|
+
file: z.object({
|
|
405
|
+
name: z.string(),
|
|
406
|
+
mime: z.string(),
|
|
407
|
+
size: z.number()
|
|
408
|
+
})
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// src/server/actions/drive.ts
|
|
412
|
+
var handleDriveAction = async (ctx) => {
|
|
413
|
+
const { req, res, action, config, owner, isRootMode, authenticated, information, provider, accountId } = ctx;
|
|
414
|
+
switch (action) {
|
|
415
|
+
case "list": {
|
|
416
|
+
if (req.method !== "GET") return void res.status(405).json({ status: 405, message: "Listing files requires a GET request" });
|
|
417
|
+
const listQuery = listQuerySchema.safeParse(req.query);
|
|
418
|
+
if (!listQuery.success) return void res.status(400).json({ status: 400, message: "Could not list files: invalid request parameters" });
|
|
419
|
+
const { folderId, limit, afterId } = listQuery.data;
|
|
420
|
+
try {
|
|
421
|
+
await provider.sync(folderId || "root", owner, accountId);
|
|
422
|
+
} catch (e) {
|
|
423
|
+
console.error("Sync failed", e);
|
|
424
|
+
}
|
|
425
|
+
const query = {
|
|
426
|
+
"provider.type": provider.name,
|
|
427
|
+
storageAccountId: accountId || null,
|
|
428
|
+
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
429
|
+
trashedAt: null
|
|
430
|
+
};
|
|
431
|
+
if (!isRootMode) {
|
|
432
|
+
query.owner = owner;
|
|
433
|
+
}
|
|
434
|
+
if (afterId) query._id = { $lt: afterId };
|
|
435
|
+
const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
|
|
436
|
+
const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
|
|
437
|
+
res.status(200).json({ status: 200, message: "Items retrieved", data: { items: plainItems, hasMore: items.length === limit } });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
case "search": {
|
|
441
|
+
const searchData = searchQuerySchema.safeParse(req.query);
|
|
442
|
+
if (!searchData.success) return void res.status(400).json({ status: 400, message: "Could not search: invalid request parameters" });
|
|
443
|
+
const { q, folderId, limit, trashed } = searchData.data;
|
|
444
|
+
if (!trashed) {
|
|
445
|
+
try {
|
|
446
|
+
await provider.search(q, owner, accountId);
|
|
447
|
+
} catch (e) {
|
|
448
|
+
console.error("Search sync failed", e);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const query = {
|
|
452
|
+
"provider.type": provider.name,
|
|
453
|
+
storageAccountId: accountId || null,
|
|
454
|
+
trashedAt: trashed ? { $ne: null } : null,
|
|
455
|
+
name: { $regex: q, $options: "i" }
|
|
456
|
+
};
|
|
457
|
+
if (!isRootMode) {
|
|
458
|
+
query.owner = owner;
|
|
459
|
+
}
|
|
460
|
+
if (folderId && folderId !== "root") query.parentId = folderId;
|
|
461
|
+
const items = await drive_default.find(query, {}, { limit, sort: { createdAt: -1 } });
|
|
462
|
+
const plainItems = withSignedUrls(await Promise.all(items.map((i) => i.toClient())), config);
|
|
463
|
+
res.status(200).json({ status: 200, message: "Results", data: { items: plainItems } });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
case "upload": {
|
|
467
|
+
await handleUpload(req, res, { config, owner, authenticated, isRootMode, information, provider, accountId });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
case "createFolder": {
|
|
471
|
+
const folderData = createFolderBodySchema.safeParse(req.body);
|
|
472
|
+
if (!folderData.success) return void res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
|
|
473
|
+
const { name, parentId } = folderData.data;
|
|
474
|
+
const item = withSignedUrl(await provider.createFolder(name, parentId ?? null, owner, accountId), config);
|
|
475
|
+
res.status(201).json({ status: 201, message: "Folder created", data: { item } });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
case "delete": {
|
|
479
|
+
const deleteData = deleteQuerySchema.safeParse(req.query);
|
|
480
|
+
if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not move to trash: invalid ID" });
|
|
481
|
+
const { id } = deleteData.data;
|
|
482
|
+
const drive = await drive_default.findById(id);
|
|
483
|
+
if (!drive) return void res.status(404).json({ status: 404, message: "Could not move to trash: item not found" });
|
|
484
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
485
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
486
|
+
try {
|
|
487
|
+
await itemProvider.trash([id], owner, itemAccountId);
|
|
488
|
+
} catch (e) {
|
|
489
|
+
console.error("Provider trash failed:", e);
|
|
490
|
+
}
|
|
491
|
+
drive.trashedAt = /* @__PURE__ */ new Date();
|
|
492
|
+
await drive.save();
|
|
493
|
+
res.status(200).json({ status: 200, message: "Moved to trash", data: null });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
case "deletePermanent": {
|
|
497
|
+
const deleteData = deleteQuerySchema.safeParse(req.query);
|
|
498
|
+
if (!deleteData.success) return void res.status(400).json({ status: 400, message: "Could not delete: invalid ID" });
|
|
499
|
+
const { id } = deleteData.data;
|
|
500
|
+
await provider.delete([id], owner, accountId);
|
|
501
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
502
|
+
res.status(200).json({ status: 200, message: "Deleted", statistic: { storage: quota } });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
case "quota": {
|
|
506
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
507
|
+
res.status(200).json({
|
|
508
|
+
status: 200,
|
|
509
|
+
message: "Quota retrieved",
|
|
510
|
+
data: {
|
|
511
|
+
usedInBytes: quota.usedInBytes,
|
|
512
|
+
totalInBytes: quota.quotaInBytes,
|
|
513
|
+
availableInBytes: Math.max(0, quota.quotaInBytes - quota.usedInBytes),
|
|
514
|
+
percentage: quota.quotaInBytes > 0 ? Math.round(quota.usedInBytes / quota.quotaInBytes * 100) : 0
|
|
515
|
+
},
|
|
516
|
+
statistic: { storage: quota }
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
case "trash": {
|
|
521
|
+
try {
|
|
522
|
+
const { provider: trashProvider, accountId: trashAccountId } = await resolveProvider(req, owner);
|
|
523
|
+
await trashProvider.syncTrash(owner, trashAccountId);
|
|
524
|
+
} catch (e) {
|
|
525
|
+
console.error("Trash sync failed", e);
|
|
526
|
+
}
|
|
527
|
+
const query = {
|
|
528
|
+
owner,
|
|
529
|
+
"provider.type": provider.name,
|
|
530
|
+
storageAccountId: accountId || null,
|
|
531
|
+
trashedAt: { $ne: null }
|
|
532
|
+
};
|
|
533
|
+
const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
|
|
534
|
+
const plainItems = withSignedUrls(await Promise.all(items.map((item) => item.toClient())), config);
|
|
535
|
+
res.status(200).json({ status: 200, message: "Trash items", data: { items: plainItems, hasMore: false } });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
case "restore": {
|
|
539
|
+
const restoreData = deleteQuerySchema.safeParse(req.query);
|
|
540
|
+
if (!restoreData.success) return void res.status(400).json({ status: 400, message: "Could not restore: invalid ID" });
|
|
541
|
+
const { id } = restoreData.data;
|
|
542
|
+
const drive = await drive_default.findById(id);
|
|
543
|
+
if (!drive) return void res.status(404).json({ status: 404, message: "Could not restore: item not found" });
|
|
544
|
+
let targetParentId = drive.parentId;
|
|
545
|
+
if (targetParentId) {
|
|
546
|
+
const parent = await drive_default.findById(targetParentId);
|
|
547
|
+
if (parent?.trashedAt) {
|
|
548
|
+
targetParentId = null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
552
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
553
|
+
try {
|
|
554
|
+
await itemProvider.untrash([id], owner, itemAccountId);
|
|
555
|
+
if (targetParentId !== drive.parentId) {
|
|
556
|
+
await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
|
|
557
|
+
}
|
|
558
|
+
} catch (e) {
|
|
559
|
+
console.error("Provider restore failed:", e);
|
|
560
|
+
}
|
|
561
|
+
drive.trashedAt = null;
|
|
562
|
+
drive.parentId = targetParentId;
|
|
563
|
+
await drive.save();
|
|
564
|
+
res.status(200).json({
|
|
565
|
+
status: 200,
|
|
566
|
+
message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
|
|
567
|
+
data: null
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
case "move": {
|
|
572
|
+
const moveData = moveBodySchema.safeParse(req.body);
|
|
573
|
+
if (!moveData.success) return void res.status(400).json({ status: 400, message: "Could not move: invalid request data" });
|
|
574
|
+
const { ids, targetFolderId } = moveData.data;
|
|
575
|
+
const items = [];
|
|
576
|
+
const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
|
|
577
|
+
for (const id of ids) {
|
|
578
|
+
try {
|
|
579
|
+
const item = await provider.move(id, effectiveTargetId, owner, accountId);
|
|
580
|
+
items.push(item);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
console.error(`Failed to move item ${id}`, e);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
res.status(200).json({ status: 200, message: "Moved", data: { items: withSignedUrls(items, config) } });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
case "reorder": {
|
|
589
|
+
if (req.method !== "POST") {
|
|
590
|
+
return void res.status(405).json({ status: 405, message: "Reordering requires a POST request" });
|
|
591
|
+
}
|
|
592
|
+
const reorderData = reorderBodySchema.safeParse(req.body);
|
|
593
|
+
if (!reorderData.success) {
|
|
594
|
+
return void res.status(400).json({ status: 400, message: "Could not reorder: invalid request data" });
|
|
595
|
+
}
|
|
596
|
+
const { ids } = reorderData.data;
|
|
597
|
+
const query = {
|
|
598
|
+
_id: { $in: ids },
|
|
599
|
+
"provider.type": provider.name,
|
|
600
|
+
storageAccountId: accountId || null,
|
|
601
|
+
trashedAt: null
|
|
602
|
+
};
|
|
603
|
+
if (!isRootMode) {
|
|
604
|
+
query.owner = owner;
|
|
605
|
+
}
|
|
606
|
+
const existingItems = await drive_default.find(query, { _id: 1, parentId: 1 });
|
|
607
|
+
if (existingItems.length !== ids.length) {
|
|
608
|
+
return void res.status(404).json({ status: 404, message: "Could not reorder: one or more items were not found" });
|
|
609
|
+
}
|
|
610
|
+
const parentIds = new Set(existingItems.map((item) => item.parentId ? item.parentId.toString() : "root"));
|
|
611
|
+
if (parentIds.size > 1) {
|
|
612
|
+
return void res.status(400).json({ status: 400, message: "Could not reorder: all items must be in the same folder" });
|
|
613
|
+
}
|
|
614
|
+
const operations = ids.map((id, order) => ({
|
|
615
|
+
updateOne: {
|
|
616
|
+
filter: {
|
|
617
|
+
_id: id,
|
|
618
|
+
"provider.type": provider.name,
|
|
619
|
+
storageAccountId: accountId || null,
|
|
620
|
+
trashedAt: null,
|
|
621
|
+
...isRootMode ? {} : { owner }
|
|
622
|
+
},
|
|
623
|
+
update: { $set: { order } }
|
|
624
|
+
}
|
|
625
|
+
}));
|
|
626
|
+
await drive_default.bulkWrite(operations);
|
|
627
|
+
const updatedItems = await drive_default.find(query, {}, { sort: { order: 1 } });
|
|
628
|
+
const plainItems = withSignedUrls(await Promise.all(updatedItems.map((item) => item.toClient())), config);
|
|
629
|
+
res.status(200).json({ status: 200, message: "Reordered", data: { items: plainItems } });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
case "rename": {
|
|
633
|
+
const renameData = renameBodySchema.safeParse({ id: req.query.id, ...req.body });
|
|
634
|
+
if (!renameData.success) return void res.status(400).json({ status: 400, message: "Could not rename: invalid request data" });
|
|
635
|
+
const { id, newName } = renameData.data;
|
|
636
|
+
const item = withSignedUrl(await provider.rename(id, newName, owner, accountId), config);
|
|
637
|
+
res.status(200).json({ status: 200, message: "Renamed", data: { item } });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
default: {
|
|
641
|
+
res.status(400).json({ status: 400, message: `Unknown action requested: "${action}"` });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/server/index.ts
|
|
648
|
+
var parseJsonBody = (req) => {
|
|
649
|
+
return new Promise((resolve) => {
|
|
650
|
+
try {
|
|
651
|
+
const chunks = [];
|
|
652
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
653
|
+
req.on("end", () => {
|
|
654
|
+
const raw = Buffer.concat(chunks).toString();
|
|
655
|
+
if (!raw) return resolve({});
|
|
656
|
+
try {
|
|
657
|
+
resolve(JSON.parse(raw));
|
|
658
|
+
} catch {
|
|
659
|
+
resolve({});
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
req.on("error", () => resolve({}));
|
|
663
|
+
} catch {
|
|
664
|
+
resolve({});
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
};
|
|
668
|
+
var driveAPIHandler = async (req, res) => {
|
|
669
|
+
if (req.body === void 0 && typeof req.on === "function" && (req.headers["content-type"] || "").includes("application/json")) {
|
|
670
|
+
req.body = await parseJsonBody(req);
|
|
671
|
+
} else if (req.body === void 0) {
|
|
672
|
+
req.body = {};
|
|
673
|
+
}
|
|
674
|
+
const action = req.query.action || (req.query.code && req.query.state ? "callback" : void 0);
|
|
675
|
+
let config;
|
|
676
|
+
try {
|
|
677
|
+
config = getDriveConfig();
|
|
678
|
+
} catch (error) {
|
|
679
|
+
console.error("[next-drive] Configuration error:", error);
|
|
680
|
+
res.status(500).json({ status: 500, message: "Drive is not ready: failed to initialize configuration" });
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const isPreflightHandled = applyCorsHeaders(req, res, config);
|
|
684
|
+
if (isPreflightHandled) return;
|
|
685
|
+
if (!action) {
|
|
686
|
+
res.status(400).json({ status: 400, message: 'Missing "action" parameter in request' });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const wasPublicHandled = await handlePublicAction(req, res, action, config);
|
|
690
|
+
if (wasPublicHandled) return;
|
|
691
|
+
try {
|
|
692
|
+
const mode = config.mode || "NORMAL";
|
|
693
|
+
if (action === "information") {
|
|
694
|
+
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
695
|
+
const googleConfigured = !!(clientId && clientSecret && redirectUri);
|
|
696
|
+
let authenticated2 = false;
|
|
697
|
+
try {
|
|
698
|
+
await getDriveInformation({ method: "REQUEST", req });
|
|
699
|
+
authenticated2 = true;
|
|
700
|
+
} catch {
|
|
701
|
+
authenticated2 = false;
|
|
702
|
+
}
|
|
703
|
+
res.status(200).json({
|
|
704
|
+
status: 200,
|
|
705
|
+
message: "Information retrieved",
|
|
706
|
+
data: {
|
|
707
|
+
providers: {
|
|
708
|
+
google: googleConfigured
|
|
709
|
+
},
|
|
710
|
+
mode,
|
|
711
|
+
authenticated: authenticated2,
|
|
712
|
+
unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const isRootMode = mode === "ROOT";
|
|
718
|
+
let information;
|
|
719
|
+
let authenticated = true;
|
|
720
|
+
try {
|
|
721
|
+
information = await getDriveInformation({ method: "REQUEST", req });
|
|
722
|
+
} catch (err) {
|
|
723
|
+
if (action === "upload" && config.security?.unauthenticated?.enabled) {
|
|
724
|
+
information = { key: null, storage: { quotaInBytes: 0 } };
|
|
725
|
+
authenticated = false;
|
|
726
|
+
} else {
|
|
727
|
+
throw err;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const { key: owner } = information;
|
|
731
|
+
const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
|
|
732
|
+
if (wasAuthHandled) return;
|
|
733
|
+
const { provider, accountId } = await resolveProvider(req, owner);
|
|
734
|
+
await handleDriveAction({
|
|
735
|
+
req,
|
|
736
|
+
res,
|
|
737
|
+
action,
|
|
738
|
+
config,
|
|
739
|
+
owner,
|
|
740
|
+
isRootMode,
|
|
741
|
+
authenticated,
|
|
742
|
+
information,
|
|
743
|
+
provider,
|
|
744
|
+
accountId
|
|
745
|
+
});
|
|
746
|
+
} catch (error) {
|
|
747
|
+
console.error(`[next-drive] Error handling action ${action}:`, error);
|
|
748
|
+
const detail = error instanceof Error ? error.message : "Something went wrong while processing your request";
|
|
749
|
+
res.status(500).json({ status: 500, message: `Request "${action}" failed: ${detail}` });
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
export { driveAPIHandler, driveFileSchemaZod };
|
|
754
|
+
//# sourceMappingURL=chunk-ZNWS7VJP.js.map
|
|
755
|
+
//# sourceMappingURL=chunk-ZNWS7VJP.js.map
|