@muhgholy/next-drive 3.5.0 → 3.7.0
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 +2 -0
- package/dist/chunk-7PTOIRBL.cjs +1804 -0
- package/dist/chunk-7PTOIRBL.cjs.map +1 -0
- package/dist/chunk-A65ZAA2Z.cjs +17 -0
- package/dist/chunk-A65ZAA2Z.cjs.map +1 -0
- package/dist/chunk-JEQ2X3Z6.cjs +12 -0
- package/dist/chunk-JEQ2X3Z6.cjs.map +1 -0
- package/dist/{chunk-YYSAE5S2.js → chunk-NKP44P4G.js} +28 -23
- package/dist/chunk-NKP44P4G.js.map +1 -0
- package/dist/chunk-XDAVDVO6.cjs +131 -0
- package/dist/chunk-XDAVDVO6.cjs.map +1 -0
- package/dist/client/index.cjs +3861 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/schemas.cjs +13 -0
- package/dist/schemas.cjs.map +1 -0
- package/dist/server/controllers/drive.d.ts +11 -2
- package/dist/server/controllers/drive.d.ts.map +1 -1
- package/dist/server/express.cjs +46 -0
- package/dist/server/express.cjs.map +1 -0
- package/dist/server/express.js +2 -2
- package/dist/server/index.cjs +66 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.js +1 -1
- package/package.json +9 -5
- package/dist/chunk-YYSAE5S2.js.map +0 -1
|
@@ -0,0 +1,1804 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkJEQ2X3Z6_cjs = require('./chunk-JEQ2X3Z6.cjs');
|
|
4
|
+
var formidable = require('formidable');
|
|
5
|
+
var path3 = require('path');
|
|
6
|
+
var fs4 = require('fs');
|
|
7
|
+
var os2 = require('os');
|
|
8
|
+
var mongoose2 = require('mongoose');
|
|
9
|
+
var crypto3 = require('crypto');
|
|
10
|
+
var sharp = require('sharp');
|
|
11
|
+
var zod = require('zod');
|
|
12
|
+
var ffmpeg = require('fluent-ffmpeg');
|
|
13
|
+
var googleapis = require('googleapis');
|
|
14
|
+
|
|
15
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
|
+
|
|
17
|
+
var formidable__default = /*#__PURE__*/_interopDefault(formidable);
|
|
18
|
+
var path3__default = /*#__PURE__*/_interopDefault(path3);
|
|
19
|
+
var fs4__default = /*#__PURE__*/_interopDefault(fs4);
|
|
20
|
+
var os2__default = /*#__PURE__*/_interopDefault(os2);
|
|
21
|
+
var mongoose2__default = /*#__PURE__*/_interopDefault(mongoose2);
|
|
22
|
+
var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
|
|
23
|
+
var sharp__default = /*#__PURE__*/_interopDefault(sharp);
|
|
24
|
+
var ffmpeg__default = /*#__PURE__*/_interopDefault(ffmpeg);
|
|
25
|
+
|
|
26
|
+
var globalConfig = null;
|
|
27
|
+
var driveConfiguration = (config) => {
|
|
28
|
+
if (mongoose2__default.default.connection.readyState !== 1) {
|
|
29
|
+
throw new Error("Database not connected. Please connect to Mongoose before initializing next-drive.");
|
|
30
|
+
}
|
|
31
|
+
const mergedConfig = {
|
|
32
|
+
...config,
|
|
33
|
+
security: {
|
|
34
|
+
maxUploadSizeInBytes: config.security?.maxUploadSizeInBytes ?? 10 * 1024 * 1024,
|
|
35
|
+
// Default to 10MB
|
|
36
|
+
allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
|
|
37
|
+
signedUrls: config.security?.signedUrls,
|
|
38
|
+
trash: config.security?.trash
|
|
39
|
+
},
|
|
40
|
+
information: config.information ?? (async (req) => {
|
|
41
|
+
return {
|
|
42
|
+
key: { id: "default-user" },
|
|
43
|
+
storage: { quotaInBytes: 10 * 1024 * 1024 * 1024 }
|
|
44
|
+
// Default to 10GB
|
|
45
|
+
};
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
globalConfig = mergedConfig;
|
|
49
|
+
return mergedConfig;
|
|
50
|
+
};
|
|
51
|
+
var getDriveConfig = () => {
|
|
52
|
+
if (!globalConfig) throw new Error("Drive configuration not initialized");
|
|
53
|
+
return globalConfig;
|
|
54
|
+
};
|
|
55
|
+
var getDriveInformation = async (req) => {
|
|
56
|
+
const config = getDriveConfig();
|
|
57
|
+
return config.information(req);
|
|
58
|
+
};
|
|
59
|
+
var informationSchema = new mongoose2.Schema({
|
|
60
|
+
type: { type: String, enum: ["FILE", "FOLDER"], required: true },
|
|
61
|
+
sizeInBytes: { type: Number, default: 0 },
|
|
62
|
+
mime: { type: String },
|
|
63
|
+
path: { type: String },
|
|
64
|
+
width: { type: Number },
|
|
65
|
+
height: { type: Number },
|
|
66
|
+
duration: { type: Number },
|
|
67
|
+
hash: { type: String }
|
|
68
|
+
}, { _id: false });
|
|
69
|
+
var providerSchema = new mongoose2.Schema({
|
|
70
|
+
type: { type: String, enum: ["LOCAL", "GOOGLE"], required: true, default: "LOCAL" },
|
|
71
|
+
google: { type: mongoose2.Schema.Types.Mixed }
|
|
72
|
+
}, { _id: false });
|
|
73
|
+
var DriveSchema = new mongoose2.Schema(
|
|
74
|
+
{
|
|
75
|
+
owner: { type: mongoose2.Schema.Types.Mixed, default: null },
|
|
76
|
+
storageAccountId: { type: mongoose2.Schema.Types.ObjectId, ref: "StorageAccount", default: null },
|
|
77
|
+
name: { type: String, required: true },
|
|
78
|
+
parentId: { type: mongoose2.Schema.Types.ObjectId, ref: "Drive", default: null },
|
|
79
|
+
order: { type: Number, default: 0 },
|
|
80
|
+
provider: { type: providerSchema, default: () => ({ type: "LOCAL" }) },
|
|
81
|
+
metadata: { type: mongoose2.Schema.Types.Mixed, default: {} },
|
|
82
|
+
information: { type: informationSchema, required: true },
|
|
83
|
+
status: { type: String, enum: ["READY", "PROCESSING", "UPLOADING", "FAILED"], default: "PROCESSING" },
|
|
84
|
+
trashedAt: { type: Date, default: null },
|
|
85
|
+
createdAt: { type: Date, default: Date.now }
|
|
86
|
+
},
|
|
87
|
+
{ minimize: false }
|
|
88
|
+
);
|
|
89
|
+
DriveSchema.index({ owner: 1, "information.type": 1 });
|
|
90
|
+
DriveSchema.index({ owner: 1, "provider.type": 1, "provider.google.id": 1 });
|
|
91
|
+
DriveSchema.index({ owner: 1, storageAccountId: 1 });
|
|
92
|
+
DriveSchema.index({ owner: 1, trashedAt: 1 });
|
|
93
|
+
DriveSchema.index({ owner: 1, "information.hash": 1 });
|
|
94
|
+
DriveSchema.index({ owner: 1, name: "text" });
|
|
95
|
+
DriveSchema.index({ owner: 1, "provider.type": 1 });
|
|
96
|
+
DriveSchema.method("toClient", async function() {
|
|
97
|
+
const data = this.toJSON();
|
|
98
|
+
return {
|
|
99
|
+
id: String(data._id),
|
|
100
|
+
name: data.name,
|
|
101
|
+
parentId: data.parentId ? String(data.parentId) : null,
|
|
102
|
+
order: data.order,
|
|
103
|
+
provider: data.provider,
|
|
104
|
+
metadata: data.metadata,
|
|
105
|
+
information: data.information,
|
|
106
|
+
status: data.status,
|
|
107
|
+
trashedAt: data.trashedAt,
|
|
108
|
+
createdAt: data.createdAt
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
var Drive = mongoose2__default.default.models.Drive || mongoose2__default.default.model("Drive", DriveSchema);
|
|
112
|
+
var drive_default = Drive;
|
|
113
|
+
var StorageAccountSchema = new mongoose2.Schema(
|
|
114
|
+
{
|
|
115
|
+
owner: { type: mongoose2.Schema.Types.Mixed, default: null },
|
|
116
|
+
name: { type: String, required: true },
|
|
117
|
+
metadata: {
|
|
118
|
+
provider: { type: String, enum: ["GOOGLE"], required: true },
|
|
119
|
+
google: {
|
|
120
|
+
email: { type: String, required: true },
|
|
121
|
+
credentials: { type: mongoose2.Schema.Types.Mixed, required: true }
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
createdAt: { type: Date, default: Date.now }
|
|
125
|
+
},
|
|
126
|
+
{ minimize: false }
|
|
127
|
+
);
|
|
128
|
+
StorageAccountSchema.index({ owner: 1, "metadata.provider": 1 });
|
|
129
|
+
StorageAccountSchema.index({ owner: 1, "metadata.google.email": 1 });
|
|
130
|
+
StorageAccountSchema.method("toClient", async function() {
|
|
131
|
+
const data = this.toJSON();
|
|
132
|
+
return {
|
|
133
|
+
id: String(data._id),
|
|
134
|
+
owner: data.owner,
|
|
135
|
+
name: data.name,
|
|
136
|
+
metadata: data.metadata,
|
|
137
|
+
createdAt: data.createdAt
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
var StorageAccount = mongoose2__default.default.models.StorageAccount || mongoose2__default.default.model("StorageAccount", StorageAccountSchema);
|
|
141
|
+
var account_default = StorageAccount;
|
|
142
|
+
var validateMimeType = (mime, allowedTypes) => {
|
|
143
|
+
if (allowedTypes.includes("*/*")) return true;
|
|
144
|
+
return allowedTypes.some((pattern) => {
|
|
145
|
+
if (pattern === mime) return true;
|
|
146
|
+
if (pattern.endsWith("/*")) {
|
|
147
|
+
const prefix = pattern.slice(0, -2);
|
|
148
|
+
return mime.startsWith(`${prefix}/`);
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
var computeFileHash = (filePath) => new Promise((resolve, reject) => {
|
|
154
|
+
const hash = crypto3__default.default.createHash("sha256");
|
|
155
|
+
const stream = fs4__default.default.createReadStream(filePath);
|
|
156
|
+
stream.on("data", (data) => hash.update(data));
|
|
157
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
158
|
+
stream.on("error", reject);
|
|
159
|
+
});
|
|
160
|
+
var extractImageMetadata = async (filePath) => {
|
|
161
|
+
try {
|
|
162
|
+
const { width = 0, height = 0, exif } = await sharp__default.default(filePath).metadata();
|
|
163
|
+
return { width, height, ...exif && { exif: { raw: exif.toString("base64") } } };
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
var objectIdSchema = zod.z.string().refine((val) => mongoose2.isValidObjectId(val), {
|
|
169
|
+
message: "Invalid ObjectId format"
|
|
170
|
+
});
|
|
171
|
+
var sanitizeFilename = (name) => {
|
|
172
|
+
return name.replace(/[<>:"|?*\x00-\x1F]/g, "").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\.\.\//g, "").replace(/\.\.+/g, "").split("/").pop() || "".trim().slice(0, 255);
|
|
173
|
+
};
|
|
174
|
+
var sanitizeRegexInput = (input) => {
|
|
175
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 100);
|
|
176
|
+
};
|
|
177
|
+
var nameSchema = zod.z.string().min(1, "Name is required").max(255, "Name too long").transform(sanitizeFilename).refine((val) => val.length > 0, { message: "Invalid name after sanitization" });
|
|
178
|
+
var uploadChunkSchema = zod.z.object({
|
|
179
|
+
chunkIndex: zod.z.number().int().min(0).max(1e4),
|
|
180
|
+
totalChunks: zod.z.number().int().min(1).max(1e4),
|
|
181
|
+
driveId: zod.z.string().optional(),
|
|
182
|
+
fileName: nameSchema,
|
|
183
|
+
fileSize: zod.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
184
|
+
fileType: zod.z.string().min(1).max(255),
|
|
185
|
+
folderId: zod.z.string().optional()
|
|
186
|
+
}).refine((data) => data.chunkIndex < data.totalChunks, {
|
|
187
|
+
message: "Chunk index must be less than total chunks"
|
|
188
|
+
});
|
|
189
|
+
var listQuerySchema = zod.z.object({
|
|
190
|
+
folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]),
|
|
191
|
+
limit: zod.z.string().optional().transform((val) => {
|
|
192
|
+
const num = parseInt(val || "50", 10);
|
|
193
|
+
return Math.min(Math.max(1, num), 100);
|
|
194
|
+
}),
|
|
195
|
+
afterId: objectIdSchema.optional()
|
|
196
|
+
});
|
|
197
|
+
var serveQuerySchema = zod.z.object({
|
|
198
|
+
id: objectIdSchema,
|
|
199
|
+
token: zod.z.string().optional(),
|
|
200
|
+
q: zod.z.enum(["ultralow", "low", "medium", "high", "normal"]).optional(),
|
|
201
|
+
format: zod.z.enum(["webp", "jpeg", "png"]).optional()
|
|
202
|
+
});
|
|
203
|
+
var thumbnailQuerySchema = zod.z.object({
|
|
204
|
+
id: objectIdSchema,
|
|
205
|
+
size: zod.z.enum(["small", "medium", "large"]).optional().default("medium"),
|
|
206
|
+
token: zod.z.string().optional()
|
|
207
|
+
});
|
|
208
|
+
var renameBodySchema = zod.z.object({
|
|
209
|
+
id: objectIdSchema,
|
|
210
|
+
newName: nameSchema
|
|
211
|
+
});
|
|
212
|
+
var deleteQuerySchema = zod.z.object({
|
|
213
|
+
id: objectIdSchema
|
|
214
|
+
});
|
|
215
|
+
zod.z.object({
|
|
216
|
+
ids: zod.z.array(objectIdSchema).min(1).max(1e3)
|
|
217
|
+
});
|
|
218
|
+
var createFolderBodySchema = zod.z.object({
|
|
219
|
+
name: nameSchema,
|
|
220
|
+
parentId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.string().length(0), zod.z.undefined()]).optional()
|
|
221
|
+
});
|
|
222
|
+
var moveBodySchema = zod.z.object({
|
|
223
|
+
ids: zod.z.array(objectIdSchema).min(1).max(1e3),
|
|
224
|
+
targetFolderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional()
|
|
225
|
+
});
|
|
226
|
+
zod.z.object({
|
|
227
|
+
ids: zod.z.array(objectIdSchema).min(1).max(1e3)
|
|
228
|
+
});
|
|
229
|
+
var searchQuerySchema = zod.z.object({
|
|
230
|
+
q: zod.z.string().min(1).max(100).transform(sanitizeRegexInput),
|
|
231
|
+
folderId: zod.z.union([zod.z.literal("root"), objectIdSchema, zod.z.undefined()]).optional(),
|
|
232
|
+
limit: zod.z.string().optional().transform((val) => {
|
|
233
|
+
const num = parseInt(val || "50", 10);
|
|
234
|
+
return Math.min(Math.max(1, num), 100);
|
|
235
|
+
}),
|
|
236
|
+
trashed: zod.z.string().optional().transform((val) => val === "true")
|
|
237
|
+
});
|
|
238
|
+
zod.z.object({
|
|
239
|
+
id: objectIdSchema
|
|
240
|
+
});
|
|
241
|
+
var cancelQuerySchema = zod.z.object({
|
|
242
|
+
id: zod.z.string().uuid()
|
|
243
|
+
});
|
|
244
|
+
zod.z.object({
|
|
245
|
+
days: zod.z.number().int().min(1).max(365).optional()
|
|
246
|
+
});
|
|
247
|
+
var driveFileSchemaZod = zod.z.object({
|
|
248
|
+
id: zod.z.string(),
|
|
249
|
+
file: zod.z.object({
|
|
250
|
+
name: zod.z.string(),
|
|
251
|
+
mime: zod.z.string(),
|
|
252
|
+
size: zod.z.number()
|
|
253
|
+
})
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// src/server/security/cryptoUtils.ts
|
|
257
|
+
function sanitizeContentDispositionFilename(filename) {
|
|
258
|
+
const basename = filename.replace(/^.*[\\\/]/, "");
|
|
259
|
+
return basename.replace(/["\r\n]/g, "").replace(/[^\x20-\x7E]/g, "").slice(0, 255);
|
|
260
|
+
}
|
|
261
|
+
var LocalStorageProvider = {
|
|
262
|
+
name: "LOCAL",
|
|
263
|
+
sync: async (folderId, owner, accountId) => {
|
|
264
|
+
},
|
|
265
|
+
search: async (query, owner, accountId) => {
|
|
266
|
+
},
|
|
267
|
+
getQuota: async (owner, accountId, configuredQuotaInBytes) => {
|
|
268
|
+
const result = await drive_default.aggregate([
|
|
269
|
+
{
|
|
270
|
+
$match: {
|
|
271
|
+
owner,
|
|
272
|
+
"information.type": "FILE",
|
|
273
|
+
trashedAt: null,
|
|
274
|
+
"provider.type": "LOCAL",
|
|
275
|
+
storageAccountId: accountId || null
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{ $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }
|
|
279
|
+
]);
|
|
280
|
+
const usedInBytes = result[0]?.total || 0;
|
|
281
|
+
return { usedInBytes, quotaInBytes: configuredQuotaInBytes ?? 0 };
|
|
282
|
+
},
|
|
283
|
+
openStream: async (item, accountId) => {
|
|
284
|
+
if (item.information.type !== "FILE") throw new Error("Cannot stream folder");
|
|
285
|
+
const filePath = path3__default.default.join(getDriveConfig().storage.path, item.information.path);
|
|
286
|
+
if (!fs4__default.default.existsSync(filePath)) {
|
|
287
|
+
throw new Error("File not found on disk");
|
|
288
|
+
}
|
|
289
|
+
const stat = fs4__default.default.statSync(filePath);
|
|
290
|
+
const stream = fs4__default.default.createReadStream(filePath);
|
|
291
|
+
return {
|
|
292
|
+
stream,
|
|
293
|
+
mime: item.information.mime,
|
|
294
|
+
size: stat.size
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
getThumbnail: async (item, accountId) => {
|
|
298
|
+
if (item.information.type !== "FILE") throw new Error("No thumbnail for folder");
|
|
299
|
+
const storagePath = getDriveConfig().storage.path;
|
|
300
|
+
const originalPath = path3__default.default.join(storagePath, item.information.path);
|
|
301
|
+
const thumbPath = path3__default.default.join(storagePath, "cache", "thumbnails", `${item._id.toString()}.webp`);
|
|
302
|
+
if (!fs4__default.default.existsSync(originalPath)) throw new Error("Original file not found");
|
|
303
|
+
if (fs4__default.default.existsSync(thumbPath)) {
|
|
304
|
+
return fs4__default.default.createReadStream(thumbPath);
|
|
305
|
+
}
|
|
306
|
+
if (!fs4__default.default.existsSync(path3__default.default.dirname(thumbPath))) fs4__default.default.mkdirSync(path3__default.default.dirname(thumbPath), { recursive: true });
|
|
307
|
+
if (item.information.mime.startsWith("image/")) {
|
|
308
|
+
await sharp__default.default(originalPath).resize(300, 300, { fit: "inside" }).toFormat("webp", { quality: 80 }).toFile(thumbPath);
|
|
309
|
+
} else if (item.information.mime.startsWith("video/")) {
|
|
310
|
+
await new Promise((resolve, reject) => {
|
|
311
|
+
ffmpeg__default.default(originalPath).screenshots({
|
|
312
|
+
count: 1,
|
|
313
|
+
folder: path3__default.default.dirname(thumbPath),
|
|
314
|
+
filename: path3__default.default.basename(thumbPath),
|
|
315
|
+
size: "300x?"
|
|
316
|
+
}).on("end", resolve).on("error", reject);
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
throw new Error("Unsupported mime type for thumbnail");
|
|
320
|
+
}
|
|
321
|
+
return fs4__default.default.createReadStream(thumbPath);
|
|
322
|
+
},
|
|
323
|
+
createFolder: async (name, parentId, owner, accountId) => {
|
|
324
|
+
const getNextOrderValue2 = async (owner2) => {
|
|
325
|
+
const lastItem = await drive_default.findOne({ owner: owner2 }, {}, { sort: { order: -1 } });
|
|
326
|
+
return lastItem ? lastItem.order + 1 : 0;
|
|
327
|
+
};
|
|
328
|
+
const folder = new drive_default({
|
|
329
|
+
owner,
|
|
330
|
+
name,
|
|
331
|
+
parentId: parentId === "root" || !parentId ? null : parentId,
|
|
332
|
+
order: await getNextOrderValue2(owner),
|
|
333
|
+
provider: { type: "LOCAL" },
|
|
334
|
+
information: { type: "FOLDER" },
|
|
335
|
+
status: "READY"
|
|
336
|
+
});
|
|
337
|
+
await folder.save();
|
|
338
|
+
return folder.toClient();
|
|
339
|
+
},
|
|
340
|
+
uploadFile: async (drive, filePath, accountId) => {
|
|
341
|
+
if (drive.information.type !== "FILE") throw new Error("Invalid drive type");
|
|
342
|
+
const storagePath = getDriveConfig().storage.path;
|
|
343
|
+
const destPath = path3__default.default.join(storagePath, drive.information.path);
|
|
344
|
+
const dirPath = path3__default.default.dirname(destPath);
|
|
345
|
+
if (!fs4__default.default.existsSync(filePath)) {
|
|
346
|
+
throw new Error("Source file not found");
|
|
347
|
+
}
|
|
348
|
+
if (!fs4__default.default.existsSync(dirPath)) {
|
|
349
|
+
fs4__default.default.mkdirSync(dirPath, { recursive: true });
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
fs4__default.default.renameSync(filePath, destPath);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (err instanceof Error && "code" in err && err.code === "EXDEV") {
|
|
355
|
+
fs4__default.default.copyFileSync(filePath, destPath);
|
|
356
|
+
fs4__default.default.unlinkSync(filePath);
|
|
357
|
+
} else {
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!fs4__default.default.existsSync(destPath)) {
|
|
362
|
+
throw new Error("Failed to write file to destination");
|
|
363
|
+
}
|
|
364
|
+
const destStats = fs4__default.default.statSync(destPath);
|
|
365
|
+
if (destStats.size !== drive.information.sizeInBytes) {
|
|
366
|
+
fs4__default.default.unlinkSync(destPath);
|
|
367
|
+
throw new Error(`Destination file size mismatch: expected ${drive.information.sizeInBytes}, got ${destStats.size}`);
|
|
368
|
+
}
|
|
369
|
+
drive.status = "READY";
|
|
370
|
+
drive.information.hash = await computeFileHash(destPath);
|
|
371
|
+
if (drive.information.mime.startsWith("image/")) {
|
|
372
|
+
const meta = await extractImageMetadata(destPath);
|
|
373
|
+
if (meta) {
|
|
374
|
+
drive.information.width = meta.width;
|
|
375
|
+
drive.information.height = meta.height;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
await drive.save();
|
|
379
|
+
return drive.toClient();
|
|
380
|
+
},
|
|
381
|
+
delete: async (ids, owner, accountId) => {
|
|
382
|
+
const items = await drive_default.find({ _id: { $in: ids }, owner }).lean();
|
|
383
|
+
const getAllChildren = async (folderIds2) => {
|
|
384
|
+
const children = await drive_default.find({ parentId: { $in: folderIds2 }, owner }).lean();
|
|
385
|
+
if (children.length === 0) return [];
|
|
386
|
+
const subFolderIds = children.filter((c) => c.information.type === "FOLDER").map((c) => c._id.toString());
|
|
387
|
+
const subChildren = await getAllChildren(subFolderIds);
|
|
388
|
+
return [...children, ...subChildren];
|
|
389
|
+
};
|
|
390
|
+
const folderIds = items.filter((i) => i.information.type === "FOLDER").map((i) => i._id.toString());
|
|
391
|
+
const allChildren = await getAllChildren(folderIds);
|
|
392
|
+
const allItemsToDelete = [...items, ...allChildren];
|
|
393
|
+
for (const item of allItemsToDelete) {
|
|
394
|
+
if (item.information.type === "FILE" && item.information.path) {
|
|
395
|
+
const fullPath = path3__default.default.join(getDriveConfig().storage.path, item.information.path);
|
|
396
|
+
const dirPath = path3__default.default.dirname(fullPath);
|
|
397
|
+
if (fs4__default.default.existsSync(dirPath)) {
|
|
398
|
+
fs4__default.default.rmSync(dirPath, { recursive: true, force: true });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
await drive_default.deleteMany({ _id: { $in: allItemsToDelete.map((i) => i._id) } });
|
|
403
|
+
},
|
|
404
|
+
trash: async (ids, owner, accountId) => {
|
|
405
|
+
},
|
|
406
|
+
syncTrash: async (owner, accountId) => {
|
|
407
|
+
},
|
|
408
|
+
untrash: async (ids, owner, accountId) => {
|
|
409
|
+
},
|
|
410
|
+
rename: async (id, newName, owner, accountId) => {
|
|
411
|
+
const item = await drive_default.findOneAndUpdate({ _id: id, owner }, { name: newName }, { new: true });
|
|
412
|
+
if (!item) throw new Error("Item not found");
|
|
413
|
+
return item.toClient();
|
|
414
|
+
},
|
|
415
|
+
move: async (id, newParentId, owner, accountId) => {
|
|
416
|
+
const item = await drive_default.findOne({ _id: id, owner });
|
|
417
|
+
if (!item) throw new Error("Item not found");
|
|
418
|
+
item.parentId;
|
|
419
|
+
item.parentId = newParentId === "root" || !newParentId ? null : new mongoose2__default.default.Types.ObjectId(newParentId);
|
|
420
|
+
await item.save();
|
|
421
|
+
return item.toClient();
|
|
422
|
+
},
|
|
423
|
+
revokeToken: async (owner, accountId) => {
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
var createAuthClient = async (owner, accountId) => {
|
|
427
|
+
const query = { owner, "metadata.provider": "GOOGLE" };
|
|
428
|
+
if (accountId) query._id = accountId;
|
|
429
|
+
const account = await account_default.findOne(query);
|
|
430
|
+
if (!account) throw new Error("Google Drive account not connected");
|
|
431
|
+
const config = getDriveConfig();
|
|
432
|
+
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
433
|
+
if (!clientId || !clientSecret) throw new Error("Google credentials not configured on server");
|
|
434
|
+
const oAuth2Client = new googleapis.google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
|
435
|
+
if (account.metadata.provider !== "GOOGLE" || !account.metadata.google) {
|
|
436
|
+
throw new Error("Invalid Google Account Metadata");
|
|
437
|
+
}
|
|
438
|
+
oAuth2Client.setCredentials(account.metadata.google.credentials);
|
|
439
|
+
oAuth2Client.on("tokens", async (tokens) => {
|
|
440
|
+
if (tokens.refresh_token) {
|
|
441
|
+
account.metadata.google.credentials = { ...account.metadata.google.credentials, ...tokens };
|
|
442
|
+
account.markModified("metadata");
|
|
443
|
+
await account.save();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
return { client: oAuth2Client, accountId: account._id };
|
|
447
|
+
};
|
|
448
|
+
var GoogleDriveProvider = {
|
|
449
|
+
name: "GOOGLE",
|
|
450
|
+
sync: async (folderId, owner, accountId) => {
|
|
451
|
+
const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
|
|
452
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
453
|
+
let googleParentId = "root";
|
|
454
|
+
if (folderId && folderId !== "root") {
|
|
455
|
+
const folder = await drive_default.findOne({ _id: folderId, owner });
|
|
456
|
+
if (folder && folder.provider?.google?.id) {
|
|
457
|
+
googleParentId = folder.provider.google.id;
|
|
458
|
+
} else {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
let nextPageToken = void 0;
|
|
463
|
+
const allSyncedGoogleIds = /* @__PURE__ */ new Set();
|
|
464
|
+
do {
|
|
465
|
+
const res = await drive.files.list({
|
|
466
|
+
q: `'${googleParentId}' in parents and trashed = false`,
|
|
467
|
+
fields: "nextPageToken, files(id, name, mimeType, size, webViewLink, iconLink, thumbnailLink, createdTime)",
|
|
468
|
+
pageSize: 1e3,
|
|
469
|
+
pageToken: nextPageToken
|
|
470
|
+
});
|
|
471
|
+
nextPageToken = res.data.nextPageToken || void 0;
|
|
472
|
+
const files = res.data.files || [];
|
|
473
|
+
for (const file of files) {
|
|
474
|
+
if (!file.id || !file.name || !file.mimeType) continue;
|
|
475
|
+
allSyncedGoogleIds.add(file.id);
|
|
476
|
+
const isFolder = file.mimeType === "application/vnd.google-apps.folder";
|
|
477
|
+
const sizeInBytes = file.size ? parseInt(file.size) : 0;
|
|
478
|
+
const updateData = {
|
|
479
|
+
name: file.name,
|
|
480
|
+
storageAccountId: foundAccountId,
|
|
481
|
+
parentId: folderId === "root" ? null : folderId,
|
|
482
|
+
information: {
|
|
483
|
+
type: isFolder ? "FOLDER" : "FILE",
|
|
484
|
+
sizeInBytes,
|
|
485
|
+
mime: file.mimeType,
|
|
486
|
+
path: ""
|
|
487
|
+
},
|
|
488
|
+
provider: {
|
|
489
|
+
type: "GOOGLE",
|
|
490
|
+
google: {
|
|
491
|
+
id: file.id,
|
|
492
|
+
webViewLink: file.webViewLink,
|
|
493
|
+
iconLink: file.iconLink,
|
|
494
|
+
thumbnailLink: file.thumbnailLink
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
status: "READY",
|
|
498
|
+
trashedAt: null
|
|
499
|
+
};
|
|
500
|
+
const insertData = file.createdTime ? { createdAt: new Date(file.createdTime) } : {};
|
|
501
|
+
await drive_default.findOneAndUpdate(
|
|
502
|
+
{
|
|
503
|
+
owner,
|
|
504
|
+
"provider.google.id": file.id,
|
|
505
|
+
"provider.type": "GOOGLE"
|
|
506
|
+
},
|
|
507
|
+
{ $set: updateData, $setOnInsert: insertData },
|
|
508
|
+
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
} while (nextPageToken);
|
|
512
|
+
const dbItems = await drive_default.find({
|
|
513
|
+
owner,
|
|
514
|
+
storageAccountId: foundAccountId,
|
|
515
|
+
parentId: folderId === "root" ? null : folderId,
|
|
516
|
+
"provider.type": "GOOGLE"
|
|
517
|
+
});
|
|
518
|
+
for (const item of dbItems) {
|
|
519
|
+
if (item.provider?.google?.id && !allSyncedGoogleIds.has(item.provider.google.id)) {
|
|
520
|
+
item.trashedAt = /* @__PURE__ */ new Date();
|
|
521
|
+
await item.save();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
syncTrash: async (owner, accountId) => {
|
|
526
|
+
const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
|
|
527
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
528
|
+
let nextPageToken = void 0;
|
|
529
|
+
do {
|
|
530
|
+
const res = await drive.files.list({
|
|
531
|
+
q: "trashed = true",
|
|
532
|
+
fields: "nextPageToken, files(id, name, mimeType, size, webViewLink, iconLink, thumbnailLink, createdTime)",
|
|
533
|
+
pageSize: 100,
|
|
534
|
+
// Limit sync for performance
|
|
535
|
+
pageToken: nextPageToken
|
|
536
|
+
});
|
|
537
|
+
nextPageToken = res.data.nextPageToken || void 0;
|
|
538
|
+
const files = res.data.files || [];
|
|
539
|
+
for (const file of files) {
|
|
540
|
+
if (!file.id || !file.name || !file.mimeType) continue;
|
|
541
|
+
const isFolder = file.mimeType === "application/vnd.google-apps.folder";
|
|
542
|
+
const sizeInBytes = file.size ? parseInt(file.size) : 0;
|
|
543
|
+
const insertData = file.createdTime ? { createdAt: new Date(file.createdTime) } : {};
|
|
544
|
+
await drive_default.findOneAndUpdate(
|
|
545
|
+
{ owner, "provider.google.id": file.id, "provider.type": "GOOGLE" },
|
|
546
|
+
{
|
|
547
|
+
$set: {
|
|
548
|
+
name: file.name,
|
|
549
|
+
storageAccountId: foundAccountId,
|
|
550
|
+
information: {
|
|
551
|
+
type: isFolder ? "FOLDER" : "FILE",
|
|
552
|
+
sizeInBytes,
|
|
553
|
+
mime: file.mimeType,
|
|
554
|
+
path: ""
|
|
555
|
+
},
|
|
556
|
+
provider: {
|
|
557
|
+
type: "GOOGLE",
|
|
558
|
+
google: {
|
|
559
|
+
id: file.id,
|
|
560
|
+
webViewLink: file.webViewLink,
|
|
561
|
+
iconLink: file.iconLink,
|
|
562
|
+
thumbnailLink: file.thumbnailLink
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
trashedAt: /* @__PURE__ */ new Date()
|
|
566
|
+
},
|
|
567
|
+
$setOnInsert: insertData
|
|
568
|
+
},
|
|
569
|
+
{ upsert: true, setDefaultsOnInsert: true }
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
} while (nextPageToken);
|
|
573
|
+
},
|
|
574
|
+
search: async (query, owner, accountId) => {
|
|
575
|
+
const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
|
|
576
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
577
|
+
const res = await drive.files.list({
|
|
578
|
+
q: `name contains '${query}' and trashed = false`,
|
|
579
|
+
fields: "files(id, name, mimeType, size, parents, webViewLink, iconLink, thumbnailLink, createdTime)",
|
|
580
|
+
pageSize: 50
|
|
581
|
+
});
|
|
582
|
+
const files = res.data.files || [];
|
|
583
|
+
for (const file of files) {
|
|
584
|
+
if (!file.id || !file.name) continue;
|
|
585
|
+
const isFolder = file.mimeType === "application/vnd.google-apps.folder";
|
|
586
|
+
if (!isFolder && file.mimeType?.startsWith("application/vnd.google-apps.")) continue;
|
|
587
|
+
const sizeInBytes = file.size ? parseInt(file.size) : 0;
|
|
588
|
+
const insertData = file.createdTime ? { createdAt: new Date(file.createdTime) } : {};
|
|
589
|
+
await drive_default.findOneAndUpdate(
|
|
590
|
+
{ owner, "provider.google.id": file.id, "metadata.type": "GOOGLE" },
|
|
591
|
+
{
|
|
592
|
+
$set: {
|
|
593
|
+
name: file.name,
|
|
594
|
+
storageAccountId: foundAccountId,
|
|
595
|
+
information: {
|
|
596
|
+
type: isFolder ? "FOLDER" : "FILE",
|
|
597
|
+
sizeInBytes,
|
|
598
|
+
mime: file.mimeType,
|
|
599
|
+
path: ""
|
|
600
|
+
},
|
|
601
|
+
metadata: {
|
|
602
|
+
type: "GOOGLE"
|
|
603
|
+
},
|
|
604
|
+
provider: {
|
|
605
|
+
google: {
|
|
606
|
+
id: file.id,
|
|
607
|
+
webViewLink: file.webViewLink,
|
|
608
|
+
iconLink: file.iconLink,
|
|
609
|
+
thumbnailLink: file.thumbnailLink
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Don't overwrite parentId if it exists.
|
|
613
|
+
// New items will default to null (Root) via schema default
|
|
614
|
+
},
|
|
615
|
+
$setOnInsert: insertData
|
|
616
|
+
},
|
|
617
|
+
{ upsert: true, setDefaultsOnInsert: true }
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
getQuota: async (owner, accountId, _configuredQuotaInBytes) => {
|
|
622
|
+
try {
|
|
623
|
+
const { client } = await createAuthClient(owner, accountId);
|
|
624
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
625
|
+
const res = await drive.about.get({ fields: "storageQuota" });
|
|
626
|
+
return {
|
|
627
|
+
usedInBytes: parseInt(res.data.storageQuota?.usage || "0"),
|
|
628
|
+
quotaInBytes: parseInt(res.data.storageQuota?.limit || "0")
|
|
629
|
+
};
|
|
630
|
+
} catch {
|
|
631
|
+
return { usedInBytes: 0, quotaInBytes: 0 };
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
openStream: async (item, accountId) => {
|
|
635
|
+
const { client } = await createAuthClient(item.owner, accountId || item.storageAccountId?.toString());
|
|
636
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
637
|
+
if (!item.provider?.google?.id) throw new Error("Missing Google File ID");
|
|
638
|
+
if (item.information.type === "FOLDER") throw new Error("Cannot stream folder");
|
|
639
|
+
const res = await drive.files.get({ fileId: item.provider.google.id, alt: "media" }, { responseType: "stream" });
|
|
640
|
+
return {
|
|
641
|
+
stream: res.data,
|
|
642
|
+
mime: item.information.mime,
|
|
643
|
+
size: item.information.sizeInBytes
|
|
644
|
+
};
|
|
645
|
+
},
|
|
646
|
+
getThumbnail: async (item, accountId) => {
|
|
647
|
+
const { client } = await createAuthClient(item.owner, accountId || item.storageAccountId?.toString());
|
|
648
|
+
if (!item.provider?.google?.thumbnailLink) throw new Error("No thumbnail available");
|
|
649
|
+
const res = await client.request({ url: item.provider.google.thumbnailLink, responseType: "stream" });
|
|
650
|
+
return res.data;
|
|
651
|
+
},
|
|
652
|
+
createFolder: async (name, parentId, owner, accountId) => {
|
|
653
|
+
const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
|
|
654
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
655
|
+
let googleParentId = "root";
|
|
656
|
+
if (parentId && parentId !== "root") {
|
|
657
|
+
const parent = await drive_default.findOne({ _id: parentId, owner });
|
|
658
|
+
if (parent?.provider?.google?.id) googleParentId = parent.provider.google.id;
|
|
659
|
+
}
|
|
660
|
+
const res = await drive.files.create({
|
|
661
|
+
requestBody: {
|
|
662
|
+
name,
|
|
663
|
+
mimeType: "application/vnd.google-apps.folder",
|
|
664
|
+
parents: [googleParentId]
|
|
665
|
+
},
|
|
666
|
+
fields: "id, name, mimeType, webViewLink, iconLink"
|
|
667
|
+
});
|
|
668
|
+
const file = res.data;
|
|
669
|
+
if (!file.id) throw new Error("Failed to create folder on Google Drive");
|
|
670
|
+
const folder = new drive_default({
|
|
671
|
+
owner,
|
|
672
|
+
name: file.name,
|
|
673
|
+
parentId: parentId === "root" || !parentId ? null : parentId,
|
|
674
|
+
provider: {
|
|
675
|
+
type: "GOOGLE",
|
|
676
|
+
google: {
|
|
677
|
+
id: file.id,
|
|
678
|
+
webViewLink: file.webViewLink,
|
|
679
|
+
iconLink: file.iconLink
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
storageAccountId: foundAccountId,
|
|
683
|
+
information: { type: "FOLDER" },
|
|
684
|
+
status: "READY"
|
|
685
|
+
});
|
|
686
|
+
await folder.save();
|
|
687
|
+
return folder.toClient();
|
|
688
|
+
},
|
|
689
|
+
uploadFile: async (drive, filePath, accountId) => {
|
|
690
|
+
if (drive.information.type !== "FILE") throw new Error("Invalid drive type");
|
|
691
|
+
const { client } = await createAuthClient(drive.owner, accountId || drive.storageAccountId?.toString());
|
|
692
|
+
const googleDrive = googleapis.google.drive({ version: "v3", auth: client });
|
|
693
|
+
let googleParentId = "root";
|
|
694
|
+
if (drive.parentId) {
|
|
695
|
+
const parent = await drive_default.findById(drive.parentId);
|
|
696
|
+
if (parent?.provider?.google?.id) googleParentId = parent.provider.google.id;
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
const res = await googleDrive.files.create({
|
|
700
|
+
requestBody: {
|
|
701
|
+
name: drive.name,
|
|
702
|
+
parents: [googleParentId],
|
|
703
|
+
mimeType: drive.information.mime
|
|
704
|
+
},
|
|
705
|
+
media: {
|
|
706
|
+
mimeType: drive.information.mime,
|
|
707
|
+
body: fs4__default.default.createReadStream(filePath)
|
|
708
|
+
},
|
|
709
|
+
fields: "id, name, mimeType, webViewLink, iconLink, thumbnailLink, size"
|
|
710
|
+
});
|
|
711
|
+
const gFile = res.data;
|
|
712
|
+
if (!gFile.id) throw new Error("Upload to Google Drive failed");
|
|
713
|
+
drive.status = "READY";
|
|
714
|
+
drive.provider = {
|
|
715
|
+
type: "GOOGLE",
|
|
716
|
+
google: {
|
|
717
|
+
id: gFile.id,
|
|
718
|
+
webViewLink: gFile.webViewLink || void 0,
|
|
719
|
+
iconLink: gFile.iconLink || void 0,
|
|
720
|
+
thumbnailLink: gFile.thumbnailLink || void 0
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
} catch (error) {
|
|
724
|
+
drive.status = "FAILED";
|
|
725
|
+
console.error("Google Upload Error:", error);
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
await drive.save();
|
|
729
|
+
return drive.toClient();
|
|
730
|
+
},
|
|
731
|
+
delete: async (ids, owner, accountId) => {
|
|
732
|
+
const { client } = await createAuthClient(owner, accountId);
|
|
733
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
734
|
+
const items = await drive_default.find({ _id: { $in: ids }, owner });
|
|
735
|
+
for (const item of items) {
|
|
736
|
+
if (item.provider?.google?.id) {
|
|
737
|
+
try {
|
|
738
|
+
await drive.files.delete({ fileId: item.provider.google.id });
|
|
739
|
+
} catch (e) {
|
|
740
|
+
console.error("Failed to delete Google file", e);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
await drive_default.deleteMany({ _id: { $in: ids } });
|
|
745
|
+
},
|
|
746
|
+
trash: async (ids, owner, accountId) => {
|
|
747
|
+
const { client } = await createAuthClient(owner, accountId);
|
|
748
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
749
|
+
const items = await drive_default.find({ _id: { $in: ids }, owner });
|
|
750
|
+
for (const item of items) {
|
|
751
|
+
if (item.provider?.google?.id) {
|
|
752
|
+
try {
|
|
753
|
+
await drive.files.update({
|
|
754
|
+
fileId: item.provider.google.id,
|
|
755
|
+
requestBody: { trashed: true }
|
|
756
|
+
});
|
|
757
|
+
} catch (e) {
|
|
758
|
+
console.error("Failed to trash Google file", e);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
untrash: async (ids, owner, accountId) => {
|
|
764
|
+
const { client } = await createAuthClient(owner, accountId);
|
|
765
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
766
|
+
const items = await drive_default.find({ _id: { $in: ids }, owner });
|
|
767
|
+
for (const item of items) {
|
|
768
|
+
if (item.provider?.google?.id) {
|
|
769
|
+
try {
|
|
770
|
+
await drive.files.update({
|
|
771
|
+
fileId: item.provider.google.id,
|
|
772
|
+
requestBody: { trashed: false }
|
|
773
|
+
});
|
|
774
|
+
} catch (e) {
|
|
775
|
+
console.error("Failed to restore Google file", e);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
rename: async (id, newName, owner, accountId) => {
|
|
781
|
+
const { client } = await createAuthClient(owner, accountId);
|
|
782
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
783
|
+
const item = await drive_default.findOne({ _id: id, owner });
|
|
784
|
+
if (!item || !item.provider?.google?.id) throw new Error("Item not found");
|
|
785
|
+
await drive.files.update({
|
|
786
|
+
fileId: item.provider.google.id,
|
|
787
|
+
requestBody: { name: newName }
|
|
788
|
+
});
|
|
789
|
+
item.name = newName;
|
|
790
|
+
await item.save();
|
|
791
|
+
return item.toClient();
|
|
792
|
+
},
|
|
793
|
+
move: async (id, newParentId, owner, accountId) => {
|
|
794
|
+
const { client, accountId: foundAccountId } = await createAuthClient(owner, accountId);
|
|
795
|
+
const drive = googleapis.google.drive({ version: "v3", auth: client });
|
|
796
|
+
const item = await drive_default.findOne({ _id: id, owner });
|
|
797
|
+
if (!item || !item.provider?.google?.id) throw new Error("Item not found or not synced");
|
|
798
|
+
let previousGoogleParentId = void 0;
|
|
799
|
+
if (item.parentId) {
|
|
800
|
+
const oldParent = await drive_default.findOne({ _id: item.parentId, owner });
|
|
801
|
+
if (oldParent && oldParent.provider?.google?.id) {
|
|
802
|
+
previousGoogleParentId = oldParent.provider.google.id;
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
try {
|
|
806
|
+
const gFile = await drive.files.get({ fileId: item.provider.google.id, fields: "parents" });
|
|
807
|
+
if (gFile.data.parents && gFile.data.parents.length > 0) {
|
|
808
|
+
previousGoogleParentId = gFile.data.parents.join(",");
|
|
809
|
+
}
|
|
810
|
+
} catch (e) {
|
|
811
|
+
console.warn("Could not fetch parents for move", e);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
let newGoogleParentId = "root";
|
|
815
|
+
if (newParentId && newParentId !== "root") {
|
|
816
|
+
const newParent = await drive_default.findOne({ _id: newParentId, owner });
|
|
817
|
+
if (!newParent || !newParent.provider?.google?.id) throw new Error("Target folder not found in Google Drive");
|
|
818
|
+
newGoogleParentId = newParent.provider.google.id;
|
|
819
|
+
}
|
|
820
|
+
await drive.files.update({
|
|
821
|
+
fileId: item.provider.google.id,
|
|
822
|
+
addParents: newGoogleParentId,
|
|
823
|
+
removeParents: previousGoogleParentId,
|
|
824
|
+
fields: "id, parents"
|
|
825
|
+
});
|
|
826
|
+
item.parentId = newParentId === "root" || !newParentId ? null : new mongoose2__default.default.Types.ObjectId(newParentId);
|
|
827
|
+
await item.save();
|
|
828
|
+
return item.toClient();
|
|
829
|
+
},
|
|
830
|
+
revokeToken: async (owner, accountId) => {
|
|
831
|
+
if (!accountId) return;
|
|
832
|
+
const { client } = await createAuthClient(owner, accountId);
|
|
833
|
+
const account = await account_default.findById(accountId);
|
|
834
|
+
if (account?.metadata?.provider === "GOOGLE" && account.metadata.google?.credentials) {
|
|
835
|
+
const creds = account.metadata.google.credentials;
|
|
836
|
+
if (typeof creds === "object" && "access_token" in creds) {
|
|
837
|
+
await client.revokeToken(creds.access_token);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
var getNextOrderValue = async (owner) => {
|
|
843
|
+
const lastItem = await drive_default.findOne({ owner }, {}, { sort: { order: -1 } });
|
|
844
|
+
return lastItem ? lastItem.order + 1 : 0;
|
|
845
|
+
};
|
|
846
|
+
var driveGetUrl = (fileId, options) => {
|
|
847
|
+
const config = getDriveConfig();
|
|
848
|
+
if (!config.security.signedUrls?.enabled) {
|
|
849
|
+
return `/api/drive?action=serve&id=${fileId}`;
|
|
850
|
+
}
|
|
851
|
+
const { secret, expiresIn } = config.security.signedUrls;
|
|
852
|
+
let expiryTimestamp;
|
|
853
|
+
if (options?.expiry instanceof Date) {
|
|
854
|
+
expiryTimestamp = Math.floor(options.expiry.getTime() / 1e3);
|
|
855
|
+
} else if (typeof options?.expiry === "number") {
|
|
856
|
+
expiryTimestamp = Math.floor(Date.now() / 1e3) + options.expiry;
|
|
857
|
+
} else {
|
|
858
|
+
expiryTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
859
|
+
}
|
|
860
|
+
const signature = crypto3__default.default.createHmac("sha256", secret).update(`${fileId}:${expiryTimestamp}`).digest("hex");
|
|
861
|
+
const token = Buffer.from(`${expiryTimestamp}:${signature}`).toString("base64url");
|
|
862
|
+
return `/api/drive?action=serve&id=${fileId}&token=${token}`;
|
|
863
|
+
};
|
|
864
|
+
var driveReadFile = async (file) => {
|
|
865
|
+
let drive;
|
|
866
|
+
if (typeof file === "string") {
|
|
867
|
+
const doc = await drive_default.findById(file);
|
|
868
|
+
if (!doc) throw new Error(`File not found: ${file}`);
|
|
869
|
+
drive = doc;
|
|
870
|
+
} else if ("toClient" in file) {
|
|
871
|
+
drive = file;
|
|
872
|
+
} else {
|
|
873
|
+
throw new Error("Invalid file parameter provided");
|
|
874
|
+
}
|
|
875
|
+
if (drive.information.type !== "FILE") {
|
|
876
|
+
throw new Error("Cannot read a folder");
|
|
877
|
+
}
|
|
878
|
+
const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
879
|
+
const accountId = drive.storageAccountId?.toString();
|
|
880
|
+
return await provider.openStream(drive, accountId);
|
|
881
|
+
};
|
|
882
|
+
var driveInfo = async (source) => {
|
|
883
|
+
const fileId = typeof source === "string" ? source : source.id;
|
|
884
|
+
const drive = await drive_default.findById(fileId);
|
|
885
|
+
if (!drive) throw new Error(`File not found: ${fileId}`);
|
|
886
|
+
let parentName;
|
|
887
|
+
if (drive.parentId) {
|
|
888
|
+
const parent = await drive_default.findById(drive.parentId);
|
|
889
|
+
if (parent) parentName = parent.name;
|
|
890
|
+
}
|
|
891
|
+
const info = {
|
|
892
|
+
id: String(drive._id),
|
|
893
|
+
name: drive.name,
|
|
894
|
+
type: drive.information.type,
|
|
895
|
+
status: drive.status,
|
|
896
|
+
provider: drive.provider,
|
|
897
|
+
parent: {
|
|
898
|
+
id: drive.parentId ? String(drive.parentId) : null,
|
|
899
|
+
name: parentName
|
|
900
|
+
},
|
|
901
|
+
createdAt: drive.createdAt,
|
|
902
|
+
trashedAt: drive.trashedAt
|
|
903
|
+
};
|
|
904
|
+
if (drive.information.type === "FILE") {
|
|
905
|
+
info.mime = drive.information.mime;
|
|
906
|
+
info.size = drive.information.sizeInBytes;
|
|
907
|
+
info.hash = drive.information.hash;
|
|
908
|
+
if (drive.information.width && drive.information.height) {
|
|
909
|
+
info.dimensions = {
|
|
910
|
+
width: drive.information.width,
|
|
911
|
+
height: drive.information.height
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (drive.information.duration) {
|
|
915
|
+
info.duration = drive.information.duration;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return info;
|
|
919
|
+
};
|
|
920
|
+
var driveFilePath = async (file) => {
|
|
921
|
+
let drive;
|
|
922
|
+
if (typeof file === "string") {
|
|
923
|
+
const doc = await drive_default.findById(file);
|
|
924
|
+
if (!doc) throw new Error(`File not found: ${file}`);
|
|
925
|
+
drive = doc;
|
|
926
|
+
} else if ("toClient" in file) {
|
|
927
|
+
drive = file;
|
|
928
|
+
} else {
|
|
929
|
+
throw new Error("Invalid file parameter provided");
|
|
930
|
+
}
|
|
931
|
+
if (drive.information.type !== "FILE") {
|
|
932
|
+
throw new Error("Cannot get path for a folder");
|
|
933
|
+
}
|
|
934
|
+
const config = getDriveConfig();
|
|
935
|
+
const STORAGE_PATH = config.storage.path;
|
|
936
|
+
const providerType = drive.provider?.type || "LOCAL";
|
|
937
|
+
if (providerType === "LOCAL") {
|
|
938
|
+
const filePath = path3__default.default.join(STORAGE_PATH, drive.information.path);
|
|
939
|
+
if (!fs4__default.default.existsSync(filePath)) {
|
|
940
|
+
throw new Error(`Local file not found on disk: ${filePath}`);
|
|
941
|
+
}
|
|
942
|
+
return Object.freeze({
|
|
943
|
+
path: filePath,
|
|
944
|
+
name: drive.name,
|
|
945
|
+
mime: drive.information.mime,
|
|
946
|
+
size: drive.information.sizeInBytes,
|
|
947
|
+
provider: "LOCAL"
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
if (providerType === "GOOGLE") {
|
|
951
|
+
const libraryDir = path3__default.default.join(STORAGE_PATH, "library", "google");
|
|
952
|
+
const fileName = `${drive._id}${path3__default.default.extname(drive.name)}`;
|
|
953
|
+
const cachedFilePath = path3__default.default.join(libraryDir, fileName);
|
|
954
|
+
if (fs4__default.default.existsSync(cachedFilePath)) {
|
|
955
|
+
const stats = fs4__default.default.statSync(cachedFilePath);
|
|
956
|
+
if (stats.size === drive.information.sizeInBytes) {
|
|
957
|
+
return Object.freeze({
|
|
958
|
+
path: cachedFilePath,
|
|
959
|
+
name: drive.name,
|
|
960
|
+
mime: drive.information.mime,
|
|
961
|
+
size: drive.information.sizeInBytes,
|
|
962
|
+
provider: "GOOGLE"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
fs4__default.default.unlinkSync(cachedFilePath);
|
|
966
|
+
}
|
|
967
|
+
const accountId = drive.storageAccountId?.toString();
|
|
968
|
+
const { stream } = await GoogleDriveProvider.openStream(drive, accountId);
|
|
969
|
+
if (!fs4__default.default.existsSync(libraryDir)) {
|
|
970
|
+
fs4__default.default.mkdirSync(libraryDir, { recursive: true });
|
|
971
|
+
}
|
|
972
|
+
const tempPath = `${cachedFilePath}.tmp`;
|
|
973
|
+
const writeStream = fs4__default.default.createWriteStream(tempPath);
|
|
974
|
+
await new Promise((resolve, reject) => {
|
|
975
|
+
stream.pipe(writeStream);
|
|
976
|
+
writeStream.on("finish", resolve);
|
|
977
|
+
writeStream.on("error", reject);
|
|
978
|
+
stream.on("error", reject);
|
|
979
|
+
});
|
|
980
|
+
try {
|
|
981
|
+
fs4__default.default.renameSync(tempPath, cachedFilePath);
|
|
982
|
+
} catch (err) {
|
|
983
|
+
if (err instanceof Error && "code" in err && err.code === "EXDEV") {
|
|
984
|
+
fs4__default.default.copyFileSync(tempPath, cachedFilePath);
|
|
985
|
+
fs4__default.default.unlinkSync(tempPath);
|
|
986
|
+
} else {
|
|
987
|
+
throw err;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return Object.freeze({
|
|
991
|
+
path: cachedFilePath,
|
|
992
|
+
name: drive.name,
|
|
993
|
+
mime: drive.information.mime,
|
|
994
|
+
size: drive.information.sizeInBytes,
|
|
995
|
+
provider: "GOOGLE"
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
throw new Error(`Unsupported provider: ${providerType}`);
|
|
999
|
+
};
|
|
1000
|
+
var driveList = async (options) => {
|
|
1001
|
+
const { key, folderId, accountId, limit = 100, afterId } = options;
|
|
1002
|
+
let providerName = "LOCAL";
|
|
1003
|
+
if (accountId && accountId !== "LOCAL") {
|
|
1004
|
+
const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
|
|
1005
|
+
if (!account) {
|
|
1006
|
+
throw new Error("Invalid Storage Account");
|
|
1007
|
+
}
|
|
1008
|
+
if (account.metadata.provider === "GOOGLE") {
|
|
1009
|
+
providerName = "GOOGLE";
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const query = {
|
|
1013
|
+
owner: key,
|
|
1014
|
+
"provider.type": providerName,
|
|
1015
|
+
storageAccountId: accountId || null,
|
|
1016
|
+
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
1017
|
+
trashedAt: null
|
|
1018
|
+
};
|
|
1019
|
+
if (afterId) {
|
|
1020
|
+
query._id = { $lt: afterId };
|
|
1021
|
+
}
|
|
1022
|
+
const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
|
|
1023
|
+
return await Promise.all(items.map((item) => item.toClient()));
|
|
1024
|
+
};
|
|
1025
|
+
var driveDelete = async (source, options) => {
|
|
1026
|
+
const { recurse = true } = options || {};
|
|
1027
|
+
let drive;
|
|
1028
|
+
let driveId;
|
|
1029
|
+
if (typeof source === "string") {
|
|
1030
|
+
const doc = await drive_default.findById(source);
|
|
1031
|
+
if (!doc) throw new Error(`File not found: ${source}`);
|
|
1032
|
+
drive = doc;
|
|
1033
|
+
driveId = source;
|
|
1034
|
+
} else if ("toClient" in source) {
|
|
1035
|
+
drive = source;
|
|
1036
|
+
driveId = String(drive._id);
|
|
1037
|
+
} else {
|
|
1038
|
+
const doc = await drive_default.findById(source.id);
|
|
1039
|
+
if (!doc) throw new Error(`File not found: ${source.id}`);
|
|
1040
|
+
drive = doc;
|
|
1041
|
+
driveId = source.id;
|
|
1042
|
+
}
|
|
1043
|
+
if (drive.information.type === "FOLDER" && !recurse) {
|
|
1044
|
+
const owner2 = drive.owner;
|
|
1045
|
+
const childCount = await drive_default.countDocuments({
|
|
1046
|
+
owner: owner2,
|
|
1047
|
+
parentId: driveId,
|
|
1048
|
+
trashedAt: null
|
|
1049
|
+
});
|
|
1050
|
+
if (childCount > 0) {
|
|
1051
|
+
throw new Error(`Cannot delete folder: it contains ${childCount} item(s). Use recurse: true to delete folder and all its contents.`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const provider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
1055
|
+
const accountId = drive.storageAccountId?.toString();
|
|
1056
|
+
const owner = drive.owner;
|
|
1057
|
+
await provider.delete([driveId], owner, accountId);
|
|
1058
|
+
};
|
|
1059
|
+
var driveUpload = async (source, key, options) => {
|
|
1060
|
+
const config = getDriveConfig();
|
|
1061
|
+
let provider = LocalStorageProvider;
|
|
1062
|
+
const accountId = options.accountId;
|
|
1063
|
+
if (accountId && accountId !== "LOCAL") {
|
|
1064
|
+
const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner: key });
|
|
1065
|
+
if (!account) {
|
|
1066
|
+
throw new Error("Invalid Storage Account");
|
|
1067
|
+
}
|
|
1068
|
+
if (account.metadata.provider === "GOOGLE") {
|
|
1069
|
+
provider = GoogleDriveProvider;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
let tempFilePath = null;
|
|
1073
|
+
let sourceFilePath;
|
|
1074
|
+
let fileSize;
|
|
1075
|
+
if (typeof source === "string") {
|
|
1076
|
+
if (!fs4__default.default.existsSync(source)) {
|
|
1077
|
+
throw new Error(`Source file not found: ${source}`);
|
|
1078
|
+
}
|
|
1079
|
+
sourceFilePath = source;
|
|
1080
|
+
const stats = fs4__default.default.statSync(source);
|
|
1081
|
+
fileSize = stats.size;
|
|
1082
|
+
} else if (Buffer.isBuffer(source)) {
|
|
1083
|
+
const tempDir = path3__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
|
|
1084
|
+
if (!fs4__default.default.existsSync(tempDir)) {
|
|
1085
|
+
fs4__default.default.mkdirSync(tempDir, { recursive: true });
|
|
1086
|
+
}
|
|
1087
|
+
tempFilePath = path3__default.default.join(tempDir, `upload-${crypto3__default.default.randomUUID()}.tmp`);
|
|
1088
|
+
fs4__default.default.writeFileSync(tempFilePath, source);
|
|
1089
|
+
sourceFilePath = tempFilePath;
|
|
1090
|
+
fileSize = source.length;
|
|
1091
|
+
} else {
|
|
1092
|
+
const tempDir = path3__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
|
|
1093
|
+
if (!fs4__default.default.existsSync(tempDir)) {
|
|
1094
|
+
fs4__default.default.mkdirSync(tempDir, { recursive: true });
|
|
1095
|
+
}
|
|
1096
|
+
tempFilePath = path3__default.default.join(tempDir, `upload-${crypto3__default.default.randomUUID()}.tmp`);
|
|
1097
|
+
const writeStream = fs4__default.default.createWriteStream(tempFilePath);
|
|
1098
|
+
await new Promise((resolve, reject) => {
|
|
1099
|
+
source.pipe(writeStream);
|
|
1100
|
+
writeStream.on("finish", resolve);
|
|
1101
|
+
writeStream.on("error", reject);
|
|
1102
|
+
source.on("error", reject);
|
|
1103
|
+
});
|
|
1104
|
+
sourceFilePath = tempFilePath;
|
|
1105
|
+
const stats = fs4__default.default.statSync(tempFilePath);
|
|
1106
|
+
fileSize = stats.size;
|
|
1107
|
+
}
|
|
1108
|
+
try {
|
|
1109
|
+
let mimeType;
|
|
1110
|
+
if (options.mime) {
|
|
1111
|
+
mimeType = options.mime;
|
|
1112
|
+
} else {
|
|
1113
|
+
const ext = path3__default.default.extname(options.name).toLowerCase();
|
|
1114
|
+
const mimeTypes = {
|
|
1115
|
+
".jpg": "image/jpeg",
|
|
1116
|
+
".jpeg": "image/jpeg",
|
|
1117
|
+
".png": "image/png",
|
|
1118
|
+
".gif": "image/gif",
|
|
1119
|
+
".webp": "image/webp",
|
|
1120
|
+
".svg": "image/svg+xml",
|
|
1121
|
+
".mp4": "video/mp4",
|
|
1122
|
+
".mov": "video/quicktime",
|
|
1123
|
+
".avi": "video/x-msvideo",
|
|
1124
|
+
".pdf": "application/pdf",
|
|
1125
|
+
".doc": "application/msword",
|
|
1126
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1127
|
+
".xls": "application/vnd.ms-excel",
|
|
1128
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1129
|
+
".txt": "text/plain",
|
|
1130
|
+
".json": "application/json",
|
|
1131
|
+
".zip": "application/zip"
|
|
1132
|
+
};
|
|
1133
|
+
mimeType = mimeTypes[ext] || "application/octet-stream";
|
|
1134
|
+
}
|
|
1135
|
+
if (!validateMimeType(mimeType, config.security.allowedMimeTypes)) {
|
|
1136
|
+
throw new Error(`File type ${mimeType} not allowed`);
|
|
1137
|
+
}
|
|
1138
|
+
if (fileSize > config.security.maxUploadSizeInBytes) {
|
|
1139
|
+
throw new Error(`File size ${fileSize} exceeds maximum allowed size ${config.security.maxUploadSizeInBytes}`);
|
|
1140
|
+
}
|
|
1141
|
+
if (!options.enforce) {
|
|
1142
|
+
const quota = await provider.getQuota(key, accountId, void 0);
|
|
1143
|
+
if (quota.usedInBytes + fileSize > quota.quotaInBytes) {
|
|
1144
|
+
throw new Error("Storage quota exceeded");
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const drive = new drive_default({
|
|
1148
|
+
owner: key,
|
|
1149
|
+
storageAccountId: accountId || null,
|
|
1150
|
+
provider: { type: provider.name },
|
|
1151
|
+
name: options.name,
|
|
1152
|
+
parentId: options.parentId === "root" || !options.parentId ? null : options.parentId,
|
|
1153
|
+
order: await getNextOrderValue(key),
|
|
1154
|
+
information: { type: "FILE", sizeInBytes: fileSize, mime: mimeType, path: "" },
|
|
1155
|
+
status: "UPLOADING"
|
|
1156
|
+
});
|
|
1157
|
+
if (provider.name === "LOCAL" && drive.information.type === "FILE") {
|
|
1158
|
+
let sanitizedExt = path3__default.default.extname(options.name) || ".bin";
|
|
1159
|
+
sanitizedExt = sanitizedExt.replace(/[^a-zA-Z0-9.]/g, "").slice(0, 11);
|
|
1160
|
+
if (!sanitizedExt.startsWith(".")) sanitizedExt = ".bin";
|
|
1161
|
+
drive.information.path = path3__default.default.join(String(drive._id), `data${sanitizedExt}`);
|
|
1162
|
+
}
|
|
1163
|
+
await drive.save();
|
|
1164
|
+
try {
|
|
1165
|
+
const item = await provider.uploadFile(drive, sourceFilePath, accountId);
|
|
1166
|
+
return {
|
|
1167
|
+
id: item.id,
|
|
1168
|
+
file: {
|
|
1169
|
+
name: item.name,
|
|
1170
|
+
mime: item.information.type === "FILE" ? item.information.mime : "application/x-folder",
|
|
1171
|
+
size: item.information.type === "FILE" ? item.information.sizeInBytes : 0
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
await drive_default.deleteOne({ _id: drive._id });
|
|
1176
|
+
throw err;
|
|
1177
|
+
}
|
|
1178
|
+
} finally {
|
|
1179
|
+
if (tempFilePath && fs4__default.default.existsSync(tempFilePath)) {
|
|
1180
|
+
fs4__default.default.rmSync(tempFilePath, { force: true });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
// src/server/index.ts
|
|
1186
|
+
var getProvider = async (req, owner) => {
|
|
1187
|
+
const accountId = req.headers["x-drive-account"];
|
|
1188
|
+
if (!accountId || accountId === "LOCAL") {
|
|
1189
|
+
return { provider: LocalStorageProvider };
|
|
1190
|
+
}
|
|
1191
|
+
const account = await account_default.findOne({ _id: accountId, owner });
|
|
1192
|
+
if (!account) {
|
|
1193
|
+
throw new Error("Invalid Storage Account");
|
|
1194
|
+
}
|
|
1195
|
+
if (account.metadata.provider === "GOOGLE") return { provider: GoogleDriveProvider, accountId: account._id.toString() };
|
|
1196
|
+
return { provider: LocalStorageProvider };
|
|
1197
|
+
};
|
|
1198
|
+
var applyCorsHeaders = (req, res, config) => {
|
|
1199
|
+
const cors = config.cors;
|
|
1200
|
+
if (!cors?.enabled) return false;
|
|
1201
|
+
const origin = req.headers.origin;
|
|
1202
|
+
const allowedOrigins = cors.origins ?? "*";
|
|
1203
|
+
const methods = cors.methods ?? ["GET", "POST", "PUT", "DELETE", "OPTIONS"];
|
|
1204
|
+
const allowedHeaders = cors.allowedHeaders ?? ["Content-Type", "Authorization", "X-Drive-Account"];
|
|
1205
|
+
const exposedHeaders = cors.exposedHeaders ?? ["Content-Length", "Content-Type", "Content-Disposition"];
|
|
1206
|
+
const credentials = cors.credentials ?? false;
|
|
1207
|
+
const maxAge = cors.maxAge ?? 86400;
|
|
1208
|
+
let allowOrigin = null;
|
|
1209
|
+
if (origin) {
|
|
1210
|
+
if (allowedOrigins === "*") {
|
|
1211
|
+
allowOrigin = origin;
|
|
1212
|
+
} else if (Array.isArray(allowedOrigins)) {
|
|
1213
|
+
if (allowedOrigins.includes(origin)) {
|
|
1214
|
+
allowOrigin = origin;
|
|
1215
|
+
}
|
|
1216
|
+
} else if (allowedOrigins === origin) {
|
|
1217
|
+
allowOrigin = origin;
|
|
1218
|
+
}
|
|
1219
|
+
} else if (allowedOrigins === "*") {
|
|
1220
|
+
allowOrigin = "*";
|
|
1221
|
+
}
|
|
1222
|
+
if (!allowOrigin) {
|
|
1223
|
+
if (req.method === "OPTIONS") {
|
|
1224
|
+
res.status(403).end();
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
1230
|
+
res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
|
|
1231
|
+
res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
|
|
1232
|
+
res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(", "));
|
|
1233
|
+
res.setHeader("Access-Control-Max-Age", maxAge.toString());
|
|
1234
|
+
if (credentials) {
|
|
1235
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
1236
|
+
}
|
|
1237
|
+
if (req.method === "OPTIONS") {
|
|
1238
|
+
res.status(204).end();
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
return false;
|
|
1242
|
+
};
|
|
1243
|
+
var driveAPIHandler = async (req, res) => {
|
|
1244
|
+
const action = req.query.action || (req.query.code && req.query.state ? "callback" : void 0);
|
|
1245
|
+
let config;
|
|
1246
|
+
try {
|
|
1247
|
+
config = getDriveConfig();
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
console.error("[next-drive] Configuration error:", error);
|
|
1250
|
+
res.status(500).json({ status: 500, message: "Failed to initialize drive configuration" });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const isPreflightHandled = applyCorsHeaders(req, res, config);
|
|
1254
|
+
if (isPreflightHandled) return;
|
|
1255
|
+
if (!action) {
|
|
1256
|
+
res.status(400).json({ status: 400, message: "Missing action query parameter" });
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
const information = await getDriveInformation(req);
|
|
1261
|
+
const { key: owner } = information;
|
|
1262
|
+
const STORAGE_PATH = config.storage.path;
|
|
1263
|
+
if (action === "information") {
|
|
1264
|
+
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
1265
|
+
const googleConfigured = !!(clientId && clientSecret && redirectUri);
|
|
1266
|
+
return res.status(200).json({
|
|
1267
|
+
status: 200,
|
|
1268
|
+
message: "Information retrieved",
|
|
1269
|
+
data: {
|
|
1270
|
+
providers: {
|
|
1271
|
+
google: googleConfigured
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (["getAuthUrl", "callback", "listAccounts", "removeAccount"].includes(action)) {
|
|
1277
|
+
switch (action) {
|
|
1278
|
+
case "getAuthUrl": {
|
|
1279
|
+
const { provider: provider2 } = req.query;
|
|
1280
|
+
if (provider2 === "GOOGLE") {
|
|
1281
|
+
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
1282
|
+
if (!clientId || !clientSecret || !redirectUri) return res.status(500).json({ status: 500, message: "Google not configured" });
|
|
1283
|
+
const { google: google2 } = chunkJEQ2X3Z6_cjs.__require("googleapis");
|
|
1284
|
+
const callbackUri = new URL(redirectUri);
|
|
1285
|
+
callbackUri.searchParams.set("action", "callback");
|
|
1286
|
+
const oAuth2Client = new google2.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
|
|
1287
|
+
const state = Buffer.from(JSON.stringify({ owner })).toString("base64");
|
|
1288
|
+
const url = oAuth2Client.generateAuthUrl({
|
|
1289
|
+
access_type: "offline",
|
|
1290
|
+
scope: ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email"],
|
|
1291
|
+
state,
|
|
1292
|
+
prompt: "consent"
|
|
1293
|
+
// force refresh token
|
|
1294
|
+
});
|
|
1295
|
+
return res.status(200).json({ status: 200, message: "Auth URL generated", data: { url } });
|
|
1296
|
+
}
|
|
1297
|
+
return res.status(400).json({ status: 400, message: "Unknown provider" });
|
|
1298
|
+
}
|
|
1299
|
+
case "callback": {
|
|
1300
|
+
const { code, state } = req.query;
|
|
1301
|
+
if (!code) return res.status(400).json({ status: 400, message: "Missing code" });
|
|
1302
|
+
const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
|
|
1303
|
+
if (!clientId || !clientSecret || !redirectUri) return res.status(500).json({ status: 500, message: "Google not configured" });
|
|
1304
|
+
const { google: google2 } = chunkJEQ2X3Z6_cjs.__require("googleapis");
|
|
1305
|
+
const callbackUri = new URL(redirectUri);
|
|
1306
|
+
callbackUri.searchParams.set("action", "callback");
|
|
1307
|
+
const oAuth2Client = new google2.auth.OAuth2(clientId, clientSecret, callbackUri.toString());
|
|
1308
|
+
const { tokens } = await oAuth2Client.getToken(code);
|
|
1309
|
+
oAuth2Client.setCredentials(tokens);
|
|
1310
|
+
const oauth2 = google2.oauth2({ version: "v2", auth: oAuth2Client });
|
|
1311
|
+
const userInfo = await oauth2.userinfo.get();
|
|
1312
|
+
const existing = await account_default.findOne({ owner, "metadata.google.email": userInfo.data.email, "metadata.provider": "GOOGLE" });
|
|
1313
|
+
if (existing) {
|
|
1314
|
+
existing.metadata.google.credentials = tokens;
|
|
1315
|
+
existing.markModified("metadata");
|
|
1316
|
+
await existing.save();
|
|
1317
|
+
} else {
|
|
1318
|
+
await account_default.create({
|
|
1319
|
+
owner,
|
|
1320
|
+
name: userInfo.data.name || "Google Drive",
|
|
1321
|
+
metadata: {
|
|
1322
|
+
provider: "GOOGLE",
|
|
1323
|
+
google: {
|
|
1324
|
+
email: userInfo.data.email,
|
|
1325
|
+
credentials: tokens
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
res.setHeader("Content-Type", "text/html");
|
|
1331
|
+
return res.send(`<!DOCTYPE html>
|
|
1332
|
+
<html>
|
|
1333
|
+
<head><title>Authentication Complete</title></head>
|
|
1334
|
+
<body>
|
|
1335
|
+
<p>Authentication successful! This window will close automatically.</p>
|
|
1336
|
+
<script>
|
|
1337
|
+
(function() {
|
|
1338
|
+
// Method 1: postMessage for popup windows
|
|
1339
|
+
if (window.opener) {
|
|
1340
|
+
try {
|
|
1341
|
+
window.opener.postMessage('oauth-success', '*');
|
|
1342
|
+
} catch (e) {}
|
|
1343
|
+
}
|
|
1344
|
+
// Method 2: localStorage event for new tabs (macOS fullscreen mode)
|
|
1345
|
+
try {
|
|
1346
|
+
localStorage.setItem('next-drive-oauth-success', Date.now().toString());
|
|
1347
|
+
localStorage.removeItem('next-drive-oauth-success');
|
|
1348
|
+
} catch (e) {}
|
|
1349
|
+
// Close the window/tab
|
|
1350
|
+
window.close();
|
|
1351
|
+
// Fallback: If window.close() doesn't work (some browsers block it),
|
|
1352
|
+
// show a message to manually close
|
|
1353
|
+
setTimeout(function() {
|
|
1354
|
+
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>';
|
|
1355
|
+
}, 500);
|
|
1356
|
+
})();
|
|
1357
|
+
</script>
|
|
1358
|
+
</body>
|
|
1359
|
+
</html>`);
|
|
1360
|
+
}
|
|
1361
|
+
case "listAccounts": {
|
|
1362
|
+
const accounts = await account_default.find({ owner });
|
|
1363
|
+
return res.status(200).json({
|
|
1364
|
+
status: 200,
|
|
1365
|
+
data: {
|
|
1366
|
+
accounts: accounts.map((a) => ({
|
|
1367
|
+
id: a._id.toString(),
|
|
1368
|
+
name: a.name,
|
|
1369
|
+
email: a.metadata.google?.email || "",
|
|
1370
|
+
provider: a.metadata.provider
|
|
1371
|
+
}))
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
case "removeAccount": {
|
|
1376
|
+
const { id } = req.query;
|
|
1377
|
+
const account = await account_default.findOne({ _id: id, owner });
|
|
1378
|
+
if (!account) return res.status(404).json({ status: 404, message: "Account not found" });
|
|
1379
|
+
if (account.metadata.provider === "GOOGLE") {
|
|
1380
|
+
try {
|
|
1381
|
+
await GoogleDriveProvider.revokeToken(owner, account._id.toString());
|
|
1382
|
+
} catch (e) {
|
|
1383
|
+
console.error("Failed to revoke Google token:", e);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
await account_default.deleteOne({ _id: id, owner });
|
|
1387
|
+
await drive_default.deleteMany({ owner, storageAccountId: id });
|
|
1388
|
+
return res.status(200).json({ status: 200, message: "Account removed" });
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
const { provider, accountId } = await getProvider(req, owner);
|
|
1393
|
+
switch (action) {
|
|
1394
|
+
// ** 1. LIST **
|
|
1395
|
+
case "list": {
|
|
1396
|
+
if (req.method !== "GET") return res.status(405).json({ status: 405, message: "Only GET allowed" });
|
|
1397
|
+
const listQuery = listQuerySchema.safeParse(req.query);
|
|
1398
|
+
if (!listQuery.success) return res.status(400).json({ status: 400, message: "Invalid parameters" });
|
|
1399
|
+
const { folderId, limit, afterId } = listQuery.data;
|
|
1400
|
+
try {
|
|
1401
|
+
await provider.sync(folderId || "root", owner, accountId);
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
console.error("Sync failed", e);
|
|
1404
|
+
}
|
|
1405
|
+
const query = {
|
|
1406
|
+
owner,
|
|
1407
|
+
"provider.type": provider.name,
|
|
1408
|
+
storageAccountId: accountId || null,
|
|
1409
|
+
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
1410
|
+
trashedAt: null
|
|
1411
|
+
};
|
|
1412
|
+
if (afterId) query._id = { $lt: afterId };
|
|
1413
|
+
const items = await drive_default.find(query, {}, { sort: { order: 1, _id: -1 }, limit });
|
|
1414
|
+
const plainItems = await Promise.all(items.map((item) => item.toClient()));
|
|
1415
|
+
res.status(200).json({ status: 200, message: "Items retrieved", data: { items: plainItems, hasMore: items.length === limit } });
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
// ** 2. SEARCH **
|
|
1419
|
+
case "search": {
|
|
1420
|
+
const searchData = searchQuerySchema.safeParse(req.query);
|
|
1421
|
+
if (!searchData.success) return res.status(400).json({ status: 400, message: "Invalid params" });
|
|
1422
|
+
const { q, folderId, limit, trashed } = searchData.data;
|
|
1423
|
+
if (!trashed) {
|
|
1424
|
+
try {
|
|
1425
|
+
await provider.search(q, owner, accountId);
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
console.error("Search sync failed", e);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
const query = {
|
|
1431
|
+
owner,
|
|
1432
|
+
"provider.type": provider.name,
|
|
1433
|
+
storageAccountId: accountId || null,
|
|
1434
|
+
trashedAt: trashed ? { $ne: null } : null,
|
|
1435
|
+
name: { $regex: q, $options: "i" }
|
|
1436
|
+
};
|
|
1437
|
+
if (folderId && folderId !== "root") query.parentId = folderId;
|
|
1438
|
+
const items = await drive_default.find(query, {}, { limit, sort: { createdAt: -1 } });
|
|
1439
|
+
const plainItems = await Promise.all(items.map((i) => i.toClient()));
|
|
1440
|
+
return res.status(200).json({ status: 200, message: "Results", data: { items: plainItems } });
|
|
1441
|
+
}
|
|
1442
|
+
// ** 3. UPLOAD **
|
|
1443
|
+
case "upload": {
|
|
1444
|
+
if (req.method !== "POST") return res.status(405).json({ status: 405, message: "Only POST allowed" });
|
|
1445
|
+
const systemTmpDir = path3__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
|
|
1446
|
+
if (!fs4__default.default.existsSync(systemTmpDir)) fs4__default.default.mkdirSync(systemTmpDir, { recursive: true });
|
|
1447
|
+
const form = formidable__default.default({
|
|
1448
|
+
multiples: false,
|
|
1449
|
+
maxFileSize: config.security.maxUploadSizeInBytes * 2,
|
|
1450
|
+
uploadDir: systemTmpDir,
|
|
1451
|
+
keepExtensions: true
|
|
1452
|
+
});
|
|
1453
|
+
const [fields, files] = await new Promise((resolve, reject) => {
|
|
1454
|
+
form.parse(req, (err, fields2, files2) => {
|
|
1455
|
+
if (err) reject(err);
|
|
1456
|
+
else resolve([fields2, files2]);
|
|
1457
|
+
});
|
|
1458
|
+
});
|
|
1459
|
+
const cleanupTempFiles = (files2) => {
|
|
1460
|
+
Object.values(files2).flat().forEach((file) => {
|
|
1461
|
+
if (file && fs4__default.default.existsSync(file.filepath)) fs4__default.default.rmSync(file.filepath, { force: true });
|
|
1462
|
+
});
|
|
1463
|
+
};
|
|
1464
|
+
const getString = (f) => Array.isArray(f) ? f[0] : f || "";
|
|
1465
|
+
const getInt = (f) => parseInt(getString(f) || "0", 10);
|
|
1466
|
+
const uploadData = uploadChunkSchema.safeParse({
|
|
1467
|
+
chunkIndex: getInt(fields.chunkIndex),
|
|
1468
|
+
totalChunks: getInt(fields.totalChunks),
|
|
1469
|
+
driveId: getString(fields.driveId) || void 0,
|
|
1470
|
+
fileName: getString(fields.fileName),
|
|
1471
|
+
fileSize: getInt(fields.fileSize),
|
|
1472
|
+
fileType: getString(fields.fileType),
|
|
1473
|
+
folderId: getString(fields.folderId) || void 0
|
|
1474
|
+
});
|
|
1475
|
+
if (!uploadData.success) {
|
|
1476
|
+
cleanupTempFiles(files);
|
|
1477
|
+
return res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
|
|
1478
|
+
}
|
|
1479
|
+
const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId } = uploadData.data;
|
|
1480
|
+
let currentUploadId = driveId;
|
|
1481
|
+
const tempBaseDir = path3__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
|
|
1482
|
+
if (!currentUploadId) {
|
|
1483
|
+
if (chunkIndex !== 0) return res.status(400).json({ message: "Missing upload ID for non-zero chunk" });
|
|
1484
|
+
if (fileType && !validateMimeType(fileType, config.security.allowedMimeTypes)) {
|
|
1485
|
+
cleanupTempFiles(files);
|
|
1486
|
+
return res.status(400).json({ status: 400, message: `File type ${fileType} not allowed` });
|
|
1487
|
+
}
|
|
1488
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
1489
|
+
if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
|
|
1490
|
+
cleanupTempFiles(files);
|
|
1491
|
+
return res.status(413).json({ status: 413, message: "Storage quota exceeded" });
|
|
1492
|
+
}
|
|
1493
|
+
currentUploadId = crypto.randomUUID();
|
|
1494
|
+
const uploadDir = path3__default.default.join(tempBaseDir, currentUploadId);
|
|
1495
|
+
fs4__default.default.mkdirSync(uploadDir, { recursive: true });
|
|
1496
|
+
const metadata = {
|
|
1497
|
+
owner,
|
|
1498
|
+
accountId,
|
|
1499
|
+
providerName: provider.name,
|
|
1500
|
+
name: fileName,
|
|
1501
|
+
parentId: folderId === "root" || !folderId ? null : folderId,
|
|
1502
|
+
fileSize: fileSizeInBytes,
|
|
1503
|
+
mimeType: fileType,
|
|
1504
|
+
totalChunks
|
|
1505
|
+
};
|
|
1506
|
+
fs4__default.default.writeFileSync(path3__default.default.join(uploadDir, "metadata.json"), JSON.stringify(metadata));
|
|
1507
|
+
}
|
|
1508
|
+
if (currentUploadId) {
|
|
1509
|
+
const uploadDir = path3__default.default.join(tempBaseDir, currentUploadId);
|
|
1510
|
+
if (!fs4__default.default.existsSync(uploadDir)) {
|
|
1511
|
+
cleanupTempFiles(files);
|
|
1512
|
+
return res.status(404).json({ status: 404, message: "Upload session not found or expired" });
|
|
1513
|
+
}
|
|
1514
|
+
try {
|
|
1515
|
+
const chunkFile = Array.isArray(files.chunk) ? files.chunk[0] : files.chunk;
|
|
1516
|
+
if (!chunkFile) throw new Error("No chunk file received");
|
|
1517
|
+
const partPath = path3__default.default.join(uploadDir, `part_${chunkIndex}`);
|
|
1518
|
+
try {
|
|
1519
|
+
fs4__default.default.renameSync(chunkFile.filepath, partPath);
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
if (err instanceof Error && "code" in err && err.code === "EXDEV") {
|
|
1522
|
+
fs4__default.default.copyFileSync(chunkFile.filepath, partPath);
|
|
1523
|
+
fs4__default.default.unlinkSync(chunkFile.filepath);
|
|
1524
|
+
} else {
|
|
1525
|
+
throw err;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
const uploadedParts = fs4__default.default.readdirSync(uploadDir).filter((f) => f.startsWith("part_"));
|
|
1529
|
+
if (uploadedParts.length === totalChunks) {
|
|
1530
|
+
const metaPath = path3__default.default.join(uploadDir, "metadata.json");
|
|
1531
|
+
const meta = JSON.parse(fs4__default.default.readFileSync(metaPath, "utf-8"));
|
|
1532
|
+
const finalTempPath = path3__default.default.join(uploadDir, "final.bin");
|
|
1533
|
+
const writeStream = fs4__default.default.createWriteStream(finalTempPath);
|
|
1534
|
+
await new Promise((resolve, reject) => {
|
|
1535
|
+
writeStream.on("open", () => resolve());
|
|
1536
|
+
writeStream.on("error", reject);
|
|
1537
|
+
});
|
|
1538
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
1539
|
+
const pPath = path3__default.default.join(uploadDir, `part_${i}`);
|
|
1540
|
+
if (!fs4__default.default.existsSync(pPath)) {
|
|
1541
|
+
writeStream.destroy();
|
|
1542
|
+
throw new Error(`Missing chunk part: ${i}`);
|
|
1543
|
+
}
|
|
1544
|
+
const data = fs4__default.default.readFileSync(pPath);
|
|
1545
|
+
writeStream.write(data);
|
|
1546
|
+
}
|
|
1547
|
+
await new Promise((resolve, reject) => {
|
|
1548
|
+
writeStream.end();
|
|
1549
|
+
writeStream.on("finish", resolve);
|
|
1550
|
+
writeStream.on("error", reject);
|
|
1551
|
+
});
|
|
1552
|
+
if (!fs4__default.default.existsSync(finalTempPath)) {
|
|
1553
|
+
throw new Error("Failed to create merged file");
|
|
1554
|
+
}
|
|
1555
|
+
const finalStats = fs4__default.default.statSync(finalTempPath);
|
|
1556
|
+
if (finalStats.size !== meta.fileSize) {
|
|
1557
|
+
throw new Error(`File size mismatch: expected ${meta.fileSize}, got ${finalStats.size}`);
|
|
1558
|
+
}
|
|
1559
|
+
const drive = new drive_default({
|
|
1560
|
+
owner: meta.owner,
|
|
1561
|
+
storageAccountId: meta.accountId || null,
|
|
1562
|
+
provider: { type: meta.providerName },
|
|
1563
|
+
name: meta.name,
|
|
1564
|
+
parentId: meta.parentId,
|
|
1565
|
+
order: 0,
|
|
1566
|
+
information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
|
|
1567
|
+
// path set by provider
|
|
1568
|
+
status: "UPLOADING",
|
|
1569
|
+
currentChunk: totalChunks,
|
|
1570
|
+
totalChunks
|
|
1571
|
+
});
|
|
1572
|
+
if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
|
|
1573
|
+
let ext = path3__default.default.extname(meta.name) || ".bin";
|
|
1574
|
+
ext = ext.replace(/[^a-zA-Z0-9.]/g, "").slice(0, 11);
|
|
1575
|
+
if (!ext.startsWith(".")) ext = ".bin";
|
|
1576
|
+
drive.information.path = path3__default.default.join(String(drive._id), `data${ext}`);
|
|
1577
|
+
}
|
|
1578
|
+
await drive.save();
|
|
1579
|
+
try {
|
|
1580
|
+
const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
|
|
1581
|
+
fs4__default.default.rmSync(uploadDir, { recursive: true, force: true });
|
|
1582
|
+
const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
|
|
1583
|
+
res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item }, statistic: { storage: newQuota } });
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
await drive_default.deleteOne({ _id: drive._id });
|
|
1586
|
+
throw err;
|
|
1587
|
+
}
|
|
1588
|
+
} else {
|
|
1589
|
+
const newQuota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
1590
|
+
if (chunkIndex === 0) {
|
|
1591
|
+
res.status(200).json({ status: 200, message: "Upload started", data: { type: "UPLOAD_STARTED", driveId: currentUploadId }, statistic: { storage: newQuota } });
|
|
1592
|
+
} else {
|
|
1593
|
+
res.status(200).json({ status: 200, message: "Chunk received", data: { type: "CHUNK_RECEIVED", driveId: currentUploadId, chunkIndex }, statistic: { storage: newQuota } });
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
} catch (e) {
|
|
1597
|
+
cleanupTempFiles(files);
|
|
1598
|
+
throw e;
|
|
1599
|
+
}
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
cleanupTempFiles(files);
|
|
1603
|
+
return res.status(400).json({ status: 400, message: "Invalid upload request" });
|
|
1604
|
+
}
|
|
1605
|
+
// ** 4. CANCEL UPLOAD **
|
|
1606
|
+
case "cancel": {
|
|
1607
|
+
const cancelData = cancelQuerySchema.safeParse(req.query);
|
|
1608
|
+
if (!cancelData.success) return res.status(400).json({ status: 400, message: "Invalid ID" });
|
|
1609
|
+
const { id } = cancelData.data;
|
|
1610
|
+
const tempUploadDir = path3__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads", id);
|
|
1611
|
+
if (fs4__default.default.existsSync(tempUploadDir)) {
|
|
1612
|
+
try {
|
|
1613
|
+
fs4__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
|
|
1614
|
+
} catch (e) {
|
|
1615
|
+
console.error("Failed to cleanup temp upload:", e);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return res.status(200).json({ status: 200, message: "Upload cancelled", data: null });
|
|
1619
|
+
}
|
|
1620
|
+
// ** 5. CREATE FOLDER **
|
|
1621
|
+
case "createFolder": {
|
|
1622
|
+
const folderData = createFolderBodySchema.safeParse(req.body);
|
|
1623
|
+
if (!folderData.success) return res.status(400).json({ status: 400, message: folderData.error.errors[0].message });
|
|
1624
|
+
const { name, parentId } = folderData.data;
|
|
1625
|
+
const item = await provider.createFolder(name, parentId ?? null, owner, accountId);
|
|
1626
|
+
return res.status(201).json({ status: 201, message: "Folder created", data: { item } });
|
|
1627
|
+
}
|
|
1628
|
+
// ** 5. DELETE **
|
|
1629
|
+
case "delete": {
|
|
1630
|
+
const deleteData = deleteQuerySchema.safeParse(req.query);
|
|
1631
|
+
if (!deleteData.success) return res.status(400).json({ status: 400, message: "Invalid ID" });
|
|
1632
|
+
const { id } = deleteData.data;
|
|
1633
|
+
const drive = await drive_default.findById(id);
|
|
1634
|
+
if (!drive) return res.status(404).json({ status: 404, message: "Not found" });
|
|
1635
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
1636
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
1637
|
+
try {
|
|
1638
|
+
await itemProvider.trash([id], owner, itemAccountId);
|
|
1639
|
+
} catch (e) {
|
|
1640
|
+
console.error("Provider trash failed:", e);
|
|
1641
|
+
}
|
|
1642
|
+
drive.trashedAt = /* @__PURE__ */ new Date();
|
|
1643
|
+
await drive.save();
|
|
1644
|
+
return res.status(200).json({ status: 200, message: "Moved to trash", data: null });
|
|
1645
|
+
}
|
|
1646
|
+
// ** 6. HARD DELETE **
|
|
1647
|
+
case "deletePermanent": {
|
|
1648
|
+
const deleteData = deleteQuerySchema.safeParse(req.query);
|
|
1649
|
+
if (!deleteData.success) return res.status(400).json({ status: 400, message: "Invalid ID" });
|
|
1650
|
+
const { id } = deleteData.data;
|
|
1651
|
+
await provider.delete([id], owner, accountId);
|
|
1652
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
1653
|
+
return res.status(200).json({ status: 200, message: "Deleted", statistic: { storage: quota } });
|
|
1654
|
+
}
|
|
1655
|
+
// ** 7. QUOTA **
|
|
1656
|
+
case "quota": {
|
|
1657
|
+
const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
|
|
1658
|
+
return res.status(200).json({
|
|
1659
|
+
status: 200,
|
|
1660
|
+
message: "Quota retrieved",
|
|
1661
|
+
data: { usedInBytes: quota.usedInBytes, totalInBytes: quota.quotaInBytes, availableInBytes: Math.max(0, quota.quotaInBytes - quota.usedInBytes), percentage: quota.quotaInBytes > 0 ? Math.round(quota.usedInBytes / quota.quotaInBytes * 100) : 0 },
|
|
1662
|
+
statistic: { storage: quota }
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
// ** 7B. TRASH **
|
|
1666
|
+
case "trash": {
|
|
1667
|
+
try {
|
|
1668
|
+
const { provider: trashProvider, accountId: trashAccountId } = await getProvider(req, owner);
|
|
1669
|
+
await trashProvider.syncTrash(owner, trashAccountId);
|
|
1670
|
+
} catch (e) {
|
|
1671
|
+
console.error("Trash sync failed", e);
|
|
1672
|
+
}
|
|
1673
|
+
const query = {
|
|
1674
|
+
owner,
|
|
1675
|
+
"provider.type": provider.name,
|
|
1676
|
+
storageAccountId: accountId || null,
|
|
1677
|
+
trashedAt: { $ne: null }
|
|
1678
|
+
};
|
|
1679
|
+
const items = await drive_default.find(query, {}, { sort: { trashedAt: -1 } });
|
|
1680
|
+
const plainItems = await Promise.all(items.map((item) => item.toClient()));
|
|
1681
|
+
return res.status(200).json({
|
|
1682
|
+
status: 200,
|
|
1683
|
+
message: "Trash items",
|
|
1684
|
+
data: { items: plainItems, hasMore: false }
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
// ** 7C. RESTORE **
|
|
1688
|
+
case "restore": {
|
|
1689
|
+
const restoreData = deleteQuerySchema.safeParse(req.query);
|
|
1690
|
+
if (!restoreData.success) return res.status(400).json({ status: 400, message: "Invalid ID" });
|
|
1691
|
+
const { id } = restoreData.data;
|
|
1692
|
+
const drive = await drive_default.findById(id);
|
|
1693
|
+
if (!drive) return res.status(404).json({ status: 404, message: "Not found" });
|
|
1694
|
+
let targetParentId = drive.parentId;
|
|
1695
|
+
if (targetParentId) {
|
|
1696
|
+
const parent = await drive_default.findById(targetParentId);
|
|
1697
|
+
if (parent?.trashedAt) {
|
|
1698
|
+
targetParentId = null;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
1702
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
1703
|
+
try {
|
|
1704
|
+
await itemProvider.untrash([id], owner, itemAccountId);
|
|
1705
|
+
if (targetParentId !== drive.parentId) {
|
|
1706
|
+
await itemProvider.move(id, targetParentId?.toString() ?? null, owner, itemAccountId);
|
|
1707
|
+
}
|
|
1708
|
+
} catch (e) {
|
|
1709
|
+
console.error("Provider restore failed:", e);
|
|
1710
|
+
}
|
|
1711
|
+
drive.trashedAt = null;
|
|
1712
|
+
drive.parentId = targetParentId;
|
|
1713
|
+
await drive.save();
|
|
1714
|
+
return res.status(200).json({
|
|
1715
|
+
status: 200,
|
|
1716
|
+
message: targetParentId === null && drive.parentId !== null ? "Restored to root (parent folder was trashed)" : "Restored",
|
|
1717
|
+
data: null
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
// ** 7D. MOVE **
|
|
1721
|
+
case "move": {
|
|
1722
|
+
const moveData = moveBodySchema.safeParse(req.body);
|
|
1723
|
+
if (!moveData.success) return res.status(400).json({ status: 400, message: "Invalid data" });
|
|
1724
|
+
const { ids, targetFolderId } = moveData.data;
|
|
1725
|
+
const items = [];
|
|
1726
|
+
const effectiveTargetId = targetFolderId === "root" || !targetFolderId ? null : targetFolderId;
|
|
1727
|
+
for (const id of ids) {
|
|
1728
|
+
try {
|
|
1729
|
+
const item = await provider.move(id, effectiveTargetId, owner, accountId);
|
|
1730
|
+
items.push(item);
|
|
1731
|
+
} catch (e) {
|
|
1732
|
+
console.error(`Failed to move item ${id}`, e);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return res.status(200).json({ status: 200, message: "Moved", data: { items } });
|
|
1736
|
+
}
|
|
1737
|
+
// ** 8. RENAME **
|
|
1738
|
+
case "rename": {
|
|
1739
|
+
const renameData = renameBodySchema.safeParse({ id: req.query.id, ...req.body });
|
|
1740
|
+
if (!renameData.success) return res.status(400).json({ status: 400, message: "Invalid data" });
|
|
1741
|
+
const { id, newName } = renameData.data;
|
|
1742
|
+
const item = await provider.rename(id, newName, owner, accountId);
|
|
1743
|
+
return res.status(200).json({ status: 200, message: "Renamed", data: { item } });
|
|
1744
|
+
}
|
|
1745
|
+
// ** 9. THUMBNAIL **
|
|
1746
|
+
case "thumbnail": {
|
|
1747
|
+
const thumbQuery = thumbnailQuerySchema.safeParse(req.query);
|
|
1748
|
+
if (!thumbQuery.success) return res.status(400).json({ status: 400, message: "Invalid params" });
|
|
1749
|
+
const { id } = thumbQuery.data;
|
|
1750
|
+
const drive = await drive_default.findById(id);
|
|
1751
|
+
if (!drive) return res.status(404).json({ status: 404, message: "Not found" });
|
|
1752
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
1753
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
1754
|
+
const stream = await itemProvider.getThumbnail(drive, itemAccountId);
|
|
1755
|
+
res.setHeader("Content-Type", "image/webp");
|
|
1756
|
+
if (config.cors?.enabled) {
|
|
1757
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1758
|
+
}
|
|
1759
|
+
stream.pipe(res);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
// ** 10. SERVE / DOWNLOAD **
|
|
1763
|
+
case "serve": {
|
|
1764
|
+
const serveQuery = serveQuerySchema.safeParse(req.query);
|
|
1765
|
+
if (!serveQuery.success) return res.status(400).json({ status: 400, message: "Invalid params" });
|
|
1766
|
+
const { id } = serveQuery.data;
|
|
1767
|
+
const drive = await drive_default.findById(id);
|
|
1768
|
+
if (!drive) return res.status(404).json({ status: 404, message: "Not found" });
|
|
1769
|
+
const itemProvider = drive.provider?.type === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
|
|
1770
|
+
const itemAccountId = drive.storageAccountId ? drive.storageAccountId.toString() : void 0;
|
|
1771
|
+
const { stream, mime, size } = await itemProvider.openStream(drive, itemAccountId);
|
|
1772
|
+
const safeFilename = sanitizeContentDispositionFilename(drive.name);
|
|
1773
|
+
res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
|
|
1774
|
+
res.setHeader("Content-Type", mime);
|
|
1775
|
+
if (config.cors?.enabled) {
|
|
1776
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1777
|
+
}
|
|
1778
|
+
if (size) res.setHeader("Content-Length", size);
|
|
1779
|
+
stream.pipe(res);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
default:
|
|
1783
|
+
res.status(400).json({ status: 400, message: `Unknown action: ${action}` });
|
|
1784
|
+
}
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
console.error(`[next-drive] Error handling action ${action}:`, error);
|
|
1787
|
+
res.status(500).json({ status: 500, message: error instanceof Error ? error.message : "Unknown error" });
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
exports.driveAPIHandler = driveAPIHandler;
|
|
1792
|
+
exports.driveConfiguration = driveConfiguration;
|
|
1793
|
+
exports.driveDelete = driveDelete;
|
|
1794
|
+
exports.driveFilePath = driveFilePath;
|
|
1795
|
+
exports.driveFileSchemaZod = driveFileSchemaZod;
|
|
1796
|
+
exports.driveGetUrl = driveGetUrl;
|
|
1797
|
+
exports.driveInfo = driveInfo;
|
|
1798
|
+
exports.driveList = driveList;
|
|
1799
|
+
exports.driveReadFile = driveReadFile;
|
|
1800
|
+
exports.driveUpload = driveUpload;
|
|
1801
|
+
exports.getDriveConfig = getDriveConfig;
|
|
1802
|
+
exports.getDriveInformation = getDriveInformation;
|
|
1803
|
+
//# sourceMappingURL=chunk-7PTOIRBL.cjs.map
|
|
1804
|
+
//# sourceMappingURL=chunk-7PTOIRBL.cjs.map
|