google-drive-mock 1.0.9 → 1.0.11
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/AGENTS.md +3 -2
- package/dist/mappers.js +2 -0
- package/dist/routes/v2.js +17 -2
- package/dist/routes/v3.js +139 -67
- package/dist/store.d.ts +3 -0
- package/dist/store.js +60 -2
- package/package.json +2 -2
- package/scripts/check-token.ts +75 -0
- package/src/mappers.ts +2 -0
- package/src/routes/v2.ts +20 -2
- package/src/routes/v3.ts +153 -68
- package/src/store.ts +34 -0
- package/test/batch_and_query.test.ts +206 -0
- package/test/dates_and_sorting.test.ts +485 -0
- package/test/mime_types.test.ts +1 -1
- package/test/missing_fields.test.ts +125 -0
- package/test/multipart_behavior.test.ts +436 -0
- package/test/v2_content.test.ts +184 -0
package/AGENTS.md
CHANGED
|
@@ -10,5 +10,6 @@ After making changes to the codebase or to the tests, always run the following c
|
|
|
10
10
|
|
|
11
11
|
## Remember
|
|
12
12
|
|
|
13
|
-
Whenever the `npm test:real` fails, fix the tests to match the "real" backend.
|
|
14
|
-
Ensure that the behavior of the mock is exactly equal to the "real" backend.
|
|
13
|
+
- Whenever the `npm test:real` fails, fix the tests to match the "real" backend.
|
|
14
|
+
- Ensure that the behavior of the mock is exactly equal to the "real" backend.
|
|
15
|
+
- The header field "If-None-Match" does not work in google drive. Do never use it or assume it works.
|
package/dist/mappers.js
CHANGED
package/dist/routes/v2.js
CHANGED
|
@@ -331,8 +331,19 @@ const createV2Router = (config) => {
|
|
|
331
331
|
// V2 Upload (POST)
|
|
332
332
|
app.post('/upload/drive/v2/files', (req, res) => {
|
|
333
333
|
const uploadType = req.query.uploadType;
|
|
334
|
+
// V2 behavior: If-None-Match on POST seems to check against the collection or just strictly fail if an entity exists contextually.
|
|
335
|
+
// Tests show it returns 412 Precondition Failed when '*' is used.
|
|
336
|
+
const ifNoneMatch = req.headers['if-none-match'];
|
|
337
|
+
if (ifNoneMatch) {
|
|
338
|
+
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
334
341
|
if (uploadType === 'media') {
|
|
335
|
-
|
|
342
|
+
let rawBody = req.body;
|
|
343
|
+
// Handle edge case where express.json() parses empty body as {}
|
|
344
|
+
if (req.headers['content-length'] === '0' && JSON.stringify(rawBody) === '{}') {
|
|
345
|
+
rawBody = '';
|
|
346
|
+
}
|
|
336
347
|
// For simple upload, metadata is default
|
|
337
348
|
const name = "Untitled";
|
|
338
349
|
const newFile = store_1.driveStore.createFile({
|
|
@@ -401,7 +412,11 @@ const createV2Router = (config) => {
|
|
|
401
412
|
}
|
|
402
413
|
const uploadType = req.query.uploadType;
|
|
403
414
|
if (uploadType === 'media') {
|
|
404
|
-
|
|
415
|
+
let rawBody = req.body;
|
|
416
|
+
// Handle edge case where express.json() parses empty body as {}
|
|
417
|
+
if (req.headers['content-length'] === '0' && JSON.stringify(rawBody) === '{}') {
|
|
418
|
+
rawBody = '';
|
|
419
|
+
}
|
|
405
420
|
const updatedFile = store_1.driveStore.updateFile(fileId, {
|
|
406
421
|
content: rawBody,
|
|
407
422
|
modifiedTime: new Date().toISOString()
|
package/dist/routes/v3.js
CHANGED
|
@@ -20,54 +20,67 @@ const createV3Router = () => {
|
|
|
20
20
|
const orderBy = req.query.orderBy;
|
|
21
21
|
if (q) {
|
|
22
22
|
// Enhanced query parser for Mock
|
|
23
|
-
const
|
|
23
|
+
const orParts = q.split(' or ');
|
|
24
24
|
files = files.filter(file => {
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
25
|
+
return orParts.some(orPart => {
|
|
26
|
+
const andParts = orPart.split(' and ').map(p => p.trim());
|
|
27
|
+
return andParts.every(part => {
|
|
28
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
29
|
+
// name = '...'
|
|
30
|
+
if (part.startsWith("name = '")) {
|
|
31
|
+
const name = (_a = part.match(/name = '(.*)'/)) === null || _a === void 0 ? void 0 : _a[1];
|
|
32
|
+
// Handle escaped quotes if simple match fails, but simple regex here might be enough for now
|
|
33
|
+
// The user does name.replace("'", "\\'") but the regex (.*) is greedy and might consume escaped quotes correctly-ish
|
|
34
|
+
// but we are stripping the outer quotes.
|
|
35
|
+
// If user sends: name = 'foo\'bar', regex captures: foo\'bar
|
|
36
|
+
// We need to unescape? The user sends escaped string for the Query Parser.
|
|
37
|
+
// Drive API expects the string inside quotes to be the value.
|
|
38
|
+
// If `name = 'a\'b'`, the value is `a'b`.
|
|
39
|
+
// JSON.parse(`"${name}"`) might decode it if we treat it as JSON string? Use simple replace for now.
|
|
40
|
+
const finalName = name ? name.replace(/\\'/g, "'") : name;
|
|
41
|
+
return file.name === finalName;
|
|
42
|
+
}
|
|
43
|
+
// name contains '...'
|
|
44
|
+
if (part.startsWith("name contains '")) {
|
|
45
|
+
const token = (_b = part.match(/name contains '(.*)'/)) === null || _b === void 0 ? void 0 : _b[1];
|
|
46
|
+
const finalToken = token ? token.replace(/\\'/g, "'") : token;
|
|
47
|
+
return finalToken && file.name.includes(finalToken);
|
|
48
|
+
}
|
|
49
|
+
// 'ID' in parents
|
|
50
|
+
if (part.includes(" in parents")) {
|
|
51
|
+
const parentId = (_c = part.match(/'(.*)' in parents/)) === null || _c === void 0 ? void 0 : _c[1];
|
|
52
|
+
return parentId && ((_d = file.parents) === null || _d === void 0 ? void 0 : _d.includes(parentId));
|
|
53
|
+
}
|
|
54
|
+
// trashed = ...
|
|
55
|
+
if (part === "trashed = false") {
|
|
56
|
+
return file.trashed !== true;
|
|
57
|
+
}
|
|
58
|
+
if (part === "trashed = true") {
|
|
59
|
+
return file.trashed === true;
|
|
60
|
+
}
|
|
61
|
+
// mimeType = '...'
|
|
62
|
+
if (part.startsWith("mimeType = '")) {
|
|
63
|
+
const mime = (_e = part.match(/mimeType = '(.*)'/)) === null || _e === void 0 ? void 0 : _e[1];
|
|
64
|
+
return file.mimeType === mime;
|
|
65
|
+
}
|
|
66
|
+
// mimeType != '...'
|
|
67
|
+
if (part.startsWith("mimeType != '")) {
|
|
68
|
+
const mime = (_f = part.match(/mimeType != '(.*)'/)) === null || _f === void 0 ? void 0 : _f[1];
|
|
69
|
+
return file.mimeType !== mime;
|
|
70
|
+
}
|
|
71
|
+
// modifiedTime > '...'
|
|
72
|
+
if (part.startsWith("modifiedTime > '")) {
|
|
73
|
+
const timeStr = (_g = part.match(/modifiedTime > '(.*)'/)) === null || _g === void 0 ? void 0 : _g[1];
|
|
74
|
+
return timeStr && new Date(file.modifiedTime) > new Date(timeStr);
|
|
75
|
+
}
|
|
76
|
+
// modifiedTime < '...'
|
|
77
|
+
if (part.startsWith("modifiedTime < '")) {
|
|
78
|
+
const timeStr = (_h = part.match(/modifiedTime < '(.*)'/)) === null || _h === void 0 ? void 0 : _h[1];
|
|
79
|
+
return timeStr && new Date(file.modifiedTime) < new Date(timeStr);
|
|
80
|
+
}
|
|
81
|
+
// Ignore unknown filters for now
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
71
84
|
});
|
|
72
85
|
});
|
|
73
86
|
}
|
|
@@ -134,8 +147,23 @@ const createV3Router = () => {
|
|
|
134
147
|
// Upload Files Route
|
|
135
148
|
app.post('/upload/drive/v3/files', (req, res) => {
|
|
136
149
|
const uploadType = req.query.uploadType;
|
|
137
|
-
if (uploadType !== 'multipart') {
|
|
138
|
-
res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart is supported in this mock route" } });
|
|
150
|
+
if (uploadType !== 'multipart' && uploadType !== 'media') {
|
|
151
|
+
res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart or uploadType=media is supported in this mock route" } });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (uploadType === 'media') {
|
|
155
|
+
const rawBody = req.body;
|
|
156
|
+
// Handle edge case where express.json() parses empty body as {}
|
|
157
|
+
if (req.headers['content-length'] === '0' && JSON.stringify(rawBody) === '{}') {
|
|
158
|
+
// Empty body
|
|
159
|
+
}
|
|
160
|
+
const newFile = store_1.driveStore.createFile({
|
|
161
|
+
name: "Untitled",
|
|
162
|
+
mimeType: req.headers['content-type'] || "application/octet-stream",
|
|
163
|
+
parents: [],
|
|
164
|
+
content: typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody) // Handle body if parsed
|
|
165
|
+
});
|
|
166
|
+
res.status(200).json(newFile);
|
|
139
167
|
return;
|
|
140
168
|
}
|
|
141
169
|
const contentTypeHeader = req.headers['content-type'];
|
|
@@ -196,22 +224,6 @@ const createV3Router = () => {
|
|
|
196
224
|
catch (_b) {
|
|
197
225
|
content = contentPart.body;
|
|
198
226
|
}
|
|
199
|
-
const existing = store_1.driveStore.listFiles().find(f => {
|
|
200
|
-
if (f.name !== metadata.name)
|
|
201
|
-
return false;
|
|
202
|
-
// Filter trashed?
|
|
203
|
-
if (f.trashed)
|
|
204
|
-
return false;
|
|
205
|
-
const newParents = metadata.parents || [];
|
|
206
|
-
const existingParents = f.parents || [];
|
|
207
|
-
if (newParents.length === 0 && existingParents.length === 0)
|
|
208
|
-
return true;
|
|
209
|
-
return newParents.some((p) => existingParents.includes(p));
|
|
210
|
-
});
|
|
211
|
-
if (existing) {
|
|
212
|
-
res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
227
|
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, metadata), { content: content }));
|
|
216
228
|
res.status(200).json(newFile);
|
|
217
229
|
});
|
|
@@ -238,8 +250,68 @@ const createV3Router = () => {
|
|
|
238
250
|
res.status(200).json(updatedFile);
|
|
239
251
|
return;
|
|
240
252
|
}
|
|
241
|
-
|
|
242
|
-
|
|
253
|
+
const contentTypeHeader = req.headers['content-type'];
|
|
254
|
+
const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
|
|
255
|
+
// Check for multipart
|
|
256
|
+
if (contentType && contentType.includes('multipart/related')) {
|
|
257
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
258
|
+
if (!boundaryMatch) {
|
|
259
|
+
res.status(400).json({ error: { code: 400, message: "Multipart boundary missing" } });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
let boundary = boundaryMatch[1];
|
|
263
|
+
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
|
264
|
+
boundary = boundary.substring(1, boundary.length - 1);
|
|
265
|
+
}
|
|
266
|
+
const rawBody = req.body;
|
|
267
|
+
if (typeof rawBody !== 'string') {
|
|
268
|
+
res.status(400).json({ error: { code: 400, message: "Body parsing failed" } });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const parts = rawBody.split(`--${boundary}`);
|
|
272
|
+
const validParts = parts.filter(p => p.trim() !== '' && p.trim() !== '--');
|
|
273
|
+
if (validParts.length < 2) {
|
|
274
|
+
res.status(400).json({ error: { code: 400, message: "Invalid multipart body: expected at least metadata and content" } });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const parsePart = (rawPart) => {
|
|
278
|
+
const splitIndex = rawPart.indexOf('\r\n\r\n');
|
|
279
|
+
if (splitIndex === -1)
|
|
280
|
+
return null;
|
|
281
|
+
const headers = rawPart.substring(0, splitIndex).trim();
|
|
282
|
+
const body = rawPart.substring(splitIndex + 4);
|
|
283
|
+
return {
|
|
284
|
+
headers,
|
|
285
|
+
body: body.replace(/\r\n$/, '') // Remove trailing CRLF from part body
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
const metadataPart = parsePart(validParts[0]);
|
|
289
|
+
const contentPart = parsePart(validParts[1]);
|
|
290
|
+
if (!metadataPart || !contentPart) {
|
|
291
|
+
res.status(400).json({ error: { code: 400, message: "Failed to parse parts" } });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
let metadata;
|
|
295
|
+
try {
|
|
296
|
+
metadata = JSON.parse(metadataPart.body);
|
|
297
|
+
}
|
|
298
|
+
catch (_a) {
|
|
299
|
+
res.status(400).json({ error: { code: 400, message: "Invalid JSON in metadata part" } });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
let content;
|
|
303
|
+
try {
|
|
304
|
+
content = JSON.parse(contentPart.body);
|
|
305
|
+
}
|
|
306
|
+
catch (_b) {
|
|
307
|
+
content = contentPart.body;
|
|
308
|
+
}
|
|
309
|
+
// Perform update
|
|
310
|
+
const updatedFile = store_1.driveStore.updateFile(fileId, Object.assign(Object.assign({}, metadata), { content: content, modifiedTime: new Date().toISOString() }));
|
|
311
|
+
res.status(200).json(updatedFile);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
res.status(400).json({ error: { code: 400, message: "Only uploadType=media or multipart/related is supported for V3 PATCH upload" } });
|
|
243
315
|
});
|
|
244
316
|
// Files: Create (Standard)
|
|
245
317
|
app.post('/drive/v3/files', (req, res) => {
|
package/dist/store.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface DriveFile {
|
|
|
9
9
|
trashed: boolean;
|
|
10
10
|
createdTime: string;
|
|
11
11
|
modifiedTime: string;
|
|
12
|
+
size: string;
|
|
13
|
+
md5Checksum: string;
|
|
12
14
|
[key: string]: unknown;
|
|
13
15
|
}
|
|
14
16
|
export interface DriveChange {
|
|
@@ -38,6 +40,7 @@ export declare class DriveStore {
|
|
|
38
40
|
private files;
|
|
39
41
|
private changes;
|
|
40
42
|
constructor();
|
|
43
|
+
private calculateStats;
|
|
41
44
|
createFile(file: Partial<DriveFile> & {
|
|
42
45
|
name: string;
|
|
43
46
|
}): DriveFile;
|
package/dist/store.js
CHANGED
|
@@ -1,18 +1,71 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.driveStore = exports.DriveStore = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
4
38
|
class DriveStore {
|
|
5
39
|
constructor() {
|
|
6
40
|
this.files = new Map();
|
|
7
41
|
this.changes = [];
|
|
8
42
|
}
|
|
43
|
+
calculateStats(content) {
|
|
44
|
+
let buffer;
|
|
45
|
+
if (typeof content === 'string') {
|
|
46
|
+
buffer = Buffer.from(content);
|
|
47
|
+
}
|
|
48
|
+
else if (content === undefined || content === null) {
|
|
49
|
+
buffer = Buffer.from('');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
buffer = Buffer.from(JSON.stringify(content));
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
size: String(buffer.length),
|
|
56
|
+
md5Checksum: crypto.createHash('md5').update(buffer).digest('hex')
|
|
57
|
+
};
|
|
58
|
+
}
|
|
9
59
|
createFile(file) {
|
|
10
60
|
if (!file.name) {
|
|
11
61
|
throw new Error("File name is required");
|
|
12
62
|
}
|
|
13
63
|
const id = file.id || Math.random().toString(36).substring(7);
|
|
14
64
|
const now = new Date().toISOString();
|
|
15
|
-
const
|
|
65
|
+
const stats = this.calculateStats(file.content);
|
|
66
|
+
const newFile = Object.assign(Object.assign({ kind: "drive#file", mimeType: "application/octet-stream", trashed: false, createdTime: now, modifiedTime: now }, file), { id, version: 1, etag: "1",
|
|
67
|
+
// Ensure calculated stats override provided ones
|
|
68
|
+
size: stats.size, md5Checksum: stats.md5Checksum });
|
|
16
69
|
this.files.set(id, newFile);
|
|
17
70
|
this.addChange(newFile);
|
|
18
71
|
return newFile;
|
|
@@ -21,9 +74,14 @@ class DriveStore {
|
|
|
21
74
|
const file = this.files.get(id);
|
|
22
75
|
if (!file)
|
|
23
76
|
return null;
|
|
77
|
+
// If content is being updated, recalculate stats
|
|
78
|
+
let statsUpdates = {};
|
|
79
|
+
if (updates.content !== undefined) {
|
|
80
|
+
statsUpdates = this.calculateStats(updates.content);
|
|
81
|
+
}
|
|
24
82
|
// Merge updates and increment version
|
|
25
83
|
const newVersion = file.version + 1;
|
|
26
|
-
const updatedFile = Object.assign(Object.assign(Object.assign({}, file), updates), { version: newVersion, etag: String(newVersion), modifiedTime: new Date().toISOString() });
|
|
84
|
+
const updatedFile = Object.assign(Object.assign(Object.assign(Object.assign({}, file), updates), statsUpdates), { version: newVersion, etag: String(newVersion), modifiedTime: new Date().toISOString() });
|
|
27
85
|
this.files.set(id, updatedFile);
|
|
28
86
|
this.addChange(updatedFile);
|
|
29
87
|
return updatedFile;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-drive-mock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Mock-Server that simulates being google-drive. Used for testing.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test:slow": "LATENCY=20 vitest run",
|
|
13
13
|
"test:browser": "start-server-and-test dev http://localhost:3000 'TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser --no-file-parallelism'",
|
|
14
14
|
"test:browser:real": "TEST_TARGET=real BROWSER_ENABLED=true vitest run --browser",
|
|
15
|
-
"test:real": "TEST_TARGET=real vitest run",
|
|
15
|
+
"test:real": "ts-node scripts/check-token.ts && TEST_TARGET=real vitest run",
|
|
16
16
|
"example:login": "ts-node examples/serve-login.ts",
|
|
17
17
|
"lint": "eslint .",
|
|
18
18
|
"lint:fix": "eslint . --fix"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as https from 'https';
|
|
4
|
+
|
|
5
|
+
// Load .ENV manually to avoid devDependency issues if dotenv isn't available in this context,
|
|
6
|
+
// though project seems to use it.
|
|
7
|
+
const envPath = path.resolve(__dirname, '../.ENV');
|
|
8
|
+
if (fs.existsSync(envPath)) {
|
|
9
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
10
|
+
envContent.split('\n').forEach(line => {
|
|
11
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
12
|
+
if (match) {
|
|
13
|
+
const key = match[1].trim();
|
|
14
|
+
const value = match[2].trim().replace(/^['"]|['"]$/g, ''); // strip quotes
|
|
15
|
+
if (!process.env[key]) {
|
|
16
|
+
process.env[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const token = process.env.GDRIVE_TOKEN;
|
|
23
|
+
|
|
24
|
+
if (!token) {
|
|
25
|
+
console.error('❌ Error: GDRIVE_TOKEN not found in environment or .ENV file.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Simple check only if running real tests (though script is likely invoked specifically for that)
|
|
30
|
+
// The user asked to run this BEFORE test:real.
|
|
31
|
+
|
|
32
|
+
console.log('🔄 Verifying GDRIVE_TOKEN...');
|
|
33
|
+
|
|
34
|
+
const options = {
|
|
35
|
+
hostname: 'www.googleapis.com',
|
|
36
|
+
path: '/drive/v3/about?fields=user',
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: {
|
|
39
|
+
'Authorization': `Bearer ${token}`,
|
|
40
|
+
'User-Agent': 'node-script'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const req = https.request(options, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
|
|
47
|
+
res.on('data', (chunk) => {
|
|
48
|
+
data += chunk;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
res.on('end', () => {
|
|
52
|
+
if (res.statusCode === 200) {
|
|
53
|
+
try {
|
|
54
|
+
const body = JSON.parse(data);
|
|
55
|
+
console.log(`✅ Token is valid. User: ${body.user?.emailAddress || 'Unknown'}`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
} catch (e: unknown) {
|
|
58
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
59
|
+
console.error('❌ Error parsing response:', msg);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.error(`❌ Token verification failed. Tell the human to update the .ENV file with a valid token. Status: ${res.statusCode}`);
|
|
64
|
+
console.error('Response:', data);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.on('error', (e) => {
|
|
71
|
+
console.error(`❌ Request error: ${e.message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
req.end();
|
package/src/mappers.ts
CHANGED
|
@@ -28,6 +28,8 @@ export function toV2File(file: DriveFile): Record<string, unknown> {
|
|
|
28
28
|
isRoot: false // Mock simplification
|
|
29
29
|
})),
|
|
30
30
|
version: file.version,
|
|
31
|
+
fileSize: file.size,
|
|
32
|
+
md5Checksum: file.md5Checksum,
|
|
31
33
|
downloadUrl: `http://localhost/drive/v2/files/${file.id}?alt=media`
|
|
32
34
|
};
|
|
33
35
|
}
|
package/src/routes/v2.ts
CHANGED
|
@@ -375,8 +375,21 @@ export const createV2Router = (config: AppConfig) => {
|
|
|
375
375
|
app.post('/upload/drive/v2/files', (req: Request, res: Response) => {
|
|
376
376
|
const uploadType = req.query.uploadType as string;
|
|
377
377
|
|
|
378
|
+
// V2 behavior: If-None-Match on POST seems to check against the collection or just strictly fail if an entity exists contextually.
|
|
379
|
+
// Tests show it returns 412 Precondition Failed when '*' is used.
|
|
380
|
+
const ifNoneMatch = req.headers['if-none-match'];
|
|
381
|
+
if (ifNoneMatch) {
|
|
382
|
+
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
378
386
|
if (uploadType === 'media') {
|
|
379
|
-
|
|
387
|
+
let rawBody = req.body;
|
|
388
|
+
// Handle edge case where express.json() parses empty body as {}
|
|
389
|
+
if (req.headers['content-length'] === '0' && JSON.stringify(rawBody) === '{}') {
|
|
390
|
+
rawBody = '';
|
|
391
|
+
}
|
|
392
|
+
|
|
380
393
|
// For simple upload, metadata is default
|
|
381
394
|
const name = "Untitled";
|
|
382
395
|
|
|
@@ -466,7 +479,12 @@ export const createV2Router = (config: AppConfig) => {
|
|
|
466
479
|
const uploadType = req.query.uploadType as string;
|
|
467
480
|
|
|
468
481
|
if (uploadType === 'media') {
|
|
469
|
-
|
|
482
|
+
let rawBody = req.body;
|
|
483
|
+
// Handle edge case where express.json() parses empty body as {}
|
|
484
|
+
if (req.headers['content-length'] === '0' && JSON.stringify(rawBody) === '{}') {
|
|
485
|
+
rawBody = '';
|
|
486
|
+
}
|
|
487
|
+
|
|
470
488
|
const updatedFile = driveStore.updateFile(fileId, {
|
|
471
489
|
content: rawBody,
|
|
472
490
|
modifiedTime: new Date().toISOString()
|