Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
13.95% covered (danger)
13.95%
6 / 43
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FtpServerCommand
13.95% covered (danger)
13.95%
6 / 43
50.00% covered (danger)
50.00%
2 / 4
14.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 handleConnection
0.00% covered (danger)
0.00%
0 / 22
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\ApieFileSystem\ApieFilesystemFactory;
8use Apie\Core\ContextBuilders\ContextBuilderFactory;
9use Apie\FtpServer\Factories\ServerFactoryInterface;
10use Apie\FtpServer\Factories\SimpleFtpServerFactory;
11use Apie\FtpServer\Transfers\NoTransferSet;
12use Apie\FtpServer\Transfers\TransferInterface;
13use React\EventLoop\Loop;
14use React\Socket\ConnectionInterface;
15use Symfony\Component\Console\Command\Command;
16use Symfony\Component\Console\Input\InputInterface;
17use Symfony\Component\Console\Input\InputOption;
18use Symfony\Component\Console\Output\OutputInterface;
19use Symfony\Component\Console\Style\SymfonyStyle;
20
21class FtpServerCommand extends Command
22{
23    public function __construct(
24        private readonly FtpServerRunner $runner,
25        private readonly ApieFilesystemFactory $filesystemFactory,
26        private readonly ContextBuilderFactory $contextBuilder,
27        private readonly ServerFactoryInterface $serverFactory = new SimpleFtpServerFactory(),
28        private readonly string $defaultIpAddress = '127.0.0.1',
29        private readonly string $passiveMinPort = '49152',
30        private readonly string $passiveMaxPort = '65534',
31    ) {
32        parent::__construct('apie:ftp-server');
33    }
34
35    protected function configure(): void
36    {
37        $this
38            ->addOption('host', null, InputOption::VALUE_REQUIRED, 'Host to bind to', $this->defaultIpAddress)
39            ->addOption('port', null, InputOption::VALUE_REQUIRED, 'Port to listen on', '2121')
40            ->setDescription('Runs a virtual FTP server to link with Apie')
41            ->setHelp('Start an APIE FTP server.');
42    }
43
44    protected function execute(InputInterface $input, OutputInterface $output): int
45    {
46        $io = new SymfonyStyle($input, $output);
47
48        $host = (string) $input->getOption('host');
49        $port = (int) $input->getOption('port');
50
51        $io->title('APIE FTP server');
52        $io->listing([
53            'Host: ' . $host,
54            'Port: ' . $port,
55        ]);
56
57        $loop = Loop::get();
58
59        $server = $this->serverFactory->createServer($port);
60        $server->on('connection', function (ConnectionInterface $conn) use ($input, $output) {
61            $this->handleConnection($conn, $input, $output);
62        });
63
64        $loop->run();
65        return Command::SUCCESS;
66    }
67
68    private function handleConnection(ConnectionInterface $conn, InputInterface $input, OutputInterface $output)
69    {
70        $conn->write("220 Apie FTP Server Ready\r\n");
71        $context = $this->contextBuilder->createGeneralContext([
72            'ftp' => true,
73            ConnectionInterface::class => $conn,
74            ApieFilesystemFactory::class => $this->filesystemFactory,
75            FtpConstants::CURRENT_PWD => '/',
76            TransferInterface::class => new NoTransferSet(),
77            FtpConstants::PUBLIC_IP => $input->getOption('host'),
78            FtpConstants::PASV_MIN_PORT => $this->passiveMinPort,
79            FtpConstants::PASV_MAX_PORT => $this->passiveMaxPort,
80        ]);
81        $filesystem = $this->filesystemFactory->create($context);
82        $context = $context
83            ->withContext(ApieFilesystem::class, $filesystem)
84            ->withContext(FtpConstants::CURRENT_FOLDER, $filesystem->rootFolder);
85
86        $conn->on('data', function ($data) use ($conn, $output, &$context) {
87            $command = trim($data);
88
89            [$cmd, $arg] = array_pad(explode(' ', $command, 2), 2, null);
90            $cmd = strtoupper($cmd);
91            $output->writeln("Command $cmd $arg");
92
93            $context = $this->runner->run($context, $cmd, $arg ?? '');
94        });
95    }
96}