@jisan901/fs-browser 1.0.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.
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/withfs.js +276 -0
- package/package.json +67 -0
- package/plugin/fs-handlers.js +419 -0
- package/plugin/vite.js +67 -0
- package/src/index.d.ts +288 -0
- package/src/index.js +402 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared FS API handlers for both Vite plugin and withfs CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Helper to read raw body from request
|
|
10
|
+
*/
|
|
11
|
+
export const getRawBody = (req) => {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
15
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
16
|
+
req.on('error', reject);
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Helper to parse query string
|
|
22
|
+
*/
|
|
23
|
+
export const parseQuery = (url) => {
|
|
24
|
+
const queryString = url.split('?')[1];
|
|
25
|
+
if (!queryString) return {};
|
|
26
|
+
return Object.fromEntries(new URLSearchParams(queryString));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create path resolver with base directory restriction
|
|
31
|
+
*/
|
|
32
|
+
export const createPathResolver = (baseDir) => {
|
|
33
|
+
return (filePath) => {
|
|
34
|
+
// Remove leading slash to treat all paths as relative
|
|
35
|
+
const normalizedPath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
36
|
+
const resolved = path.resolve(baseDir, normalizedPath);
|
|
37
|
+
if (!resolved.startsWith(baseDir)) {
|
|
38
|
+
throw new Error('Invalid path: outside base directory');
|
|
39
|
+
}
|
|
40
|
+
return resolved;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create FS API handlers
|
|
46
|
+
* @param {string} baseDir - Base directory for file operations
|
|
47
|
+
* @returns {Object} - Handler functions mapped by route key
|
|
48
|
+
*/
|
|
49
|
+
export const createFsHandlers = (baseDir) => {
|
|
50
|
+
const resolvePath = createPathResolver(baseDir);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
// Root endpoint
|
|
54
|
+
'GET /': async (req, res) => {
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
56
|
+
res.end(JSON.stringify({
|
|
57
|
+
message: 'fs-browser API',
|
|
58
|
+
baseDirectory: baseDir
|
|
59
|
+
}));
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// List all methods
|
|
63
|
+
'GET /methods': async (req, res) => {
|
|
64
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
65
|
+
res.end(JSON.stringify({
|
|
66
|
+
methods: [
|
|
67
|
+
'readFile', 'writeFile', 'appendFile', 'copyFile',
|
|
68
|
+
'readdir', 'mkdir', 'rmdir', 'rm',
|
|
69
|
+
'rename', 'unlink',
|
|
70
|
+
'stat', 'lstat', 'readlink', 'realpath'
|
|
71
|
+
]
|
|
72
|
+
}));
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Read file
|
|
76
|
+
'GET /readFile': async (req, res) => {
|
|
77
|
+
const query = parseQuery(req.url);
|
|
78
|
+
const { path: filePath, encoding = 'utf8' } = query;
|
|
79
|
+
const fullPath = resolvePath(filePath);
|
|
80
|
+
const data = await fs.readFile(fullPath, encoding);
|
|
81
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
82
|
+
res.end(JSON.stringify({ data }));
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Read directory
|
|
86
|
+
'GET /readdir': async (req, res) => {
|
|
87
|
+
const query = parseQuery(req.url);
|
|
88
|
+
const { path: dirPath = '.', withFileTypes = 'false' } = query;
|
|
89
|
+
const fullPath = resolvePath(dirPath);
|
|
90
|
+
const files = await fs.readdir(fullPath, {
|
|
91
|
+
withFileTypes: withFileTypes === 'true'
|
|
92
|
+
});
|
|
93
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
94
|
+
res.end(JSON.stringify({ files }));
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Get file stats
|
|
98
|
+
'GET /stat': async (req, res) => {
|
|
99
|
+
const query = parseQuery(req.url);
|
|
100
|
+
const { path: filePath } = query;
|
|
101
|
+
const fullPath = resolvePath(filePath);
|
|
102
|
+
const stats = await fs.stat(fullPath);
|
|
103
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
104
|
+
res.end(JSON.stringify({
|
|
105
|
+
stats: {
|
|
106
|
+
isFile: stats.isFile(),
|
|
107
|
+
isDirectory: stats.isDirectory(),
|
|
108
|
+
isSymbolicLink: stats.isSymbolicLink(),
|
|
109
|
+
size: stats.size,
|
|
110
|
+
mode: stats.mode,
|
|
111
|
+
mtime: stats.mtime,
|
|
112
|
+
atime: stats.atime,
|
|
113
|
+
ctime: stats.ctime,
|
|
114
|
+
birthtime: stats.birthtime
|
|
115
|
+
}
|
|
116
|
+
}));
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Get file stats (no symlink follow)
|
|
120
|
+
'GET /lstat': async (req, res) => {
|
|
121
|
+
const query = parseQuery(req.url);
|
|
122
|
+
const { path: filePath } = query;
|
|
123
|
+
const fullPath = resolvePath(filePath);
|
|
124
|
+
const stats = await fs.lstat(fullPath);
|
|
125
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
126
|
+
res.end(JSON.stringify({
|
|
127
|
+
stats: {
|
|
128
|
+
isFile: stats.isFile(),
|
|
129
|
+
isDirectory: stats.isDirectory(),
|
|
130
|
+
isSymbolicLink: stats.isSymbolicLink(),
|
|
131
|
+
size: stats.size,
|
|
132
|
+
mode: stats.mode,
|
|
133
|
+
mtime: stats.mtime,
|
|
134
|
+
atime: stats.atime,
|
|
135
|
+
ctime: stats.ctime,
|
|
136
|
+
birthtime: stats.birthtime
|
|
137
|
+
}
|
|
138
|
+
}));
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Get real path
|
|
142
|
+
'GET /realpath': async (req, res) => {
|
|
143
|
+
const query = parseQuery(req.url);
|
|
144
|
+
const { path: filePath } = query;
|
|
145
|
+
const fullPath = resolvePath(filePath);
|
|
146
|
+
const realPath = await fs.realpath(fullPath);
|
|
147
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
148
|
+
res.end(JSON.stringify({ realPath }));
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// Read symlink
|
|
152
|
+
'GET /readlink': async (req, res) => {
|
|
153
|
+
const query = parseQuery(req.url);
|
|
154
|
+
const { path: linkPath } = query;
|
|
155
|
+
const fullPath = resolvePath(linkPath);
|
|
156
|
+
const target = await fs.readlink(fullPath);
|
|
157
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
158
|
+
res.end(JSON.stringify({ target }));
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// Write file
|
|
162
|
+
'POST /writeFile': async (req, res) => {
|
|
163
|
+
const contentType = req.headers['content-type'] || '';
|
|
164
|
+
const query = parseQuery(req.url);
|
|
165
|
+
let filePath = query.path;
|
|
166
|
+
let writeData;
|
|
167
|
+
let type;
|
|
168
|
+
|
|
169
|
+
const rawBody = await getRawBody(req);
|
|
170
|
+
|
|
171
|
+
// Handle binary content types
|
|
172
|
+
if (contentType.includes('application/octet-stream') ||
|
|
173
|
+
contentType.includes('image/') ||
|
|
174
|
+
contentType.includes('video/') ||
|
|
175
|
+
contentType.includes('audio/') ||
|
|
176
|
+
contentType.includes('application/pdf')) {
|
|
177
|
+
writeData = rawBody;
|
|
178
|
+
type = 'binary';
|
|
179
|
+
}
|
|
180
|
+
// Handle plain text
|
|
181
|
+
else if (contentType.includes('text/plain')) {
|
|
182
|
+
writeData = rawBody;
|
|
183
|
+
type = 'text';
|
|
184
|
+
}
|
|
185
|
+
// Handle JSON
|
|
186
|
+
else if (contentType.includes('application/json')) {
|
|
187
|
+
// If path is in query string, treat rawBody as the content to write
|
|
188
|
+
if (filePath) {
|
|
189
|
+
writeData = rawBody;
|
|
190
|
+
type = 'json';
|
|
191
|
+
}
|
|
192
|
+
// Otherwise, parse as structured request body
|
|
193
|
+
else {
|
|
194
|
+
if (rawBody.length === 0) {
|
|
195
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
196
|
+
res.end(JSON.stringify({ error: 'Empty request body' }));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const body = JSON.parse(rawBody.toString());
|
|
201
|
+
filePath = body.path;
|
|
202
|
+
|
|
203
|
+
if (!filePath) {
|
|
204
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
205
|
+
res.end(JSON.stringify({ error: 'Path is required' }));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { data, type: dataType = 'text', encoding = 'utf8' } = body;
|
|
210
|
+
|
|
211
|
+
if (data === undefined || data === null) {
|
|
212
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
213
|
+
res.end(JSON.stringify({ error: 'Data is required' }));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
type = dataType;
|
|
218
|
+
|
|
219
|
+
switch (dataType) {
|
|
220
|
+
case 'text':
|
|
221
|
+
writeData = Buffer.from(String(data), encoding);
|
|
222
|
+
break;
|
|
223
|
+
case 'json':
|
|
224
|
+
writeData = Buffer.from(JSON.stringify(data, null, 2), encoding);
|
|
225
|
+
break;
|
|
226
|
+
case 'buffer':
|
|
227
|
+
writeData = Array.isArray(data) ? Buffer.from(data) : Buffer.from(data, 'base64');
|
|
228
|
+
break;
|
|
229
|
+
default:
|
|
230
|
+
writeData = Buffer.from(String(data), encoding);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Fallback for unknown content types
|
|
235
|
+
else {
|
|
236
|
+
writeData = rawBody;
|
|
237
|
+
type = 'unknown';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Validate we have data to write
|
|
241
|
+
if (!writeData || writeData.length === 0) {
|
|
242
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
243
|
+
res.end(JSON.stringify({ error: 'No data to write' }));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const fullPath = resolvePath(filePath);
|
|
248
|
+
await fs.writeFile(fullPath, writeData);
|
|
249
|
+
|
|
250
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
251
|
+
res.end(JSON.stringify({
|
|
252
|
+
message: 'File written successfully',
|
|
253
|
+
path: filePath,
|
|
254
|
+
type,
|
|
255
|
+
size: writeData.length
|
|
256
|
+
}));
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// Append file
|
|
260
|
+
'POST /appendFile': async (req, res) => {
|
|
261
|
+
const contentType = req.headers['content-type'] || '';
|
|
262
|
+
const query = parseQuery(req.url);
|
|
263
|
+
let filePath = query.path;
|
|
264
|
+
let appendData;
|
|
265
|
+
let type;
|
|
266
|
+
|
|
267
|
+
const rawBody = await getRawBody(req);
|
|
268
|
+
|
|
269
|
+
// Handle binary content types
|
|
270
|
+
if (contentType.includes('application/octet-stream') ||
|
|
271
|
+
contentType.includes('image/') ||
|
|
272
|
+
contentType.includes('video/') ||
|
|
273
|
+
contentType.includes('audio/') ||
|
|
274
|
+
contentType.includes('application/pdf')) {
|
|
275
|
+
appendData = rawBody;
|
|
276
|
+
type = 'binary';
|
|
277
|
+
}
|
|
278
|
+
// Handle plain text
|
|
279
|
+
else if (contentType.includes('text/plain')) {
|
|
280
|
+
appendData = rawBody;
|
|
281
|
+
type = 'text';
|
|
282
|
+
}
|
|
283
|
+
// Handle JSON
|
|
284
|
+
else if (contentType.includes('application/json')) {
|
|
285
|
+
// If path is in query string, treat rawBody as the content to append
|
|
286
|
+
if (filePath) {
|
|
287
|
+
appendData = rawBody;
|
|
288
|
+
type = 'json';
|
|
289
|
+
}
|
|
290
|
+
// Otherwise, parse as structured request body
|
|
291
|
+
else {
|
|
292
|
+
if (rawBody.length === 0) {
|
|
293
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
294
|
+
res.end(JSON.stringify({ error: 'Empty request body' }));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const body = JSON.parse(rawBody.toString());
|
|
299
|
+
filePath = body.path;
|
|
300
|
+
|
|
301
|
+
if (!filePath) {
|
|
302
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
303
|
+
res.end(JSON.stringify({ error: 'Path is required' }));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { data, type: dataType = 'text', encoding = 'utf8' } = body;
|
|
308
|
+
|
|
309
|
+
if (data === undefined || data === null) {
|
|
310
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
311
|
+
res.end(JSON.stringify({ error: 'Data is required' }));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
type = dataType;
|
|
316
|
+
|
|
317
|
+
switch (dataType) {
|
|
318
|
+
case 'text':
|
|
319
|
+
appendData = Buffer.from(String(data), encoding);
|
|
320
|
+
break;
|
|
321
|
+
case 'json':
|
|
322
|
+
appendData = Buffer.from(JSON.stringify(data, null, 2), encoding);
|
|
323
|
+
break;
|
|
324
|
+
case 'buffer':
|
|
325
|
+
appendData = Array.isArray(data) ? Buffer.from(data) : Buffer.from(data, 'base64');
|
|
326
|
+
break;
|
|
327
|
+
default:
|
|
328
|
+
appendData = Buffer.from(String(data), encoding);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Fallback
|
|
333
|
+
else {
|
|
334
|
+
appendData = rawBody;
|
|
335
|
+
type = 'unknown';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Validate we have data
|
|
339
|
+
if (!appendData || appendData.length === 0) {
|
|
340
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
341
|
+
res.end(JSON.stringify({ error: 'No data to append' }));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const fullPath = resolvePath(filePath);
|
|
346
|
+
await fs.appendFile(fullPath, appendData);
|
|
347
|
+
|
|
348
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
349
|
+
res.end(JSON.stringify({
|
|
350
|
+
message: 'Data appended successfully',
|
|
351
|
+
path: filePath,
|
|
352
|
+
type,
|
|
353
|
+
size: appendData.length
|
|
354
|
+
}));
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
// Copy file
|
|
358
|
+
'POST /copyFile': async (req, res) => {
|
|
359
|
+
const rawBody = await getRawBody(req);
|
|
360
|
+
const { src, dest, flags = 0 } = JSON.parse(rawBody.toString());
|
|
361
|
+
const srcPath = resolvePath(src);
|
|
362
|
+
const destPath = resolvePath(dest);
|
|
363
|
+
await fs.copyFile(srcPath, destPath, flags);
|
|
364
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
365
|
+
res.end(JSON.stringify({ message: 'File copied successfully', src, dest }));
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
// Create directory
|
|
369
|
+
'POST /mkdir': async (req, res) => {
|
|
370
|
+
const rawBody = await getRawBody(req);
|
|
371
|
+
const { path: dirPath, recursive = true } = JSON.parse(rawBody.toString());
|
|
372
|
+
const fullPath = resolvePath(dirPath);
|
|
373
|
+
await fs.mkdir(fullPath, { recursive });
|
|
374
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
375
|
+
res.end(JSON.stringify({ message: 'Directory created', path: dirPath }));
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
// Remove directory
|
|
379
|
+
'DELETE /rmdir': async (req, res) => {
|
|
380
|
+
const rawBody = await getRawBody(req);
|
|
381
|
+
const { path: dirPath, recursive = false } = JSON.parse(rawBody.toString());
|
|
382
|
+
const fullPath = resolvePath(dirPath);
|
|
383
|
+
await fs.rmdir(fullPath, { recursive });
|
|
384
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
385
|
+
res.end(JSON.stringify({ message: 'Directory removed', path: dirPath }));
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
// Remove file or directory
|
|
389
|
+
'DELETE /rm': async (req, res) => {
|
|
390
|
+
const rawBody = await getRawBody(req);
|
|
391
|
+
const { path: targetPath, recursive = false, force = false } = JSON.parse(rawBody.toString());
|
|
392
|
+
const fullPath = resolvePath(targetPath);
|
|
393
|
+
await fs.rm(fullPath, { recursive, force });
|
|
394
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
395
|
+
res.end(JSON.stringify({ message: 'Removed successfully', path: targetPath }));
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
// Rename
|
|
399
|
+
'PUT /rename': async (req, res) => {
|
|
400
|
+
const rawBody = await getRawBody(req);
|
|
401
|
+
const { oldPath, newPath } = JSON.parse(rawBody.toString());
|
|
402
|
+
const oldFullPath = resolvePath(oldPath);
|
|
403
|
+
const newFullPath = resolvePath(newPath);
|
|
404
|
+
await fs.rename(oldFullPath, newFullPath);
|
|
405
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
406
|
+
res.end(JSON.stringify({ message: 'Renamed successfully', oldPath, newPath }));
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
// Delete file
|
|
410
|
+
'DELETE /unlink': async (req, res) => {
|
|
411
|
+
const rawBody = await getRawBody(req);
|
|
412
|
+
const { path: filePath } = JSON.parse(rawBody.toString());
|
|
413
|
+
const fullPath = resolvePath(filePath);
|
|
414
|
+
await fs.unlink(fullPath);
|
|
415
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
416
|
+
res.end(JSON.stringify({ message: 'File deleted', path: filePath }));
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
};
|
package/plugin/vite.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { createFsHandlers } from './fs-handlers.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Vite plugin that adds filesystem API routes to the dev server
|
|
11
|
+
* @param {Object} options - Plugin options
|
|
12
|
+
* @param {string} options.baseDir - Base directory for file operations (default: './data')
|
|
13
|
+
* @param {string} options.apiPrefix - API route prefix (default: '/api/fs')
|
|
14
|
+
*/
|
|
15
|
+
export default function vitePluginFsApi(options = {}) {
|
|
16
|
+
const BASE_DIR = path.resolve(process.cwd(), options.baseDir || './data');
|
|
17
|
+
const API_PREFIX = options.apiPrefix || '/api/fs';
|
|
18
|
+
|
|
19
|
+
// Ensure base directory exists
|
|
20
|
+
if (!fsSync.existsSync(BASE_DIR)) {
|
|
21
|
+
fsSync.mkdirSync(BASE_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// Route handlers
|
|
26
|
+
const handlers = createFsHandlers(BASE_DIR);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
name: 'vite-plugin-fs-api',
|
|
30
|
+
|
|
31
|
+
configureServer(server) {
|
|
32
|
+
server.middlewares.use(async (req, res, next) => {
|
|
33
|
+
// Only handle our API routes
|
|
34
|
+
if (!req.url.startsWith(API_PREFIX)) {
|
|
35
|
+
return next();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Remove API prefix from URL
|
|
40
|
+
const routePath = req.url.substring(API_PREFIX.length) || '/';
|
|
41
|
+
const handlerKey = `${req.method} ${routePath.split('?')[0]}`;
|
|
42
|
+
|
|
43
|
+
const handler = handlers[handlerKey];
|
|
44
|
+
|
|
45
|
+
if (handler) {
|
|
46
|
+
await handler(req, res);
|
|
47
|
+
} else {
|
|
48
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
49
|
+
res.end(JSON.stringify({ error: 'Route not found' }));
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('FS API Error:', error);
|
|
53
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
54
|
+
res.end(JSON.stringify({
|
|
55
|
+
error: {
|
|
56
|
+
message: error.message,
|
|
57
|
+
status: 500
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(`\n FS API available at: ${API_PREFIX}`);
|
|
64
|
+
console.log(` Base directory: ${BASE_DIR}\n`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|