google-drive-mock 1.0.7 → 1.0.8
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/index.js +1 -1
- package/dist/routes/v2.js +204 -38
- package/dist/routes/v3.js +42 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/routes/v2.ts +247 -49
- package/src/routes/v3.ts +47 -0
- package/test/advanced_changes.test.ts +1 -1
- package/test/mime_types.test.ts +161 -0
- package/test/v2_missing_ops.test.ts +172 -0
- package/test/v2_routes.test.ts +28 -10
- package/test/v2_upload.test.ts +142 -0
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ const createApp = (config = {}) => {
|
|
|
45
45
|
next();
|
|
46
46
|
}));
|
|
47
47
|
app.use(express_1.default.json());
|
|
48
|
-
app.use(express_1.default.text({ type: ['multipart/mixed', 'multipart/related'] }));
|
|
48
|
+
app.use(express_1.default.text({ type: ['multipart/mixed', 'multipart/related', 'text/*', 'application/xml'] }));
|
|
49
49
|
// Batch Route
|
|
50
50
|
app.post('/batch', batch_1.handleBatchRequest);
|
|
51
51
|
app.post('/batch/drive/v3', batch_1.handleBatchRequest);
|
package/dist/routes/v2.js
CHANGED
|
@@ -17,6 +17,19 @@ const createV2Router = (config) => {
|
|
|
17
17
|
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, fileData), { name: name, mimeType: fileData.mimeType || "application/octet-stream", parents: fileData.parents || [] }));
|
|
18
18
|
res.status(200).json((0, mappers_1.toV2File)(newFile));
|
|
19
19
|
});
|
|
20
|
+
// V2 Generate IDs (Must come before /:fileId)
|
|
21
|
+
app.get('/drive/v2/files/generateIds', (req, res) => {
|
|
22
|
+
const count = parseInt(req.query.maxResults) || 10;
|
|
23
|
+
const ids = [];
|
|
24
|
+
for (let i = 0; i < count; i++) {
|
|
25
|
+
ids.push(Math.random().toString(36).substring(2, 15));
|
|
26
|
+
}
|
|
27
|
+
res.json({
|
|
28
|
+
kind: "drive#generatedIds",
|
|
29
|
+
ids: ids,
|
|
30
|
+
space: req.query.space || 'drive'
|
|
31
|
+
});
|
|
32
|
+
});
|
|
20
33
|
// V2 Files: Get
|
|
21
34
|
app.get('/drive/v2/files/:fileId', (req, res) => {
|
|
22
35
|
const fileId = req.params.fileId;
|
|
@@ -33,6 +46,9 @@ const createV2Router = (config) => {
|
|
|
33
46
|
res.setHeader('ETag', file.etag);
|
|
34
47
|
}
|
|
35
48
|
if (req.query.alt === 'media') {
|
|
49
|
+
if (file.mimeType) {
|
|
50
|
+
res.setHeader('Content-Type', file.mimeType);
|
|
51
|
+
}
|
|
36
52
|
if (file.content === undefined) {
|
|
37
53
|
res.send("");
|
|
38
54
|
return;
|
|
@@ -47,6 +63,43 @@ const createV2Router = (config) => {
|
|
|
47
63
|
}
|
|
48
64
|
res.json((0, mappers_1.toV2File)(file));
|
|
49
65
|
});
|
|
66
|
+
// V2 Export
|
|
67
|
+
app.get('/drive/v2/files/:fileId/export', (req, res) => {
|
|
68
|
+
const fileId = req.params.fileId;
|
|
69
|
+
if (typeof fileId !== 'string') {
|
|
70
|
+
res.status(400).send("Invalid file ID");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const file = store_1.driveStore.getFile(fileId);
|
|
74
|
+
if (!file) {
|
|
75
|
+
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Mock export: just return content. Real API validates mimeType compatibility.
|
|
79
|
+
res.send(file.content || "");
|
|
80
|
+
});
|
|
81
|
+
// V2 Watch
|
|
82
|
+
app.post('/drive/v2/files/:fileId/watch', (req, res) => {
|
|
83
|
+
const fileId = req.params.fileId;
|
|
84
|
+
if (typeof fileId !== 'string') {
|
|
85
|
+
res.status(400).send("Invalid file ID");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const file = store_1.driveStore.getFile(fileId);
|
|
89
|
+
if (!file) {
|
|
90
|
+
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Mock Channel response
|
|
94
|
+
res.json({
|
|
95
|
+
kind: "api#channel",
|
|
96
|
+
id: req.body.id || Math.random().toString(36).substring(7),
|
|
97
|
+
resourceId: fileId,
|
|
98
|
+
resourceUri: `${config.apiEndpoint}/drive/v2/files/${fileId}`,
|
|
99
|
+
token: req.body.token,
|
|
100
|
+
expiration: Date.now() + 3600000 // 1 hour
|
|
101
|
+
});
|
|
102
|
+
});
|
|
50
103
|
// V2 Files: Update (PUT)
|
|
51
104
|
app.put('/drive/v2/files/:fileId', (req, res) => {
|
|
52
105
|
const fileId = req.params.fileId;
|
|
@@ -69,6 +122,25 @@ const createV2Router = (config) => {
|
|
|
69
122
|
return;
|
|
70
123
|
}
|
|
71
124
|
}
|
|
125
|
+
// Handle addParents/removeParents
|
|
126
|
+
let parents = existingFile.parents || [];
|
|
127
|
+
const addParents = req.query.addParents;
|
|
128
|
+
const removeParents = req.query.removeParents;
|
|
129
|
+
if (addParents) {
|
|
130
|
+
const toAdd = addParents.split(',');
|
|
131
|
+
toAdd.forEach(id => {
|
|
132
|
+
if (!parents.includes(id))
|
|
133
|
+
parents.push(id);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (removeParents) {
|
|
137
|
+
const toRemove = removeParents.split(',');
|
|
138
|
+
parents = parents.filter(id => !toRemove.includes(id));
|
|
139
|
+
}
|
|
140
|
+
// Merge parents into updates if they were modified
|
|
141
|
+
if (addParents || removeParents) {
|
|
142
|
+
updates.parents = parents;
|
|
143
|
+
}
|
|
72
144
|
const updatedFile = store_1.driveStore.updateFile(fileId, updates);
|
|
73
145
|
res.json((0, mappers_1.toV2File)(updatedFile));
|
|
74
146
|
});
|
|
@@ -94,6 +166,25 @@ const createV2Router = (config) => {
|
|
|
94
166
|
return;
|
|
95
167
|
}
|
|
96
168
|
}
|
|
169
|
+
// Handle addParents/removeParents
|
|
170
|
+
let parents = existingFile.parents || [];
|
|
171
|
+
const addParents = req.query.addParents;
|
|
172
|
+
const removeParents = req.query.removeParents;
|
|
173
|
+
if (addParents) {
|
|
174
|
+
const toAdd = addParents.split(',');
|
|
175
|
+
toAdd.forEach(id => {
|
|
176
|
+
if (!parents.includes(id))
|
|
177
|
+
parents.push(id);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (removeParents) {
|
|
181
|
+
const toRemove = removeParents.split(',');
|
|
182
|
+
parents = parents.filter(id => !toRemove.includes(id));
|
|
183
|
+
}
|
|
184
|
+
// Merge parents into updates if they were modified
|
|
185
|
+
if (addParents || removeParents) {
|
|
186
|
+
updates.parents = parents;
|
|
187
|
+
}
|
|
97
188
|
const updatedFile = store_1.driveStore.updateFile(fileId, updates);
|
|
98
189
|
res.json((0, mappers_1.toV2File)(updatedFile));
|
|
99
190
|
});
|
|
@@ -192,38 +283,12 @@ const createV2Router = (config) => {
|
|
|
192
283
|
startPageToken: token
|
|
193
284
|
});
|
|
194
285
|
});
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
const uploadType = req.query.uploadType;
|
|
198
|
-
if (uploadType !== 'multipart') {
|
|
199
|
-
res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart is supported in this mock route" } });
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
const contentTypeHeader = req.headers['content-type'];
|
|
203
|
-
const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
|
|
204
|
-
if (!contentType || !contentType.includes('multipart/related')) {
|
|
205
|
-
res.status(400).json({ error: { code: 400, message: "Content-Type must be multipart/related" } });
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
209
|
-
if (!boundaryMatch) {
|
|
210
|
-
res.status(400).json({ error: { code: 400, message: "Multipart boundary missing" } });
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
let boundary = boundaryMatch[1];
|
|
214
|
-
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
|
215
|
-
boundary = boundary.substring(1, boundary.length - 1);
|
|
216
|
-
}
|
|
217
|
-
const rawBody = req.body;
|
|
218
|
-
if (typeof rawBody !== 'string') {
|
|
219
|
-
res.status(400).json({ error: { code: 400, message: "Body parsing failed" } });
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
286
|
+
// Helper for multipart parsing
|
|
287
|
+
const parseMultipart = (rawBody, boundary) => {
|
|
222
288
|
const parts = rawBody.split(`--${boundary}`);
|
|
223
289
|
const validParts = parts.filter(p => p.trim() !== '' && p.trim() !== '--');
|
|
224
290
|
if (validParts.length < 2) {
|
|
225
|
-
|
|
226
|
-
return;
|
|
291
|
+
return null;
|
|
227
292
|
}
|
|
228
293
|
const parsePart = (rawPart) => {
|
|
229
294
|
let splitIndex = rawPart.indexOf('\r\n\r\n');
|
|
@@ -233,7 +298,6 @@ const createV2Router = (config) => {
|
|
|
233
298
|
separatorLength = 2;
|
|
234
299
|
}
|
|
235
300
|
if (splitIndex === -1) {
|
|
236
|
-
console.log('V2 Upload: Could not find header/body separator in part', rawPart.substring(0, 50));
|
|
237
301
|
return null;
|
|
238
302
|
}
|
|
239
303
|
const headers = rawPart.substring(0, splitIndex).trim();
|
|
@@ -246,16 +310,14 @@ const createV2Router = (config) => {
|
|
|
246
310
|
const metadataPart = parsePart(validParts[0]);
|
|
247
311
|
const contentPart = parsePart(validParts[1]);
|
|
248
312
|
if (!metadataPart || !contentPart) {
|
|
249
|
-
|
|
250
|
-
return;
|
|
313
|
+
return null;
|
|
251
314
|
}
|
|
252
315
|
let metadata;
|
|
253
316
|
try {
|
|
254
317
|
metadata = JSON.parse(metadataPart.body);
|
|
255
318
|
}
|
|
256
319
|
catch (_a) {
|
|
257
|
-
|
|
258
|
-
return;
|
|
320
|
+
return null;
|
|
259
321
|
}
|
|
260
322
|
let content;
|
|
261
323
|
try {
|
|
@@ -264,10 +326,114 @@ const createV2Router = (config) => {
|
|
|
264
326
|
catch (_b) {
|
|
265
327
|
content = contentPart.body;
|
|
266
328
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
329
|
+
return { metadata, content };
|
|
330
|
+
};
|
|
331
|
+
// V2 Upload (POST)
|
|
332
|
+
app.post('/upload/drive/v2/files', (req, res) => {
|
|
333
|
+
const uploadType = req.query.uploadType;
|
|
334
|
+
if (uploadType === 'media') {
|
|
335
|
+
const rawBody = req.body;
|
|
336
|
+
// For simple upload, metadata is default
|
|
337
|
+
const name = "Untitled";
|
|
338
|
+
const newFile = store_1.driveStore.createFile({
|
|
339
|
+
name: name,
|
|
340
|
+
mimeType: req.headers['content-type'] || "application/octet-stream",
|
|
341
|
+
parents: [],
|
|
342
|
+
content: rawBody
|
|
343
|
+
});
|
|
344
|
+
res.status(200).json((0, mappers_1.toV2File)(newFile));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (uploadType === 'multipart') {
|
|
348
|
+
const contentTypeHeader = req.headers['content-type'];
|
|
349
|
+
const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
|
|
350
|
+
if (!contentType || !contentType.includes('multipart/related')) {
|
|
351
|
+
res.status(400).json({ error: { code: 400, message: "Content-Type must be multipart/related" } });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
355
|
+
if (!boundaryMatch) {
|
|
356
|
+
res.status(400).json({ error: { code: 400, message: "Multipart boundary missing" } });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let boundary = boundaryMatch[1];
|
|
360
|
+
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
|
361
|
+
boundary = boundary.substring(1, boundary.length - 1);
|
|
362
|
+
}
|
|
363
|
+
const rawBody = req.body;
|
|
364
|
+
if (typeof rawBody !== 'string') {
|
|
365
|
+
res.status(400).json({ error: { code: 400, message: "Body parsing failed" } });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const parsed = parseMultipart(rawBody, boundary);
|
|
369
|
+
if (!parsed) {
|
|
370
|
+
res.status(400).json({ error: { code: 400, message: "Invalid multipart body" } });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const { metadata, content } = parsed;
|
|
374
|
+
const fileData = (0, mappers_1.fromV2Update)(metadata);
|
|
375
|
+
const name = fileData.name || metadata.title || "Untitled";
|
|
376
|
+
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, fileData), { name: name, mimeType: fileData.mimeType || "application/octet-stream", parents: fileData.parents || [], content: content }));
|
|
377
|
+
res.status(200).json((0, mappers_1.toV2File)(newFile));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
res.status(400).json({ error: { code: 400, message: "Invalid uploadType" } });
|
|
381
|
+
});
|
|
382
|
+
// V2 Upload (PUT)
|
|
383
|
+
app.put('/upload/drive/v2/files/:fileId', (req, res) => {
|
|
384
|
+
const fileId = req.params.fileId;
|
|
385
|
+
if (typeof fileId !== 'string') {
|
|
386
|
+
res.status(400).send("Invalid file ID");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const existingFile = store_1.driveStore.getFile(fileId);
|
|
390
|
+
if (!existingFile) {
|
|
391
|
+
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const uploadType = req.query.uploadType;
|
|
395
|
+
if (uploadType === 'media') {
|
|
396
|
+
const rawBody = req.body;
|
|
397
|
+
const updatedFile = store_1.driveStore.updateFile(fileId, {
|
|
398
|
+
content: rawBody,
|
|
399
|
+
modifiedTime: new Date().toISOString()
|
|
400
|
+
});
|
|
401
|
+
res.status(200).json((0, mappers_1.toV2File)(updatedFile));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (uploadType === 'multipart') {
|
|
405
|
+
const contentTypeHeader = req.headers['content-type'];
|
|
406
|
+
const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
|
|
407
|
+
if (!contentType || !contentType.includes('multipart/related')) {
|
|
408
|
+
res.status(400).json({ error: { code: 400, message: "Content-Type must be multipart/related" } });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
412
|
+
if (!boundaryMatch) {
|
|
413
|
+
res.status(400).json({ error: { code: 400, message: "Multipart boundary missing" } });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
let boundary = boundaryMatch[1];
|
|
417
|
+
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
|
418
|
+
boundary = boundary.substring(1, boundary.length - 1);
|
|
419
|
+
}
|
|
420
|
+
const rawBody = req.body;
|
|
421
|
+
if (typeof rawBody !== 'string') {
|
|
422
|
+
res.status(400).json({ error: { code: 400, message: "Body parsing failed" } });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const parsed = parseMultipart(rawBody, boundary);
|
|
426
|
+
if (!parsed) {
|
|
427
|
+
res.status(400).json({ error: { code: 400, message: "Invalid multipart body" } });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const { metadata, content } = parsed;
|
|
431
|
+
const fileData = (0, mappers_1.fromV2Update)(metadata);
|
|
432
|
+
const updatedFile = store_1.driveStore.updateFile(fileId, Object.assign(Object.assign({}, fileData), { content: content, modifiedTime: new Date().toISOString() }));
|
|
433
|
+
res.status(200).json((0, mappers_1.toV2File)(updatedFile));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
res.status(400).json({ error: { code: 400, message: "Invalid uploadType" } });
|
|
271
437
|
});
|
|
272
438
|
// V2 Trash
|
|
273
439
|
app.post('/drive/v2/files/:fileId/trash', (req, res) => {
|
package/dist/routes/v3.js
CHANGED
|
@@ -215,6 +215,32 @@ const createV3Router = () => {
|
|
|
215
215
|
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, metadata), { content: content }));
|
|
216
216
|
res.status(200).json(newFile);
|
|
217
217
|
});
|
|
218
|
+
// Upload Files: Update (PATCH)
|
|
219
|
+
app.patch('/upload/drive/v3/files/:fileId', (req, res) => {
|
|
220
|
+
const fileId = req.params.fileId;
|
|
221
|
+
if (typeof fileId !== 'string') {
|
|
222
|
+
res.status(400).send("Invalid file ID");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const existingFile = store_1.driveStore.getFile(fileId);
|
|
226
|
+
if (!existingFile) {
|
|
227
|
+
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const uploadType = req.query.uploadType;
|
|
231
|
+
if (uploadType === 'media') {
|
|
232
|
+
const rawBody = req.body;
|
|
233
|
+
// V3 update content via media upload
|
|
234
|
+
const updatedFile = store_1.driveStore.updateFile(fileId, {
|
|
235
|
+
content: rawBody,
|
|
236
|
+
modifiedTime: new Date().toISOString()
|
|
237
|
+
});
|
|
238
|
+
res.status(200).json(updatedFile);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Add multipart support if needed, but media is primary for now
|
|
242
|
+
res.status(400).json({ error: { code: 400, message: "Only uploadType=media is currently supported for V3 PATCH upload" } });
|
|
243
|
+
});
|
|
218
244
|
// Files: Create (Standard)
|
|
219
245
|
app.post('/drive/v3/files', (req, res) => {
|
|
220
246
|
const body = req.body || {};
|
|
@@ -239,6 +265,22 @@ const createV3Router = () => {
|
|
|
239
265
|
res.status(400).json({ error: { code: 400, message: "Invalid field selection: etag" } });
|
|
240
266
|
return;
|
|
241
267
|
}
|
|
268
|
+
if (req.query.alt === 'media') {
|
|
269
|
+
if (file.mimeType) {
|
|
270
|
+
res.setHeader('Content-Type', file.mimeType);
|
|
271
|
+
}
|
|
272
|
+
if (file.content === undefined) {
|
|
273
|
+
res.send("");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (typeof file.content === 'object') {
|
|
277
|
+
res.json(file.content);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
res.send(file.content);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
242
284
|
res.json(file);
|
|
243
285
|
});
|
|
244
286
|
// Files: Update
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -36,7 +36,7 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
app.use(express.json());
|
|
39
|
-
app.use(express.text({ type: ['multipart/mixed', 'multipart/related'] }));
|
|
39
|
+
app.use(express.text({ type: ['multipart/mixed', 'multipart/related', 'text/*', 'application/xml'] }));
|
|
40
40
|
|
|
41
41
|
// Batch Route
|
|
42
42
|
app.post('/batch', handleBatchRequest);
|