Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FtpServerCommand
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 4
20
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 configure
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 handleConnection
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare(strict_types=1);
3
4namespace Apie\FtpServer;
5
6use Apie\ApieFileSystem\ApieFilesystem;
7use Apie\Core\ContextBuilders\ContextBuilderFactory;
8use Apie\FtpServer\FtpServerRunner;
9use React\EventLoop\Factory;
10use React\EventLoop\Loop;
11use React\Socket\ConnectionInterface;
12use React\Socket\SocketServer;
13use Symfony\Component\Console\Command\Command;
14use Symfony\Component\Console\Input\InputInterface;
15use Symfony\Component\Console\Input\InputOption;
16use Symfony\Component\Console\Output\OutputInterface;
17use Symfony\Component\Console\Style\SymfonyStyle;
18
19class FtpServerCommand extends Command
20{
21    protected static $defaultName = 'apie:ftp:serve';
22    protected static $defaultDescription = 'Run a lightweight FTP server';
23
24    public function __construct(
25        private readonly FtpServerRunner $runner,
26        private readonly ApieFilesystem $filesystem,
27        private readonly ContextBuilderFactory $contextBuilder,
28    ) {
29        parent::__construct();
30    }
31
32    protected function configure(): void
33    {
34        $this
35            ->addOption('host', null, InputOption::VALUE_REQUIRED, 'Host to bind to', '127.0.0.1')
36            ->addOption('port', null, InputOption::VALUE_REQUIRED, 'Port to listen on', '2121')
37            ->setHelp('Start an APIE FTP server.');
38    }
39
40    protected function execute(InputInterface $input, OutputInterface $output): int
41    {
42        $io = new SymfonyStyle($input, $output);
43
44        $host = (string) $input->getOption('host');
45        $port = (int) $input->getOption('port');
46
47        $io->title('FTP Server (skeleton)');
48        $io->listing([
49            'Host: ' . $host,
50            'Port: ' . $port,
51        ]);
52
53        $loop = Loop::get();
54
55        $server = new SocketServer("0.0.0.0:$port", [], $loop);
56        $server->on('connection', function (ConnectionInterface $conn) {
57            $this->handleConnection($conn);
58        });
59
60        $io->warning('FTP server functionality is not implemented. This command is a skeleton.');
61        $loop->run();
62        return Command::SUCCESS;
63    }
64
65    private function handleConnection(ConnectionInterface $conn)
66    {
67        $conn->write("220 PHP Virtual FTP Server Ready\r\n");
68        $context = $this->contextBuilder->createGeneralContext([
69            'ftp' => true,
70            ConnectionInterface::class => $conn,
71            ApieFilesystem::class => $this->filesystem,
72            'ftp_current_folder' => $this->filesystem->rootFolder,
73            'ftp_cwd' => '/',
74        ]);
75
76        $conn->on('data', function ($data) use ($conn, &$context) {
77            $command = trim($data);
78            //echo "⇢ $command\n";
79
80            [$cmd, $arg] = array_pad(explode(' ', $command, 2), 2, null);
81            $cmd = strtoupper($cmd);
82
83            $context = $this->runner->run($context, $cmd, $arg ?? '');
84        });
85    }
86}