google-drive-mock 1.0.6 → 1.0.7
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.d.ts +1 -4
- package/dist/index.js +9 -462
- package/dist/routes/v2.d.ts +2 -0
- package/dist/routes/v2.js +512 -0
- package/dist/routes/v3.d.ts +1 -0
- package/dist/routes/v3.js +279 -0
- package/dist/store.d.ts +16 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.js +2 -0
- package/package.json +2 -2
- package/src/index.ts +12 -549
- package/src/routes/v2.ts +596 -0
- package/src/routes/v3.ts +323 -0
- package/src/store.ts +17 -1
- package/src/types.ts +5 -0
- package/test/v2_routes.test.ts +223 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
serverLagBefore?: number;
|
|
3
|
-
serverLagAfter?: number;
|
|
4
|
-
}
|
|
1
|
+
import { AppConfig } from './types';
|
|
5
2
|
declare const createApp: (config?: AppConfig) => import("express-serve-static-core").Express;
|
|
6
3
|
declare const startServer: (port: number, host?: string, config?: AppConfig) => import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>;
|
|
7
4
|
export { createApp, startServer };
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,13 @@ const express_1 = __importDefault(require("express"));
|
|
|
17
17
|
const cors_1 = __importDefault(require("cors"));
|
|
18
18
|
const store_1 = require("./store");
|
|
19
19
|
const batch_1 = require("./batch");
|
|
20
|
-
const
|
|
20
|
+
const v2_1 = require("./routes/v2");
|
|
21
|
+
const v3_1 = require("./routes/v3");
|
|
21
22
|
const createApp = (config = {}) => {
|
|
23
|
+
// If apiEndpoint is not provided, default to localhost or empty (relative)
|
|
24
|
+
if (!config.apiEndpoint) {
|
|
25
|
+
config.apiEndpoint = "";
|
|
26
|
+
}
|
|
22
27
|
const app = (0, express_1.default)();
|
|
23
28
|
app.use((0, cors_1.default)({
|
|
24
29
|
exposedHeaders: ['ETag']
|
|
@@ -69,467 +74,9 @@ const createApp = (config = {}) => {
|
|
|
69
74
|
}
|
|
70
75
|
next();
|
|
71
76
|
});
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
app.
|
|
75
|
-
const about = store_1.driveStore.getAbout();
|
|
76
|
-
res.json(Object.assign({ kind: "drive#about" }, about));
|
|
77
|
-
});
|
|
78
|
-
// Files: List
|
|
79
|
-
app.get('/drive/v3/files', (req, res) => {
|
|
80
|
-
let files = store_1.driveStore.listFiles();
|
|
81
|
-
const q = req.query.q;
|
|
82
|
-
const orderBy = req.query.orderBy;
|
|
83
|
-
if (q) {
|
|
84
|
-
// Enhanced query parser for Mock
|
|
85
|
-
// Supports:
|
|
86
|
-
// - name = '...'
|
|
87
|
-
// - mimeType = '...'
|
|
88
|
-
// - trashed = true/false
|
|
89
|
-
// - 'ID' in parents
|
|
90
|
-
// - name contains '...'
|
|
91
|
-
const parts = q.split(' and ').map(p => p.trim());
|
|
92
|
-
files = files.filter(file => {
|
|
93
|
-
return parts.every(part => {
|
|
94
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
95
|
-
// name = '...'
|
|
96
|
-
if (part.startsWith("name = '")) {
|
|
97
|
-
const name = (_a = part.match(/name = '(.*)'/)) === null || _a === void 0 ? void 0 : _a[1];
|
|
98
|
-
return file.name === name;
|
|
99
|
-
}
|
|
100
|
-
// name contains '...'
|
|
101
|
-
if (part.startsWith("name contains '")) {
|
|
102
|
-
const token = (_b = part.match(/name contains '(.*)'/)) === null || _b === void 0 ? void 0 : _b[1];
|
|
103
|
-
return token && file.name.includes(token);
|
|
104
|
-
}
|
|
105
|
-
// 'ID' in parents
|
|
106
|
-
if (part.includes(" in parents")) {
|
|
107
|
-
const parentId = (_c = part.match(/'(.*)' in parents/)) === null || _c === void 0 ? void 0 : _c[1];
|
|
108
|
-
return parentId && ((_d = file.parents) === null || _d === void 0 ? void 0 : _d.includes(parentId));
|
|
109
|
-
}
|
|
110
|
-
// trashed = ...
|
|
111
|
-
if (part === "trashed = false") {
|
|
112
|
-
return file.trashed !== true;
|
|
113
|
-
}
|
|
114
|
-
if (part === "trashed = true") {
|
|
115
|
-
return file.trashed === true;
|
|
116
|
-
}
|
|
117
|
-
// mimeType = '...'
|
|
118
|
-
if (part.startsWith("mimeType = '")) {
|
|
119
|
-
const mime = (_e = part.match(/mimeType = '(.*)'/)) === null || _e === void 0 ? void 0 : _e[1];
|
|
120
|
-
return file.mimeType === mime;
|
|
121
|
-
}
|
|
122
|
-
// mimeType != '...'
|
|
123
|
-
if (part.startsWith("mimeType != '")) {
|
|
124
|
-
const mime = (_f = part.match(/mimeType != '(.*)'/)) === null || _f === void 0 ? void 0 : _f[1];
|
|
125
|
-
return file.mimeType !== mime;
|
|
126
|
-
}
|
|
127
|
-
// modifiedTime > '...'
|
|
128
|
-
if (part.startsWith("modifiedTime > '")) {
|
|
129
|
-
const timeStr = (_g = part.match(/modifiedTime > '(.*)'/)) === null || _g === void 0 ? void 0 : _g[1];
|
|
130
|
-
return timeStr && new Date(file.modifiedTime) > new Date(timeStr);
|
|
131
|
-
}
|
|
132
|
-
// modifiedTime < '...'
|
|
133
|
-
if (part.startsWith("modifiedTime < '")) {
|
|
134
|
-
const timeStr = (_h = part.match(/modifiedTime < '(.*)'/)) === null || _h === void 0 ? void 0 : _h[1];
|
|
135
|
-
return timeStr && new Date(file.modifiedTime) < new Date(timeStr);
|
|
136
|
-
}
|
|
137
|
-
// Ignore unknown filters for now
|
|
138
|
-
return true;
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
// Sorting (orderBy)
|
|
143
|
-
if (orderBy) {
|
|
144
|
-
// Basic support for single keys: 'folder,name', 'modifiedTime desc', etc.
|
|
145
|
-
// Splitting by comma
|
|
146
|
-
const sortKeys = orderBy.split(',').map(k => k.trim());
|
|
147
|
-
files.sort((a, b) => {
|
|
148
|
-
for (const keyDef of sortKeys) {
|
|
149
|
-
const [key, direction] = keyDef.split(' ');
|
|
150
|
-
const dir = direction || 'asc';
|
|
151
|
-
// Handle special virtual key 'folder'
|
|
152
|
-
if (key === 'folder') {
|
|
153
|
-
const aIsFolder = a.mimeType === 'application/vnd.google-apps.folder';
|
|
154
|
-
const bIsFolder = b.mimeType === 'application/vnd.google-apps.folder';
|
|
155
|
-
if (aIsFolder !== bIsFolder) {
|
|
156
|
-
// Folders first in 'folder' sort usually?
|
|
157
|
-
// Google docs say: "folder sets folders to appear before..."
|
|
158
|
-
const valA = aIsFolder ? 0 : 1;
|
|
159
|
-
const valB = bIsFolder ? 0 : 1;
|
|
160
|
-
if (valA !== valB)
|
|
161
|
-
return dir === 'desc' ? valB - valA : valA - valB;
|
|
162
|
-
}
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const valA = a[key];
|
|
166
|
-
const valB = b[key];
|
|
167
|
-
if (valA === undefined || valB === undefined)
|
|
168
|
-
return 0;
|
|
169
|
-
if (valA < valB)
|
|
170
|
-
return dir === 'desc' ? 1 : -1;
|
|
171
|
-
if (valA > valB)
|
|
172
|
-
return dir === 'desc' ? -1 : 1;
|
|
173
|
-
}
|
|
174
|
-
return 0;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
res.json({
|
|
178
|
-
kind: "drive#fileList",
|
|
179
|
-
incompleteSearch: false,
|
|
180
|
-
files: files
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
// Changes: Get Start Page Token
|
|
184
|
-
app.get('/drive/v3/changes/startPageToken', (req, res) => {
|
|
185
|
-
const token = store_1.driveStore.getStartPageToken();
|
|
186
|
-
res.json({
|
|
187
|
-
kind: "drive#startPageToken",
|
|
188
|
-
startPageToken: token
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
// Changes: List
|
|
192
|
-
app.get('/drive/v3/changes', (req, res) => {
|
|
193
|
-
const pageToken = req.query.pageToken;
|
|
194
|
-
if (!pageToken) {
|
|
195
|
-
res.status(400).json({ error: { code: 400, message: "Bad Request: pageToken is required" } });
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const result = store_1.driveStore.getChanges(pageToken);
|
|
199
|
-
res.json({
|
|
200
|
-
kind: "drive#changeList",
|
|
201
|
-
newStartPageToken: result.newStartPageToken,
|
|
202
|
-
nextPageToken: result.nextPageToken,
|
|
203
|
-
changes: result.changes
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
// Upload Files Route
|
|
207
|
-
app.post('/upload/drive/v3/files', (req, res) => {
|
|
208
|
-
const uploadType = req.query.uploadType;
|
|
209
|
-
if (uploadType !== 'multipart') {
|
|
210
|
-
res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart is supported in this mock route" } });
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const contentTypeHeader = req.headers['content-type'];
|
|
214
|
-
const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
|
|
215
|
-
if (!contentType || !contentType.includes('multipart/related')) {
|
|
216
|
-
res.status(400).json({ error: { code: 400, message: "Content-Type must be multipart/related" } });
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
220
|
-
if (!boundaryMatch) {
|
|
221
|
-
res.status(400).json({ error: { code: 400, message: "Multipart boundary missing" } });
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
let boundary = boundaryMatch[1];
|
|
225
|
-
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
|
226
|
-
boundary = boundary.substring(1, boundary.length - 1);
|
|
227
|
-
}
|
|
228
|
-
const rawBody = req.body;
|
|
229
|
-
if (typeof rawBody !== 'string') {
|
|
230
|
-
res.status(400).json({ error: { code: 400, message: "Body parsing failed" } });
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
// Simple Multipart Parsing
|
|
234
|
-
const parts = rawBody.split(`--${boundary}`);
|
|
235
|
-
// Part 0 is usually empty (preamble)
|
|
236
|
-
// Part 1 is Metadata
|
|
237
|
-
// Part 2 is Content
|
|
238
|
-
// Last part is --
|
|
239
|
-
const validParts = parts.filter(p => p.trim() !== '' && p.trim() !== '--');
|
|
240
|
-
if (validParts.length < 2) {
|
|
241
|
-
res.status(400).json({ error: { code: 400, message: "Invalid multipart body: expected at least metadata and content" } });
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const parsePart = (rawPart) => {
|
|
245
|
-
const splitIndex = rawPart.indexOf('\r\n\r\n');
|
|
246
|
-
if (splitIndex === -1)
|
|
247
|
-
return null;
|
|
248
|
-
const headers = rawPart.substring(0, splitIndex).trim();
|
|
249
|
-
const body = rawPart.substring(splitIndex + 4); // No trim at end to preserve content whitespace?
|
|
250
|
-
// Actually Multipart usually has \r\n at end before boundary, so we might want to trim that.
|
|
251
|
-
// But relying on split --boundary usually leaves the preceding \r\n attached to the body part?
|
|
252
|
-
// split uses the separator.
|
|
253
|
-
// "Part1\r\n--boundary\r\nPart2"
|
|
254
|
-
// Split by --boundary: ["Part1\r\n", "\r\nPart2"]
|
|
255
|
-
// So Part1 has a trailing \r\n.
|
|
256
|
-
return {
|
|
257
|
-
headers,
|
|
258
|
-
body: body.replace(/\r\n$/, '') // Remove trailing CRLF
|
|
259
|
-
};
|
|
260
|
-
};
|
|
261
|
-
const metadataPart = parsePart(validParts[0]);
|
|
262
|
-
const contentPart = parsePart(validParts[1]);
|
|
263
|
-
if (!metadataPart || !contentPart) {
|
|
264
|
-
res.status(400).json({ error: { code: 400, message: "Failed to parse parts" } });
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
let metadata;
|
|
268
|
-
try {
|
|
269
|
-
metadata = JSON.parse(metadataPart.body);
|
|
270
|
-
}
|
|
271
|
-
catch (_a) {
|
|
272
|
-
res.status(400).json({ error: { code: 400, message: "Invalid JSON in metadata part" } });
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
let content;
|
|
276
|
-
// Try to parse content as JSON if applicable, else keep as string?
|
|
277
|
-
// In the user request, it is JSON content.
|
|
278
|
-
// And store expects 'content' property to be anything.
|
|
279
|
-
try {
|
|
280
|
-
content = JSON.parse(contentPart.body);
|
|
281
|
-
}
|
|
282
|
-
catch (_b) {
|
|
283
|
-
content = contentPart.body;
|
|
284
|
-
}
|
|
285
|
-
// Create File
|
|
286
|
-
// Ensure name uniqueness check if needed (reusing logic from normal create)
|
|
287
|
-
const existing = store_1.driveStore.listFiles().find(f => {
|
|
288
|
-
if (f.name !== metadata.name)
|
|
289
|
-
return false;
|
|
290
|
-
// Filter trashed?
|
|
291
|
-
if (f.trashed)
|
|
292
|
-
return false;
|
|
293
|
-
const newParents = metadata.parents || [];
|
|
294
|
-
const existingParents = f.parents || [];
|
|
295
|
-
// If both new and existing have NO parents, they are both in root -> Conflict
|
|
296
|
-
if (newParents.length === 0 && existingParents.length === 0)
|
|
297
|
-
return true;
|
|
298
|
-
// Check intersection of parents
|
|
299
|
-
return newParents.some((p) => existingParents.includes(p));
|
|
300
|
-
});
|
|
301
|
-
if (existing) {
|
|
302
|
-
res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, metadata), { content: content }));
|
|
306
|
-
res.status(200).json(newFile);
|
|
307
|
-
});
|
|
308
|
-
// Files: Create (Standard)
|
|
309
|
-
app.post('/drive/v3/files', (req, res) => {
|
|
310
|
-
const body = req.body || {};
|
|
311
|
-
// Real API allows missing name (defaults to "Untitled"?) or just works.
|
|
312
|
-
// Parity: Allow missing name.
|
|
313
|
-
const name = body.name || "Untitled";
|
|
314
|
-
// Enforce Unique Name Constraint (Mock Behavior customization)
|
|
315
|
-
// Real API allows duplicates. Removing constraint for parity.
|
|
316
|
-
/*
|
|
317
|
-
const existing = driveStore.listFiles().find(f => {
|
|
318
|
-
if (f.name !== body.name) return false;
|
|
319
|
-
// ...
|
|
320
|
-
return newParents.some((p: string) => existingParents.includes(p));
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
if (existing) {
|
|
324
|
-
res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
*/
|
|
328
|
-
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, body), { name: name, mimeType: body.mimeType || "application/octet-stream", parents: body.parents || [] }));
|
|
329
|
-
res.status(200).json(newFile);
|
|
330
|
-
});
|
|
331
|
-
// Files: Get
|
|
332
|
-
app.get('/drive/v3/files/:fileId', (req, res) => {
|
|
333
|
-
const fileId = req.params.fileId;
|
|
334
|
-
if (typeof fileId !== 'string') {
|
|
335
|
-
res.status(400).send("Invalid file ID");
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
const file = store_1.driveStore.getFile(fileId);
|
|
339
|
-
if (!file) {
|
|
340
|
-
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
// Parity: Real V3 API returns 400 if 'etag' is requested in fields
|
|
344
|
-
const fields = req.query.fields;
|
|
345
|
-
if (fields && (fields.includes('etag') || fields.includes('kind,etag'))) {
|
|
346
|
-
res.status(400).json({ error: { code: 400, message: "Invalid field selection: etag" } });
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
// Mock does not return ETag header because Real API (v3) does not return it by default/in this context.
|
|
350
|
-
// res.setHeader('ETag', etag);
|
|
351
|
-
// Real API also ignores If-None-Match if ETag is not supported?
|
|
352
|
-
// match behavior: do nothing.
|
|
353
|
-
/*
|
|
354
|
-
if (req.headers['if-none-match'] === etag) {
|
|
355
|
-
res.status(304).end();
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
*/
|
|
359
|
-
res.json(file);
|
|
360
|
-
});
|
|
361
|
-
// Files: Update
|
|
362
|
-
app.patch('/drive/v3/files/:fileId', (req, res) => {
|
|
363
|
-
const fileId = req.params.fileId;
|
|
364
|
-
if (typeof fileId !== 'string') {
|
|
365
|
-
res.status(400).send("Invalid file ID");
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
const updates = req.body;
|
|
369
|
-
if (!updates) {
|
|
370
|
-
res.status(400).json({ error: { code: 400, message: "Bad Request: No updates provided" } });
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
// Check for Precondition (If-Match)
|
|
374
|
-
// Real Google Drive API V3 observed behavior: Ignores If-Match on PATCH (Last Write Wins).
|
|
375
|
-
// Mock matches this Parity.
|
|
376
|
-
/*
|
|
377
|
-
const existingFile = driveStore.getFile(fileId);
|
|
378
|
-
if (existingFile) {
|
|
379
|
-
const ifMatch = req.headers['if-match'];
|
|
380
|
-
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
381
|
-
// Also support quoted etag if user sends it
|
|
382
|
-
if (ifMatch !== `"${existingFile.etag}"`) {
|
|
383
|
-
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
*/
|
|
389
|
-
const updatedFile = store_1.driveStore.updateFile(fileId, updates);
|
|
390
|
-
if (!updatedFile) {
|
|
391
|
-
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
res.json(updatedFile);
|
|
395
|
-
});
|
|
396
|
-
// Files: Delete
|
|
397
|
-
app.delete('/drive/v3/files/:fileId', (req, res) => {
|
|
398
|
-
const fileId = req.params.fileId;
|
|
399
|
-
if (typeof fileId !== 'string') {
|
|
400
|
-
res.status(400).send("Invalid file ID");
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
// Check for Precondition (If-Match)
|
|
404
|
-
// Real API behavior: Ignores If-Match (returns 204 even on mismatch)
|
|
405
|
-
/*
|
|
406
|
-
const existingFile = driveStore.getFile(fileId);
|
|
407
|
-
if (existingFile) {
|
|
408
|
-
const ifMatch = req.headers['if-match'];
|
|
409
|
-
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
410
|
-
// Strict logic removed for Parity
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
*/
|
|
414
|
-
const deleted = store_1.driveStore.deleteFile(fileId);
|
|
415
|
-
if (!deleted) {
|
|
416
|
-
// According to Google API, delete might return 404 if not found, or 204 if successful (or 200).
|
|
417
|
-
// Docs says "If successful, this method returns an empty response body." usually 204.
|
|
418
|
-
// But if not found:
|
|
419
|
-
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
res.status(204).send();
|
|
423
|
-
});
|
|
424
|
-
// ==========================================
|
|
425
|
-
// Google Drive API V2 Routes
|
|
426
|
-
// ==========================================
|
|
427
|
-
// V2 Files: Create
|
|
428
|
-
app.post('/drive/v2/files', (req, res) => {
|
|
429
|
-
const v2Body = req.body || {};
|
|
430
|
-
const fileData = (0, mappers_1.fromV2Update)(v2Body);
|
|
431
|
-
// V2 typical defaults
|
|
432
|
-
const name = fileData.name || v2Body.title || "Untitled"; // Fallback if mapper missed it or explicit
|
|
433
|
-
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, fileData), { name: name, mimeType: fileData.mimeType || "application/octet-stream", parents: fileData.parents || [] }));
|
|
434
|
-
res.status(200).json((0, mappers_1.toV2File)(newFile));
|
|
435
|
-
});
|
|
436
|
-
// V2 Files: Get
|
|
437
|
-
app.get('/drive/v2/files/:fileId', (req, res) => {
|
|
438
|
-
const fileId = req.params.fileId;
|
|
439
|
-
if (typeof fileId !== 'string') {
|
|
440
|
-
res.status(400).send("Invalid file ID");
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
const file = store_1.driveStore.getFile(fileId);
|
|
444
|
-
if (!file) {
|
|
445
|
-
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
// V2 ETag handling - usually sends ETag header
|
|
449
|
-
if (file.etag) {
|
|
450
|
-
res.setHeader('ETag', file.etag);
|
|
451
|
-
}
|
|
452
|
-
res.json((0, mappers_1.toV2File)(file));
|
|
453
|
-
});
|
|
454
|
-
// V2 Files: Update (PUT)
|
|
455
|
-
app.put('/drive/v2/files/:fileId', (req, res) => {
|
|
456
|
-
const fileId = req.params.fileId;
|
|
457
|
-
if (typeof fileId !== 'string') {
|
|
458
|
-
res.status(400).send("Invalid file ID");
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
const v2Body = req.body || {};
|
|
462
|
-
const updates = (0, mappers_1.fromV2Update)(v2Body);
|
|
463
|
-
const existingFile = store_1.driveStore.getFile(fileId);
|
|
464
|
-
if (!existingFile) {
|
|
465
|
-
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
// Check for Precondition (If-Match)
|
|
469
|
-
const ifMatchHeader = req.headers['if-match'];
|
|
470
|
-
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
471
|
-
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
472
|
-
// Also support quoted etag if user sends it
|
|
473
|
-
// Internal etag might be "version", validation needs exact match
|
|
474
|
-
if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
|
|
475
|
-
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
const updatedFile = store_1.driveStore.updateFile(fileId, updates);
|
|
480
|
-
res.json((0, mappers_1.toV2File)(updatedFile));
|
|
481
|
-
});
|
|
482
|
-
// V2 Files: Patch (PATCH)
|
|
483
|
-
app.patch('/drive/v2/files/:fileId', (req, res) => {
|
|
484
|
-
const fileId = req.params.fileId;
|
|
485
|
-
if (typeof fileId !== 'string') {
|
|
486
|
-
res.status(400).send("Invalid file ID");
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const v2Body = req.body || {};
|
|
490
|
-
const updates = (0, mappers_1.fromV2Update)(v2Body);
|
|
491
|
-
const existingFile = store_1.driveStore.getFile(fileId);
|
|
492
|
-
if (!existingFile) {
|
|
493
|
-
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
// Check for Precondition (If-Match)
|
|
497
|
-
const ifMatchHeader = req.headers['if-match'];
|
|
498
|
-
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
499
|
-
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
500
|
-
if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
|
|
501
|
-
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
const updatedFile = store_1.driveStore.updateFile(fileId, updates);
|
|
506
|
-
res.json((0, mappers_1.toV2File)(updatedFile));
|
|
507
|
-
});
|
|
508
|
-
// V2 Files: Delete
|
|
509
|
-
app.delete('/drive/v2/files/:fileId', (req, res) => {
|
|
510
|
-
const fileId = req.params.fileId;
|
|
511
|
-
if (typeof fileId !== 'string') {
|
|
512
|
-
res.status(400).send("Invalid file ID");
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
const existingFile = store_1.driveStore.getFile(fileId);
|
|
516
|
-
// V2 specific: often returns 404 for not found, same as V3 check
|
|
517
|
-
if (!existingFile) {
|
|
518
|
-
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
// Check for Precondition (If-Match) - V2 respects this more often
|
|
522
|
-
const ifMatchHeader = req.headers['if-match'];
|
|
523
|
-
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
524
|
-
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
525
|
-
if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
|
|
526
|
-
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
store_1.driveStore.deleteFile(fileId);
|
|
531
|
-
res.status(204).send();
|
|
532
|
-
});
|
|
77
|
+
// Mount Routers
|
|
78
|
+
app.use((0, v3_1.createV3Router)());
|
|
79
|
+
app.use((0, v2_1.createV2Router)(config));
|
|
533
80
|
return app;
|
|
534
81
|
};
|
|
535
82
|
exports.createApp = createApp;
|