* This implements folder sharing for the built-in Spice client, tested working with Win10 guest. * The basic idea is taken from virt-viewer. Signed-off-by: Jitao Lu <dianlujitao@xxxxxxxxx> --- ui/spicewebdav.ui | 129 +++++++++++++++++++++++++++++ ui/vmwindow.ui | 17 +++- virtManager/details/console.py | 8 ++ virtManager/details/spicewebdav.py | 60 ++++++++++++++ virtManager/details/viewers.py | 68 +++++++++++++++ virtManager/vmwindow.py | 8 ++ 6 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 ui/spicewebdav.ui create mode 100644 virtManager/details/spicewebdav.py diff --git a/ui/spicewebdav.ui b/ui/spicewebdav.ui new file mode 100644 index 00000000..a2a372b1 --- /dev/null +++ b/ui/spicewebdav.ui @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.36.0 --> +<interface> + <requires lib="gtk+" version="3.22"/> + <object class="GtkWindow" id="vmm-spice-webdav-folder-share"> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="title" translatable="yes">Share folder</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="type_hint">dialog</property> + <signal name="delete-event" handler="on_vmm_spice_webdav_folder_share_delete_event" swapped="no"/> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">6</property> + <property name="column_spacing">6</property> + <child> + <object class="GtkCheckButton" id="share-folder-cb"> + <property name="label" translatable="yes">Share folder</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="share-folder-ro-cb"> + <property name="label" translatable="yes">Read-only</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkFileChooserButton" id="shared-folder-fc"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="action">select-folder</property> + <property name="title" translatable="yes"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>SPICE WebDAV folder sharing</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="folder-share-close"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_vmm_spice_webdav_folder_share_close_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="titlebar"> + <placeholder/> + </child> + </object> +</interface> diff --git a/ui/vmwindow.ui b/ui/vmwindow.ui index 6c78890a..a9330c9a 100644 --- a/ui/vmwindow.ui +++ b/ui/vmwindow.ui @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.22.1 --> +<!-- Generated with glade 3.36.0 --> <interface> <requires lib="gtk+" version="3.22"/> <object class="GtkAccelGroup" id="accelgroup1"/> @@ -18,9 +18,6 @@ </accel-groups> <signal name="configure-event" handler="on_vmm_details_configure_event" swapped="no"/> <signal name="delete-event" handler="on_vmm_details_delete_event" swapped="no"/> - <child type="titlebar"> - <placeholder/> - </child> <child> <object class="GtkBox" id="vbox2"> <property name="visible">True</property> @@ -115,6 +112,15 @@ <signal name="activate" handler="on_details_menu_usb_redirection" swapped="no"/> </object> </child> + <child> + <object class="GtkMenuItem" id="details-menu-spice-webdav-folder-sharing"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_SPICE WebDAV folder sharing</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_details_menu_spice_webdav_folder_sharing" swapped="no"/> + </object> + </child> </object> </child> </object> @@ -821,5 +827,8 @@ </child> </object> </child> + <child type="titlebar"> + <placeholder/> + </child> </object> </interface> diff --git a/virtManager/details/console.py b/virtManager/details/console.py index 062a019c..613cb63a 100644 --- a/virtManager/details/console.py +++ b/virtManager/details/console.py @@ -667,6 +667,9 @@ class vmmConsolePages(vmmGObjectUI): bool(is_viewer and self._viewer and self._viewer.console_has_usb_redirection() and self.vm.has_spicevmc_type_redirdev())) + self.widget("details-menu-spice-webdav-folder-sharing").set_sensitive( + bool(is_viewer and self._viewer and + self._viewer.console_has_spice_webdav_folder_sharing())) can_sendkey = (is_viewer and not paused) for c in self._keycombo_menu.get_children(): @@ -1013,6 +1016,11 @@ class vmmConsolePages(vmmGObjectUI): self._viewer.console_has_usb_redirection()) def details_viewer_get_usb_widget(self): return self._viewer.console_get_usb_widget() + def details_viewer_has_spice_webdav_folder_sharing(self): + return bool(self._viewer and + self._viewer.console_has_spice_webdav_folder_sharing()) + def details_viewer_get_spice_webdav_folder_sharing_dialog(self): + return self._viewer.console_get_spice_webdav_folder_sharing_dialog() def details_viewer_get_pixbuf(self): return self._viewer.console_get_pixbuf() diff --git a/virtManager/details/spicewebdav.py b/virtManager/details/spicewebdav.py new file mode 100644 index 00000000..1774675b --- /dev/null +++ b/virtManager/details/spicewebdav.py @@ -0,0 +1,60 @@ +# Copyright (C) 2019-2020 Jitao Lu <dianlujitao@xxxxxxxxx> +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +from ..baseclass import vmmGObjectUI + + +class vmmSpiceWebdav(vmmGObjectUI): + SHARE_FOLDER = "share-folder" + SHARE_FOLDER_RO = "share-folder-ro" + SHARED_FOLDER = "shared-folder" + + __gsignals__ = { + SHARE_FOLDER: (vmmGObjectUI.RUN_FIRST, None, [bool]), + SHARE_FOLDER_RO: (vmmGObjectUI.RUN_FIRST, None, [bool]), + SHARED_FOLDER: (vmmGObjectUI.RUN_FIRST, None, [str]), + } + + def __init__(self, enabled, ro, folder): + vmmGObjectUI.__init__(self, "spicewebdav.ui", + "vmm-spice-webdav-folder-share") + self._cleanup_on_app_close() + self.bind_escape_key_close() + + self.builder.connect_signals({ + "on_vmm_spice_webdav_folder_share_delete_event": + self.close, + "on_vmm_spice_webdav_folder_share_close_clicked": + self.close, + }) + + share_folder_cb = self.widget("share-folder-cb") + share_folder_cb.set_active(enabled) + share_folder_cb.connect( + "toggled", lambda src: self.emit(self.__class__.SHARE_FOLDER, + src.get_active())) + + share_folder_ro_cb = self.widget("share-folder-ro-cb") + share_folder_ro_cb.set_active(ro) + share_folder_ro_cb.connect( + "toggled", lambda src: self.emit(self.__class__.SHARE_FOLDER_RO, + src.get_active())) + + shared_folder_fc = self.widget("shared-folder-fc") + shared_folder_fc.set_current_folder(folder) + shared_folder_fc.connect( + "file-set", lambda src: self.emit(self.__class__.SHARED_FOLDER, + src.get_filename())) + + def show(self, parent): + self.topwin.set_transient_for(parent) + self.topwin.present() + + def _cleanup(self): + pass + + def close(self, ignore1=None, ignore2=None): + self.topwin.hide() + return 1 diff --git a/virtManager/details/viewers.py b/virtManager/details/viewers.py index 4ae0e668..edaec035 100644 --- a/virtManager/details/viewers.py +++ b/virtManager/details/viewers.py @@ -24,6 +24,7 @@ except (ValueError, ImportError): from virtinst import log from .sshtunnels import SSHTunnels +from .spicewebdav import vmmSpiceWebdav from ..baseclass import vmmGObject @@ -207,6 +208,11 @@ class Viewer(vmmGObject): def _has_agent(self): raise NotImplementedError() + def _has_spice_webdav_folder_sharing(self): + raise NotImplementedError() + def _get_spice_webdav_folder_sharing_dialog(self): + raise NotImplementedError() + #################################### # APIs accessed by vmmConsolePages # @@ -270,6 +276,11 @@ class Viewer(vmmGObject): def console_has_agent(self): return self._has_agent() + def console_has_spice_webdav_folder_sharing(self): + return self._has_spice_webdav_folder_sharing() + def console_get_spice_webdav_folder_sharing_dialog(self): + return self._get_spice_webdav_folder_sharing_dialog() + def console_remove_display_from_widget(self, widget): if self._display and self._display in widget.get_children(): widget.remove(self._display) @@ -437,6 +448,11 @@ class VNCViewer(Viewer): def _has_agent(self): return False + def _has_spice_webdav_folder_sharing(self): + return False + def _get_spice_webdav_folder_sharing_dialog(self): + return None + ####################### # Connection routines # @@ -492,6 +508,8 @@ class SpiceViewer(Viewer): self._main_channel_hids = [] self._display_channel = None self._usbdev_manager = None + self._webdav_channel = None + self._webdav_folder_sharing_dialog = None ################### @@ -631,6 +649,9 @@ class SpiceViewer(Viewer): SpiceClientGLib.RecordChannel] and not self._audio): self._audio = SpiceClientGLib.Audio.get(self._spice_session, None) + elif (type(channel) == SpiceClientGLib.WebdavChannel and + not self._webdav_channel): + self._webdav_channel = channel def _agent_connected_cb(self, src, val): self.emit("agent-connected") @@ -653,6 +674,9 @@ class SpiceViewer(Viewer): self._close_main_channel() self._usbdev_manager = None + self._webdav_channel = None + self._webdav_folder_sharing_dialog = None + def _is_open(self): return self._spice_session is not None @@ -763,3 +787,47 @@ class SpiceViewer(Viewer): if c.__class__ is SpiceClientGLib.UsbredirChannel: return True return False + + def _has_spice_webdav_folder_sharing(self): + return self._webdav_channel is not None + + def _get_spice_webdav_folder_sharing_dialog(self): + if not self._spice_session: + return + + try: + if not self._webdav_folder_sharing_dialog: + self._webdav_folder_sharing_dialog = vmmSpiceWebdav( + self._webdav_channel.get_property("port-opened"), + self._spice_session.get_property("share-dir-ro"), + self._spice_session.get_property("shared-dir")) + + self._webdav_folder_sharing_dialog.connect( + vmmSpiceWebdav.SHARE_FOLDER_RO, + self._on_webdav_folder_share_ro_toggled) + self._webdav_folder_sharing_dialog.connect( + vmmSpiceWebdav.SHARE_FOLDER, + self._on_webdav_folder_share_toggled) + self._webdav_folder_sharing_dialog.connect( + vmmSpiceWebdav.SHARED_FOLDER, + self._on_webdav_folder_share_changed) + except Exception as e: + log.error("Error launching 'Share folder' dialog: %s", e) + + return self._webdav_folder_sharing_dialog + + def _on_webdav_folder_share_ro_toggled(self, ignore, enabled): + self._spice_session.set_property("share-dir-ro", enabled) + + def _on_webdav_folder_share_toggled(self, ignore, enabled): + if enabled: + log.debug("Enabling folder sharing, shared dir: %s read-only: %r", + self._spice_session.get_property("shared-dir"), + self._spice_session.get_property("share-dir-ro")) + self._webdav_channel.connect() + else: + log.debug("Disabling folder sharing") + self._webdav_channel.disconnect(SpiceClientGLib.ChannelEvent.NONE) + + def _on_webdav_folder_share_changed(self, ignore, folder): + self._spice_session.set_property("shared-dir", folder) diff --git a/virtManager/vmwindow.py b/virtManager/vmwindow.py index 54e37399..9e8a2c85 100644 --- a/virtManager/vmwindow.py +++ b/virtManager/vmwindow.py @@ -111,6 +111,7 @@ class vmmVMWindow(vmmGObjectUI): "on_details_menu_virtual_manager_activate": self.control_vm_menu, "on_details_menu_screenshot_activate": self.control_vm_screenshot, "on_details_menu_usb_redirection": self.control_vm_usb_redirection, + "on_details_menu_spice_webdav_folder_sharing": self.control_vm_spice_webdav_folder_sharing, "on_details_menu_view_toolbar_activate": self.toggle_toolbar, "on_details_menu_view_manager_activate": self.view_manager, "on_details_menu_view_details_toggled": self.details_console_changed, @@ -472,6 +473,9 @@ class vmmVMWindow(vmmGObjectUI): self.vm.has_spicevmc_type_redirdev()) self.widget("details-menu-usb-redirection").set_sensitive(can_usb) + can_share_folder = self.console.details_viewer_has_spice_webdav_folder_sharing() + self.widget("details-menu-spice-webdav-folder-sharing").set_sensitive(can_share_folder) + def control_vm_run(self, src_ignore): if self._details.vmwindow_has_unapplied_changes(): return @@ -501,6 +505,10 @@ class vmmVMWindow(vmmGObjectUI): widget=spice_usbdev_widget, buttons=Gtk.ButtonsType.CLOSE) + def control_vm_spice_webdav_folder_sharing(self, ignore): + folder_sharing_dialog = self.console.details_viewer_get_spice_webdav_folder_sharing_dialog() + folder_sharing_dialog.show(self.topwin) + def _take_screenshot(self): image = self.console.details_viewer_get_pixbuf() -- 2.27.0