diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py
index d8b977ae3..b2e9a56f4 100644
--- a/gns3server/controller/__init__.py
+++ b/gns3server/controller/__init__.py
@@ -286,6 +286,20 @@ class Controller:
yield from project.open()
return project
+ def get_free_project_name(self, base_name):
+ """
+ Generate a free project name base on the base name
+ """
+ names = [ p.name for p in self._projects.values() ]
+ if base_name not in names:
+ return base_name
+ i = 1
+ while "{}-{}".format(base_name, i) in names:
+ i += 1
+ if i > 1000000:
+ raise aiohttp.web.HTTPConflict(text="A project name could not be allocated (node limit reached?)")
+ return "{}-{}".format(base_name, i)
+
@property
def projects(self):
"""
diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py
new file mode 100644
index 000000000..32ff1fc1e
--- /dev/null
+++ b/gns3server/controller/import_project.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import os
+import json
+import uuid
+import asyncio
+import zipfile
+import aiohttp
+
+from ..config import Config
+
+
+"""
+Handle the import of project from a .gns3project
+"""
+
+@asyncio.coroutine
+def import_project(controller, project_id, stream, gns3vm=True):
+ """
+ Import a project contain in a zip file
+
+ You need to handle OSerror exceptions
+
+ :param stream: A io.BytesIO of the zipfile
+ :param gns3vm: True move Docker, IOU and Qemu to the GNS3 VM
+ :returns: Project
+ """
+ server_config = Config.instance().get_section_config("Server")
+ projects_path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects"))
+ os.makedirs(projects_path, exist_ok=True)
+
+ with zipfile.ZipFile(stream) as myzip:
+
+ try:
+ topology = json.loads(myzip.read("project.gns3").decode())
+ # If the project name is already used we generate a new one
+ topology["name"] = controller.get_free_project_name(topology["name"])
+ except KeyError:
+ raise aiohttp.web.HTTPConflict(text="Can't import topology the .gns3 is corrupted or missing")
+
+ path = os.path.join(projects_path, topology["name"])
+ os.makedirs(path)
+ myzip.extractall(path)
+
+ dot_gns3_path = os.path.join(path, topology["name"] + ".gns3")
+ # We change the project_id to avoid erasing the project
+ topology["project_id"] = project_id
+ with open(dot_gns3_path, "w+") as f:
+ json.dump(topology, f, indent=4)
+ os.remove(os.path.join(path, "project.gns3"))
+
+ project = yield from controller.load_project(dot_gns3_path)
+ return project
+
diff --git a/gns3server/handlers/api/compute/project_handler.py b/gns3server/handlers/api/compute/project_handler.py
index 29e3d527e..ce9300dcc 100644
--- a/gns3server/handlers/api/compute/project_handler.py
+++ b/gns3server/handlers/api/compute/project_handler.py
@@ -409,7 +409,7 @@ class ProjectHandler:
# We write the content to a temporary location and after we extract it all.
# It could be more optimal to stream this but it is not implemented in Python.
- # Spooled means the file is temporary kept in memory until max_size is reached
+ # Spooled means the file is temporary kept in memory until max_size is reached
try:
with tempfile.SpooledTemporaryFile(max_size=10000) as temp:
while True:
diff --git a/gns3server/handlers/api/controller/project_handler.py b/gns3server/handlers/api/controller/project_handler.py
index f956526f1..fc5521169 100644
--- a/gns3server/handlers/api/controller/project_handler.py
+++ b/gns3server/handlers/api/controller/project_handler.py
@@ -18,10 +18,12 @@
import os
import aiohttp
import asyncio
-
+import tempfile
from gns3server.web.route import Route
from gns3server.controller import Controller
+from gns3server.controller.project import Project
+from gns3server.controller.import_project import import_project
from gns3server.config import Config
@@ -251,6 +253,39 @@ class ProjectHandler:
yield from response.write_eof()
+ @Route.post(
+ r"/projects/{project_id}/import",
+ description="Import a project from a portable archive",
+ parameters={
+ "project_id": "Project UUID",
+ },
+ raw=True,
+ output=PROJECT_OBJECT_SCHEMA,
+ status_codes={
+ 200: "Project imported",
+ 403: "Forbidden to import project"
+ })
+ def import_project(request, response):
+
+ controller = Controller.instance()
+
+ # We write the content to a temporary location and after we extract it all.
+ # It could be more optimal to stream this but it is not implemented in Python.
+ # Spooled means the file is temporary kept in memory until max_size is reached
+ try:
+ with tempfile.SpooledTemporaryFile(max_size=10000) as temp:
+ while True:
+ packet = yield from request.content.read(512)
+ if not packet:
+ break
+ temp.write(packet)
+ project = yield from import_project(controller, request.match_info["project_id"], temp, gns3vm=bool(int(request.GET.get("gns3vm", "1"))))
+ except OSError as e:
+ raise aiohttp.web.HTTPInternalServerError(text="Could not import the project: {}".format(e))
+
+ response.json(project)
+ response.set_status(201)
+
@Route.get(
r"/projects/{project_id}/files/{path:.+}",
description="Get a file from a project. Beware you have warranty to be able to access only to file global to the project (for example README.txt)",
@@ -332,3 +367,5 @@ class ProjectHandler:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
raise aiohttp.web.HTTPForbidden()
+
+
diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py
index 0bca4b2dd..b36dba317 100644
--- a/tests/controller/test_controller.py
+++ b/tests/controller/test_controller.py
@@ -335,3 +335,13 @@ def test_load_project(controller, async_run, tmpdir):
with asyncio_patch("gns3server.controller.Controller.add_project") as mock_add_project:
project = async_run(controller.load_project(str(tmpdir / "test.gns3")))
assert not mock_add_project.called
+
+
+def test_get_free_project_name(controller, async_run):
+
+ async_run(controller.add_project(project_id=str(uuid.uuid4()), name="Test"))
+ assert controller.get_free_project_name("Test") == "Test-1"
+ async_run(controller.add_project(project_id=str(uuid.uuid4()), name="Test-1"))
+ assert controller.get_free_project_name("Test") == "Test-2"
+ assert controller.get_free_project_name("Hello") == "Hello"
+
diff --git a/tests/controller/test_import_project.py b/tests/controller/test_import_project.py
new file mode 100644
index 000000000..f8067fbfa
--- /dev/null
+++ b/tests/controller/test_import_project.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import os
+import uuid
+import json
+import zipfile
+
+
+from gns3server.controller.project import Project
+from gns3server.controller.import_project import import_project
+
+
+def test_import_project(async_run, tmpdir, controller):
+ project_id = str(uuid.uuid4())
+
+ topology = {
+ "project_id": str(uuid.uuid4()),
+ "name": "test",
+ "topology": {
+ },
+ "version": "2.0.0"
+ }
+
+ with open(str(tmpdir / "project.gns3"), 'w+') as f:
+ json.dump(topology, f)
+ with open(str(tmpdir / "b.png"), 'w+') as f:
+ f.write("B")
+
+ zip_path = str(tmpdir / "project.zip")
+ with zipfile.ZipFile(zip_path, 'w') as myzip:
+ myzip.write(str(tmpdir / "project.gns3"), "project.gns3")
+ myzip.write(str(tmpdir / "b.png"), "b.png")
+ myzip.write(str(tmpdir / "b.png"), "project-files/dynamips/test")
+ myzip.write(str(tmpdir / "b.png"), "project-files/qemu/test")
+
+ with open(zip_path, "rb") as f:
+ project = async_run(import_project(controller, project_id, f))
+
+ assert project.name == "test"
+ assert project.id == project_id # The project should changed
+
+ assert os.path.exists(os.path.join(project.path, "b.png"))
+ assert not os.path.exists(os.path.join(project.path, "project.gns3"))
+ assert os.path.exists(os.path.join(project.path, "test.gns3"))
+ assert os.path.exists(os.path.join(project.path, "project-files/dynamips/test"))
+ assert os.path.exists(os.path.join(project.path, "project-files/qemu/test"))
+
+ # A new project name is generated when you import twice the same name
+ with open(zip_path, "rb") as f:
+ project = async_run(import_project(controller, str(uuid.uuid4()), f))
+ assert project.name != "test"
+
+
+
diff --git a/tests/handlers/api/compute/test_project.py b/tests/handlers/api/compute/test_project.py
index 86011dff7..4eeec3cd0 100644
--- a/tests/handlers/api/compute/test_project.py
+++ b/tests/handlers/api/compute/test_project.py
@@ -221,12 +221,12 @@ def test_export(http_compute, tmpdir, loop, project):
assert content == b"hello"
-def test_import(http_compute, tmpdir, loop, project):
+def test_import(http_compute, tmpdir, loop):
with zipfile.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip:
myzip.writestr("demo", b"hello")
- project_id = project.id
+ project_id = str(uuid.uuid4())
with open(str(tmpdir / "test.zip"), "rb") as f:
response = http_compute.post("/projects/{project_id}/import".format(project_id=project_id), body=f.read(), raw=True)
diff --git a/tests/handlers/api/controller/test_project.py b/tests/handlers/api/controller/test_project.py
index 88ac41d1d..7f301a2a6 100644
--- a/tests/handlers/api/controller/test_project.py
+++ b/tests/handlers/api/controller/test_project.py
@@ -202,3 +202,21 @@ def test_write_file(http_controller, tmpdir, project):
response = http_controller.post("/projects/{project_id}/files/../hello".format(project_id=project.id), raw=True)
assert response.status == 403
+
+
+def test_import(http_controller, tmpdir, controller):
+
+ with zipfile.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip:
+ myzip.writestr("project.gns3", b'{"version": "2.0.0", "name": "test"}')
+ myzip.writestr("demo", b"hello")
+
+ project_id = str(uuid.uuid4())
+
+ with open(str(tmpdir / "test.zip"), "rb") as f:
+ response = http_controller.post("/projects/{project_id}/import".format(project_id=project_id), body=f.read(), raw=True)
+ assert response.status == 201
+
+ project = controller.get_project(project_id)
+ with open(os.path.join(project.path, "demo")) as f:
+ content = f.read()
+ assert content == "hello"