Export portable projects

* Licence for zipstream

Ref #476
This commit is contained in:
Julien Duponchelle
2016-03-30 15:43:49 +02:00
parent 365808eff2
commit 422ed0a5e2
8 changed files with 178 additions and 425 deletions

13
COPYING
View File

@@ -481,10 +481,19 @@ License notice for Python
-------------------------
https://www.python.org/download/releases/3.4.2/license/
License for busybox
--------------------
License notice for BusyBox
---------------------------
BusyBox is distributed under version 2 of the General Public License
https://busybox.net/license.html
Source code is available here:
https://github.com/GNS3/busybox
Licence notice for zipstream
-----------------------------
zipstream is distributed under version 3 of the General Public License
https://github.com/allanlei/python-zipstream/blob/master/LICENSE
Source code is available here:
https://pypi.python.org/pypi/zipstream

View File

@@ -50,6 +50,7 @@ from .utils.progress_dialog import ProgressDialog
from .utils.process_files_worker import ProcessFilesWorker
from .utils.wait_for_connection_worker import WaitForConnectionWorker
from .utils.wait_for_vm_worker import WaitForVMWorker
from .utils.export_project_worker import ExportProjectWorker
from .utils.message_box import MessageBox
from .ports.port import Port
from .items.node_item import NodeItem
@@ -58,7 +59,6 @@ from .items.shape_item import ShapeItem
from .items.image_item import ImageItem
from .items.note_item import NoteItem
from .topology import Topology
from .utils.download_project import DownloadProjectWorker
from .project import Project
from .http_client import HTTPClient
from .progress import Progress
@@ -205,7 +205,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiOpenApplianceAction.triggered.connect(self.openApplianceActionSlot)
self.uiSaveProjectAction.triggered.connect(self._saveProjectActionSlot)
self.uiSaveProjectAsAction.triggered.connect(self._saveProjectAsActionSlot)
self.uiDownloadRemoteProject.triggered.connect(self._downloadRemoteProjectActionSlot)
self.uiExportProjectAction.triggered.connect(self._exportProjectActionSlot)
self.uiImportExportConfigsAction.triggered.connect(self._importExportConfigsActionSlot)
self.uiScreenshotAction.triggered.connect(self._screenshotActionSlot)
self.uiSnapshotAction.triggered.connect(self._snapshotActionSlot)
@@ -1558,19 +1558,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
QtCore.QTimer.singleShot(counter, callback)
def _downloadRemoteProjectActionSlot(self):
if self._project.temporary():
QtWidgets.QMessageBox.warning(self, "Download project", "You cannot download a temporary project")
return
def _exportProjectActionSlot(self):
running_nodes = self._running_nodes()
if running_nodes:
nodes = "\n".join(running_nodes)
MessageBox(self, "Download project", "Please stop the following nodes before downloading the project", nodes)
MessageBox(self, "Download project", "Please stop the following nodes before exporting the project", nodes)
return
download_worker = DownloadProjectWorker(self, self._project, Servers.instance())
progress_dialog = ProgressDialog(download_worker, "Download remote project", "Downloading project files...", "Cancel", parent=self)
export_worker = ExportProjectWorker(self, self._project)
progress_dialog = ProgressDialog(export_worker, "Export project", "Exporting project files...", "Cancel", parent=self)
progress_dialog.show()
progress_dialog.exec_()

View File

@@ -105,6 +105,12 @@ class Project(QtCore.QObject):
self._temporary = temporary
def servers(self):
"""
:returns: List of server where GNS3 is running
"""
return self._created_servers
def id(self):
"""
Get project identifier

View File

@@ -91,12 +91,8 @@ background-none;
<addaction name="uiOpenProjectAction"/>
<addaction name="uiSaveProjectAction"/>
<addaction name="uiSaveProjectAsAction"/>
<addaction name="uiExportProjectAction"/>
<addaction name="uiImportProjectAction"/>
<addaction name="separator"/>
<addaction name="uiMoveLocalProjectToCloudAction"/>
<addaction name="uiMoveCloudProjectToLocalAction"/>
<addaction name="uiDownloadRemoteProject"/>
<addaction name="uiExportProjectAction"/>
<addaction name="separator"/>
<addaction name="uiOpenApplianceAction"/>
<addaction name="uiImportExportConfigsAction"/>
@@ -1154,38 +1150,6 @@ background-none;
<string>Fit in view</string>
</property>
</action>
<action name="uiExportProjectAction">
<property name="text">
<string>Backup project to cloud</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
</action>
<action name="uiImportProjectAction">
<property name="text">
<string>Restore backup from cloud</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
</action>
<action name="uiMoveLocalProjectToCloudAction">
<property name="text">
<string>Move local project to cloud</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
</action>
<action name="uiMoveCloudProjectToLocalAction">
<property name="text">
<string>Move cloud project to local</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
</action>
<action name="uiDarkStyleAction">
<property name="text">
<string>Dark Style</string>
@@ -1257,6 +1221,20 @@ background-none;
<string>GNS3 &amp;Doctor</string>
</property>
</action>
<action name="actionExport_project">
<property name="text">
<string>Export project</string>
</property>
</action>
<action name="uiExportProjectAction">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/save.svg</normaloff>:/icons/save.svg</iconset>
</property>
<property name="text">
<string>Export project</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@@ -208,22 +208,22 @@ class Ui_MainWindow(object):
self.uiOnlineHelpAction.setObjectName("uiOnlineHelpAction")
self.uiScreenshotAction = QtWidgets.QAction(MainWindow)
icon4 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap(":/icons/camera-photo.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon4.addPixmap(QtGui.QPixmap(":/icons/camera-photo-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon4.addPixmap(QtGui.QPixmap(":/icons/camera-photo.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiScreenshotAction.setIcon(icon4)
self.uiScreenshotAction.setObjectName("uiScreenshotAction")
self.uiStartAllAction = QtWidgets.QAction(MainWindow)
self.uiStartAllAction.setEnabled(True)
icon5 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap(":/icons/start.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon5.addPixmap(QtGui.QPixmap(":/icons/start-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon5.addPixmap(QtGui.QPixmap(":/icons/start.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiStartAllAction.setIcon(icon5)
self.uiStartAllAction.setObjectName("uiStartAllAction")
self.uiStopAllAction = QtWidgets.QAction(MainWindow)
self.uiStopAllAction.setEnabled(True)
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap(":/icons/stop.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon6.addPixmap(QtGui.QPixmap(":/icons/stop-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon6.addPixmap(QtGui.QPixmap(":/icons/stop.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiStopAllAction.setIcon(icon6)
self.uiStopAllAction.setObjectName("uiStopAllAction")
self.uiConsoleAllAction = QtWidgets.QAction(MainWindow)
@@ -237,14 +237,14 @@ class Ui_MainWindow(object):
self.uiAboutQtAction.setObjectName("uiAboutQtAction")
self.uiZoomInAction = QtWidgets.QAction(MainWindow)
icon8 = QtGui.QIcon()
icon8.addPixmap(QtGui.QPixmap(":/icons/zoom-in.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon8.addPixmap(QtGui.QPixmap(":/icons/zoom-in-hover.png"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon8.addPixmap(QtGui.QPixmap(":/icons/zoom-in.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiZoomInAction.setIcon(icon8)
self.uiZoomInAction.setObjectName("uiZoomInAction")
self.uiZoomOutAction = QtWidgets.QAction(MainWindow)
icon9 = QtGui.QIcon()
icon9.addPixmap(QtGui.QPixmap(":/icons/zoom-out.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon9.addPixmap(QtGui.QPixmap(":/icons/zoom-out-hover.png"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon9.addPixmap(QtGui.QPixmap(":/icons/zoom-out.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiZoomOutAction.setIcon(icon9)
self.uiZoomOutAction.setObjectName("uiZoomOutAction")
self.uiZoomResetAction = QtWidgets.QAction(MainWindow)
@@ -261,8 +261,8 @@ class Ui_MainWindow(object):
self.uiPreferencesAction.setObjectName("uiPreferencesAction")
self.uiSuspendAllAction = QtWidgets.QAction(MainWindow)
icon11 = QtGui.QIcon()
icon11.addPixmap(QtGui.QPixmap(":/icons/pause.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon11.addPixmap(QtGui.QPixmap(":/icons/pause-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon11.addPixmap(QtGui.QPixmap(":/icons/pause.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiSuspendAllAction.setIcon(icon11)
self.uiSuspendAllAction.setObjectName("uiSuspendAllAction")
self.uiAddNoteAction = QtWidgets.QAction(MainWindow)
@@ -290,15 +290,15 @@ class Ui_MainWindow(object):
self.uiDrawRectangleAction = QtWidgets.QAction(MainWindow)
self.uiDrawRectangleAction.setCheckable(True)
icon16 = QtGui.QIcon()
icon16.addPixmap(QtGui.QPixmap(":/icons/rectangle.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon16.addPixmap(QtGui.QPixmap(":/icons/rectangle-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon16.addPixmap(QtGui.QPixmap(":/icons/rectangle.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiDrawRectangleAction.setIcon(icon16)
self.uiDrawRectangleAction.setObjectName("uiDrawRectangleAction")
self.uiDrawEllipseAction = QtWidgets.QAction(MainWindow)
self.uiDrawEllipseAction.setCheckable(True)
icon17 = QtGui.QIcon()
icon17.addPixmap(QtGui.QPixmap(":/icons/ellipse.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon17.addPixmap(QtGui.QPixmap(":/icons/ellipse-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon17.addPixmap(QtGui.QPixmap(":/icons/ellipse.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiDrawEllipseAction.setIcon(icon17)
self.uiDrawEllipseAction.setObjectName("uiDrawEllipseAction")
self.uiShowPortNamesAction = QtWidgets.QAction(MainWindow)
@@ -344,41 +344,41 @@ class Ui_MainWindow(object):
self.uiDefaultStyleAction.setObjectName("uiDefaultStyleAction")
self.uiBrowseRoutersAction = QtWidgets.QAction(MainWindow)
icon23 = QtGui.QIcon()
icon23.addPixmap(QtGui.QPixmap(":/icons/router.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon23.addPixmap(QtGui.QPixmap(":/icons/router-hover.png"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon23.addPixmap(QtGui.QPixmap(":/icons/router.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseRoutersAction.setIcon(icon23)
self.uiBrowseRoutersAction.setObjectName("uiBrowseRoutersAction")
self.uiBrowseSwitchesAction = QtWidgets.QAction(MainWindow)
icon24 = QtGui.QIcon()
icon24.addPixmap(QtGui.QPixmap(":/icons/switch.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon24.addPixmap(QtGui.QPixmap(":/icons/switch-hover.png"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon24.addPixmap(QtGui.QPixmap(":/icons/switch.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseSwitchesAction.setIcon(icon24)
self.uiBrowseSwitchesAction.setObjectName("uiBrowseSwitchesAction")
self.uiBrowseEndDevicesAction = QtWidgets.QAction(MainWindow)
icon25 = QtGui.QIcon()
icon25.addPixmap(QtGui.QPixmap(":/icons/PC.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon25.addPixmap(QtGui.QPixmap(":/icons/PC-hover.png"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon25.addPixmap(QtGui.QPixmap(":/icons/PC.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseEndDevicesAction.setIcon(icon25)
self.uiBrowseEndDevicesAction.setObjectName("uiBrowseEndDevicesAction")
self.uiBrowseSecurityDevicesAction = QtWidgets.QAction(MainWindow)
icon26 = QtGui.QIcon()
icon26.addPixmap(QtGui.QPixmap(":/icons/firewall.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon26.addPixmap(QtGui.QPixmap(":/icons/firewall-hover.png"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon26.addPixmap(QtGui.QPixmap(":/icons/firewall.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseSecurityDevicesAction.setIcon(icon26)
self.uiBrowseSecurityDevicesAction.setObjectName("uiBrowseSecurityDevicesAction")
self.uiBrowseAllDevicesAction = QtWidgets.QAction(MainWindow)
icon27 = QtGui.QIcon()
icon27.addPixmap(QtGui.QPixmap(":/icons/browse-all-icons.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon27.addPixmap(QtGui.QPixmap(":/icons/browse-all-icons-hover.png"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon27.addPixmap(QtGui.QPixmap(":/icons/browse-all-icons.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseAllDevicesAction.setIcon(icon27)
self.uiBrowseAllDevicesAction.setObjectName("uiBrowseAllDevicesAction")
self.uiAddLinkAction = QtWidgets.QAction(MainWindow)
self.uiAddLinkAction.setCheckable(True)
icon28 = QtGui.QIcon()
icon28.addPixmap(QtGui.QPixmap(":/icons/connection-new.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon28.addPixmap(QtGui.QPixmap(":/icons/connection-new-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon28.addPixmap(QtGui.QPixmap(":/icons/cancel-connection.svg"), QtGui.QIcon.Active, QtGui.QIcon.On)
icon28.addPixmap(QtGui.QPixmap(":/icons/connection-new-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon28.addPixmap(QtGui.QPixmap(":/icons/cancel-connection.svg"), QtGui.QIcon.Normal, QtGui.QIcon.On)
icon28.addPixmap(QtGui.QPixmap(":/icons/connection-new.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiAddLinkAction.setIcon(icon28)
self.uiAddLinkAction.setObjectName("uiAddLinkAction")
self.uiGettingStartedAction = QtWidgets.QAction(MainWindow)
@@ -387,18 +387,6 @@ class Ui_MainWindow(object):
self.uiLabInstructionsAction.setObjectName("uiLabInstructionsAction")
self.uiFitInViewAction = QtWidgets.QAction(MainWindow)
self.uiFitInViewAction.setObjectName("uiFitInViewAction")
self.uiExportProjectAction = QtWidgets.QAction(MainWindow)
self.uiExportProjectAction.setVisible(False)
self.uiExportProjectAction.setObjectName("uiExportProjectAction")
self.uiImportProjectAction = QtWidgets.QAction(MainWindow)
self.uiImportProjectAction.setVisible(False)
self.uiImportProjectAction.setObjectName("uiImportProjectAction")
self.uiMoveLocalProjectToCloudAction = QtWidgets.QAction(MainWindow)
self.uiMoveLocalProjectToCloudAction.setVisible(False)
self.uiMoveLocalProjectToCloudAction.setObjectName("uiMoveLocalProjectToCloudAction")
self.uiMoveCloudProjectToLocalAction = QtWidgets.QAction(MainWindow)
self.uiMoveCloudProjectToLocalAction.setVisible(False)
self.uiMoveCloudProjectToLocalAction.setObjectName("uiMoveCloudProjectToLocalAction")
self.uiDarkStyleAction = QtWidgets.QAction(MainWindow)
self.uiDarkStyleAction.setObjectName("uiDarkStyleAction")
self.uiActionFullscreen = QtWidgets.QAction(MainWindow)
@@ -427,6 +415,11 @@ class Ui_MainWindow(object):
self.uiExportDebugInformationAction.setObjectName("uiExportDebugInformationAction")
self.uiDoctorAction = QtWidgets.QAction(MainWindow)
self.uiDoctorAction.setObjectName("uiDoctorAction")
self.actionExport_project = QtWidgets.QAction(MainWindow)
self.actionExport_project.setObjectName("actionExport_project")
self.uiExportProjectAction = QtWidgets.QAction(MainWindow)
self.uiExportProjectAction.setIcon(icon2)
self.uiExportProjectAction.setObjectName("uiExportProjectAction")
self.uiEditMenu.addAction(self.uiSelectAllAction)
self.uiEditMenu.addAction(self.uiSelectNoneAction)
self.uiEditMenu.addSeparator()
@@ -435,12 +428,8 @@ class Ui_MainWindow(object):
self.uiFileMenu.addAction(self.uiOpenProjectAction)
self.uiFileMenu.addAction(self.uiSaveProjectAction)
self.uiFileMenu.addAction(self.uiSaveProjectAsAction)
self.uiFileMenu.addAction(self.uiExportProjectAction)
self.uiFileMenu.addAction(self.uiImportProjectAction)
self.uiFileMenu.addSeparator()
self.uiFileMenu.addAction(self.uiMoveLocalProjectToCloudAction)
self.uiFileMenu.addAction(self.uiMoveCloudProjectToLocalAction)
self.uiFileMenu.addAction(self.uiDownloadRemoteProject)
self.uiFileMenu.addAction(self.uiExportProjectAction)
self.uiFileMenu.addSeparator()
self.uiFileMenu.addAction(self.uiOpenApplianceAction)
self.uiFileMenu.addAction(self.uiImportExportConfigsAction)
@@ -672,10 +661,6 @@ class Ui_MainWindow(object):
self.uiGettingStartedAction.setToolTip(_translate("MainWindow", "Show GNS3 news"))
self.uiLabInstructionsAction.setText(_translate("MainWindow", "&Lab instructions"))
self.uiFitInViewAction.setText(_translate("MainWindow", "Fit in view"))
self.uiExportProjectAction.setText(_translate("MainWindow", "Backup project to cloud"))
self.uiImportProjectAction.setText(_translate("MainWindow", "Restore backup from cloud"))
self.uiMoveLocalProjectToCloudAction.setText(_translate("MainWindow", "Move local project to cloud"))
self.uiMoveCloudProjectToLocalAction.setText(_translate("MainWindow", "Move cloud project to local"))
self.uiDarkStyleAction.setText(_translate("MainWindow", "Dark Style"))
self.uiActionFullscreen.setText(_translate("MainWindow", "Fullscreen"))
self.uiActionFullscreen.setShortcut(_translate("MainWindow", "Ctrl+F"))
@@ -688,6 +673,8 @@ class Ui_MainWindow(object):
self.uiExportDebugInformationAction.setText(_translate("MainWindow", "Export debug information"))
self.uiExportDebugInformationAction.setToolTip(_translate("MainWindow", "&Export debug information"))
self.uiDoctorAction.setText(_translate("MainWindow", "GNS3 &Doctor"))
self.actionExport_project.setText(_translate("MainWindow", "Export project"))
self.uiExportProjectAction.setText(_translate("MainWindow", "Export project"))
from ..console_view import ConsoleView
from ..graphics_view import GraphicsView

View File

@@ -1,137 +0,0 @@
# -*- 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 <http://www.gnu.org/licenses/>.
import os
from ..qt import QtCore
from ..utils import md5_hash_file
import logging
log = logging.getLogger(__name__)
class DownloadProjectWorker(QtCore.QObject):
"""
Downloads project from cloud storage
"""
# signals to update the progress dialog.
error = QtCore.pyqtSignal(str, bool)
finished = QtCore.pyqtSignal()
updated = QtCore.pyqtSignal(int)
file_downloaded = QtCore.pyqtSignal()
file_list_received = QtCore.pyqtSignal()
def __init__(self, parent, project, servers):
self._is_running = False
self._project = project
self._servers = servers
self._files_to_download = []
self._get_file_lists = 0 # Counter to know how many file list remain to download
self._total_files_to_download = 0
super().__init__(parent)
def run(self):
self.updated.emit(0)
self._is_running = True
try:
if self._servers.vmServer():
self._project.get(self._servers.vmServer(), "/files", self._fileListReceived)
self._get_file_lists += 1
for server in self._servers.remoteServers().values():
self._project.get(server, "/files", self._fileListReceived)
self._get_file_lists += 1
except Exception as e:
self.error.emit("Error importing project: {}".format(e), True)
if self._get_file_lists == 0:
self._is_running = False
def _fileListReceived(self, result, error=False, server=None, **kwargs):
self._get_file_lists -= 1
if error:
msg = "Error while downloading project {}".format(result["message"])
log.error(msg)
self.error.emit(msg, True)
return
for file in result:
file["server"] = server
self._files_to_download.append(file)
self._total_files_to_download += 1
if self._get_file_lists <= 0:
self._downloadNextFile()
else:
self._is_running = False
self.finished.emit()
def _downloadNextFile(self):
if not self._is_running:
return
try:
file_to_download = self._files_to_download.pop()
except IndexError:
self.finished.emit()
return
file_path = os.path.join(self._project.filesDir(), file_to_download["path"])
self.updated.emit(round(100.0 / self._total_files_to_download * (self._total_files_to_download - (len(self._files_to_download) + 1))))
try:
if os.path.dirname(file_path) is not None:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
if os.path.exists(file_path):
if md5_hash_file(file_path) == file_to_download["md5sum"]:
self._downloadNextFile()
return
f = open(file_path, "wb+")
except OSError as e:
self.error.emit("Could not write file {}: {}".format(file_path, e), False)
return
self._project.get(file_to_download["server"], "/files/{}".format(file_to_download["path"]), self._downloadFileReceived, context={"fd": f, "file_path": file_path}, downloadProgressCallback=self._downloadFileProgress)
def _downloadFileReceived(self, content, error=False, server=None, context={}):
"""
Called when download finish
"""
context["fd"].close()
self._downloadNextFile()
def _downloadFileProgress(self, content, server=None, context={}):
"""
Called for each part of the file
"""
try:
context["fd"].write(content)
except OSError as e:
self.error.emit("Could not write file {}: {}".format(context["file_path"], e), False)
def cancel(self):
"""
Cancel this worker.
"""
if not self:
return
self._is_running = False

View File

@@ -0,0 +1,117 @@
#!/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 <http://www.gnu.org/licenses/>.
import os
import zipfile
import shutil
from ..qt import QtCore, QtWidgets
from ..servers import Servers
class ExportProjectWorker(QtCore.QObject):
"""
Export the current topology to a portable format
"""
# signals to update the progress dialog.
error = QtCore.pyqtSignal(str, bool)
finished = QtCore.pyqtSignal()
updated = QtCore.pyqtSignal(int)
def __init__(self, parent, project):
super().__init__(parent)
self._project = project
def run(self):
if self._project.temporary():
self.error.emit("You cannot export a temporary project", True)
self.finished.emit()
return
for server in self._project.servers():
if not server.isLocal() and not server.isGNS3VM():
self.error.emit("Project from remote server can not be exported. Only project for local and GNS3 VM are supported.", True)
self.finished.emit()
return
self._path, _ = QtWidgets.QFileDialog.getSaveFileName(self.parent(), "Export project", None, "GNS3 Topology (*.gns3z)", "GNS3 Topology (*.gns3z)")
if self._path is None:
self.finished.emit()
return
try:
open(self._path, 'wb+').close()
except OSError as e:
self.error.emit("Can't write the topology {}: {}".format(self._path, str(e)), True)
self.finished.emit()
return
vm_server = None
for server in self._project.servers():
if server.isGNS3VM():
vm_server = server
if vm_server:
self._project.get(vm_server, "/export", self._exportVmReceived, downloadProgressCallback=self._downloadFileProgress)
else:
self._project.get(Servers.instance().localServer(), "/export", self._exportLocalReceived, downloadProgressCallback=self._downloadFileProgress)
def _exportVmReceived(self, content, error=False, server=None, context={}, **kwargs):
if error:
self.error.emit("Can't export the project from the VM", True)
self.finished.emit()
return
vm_path = os.path.join(self._project.filesDir(), "servers", "vm")
if os.path.exists(vm_path):
shutil.rmtree(vm_path)
os.makedirs(vm_path, exist_ok=True)
with zipfile.ZipFile(self._path) as myzip:
myzip.extractall(vm_path)
# We reset the content of the file
try:
open(self._path, 'wb+').close()
except OSError as e:
self.error.emit("Can't write the topology {}: {}".format(self._path, str(e)), True)
self.finished.emit()
return
self._project.get(Servers.instance().localServer(), "/export", self._exportLocalReceived, downloadProgressCallback=self._downloadFileProgress)
def _exportLocalReceived(self, content, error=False, server=None, context={}, **kwargs):
if error:
self.error.emit("Can't export the project from the local server", True)
self.finished.emit()
return
self.finished.emit()
def _downloadFileProgress(self, content, server=None, context={}, **kwargs):
"""
Called for each part of the file
"""
try:
with open(self._path, 'ab') as f:
f.write(content)
except OSError as e:
self.error.emit("Can't write the topology {}: {}".format(self._path, str(e)), True)
self.finished.emit()
return
def cancel(self):
pass

View File

@@ -1,203 +0,0 @@
# -*- 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 <http://www.gnu.org/licenses/>.
import pytest
import os
from unittest.mock import MagicMock
from gns3.project import Project
from gns3.servers import Servers
from gns3.utils.download_project import DownloadProjectWorker
@pytest.fixture(scope="function")
def servers():
Servers._instance = None
servers = Servers.instance()
servers._remote_servers = {} #  Erase server from settings
return servers
@pytest.fixture(scope="function")
def project(tmpdir):
p = MagicMock()
p.filesDir.return_value = str(tmpdir)
return p
@pytest.fixture(scope="function")
def download_project(project, servers):
return DownloadProjectWorker(None, project, servers)
def test_download_project_without_server(download_project):
download_project.run()
def test_download_project_with_one_server(download_project, servers, tmpdir, project):
one = servers.getRemoteServer("http", "127.0.0.1", 3080, None)
download_project.run()
project.get.assert_called_with(one, "/files", download_project._fileListReceived)
assert download_project._get_file_lists == 1
def test_fileListReceived(download_project, servers):
download_project._get_file_lists = 3
one = servers.getRemoteServer("http", "127.0.0.1", 3080, None)
two = servers.getRemoteServer("http", "127.0.0.1", 8001, None)
download_project._fileListReceived([{"path": "a", "md5sum": "d8e8fca2dc0f896fd7cb4cb0031ba249"}], server=one)
download_project._fileListReceived([{"path": "b", "md5sum": "126a8a51b9d1bbd07fddc65819a542c3"}], server=two)
assert download_project._get_file_lists == 1
assert download_project._files_to_download == [
{"path": "a", "md5sum": "d8e8fca2dc0f896fd7cb4cb0031ba249", "server": one},
{"path": "b", "md5sum": "126a8a51b9d1bbd07fddc65819a542c3", "server": two}
]
assert download_project._total_files_to_download == 2
def test_downloadNextFile_empty_list(download_project):
mark_finished = MagicMock()
download_project.finished.connect(mark_finished)
download_project._is_running = True
download_project._downloadNextFile()
download_project._is_running = False
assert mark_finished.called
def test_downloadNextFile_non_empty_list(download_project, servers, project):
one = servers.getRemoteServer("http", "127.0.0.1", 3080, None)
download_project._files_to_download = [
{"path": "a", "md5sum": "d8e8fca2dc0f896fd7cb4cb0031ba249", "server": one}
]
download_project._total_files_to_download = 1
download_project._is_running = True
mark_finished = MagicMock()
download_project.finished.connect(mark_finished)
download_project._downloadNextFile()
assert not mark_finished.called
assert os.path.exists(os.path.join(project.filesDir(), "a"))
assert project.get.called
args, kwargs = project.get.call_args
assert args[0] == one
assert args[1] == "/files/a"
assert args[2] == download_project._downloadFileReceived
assert kwargs["context"] is not None
assert kwargs["downloadProgressCallback"] == download_project._downloadFileProgress
def test_downloadNextFile_file_exist_but_different(download_project, servers, project):
one = servers.getRemoteServer("http", "127.0.0.1", 3080, None)
download_project._files_to_download = [
{"path": "a", "md5sum": "d8e8fca2dc0f896fd7cb4cb0031ba249", "server": one}
]
download_project._total_files_to_download = 1
download_project._is_running = True
with open(os.path.join(project.filesDir(), "a"), "w+") as f:
f.write("hello")
download_project._downloadNextFile()
assert project.get.called
args, kwargs = project.get.call_args
assert args[0] == one
assert args[1] == "/files/a"
assert args[2] == download_project._downloadFileReceived
assert kwargs["context"] is not None
assert kwargs["downloadProgressCallback"] == download_project._downloadFileProgress
def test_downloadNextFile_file_exist_and_the_same(download_project, servers, project):
one = servers.getRemoteServer("http", "127.0.0.1", 3080, None)
download_project._files_to_download = [
{"path": "a", "md5sum": "5d41402abc4b2a76b9719d911017c592", "server": one}
]
download_project._total_files_to_download = 1
with open(os.path.join(project.filesDir(), "a"), "w+") as f:
f.write("hello")
download_project._downloadNextFile()
assert not project.get.called
def test_downloadNextFile_subdirectory(download_project, servers, project):
one = servers.getRemoteServer("http", "127.0.0.1", 3080, None)
download_project._files_to_download = [
{"path": "a/b/c", "md5sum": "d8e8fca2dc0f896fd7cb4cb0031ba249", "server": one}
]
download_project._total_files_to_download = 4
download_project._is_running = True
mark_finished = MagicMock()
download_project.finished.connect(mark_finished)
progress = MagicMock()
download_project.updated.connect(progress)
download_project._downloadNextFile()
assert not mark_finished.called
progress.assert_called_once_with(75)
assert os.path.exists(os.path.join(project.filesDir(), "a", "b", "c"))
assert project.get.called
args, kwargs = project.get.call_args
assert args[0] == one
assert args[1] == "/files/a/b/c"
assert args[2] == download_project._downloadFileReceived
assert kwargs["context"] is not None
assert kwargs["downloadProgressCallback"] == download_project._downloadFileProgress
def test_downloadFileReceived(download_project, tmpdir):
f = open(str(tmpdir / "a"), "w+")
download_project._downloadFileReceived(None, context={"fd": f})
assert f.closed
# TODO: Test progress
# TODO: MD5 sum
def test_downloadFileReceived(download_project, tmpdir):
f = open(str(tmpdir / "a"), "w+")
download_project._downloadFileProgress("hello", context={"fd": f})
f.close()
with open(str(tmpdir / "a")) as f:
assert f.read() == "hello"