Search by

enricodias / ftp-server

A pure PHP FTP server for non-filesystem backends with multi-node cluster support

Maintainers

Package info

github.com/enricodias/ftp-server-php

pkg:composer/enricodias/ftp-server

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-08-05 02:55 UTC

This package is auto-updated.

Last update: 2026-08-23 22:54:32 UTC


README

A pure PHP FTP server for non-filesystem backends with multi-node cluster support.

This package implements the FTP protocol and passive-mode data transfer. It does not implement storage or authentication itself: the consumer provides those by implementing a small set of interfaces, so the same server can expose an FTP interface over anything (a database-backed virtual filesystem, cloud storage, etc), not just the local disk.

Only passive mode is supported.

Requirements

  • PHP 7.3 or later

Installation

composer require enricodias/ftp-server

Configuration

The Config object describes how the server should run. It can be used in three ways, depending on how much of the package's functionality you need:

Single server, plain FTP

The simplest setup: one server, no encryption, no clustering.

use enricodias\FtpServer\Config\Config;
use enricodias\FtpServer\Config\PortRange;

$config = new Config(
  21,                         // main (control) port
  new PortRange(50000, 50100) // passive port range
);

Single server with FTPS

Supplying a TlsCertificate enables explicit FTPS (AUTH TLS) on the control connection.

use enricodias\FtpServer\Config\Config;
use enricodias\FtpServer\Config\PortRange;
use enricodias\FtpServer\Config\TlsCertificate;

$config = new Config(
  21,
  new PortRange(50000, 50100),
  new TlsCertificate('/path/to/cert.pem', '/path/to/key.pem')
);

Multiple servers in a cluster

To let this node redirect passive transfers to other nodes that own the requested file, provide the full cluster group together: this node's id, the node coordination channel's port, every node in the cluster (including this one), and a shared secret. A TLS certificate is required in this mode, since the inter-node coordination channel is always encrypted; the same certificate also secures the client-facing FTPS connection.

use enricodias\FtpServer\Config\ClusterNode;
use enricodias\FtpServer\Config\Config;
use enricodias\FtpServer\Config\PortRange;
use enricodias\FtpServer\Config\TlsCertificate;

$config = new Config(
    21,
    new PortRange(50000, 50100),
    new TlsCertificate('/path/to/cert.pem', '/path/to/key.pem'),
    'node-a', // this node's id
    2121,     // node channel port, used for inter node communication
    [
        new ClusterNode('node-a', '192.168.1.1'),
        new ClusterNode('node-b', '192.168.1.2'),
    ],
    'a-shared-secret'
);

The cluster arguments (node id, node channel port, node list, shared secret) are all-or-nothing: providing some but not all of them raises InvalidConfigException, as does any other invalid combination.

Implementing the storage and authentication contracts

Everything the server needs from your application lives under enricodias\FtpServer\Contract:

  • AuthenticatorInterface validates credentials and returns an AuthResultInterface.
  • AuthResultInterface reports whether authentication succeeded, the authenticated UserInterface, whether the client's data-connection IP should be validated against its control connection (some ISPs use IP pools that change mid-session), and an optional starting guess at which cluster node the user's files are probably on.
  • UserInterface is an opaque handle for the authenticated user; the package never inspects it, only threads it through to the other contracts.
  • DirectoryReaderInterface lists, creates, deletes, and renames paths, returning DirectoryEntryInterface items (name, whether it's a directory, size, last-modified time) from list().
  • FileReaderInterface / FileWriterInterface open read/write streams for file content, with offset and append support for resumable transfers.
  • FileLocatorInterface resolves which cluster node currently owns a given file, returning a NodeLocationInterface (used only in clustered mode).

Every method that touches a path receives the authenticated UserInterface, so implementations can enforce per-user access rules.

Running the server

Single server

use enricodias\FtpServer\Server;

$server = new Server($config, $authenticator, $directoryReader, $fileReader, $fileWriter);

$server->run(); // serves connections until the process is stopped

Cluster

A FileLocatorInterface implementation is required as the sixth argument when clustering is enabled in $config (the server throws InvalidConfigException otherwise); pass null for a single, non-clustered server:

$server = new Server($config, $authenticator, $directoryReader, $fileReader, $fileWriter, $fileLocator);

The control connection will try to open data connections from the node that it believes the user's files are stored. This is a guess because the ftp protocol requires the data connection to be opened before sending the file path it needs. If the requested file is not found in the node who opened the data connection, the file will be streamed between nodes to fulfill the request to the client. The new node will be remembered for this user and used for subsequent requests, even if the client reconnects, until the server restarts.

Logging

Passing a PSR-3 logger as a seventh argument logs server lifecycle events (startup, bind failures, client connect/disconnect, timeouts, unexpected errors). Without it, nothing is logged:

$server = new Server($config, $authenticator, $directoryReader, $fileReader, $fileWriter, $fileLocator, $logger);

Trying it out with a real FTP client

examples/serve.php runs a demo server backed by an in-memory virtual filesystem, so you can point any FTP client at it without writing your own storage backend:

composer install
composer serve

This starts a server on 127.0.0.1:2121 with username demo and password demo (all overridable: php examples/serve.php [port] [username] [password]).