diff --git a/.gitignore b/.gitignore index 7a9a605..45dcdd0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ vendor/ .idea/ log/* !log/.gitkeep +data/snapshots/* +!data/snapshots/.gitkeep diff --git a/README.md b/README.md index 53edca5..c7dceea 100755 --- a/README.md +++ b/README.md @@ -2,52 +2,15 @@ **ts3web** is a webinterface for any TeamSpeak 3 Server used with serverQuery login. -Most TeamSpeak 3 interfaces are bloated although nearly all configuration can be done entirely in the TeamSpeak 3 Client. - -This webinterface aims to be as simple as possible. It does not provide complex features which can be configured within the client. Instead, it only supports features considered useful for a TeamSpeak 3 web interface. **The minimalistic approach is intentional!** +This webinterface aims to be as simple as possible. It does not provide complex features. Instead, it only supports features considered useful for a TeamSpeak 3 web interface. **The minimalistic approach is intentional!** -If you like to help (to translate or implement missing features), open an issue before. Then create a pull request. You should use existing code to implement new features. PRs will be merged after a code review. +If you like to help (to translate or implement missing features), open an issue first. You should use existing code to implement new features. PRs will be merged after a code review. Things you **cannot** do: -- Channels create -- Permissions add, edit, delete (servergroups, channelgroups, client) -- File management (create, download, delete) -- Move online users -- features which are not *explicitly* supported - -Things you **can** do: -- view - - instance and host information - - global log - - virtual servers - - users online - - all known clients - - channels - - groups - - channel groups - - files - - banlist - - complain list - - permissions (server, channel, client) -- edit - - virtual server - - instance -- delete - - bans - - complains - - virtual servers - - clients - - server groups - - channel groups -- other actions - - create virtual servers - - generate serverQuery password - - send message to users, servers, channels - - ban a user - - kick a user - - poke a user - - add to server group - - remove from server group +- Permissions Management (add, edit, delete for servergroups, channelgroups, clients) +- File Management (except viewing) +- Temporary passwords +- Move online clients ## Install ## diff --git a/composer.json b/composer.json index 18c3a0c..8387eb1 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,9 @@ "par0noid/ts3admin": "^1.0", "planetteamspeak/ts3-php-framework": "^1.1", "nesbot/carbon": "^1.25", - "bryanjhv/slim-session": "^3.5" + "bryanjhv/slim-session": "^3.5", + "symfony/filesystem": "^4.0", + "symfony/finder": "^4.0" }, "config": { "bin-dir": "bin/" diff --git a/config/ACL.php b/config/ACL.php index 6297076..9bcbba7 100644 --- a/config/ACL.php +++ b/config/ACL.php @@ -41,6 +41,11 @@ class ACL extends \Zend\Permissions\Acl\Acl '/servers/send/{sid}', '/servers/edit/{sid}', + '/snapshots/{sid}', + '/snapshots/create/{sid}', + '/snapshots/deploy/{sid}/{name}', + '/snapshots/delete/{sid}/{name}', + '/tokens/{sid}', '/tokens/add/{sid}', '/tokens/delete/{sid}/{token}', diff --git a/config/routes.php b/config/routes.php index 7f41210..256965e 100644 --- a/config/routes.php +++ b/config/routes.php @@ -305,3 +305,24 @@ $container[TokenDeleteAction::class] = function ($container) { return new TokenDeleteAction($container); }; $app->get('/tokens/delete/{sid}/{token}', TokenDeleteAction::class); + +// snapshots +$container[SnapshotsAction::class] = function ($container) { + return new SnapshotsAction($container); +}; +$app->get('/snapshots/{sid}', SnapshotsAction::class); + +$container[SnapshotCreateAction::class] = function ($container) { + return new SnapshotCreateAction($container); +}; +$app->get('/snapshots/create/{sid}', SnapshotCreateAction::class); + +$container[SnapshotDeployAction::class] = function ($container) { + return new SnapshotDeployAction($container); +}; +$app->get('/snapshots/deploy/{sid}/{name}', SnapshotDeployAction::class); + +$container[SnapshotDeleteAction::class] = function ($container) { + return new SnapshotDeleteAction($container); +}; +$app->get('/snapshots/delete/{sid}/{name}', SnapshotDeleteAction::class); diff --git a/data/locale/en.yml b/data/locale/en.yml index 8b23a3e..8f7c327 100644 --- a/data/locale/en.yml +++ b/data/locale/en.yml @@ -64,6 +64,7 @@ menu.servers.groups: "Groups" menu.servers.bans: "Bans" menu.servers.complains: "Complains" menu.servers.tokens: "Tokens" +menu.servers.snapshots: "Snapshots" menu.servers.logs: "Log" # titles @@ -84,6 +85,7 @@ servergroup_info.title: "Server Group Info" channelgroup_info.title: "Channelgroup" profile.title: "Profile" tokens.title: "Tokens" +snapshots.title: "Snapshots" # dynamic render of key value pairs key: "Attribute" @@ -232,5 +234,13 @@ tokens.add.serverGroup: "Servergroup (type SERVER has to be selected)" tokens.add.channelGroup: "Channelgroup (type CHANNEL has to be selected)" tokens.add.channel: "Channel (type CHANNEL has to be selected)" tokens.add.description: "Description" -tokens.type.servergroup: "Server: " -tokens.type.channelgroup: "Channel: " \ No newline at end of file + +# snapshots +file.exists: "File already exists" +file.notexists: "File does not exist" +snapshots.h.actions: "Actions" +snapshots.create: "Create a new snapshot" +snapshots.h.details: "Details" +snapshots.deploy: "Deploy" +snapshots.delete: "Delete" + diff --git a/data/snapshots/.gitkeep b/data/snapshots/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/index.php b/public/index.php index c72916b..103ccc7 100644 --- a/public/index.php +++ b/public/index.php @@ -139,11 +139,9 @@ $container['view'] = function ($container) use ($app) { return $container['session']->get($key); })); - // ts specific: file size + // file size $fileSizeFilter = new Twig_SimpleFilter('file', function($bytes, $decimals = 2) { - $sz = 'BKMGTP'; - $factor = floor((strlen($bytes) - 1) / 3); - return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; + return FileHelper::humanFileSize($bytes, $decimals); }); $view->getEnvironment()->addFilter($fileSizeFilter); diff --git a/src/Control/Actions/ServerDeleteAction.php b/src/Control/Actions/ServerDeleteAction.php index d856ccb..ea8f90c 100644 --- a/src/Control/Actions/ServerDeleteAction.php +++ b/src/Control/Actions/ServerDeleteAction.php @@ -10,7 +10,6 @@ final class ServerDeleteAction extends AbstractAction $sid = $args['sid']; $this->ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); - $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); $dataResult = $this->ts->getInstance()->serverDelete($sid); diff --git a/src/Control/Actions/SnapshotCreateAction.php b/src/Control/Actions/SnapshotCreateAction.php new file mode 100644 index 0000000..6c67dd6 --- /dev/null +++ b/src/Control/Actions/SnapshotCreateAction.php @@ -0,0 +1,32 @@ +ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); + $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); + + $snapshotCreateResult = $this->ts->getInstance()->serverSnapshotCreate(); + + $fileSystem = new Filesystem(); + $name = Carbon::now()->getTimestamp(); + $path = FileHelper::SNAPSHOTS_PATH . DIRECTORY_SEPARATOR . $sid . DIRECTORY_SEPARATOR . $name; + + if ($fileSystem->exists($path)) { + $this->flash->addMessage('error', $this->translator->trans('file.exists')); + } else { + $fileSystem->appendToFile($path, trim($snapshotCreateResult['data'])); + $this->flash->addMessage('success', $this->translator->trans('done')); + } + + return $response->withRedirect('/snapshots/' . $sid); + } +} \ No newline at end of file diff --git a/src/Control/Actions/SnapshotDeleteAction.php b/src/Control/Actions/SnapshotDeleteAction.php new file mode 100644 index 0000000..a76addc --- /dev/null +++ b/src/Control/Actions/SnapshotDeleteAction.php @@ -0,0 +1,37 @@ +ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); + $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); + + $fileSystem = new Filesystem(); + $path = FileHelper::SNAPSHOTS_PATH . DIRECTORY_SEPARATOR . $sid . DIRECTORY_SEPARATOR . $name; + + if (!$fileSystem->exists($path)) { + $this->flash->addMessage('error', $this->translator->trans('file.notexists')); + } else { + $fileSystem->remove($path); + + $serverPath = FileHelper::SNAPSHOTS_PATH . DIRECTORY_SEPARATOR . $sid; + + if (count(FileHelper::getFiles($serverPath)) == 0) { + $fileSystem->remove($serverPath); + } + + $this->flash->addMessage('success', $this->translator->trans('done')); + } + + return $response->withRedirect('/snapshots/' . $sid); + } +} \ No newline at end of file diff --git a/src/Control/Actions/SnapshotDeployAction.php b/src/Control/Actions/SnapshotDeployAction.php new file mode 100644 index 0000000..123e279 --- /dev/null +++ b/src/Control/Actions/SnapshotDeployAction.php @@ -0,0 +1,31 @@ +ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); + $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); + + $fileSystem = new Filesystem(); + $path = FileHelper::SNAPSHOTS_PATH . DIRECTORY_SEPARATOR . $sid . DIRECTORY_SEPARATOR . $name; + + if (!$fileSystem->exists($path)) { + $this->flash->addMessage('error', $this->translator->trans('file.notexists')); + } else { + $snapshotData = file_get_contents($path); + $this->ts->getInstance()->serverSnapshotDeploy($snapshotData, true); + $this->flash->addMessage('success', $this->translator->trans('done')); + } + + return $response->withRedirect('/snapshots/' . $sid); + } +} \ No newline at end of file diff --git a/src/Control/Actions/SnapshotsAction.php b/src/Control/Actions/SnapshotsAction.php new file mode 100644 index 0000000..a48cbdb --- /dev/null +++ b/src/Control/Actions/SnapshotsAction.php @@ -0,0 +1,24 @@ +ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); + $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); + + $snapshots = FileHelper::getFiles(FileHelper::SNAPSHOTS_PATH . DIRECTORY_SEPARATOR . $sid); + + // render GET + $this->view->render($response, 'snapshots.twig', [ + 'title' => $this->translator->trans('snapshots.title'), + 'data' => $snapshots, + 'sid' => $sid + ]); + } +} \ No newline at end of file diff --git a/src/Util/FileHelper.php b/src/Util/FileHelper.php new file mode 100644 index 0000000..2e9b4e0 --- /dev/null +++ b/src/Util/FileHelper.php @@ -0,0 +1,52 @@ +exists($directory)) { + return $files; + } + + $finder = new Finder(); + $finder->files()->in($directory)->sortByChangedTime(); + + foreach ($finder as $file) { + $files[] = [ + 'name' => $file->getFilename(), + 'size' => FileHelper::humanFileSize($file->getSize()), + 'date' => Carbon::createFromTimestamp($file->getMTime()) + ]; + } + + return $files; + } + + /** + * Output human readable file size + * + * @param $bytes + * @param int $decimals + * @return string + */ + public static function humanFileSize($bytes, $decimals = 2) { + $sz = 'BKMGTP'; + $factor = floor((strlen($bytes) - 1) / 3); + return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; + } +} \ No newline at end of file diff --git a/src/View/material/layout/menu.twig b/src/View/material/layout/menu.twig index 5ffe12c..d84f9d9 100644 --- a/src/View/material/layout/menu.twig +++ b/src/View/material/layout/menu.twig @@ -40,6 +40,9 @@
  • {% trans %}menu.servers.tokens{% endtrans %}
  • + +
  • {% trans %}menu.servers.snapshots{% endtrans %}
  • +
  • {% trans %}menu.servers.logs{% endtrans %}
  • diff --git a/src/View/material/snapshots.twig b/src/View/material/snapshots.twig new file mode 100644 index 0000000..465b603 --- /dev/null +++ b/src/View/material/snapshots.twig @@ -0,0 +1,31 @@ +{% extends 'layout.twig' %} + +{% block content %} +

    {{ title }}

    + +

    {% trans %}snapshots.h.actions{% endtrans %}

    + {% trans %}snapshots.create{% endtrans %} + + {% if data|length > 0 %} +

    {% trans %}snapshots.h.details{% endtrans %}

    + + {% include 'table.twig' with {'data': data, + + 'additional_links': [ + { + 'header_label': 'snapshots.deploy'|trans, + 'label': 'check_circle', + 'uri': '/snapshots/deploy/' ~ sid, + 'uri_param': 'name' + }, + { + 'header_label': 'snapshots.delete'|trans, + 'label': 'delete', + 'uri': '/snapshots/delete/' ~ sid, + 'uri_param': 'name' + } + ], + + } %} + {% endif %} +{% endblock %} \ No newline at end of file