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 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
@@ -30,6 +30,8 @@ function toV2File(file) {
30
30
  isRoot: false // Mock simplification
31
31
  })),
32
32
  version: file.version,
33
+ fileSize: file.size,
34
+ md5Checksum: file.md5Checksum,
33
35
  downloadUrl: `http://localhost/drive/v2/files/${file.id}?alt=media`
34
36
  };
35
37
  }
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
- const rawBody = req.body;
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
- const rawBody = req.body;
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 parts = q.split(' and ').map(p => p.trim());
23
+ const orParts = q.split(' or ');
24
24
  files = files.filter(file => {
25
- return parts.every(part => {
26
- var _a, _b, _c, _d, _e, _f, _g, _h;
27
- // name = '...'
28
- if (part.startsWith("name = '")) {
29
- const name = (_a = part.match(/name = '(.*)'/)) === null || _a === void 0 ? void 0 : _a[1];
30
- return file.name === name;
31
- }
32
- // name contains '...'
33
- if (part.startsWith("name contains '")) {
34
- const token = (_b = part.match(/name contains '(.*)'/)) === null || _b === void 0 ? void 0 : _b[1];
35
- return token && file.name.includes(token);
36
- }
37
- // 'ID' in parents
38
- if (part.includes(" in parents")) {
39
- const parentId = (_c = part.match(/'(.*)' in parents/)) === null || _c === void 0 ? void 0 : _c[1];
40
- return parentId && ((_d = file.parents) === null || _d === void 0 ? void 0 : _d.includes(parentId));
41
- }
42
- // trashed = ...
43
- if (part === "trashed = false") {
44
- return file.trashed !== true;
45
- }
46
- if (part === "trashed = true") {
47
- return file.trashed === true;
48
- }
49
- // mimeType = '...'
50
- if (part.startsWith("mimeType = '")) {
51
- const mime = (_e = part.match(/mimeType = '(.*)'/)) === null || _e === void 0 ? void 0 : _e[1];
52
- return file.mimeType === mime;
53
- }
54
- // mimeType != '...'
55
- if (part.startsWith("mimeType != '")) {
56
- const mime = (_f = part.match(/mimeType != '(.*)'/)) === null || _f === void 0 ? void 0 : _f[1];
57
- return file.mimeType !== mime;
58
- }
59
- // modifiedTime > '...'
60
- if (part.startsWith("modifiedTime > '")) {
61
- const timeStr = (_g = part.match(/modifiedTime > '(.*)'/)) === null || _g === void 0 ? void 0 : _g[1];
62
- return timeStr && new Date(file.modifiedTime) > new Date(timeStr);
63
- }
64
- // modifiedTime < '...'
65
- if (part.startsWith("modifiedTime < '")) {
66
- const timeStr = (_h = part.match(/modifiedTime < '(.*)'/)) === null || _h === void 0 ? void 0 : _h[1];
67
- return timeStr && new Date(file.modifiedTime) < new Date(timeStr);
68
- }
69
- // Ignore unknown filters for now
70
- return true;
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
- // 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" } });
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 newFile = Object.assign(Object.assign({ kind: "drive#file", mimeType: "application/octet-stream", trashed: false, createdTime: now, modifiedTime: now }, file), { id, version: 1, etag: "1" });
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.9",
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
- const rawBody = req.body;
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
- const rawBody = req.body;
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()