Multi-share, permission-aware, lightweight FTP(S) server for Node.js built on top of @n-car/ftp-srv.
You need a small, configurable FTP / FTPS server you can embed or run via CLI, with:
- Multiple logical shares (each a real filesystem path) exposed as top-level directories
- Anonymous + named users, per-user or anonymous read (r) / read-write (rw) permissions
- Simple JSON configuration (users, shares, server) + hot reload (
--watch) - Optional quotas and upload size limits
- Virtual status file exposing permissions & usage
- Explicit or implicit TLS
- Passive mode (PASV) range + public address support
- Basic i18n (built-in
en;itprovided in examples) and runtime language switch viaLANG
- Multi-share virtual root
- Read / read-write permissions (
r,rw) per user and anonymous - Anonymous enable/disable toggle
- Share quota (
maxSizeBytes) + per-upload size limit (limits.maxUploadBytes) - Virtual
.statusfile (root + each share) - Explicit & implicit TLS (FTPS)
- Passive mode with configurable port range & external URL
- Hot configuration reload (
--watch) - i18n + dynamic
LANG <code>command - JSON schema validation (Ajv) (optional)
- TypeScript definitions included
Library usage:
npm install ftp-multi-srvGlobal CLI (optional):
npm install -g ftp-multi-srvconst { createFtpServer } = require('ftp-multi-srv');
const users = [
{ username: 'alice', password: 'alicepw' },
{ username: 'bob', password: 'bobpw' }
];
const shares = [
{
name: 'public',
path: './data/public',
public: true,
anonymousPermission: 'r',
users: { alice: 'rw', bob: 'r' }
}
];
const serverConf = {
host: '0.0.0.0',
port: 2121,
anonymous: { enabled: true },
pasv: { enabled: true, url: '203.0.113.10', min: 50000, max: 50010 }
};
const { ftpServer } = createFtpServer({ users, shares, serverConf });
ftpServer.listen().then(() => console.log('FTP server listening')); Disable built-in validation (if you validate beforehand):
createFtpServer({ users, shares, serverConf, validate: false });Local project:
npm startGlobal:
ftp-multi-srvWatch mode (auto-reload on config changes):
ftp-multi-srv --watchDefault locations (override via env vars):
./config/server.json./config/users.json./config/shares.json
| Key | Type | Default | Notes |
|---|---|---|---|
host |
string | 0.0.0.0 |
Bind address |
port |
number | 2121 |
Listening port |
anonymous.enabled |
boolean | false |
Allow anonymous login |
limits.maxUploadBytes |
number/null | null |
Per-file upload size cap |
pasv.enabled |
boolean | false |
Enable passive mode |
pasv.url |
string/null | null |
Public IP/host in PASV reply (NAT) |
pasv.min |
number | 50000 |
Start of passive port range |
pasv.max |
number | 50100 |
End of passive port range |
tls.enabled |
boolean | false |
Enable FTPS |
tls.mode |
explicit |
implicit |
explicit |
tls.cert |
string | ./certs/cert.pem |
Certificate path |
tls.key |
string | ./certs/key.pem |
Private key path |
locale |
string | en |
Default locale |
fallbackLocale |
string | en |
Fallback if missing key |
Notes:
- If TLS files are unreadable, TLS is disabled with a warning
- Use a dedicated port for implicit FTPS (e.g. 990)
Guidelines: unique usernames; hash passwords for production.
[
{
"name": "public",
"path": "./data/public",
"public": true,
"anonymousPermission": "r",
"maxSizeBytes": null,
"users": { "alice": "rw", "bob": "r" }
}
]| Key | Type | Default | Description |
|---|---|---|---|
name |
string | (required) | Directory name at FTP root |
path |
string | (required) | Filesystem path (created if missing) |
public |
boolean | false |
Visible to anonymous |
anonymousPermission |
r |
rw |
r |
users |
object | {} |
`username -> r |
maxSizeBytes |
number/null | null |
Total quota for share |
Permission resolution (highest first):
- Named user specific permission
- Else if
public=true:anonymousPermission - Else: no access
| Name | Default | Purpose |
|---|---|---|
FTP_HOST |
0.0.0.0 |
Override server host |
FTP_PORT |
2121 |
Override server port |
FTP_USERS |
./config/users.json |
Users file path |
FTP_SHARES |
./config/shares.json |
Shares file path |
FTP_SERVER_CONF |
./config/server.json |
Server config path |
limits.maxUploadBytes: per-upload cap - enforced from startshare.maxSizeBytes: aggregate size quota (recursive). Calculated and cached (30s). Cache invalidated after each upload closes. If quota exceeded, upload is rejected.
- Appears at root: summary line per visible share (
name permissions=rw quota=50MB used=12.3MB) - Appears inside share root: key/value lines (
permissions=rw, optionalquota=,used=) - Read-only, virtual; not stored on disk.
- Built-in locales:
en - Italian is provided as an example at
examples/config/messages.it.json. To enable it at runtime, copy it next to your configs as./config/messages.it.json. - Add more by placing
messages.<locale>.jsoninconfig/ - Quick enable for Italian (from project root):
mkdir -p config cp examples/config/messages.it.json config/
- Connection greets with available locales
- Client can issue
LANG <code>; unsupported ->504reply - Missing keys fallback -> default locale -> English
| Mode | Flow |
|---|---|
| Explicit | Plain control connection upgraded with AUTH TLS |
| Implicit | TLS from the first byte (use dedicated port) |
If certificate or key read fails, TLS is disabled (server continues).
Configure a narrow port range (pasv.min-pasv.max) and ensure firewall/NAT forwards it. Set pasv.url when behind NAT so clients receive the correct external address.
Run with --watch to automatically reload when any of the three JSON config files change. On validation error the previous server instance stays active.
Types are shipped (index.d.ts).
import { createFtpServer, CreateServerOptions } from 'ftp-multi-srv';Return value:
{
ftpServer, // ftp-srv instance with listen/close
shareMap, // normalized shares by name
userMap, // Map<string,FtpUser>
defaultLocale,
fallbackLocale
}Ajv schema validation runs at startup / reload. Disable by passing validate: false if you pre-validate or customize.
- Use a trusted cert (not self-signed); automate renewal (e.g. cron + ACME)
- Structured logging + rotation (pino / logrotate)
- Rate limit and/or throttle anonymous actions
- Enforce filename normalization & scanning (if needed)
- Store hashed passwords (bcrypt/argon2) or delegate to external auth
- Monitor disk usage vs share quotas
// server.json
{ "host": "0.0.0.0", "port": 2121, "anonymous": { "enabled": false } }
// users.json
[{ "username": "alice", "password": "alicepw" }]
// shares.json
[{ "name": "public", "path": "./data/public", "public": true, "anonymousPermission": "r" }]
[ { "username": "alice", "password": "alicepw" } ]