[virt-tools-list] [PATCH] virtManager: Folder sharing implementation for SPICE session

Jitao Lu dianlujitao at gmail.com
Sat Dec 28 12:12:34 UTC 2019


 * 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 at gmail.com>
---
Feel free to improve the patch!

 ui/foldershare.ui              | 129 +++++++++++++++++++++++++++++++++
 ui/vmwindow.ui                 |   9 +++
 virtManager/details/console.py |   8 ++
 virtManager/details/viewers.py |  63 ++++++++++++++++
 virtManager/foldershare.py     |  90 +++++++++++++++++++++++
 virtManager/vmwindow.py        |   8 ++
 6 files changed, 307 insertions(+)
 create mode 100644 ui/foldershare.ui
 create mode 100644 virtManager/foldershare.py

diff --git a/ui/foldershare.ui b/ui/foldershare.ui
new file mode 100644
index 00000000..8fd3c5e8
--- /dev/null
+++ b/ui/foldershare.ui
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+  <requires lib="gtk+" version="3.22"/>
+  <object class="GtkWindow" id="vmm-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_folder_share_delete_event" swapped="no"/>
+    <child type="titlebar">
+      <placeholder/>
+    </child>
+    <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>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_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>
+  </object>
+</interface>
diff --git a/ui/vmwindow.ui b/ui/vmwindow.ui
index 6c78890a..988293b4 100644
--- a/ui/vmwindow.ui
+++ b/ui/vmwindow.ui
@@ -115,6 +115,15 @@
                         <signal name="activate" handler="on_details_menu_usb_redirection" swapped="no"/>
                       </object>
                     </child>
+                    <child>
+                      <object class="GtkMenuItem" id="details-menu-folder-sharing">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">_Share folder</property>
+                        <property name="use_underline">True</property>
+                        <signal name="activate" handler="on_details_menu_folder_sharing" swapped="no"/>
+                      </object>
+                    </child>
                   </object>
                 </child>
               </object>
diff --git a/virtManager/details/console.py b/virtManager/details/console.py
index 193e79eb..7c0b7495 100644
--- a/virtManager/details/console.py
+++ b/virtManager/details/console.py
@@ -666,6 +666,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-folder-sharing").set_sensitive(
+            bool(is_viewer and self._viewer and
+            self._viewer.console_has_folder_sharing()))
 
         can_sendkey = (is_viewer and not paused)
         for c in self._keycombo_menu.get_children():
@@ -1012,6 +1015,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_folder_sharing(self):
+        return bool(self._viewer and
+            self._viewer.console_has_folder_sharing())
+    def details_viewer_get_folder_share_dialog(self):
+        return self._viewer.console_get_folder_share_dialog()
     def details_viewer_get_pixbuf(self):
         return self._viewer.console_get_pixbuf()
 
diff --git a/virtManager/details/viewers.py b/virtManager/details/viewers.py
index 1614ba6d..188ecee1 100644
--- a/virtManager/details/viewers.py
+++ b/virtManager/details/viewers.py
@@ -25,6 +25,7 @@ from virtinst import log
 
 from .sshtunnels import SSHTunnels
 from ..baseclass import vmmGObject
+from ..foldershare import vmmFolderShare
 
 
 ##################################
@@ -207,6 +208,11 @@ class Viewer(vmmGObject):
     def _has_agent(self):
         raise NotImplementedError()
 
+    def _has_folder_sharing(self):
+        raise NotImplementedError()
+    def _get_folder_share_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_folder_sharing(self):
+        return self._has_folder_sharing()
+    def console_get_folder_share_dialog(self):
+        return self._get_folder_share_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_folder_sharing(self):
+        return False
+    def _get_folder_share_dialog(self):
+        return None
+
 
     #######################
     # Connection routines #
@@ -489,6 +505,8 @@ class SpiceViewer(Viewer):
         self._main_channel_hids = []
         self._display_channel = None
         self._usbdev_manager = None
+        self._webdav_channel = None
+        self._folder_share_dialog = None
 
 
     ###################
@@ -628,6 +646,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")
@@ -760,3 +781,45 @@ class SpiceViewer(Viewer):
             if c.__class__ is SpiceClientGLib.UsbredirChannel:
                 return True
         return False
+
+    def _has_folder_sharing(self):
+        if not self._spice_session:
+            return False
+
+        return self._webdav_channel is not None
+
+    def _get_folder_share_dialog(self):
+        if not self._spice_session:
+            return
+
+        try:
+            if not self._folder_share_dialog:
+                self._folder_share_dialog = vmmFolderShare()
+                GObject.GObject.bind_property(
+                    self._spice_session, "share-dir-ro", self._folder_share_dialog,
+                    vmmFolderShare.SHARE_FOLDER_RO, GObject.BindingFlags.BIDIRECTIONAL
+                    | GObject.BindingFlags.SYNC_CREATE)
+                GObject.GObject.bind_property(
+                    self._spice_session, "shared-dir", self._folder_share_dialog,
+                    vmmFolderShare.SHARED_FOLDER, GObject.BindingFlags.BIDIRECTIONAL
+                    | GObject.BindingFlags.SYNC_CREATE)
+                self._folder_share_dialog.set_property(
+                    vmmFolderShare.SHARE_FOLDER,
+                    self._webdav_channel.get_property("port-opened"))
+                self._folder_share_dialog.connect("notify::" + vmmFolderShare.SHARE_FOLDER,
+                                            self._update_share_folder)
+        except Exception as e:
+            log.error("Error launching 'Share folder' dialog: %s", e)
+
+        return self._folder_share_dialog
+
+    def _update_share_folder(self, dialog, ignore):
+        share = dialog.get_property(vmmFolderShare.SHARE_FOLDER)
+        if share:
+            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)
diff --git a/virtManager/foldershare.py b/virtManager/foldershare.py
new file mode 100644
index 00000000..c92ccab4
--- /dev/null
+++ b/virtManager/foldershare.py
@@ -0,0 +1,90 @@
+# Copyright (C) 2019 Jitao Lu <dianlujitao at gmail.com>
+#
+# This work is licensed under the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+
+from gi.repository import GLib, GObject
+
+from .baseclass import vmmGObjectUI
+
+
+class vmmFolderShare(vmmGObjectUI):
+    SHARE_FOLDER = "share-folder"
+    SHARE_FOLDER_RO = "share-folder-ro"
+    SHARED_FOLDER = "shared-folder"
+
+    __gproperties__ = {
+        # 'name': (GObject.TYPE_*,
+        #           nickname, long desc, (type related args), mode)
+        # Type related args can be min, max for int (etc.), or default value
+        # for strings and bool
+        SHARE_FOLDER:
+        (GObject.TYPE_BOOLEAN, "Share folder",
+         "Indicates whether to share folder", False, GObject.PARAM_READWRITE),
+        SHARE_FOLDER_RO: (GObject.TYPE_BOOLEAN, "Share folder read-only",
+                          "Indicates whether to share folder in read-only",
+                          False, GObject.PARAM_READWRITE),
+        SHARED_FOLDER:
+        (GObject.TYPE_STRING, "Shared folder", "Indicates the shared folder",
+         GLib.get_user_special_dir(GLib.USER_DIRECTORY_PUBLIC_SHARE),
+         GObject.PARAM_READWRITE),
+    }
+
+    def __init__(self):
+        vmmGObjectUI.__init__(self, "foldershare.ui", "vmm-folder-share")
+        self._cleanup_on_app_close()
+        self.bind_escape_key_close()
+
+        self.builder.connect_signals({
+            "on_vmm_folder_share_delete_event":
+            self.close,
+            "on_folder_share_close_clicked":
+            self.close,
+        })
+
+        self.share_folder = False
+        self.share_folder_ro = False
+        self.shared_folder = GLib.get_user_special_dir(
+            GLib.USER_DIRECTORY_PUBLIC_SHARE)
+
+        self.share_folder_cb = self.widget("share-folder-cb")
+        GObject.GObject.bind_property(
+            self, self.__class__.SHARE_FOLDER, self.share_folder_cb, "active",
+            GObject.BindingFlags.BIDIRECTIONAL
+            | GObject.BindingFlags.SYNC_CREATE)
+
+        self.share_folder_ro_cb = self.widget("share-folder-ro-cb")
+        GObject.GObject.bind_property(
+            self, self.__class__.SHARE_FOLDER_RO, self.share_folder_ro_cb,
+            "active", GObject.BindingFlags.BIDIRECTIONAL
+            | GObject.BindingFlags.SYNC_CREATE)
+
+        self.shared_folder_fc = self.widget("shared-folder-fc")
+        self.shared_folder_fc.connect(
+            "file-set", lambda fc: self.set_property(
+                self.__class__.SHARED_FOLDER, fc.get_filename()))
+
+    def show(self, parent):
+        self.shared_folder_fc.set_current_folder(self.shared_folder)
+        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
+
+    # Properties are passed to use with "-" in the name, but python
+    # variables can't be named like that
+    def _sanitize_param_spec_name(self, name):
+        return name.replace("-", "_")
+
+    def do_get_property(self, param_spec):
+        name = self._sanitize_param_spec_name(param_spec.name)
+        return getattr(self, name)
+
+    def do_set_property(self, param_spec, value):
+        name = self._sanitize_param_spec_name(param_spec.name)
+        setattr(self, name, value)
diff --git a/virtManager/vmwindow.py b/virtManager/vmwindow.py
index 54e37399..552a14c7 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_folder_sharing": self.control_vm_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_folder = self.console.details_viewer_has_folder_sharing()
+        self.widget("details-menu-folder-sharing").set_sensitive(can_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_folder_sharing(self, ignore):
+        folder_share_dialog = self.console.details_viewer_get_folder_share_dialog()
+        folder_share_dialog.show(self.topwin)
+
     def _take_screenshot(self):
         image = self.console.details_viewer_get_pixbuf()
 
-- 
2.24.1





More information about the virt-tools-list mailing list