WebSocket-based file browser for Node.js - the truth is in your file system
npm install x-files.js```
__ __ ______ _ _ _
\ \/ / | ____(_) | (_)
\ /______| |__ _| | ___ ___ _ ___
/ \______| __| | | |/ _ \/ __| | / __|
/_/\_\ | | | | | __/\__ \ _ | \__ \
|_| |_|_|\___||___/ (_)| |___/
_/ |
|__/
The truth is in your file system ๐ฝ


Installation โข Quick Start โข UI Components โข API โข Security โข Examples
---
WebSocket-based file browser for Node.js with full upload/download capabilities. Browse, read, write, upload, download, and manage remote files through a simple, secure API. Supports both text and binary files with automatic file type detection.
- WebSocket-based - Real-time, bidirectional communication
- Binary file support - Upload and download text and binary files seamlessly
- Security-first - Path whitelisting, auth hooks, granular permissions
- Lightweight - ~10KB client bundle, minimal dependencies
- TypeScript - Full type definitions included
- Universal - Works in browsers and Node.js
- UI Components - Drop-in Web Components that work with any framework
`bash`
npm install x-files.js
`typescript
import { XFilesHandler } from 'x-files.js';
import { WebSocketServer } from 'ws';
const handler = new XFilesHandler({
allowedPaths: ['/home/user/projects'],
allowWrite: true,
});
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => handler.handleConnection(ws, req));
console.log('x-files server running on ws://localhost:8080');
`
`javascript
import { XFilesClient } from 'x-files.js/client/browser';
const client = new XFilesClient({ url: 'ws://localhost:8080' });
await client.connect();
// List files
const files = await client.listDirectory('/home/user/projects');
// Read a file
const { content } = await client.readFile('/home/user/projects/README.md');
// Write a file
await client.writeFile('/home/user/projects/hello.txt', 'Hello, World!');
// Upload binary file
const imageBuffer = await fs.readFile('./image.jpg');
await client.uploadBinary('/home/user/projects/image.jpg', imageBuffer);
// Download file (auto-detects text vs binary)
const { content, isBinary } = await client.downloadFile('/home/user/projects/image.jpg');
if (isBinary) {
const buffer = Buffer.from(content, 'base64');
// Handle binary data
} else {
console.log(content); // Handle text data
}
`
`typescript
import { XFilesClient } from 'x-files.js/client';
const client = new XFilesClient({ url: 'ws://localhost:8080' });
await client.connect();
const files = await client.listDirectory('/home/user');
`
x-files.js includes ready-to-use Web Components built with Lit. They work with any framework (React, Vue, Angular, Svelte, vanilla JS).
`html
path="/home/user/projects"
>
`
| Component | Description |
|-----------|-------------|
| | Full file browser with navigation, toolbar, and context menu |
| | Tabbed file browser with multiple independent tabs |
| | File/folder icon with type detection |
| | Breadcrumb path navigation |
`html
path="/home/user"
show-hidden
readonly
>
`
The tabbed browser allows users to open multiple directories in tabs for efficient multitasking:
`html
path="/home/user"
max-tabs="8"
show-hidden
>
`
Key Features:
- Multiple independent tabs with shared connection
- Double-click directories to open in new tabs
- Tab switching and closing with intuitive UI
- Configurable maximum tabs (default: 10)
- All events include tabId for tab identification
- Responsive design with mobile support
Both browser components support the same events. All event listeners are optional - you only need to listen to the events you care about.
| Event | When Triggered | Detail Properties |
|-------|----------------|-------------------|
| select | On every file/folder click (selection) | file: FileEntry, tabId?: string |open
| | On double-click or context menu "Open" | file: FileEntry, tabId?: string |navigate
| | When changing directories | path: string, tabId?: string |
Note: The tabId property is only included in events from to identify which tab triggered the event.
Use the theme attribute for built-in themes:
`html
`
Or customize with CSS custom properties:
`css
x-files-browser {
/ Colors /
--x-files-bg: #1e1e1e;
--x-files-bg-hover: #2d2d2d;
--x-files-bg-selected: #094771;
--x-files-border: #3c3c3c;
--x-files-text: #cccccc;
--x-files-text-muted: #808080;
--x-files-accent: #0078d4;
--x-files-danger: #f44336;
/ Icons /
--x-files-icon-folder: #dcb67a;
--x-files-icon-file: #cccccc;
/ Sizing /
--x-files-font-size: 13px;
--x-files-row-height: 28px;
--x-files-icon-size: 16px;
--x-files-padding: 8px;
--x-files-radius: 4px;
--x-files-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
`
`jsx
import 'x-files.js/ui/browser';
function App() {
const handleSelect = (e) => {
console.log('Selected:', e.detail.file);
};
const handleTabbedSelect = (e) => {
console.log(Tab ${e.detail.tabId} selected:, e.detail.file);
};
const handleTabbedNavigate = (e) => {
console.log(Tab ${e.detail.tabId} navigated to:, e.detail.path);
};
return (
{/ Tabbed browser (supports all the same events + tabId) /}
path="/home/user"
max-tabs="8"
onSelect={handleTabbedSelect}
onNavigate={handleTabbedNavigate}
/>
$3
`vue
url="ws://localhost:8080"
path="/home/user"
@select="onSelect"
/>
url="ws://localhost:8080"
path="/home/user"
max-tabs="8"
@select="onTabbedSelect"
@open="onTabbedOpen"
@navigate="onTabbedNavigate"
/>
`API
$3
`typescript
const handler = new XFilesHandler({
// Directories users can access (required for security)
allowedPaths: ['/data', '/uploads'], // Default: [os.homedir()] // Permissions
allowWrite: false, // Allow create/edit operations
allowDelete: false, // Allow delete operations
// Limits
maxFileSize: 10 1024 1024, // 10MB default
// Authentication (called on each connection)
authenticate: async (req) => {
const token = req.headers.authorization;
return await validateToken(token);
},
// Authorization (called on each operation)
authorize: async (operation, path, req) => {
// Fine-grained per-operation control
return true;
},
});
`$3
| Method | Description |
|--------|-------------|
|
connect() | Connect to server |
| disconnect() | Disconnect from server |
| isConnected() | Check connection status |
| getServerConfig() | Get server configuration |$3
| Method | Description | Requires |
|--------|-------------|----------|
|
listDirectory(path) | List directory contents | - |
| getStats(path) | Get file/directory info | - |
| readFile(path, encoding?) | Read file contents | - |
| writeFile(path, content, encoding?) | Write file | allowWrite |
| createDirectory(path) | Create directory | allowWrite |
| deleteItem(path) | Delete file/directory | allowDelete |
| rename(oldPath, newPath) | Rename/move | allowWrite |
| copy(source, destination) | Copy file/directory | allowWrite |
| exists(path) | Check if path exists | - |
| search(path, pattern, options?) | Search for files | - |
| uploadFile(path, content, encoding?, isBinary?) | Upload text or binary file | allowWrite |
| uploadBinary(path, buffer) | Upload binary file from Buffer | allowWrite |
| downloadFile(path, asBinary?) | Download file (auto-detects binary) | - |
| downloadBinary(path) | Download file as Buffer | - |$3
`typescript
interface FileEntry {
name: string; // File name
path: string; // Full path
isDirectory: boolean;
isFile: boolean;
size: number; // Size in bytes
modified: string; // ISO date string
created: string; // ISO date string
}
`$3
`typescript
client.onConnect(() => console.log('Connected!'));
client.onDisconnect(() => console.log('Disconnected'));
client.onError((err) => console.error('Error:', err));
`Security
x-files.js is designed with security as a priority:
| Feature | Description |
|---------|-------------|
| Path Whitelisting | Only explicitly allowed directories are accessible |
| Traversal Protection | Paths are normalized and validated |
| Read-Only Default | Write/delete must be explicitly enabled |
| Authentication Hook | Custom auth logic per connection |
| Authorization Hook | Per-operation permission checks |
| Size Limits | Configurable max file size |
$3
`typescript
const handler = new XFilesHandler({
// 1. Whitelist specific directories only
allowedPaths: ['/app/user-data'], // 2. Enable only what's needed
allowWrite: true,
allowDelete: false,
// 3. Limit file sizes
maxFileSize: 5 1024 1024, // 5MB
// 4. Authenticate connections
authenticate: async (req) => {
const token = req.headers['authorization']?.replace('Bearer ', '');
if (!token) return false;
return await verifyJWT(token);
},
// 5. Authorize operations
authorize: async (operation, path, req) => {
const user = req.user;
// Users can only access their own directory
return path.startsWith(
/app/user-data/${user.id}/);
},
});
`Examples
$3
`typescript
import { XFilesClient } from 'x-files.js/client';
import { promises as fs } from 'fs';const client = new XFilesClient({ url: 'ws://localhost:8080' });
await client.connect();
// Upload text file
await client.uploadFile('/remote/path/document.txt', 'Hello, World!');
// Upload binary file from Buffer
const imageData = await fs.readFile('./local-image.jpg');
await client.uploadBinary('/remote/path/image.jpg', imageData);
// Download and auto-detect file type
const { content, isBinary, size } = await client.downloadFile('/remote/path/image.jpg');
if (isBinary) {
// Save binary file
const buffer = Buffer.from(content, 'base64');
await fs.writeFile('./downloaded-image.jpg', buffer);
} else {
// Handle text file
console.log('Text content:', content);
}
// Download binary file directly as Buffer
const { buffer } = await client.downloadBinary('/remote/path/image.jpg');
await fs.writeFile('./downloaded-binary.jpg', buffer);
`$3
`typescript
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { XFilesHandler } from 'x-files.js';const app = express();
const server = createServer(app);
// Mount on /files path
const wss = new WebSocketServer({ server, path: '/files' });
const handler = new XFilesHandler({
allowedPaths: ['/data'],
allowWrite: true,
});
wss.on('connection', (ws, req) => handler.handleConnection(ws, req));
server.listen(3000);
`$3
Using the built-in Web Component:
`html
url="ws://localhost:8080"
path="/data"
style="height: 400px;"
>
`Or build your own UI with the headless client:
`html
`Development
$3
`bash
npm run build # Compile TypeScript + browser bundle
npm run build:browser # Just browser bundle
npm run watch # Watch mode for TypeScript
npm run clean # Remove dist/
`$3
`bash
npm run version # Show current version and usage
npm run version get # Get current version
npm run version:bump patch # Bump patch version (0.3.1 โ 0.3.2)
npm run version:bump minor # Bump minor version (0.3.1 โ 0.4.0)
npm run version:bump major # Bump major version (0.3.1 โ 1.0.0)
`$3
The project uses automated CI/CD through GitHub Actions:
`bash
npm run release:create # Create and push release tag
`This will:
1. โ
Check that working directory is clean
2. ๐๏ธ Build the project
3. ๐งช Run tests
4. ๐ท๏ธ Create and push git tag (
v{version})
5. ๐ Trigger GitHub Actions to publish to NPM and create GitHub releaseRelease Workflow:
1. Make your changes and commit them
2. Bump version:
npm run version:bump patch (or minor/major)
3. Commit version bump: git commit -am "Bump version to v0.3.2"
4. Create release: npm run release:create`The GitHub Actions workflow will automatically:
- Run tests on multiple Node.js versions
- Build the project
- Publish to NPM with provenance
- Create a GitHub release with auto-generated release notes
- Web IDEs - Browse and edit remote code
- Admin Panels - Manage server files
- File Managers - Build custom file browsers
- Dev Tools - Remote development environments
- Media Browsers - Browse remote media libraries
- xterm.js - Terminal for the browser
- electron-to-web - Run Electron apps in the browser
MIT