@juppytt/fws 0.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.
@@ -0,0 +1,342 @@
1
+ import { Router } from 'express';
2
+ import { getStore } from '../../store/index.js';
3
+ import { generateId } from '../../util/id.js';
4
+
5
+ export function driveRoutes(): Router {
6
+ const r = Router();
7
+ const PREFIX = '/drive/v3';
8
+
9
+ // GET about
10
+ r.get(`${PREFIX}/about`, (_req, res) => {
11
+ const store = getStore();
12
+ const userEmail = store.gmail.profile.emailAddress;
13
+ res.json({
14
+ kind: 'drive#about',
15
+ user: {
16
+ displayName: 'Test User',
17
+ emailAddress: userEmail,
18
+ kind: 'drive#user',
19
+ me: true,
20
+ },
21
+ storageQuota: {
22
+ limit: '16106127360',
23
+ usage: '0',
24
+ usageInDrive: '0',
25
+ usageInDriveTrash: '0',
26
+ },
27
+ });
28
+ });
29
+
30
+ // LIST files
31
+ r.get(`${PREFIX}/files`, (req, res) => {
32
+ const store = getStore();
33
+ let files = Object.values(store.drive.files);
34
+
35
+ // Filter by q
36
+ const q = req.query.q as string | undefined;
37
+ if (q) {
38
+ files = filterDriveQuery(files, q);
39
+ }
40
+
41
+ // Default: hide trashed
42
+ if (!q || !q.includes('trashed')) {
43
+ files = files.filter(f => !f.trashed);
44
+ }
45
+
46
+ // Sort
47
+ const orderBy = req.query.orderBy as string | undefined;
48
+ if (orderBy) {
49
+ const [field, dir] = orderBy.split(' ');
50
+ const desc = dir === 'desc';
51
+ files.sort((a: any, b: any) => {
52
+ const av = a[field] || '';
53
+ const bv = b[field] || '';
54
+ const cmp = av < bv ? -1 : av > bv ? 1 : 0;
55
+ return desc ? -cmp : cmp;
56
+ });
57
+ }
58
+
59
+ const pageSize = Math.min(parseInt(req.query.pageSize as string) || 100, 1000);
60
+ files = files.slice(0, pageSize);
61
+
62
+ res.json({
63
+ kind: 'drive#fileList',
64
+ files: files.map(f => ({
65
+ kind: f.kind,
66
+ id: f.id,
67
+ name: f.name,
68
+ mimeType: f.mimeType,
69
+ })),
70
+ incompleteSearch: false,
71
+ });
72
+ });
73
+
74
+ // CREATE file
75
+ r.post(`${PREFIX}/files`, (req, res) => {
76
+ const store = getStore();
77
+ const id = generateId();
78
+ const now = new Date().toISOString();
79
+ const userEmail = store.gmail.profile.emailAddress;
80
+
81
+ const file = {
82
+ kind: 'drive#file' as const,
83
+ id,
84
+ name: req.body.name || 'Untitled',
85
+ mimeType: req.body.mimeType || 'application/octet-stream',
86
+ parents: req.body.parents || ['root'],
87
+ createdTime: now,
88
+ modifiedTime: now,
89
+ size: req.body.size,
90
+ trashed: false,
91
+ starred: req.body.starred || false,
92
+ owners: [{ emailAddress: userEmail, displayName: 'Test User' }],
93
+ description: req.body.description,
94
+ };
95
+
96
+ store.drive.files[id] = file;
97
+ res.json(file);
98
+ });
99
+
100
+ // GET file
101
+ r.get(`${PREFIX}/files/:fileId`, (req, res) => {
102
+ const file = getStore().drive.files[req.params.fileId];
103
+ if (!file) {
104
+ return res.status(404).json({
105
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
106
+ });
107
+ }
108
+ res.json(file);
109
+ });
110
+
111
+ // PATCH file
112
+ r.patch(`${PREFIX}/files/:fileId`, (req, res) => {
113
+ const store = getStore();
114
+ const file = store.drive.files[req.params.fileId];
115
+ if (!file) {
116
+ return res.status(404).json({
117
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
118
+ });
119
+ }
120
+ Object.assign(file, req.body, { id: file.id, kind: file.kind });
121
+ file.modifiedTime = new Date().toISOString();
122
+ res.json(file);
123
+ });
124
+
125
+ // EMPTY TRASH (must be before DELETE /files/:fileId)
126
+ r.delete(`${PREFIX}/files/trash`, (_req, res) => {
127
+ const store = getStore();
128
+ for (const [id, file] of Object.entries(store.drive.files)) {
129
+ if (file.trashed) {
130
+ delete store.drive.files[id];
131
+ }
132
+ }
133
+ res.status(204).send();
134
+ });
135
+
136
+ // DELETE file
137
+ r.delete(`${PREFIX}/files/:fileId`, (req, res) => {
138
+ const store = getStore();
139
+ if (!store.drive.files[req.params.fileId]) {
140
+ return res.status(404).json({
141
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
142
+ });
143
+ }
144
+ delete store.drive.files[req.params.fileId];
145
+ res.status(204).send();
146
+ });
147
+
148
+ // COPY file
149
+ r.post(`${PREFIX}/files/:fileId/copy`, (req, res) => {
150
+ const store = getStore();
151
+ const original = store.drive.files[req.params.fileId];
152
+ if (!original) {
153
+ return res.status(404).json({
154
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
155
+ });
156
+ }
157
+
158
+ const id = generateId();
159
+ const now = new Date().toISOString();
160
+ const copy = {
161
+ ...original,
162
+ id,
163
+ name: req.body.name || `Copy of ${original.name}`,
164
+ parents: req.body.parents || original.parents,
165
+ createdTime: now,
166
+ modifiedTime: now,
167
+ };
168
+ store.drive.files[id] = copy;
169
+ res.json(copy);
170
+ });
171
+
172
+ // === Permissions ===
173
+
174
+ // LIST permissions
175
+ r.get(`${PREFIX}/files/:fileId/permissions`, (req, res) => {
176
+ const file = getStore().drive.files[req.params.fileId];
177
+ if (!file) {
178
+ return res.status(404).json({
179
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
180
+ });
181
+ }
182
+ // Return owner as default permission
183
+ const userEmail = getStore().gmail.profile.emailAddress;
184
+ res.json({
185
+ kind: 'drive#permissionList',
186
+ permissions: [
187
+ {
188
+ kind: 'drive#permission',
189
+ id: 'owner',
190
+ type: 'user',
191
+ emailAddress: userEmail,
192
+ role: 'owner',
193
+ displayName: 'Test User',
194
+ },
195
+ ],
196
+ });
197
+ });
198
+
199
+ // GET permission
200
+ r.get(`${PREFIX}/files/:fileId/permissions/:permissionId`, (req, res) => {
201
+ const file = getStore().drive.files[req.params.fileId];
202
+ if (!file) {
203
+ return res.status(404).json({
204
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
205
+ });
206
+ }
207
+ const userEmail = getStore().gmail.profile.emailAddress;
208
+ if (req.params.permissionId === 'owner') {
209
+ return res.json({
210
+ kind: 'drive#permission',
211
+ id: 'owner',
212
+ type: 'user',
213
+ emailAddress: userEmail,
214
+ role: 'owner',
215
+ displayName: 'Test User',
216
+ });
217
+ }
218
+ res.status(404).json({
219
+ error: { code: 404, message: 'Permission not found.', status: 'NOT_FOUND' },
220
+ });
221
+ });
222
+
223
+ // CREATE permission
224
+ r.post(`${PREFIX}/files/:fileId/permissions`, (req, res) => {
225
+ const file = getStore().drive.files[req.params.fileId];
226
+ if (!file) {
227
+ return res.status(404).json({
228
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
229
+ });
230
+ }
231
+ const id = generateId(8);
232
+ res.json({
233
+ kind: 'drive#permission',
234
+ id,
235
+ type: req.body.type || 'user',
236
+ emailAddress: req.body.emailAddress,
237
+ role: req.body.role || 'reader',
238
+ });
239
+ });
240
+
241
+ // UPDATE permission
242
+ r.patch(`${PREFIX}/files/:fileId/permissions/:permissionId`, (req, res) => {
243
+ const file = getStore().drive.files[req.params.fileId];
244
+ if (!file) {
245
+ return res.status(404).json({
246
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
247
+ });
248
+ }
249
+ res.json({
250
+ kind: 'drive#permission',
251
+ id: req.params.permissionId,
252
+ ...req.body,
253
+ });
254
+ });
255
+
256
+ // DELETE permission
257
+ r.delete(`${PREFIX}/files/:fileId/permissions/:permissionId`, (req, res) => {
258
+ const file = getStore().drive.files[req.params.fileId];
259
+ if (!file) {
260
+ return res.status(404).json({
261
+ error: { code: 404, message: 'File not found.', status: 'NOT_FOUND' },
262
+ });
263
+ }
264
+ res.status(204).send();
265
+ });
266
+
267
+ // === Shared Drives ===
268
+
269
+ // LIST drives
270
+ r.get(`${PREFIX}/drives`, (_req, res) => {
271
+ res.json({
272
+ kind: 'drive#driveList',
273
+ drives: [],
274
+ });
275
+ });
276
+
277
+ // CREATE drive
278
+ r.post(`${PREFIX}/drives`, (req, res) => {
279
+ const id = generateId();
280
+ res.json({
281
+ kind: 'drive#drive',
282
+ id,
283
+ name: req.body.name || 'Untitled Drive',
284
+ createdTime: new Date().toISOString(),
285
+ });
286
+ });
287
+
288
+ // GET drive
289
+ r.get(`${PREFIX}/drives/:driveId`, (req, res) => {
290
+ res.status(404).json({
291
+ error: { code: 404, message: 'Shared drive not found.', status: 'NOT_FOUND' },
292
+ });
293
+ });
294
+
295
+ // UPDATE drive
296
+ r.patch(`${PREFIX}/drives/:driveId`, (req, res) => {
297
+ res.status(404).json({
298
+ error: { code: 404, message: 'Shared drive not found.', status: 'NOT_FOUND' },
299
+ });
300
+ });
301
+
302
+ // DELETE drive
303
+ r.delete(`${PREFIX}/drives/:driveId`, (req, res) => {
304
+ res.status(404).json({
305
+ error: { code: 404, message: 'Shared drive not found.', status: 'NOT_FOUND' },
306
+ });
307
+ });
308
+
309
+ return r;
310
+ }
311
+
312
+ function filterDriveQuery(files: any[], q: string): any[] {
313
+ // Simple query parser for common Drive query patterns
314
+ const conditions = q.split(/\s+and\s+/i);
315
+ return files.filter(f => {
316
+ return conditions.every(cond => {
317
+ cond = cond.trim();
318
+
319
+ // name = 'X'
320
+ let match = cond.match(/^name\s*=\s*'([^']+)'$/);
321
+ if (match) return f.name === match[1];
322
+
323
+ // name contains 'X'
324
+ match = cond.match(/^name\s+contains\s+'([^']+)'$/i);
325
+ if (match) return f.name.toLowerCase().includes(match[1].toLowerCase());
326
+
327
+ // mimeType = 'X'
328
+ match = cond.match(/^mimeType\s*=\s*'([^']+)'$/);
329
+ if (match) return f.mimeType === match[1];
330
+
331
+ // 'parentId' in parents
332
+ match = cond.match(/^'([^']+)'\s+in\s+parents$/);
333
+ if (match) return f.parents?.includes(match[1]);
334
+
335
+ // trashed = true/false
336
+ match = cond.match(/^trashed\s*=\s*(true|false)$/);
337
+ if (match) return f.trashed === (match[1] === 'true');
338
+
339
+ return true; // unknown conditions pass through
340
+ });
341
+ });
342
+ }