google-drive-mock 0.0.1

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/.ENV_EXAMPLE ADDED
@@ -0,0 +1,15 @@
1
+ # To run tests against the real Google Drive API
2
+ # Copy this file to .ENV and fill in the values
3
+
4
+ # 'mock' (default) or 'real'
5
+ TEST_TARGET=real
6
+
7
+ # OAuth2 Access Token for your Google Account
8
+ # Get one via 'npm run example:login'
9
+ GDRIVE_TOKEN=ya29.a0...
10
+
11
+ # Optional: Simulate latency in ms (only works with TEST_TARGET=mock)
12
+ # LATENCY=50
13
+
14
+ # Client ID for the Google Login Example
15
+ # GDRIVE_CLIENT_ID=...
package/.aiexclude ADDED
@@ -0,0 +1,6 @@
1
+ node_modules/**
2
+ dist/**
3
+ .env
4
+ .git/**
5
+ coverage/**
6
+ test/__screenshots__/
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+ branches: [ main, master ]
8
+
9
+ jobs:
10
+ build-and-test:
11
+ runs-on: ubuntu-24.04
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Use Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '24'
20
+ cache: 'npm'
21
+
22
+ - name: Install dependencies
23
+ run: npm install
24
+
25
+ - name: Lint
26
+ run: npm run lint
27
+
28
+ - name: Build
29
+ run: npm run build
30
+
31
+ - name: Test (Node)
32
+ run: npm test
33
+
34
+ - name: Install Playwright Browsers
35
+ run: npx playwright@1.58.0 install chromium
36
+
37
+ - name: Test (Browser)
38
+ run: npm run test:browser
39
+
40
+ - name: Test Slow
41
+ run: npm run test:slow
@@ -0,0 +1,24 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-24.04
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: '24'
15
+ registry-url: 'https://registry.npmjs.org'
16
+ cache: 'npm'
17
+
18
+ - run: npm install
19
+ - run: npm run build
20
+ - run: npm run lint
21
+ - run: npm test
22
+ - run: npm publish
23
+ env:
24
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/AGENTS.md ADDED
@@ -0,0 +1,9 @@
1
+ # AI Instructions
2
+
3
+ Always run the following commands after making changes to the codebase to ensure integrity:
4
+
5
+ 1. `npm run build`
6
+ 2. `npm run lint`
7
+ 3. `npm test`
8
+ 4. `npm test:browser`
9
+ 5. `npm test:real`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Meyer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # google-drive-mock
2
+ ![google-drive-mock](google-drive-mock.png)
3
+
4
+ <br />
5
+
6
+ <p style="text-align: center;">
7
+ Mock-Server that simulates being google-drive.<br />
8
+ Used for testing the <a href="https://rxdb.info/" target="_blank">RxDB Google-Drive-Sync</a>.<br />
9
+ Mostly Vibe-Coded.<br />
10
+ </p>
11
+
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install google-drive-mock
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```typescript
22
+ import { startServer } from 'google-drive-mock';
23
+
24
+ // start the server
25
+ const port = 3000;
26
+ const server = startServer(port);
27
+
28
+ // Store a file
29
+ const createResponse = await fetch('http://localhost:3000/drive/v3/files', {
30
+ method: 'POST',
31
+ headers: {
32
+ 'Authorization': 'Bearer valid-token',
33
+ 'Content-Type': 'application/json'
34
+ },
35
+ body: JSON.stringify({
36
+ name: 'test-file.txt',
37
+ mimeType: 'text/plain'
38
+ })
39
+ });
40
+ const file = await createResponse.json();
41
+ console.log('Created File:', file);
42
+
43
+ // Read the file
44
+ const readResponse = await fetch(`http://localhost:3000/drive/v3/files/${file.id}`, {
45
+ method: 'GET',
46
+ headers: {
47
+ 'Authorization': 'Bearer valid-token'
48
+ }
49
+ });
50
+ const fileContent = await readResponse.json();
51
+ console.log('Read File:', fileContent);
52
+
53
+ // Stop the server
54
+ server.close();
55
+
56
+ ```
57
+
58
+ ## Tech
59
+
60
+ - TypeScript
61
+ - Express
62
+ - Vitest
63
+
64
+ ## Browser Testing
65
+
66
+ To run tests inside a headless browser (Chromium):
67
+
68
+ ```bash
69
+ npm run test:browser
70
+ ```
71
+
72
+ ## Real Google Drive API Testing
73
+
74
+ To run tests against the real Google Drive API instead of the mock:
75
+
76
+ 1. Create a `.ENV` file (see `.ENV_EXAMPLE`):
77
+ ```
78
+ TEST_TARGET=real
79
+ GDRIVE_TOKEN=your-access-token
80
+ ```
81
+ 2. Run tests:
82
+ ```bash
83
+ npm test:real
84
+ ```
85
+
86
+ ## Contributing
87
+
88
+ GitHub issues for this project are closed. If you find a bug, please create a Pull Request with a test case reproducing the issue.
89
+
@@ -0,0 +1,2 @@
1
+ import { Request, Response } from 'express';
2
+ export declare const handleBatchRequest: (req: Request, res: Response) => Response<any, Record<string, any>> | undefined;
package/dist/batch.js ADDED
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleBatchRequest = void 0;
4
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
+ const store_1 = require("./store");
6
+ const handleBatchRequest = (req, res) => {
7
+ const contentType = req.headers['content-type'];
8
+ if (!contentType || !contentType.includes('multipart/mixed')) {
9
+ return res.status(400).send('Content-Type must be multipart/mixed');
10
+ }
11
+ const boundaryMatch = contentType.match(/boundary=(.+)/);
12
+ if (!boundaryMatch) {
13
+ return res.status(400).send('Multipart boundary missing');
14
+ }
15
+ let boundary = boundaryMatch[1];
16
+ // Boundaries in header can be quoted
17
+ if (boundary.startsWith('"') && boundary.endsWith('"')) {
18
+ boundary = boundary.substring(1, boundary.length - 1);
19
+ }
20
+ const rawBody = req.body;
21
+ if (typeof rawBody !== 'string') {
22
+ return res.status(400).send('Body parsing failed');
23
+ }
24
+ const parts = parseMultipart(rawBody, boundary);
25
+ const responses = [];
26
+ for (const part of parts) {
27
+ const response = processPart(part);
28
+ responses.push(response);
29
+ }
30
+ const responseBoundary = `batch_${Math.random().toString(36).substring(2)}`;
31
+ const responseBody = buildMultipartResponse(responses, responseBoundary);
32
+ res.set('Content-Type', `multipart/mixed; boundary=${responseBoundary}`);
33
+ res.end(responseBody);
34
+ };
35
+ exports.handleBatchRequest = handleBatchRequest;
36
+ function parseMultipart(body, boundary) {
37
+ const parts = [];
38
+ // Split by --boundary
39
+ // Note: The last part ends with --boundary--
40
+ const rawParts = body.split(`--${boundary}`);
41
+ for (const rawPart of rawParts) {
42
+ // Skip empty or end parts
43
+ if (rawPart.trim() === '' || rawPart.trim() === '--')
44
+ continue;
45
+ // Parse outer headers
46
+ const sections = rawPart.trim().split(/\r?\n\r?\n/);
47
+ const headersSection = sections[0];
48
+ const rest = sections.slice(1);
49
+ let contentId = '';
50
+ const headerLines = headersSection.split(/\r?\n/);
51
+ for (const line of headerLines) {
52
+ if (line.toLowerCase().startsWith('content-id:')) {
53
+ contentId = line.split(':')[1].trim();
54
+ }
55
+ }
56
+ const httpContent = rest.join('\r\n\r\n'); // Reconstruct body if multiple parts?
57
+ if (!httpContent && sections.length < 2)
58
+ continue; // No body?
59
+ // Ideally, httpContent is the rest.
60
+ // But if we split by double newline, we might have split the inner body too.
61
+ // Better: Find first double newline index manually.
62
+ // ... (Rewriting loop to be safer)
63
+ const firstDoubleNewline = rawPart.indexOf('\r\n\r\n');
64
+ const firstDoubleNewlineLF = rawPart.indexOf('\n\n');
65
+ let splitIndex = -1;
66
+ let splitLen = 0;
67
+ if (firstDoubleNewline !== -1) {
68
+ splitIndex = firstDoubleNewline;
69
+ splitLen = 4;
70
+ }
71
+ else if (firstDoubleNewlineLF !== -1) {
72
+ splitIndex = firstDoubleNewlineLF;
73
+ splitLen = 2;
74
+ }
75
+ if (splitIndex === -1)
76
+ continue;
77
+ const headersStr = rawPart.substring(0, splitIndex).trim();
78
+ const bodyStr = rawPart.substring(splitIndex + splitLen); // No trim on body?
79
+ // Parse outer headers
80
+ const hLines = headersStr.split(/\r?\n/);
81
+ for (const line of hLines) {
82
+ if (line.toLowerCase().startsWith('content-id:')) {
83
+ contentId = line.split(':')[1].trim();
84
+ }
85
+ }
86
+ if (!bodyStr)
87
+ continue;
88
+ // Inner HTTP part
89
+ // Same logic for inner split
90
+ const innerSplitIndexCRLF = bodyStr.indexOf('\r\n\r\n');
91
+ const innerSplitIndexLF = bodyStr.indexOf('\n\n');
92
+ let innerSplitIndex = -1;
93
+ let innerSplitLen = 0;
94
+ if (innerSplitIndexCRLF !== -1) {
95
+ innerSplitIndex = innerSplitIndexCRLF;
96
+ innerSplitLen = 4;
97
+ }
98
+ else if (innerSplitIndexLF !== -1 && (innerSplitIndexCRLF === -1 || innerSplitIndexLF < innerSplitIndexCRLF)) {
99
+ innerSplitIndex = innerSplitIndexLF;
100
+ innerSplitLen = 2;
101
+ }
102
+ // If NO header terminator found in inner body, maybe no headers? (But request line exists)
103
+ // Request line is mandatory.
104
+ let requestLine = '';
105
+ let innerHeadersStr = '';
106
+ let httpBody = '';
107
+ if (innerSplitIndex !== -1) {
108
+ const head = bodyStr.substring(0, innerSplitIndex);
109
+ httpBody = bodyStr.substring(innerSplitIndex + innerSplitLen);
110
+ const lines = head.split(/\r?\n/);
111
+ requestLine = lines[0];
112
+ innerHeadersStr = lines.slice(1).join('\n');
113
+ }
114
+ else {
115
+ // Maybe no body? Just headers?
116
+ const lines = bodyStr.trim().split(/\r?\n/);
117
+ requestLine = lines[0];
118
+ innerHeadersStr = lines.slice(1).join('\n');
119
+ httpBody = '';
120
+ }
121
+ const [method, url] = requestLine.split(' ');
122
+ // Parse inner headers
123
+ const headers = {};
124
+ const innerHLines = innerHeadersStr.split(/\r?\n/);
125
+ for (const line of innerHLines) {
126
+ const [key, ...value] = line.split(':');
127
+ if (key)
128
+ headers[key.toLowerCase()] = value.join(':').trim();
129
+ }
130
+ let parsedBody;
131
+ if (httpBody && httpBody.trim()) {
132
+ try {
133
+ parsedBody = JSON.parse(httpBody);
134
+ }
135
+ catch (_a) {
136
+ parsedBody = httpBody;
137
+ }
138
+ }
139
+ // Clean URL (remove prefix if present, though clients usually send relative path)
140
+ // Ensure /drive/v3/files...
141
+ parts.push({
142
+ contentId,
143
+ method,
144
+ url,
145
+ headers,
146
+ body: parsedBody
147
+ });
148
+ }
149
+ return parts;
150
+ }
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
+ }
@@ -0,0 +1,7 @@
1
+ interface AppConfig {
2
+ serverLagBefore?: number;
3
+ serverLagAfter?: number;
4
+ }
5
+ declare const createApp: (config?: AppConfig) => import("express-serve-static-core").Express;
6
+ declare const startServer: (port: number, host?: string, config?: AppConfig) => import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>;
7
+ export { createApp, startServer };
package/dist/index.js ADDED
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.startServer = exports.createApp = void 0;
16
+ const express_1 = __importDefault(require("express"));
17
+ const store_1 = require("./store");
18
+ const batch_1 = require("./batch");
19
+ const createApp = (config = {}) => {
20
+ const app = (0, express_1.default)();
21
+ app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
22
+ if (config.serverLagBefore && config.serverLagBefore > 0) {
23
+ yield new Promise(resolve => setTimeout(resolve, config.serverLagBefore));
24
+ }
25
+ if (config.serverLagAfter && config.serverLagAfter > 0) {
26
+ const originalSend = res.send;
27
+ res.send = function (...args) {
28
+ setTimeout(() => {
29
+ originalSend.apply(res, args);
30
+ }, config.serverLagAfter);
31
+ return res;
32
+ };
33
+ }
34
+ next();
35
+ }));
36
+ app.use(express_1.default.json());
37
+ app.use(express_1.default.text({ type: 'multipart/mixed' }));
38
+ // Batch Route
39
+ app.post('/batch', batch_1.handleBatchRequest);
40
+ // Debug Route (for testing)
41
+ app.post('/debug/clear', (req, res) => {
42
+ store_1.driveStore.clear();
43
+ res.status(200).send('Cleared');
44
+ });
45
+ // Auth Middleware
46
+ const validTokens = ['valid-token', 'another-valid-token'];
47
+ app.use((req, res, next) => {
48
+ const authHeader = req.headers.authorization;
49
+ if (!authHeader) {
50
+ res.status(401).json({ error: { code: 401, message: "Unauthorized: No token provided" } });
51
+ return;
52
+ }
53
+ const token = authHeader.split(' ')[1];
54
+ if (!validTokens.includes(token)) {
55
+ res.status(401).json({ error: { code: 401, message: "Unauthorized: Invalid token" } });
56
+ return;
57
+ }
58
+ next();
59
+ });
60
+ // Middleware to simulate some Google API behaviors (optional, can be expanded)
61
+ // About
62
+ app.get('/drive/v3/about', (req, res) => {
63
+ const about = store_1.driveStore.getAbout();
64
+ res.json(Object.assign({ kind: "drive#about" }, about));
65
+ });
66
+ // Files: List
67
+ app.get('/drive/v3/files', (req, res) => {
68
+ const files = store_1.driveStore.listFiles();
69
+ res.json({
70
+ kind: "drive#fileList",
71
+ incompleteSearch: false,
72
+ files: files
73
+ });
74
+ });
75
+ // Files: Create
76
+ app.post('/drive/v3/files', (req, res) => {
77
+ const body = req.body;
78
+ if (!body || !body.name) {
79
+ res.status(400).json({ error: { code: 400, message: "Bad Request: Name is required" } });
80
+ return;
81
+ }
82
+ // Enforce Unique Name Constraint (Mock Behavior customization)
83
+ const existing = store_1.driveStore.listFiles().find(f => f.name === body.name);
84
+ if (existing) {
85
+ res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
86
+ return;
87
+ }
88
+ const newFile = store_1.driveStore.createFile({
89
+ name: body.name,
90
+ mimeType: body.mimeType || "application/octet-stream",
91
+ parents: body.parents || []
92
+ });
93
+ res.status(200).json(newFile);
94
+ });
95
+ // Files: Get
96
+ app.get('/drive/v3/files/:fileId', (req, res) => {
97
+ const fileId = req.params.fileId;
98
+ if (typeof fileId !== 'string') {
99
+ res.status(400).send("Invalid file ID");
100
+ return;
101
+ }
102
+ const file = store_1.driveStore.getFile(fileId);
103
+ if (!file) {
104
+ res.status(404).json({ error: { code: 404, message: "File not found" } });
105
+ return;
106
+ }
107
+ const etag = `"${file.version}"`;
108
+ res.setHeader('ETag', etag);
109
+ if (req.headers['if-none-match'] === etag) {
110
+ res.status(304).end();
111
+ return;
112
+ }
113
+ res.json(file);
114
+ });
115
+ // Files: Update
116
+ app.patch('/drive/v3/files/:fileId', (req, res) => {
117
+ const fileId = req.params.fileId;
118
+ if (typeof fileId !== 'string') {
119
+ res.status(400).send("Invalid file ID");
120
+ return;
121
+ }
122
+ const updates = req.body;
123
+ if (!updates) {
124
+ res.status(400).json({ error: { code: 400, message: "Bad Request: No updates provided" } });
125
+ return;
126
+ }
127
+ // Check for Precondition (If-Match)
128
+ // Note: Real Google Drive API V3 was observed to allow overwrites (status 200)
129
+ // on PATCH even with mismatching If-Match headers (likely due to ETag generation nuances).
130
+ // Relaxing Mock to match Real API behavior (Last Write Wins).
131
+ /*
132
+ const existingFile = driveStore.getFile(fileId);
133
+ if (existingFile) {
134
+ const ifMatch = req.headers['if-match'];
135
+ if (ifMatch && ifMatch !== '*' && ifMatch !== `"${existingFile.version}"`) {
136
+ res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
137
+ return;
138
+ }
139
+ }
140
+ */
141
+ const updatedFile = store_1.driveStore.updateFile(fileId, updates);
142
+ if (!updatedFile) {
143
+ res.status(404).json({ error: { code: 404, message: "File not found" } });
144
+ return;
145
+ }
146
+ res.json(updatedFile);
147
+ });
148
+ // Files: Delete
149
+ app.delete('/drive/v3/files/:fileId', (req, res) => {
150
+ const fileId = req.params.fileId;
151
+ if (typeof fileId !== 'string') {
152
+ res.status(400).send("Invalid file ID");
153
+ return;
154
+ }
155
+ // Check for Precondition (If-Match)
156
+ const existingFile = store_1.driveStore.getFile(fileId);
157
+ if (existingFile) {
158
+ const ifMatch = req.headers['if-match'];
159
+ if (ifMatch && ifMatch !== '*' && ifMatch !== `"${existingFile.version}"`) {
160
+ res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
161
+ return;
162
+ }
163
+ }
164
+ const deleted = store_1.driveStore.deleteFile(fileId);
165
+ if (!deleted) {
166
+ // According to Google API, delete might return 404 if not found, or 204 if successful (or 200).
167
+ // Docs says "If successful, this method returns an empty response body." usually 204.
168
+ // But if not found:
169
+ res.status(404).json({ error: { code: 404, message: "File not found" } });
170
+ return;
171
+ }
172
+ res.status(204).send();
173
+ });
174
+ return app;
175
+ };
176
+ exports.createApp = createApp;
177
+ const startServer = (port, host = 'localhost', config = {}) => {
178
+ const app = createApp(config);
179
+ return app.listen(port, host, () => {
180
+ console.log(`Server is running on http://${host}:${port}`);
181
+ });
182
+ };
183
+ exports.startServer = startServer;
184
+ if (require.main === module) {
185
+ startServer(3000);
186
+ }
@@ -0,0 +1,23 @@
1
+ export interface DriveFile {
2
+ id: string;
3
+ name: string;
4
+ mimeType: string;
5
+ kind: string;
6
+ parents?: string[];
7
+ version: number;
8
+ [key: string]: unknown;
9
+ }
10
+ export declare class DriveStore {
11
+ private files;
12
+ constructor();
13
+ createFile(file: Partial<DriveFile> & {
14
+ name: string;
15
+ }): DriveFile;
16
+ updateFile(id: string, updates: Partial<DriveFile>): DriveFile | null;
17
+ getFile(id: string): DriveFile | null;
18
+ deleteFile(id: string): boolean;
19
+ listFiles(): DriveFile[];
20
+ clear(): void;
21
+ getAbout(): object;
22
+ }
23
+ export declare const driveStore: DriveStore;