From 879591eaf57cacb13beb3771e2d68c5424fb72ed Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 30 Mar 2016 11:43:31 +0200 Subject: [PATCH] Export API --- gns3server/handlers/api/project_handler.py | 29 ++++++++++++++++++++++ gns3server/modules/project.py | 29 ++++++++++++++++++++++ requirements.txt | 1 + tests/handlers/api/test_project.py | 21 ++++++++++++++++ tests/handlers/test_upload.py | 1 - tests/modules/test_project.py | 28 +++++++++++++++++++++ 6 files changed, 108 insertions(+), 1 deletion(-) diff --git a/gns3server/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py index d918dd87..398a65ae 100644 --- a/gns3server/handlers/api/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -342,3 +342,32 @@ class ProjectHandler: raise aiohttp.web.HTTPNotFound() except PermissionError: raise aiohttp.web.HTTPForbidden + + @classmethod + @Route.get( + r"/projects/{project_id}/export", + description="Export a project as a portable archive", + parameters={ + "project_id": "The UUID of the project", + }, + raw=True, + status_codes={ + 200: "Return the file", + 404: "The path doesn't exist" + }) + def export(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["project_id"]) + response.content_type = 'application/gns3z' + response.headers['CONTENT-DISPOSITION'] = 'attachment; filename="{}.gns3z"'.format(project.name) + 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) + + for data in project.export(): + response.write(data) + yield from response.drain() + + yield from response.write_eof() diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 8682490b..a3adaf8d 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -20,6 +20,8 @@ import os import shutil import asyncio import hashlib +import zipstream +import zipfile from uuid import UUID, uuid4 from .port_manager import PortManager @@ -507,3 +509,30 @@ class Project: break m.update(buf) return m.hexdigest() + + def export(self): + """ + Export the project as zip. It's a ZipStream object. + The file will be read chunk by chunk when you iterate on + the zip. + + It will ignore some files like snapshots and + + :returns: ZipStream object + """ + + z = zipstream.ZipFile() + # topdown allo to modify the list of directory in order to ignore + # directory + for root, dirs, files in os.walk(self._path, topdown=True): + # Remove snapshots + if "project-files" in root: + dirs[:] = [d for d in dirs if d != "snapshots"] + + # Ignore log files and OS noise + files = [f for f in files if not f.endswith('_log.txt') and not f.endswith('.log') and f != '.DS_Store'] + + for file in files: + path = os.path.join(root, file) + z.write(path, os.path.relpath(path, self._path)) + return z diff --git a/requirements.txt b/requirements.txt index 083172b3..7cc8f21b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ aiohttp==0.19.0 Jinja2>=2.7.3 raven>=5.2.0 psutil>=3.0.0 +zipstream>=1.1.3 diff --git a/tests/handlers/api/test_project.py b/tests/handlers/api/test_project.py index 877593a6..9596414a 100644 --- a/tests/handlers/api/test_project.py +++ b/tests/handlers/api/test_project.py @@ -23,6 +23,7 @@ import uuid import os import asyncio import aiohttp +import zipfile from unittest.mock import patch from tests.utils import asyncio_patch @@ -283,3 +284,23 @@ def test_write_file(server, tmpdir): response = server.post("/projects/{project_id}/files/../hello".format(project_id=project.id), body="universe", raw=True) assert response.status == 403 + + +def test_export(server, tmpdir, loop, project): + + os.makedirs(project.path, exist_ok=True) + with open(os.path.join(project.path, 'a'), 'w+') as f: + f.write('hello') + + response = server.get("/projects/{project_id}/export".format(project_id=project.id), raw=True) + assert response.status == 200 + assert response.headers['CONTENT-TYPE'] == 'application/gns3z' + assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3z"'.format(project.name) + + with open(str(tmpdir / 'project.zip'), 'wb+') as f: + f.write(response.body) + + with zipfile.ZipFile(str(tmpdir / 'project.zip')) as myzip: + with myzip.open("a") as myfile: + content = myfile.read() + assert content == b"hello" diff --git a/tests/handlers/test_upload.py b/tests/handlers/test_upload.py index 3ef16c78..7e39e4fc 100644 --- a/tests/handlers/test_upload.py +++ b/tests/handlers/test_upload.py @@ -219,7 +219,6 @@ def test_backup_projects(server, tmpdir, loop): 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') diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 1a9a1b93..0730e1b8 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -20,6 +20,7 @@ import os import asyncio import pytest import aiohttp +import zipfile from uuid import uuid4 from unittest.mock import patch @@ -258,3 +259,30 @@ def test_list_files(tmpdir, loop): "md5sum": "098f6bcd4621d373cade4e832627b4f6" } ] + + +def test_export(tmpdir): + project = Project() + path = project.path + os.makedirs(os.path.join(path, "vm-1", "dynamips")) + with open(os.path.join(path, "vm-1", "dynamips", "test"), 'w+') as f: + f.write("HELLO") + with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f: + f.write("LOG") + os.makedirs(os.path.join(path, "project-files", "snapshots")) + with open(os.path.join(path, "project-files", "snapshots", "test"), 'w+') as f: + f.write("WORLD") + + z = project.export() + + with open(str(tmpdir / 'zipfile.zip'), 'wb') as f: + for data in z: + f.write(data) + + with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with myzip.open("vm-1/dynamips/test") as myfile: + content = myfile.read() + assert content == b"HELLO" + + assert 'project-files/snapshots/test' not in myzip.namelist() + assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist()