Add snapshots
This commit is contained in:
parent
4ae2bd5b44
commit
871dfa5675
16 changed files with 261 additions and 51 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -12,3 +12,5 @@ vendor/
|
||||||
.idea/
|
.idea/
|
||||||
log/*
|
log/*
|
||||||
!log/.gitkeep
|
!log/.gitkeep
|
||||||
|
data/snapshots/*
|
||||||
|
!data/snapshots/.gitkeep
|
||||||
|
|
49
README.md
49
README.md
|
@ -2,52 +2,15 @@
|
||||||
|
|
||||||
**ts3web** is a webinterface for any TeamSpeak 3 Server used with serverQuery login.
|
**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. 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 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!**
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Things you **cannot** do:
|
Things you **cannot** do:
|
||||||
- Channels create
|
- Permissions Management (add, edit, delete for servergroups, channelgroups, clients)
|
||||||
- Permissions add, edit, delete (servergroups, channelgroups, client)
|
- File Management (except viewing)
|
||||||
- File management (create, download, delete)
|
- Temporary passwords
|
||||||
- Move online users
|
- Move online clients
|
||||||
- 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
|
|
||||||
|
|
||||||
## Install ##
|
## Install ##
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
"par0noid/ts3admin": "^1.0",
|
"par0noid/ts3admin": "^1.0",
|
||||||
"planetteamspeak/ts3-php-framework": "^1.1",
|
"planetteamspeak/ts3-php-framework": "^1.1",
|
||||||
"nesbot/carbon": "^1.25",
|
"nesbot/carbon": "^1.25",
|
||||||
"bryanjhv/slim-session": "^3.5"
|
"bryanjhv/slim-session": "^3.5",
|
||||||
|
"symfony/filesystem": "^4.0",
|
||||||
|
"symfony/finder": "^4.0"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"bin-dir": "bin/"
|
"bin-dir": "bin/"
|
||||||
|
|
|
@ -41,6 +41,11 @@ class ACL extends \Zend\Permissions\Acl\Acl
|
||||||
'/servers/send/{sid}',
|
'/servers/send/{sid}',
|
||||||
'/servers/edit/{sid}',
|
'/servers/edit/{sid}',
|
||||||
|
|
||||||
|
'/snapshots/{sid}',
|
||||||
|
'/snapshots/create/{sid}',
|
||||||
|
'/snapshots/deploy/{sid}/{name}',
|
||||||
|
'/snapshots/delete/{sid}/{name}',
|
||||||
|
|
||||||
'/tokens/{sid}',
|
'/tokens/{sid}',
|
||||||
'/tokens/add/{sid}',
|
'/tokens/add/{sid}',
|
||||||
'/tokens/delete/{sid}/{token}',
|
'/tokens/delete/{sid}/{token}',
|
||||||
|
|
|
@ -305,3 +305,24 @@ $container[TokenDeleteAction::class] = function ($container) {
|
||||||
return new TokenDeleteAction($container);
|
return new TokenDeleteAction($container);
|
||||||
};
|
};
|
||||||
$app->get('/tokens/delete/{sid}/{token}', TokenDeleteAction::class);
|
$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);
|
||||||
|
|
|
@ -64,6 +64,7 @@ menu.servers.groups: "Groups"
|
||||||
menu.servers.bans: "Bans"
|
menu.servers.bans: "Bans"
|
||||||
menu.servers.complains: "Complains"
|
menu.servers.complains: "Complains"
|
||||||
menu.servers.tokens: "Tokens"
|
menu.servers.tokens: "Tokens"
|
||||||
|
menu.servers.snapshots: "Snapshots"
|
||||||
menu.servers.logs: "Log"
|
menu.servers.logs: "Log"
|
||||||
|
|
||||||
# titles
|
# titles
|
||||||
|
@ -84,6 +85,7 @@ servergroup_info.title: "Server Group Info"
|
||||||
channelgroup_info.title: "Channelgroup"
|
channelgroup_info.title: "Channelgroup"
|
||||||
profile.title: "Profile"
|
profile.title: "Profile"
|
||||||
tokens.title: "Tokens"
|
tokens.title: "Tokens"
|
||||||
|
snapshots.title: "Snapshots"
|
||||||
|
|
||||||
# dynamic render of key value pairs
|
# dynamic render of key value pairs
|
||||||
key: "Attribute"
|
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.channelGroup: "Channelgroup (type CHANNEL has to be selected)"
|
||||||
tokens.add.channel: "Channel (type CHANNEL has to be selected)"
|
tokens.add.channel: "Channel (type CHANNEL has to be selected)"
|
||||||
tokens.add.description: "Description"
|
tokens.add.description: "Description"
|
||||||
tokens.type.servergroup: "Server: "
|
|
||||||
tokens.type.channelgroup: "Channel: "
|
# 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"
|
||||||
|
|
||||||
|
|
0
data/snapshots/.gitkeep
Normal file
0
data/snapshots/.gitkeep
Normal file
|
@ -139,11 +139,9 @@ $container['view'] = function ($container) use ($app) {
|
||||||
return $container['session']->get($key);
|
return $container['session']->get($key);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ts specific: file size
|
// file size
|
||||||
$fileSizeFilter = new Twig_SimpleFilter('file', function($bytes, $decimals = 2) {
|
$fileSizeFilter = new Twig_SimpleFilter('file', function($bytes, $decimals = 2) {
|
||||||
$sz = 'BKMGTP';
|
return FileHelper::humanFileSize($bytes, $decimals);
|
||||||
$factor = floor((strlen($bytes) - 1) / 3);
|
|
||||||
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor];
|
|
||||||
});
|
});
|
||||||
$view->getEnvironment()->addFilter($fileSizeFilter);
|
$view->getEnvironment()->addFilter($fileSizeFilter);
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ final class ServerDeleteAction extends AbstractAction
|
||||||
$sid = $args['sid'];
|
$sid = $args['sid'];
|
||||||
|
|
||||||
$this->ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']);
|
$this->ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']);
|
||||||
$selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId');
|
|
||||||
|
|
||||||
$dataResult = $this->ts->getInstance()->serverDelete($sid);
|
$dataResult = $this->ts->getInstance()->serverDelete($sid);
|
||||||
|
|
||||||
|
|
32
src/Control/Actions/SnapshotCreateAction.php
Normal file
32
src/Control/Actions/SnapshotCreateAction.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
final class SnapshotCreateAction extends AbstractAction
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$sid = $args['sid'];
|
||||||
|
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
37
src/Control/Actions/SnapshotDeleteAction.php
Normal file
37
src/Control/Actions/SnapshotDeleteAction.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
final class SnapshotDeleteAction extends AbstractAction
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$sid = $args['sid'];
|
||||||
|
$name = $args['name'];
|
||||||
|
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
31
src/Control/Actions/SnapshotDeployAction.php
Normal file
31
src/Control/Actions/SnapshotDeployAction.php
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
final class SnapshotDeployAction extends AbstractAction
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$sid = $args['sid'];
|
||||||
|
$name = $args['name'];
|
||||||
|
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
24
src/Control/Actions/SnapshotsAction.php
Normal file
24
src/Control/Actions/SnapshotsAction.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
final class SnapshotsAction extends AbstractAction
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$sid = $args['sid'];
|
||||||
|
|
||||||
|
$this->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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
52
src/Util/FileHelper.php
Normal file
52
src/Util/FileHelper.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
|
class FileHelper
|
||||||
|
{
|
||||||
|
const SNAPSHOTS_PATH = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'snapshots';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all files in FILES_DIR
|
||||||
|
*
|
||||||
|
* @param $directory
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getFiles($directory)
|
||||||
|
{
|
||||||
|
$files = [];
|
||||||
|
|
||||||
|
$fileSystem = new Filesystem();
|
||||||
|
if (!$fileSystem->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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,9 @@
|
||||||
<a href="/tokens/{{ session_get('sid') }}">
|
<a href="/tokens/{{ session_get('sid') }}">
|
||||||
<li class="active withripple" data-target="#page">{% trans %}menu.servers.tokens{% endtrans %}</li>
|
<li class="active withripple" data-target="#page">{% trans %}menu.servers.tokens{% endtrans %}</li>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/snapshots/{{ session_get('sid') }}">
|
||||||
|
<li class="active withripple" data-target="#page">{% trans %}menu.servers.snapshots{% endtrans %}</li>
|
||||||
|
</a>
|
||||||
<a href="/logs/{{ session_get('sid') }}">
|
<a href="/logs/{{ session_get('sid') }}">
|
||||||
<li class="active withripple" data-target="#page">{% trans %}menu.servers.logs{% endtrans %}</li>
|
<li class="active withripple" data-target="#page">{% trans %}menu.servers.logs{% endtrans %}</li>
|
||||||
</a>
|
</a>
|
||||||
|
|
31
src/View/material/snapshots.twig
Normal file
31
src/View/material/snapshots.twig
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{% extends 'layout.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="header">{{ title }}</h1>
|
||||||
|
|
||||||
|
<h2 class="header">{% trans %}snapshots.h.actions{% endtrans %}</h2>
|
||||||
|
<a href="/snapshots/create/{{ sid }}">{% trans %}snapshots.create{% endtrans %}</a>
|
||||||
|
|
||||||
|
{% if data|length > 0 %}
|
||||||
|
<h2 class="header">{% trans %}snapshots.h.details{% endtrans %}</h2>
|
||||||
|
|
||||||
|
{% include 'table.twig' with {'data': data,
|
||||||
|
|
||||||
|
'additional_links': [
|
||||||
|
{
|
||||||
|
'header_label': 'snapshots.deploy'|trans,
|
||||||
|
'label': '<i class="material-icons">check_circle</i>',
|
||||||
|
'uri': '/snapshots/deploy/' ~ sid,
|
||||||
|
'uri_param': 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'header_label': 'snapshots.delete'|trans,
|
||||||
|
'label': '<i class="material-icons">delete</i>',
|
||||||
|
'uri': '/snapshots/delete/' ~ sid,
|
||||||
|
'uri_param': 'name'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
} %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
Reference in a new issue