file-share-mcp 1.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/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # File Share MCP
2
+
3
+ An MCP server for bidirectional file sharing between AI agents and users, with a built-in web GUI.
4
+
5
+ The agent can list, read, and notify about files. The user can upload, download, and delete files via the GUI — all through a shared local folder.
6
+
7
+ ## Features
8
+
9
+ - **MCP Tools** for the AI agent:
10
+ - `get_shared_folder` — Get the current shared folder path
11
+ - `list_shared_files` — List all files available in the shared folder
12
+ - `share_file` — Notify the user that a file is ready to download
13
+ - `preview_file` — Read the content of a `.txt` or `.md` file (max 1 MB)
14
+ - **Web GUI** — Upload, download, preview, and delete files from a browser
15
+ - **Zero config** — works out of the box with sensible defaults (`~/.file-share-mcp`)
16
+ - **Proxy-ready** — optional integration with the [file-share-mcp proxy](https://github.com/...)
17
+
18
+ ---
19
+
20
+ ## Quick Start with Cursor
21
+
22
+ Add this to your Cursor MCP configuration (`~/.cursor/mcp.json` or your workspace `.cursor/mcp.json`):
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "file-share": {
28
+ "command": "npx",
29
+ "args": ["-y", "file-share-mcp"]
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Once added, restart Cursor. The GUI will be available at **http://localhost:3005**.
36
+
37
+ ---
38
+
39
+ ## Quick Start with Claude Desktop
40
+
41
+ Add this to your Claude Desktop configuration (`claude_desktop_config.json`):
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "file-share": {
47
+ "command": "npx",
48
+ "args": ["-y", "file-share-mcp"]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Configuration (optional)
57
+
58
+ All configuration is done via environment variables, passed in the MCP `env` block or in a `.env` file inside the package directory.
59
+
60
+ | Variable | Default | Description |
61
+ |---|---|---|
62
+ | `GUI_PORT` | `3005` | Port for the web GUI |
63
+ | `MCP_DATA_DIR` | `~/.file-share-mcp` | Directory for `configs.json` and persistent data |
64
+ | `PROXY_URL` | _(none)_ | URL of the proxy server (optional) |
65
+ | `PROXY_APP_PATH` | `/file-share` | Path to register on the proxy |
66
+ | `PROXY_APP_NAME` | `File Share MCP` | Display name shown in the proxy dashboard |
67
+
68
+ ### Example: custom port and data directory
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "file-share": {
74
+ "command": "npx",
75
+ "args": ["-y", "file-share-mcp"],
76
+ "env": {
77
+ "GUI_PORT": "3010",
78
+ "MCP_DATA_DIR": "C:/Users/YourName/my-shared-files"
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### Example: with proxy integration
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "file-share": {
91
+ "command": "npx",
92
+ "args": ["-y", "file-share-mcp"],
93
+ "env": {
94
+ "PROXY_URL": "http://localhost:3000",
95
+ "PROXY_APP_PATH": "/file-share",
96
+ "PROXY_APP_NAME": "File Share MCP"
97
+ }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Data & Logs
106
+
107
+ By default, all user data is stored in `~/.file-share-mcp/`:
108
+
109
+ ```
110
+ ~/.file-share-mcp/
111
+ ├── configs.json # Saved configuration (shared folder path, etc.)
112
+ ├── shared/ # Default shared folder
113
+ └── logs/
114
+ └── server.log # Application logs
115
+ ```
116
+
117
+ This directory survives package updates and reinstalls.
118
+
119
+ ---
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ # Install dependencies
125
+ npm install
126
+ cd gui && npm install && cd ..
127
+
128
+ # Build (backend + frontend)
129
+ npm run build
130
+
131
+ # Start
132
+ npm start
133
+ ```
134
+
135
+ ### Project structure
136
+
137
+ ```
138
+ src/
139
+ ├── mcp-server.ts # MCP server (MASTER process)
140
+ ├── gui.ts # Express GUI server (WORKER process)
141
+ └── utils/
142
+ ├── config-manager.ts
143
+ ├── file-manager.ts
144
+ ├── logger.ts
145
+ └── generate-version.ts
146
+ gui/ # React + Vite frontend (builds to ../public/)
147
+ public/ # Built frontend (served by the GUI server)
148
+ ```
149
+
150
+ ---
151
+
152
+ ## License
153
+
154
+ ISC
package/dist/gui.js ADDED
@@ -0,0 +1,235 @@
1
+ import express from 'express';
2
+ import multer from 'multer';
3
+ import cors from 'cors';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import fs from 'fs';
7
+ import axios from 'axios';
8
+ import { setupLogging } from './utils/logger.js';
9
+ import { getConfig, saveConfig } from './utils/config-manager.js';
10
+ import { getSharedFiles, validateSafePath, deleteFile } from './utils/file-manager.js';
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ setupLogging('GUI');
14
+ const app = express();
15
+ app.use(cors());
16
+ app.use(express.json());
17
+ // Protect stdout - setupLogging already redirects console.log/error
18
+ // but we want to be sure no other library writes to stdout.
19
+ // Multer configuration for file uploads
20
+ const storage = multer.diskStorage({
21
+ destination: (req, file, cb) => {
22
+ const config = getConfig();
23
+ if (!fs.existsSync(config.shared_folder_path)) {
24
+ fs.mkdirSync(config.shared_folder_path, { recursive: true });
25
+ }
26
+ cb(null, config.shared_folder_path);
27
+ },
28
+ filename: (req, file, cb) => {
29
+ cb(null, file.originalname);
30
+ }
31
+ });
32
+ const upload = multer({ storage });
33
+ // Endpoints
34
+ app.get('/proxy/health', (req, res) => {
35
+ res.json({ status: 'healthy' });
36
+ });
37
+ app.get('/api/files', (req, res) => {
38
+ try {
39
+ const files = getSharedFiles();
40
+ res.json(files);
41
+ }
42
+ catch (error) {
43
+ res.status(500).json({ error: error.message });
44
+ }
45
+ });
46
+ app.get('/api/download/:filename', (req, res) => {
47
+ try {
48
+ const safePath = validateSafePath(req.params.filename);
49
+ if (fs.existsSync(safePath)) {
50
+ res.download(safePath);
51
+ }
52
+ else {
53
+ res.status(404).send('File not found');
54
+ }
55
+ }
56
+ catch (error) {
57
+ res.status(400).send(error.message);
58
+ }
59
+ });
60
+ app.delete('/api/files/:filename', (req, res) => {
61
+ try {
62
+ deleteFile(req.params.filename);
63
+ res.json({ message: 'File deleted successfully' });
64
+ }
65
+ catch (error) {
66
+ res.status(400).json({ error: error.message });
67
+ }
68
+ });
69
+ const PREVIEWABLE_EXTENSIONS = ['.txt', '.md'];
70
+ const PREVIEW_MAX_SIZE = 1024 * 1024; // 1 MB
71
+ app.get('/api/preview/:filename', (req, res) => {
72
+ try {
73
+ const ext = path.extname(req.params.filename).toLowerCase();
74
+ if (!PREVIEWABLE_EXTENSIONS.includes(ext)) {
75
+ return res.status(400).json({ error: 'Preview not supported for this file type' });
76
+ }
77
+ const safePath = validateSafePath(req.params.filename);
78
+ if (!fs.existsSync(safePath)) {
79
+ return res.status(404).json({ error: 'File not found' });
80
+ }
81
+ const stats = fs.statSync(safePath);
82
+ if (stats.size > PREVIEW_MAX_SIZE) {
83
+ return res.status(400).json({ error: 'File too large to preview (max 1 MB)' });
84
+ }
85
+ const content = fs.readFileSync(safePath, 'utf-8');
86
+ res.json({ content, type: ext === '.md' ? 'markdown' : 'text' });
87
+ }
88
+ catch (error) {
89
+ res.status(400).json({ error: error.message });
90
+ }
91
+ });
92
+ app.post('/api/upload', upload.single('file'), (req, res) => {
93
+ if (req.file) {
94
+ res.json({ message: 'File uploaded successfully', filename: req.file.filename });
95
+ }
96
+ else {
97
+ res.status(400).json({ error: 'No file uploaded' });
98
+ }
99
+ });
100
+ app.get('/api/config', (req, res) => {
101
+ const config = getConfig();
102
+ res.json({
103
+ shared_folder_path: config.shared_folder_path,
104
+ mcp_data_dir: config.MCP_DATA_DIR
105
+ });
106
+ });
107
+ app.post('/api/config', (req, res) => {
108
+ try {
109
+ const { shared_folder_path } = req.body;
110
+ if (!shared_folder_path) {
111
+ return res.status(400).json({ error: 'shared_folder_path is required' });
112
+ }
113
+ saveConfig({ shared_folder_path });
114
+ res.json({ message: 'Configuration updated successfully' });
115
+ }
116
+ catch (error) {
117
+ res.status(500).json({ error: error.message });
118
+ }
119
+ });
120
+ // GET /api/configurations — Convention méta-config proxy
121
+ app.get('/api/configurations', (req, res) => {
122
+ const config = getConfig();
123
+ res.json({
124
+ configurations: [{
125
+ id: 'default',
126
+ name: 'Configuration actuelle',
127
+ description: `Dossier partagé: ${config.shared_folder_path}`,
128
+ active: true
129
+ }]
130
+ });
131
+ });
132
+ // POST /api/configurations/apply — Convention méta-config proxy
133
+ app.post('/api/configurations/apply', (req, res) => {
134
+ const { configId } = req.body;
135
+ if (configId === 'default') {
136
+ return res.json({ success: true, appliedConfig: 'default' });
137
+ }
138
+ return res.status(404).json({ success: false, error: `Configuration "${configId}" not found` });
139
+ });
140
+ // SPA Support fallback
141
+ const publicDir = path.resolve(__dirname, '../public');
142
+ app.use(express.static(publicDir));
143
+ app.get(/^(?!\/api\/|\/proxy\/)/, (req, res) => {
144
+ const indexHtml = path.join(publicDir, 'index.html');
145
+ if (fs.existsSync(indexHtml)) {
146
+ res.sendFile(indexHtml);
147
+ }
148
+ else {
149
+ res.status(404).send('GUI not built yet. Run build process.');
150
+ }
151
+ });
152
+ // Proxy registration & server startup
153
+ const config = getConfig();
154
+ async function registerWithProxy() {
155
+ if (!config.PROXY_URL || !config.PROXY_APP_PATH)
156
+ return null;
157
+ try {
158
+ // Check if proxy is up
159
+ await axios.get(`${config.PROXY_URL}/proxy/health`, { timeout: 2000 });
160
+ const response = await axios.post(`${config.PROXY_URL}/proxy/register`, {
161
+ path: config.PROXY_APP_PATH,
162
+ name: config.PROXY_APP_NAME
163
+ }, { timeout: 2000 });
164
+ const assignedPort = response.data.port;
165
+ console.log(`Successfully registered with proxy at ${config.PROXY_URL}${config.PROXY_APP_PATH} (port ${assignedPort})`);
166
+ return assignedPort;
167
+ }
168
+ catch (error) {
169
+ console.error(`Failed to register with proxy: ${error.message}`);
170
+ return null;
171
+ }
172
+ }
173
+ async function unregisterFromProxy() {
174
+ if (!config.PROXY_URL || !config.PROXY_APP_PATH)
175
+ return;
176
+ try {
177
+ await axios.delete(`${config.PROXY_URL}/proxy/unregister`, {
178
+ data: { path: config.PROXY_APP_PATH },
179
+ timeout: 2000
180
+ });
181
+ console.log(`Unregistered from proxy`);
182
+ }
183
+ catch (error) {
184
+ // Ignore error on shutdown
185
+ }
186
+ }
187
+ let server;
188
+ async function startServer() {
189
+ let port;
190
+ if (config.PROXY_URL) {
191
+ const assignedPort = await registerWithProxy();
192
+ if (assignedPort) {
193
+ port = assignedPort;
194
+ }
195
+ else {
196
+ console.error(`Could not register with proxy. Falling back to standalone mode on port ${config.GUI_PORT}.`);
197
+ port = config.GUI_PORT;
198
+ }
199
+ }
200
+ else {
201
+ port = config.GUI_PORT;
202
+ }
203
+ server = app.listen(port, () => {
204
+ console.log(`GUI API server listening on port ${port}`);
205
+ });
206
+ server.on('error', (err) => {
207
+ if (err.code === 'EADDRINUSE') {
208
+ console.error(`PORT CONFLICT: Port ${port} is already in use.`);
209
+ }
210
+ });
211
+ }
212
+ startServer();
213
+ // IPC and signals
214
+ process.on('message', async (msg) => {
215
+ if (msg === 'STOP') {
216
+ console.log('GUI received STOP signal');
217
+ await unregisterFromProxy();
218
+ server?.close(() => process.exit(0));
219
+ }
220
+ });
221
+ process.on('error', (err) => {
222
+ if (err.code === 'ERR_IPC_CHANNEL_CLOSED') {
223
+ console.error('[IPC] Channel closed (expected on disconnect).');
224
+ return;
225
+ }
226
+ console.error(`[Process] Error: ${err.message}`);
227
+ });
228
+ process.on('disconnect', () => {
229
+ console.error('Disconnected from parent, exiting.');
230
+ process.exit(0);
231
+ });
232
+ process.on('SIGTERM', async () => {
233
+ await unregisterFromProxy();
234
+ process.exit(0);
235
+ });
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { z } from 'zod';
6
+ import { fork } from 'child_process';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import fs from 'fs';
10
+ import { setupLogging } from './utils/logger.js';
11
+ import { getConfig } from './utils/config-manager.js';
12
+ import { getSharedFiles, validateSafePath } from './utils/file-manager.js';
13
+ import { VERSION } from './version.js';
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ setupLogging('MASTER');
17
+ const server = new Server({
18
+ name: 'file-share-mcp',
19
+ version: VERSION,
20
+ }, {
21
+ capabilities: {
22
+ tools: {},
23
+ },
24
+ });
25
+ let guiProcess = null;
26
+ /**
27
+ * Envoi IPC robuste — vérifie que le canal est connecté avant d'envoyer
28
+ */
29
+ function safeSend(proc, message) {
30
+ if (proc && typeof proc.send === 'function' && proc.connected) {
31
+ try {
32
+ return proc.send(message);
33
+ }
34
+ catch (err) {
35
+ console.error(`[IPC] Failed to send message: ${err.message}`);
36
+ return false;
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+ function startGui() {
42
+ if (guiProcess) {
43
+ guiProcess.kill();
44
+ }
45
+ const guiPath = path.join(__dirname, 'gui.js');
46
+ guiProcess = fork(guiPath, [], {
47
+ stdio: ['ignore', 'pipe', 'inherit', 'ipc'],
48
+ env: { ...process.env }
49
+ });
50
+ // Redirect GUI stdout to stderr to protect JSON-RPC
51
+ guiProcess.stdout?.on('data', (data) => {
52
+ process.stderr.write(data);
53
+ });
54
+ guiProcess.on('message', (message) => {
55
+ if (message === 'RESTART') {
56
+ console.log('GUI requested restart...');
57
+ startGui();
58
+ }
59
+ else if (message === 'STOP') {
60
+ console.log('GUI requested stop...');
61
+ cleanup().then(() => process.exit(0));
62
+ }
63
+ });
64
+ guiProcess.on('exit', (code) => {
65
+ if (code !== 0 && code !== null) {
66
+ console.error(`GUI process exited with code ${code}. Restarting...`);
67
+ setTimeout(startGui, 2000);
68
+ }
69
+ });
70
+ }
71
+ async function cleanup() {
72
+ console.log('Cleaning up...');
73
+ if (guiProcess) {
74
+ safeSend(guiProcess, 'STOP');
75
+ // Give it a moment to stop gracefully
76
+ await new Promise(resolve => setTimeout(resolve, 1000));
77
+ guiProcess.kill();
78
+ }
79
+ }
80
+ process.on('SIGINT', async () => {
81
+ await cleanup();
82
+ process.exit(0);
83
+ });
84
+ process.on('SIGTERM', async () => {
85
+ await cleanup();
86
+ process.exit(0);
87
+ });
88
+ // Tool Definitions
89
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
90
+ return {
91
+ tools: [
92
+ {
93
+ name: 'get_shared_folder',
94
+ description: 'Get the current path of the shared folder where files are stored.',
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {},
98
+ },
99
+ },
100
+ {
101
+ name: 'list_shared_files',
102
+ description: 'List all files currently available in the shared folder.',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {},
106
+ },
107
+ },
108
+ {
109
+ name: 'share_file',
110
+ description: 'Notify the user that a file is available in the shared folder. This tool does not move files, it just informs the user. IMPORTANT: You must provide the ABSOLUTE path to the file.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ filename: {
115
+ type: 'string',
116
+ description: 'The absolute path of the file to share.',
117
+ },
118
+ },
119
+ required: ['filename'],
120
+ },
121
+ },
122
+ {
123
+ name: 'preview_file',
124
+ description: 'Preview the content of a .txt or .md (Markdown) file from the shared folder. Returns the raw text content (max 1 MB).',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ filename: {
129
+ type: 'string',
130
+ description: 'The name of the file to preview (filename only, not the full path).',
131
+ },
132
+ },
133
+ required: ['filename'],
134
+ },
135
+ },
136
+ ],
137
+ };
138
+ });
139
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
140
+ const { name, arguments: args } = request.params;
141
+ try {
142
+ switch (name) {
143
+ case 'get_shared_folder': {
144
+ const config = getConfig();
145
+ return {
146
+ content: [{ type: 'text', text: `Shared folder path: ${config.shared_folder_path}` }],
147
+ };
148
+ }
149
+ case 'list_shared_files': {
150
+ const files = getSharedFiles();
151
+ if (files.length === 0) {
152
+ return {
153
+ content: [{ type: 'text', text: 'The shared folder is empty.' }],
154
+ };
155
+ }
156
+ const fileList = files.map(f => `- ${f.name} (${(f.size / 1024).toFixed(2)} KB)`).join('\n');
157
+ return {
158
+ content: [{ type: 'text', text: `Files in shared folder:\n${fileList}` }],
159
+ };
160
+ }
161
+ case 'share_file': {
162
+ const { filename } = z.object({ filename: z.string() }).parse(args);
163
+ return {
164
+ content: [{ type: 'text', text: `File "${filename}" has been shared and is visible in the GUI.` }],
165
+ };
166
+ }
167
+ case 'preview_file': {
168
+ const { filename } = z.object({ filename: z.string() }).parse(args);
169
+ const ext = path.extname(filename).toLowerCase();
170
+ if (!['.txt', '.md'].includes(ext)) {
171
+ return {
172
+ content: [{ type: 'text', text: `Error: Preview is only supported for .txt and .md files.` }],
173
+ isError: true,
174
+ };
175
+ }
176
+ const safePath = validateSafePath(filename);
177
+ if (!fs.existsSync(safePath)) {
178
+ return {
179
+ content: [{ type: 'text', text: `Error: File "${filename}" not found in shared folder.` }],
180
+ isError: true,
181
+ };
182
+ }
183
+ const stats = fs.statSync(safePath);
184
+ if (stats.size > 1024 * 1024) {
185
+ return {
186
+ content: [{ type: 'text', text: `Error: File too large to preview (max 1 MB).` }],
187
+ isError: true,
188
+ };
189
+ }
190
+ const content = fs.readFileSync(safePath, 'utf-8');
191
+ return {
192
+ content: [{ type: 'text', text: content }],
193
+ };
194
+ }
195
+ default:
196
+ throw new Error(`Tool not found: ${name}`);
197
+ }
198
+ }
199
+ catch (error) {
200
+ return {
201
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
202
+ isError: true,
203
+ };
204
+ }
205
+ });
206
+ async function main() {
207
+ const transport = new StdioServerTransport();
208
+ await server.connect(transport);
209
+ console.log('File Share MCP Server started');
210
+ startGui();
211
+ }
212
+ main().catch((error) => {
213
+ console.error('Fatal error in main():', error);
214
+ process.exit(1);
215
+ });
@@ -0,0 +1,58 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import dotenv from 'dotenv';
5
+ import { fileURLToPath } from 'url';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const rootDir = path.resolve(__dirname, '../../');
9
+ const envPath = path.join(rootDir, '.env');
10
+ // User-level base dir that survives package updates
11
+ const USER_BASE_DIR = path.join(os.homedir(), '.file-share-mcp');
12
+ const DEFAULT_CONFIG = {
13
+ shared_folder_path: path.join(USER_BASE_DIR, 'shared'),
14
+ };
15
+ export function getConfig() {
16
+ dotenv.config({ path: envPath, override: true });
17
+ const dataDirEnv = process.env.MCP_DATA_DIR;
18
+ const dataDir = dataDirEnv
19
+ ? (path.isAbsolute(dataDirEnv) ? dataDirEnv : path.resolve(rootDir, dataDirEnv))
20
+ : USER_BASE_DIR;
21
+ if (!fs.existsSync(dataDir)) {
22
+ fs.mkdirSync(dataDir, { recursive: true });
23
+ }
24
+ const configsPath = path.join(dataDir, 'configs.json');
25
+ let currentConfig = { ...DEFAULT_CONFIG };
26
+ if (fs.existsSync(configsPath)) {
27
+ try {
28
+ const saved = JSON.parse(fs.readFileSync(configsPath, 'utf-8'));
29
+ currentConfig = { ...currentConfig, ...saved };
30
+ }
31
+ catch (err) {
32
+ console.error('Failed to parse configs.json:', err);
33
+ }
34
+ }
35
+ else {
36
+ fs.writeFileSync(configsPath, JSON.stringify(currentConfig, null, 2));
37
+ }
38
+ return {
39
+ ...currentConfig,
40
+ GUI_PORT: parseInt(process.env.GUI_PORT || '3005', 10),
41
+ PROXY_URL: process.env.PROXY_URL,
42
+ PROXY_APP_PATH: process.env.PROXY_APP_PATH || '/file-share',
43
+ PROXY_APP_NAME: process.env.PROXY_APP_NAME || 'File Share MCP',
44
+ MCP_DATA_DIR: dataDir,
45
+ };
46
+ }
47
+ export function saveConfig(newConfig) {
48
+ const fullConfig = getConfig();
49
+ const configsPath = path.join(fullConfig.MCP_DATA_DIR, 'configs.json');
50
+ const configToSave = {
51
+ shared_folder_path: newConfig.shared_folder_path || fullConfig.shared_folder_path,
52
+ };
53
+ fs.writeFileSync(configsPath, JSON.stringify(configToSave, null, 2));
54
+ // Ensure shared folder exists
55
+ if (!fs.existsSync(configToSave.shared_folder_path)) {
56
+ fs.mkdirSync(configToSave.shared_folder_path, { recursive: true });
57
+ }
58
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getConfig } from './config-manager.js';
4
+ export function getSharedFiles() {
5
+ const { shared_folder_path } = getConfig();
6
+ if (!fs.existsSync(shared_folder_path)) {
7
+ fs.mkdirSync(shared_folder_path, { recursive: true });
8
+ return [];
9
+ }
10
+ return fs.readdirSync(shared_folder_path).filter(file => {
11
+ const filePath = path.join(shared_folder_path, file);
12
+ return fs.statSync(filePath).isFile();
13
+ }).map(file => {
14
+ const filePath = path.join(shared_folder_path, file);
15
+ const stats = fs.statSync(filePath);
16
+ return {
17
+ name: file,
18
+ size: stats.size,
19
+ mtime: stats.mtime,
20
+ extension: path.extname(file).toLowerCase(),
21
+ };
22
+ });
23
+ }
24
+ export function validateSafePath(filename) {
25
+ const { shared_folder_path } = getConfig();
26
+ const safeName = path.basename(filename);
27
+ const fullPath = path.join(shared_folder_path, safeName);
28
+ // Extra safety check
29
+ if (!fullPath.startsWith(path.resolve(shared_folder_path))) {
30
+ throw new Error('Path traversal detected');
31
+ }
32
+ return fullPath;
33
+ }
34
+ export function deleteFile(filename) {
35
+ const fullPath = validateSafePath(filename);
36
+ if (fs.existsSync(fullPath)) {
37
+ fs.unlinkSync(fullPath);
38
+ }
39
+ }