diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py
index 307eece3..5db20453 100644
--- a/gns3server/handlers/__init__.py
+++ b/gns3server/handlers/__init__.py
@@ -27,6 +27,7 @@ from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler
from gns3server.handlers.api.vpcs_handler import VPCSHandler
from gns3server.handlers.api.config_handler import ConfigHandler
from gns3server.handlers.api.server_handler import ServerHandler
+from gns3server.handlers.api.file_handler import FileHandler
from gns3server.handlers.upload_handler import UploadHandler
from gns3server.handlers.index_handler import IndexHandler
diff --git a/gns3server/handlers/api/file_handler.py b/gns3server/handlers/api/file_handler.py
new file mode 100644
index 00000000..e705ffc2
--- /dev/null
+++ b/gns3server/handlers/api/file_handler.py
@@ -0,0 +1,60 @@
+#
+# Copyright (C) 2015 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 asyncio
+import aiohttp
+
+from ...web.route import Route
+from ...schemas.file import FILE_STREAM_SCHEMA
+
+
+class FileHandler:
+
+ @classmethod
+ @Route.get(
+ r"/files/stream",
+ description="Stream a file from the server",
+ status_codes={
+ 200: "File retrieved",
+ 404: "File doesn't exist",
+ 409: "Can't access to file"
+ },
+ input=FILE_STREAM_SCHEMA
+ )
+ def read(request, response):
+ response.enable_chunked_encoding()
+
+ try:
+ with open(request.json.get("location"), "rb") as f:
+ loop = asyncio.get_event_loop()
+ response.content_type = "application/octet-stream"
+ response.set_status(200)
+ # Very important: do not send a content lenght otherwise QT close the connection but curl can consume the Feed
+ response.content_length = None
+
+ response.start(request)
+
+ while True:
+ data = yield from loop.run_in_executor(None, f.read, 16)
+ if len(data) == 0:
+ yield from asyncio.sleep(0.1)
+ else:
+ response.write(data)
+ except FileNotFoundError:
+ raise aiohttp.web.HTTPNotFound()
+ except OSError as e:
+ raise aiohttp.web.HTTPConflict(text=str(e))
+
diff --git a/gns3server/main.py b/gns3server/main.py
index ee32440f..e88e4553 100644
--- a/gns3server/main.py
+++ b/gns3server/main.py
@@ -21,6 +21,8 @@ import datetime
import sys
import locale
import argparse
+import asyncio
+import concurrent
from gns3server.server import Server
from gns3server.web.logger import init_logger
@@ -175,6 +177,9 @@ def main():
Project.clean_project_directory()
+ executor = concurrent.futures.ThreadPoolExecutor(max_workers=100) # We allow 100 parallel executors
+ loop = asyncio.get_event_loop().set_default_executor(executor)
+
CrashReport.instance()
host = server_config["host"]
port = int(server_config["port"])
diff --git a/gns3server/schemas/file.py b/gns3server/schemas/file.py
new file mode 100644
index 00000000..38ce7a10
--- /dev/null
+++ b/gns3server/schemas/file.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 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 .
+
+
+FILE_STREAM_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation retrieval of a file stream",
+ "type": "object",
+ "properties": {
+ "location": {
+ "description": "File path",
+ "type": ["string"],
+ "minLength": 1
+ }
+ },
+ "additionalProperties": False,
+ "required": ["location"]
+}
diff --git a/tests/handlers/api/base.py b/tests/handlers/api/base.py
index 6a4fd02e..4b677d17 100644
--- a/tests/handlers/api/base.py
+++ b/tests/handlers/api/base.py
@@ -44,7 +44,7 @@ class Query:
def delete(self, path, **kwargs):
return self._fetch("DELETE", path, **kwargs)
- def _get_url(self, path, version):
+ def get_url(self, path, version):
if version is None:
return "http://{}:{}{}".format(self._host, self._port, path)
return "http://{}:{}/v{}{}".format(self._host, self._port, version, path)
@@ -62,7 +62,7 @@ class Query:
@asyncio.coroutine
def go(future):
- response = yield from aiohttp.request(method, self._get_url(path, api_version), data=body)
+ response = yield from aiohttp.request(method, self.get_url(path, api_version), data=body)
future.set_result(response)
future = asyncio.Future()
asyncio.async(go(future))
diff --git a/tests/handlers/api/test_file.py b/tests/handlers/api/test_file.py
new file mode 100644
index 00000000..0b110b2e
--- /dev/null
+++ b/tests/handlers/api/test_file.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 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 .
+
+"""
+This test suite check /files endpoint
+"""
+
+import json
+import asyncio
+import aiohttp
+
+from gns3server.version import __version__
+
+
+def test_stream(server, tmpdir, loop):
+ with open(str(tmpdir / "test"), 'w+') as f:
+ f.write("hello")
+
+ def go(future):
+ query = json.dumps({"location": str(tmpdir / "test")})
+ headers = {'content-type': 'application/json'}
+ response = yield from aiohttp.request("GET", server.get_url("/files/stream", 1), data=query, headers=headers)
+ response.body = yield from response.content.read(5)
+ with open(str(tmpdir / "test"), 'a') as f:
+ f.write("world")
+ response.body += yield from response.content.read(5)
+ response.close()
+ future.set_result(response)
+
+ future = asyncio.Future()
+ asyncio.async(go(future))
+ response = loop.run_until_complete(future)
+ assert response.status == 200
+ assert response.body == b'helloworld'
+
+
+def test_stream_file_not_found(server, tmpdir, loop):
+ def go(future):
+ query = json.dumps({"location": str(tmpdir / "test")})
+ headers = {'content-type': 'application/json'}
+ response = yield from aiohttp.request("GET", server.get_url("/files/stream", 1), data=query, headers=headers)
+ response.close()
+ future.set_result(response)
+
+ future = asyncio.Future()
+ asyncio.async(go(future))
+ response = loop.run_until_complete(future)
+ assert response.status == 404