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 @@