2020-06-20 18:07:43 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
class FileServer
|
|
|
|
{
|
|
|
|
private ZMQSocket $commandSocket;
|
|
|
|
private ZMQSocket $querySocket;
|
|
|
|
private ZMQPoll $poll;
|
|
|
|
private string $rootDirectory;
|
|
|
|
|
2020-06-20 19:04:10 +02:00
|
|
|
public function __construct(ZMQContext $context, string $rootDirectory, array $commandDSNs, array $queryDSNs)
|
2020-06-20 18:07:43 +02:00
|
|
|
{
|
2020-06-20 19:04:10 +02:00
|
|
|
$this->initializeCommandSocket($context, $commandDSNs);
|
|
|
|
$this->initializeQuerySocket($context, $queryDSNs);
|
2020-06-20 18:07:43 +02:00
|
|
|
$this->initializeRootDirectory($rootDirectory);
|
|
|
|
$this->initializePoll();
|
|
|
|
}
|
|
|
|
|
|
|
|
private function initializeRootDirectory(string $rootDirectory): void
|
|
|
|
{
|
|
|
|
$this->rootDirectory = rtrim($rootDirectory, DIRECTORY_SEPARATOR);
|
|
|
|
if (!is_dir($this->rootDirectory)) {
|
|
|
|
mkdir($this->rootDirectory, 0777, true);
|
|
|
|
}
|
2020-06-20 19:04:10 +02:00
|
|
|
if (!is_dir($this->rootDirectory)) {
|
|
|
|
throw new RuntimeException("Unable to create directory '{$this->rootDirectory}'");
|
|
|
|
}
|
2020-06-20 18:07:43 +02:00
|
|
|
}
|
|
|
|
|
2020-06-20 19:04:10 +02:00
|
|
|
private function initializeCommandSocket(ZMQContext $context, array $commandDSNs): void
|
2020-06-20 18:07:43 +02:00
|
|
|
{
|
2020-06-20 19:04:10 +02:00
|
|
|
if (count($commandDSNs) === 0) {
|
2020-06-20 18:07:43 +02:00
|
|
|
throw new InvalidArgumentException("At least one binding required");
|
|
|
|
}
|
|
|
|
$this->commandSocket = new ZMQSocket($context, ZMQ::SOCKET_PULL);
|
|
|
|
$this->commandSocket->setSockOpt(ZMQ::SOCKOPT_HWM, 5);
|
2020-06-20 19:04:10 +02:00
|
|
|
foreach ($commandDSNs as $dsn) {
|
2020-06-20 18:07:43 +02:00
|
|
|
$this->commandSocket->bind($dsn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-20 19:04:10 +02:00
|
|
|
private function initializeQuerySocket(ZMQContext $context, array $queryDSNs): void
|
2020-06-20 18:07:43 +02:00
|
|
|
{
|
2020-06-20 19:04:10 +02:00
|
|
|
if (count($queryDSNs) === 0) {
|
2020-06-20 18:07:43 +02:00
|
|
|
throw new InvalidArgumentException("At least one binding required");
|
|
|
|
}
|
|
|
|
$this->querySocket = new ZMQSocket($context, ZMQ::SOCKET_REP);
|
|
|
|
$this->querySocket->setSockOpt(ZMQ::SOCKOPT_HWM, 5);
|
2020-06-20 19:04:10 +02:00
|
|
|
foreach ($queryDSNs as $dsn) {
|
2020-06-20 18:07:43 +02:00
|
|
|
$this->querySocket->bind($dsn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function initializePoll(): void
|
|
|
|
{
|
|
|
|
$this->poll = new ZMQPoll();
|
|
|
|
$this->poll->add($this->commandSocket, ZMQ::POLL_IN);
|
|
|
|
$this->poll->add($this->querySocket, ZMQ::POLL_IN);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function run()
|
|
|
|
{
|
|
|
|
$readable = $writable = [];
|
|
|
|
while (true) {
|
|
|
|
$this->poll->poll($readable, $writable);
|
|
|
|
foreach ($readable as $socket) {
|
|
|
|
$socket === $this->querySocket and $this->onQuery();
|
|
|
|
$socket === $this->commandSocket and $this->onCommand();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function onQuery(): void
|
|
|
|
{
|
|
|
|
$arguments = $this->querySocket->recvMulti();
|
|
|
|
$query = array_shift($arguments);
|
|
|
|
switch ($query) {
|
|
|
|
case 'LOAD':
|
|
|
|
if (count($arguments) !== 2) {
|
|
|
|
$this->querySocket->send(-1);
|
2020-06-20 19:04:10 +02:00
|
|
|
$this->onNotEnoughArguments($query, 2, count($arguments));
|
2020-06-20 18:07:43 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
$this->querySocket->send(@file_get_contents("{$this->rootDirectory}/{$arguments[0]}/{$arguments[1]}") ?: -1);
|
|
|
|
break;
|
|
|
|
case 'CONTAINS':
|
|
|
|
if (count($arguments) !== 2) {
|
|
|
|
$this->querySocket->send(-1);
|
2020-06-20 19:04:10 +02:00
|
|
|
$this->onNotEnoughArguments($query, 2, count($arguments));
|
2020-06-20 18:07:43 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
$this->querySocket->send(file_exists("{$this->rootDirectory}/{$arguments[0]}/{$arguments[1]}") ? 'Y' : 'N');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
$this->querySocket->send(-1);
|
|
|
|
$this->onUnknownInput($query, $arguments);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function onCommand(): void
|
|
|
|
{
|
|
|
|
$arguments = $this->commandSocket->recvMulti();
|
|
|
|
$command = array_shift($arguments);
|
|
|
|
switch ($command) {
|
|
|
|
case 'SAVE':
|
|
|
|
if (count($arguments) !== 3) {
|
2020-06-20 19:04:10 +02:00
|
|
|
$this->onNotEnoughArguments($command, 3, count($arguments));
|
2020-06-20 18:07:43 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
$this->saveFile($arguments[0], $arguments[1], $arguments[2]);
|
|
|
|
break;
|
|
|
|
case 'DELETE':
|
|
|
|
if (count($arguments) !== 2) {
|
2020-06-20 19:04:10 +02:00
|
|
|
$this->onNotEnoughArguments($command, 2, count($arguments));
|
2020-06-20 18:07:43 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
$this->deleteFile($arguments[0], $arguments[1]);
|
|
|
|
break;
|
|
|
|
case 'DELETE_ALL':
|
|
|
|
if (count($arguments) !== 1) {
|
2020-06-20 19:04:10 +02:00
|
|
|
$this->onNotEnoughArguments($command, 1, count($arguments));
|
|
|
|
return;
|
2020-06-20 18:07:43 +02:00
|
|
|
}
|
|
|
|
$this->deleteAll($arguments[0]);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
$this->onUnknownInput($command, $arguments);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-20 19:04:10 +02:00
|
|
|
private function onNotEnoughArguments(string $input, int $expected, int $actual): void
|
|
|
|
{
|
|
|
|
printf(date('[H:i:s]') . " Error: unexpected amount of arguments for input '%s' (%d/%d)\n", $input, $actual, $expected);
|
|
|
|
}
|
|
|
|
|
2020-06-20 18:07:43 +02:00
|
|
|
private function onUnknownInput(string $input, array $arguments): void
|
|
|
|
{
|
|
|
|
$arguments = array_map(fn($v) => substr($v, 0, 12), $arguments);
|
2020-06-20 19:04:10 +02:00
|
|
|
printf(date('[H:i:s]') . " Error: unknown input '%s' with arguments [%s]\n", $input, implode(', ', $arguments));
|
2020-06-20 18:07:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private function saveFile(string &$namespace, string &$name, string &$content): void
|
|
|
|
{
|
|
|
|
is_dir($targetDir = "{$this->rootDirectory}/$namespace") or mkdir($targetDir, 0777, true);
|
|
|
|
file_put_contents("$targetDir/$name", $content);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function deleteFile(string &$namespace, string &$name): void
|
|
|
|
{
|
|
|
|
@unlink("{$this->rootDirectory}/$namespace/$name");
|
|
|
|
}
|
|
|
|
|
|
|
|
private function deleteAll(string &$namespace): void
|
|
|
|
{
|
|
|
|
$directory = "{$this->rootDirectory}/$namespace";
|
|
|
|
if (!is_dir($directory)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
static $source = '/tmp/zmq.fileserver.rsync_empty_dir.d';
|
|
|
|
is_dir($source) or @mkdir($source);
|
|
|
|
@system("nohup rsync --archive --recursive --delete {$source}/ {$directory}/ 2>&1 >/dev/null &");
|
|
|
|
@rmdir($directory);
|
|
|
|
}
|
|
|
|
}
|