google-drive-mock 1.0.12 → 1.1.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.
@@ -4,8 +4,8 @@ on:
4
4
  workflow_dispatch:
5
5
  inputs:
6
6
  version:
7
- description: 'New Version (e.g. 1.0.0)'
8
- required: true
7
+ description: 'New Version (e.g. 1.0.0, or leave empty for minor)'
8
+ required: false
9
9
  type: string
10
10
 
11
11
  jobs:
@@ -25,7 +25,15 @@ jobs:
25
25
  cache: 'npm'
26
26
 
27
27
  - run: npm install
28
- - run: npm version ${{ inputs.version }} --no-git-tag-version
28
+ - name: Bump Version
29
+ id: bump
30
+ run: |
31
+ if [ -z "${{ inputs.version }}" ]; then
32
+ VERSION=$(npm version minor --no-git-tag-version)
33
+ else
34
+ VERSION=$(npm version ${{ inputs.version }} --no-git-tag-version)
35
+ fi
36
+ echo "version=${VERSION}" >> $GITHUB_OUTPUT
29
37
  - run: npm run build
30
38
  - run: npm run lint
31
39
  - run: npm test
@@ -38,5 +46,5 @@ jobs:
38
46
  git config --global user.name 'github-actions[bot]'
39
47
  git config --global user.email 'github-actions[bot]@users.noreply.github.com'
40
48
  git add package.json package-lock.json
41
- git commit -m "release: ${{ inputs.version }}"
49
+ git commit -m "release: ${{ steps.bump.outputs.version }}"
42
50
  git push
package/dist/batch.js CHANGED
@@ -4,6 +4,7 @@ exports.handleBatchRequest = void 0;
4
4
  /* eslint-disable @typescript-eslint/no-explicit-any */
5
5
  const store_1 = require("./store");
6
6
  const handleBatchRequest = (req, res) => {
7
+ // ... (unchanged)
7
8
  const contentType = req.headers['content-type'];
8
9
  if (!contentType || !contentType.includes('multipart/mixed')) {
9
10
  return res.status(400).send('Content-Type must be multipart/mixed');
@@ -13,7 +14,6 @@ const handleBatchRequest = (req, res) => {
13
14
  return res.status(400).send('Multipart boundary missing');
14
15
  }
15
16
  let boundary = boundaryMatch[1];
16
- // Boundaries in header can be quoted
17
17
  if (boundary.startsWith('"') && boundary.endsWith('"')) {
18
18
  boundary = boundary.substring(1, boundary.length - 1);
19
19
  }
@@ -24,7 +24,7 @@ const handleBatchRequest = (req, res) => {
24
24
  const parts = parseMultipart(rawBody, boundary);
25
25
  const responses = [];
26
26
  for (const part of parts) {
27
- const response = processPart(part);
27
+ const response = processPart(part, req);
28
28
  responses.push(response);
29
29
  }
30
30
  const responseBoundary = `batch_${Math.random().toString(36).substring(2)}`;
@@ -33,6 +33,127 @@ const handleBatchRequest = (req, res) => {
33
33
  res.end(responseBody);
34
34
  };
35
35
  exports.handleBatchRequest = handleBatchRequest;
36
+ // ... parseMultipart unchanged ...
37
+ function processPart(part, req) {
38
+ const fileIdMatch = part.url.match(/\/drive\/v3\/files\/([^/?]+)/);
39
+ const filesListMatch = part.url.match(/\/drive\/v3\/files/);
40
+ const aboutMatch = part.url.match(/\/drive\/v3\/about/);
41
+ // Simple query parser
42
+ const queryIdx = part.url.indexOf('?');
43
+ const query = {};
44
+ if (queryIdx !== -1) {
45
+ const queryStr = part.url.substring(queryIdx + 1);
46
+ queryStr.split('&').forEach(pair => {
47
+ const [key, val] = pair.split('=');
48
+ if (key)
49
+ query[key] = val ? decodeURIComponent(val) : '';
50
+ });
51
+ }
52
+ try {
53
+ // GET File
54
+ if (part.method === 'GET' && fileIdMatch) {
55
+ const fileId = fileIdMatch[1];
56
+ const file = store_1.driveStore.getFile(fileId);
57
+ if (!file)
58
+ return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
59
+ if (query['alt'] === 'media') {
60
+ // Return 302 Redirect to download URL
61
+ // We construct a fully qualified URL if possible, or relative.
62
+ // The Mock server is running on some port. We can try relative to the batch endpoint?
63
+ // Or better, we just use the path since most clients handle it.
64
+ // Real API returns absolute URL.
65
+ // We'll mimic Real API structure roughly: /drive/v3/files/{id}?alt=media
66
+ // But we need to serve the content on that GET route.
67
+ // `src/routes/v3.ts` already handles `GET /drive/v3/files/:id?alt=media`.
68
+ // We need the host. `req.headers.host` might work if passed through.
69
+ const host = req.headers.host || 'localhost';
70
+ const protocol = req.protocol || 'http';
71
+ const location = `${protocol}://${host}/drive/v3/files/${fileId}?alt=media`;
72
+ return {
73
+ contentId: part.contentId,
74
+ statusCode: 302,
75
+ headers: { 'Location': location },
76
+ body: null // No body for 302 usually, or empty
77
+ };
78
+ }
79
+ return { contentId: part.contentId, statusCode: 200, body: file };
80
+ }
81
+ // GET Files List
82
+ if (part.method === 'GET' && filesListMatch && !fileIdMatch) {
83
+ const files = store_1.driveStore.listFiles();
84
+ return {
85
+ contentId: part.contentId,
86
+ statusCode: 200,
87
+ body: {
88
+ kind: "drive#fileList",
89
+ incompleteSearch: false,
90
+ files: files
91
+ }
92
+ };
93
+ }
94
+ // GET About
95
+ if (part.method === 'GET' && aboutMatch) {
96
+ const about = store_1.driveStore.getAbout();
97
+ return {
98
+ contentId: part.contentId,
99
+ statusCode: 200,
100
+ body: Object.assign({ kind: "drive#about" }, about)
101
+ };
102
+ }
103
+ // POST Create File (Standard)
104
+ if (part.method === 'POST' && filesListMatch) {
105
+ if (!part.body || !part.body.name) {
106
+ return { contentId: part.contentId, statusCode: 400, body: { error: { code: 400, message: 'Name required' } } };
107
+ }
108
+ const newFile = store_1.driveStore.createFile({
109
+ name: part.body.name,
110
+ mimeType: part.body.mimeType,
111
+ parents: part.body.parents
112
+ });
113
+ return { contentId: part.contentId, statusCode: 200, body: newFile };
114
+ }
115
+ if (part.method === 'PATCH' && fileIdMatch) {
116
+ const fileId = fileIdMatch[1];
117
+ const updated = store_1.driveStore.updateFile(fileId, part.body);
118
+ if (!updated)
119
+ return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
120
+ return { contentId: part.contentId, statusCode: 200, body: updated };
121
+ }
122
+ if (part.method === 'DELETE' && fileIdMatch) {
123
+ const fileId = fileIdMatch[1];
124
+ const deleted = store_1.driveStore.deleteFile(fileId);
125
+ if (!deleted)
126
+ return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
127
+ return { contentId: part.contentId, statusCode: 204 }; // No body
128
+ }
129
+ return { contentId: part.contentId, statusCode: 404, body: { error: { message: "Not handler found for batch request url " + part.url } } };
130
+ }
131
+ catch (e) {
132
+ return { contentId: part.contentId, statusCode: 500, body: { error: { message: e.message } } };
133
+ }
134
+ }
135
+ function buildMultipartResponse(responses, boundary) {
136
+ let output = '';
137
+ for (const response of responses) {
138
+ output += `--${boundary}\r\n`;
139
+ output += `Content-Type: application/http\r\n`;
140
+ output += `Content-ID: ${response.contentId}\r\n\r\n`;
141
+ output += `HTTP/1.1 ${response.statusCode} ${(response.statusCode === 200 ? 'OK' : (response.statusCode === 302 ? 'Found' : ''))}\r\n`;
142
+ output += `Content-Type: application/json; charset=UTF-8\r\n`; // Always adding this might be weird for 302/204 but ok for now
143
+ if (response.headers) {
144
+ for (const [key, value] of Object.entries(response.headers)) {
145
+ output += `${key}: ${value}\r\n`;
146
+ }
147
+ }
148
+ output += `\r\n`; // End headers
149
+ if (response.body) {
150
+ output += JSON.stringify(response.body) + '\r\n';
151
+ }
152
+ output += '\r\n';
153
+ }
154
+ output += `--${boundary}--`;
155
+ return output;
156
+ }
36
157
  function parseMultipart(body, boundary) {
37
158
  const parts = [];
38
159
  // Split by --boundary
@@ -148,89 +269,3 @@ function parseMultipart(body, boundary) {
148
269
  }
149
270
  return parts;
150
271
  }
151
- function processPart(part) {
152
- // Simple logic dispatch
153
- // We only support /drive/v3/files operations basically
154
- // Helper to match URL (Simplified for mock)
155
- const fileIdMatch = part.url.match(/\/drive\/v3\/files\/([^/?]+)/);
156
- const filesListMatch = part.url.match(/\/drive\/v3\/files/); // Matches /drive/v3/files?q=... or just .../files
157
- const aboutMatch = part.url.match(/\/drive\/v3\/about/);
158
- try {
159
- // GET File
160
- if (part.method === 'GET' && fileIdMatch) {
161
- const fileId = fileIdMatch[1];
162
- const file = store_1.driveStore.getFile(fileId);
163
- if (!file)
164
- return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
165
- return { contentId: part.contentId, statusCode: 200, body: file };
166
- }
167
- // GET Files List
168
- if (part.method === 'GET' && filesListMatch && !fileIdMatch) {
169
- const files = store_1.driveStore.listFiles();
170
- return {
171
- contentId: part.contentId,
172
- statusCode: 200,
173
- body: {
174
- kind: "drive#fileList",
175
- incompleteSearch: false,
176
- files: files
177
- }
178
- };
179
- }
180
- // GET About
181
- if (part.method === 'GET' && aboutMatch) {
182
- const about = store_1.driveStore.getAbout();
183
- return {
184
- contentId: part.contentId,
185
- statusCode: 200,
186
- body: Object.assign({ kind: "drive#about" }, about)
187
- };
188
- }
189
- // POST Create File
190
- if (part.method === 'POST' && filesListMatch) {
191
- if (!part.body || !part.body.name) {
192
- return { contentId: part.contentId, statusCode: 400, body: { error: { code: 400, message: 'Name required' } } };
193
- }
194
- const newFile = store_1.driveStore.createFile({
195
- name: part.body.name,
196
- mimeType: part.body.mimeType,
197
- parents: part.body.parents
198
- });
199
- return { contentId: part.contentId, statusCode: 200, body: newFile };
200
- }
201
- if (part.method === 'PATCH' && fileIdMatch) {
202
- const fileId = fileIdMatch[1];
203
- const updated = store_1.driveStore.updateFile(fileId, part.body);
204
- if (!updated)
205
- return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
206
- return { contentId: part.contentId, statusCode: 200, body: updated };
207
- }
208
- if (part.method === 'DELETE' && fileIdMatch) {
209
- const fileId = fileIdMatch[1];
210
- const deleted = store_1.driveStore.deleteFile(fileId);
211
- if (!deleted)
212
- return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
213
- return { contentId: part.contentId, statusCode: 204 }; // No body
214
- }
215
- return { contentId: part.contentId, statusCode: 404, body: { error: { message: "Not handler found for batch request url " + part.url } } };
216
- }
217
- catch (e) {
218
- return { contentId: part.contentId, statusCode: 500, body: { error: { message: e.message } } };
219
- }
220
- }
221
- function buildMultipartResponse(responses, boundary) {
222
- let output = '';
223
- for (const response of responses) {
224
- output += `--${boundary}\r\n`;
225
- output += `Content-Type: application/http\r\n`;
226
- output += `Content-ID: ${response.contentId}\r\n\r\n`;
227
- output += `HTTP/1.1 ${response.statusCode} OK\r\n`; // Simplified status text
228
- output += `Content-Type: application/json; charset=UTF-8\r\n\r\n`;
229
- if (response.body) {
230
- output += JSON.stringify(response.body) + '\r\n';
231
- }
232
- output += '\r\n';
233
- }
234
- output += `--${boundary}--`;
235
- return output;
236
- }
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ const createApp = (config = {}) => {
26
26
  }
27
27
  const app = (0, express_1.default)();
28
28
  app.use((0, cors_1.default)({
29
- exposedHeaders: ['ETag']
29
+ exposedHeaders: ['ETag', 'Date', 'Content-Length']
30
30
  }));
31
31
  app.set('etag', false); // Disable default ETag generation to match Real API behavior
32
32
  app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
package/dist/routes/v3.js CHANGED
@@ -22,7 +22,7 @@ const createV3Router = () => {
22
22
  // Enhanced query parser for Mock
23
23
  // Recursive function to handle nested OR/AND logic with parens
24
24
  const evaluateQuery = (queryStr, file) => {
25
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
25
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
26
26
  const str = queryStr.trim();
27
27
  if (!str)
28
28
  return true;
@@ -136,6 +136,10 @@ const createV3Router = () => {
136
136
  const timeStr = (_k = part.match(/modifiedTime < '(.*)'/)) === null || _k === void 0 ? void 0 : _k[1];
137
137
  return !!(timeStr && new Date(file.modifiedTime) < new Date(timeStr));
138
138
  }
139
+ if (part.startsWith("modifiedTime = '")) {
140
+ const timeStr = (_l = part.match(/modifiedTime = '(.*)'/)) === null || _l === void 0 ? void 0 : _l[1];
141
+ return !!(timeStr && new Date(file.modifiedTime).toISOString() === new Date(timeStr).toISOString());
142
+ }
139
143
  // Fallback / Unknown
140
144
  return true;
141
145
  };
@@ -172,10 +176,33 @@ const createV3Router = () => {
172
176
  return 0;
173
177
  });
174
178
  }
179
+ // Pagination
180
+ const pageSize = req.query.pageSize ? parseInt(req.query.pageSize, 10) : 100; // Default 100
181
+ let skip = 0;
182
+ if (req.query.pageToken) {
183
+ try {
184
+ const tokenJson = Buffer.from(req.query.pageToken, 'base64').toString('utf-8');
185
+ const tokenData = JSON.parse(tokenJson);
186
+ if (typeof tokenData.skip === 'number') {
187
+ skip = tokenData.skip;
188
+ }
189
+ }
190
+ catch (_a) {
191
+ // Ignore invalid token, start from 0
192
+ }
193
+ }
194
+ const totalFiles = files.length;
195
+ const resultFiles = files.slice(skip, skip + pageSize);
196
+ let nextPageToken;
197
+ if (skip + pageSize < totalFiles) {
198
+ const nextSkip = skip + pageSize;
199
+ nextPageToken = Buffer.from(JSON.stringify({ skip: nextSkip })).toString('base64');
200
+ }
175
201
  res.json({
176
202
  kind: "drive#fileList",
177
203
  incompleteSearch: false,
178
- files: files
204
+ files: resultFiles,
205
+ nextPageToken
179
206
  });
180
207
  });
181
208
  // Changes: Get Start Page Token
@@ -419,8 +446,10 @@ const createV3Router = () => {
419
446
  res.status(400).send("Invalid file ID");
420
447
  return;
421
448
  }
422
- const updates = req.body;
423
- if (!updates) {
449
+ const updates = req.body || {};
450
+ const hasBody = Object.keys(updates).length > 0;
451
+ const hasQueryParams = req.query.addParents || req.query.removeParents;
452
+ if (!hasBody && !hasQueryParams) {
424
453
  res.status(400).json({ error: { code: 400, message: "Bad Request: No updates provided" } });
425
454
  return;
426
455
  }
@@ -429,6 +458,27 @@ const createV3Router = () => {
429
458
  res.status(404).json({ error: { code: 404, message: "File not found" } });
430
459
  return;
431
460
  }
461
+ const addParents = req.query.addParents;
462
+ if (addParents) {
463
+ const parentsToAdd = addParents.split(',');
464
+ const currentParents = updatedFile.parents || [];
465
+ const newParents = [...new Set([...currentParents, ...parentsToAdd])]; // Union
466
+ // Update the file with new parents
467
+ const result = store_1.driveStore.updateFile(fileId, { parents: newParents });
468
+ if (result) {
469
+ Object.assign(updatedFile, result);
470
+ }
471
+ }
472
+ const removeParents = req.query.removeParents;
473
+ if (removeParents) {
474
+ const parentsToRemove = removeParents.split(',');
475
+ const currentParents = updatedFile.parents || [];
476
+ const newParents = currentParents.filter(p => !parentsToRemove.includes(p));
477
+ const result = store_1.driveStore.updateFile(fileId, { parents: newParents });
478
+ if (result) {
479
+ Object.assign(updatedFile, result);
480
+ }
481
+ }
432
482
  res.json(updatedFile);
433
483
  });
434
484
  // Files: Delete
package/dist/store.js CHANGED
@@ -81,7 +81,7 @@ class DriveStore {
81
81
  }
82
82
  // Merge updates and increment version
83
83
  const newVersion = file.version + 1;
84
- const updatedFile = Object.assign(Object.assign(Object.assign(Object.assign({}, file), updates), statsUpdates), { 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: updates.modifiedTime || new Date().toISOString() });
85
85
  this.files.set(id, updatedFile);
86
86
  this.addChange(updatedFile);
87
87
  return updatedFile;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-drive-mock",
3
- "version": "1.0.12",
3
+ "version": "1.1.0",
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",
package/src/batch.ts CHANGED
@@ -10,13 +10,16 @@ interface BatchPart {
10
10
  body?: any;
11
11
  }
12
12
 
13
+
13
14
  interface BatchResponse {
14
15
  contentId: string;
15
16
  statusCode: number;
17
+ headers?: Record<string, string>;
16
18
  body?: any;
17
19
  }
18
20
 
19
21
  export const handleBatchRequest = (req: Request, res: Response) => {
22
+ // ... (unchanged)
20
23
  const contentType = req.headers['content-type'];
21
24
  if (!contentType || !contentType.includes('multipart/mixed')) {
22
25
  return res.status(400).send('Content-Type must be multipart/mixed');
@@ -27,7 +30,6 @@ export const handleBatchRequest = (req: Request, res: Response) => {
27
30
  return res.status(400).send('Multipart boundary missing');
28
31
  }
29
32
  let boundary = boundaryMatch[1];
30
- // Boundaries in header can be quoted
31
33
  if (boundary.startsWith('"') && boundary.endsWith('"')) {
32
34
  boundary = boundary.substring(1, boundary.length - 1);
33
35
  }
@@ -41,7 +43,7 @@ export const handleBatchRequest = (req: Request, res: Response) => {
41
43
  const responses: BatchResponse[] = [];
42
44
 
43
45
  for (const part of parts) {
44
- const response = processPart(part);
46
+ const response = processPart(part, req);
45
47
  responses.push(response);
46
48
  }
47
49
 
@@ -52,6 +54,148 @@ export const handleBatchRequest = (req: Request, res: Response) => {
52
54
  res.end(responseBody);
53
55
  };
54
56
 
57
+ // ... parseMultipart unchanged ...
58
+
59
+ function processPart(part: BatchPart, req: Request): BatchResponse {
60
+ const fileIdMatch = part.url.match(/\/drive\/v3\/files\/([^/?]+)/);
61
+ const filesListMatch = part.url.match(/\/drive\/v3\/files/);
62
+ const aboutMatch = part.url.match(/\/drive\/v3\/about/);
63
+
64
+ // Simple query parser
65
+ const queryIdx = part.url.indexOf('?');
66
+ const query: Record<string, string> = {};
67
+ if (queryIdx !== -1) {
68
+ const queryStr = part.url.substring(queryIdx + 1);
69
+ queryStr.split('&').forEach(pair => {
70
+ const [key, val] = pair.split('=');
71
+ if (key) query[key] = val ? decodeURIComponent(val) : '';
72
+ });
73
+ }
74
+
75
+ try {
76
+ // GET File
77
+ if (part.method === 'GET' && fileIdMatch) {
78
+ const fileId = fileIdMatch[1];
79
+ const file = driveStore.getFile(fileId);
80
+
81
+ if (!file) return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
82
+
83
+ if (query['alt'] === 'media') {
84
+ // Return 302 Redirect to download URL
85
+ // We construct a fully qualified URL if possible, or relative.
86
+ // The Mock server is running on some port. We can try relative to the batch endpoint?
87
+ // Or better, we just use the path since most clients handle it.
88
+ // Real API returns absolute URL.
89
+ // We'll mimic Real API structure roughly: /drive/v3/files/{id}?alt=media
90
+ // But we need to serve the content on that GET route.
91
+ // `src/routes/v3.ts` already handles `GET /drive/v3/files/:id?alt=media`.
92
+
93
+ // We need the host. `req.headers.host` might work if passed through.
94
+ const host = req.headers.host || 'localhost';
95
+ const protocol = req.protocol || 'http';
96
+ const location = `${protocol}://${host}/drive/v3/files/${fileId}?alt=media`;
97
+
98
+ return {
99
+ contentId: part.contentId,
100
+ statusCode: 302,
101
+ headers: { 'Location': location },
102
+ body: null // No body for 302 usually, or empty
103
+ };
104
+ }
105
+
106
+ return { contentId: part.contentId, statusCode: 200, body: file };
107
+ }
108
+
109
+ // GET Files List
110
+ if (part.method === 'GET' && filesListMatch && !fileIdMatch) {
111
+ const files = driveStore.listFiles();
112
+ return {
113
+ contentId: part.contentId,
114
+ statusCode: 200,
115
+ body: {
116
+ kind: "drive#fileList",
117
+ incompleteSearch: false,
118
+ files: files
119
+ }
120
+ };
121
+ }
122
+
123
+ // GET About
124
+ if (part.method === 'GET' && aboutMatch) {
125
+ const about = driveStore.getAbout();
126
+ return {
127
+ contentId: part.contentId,
128
+ statusCode: 200,
129
+ body: {
130
+ kind: "drive#about",
131
+ ...about
132
+ }
133
+ };
134
+ }
135
+
136
+
137
+ // POST Create File (Standard)
138
+ if (part.method === 'POST' && filesListMatch) {
139
+ if (!part.body || !part.body.name) {
140
+ return { contentId: part.contentId, statusCode: 400, body: { error: { code: 400, message: 'Name required' } } };
141
+ }
142
+ const newFile = driveStore.createFile({
143
+ name: part.body.name,
144
+ mimeType: part.body.mimeType,
145
+ parents: part.body.parents
146
+ });
147
+ return { contentId: part.contentId, statusCode: 200, body: newFile };
148
+ }
149
+
150
+ if (part.method === 'PATCH' && fileIdMatch) {
151
+ const fileId = fileIdMatch[1];
152
+ const updated = driveStore.updateFile(fileId, part.body);
153
+ if (!updated) return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
154
+ return { contentId: part.contentId, statusCode: 200, body: updated };
155
+ }
156
+
157
+ if (part.method === 'DELETE' && fileIdMatch) {
158
+ const fileId = fileIdMatch[1];
159
+ const deleted = driveStore.deleteFile(fileId);
160
+ if (!deleted) return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
161
+ return { contentId: part.contentId, statusCode: 204 }; // No body
162
+ }
163
+
164
+ return { contentId: part.contentId, statusCode: 404, body: { error: { message: "Not handler found for batch request url " + part.url } } };
165
+
166
+ } catch (e: any) {
167
+ return { contentId: part.contentId, statusCode: 500, body: { error: { message: e.message } } };
168
+ }
169
+ }
170
+
171
+ function buildMultipartResponse(responses: BatchResponse[], boundary: string): string {
172
+ let output = '';
173
+
174
+ for (const response of responses) {
175
+ output += `--${boundary}\r\n`;
176
+ output += `Content-Type: application/http\r\n`;
177
+ output += `Content-ID: ${response.contentId}\r\n\r\n`;
178
+
179
+ output += `HTTP/1.1 ${response.statusCode} ${(response.statusCode === 200 ? 'OK' : (response.statusCode === 302 ? 'Found' : ''))}\r\n`;
180
+ output += `Content-Type: application/json; charset=UTF-8\r\n`; // Always adding this might be weird for 302/204 but ok for now
181
+
182
+ if (response.headers) {
183
+ for (const [key, value] of Object.entries(response.headers)) {
184
+ output += `${key}: ${value}\r\n`;
185
+ }
186
+ }
187
+ output += `\r\n`; // End headers
188
+
189
+ if (response.body) {
190
+ output += JSON.stringify(response.body) + '\r\n';
191
+ }
192
+ output += '\r\n';
193
+ }
194
+
195
+ output += `--${boundary}--`;
196
+ return output;
197
+ }
198
+
55
199
  function parseMultipart(body: string, boundary: string): BatchPart[] {
56
200
  const parts: BatchPart[] = [];
57
201
  // Split by --boundary
@@ -185,102 +329,3 @@ function parseMultipart(body: string, boundary: string): BatchPart[] {
185
329
  return parts;
186
330
  }
187
331
 
188
- function processPart(part: BatchPart): BatchResponse {
189
- // Simple logic dispatch
190
- // We only support /drive/v3/files operations basically
191
-
192
- // Helper to match URL (Simplified for mock)
193
- const fileIdMatch = part.url.match(/\/drive\/v3\/files\/([^/?]+)/);
194
- const filesListMatch = part.url.match(/\/drive\/v3\/files/); // Matches /drive/v3/files?q=... or just .../files
195
- const aboutMatch = part.url.match(/\/drive\/v3\/about/);
196
-
197
- try {
198
- // GET File
199
- if (part.method === 'GET' && fileIdMatch) {
200
- const fileId = fileIdMatch[1];
201
- const file = driveStore.getFile(fileId);
202
- if (!file) return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
203
- return { contentId: part.contentId, statusCode: 200, body: file };
204
- }
205
-
206
- // GET Files List
207
- if (part.method === 'GET' && filesListMatch && !fileIdMatch) {
208
- const files = driveStore.listFiles();
209
- return {
210
- contentId: part.contentId,
211
- statusCode: 200,
212
- body: {
213
- kind: "drive#fileList",
214
- incompleteSearch: false,
215
- files: files
216
- }
217
- };
218
- }
219
-
220
- // GET About
221
- if (part.method === 'GET' && aboutMatch) {
222
- const about = driveStore.getAbout();
223
- return {
224
- contentId: part.contentId,
225
- statusCode: 200,
226
- body: {
227
- kind: "drive#about",
228
- ...about
229
- }
230
- };
231
- }
232
-
233
- // POST Create File
234
- if (part.method === 'POST' && filesListMatch) {
235
- if (!part.body || !part.body.name) {
236
- return { contentId: part.contentId, statusCode: 400, body: { error: { code: 400, message: 'Name required' } } };
237
- }
238
- const newFile = driveStore.createFile({
239
- name: part.body.name,
240
- mimeType: part.body.mimeType,
241
- parents: part.body.parents
242
- });
243
- return { contentId: part.contentId, statusCode: 200, body: newFile };
244
- }
245
-
246
- if (part.method === 'PATCH' && fileIdMatch) {
247
- const fileId = fileIdMatch[1];
248
- const updated = driveStore.updateFile(fileId, part.body);
249
- if (!updated) return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
250
- return { contentId: part.contentId, statusCode: 200, body: updated };
251
- }
252
-
253
- if (part.method === 'DELETE' && fileIdMatch) {
254
- const fileId = fileIdMatch[1];
255
- const deleted = driveStore.deleteFile(fileId);
256
- if (!deleted) return { contentId: part.contentId, statusCode: 404, body: { error: { code: 404, message: 'File not found' } } };
257
- return { contentId: part.contentId, statusCode: 204 }; // No body
258
- }
259
-
260
- return { contentId: part.contentId, statusCode: 404, body: { error: { message: "Not handler found for batch request url " + part.url } } };
261
-
262
- } catch (e: any) {
263
- return { contentId: part.contentId, statusCode: 500, body: { error: { message: e.message } } };
264
- }
265
- }
266
-
267
- function buildMultipartResponse(responses: BatchResponse[], boundary: string): string {
268
- let output = '';
269
-
270
- for (const response of responses) {
271
- output += `--${boundary}\r\n`;
272
- output += `Content-Type: application/http\r\n`;
273
- output += `Content-ID: ${response.contentId}\r\n\r\n`;
274
-
275
- output += `HTTP/1.1 ${response.statusCode} OK\r\n`; // Simplified status text
276
- output += `Content-Type: application/json; charset=UTF-8\r\n\r\n`;
277
-
278
- if (response.body) {
279
- output += JSON.stringify(response.body) + '\r\n';
280
- }
281
- output += '\r\n';
282
- }
283
-
284
- output += `--${boundary}--`;
285
- return output;
286
- }