diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 9a431276..0e8036b8 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -18,6 +18,9 @@ import os import aiohttp import stat +import io +import tarfile +import asyncio from ..config import Config from ..web.route import Route @@ -81,7 +84,52 @@ class UploadHandler: return response.redirect("/upload") + @classmethod + @Route.get( + r"/upload/backup/images.tar", + description="Backup GNS3 images", + api_version=None + ) + def backup_images(request, response): + yield from UploadHandler._backup_directory(request, response, UploadHandler.image_directory()) + + @classmethod + @Route.get( + r"/upload/backup/projects.tar", + description="Backup GNS3 projects", + api_version=None + ) + def backup_images(request, response): + yield from UploadHandler._backup_directory(request, response, UploadHandler.project_directory()) + + @staticmethod + @asyncio.coroutine + def _backup_directory(request, response, directory): + response.content_type = 'application/x-gtar' + response.set_status(200) + response.enable_chunked_encoding() + # Very important: do not send a content length otherwise QT close the connection but curl can consume the Feed + response.content_length = None + response.start(request) + + buffer = io.BytesIO() + with tarfile.open('arch.tar', 'w', fileobj=buffer) as tar: + for root, dirs, files in os.walk(directory): + for file in files: + path = os.path.join(root, file) + tar.add(os.path.join(root, file), arcname=os.path.relpath(path, directory)) + response.write(buffer.getvalue()) + yield from response.drain() + buffer.truncate(0) + buffer.seek(0) + yield from response.write_eof() + @staticmethod def image_directory(): server_config = Config.instance().get_section_config("Server") return os.path.expanduser(server_config.get("images_path", "~/GNS3/images")) + + @staticmethod + def project_directory(): + server_config = Config.instance().get_section_config("Server") + return os.path.expanduser(server_config.get("projects_path", "~/GNS3/images")) diff --git a/gns3server/templates/layout.html b/gns3server/templates/layout.html index 9ecbc82c..bcb38145 100644 --- a/gns3server/templates/layout.html +++ b/gns3server/templates/layout.html @@ -4,6 +4,13 @@ GNS3 Server +
+ Home + | + Backup images + | + Backup projects +
{% block body %}{% endblock %} diff --git a/tests/handlers/api/base.py b/tests/handlers/api/base.py index 6a4fd02e..a4a3a034 100644 --- a/tests/handlers/api/base.py +++ b/tests/handlers/api/base.py @@ -88,7 +88,10 @@ class Query: except ValueError: response.json = None else: - response.html = response.body.decode("utf-8") + try: + response.html = response.body.decode("utf-8") + except UnicodeDecodeError: + response.html = None else: response.json = {} response.html = "" diff --git a/tests/handlers/test_upload.py b/tests/handlers/test_upload.py index 64fdab73..8d3a8b66 100644 --- a/tests/handlers/test_upload.py +++ b/tests/handlers/test_upload.py @@ -17,10 +17,15 @@ import aiohttp +import asyncio import os +import tarfile from unittest.mock import patch + + from gns3server.config import Config + def test_index_upload(server): response = server.get('/upload', api_version=None) assert response.status == 200 @@ -44,3 +49,66 @@ def test_upload(server, tmpdir): assert f.read() == "TEST" assert "test2" in response.body.decode("utf-8") + + +def test_backup_images(server, tmpdir, loop): + Config.instance().set('Server', 'images_path', str(tmpdir)) + + os.makedirs(str(tmpdir / 'QEMU')) + with open(str(tmpdir / 'QEMU' / 'a.img'), 'w+') as f: + f.write('hello') + with open(str(tmpdir / 'QEMU' / 'b.img'), 'w+') as f: + f.write('world') + + response = server.get('/upload/backup/images.tar', api_version=None, raw=True) + assert response.status == 200 + assert response.headers['CONTENT-TYPE'] == 'application/x-gtar' + + with open(str(tmpdir / 'images.tar'), 'wb+') as f: + print(len(response.body)) + f.write(response.body) + + tar = tarfile.open(str(tmpdir / 'images.tar'), 'r') + os.makedirs(str(tmpdir / 'extract')) + os.chdir(str(tmpdir / 'extract')) + # Extract to current working directory + tar.extractall() + tar.close() + + assert os.path.exists(os.path.join('QEMU', 'a.img')) + open(os.path.join('QEMU', 'a.img')).read() == 'hello' + + assert os.path.exists(os.path.join('QEMU', 'b.img')) + open(os.path.join('QEMU', 'b.img')).read() == 'world' + + +def test_backup_projects(server, tmpdir, loop): + Config.instance().set('Server', 'projects_path', str(tmpdir)) + + os.makedirs(str(tmpdir / 'a')) + with open(str(tmpdir / 'a' / 'a.gns3'), 'w+') as f: + f.write('hello') + os.makedirs(str(tmpdir / 'b')) + with open(str(tmpdir / 'b' / 'b.gns3'), 'w+') as f: + f.write('world') + + response = server.get('/upload/backup/projects.tar', api_version=None, raw=True) + assert response.status == 200 + assert response.headers['CONTENT-TYPE'] == 'application/x-gtar' + + with open(str(tmpdir / 'projects.tar'), 'wb+') as f: + print(len(response.body)) + f.write(response.body) + + tar = tarfile.open(str(tmpdir / 'projects.tar'), 'r') + os.makedirs(str(tmpdir / 'extract')) + os.chdir(str(tmpdir / 'extract')) + # Extract to current working directory + tar.extractall() + tar.close() + + assert os.path.exists(os.path.join('a', 'a.gns3')) + open(os.path.join('a', 'a.gns3')).read() == 'hello' + + assert os.path.exists(os.path.join('b', 'b.gns3')) + open(os.path.join('b', 'b.gns3')).read() == 'world'