@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.
@@ -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
+ }