udiskie-1.4.9/0000755000175000001440000000000012677741056013653 5ustar thomasusers00000000000000udiskie-1.4.9/CHANGES.rst0000644000175000001440000001553212677734631015464 0ustar thomasusers00000000000000CHANGELOG --------- 1.4.9 ~~~~~ Date: 02.04.2016 - add is_loop and loop_file properties for devices - fix recursive mounting of crypto devices (udiskie-mount) - prevent empty submenus from showing 1.4.8 ~~~~~ Date: 09.02.2016 - fix problem with setupscript if utf8 is not the default encoding - fix crash when starting without X - basic support for loop devices (must be enabled explicitly at this time) - fix handling of 2 more error cases 1.4.7 ~~~~~ Date: 04.01.2016 - fix typo that prevents the yaml config file from being used - fix problem with glib/gio gir API on slackware (olders versions?) - fix bug when changing device state (e.g. when formatting existing device or burning ISO file to device) - improve handling of race conditions with udisks1 backend - fix notifications for devices without labels 1.4.6 ~~~~~ Date: 28.12.2015 - cleanup recent bugfixes - close some gates for more py2/unicode related bugs 1.4.5 ~~~~~ Date: 24.12.2015 - fix another bug with unicode data on command line (py2) - slightly improve stack traces in async code - further decrease verbosity while removing devices 1.4.4 ~~~~~ Date: 24.12.2015 - fix too narrow dependency enforcement - make udiskie slightly less verbose in default mode 1.4.3 ~~~~~ Date: 24.12.2015 - fix bug with unicode data on python2 - fix bug due to event ordering in udisks1 - fix bug due to inavailability of device data at specific time 1.4.2 ~~~~~ Date: 22.12.2015 - fix regression in get_password_tty 1.4.1 ~~~~~ Date: 19.12.2015 - fix problem in SmartTray due to recent transition to async 1.4.0 ~~~~~ Date: 19.12.2015 - go async (with self-made async module for now, until gbulb becomes ready) - specify GTK/Notify versions to be imported (hence fix warnings and a problem for the tray icon resulting from accidentally importing GTK2) - add optional password caching 1.3.2 ~~~~~ - revert "respect the automount flag for devices" - make dependency on Gtk optional 1.3.1 ~~~~~ - use icon hints from udev settings in notifications - respect the automount flag for devices - don't fail if libnotify is not available 1.3.0 ~~~~~ - add actions to "Device added" notification - allow to configure which actions should be added to notifications 1.2.1 ~~~~~ - fix unicode issue in setup script - update license/copyright notices 1.2.0 ~~~~~ - use UDisks2 by default - add --password-prompt command line argument and config file entry 1.1.3 ~~~~~ - fix password prompt for GTK2 (tray is still broken for GTK2) - fix minor documentation issues 1.1.2 ~~~~~ - add key ``device_id`` for matching devices rather than only file systems - improve documentation regarding dependencies 1.1.1 ~~~~~ - fix careless error in man page 1.1.0 ~~~~~ - implemented internationalization - added spanish translation - allow to choose icons from a configurable list 1.0.4 ~~~~~ - compatibility with older version of pygobject (e.g. in Slackware 14.1) 1.0.3 ~~~~~ - handle exception if no notification service is installed 1.0.2 ~~~~~ - fix crash when calling udiskie mount/unmount utilites without udisks1 installed 1.0.1 ~~~~~ - fix crash when calling udiskie without having udisks1 installed (regression) 1.0.0 ~~~~~ - port to PyGObject, removing dependencies on pygtk, zenity, dbus-python, python-notify - use a PyGObject based password dialog - remove --password-prompt parameter - rename command line parameters - add negations for all command line parameters 0.8.0 ~~~~~ - remove the '--filters' parameter for good - change config format to YAML - change default config path to $XDG_CONFIG_HOME/udiskie/config.yml - separate ignore filters from mount option filters - allow to match multiple attributes against a device (AND-wise) - allow to overwrite udiskies default handleability settings - raise exception if --config file doesn't exist - add --options parameter for udiskie-mount - simplify local installations 0.7.0 ~~~~~ There are some backward incompatible changes, hence the version break: - command line parameter '-f'/'--filters' renamed to '-C'/'--config' - add sections in config file to disable individual mount notifications and set defaults for some program options (udisks version, prompt, etc) - refactor ``udiskie.cli``, ``udiskie.config`` and ``udiskie.tray`` - revert 'make udiskie a namespace package' - add 'Browse folder' action to tray menu - add 'Browse folder' action button to mount notifications - add '--no-automounter' command line option to disable automounting - add '--auto-tray' command line option to use a tray icon that automatically disappears when no actions are available - show notifications when devices dis-/appear (can be disabled via config file) - show 'id_label' in tray menu, if available (instead of mount path or device path) - add 'Job failed' notifications - add 'Retry' button to failed notifications - remove automatic retries to unlock LUKS partitions - pass only device name to external password prompt - add '--quiet' command line option - ignore devices ignored by udev rules 0.6.4 ~~~~~ - fix logging in setup.py - more verbose log messages (with time) when having -v on - fix mounting devices that are added as 'external' and later changed to 'internal' [udisks1] (applies to LUKS devices that are opened by an udev rule for example) 0.6.3 (bug fix) ~~~~~~~~~~~~~~~ - fix exception in Mounter.detach_device if unable to detach - fix force-detach for UDisks2 backend - automatically use UDisks2 if UDisks1 is not available - mount unlocked devices only once, removes error message on UDisks2 - mention __ignore__ in man page 0.6.2 (aesthetic) ~~~~~~~~~~~~~~~~~ - add custom icons for the context menu of the system tray widget 0.6.1 (bug fix) ~~~~~~~~~~~~~~~ - fix udisks2 external device detection bug: all devices were considered external when using ``Sniffer`` (as done in the udiskie-mount and udiskie-umount tools) 0.6.0 (udisks2 support, bug fix) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - cache device states to avoid some race conditions - show filesystem label in mount/unmount notifications - retry to unlock LUKS devices when wrong password was entered twice - show 'eject' only if media is available (udisks1 ejects only in this case) - (un-) mount/lock notifications shown even when operations failed - refactor internal API - experimental support for udisks2 0.5.3 (feature, bug fix) ~~~~~~~~~~~~~~~~~~~~~~~~ - add '__ignore__' config file option to prevent handling specific devices - delay notifications until termination of long operations 0.5.2 (tray icon) ~~~~~~~~~~~~~~~~~ - add tray icon (pygtk based) - eject / detach drives from command line 0.5.1 (mainly internal changes) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - use setuptools entry points to create the executables - make udiskie a namespace package 0.5.0 (LUKS support) ~~~~~~~~~~~~~~~~~~~~ - support for LUKS devices (using zenity for password prompt) - major refactoring - use setuptools as installer udiskie-1.4.9/test/0000755000175000001440000000000012677741056014632 5ustar thomasusers00000000000000udiskie-1.4.9/test/test_cache.py0000644000175000001440000000416312640243454017277 0ustar thomasusers00000000000000# encoding: utf-8 """ Tests for the udiskie.cache module. """ from __future__ import unicode_literals import unittest import time from udiskie.cache import PasswordCache class TestDev(object): def __init__(self, id_uuid): self.id_uuid = id_uuid class TestPasswordCache(unittest.TestCase): """ Tests for the udiskie.cache.PasswordCache class. """ # NOTE: The device names are different in each test so that they do not # interfere accidentally. def test_timeout(self): """The cached password expires after the specified timeout.""" device = TestDev('ALPHA') password = '{<}hëllo ωορλδ!{>}' cache = PasswordCache(1) cache[device] = password self.assertEqual(cache[device], password) time.sleep(1.5) with self.assertRaises(KeyError): _ = cache[device] def test_touch(self): """Key access refreshes the timeout.""" device = TestDev('BETA') password = '{<}hëllo ωορλδ!{>}' cache = PasswordCache(3) cache[device] = password time.sleep(2) self.assertEqual(cache[device], password) time.sleep(2) self.assertEqual(cache[device], password) time.sleep(4) with self.assertRaises(KeyError): _ = cache[device] def test_revoke(self): """A key can be deleted manually.""" device = TestDev('GAMMA') password = '{<}hëllo ωορλδ!{>}' cache = PasswordCache(0) cache[device] = password self.assertEqual(cache[device], password) del cache[device] with self.assertRaises(KeyError): _ = cache[device] def test_update(self): device = TestDev('DELTA') password = '{<}hëllo ωορλδ!{>}' cache = PasswordCache(0) cache[device] = password self.assertEqual(cache[device], password) cache[device] = password * 2 self.assertEqual(cache[device], password*2) del cache[device] with self.assertRaises(KeyError): _ = cache[device] if __name__ == '__main__': unittest.main() udiskie-1.4.9/test/test_match.py0000644000175000017500000000460112336433116017461 0ustar thomasthomas00000000000000# encoding: utf-8 """ Tests for the udiskie.match module. These tests are intended to demonstrate and ensure the correct usage of the config file used by udiskie for custom device options. """ import unittest import tempfile import shutil import os.path import gc from udiskie.config import Config class TestDev(object): def __init__(self, object_path, id_type, id_uuid): self.object_path = object_path self.id_type = id_type self.id_uuid = id_uuid class TestFilterMatcher(unittest.TestCase): """ Tests for the udiskie.match.FilterMatcher class. """ def setUp(self): """Create a temporary config file.""" self.base = tempfile.mkdtemp() self.config_file = os.path.join(self.base, 'filters.conf') with open(self.config_file, 'wt') as f: f.write(''' mount_options: - id_uuid: device-with-options options: noatime,nouser - id_type: vfat options: ro,nouser ignore_device: - id_uuid: ignored-DEVICE ''') config = Config.from_file(self.config_file) self.mount_options = config.mount_options self.ignore_device = config.ignore_device def tearDown(self): """Remove the config file.""" gc.collect() shutil.rmtree(self.base) def test_ignored(self): """Test the FilterMatcher.is_ignored() method.""" self.assertTrue( self.ignore_device( TestDev('/ignore', 'vfat', 'IGNORED-device'))) self.assertFalse( self.ignore_device( TestDev('/options', 'vfat', 'device-with-options'))) self.assertFalse( self.ignore_device( TestDev('/nomatch', 'vfat', 'no-matching-id'))) def test_options(self): """Test the FilterMatcher.get_mount_options() method.""" self.assertEqual( ['noatime', 'nouser'], self.mount_options( TestDev('/options', 'vfat', 'device-with-options'))) self.assertEqual( ['noatime', 'nouser'], self.mount_options( TestDev('/optonly', 'ext', 'device-with-options'))) self.assertEqual( ['ro', 'nouser'], self.mount_options( TestDev('/fsonly', 'vfat', 'no-matching-id'))) self.assertEqual( None, self.mount_options( TestDev('/nomatch', 'ext', 'no-matching-id'))) udiskie-1.4.9/setup.cfg0000644000175000001440000000007312677741056015474 0ustar thomasusers00000000000000[egg_info] tag_svn_revision = 0 tag_build = tag_date = 0 udiskie-1.4.9/MANIFEST.in0000644000175000017500000000030512431262245015527 0ustar thomasthomas00000000000000include doc/*.txt include doc/asciidoc.conf include doc/Makefile recursive-include icons *.svg recursive-include lang *.pot *.po include CONTRIBUTORS COPYING LICENSE include README.rst CHANGES.rst udiskie-1.4.9/doc/0000755000175000001440000000000012677741056014420 5ustar thomasusers00000000000000udiskie-1.4.9/doc/Makefile0000644000175000017500000000022212522134406016172 0ustar thomasthomas00000000000000udiskie.8: udiskie.8.txt asciidoc.conf a2x --asciidoc-opts="-f asciidoc.conf" -f manpage -L udiskie.8.txt .PHONY: clean clean: rm -f udiskie.8 udiskie-1.4.9/doc/udiskie.8.txt0000644000175000001440000002301612665560635016765 0ustar thomasusers00000000000000///// vim:set ts=4 sw=4 syntax=asciidoc noet: ///// udiskie(8) ========== Name ---- udiskie - automounter for removable media Synopsis -------- 'udiskie' [OPTIONS] 'udiskie-mount' [OPTIONS] (-a | DEVICE...) 'udiskie-umount' [OPTIONS] (-a | PATH...) Description ----------- *udiskie* is a front-end for UDisks written in python. Its main purpose is automatically mounting removable media, such as CDs or flash drives. It has optional mount notifications, a GTK tray icon and user level CLIs for manual mount and unmount operations. The media will be mounted in a new directory under '/media' or '/run/media/USER/', using the device name if possible. Common options -------------- *-h, \--help*:: Show help message and exit. *-V, \--version*:: Show help message and exit. *-v, \--verbose*:: Verbose output. *-q, \--quiet*:: Quiet output. *-0, \--udisks-auto*:: Auto discover UDisks version (default). Prefers UDisks2 if both are available. *-1, \--use-udisks1*:: Use UDisks1 as DBus backend. *-2, \--use-udisks2*:: Use UDisks2 as DBus backend. *-c FILE, \--config=FILE*:: Specify config file. *-C, \--no-config*:: Don't use any config file. Shared Mount and Daemon options ------------------------------- *-p COMMAND, \--password-prompt=COMMAND*:: Password retrieval command. The string is formatted with a Device object as its first argument. *-P, \--no-password-prompt*:: Disable unlocking of LUKS devices. Daemon options -------------- *-a, \--automount*:: Enable automounting new devices (default). *-A, \--no-automount*:: Disable automounting new devices. *-n, \--notify*:: Enable pop-up notifications (default). *-N, \--disable-notify*:: Disable pop-up notifications. *-t, \--tray*:: Show tray icon. *-s, \--smart-tray*:: Show tray icon that automatically hides when there is no action available. *-T, \--no-tray*:: Disable tray icon (default). *-f PROGRAM, \--file-manager=PROGRAM*:: Set program to open mounted directories. Default is \'+xdg-open+'. Pass an empty string to disable this feature. This option is deprecated and will probably be replaced by a python commands file. *-F, \--no-file-manager*:: Disable browsing. Mount options ------------- *-a, \--all*:: Mount all handled devices. *-r, \--recursive*:: Recursively mount cleartext partitions after unlocking a LUKS device. This will happen by default when running the udiskie daemon. *-R, \--no-recursive*:: Disable recursive mounting (default). *-o OPTIONS, \--options=OPTIONS*:: Set mount options. Unmount options --------------- *-a, \--all*:: Unmount all handled devices. *-d, \--detach*:: Detach drive by e.g. powering down its physical port. *-D, \--no-detach*:: Don't detach drive (default). *-e, \--eject*:: Eject media from the drive, e.g CDROM. *-E, \--no-eject*:: Don't eject media (default). *-f, \--force*:: Force removal (recursive unmounting). *-F, \--no-force*:: Don't force removal (default). *-l, \--lock*:: Lock device after unmounting (default). *-L, \--no-lock*:: Don't lock device. Example Usage[[EU]] ------------------- Start *udiskie* in '~/.xinitrc': udiskie & Unmount media and power down USB device: udiskie-umount --detach /media/Sticky Mount all media: udiskie-mount -a Mount '/dev/sdb1': udiskie-mount /dev/sdb1 Configuration ------------- The file '.config/udiskie/config.yml' can be used to configure defaults for command line parameters and customize further settings. The actual path may differ depending on '$XDG_CONFIG_HOME'. The file format is YAML, see http://en.wikipedia.org/wiki/YAML. If you don't want to install PyYAML, it is possible to use an equivalent JSON file with the name 'config.json' instead. ---------------------------------------------------------------------- # This is an example (nonsense) configuration file for udiskie. program_options: # Configure defaults for command line options udisks_version: 2 # [int] Specify the version of udisks # to be used. Set to 0 to use automatic # discovery. tray: auto # [bool] Enable the tray icon. "auto" # means auto-hide the tray icon when # there are no handled devices. automount: false # [bool] Enable automatic mounting. notify: true # [bool] Enable notifications. password_cache: 30 # [int] Password cache in minutes. Caching is # disabled by default. It can be disabled # explicitly by setting it to false file_manager: xdg-open # [string] Set program to open directories. It will be invoked # with the folder path as its # command line first argument. password_prompt: ["gnome-keyring-query", "get", "{.id_uuid}"] # [string|list] Set command to retrieve passwords. If specified # as a list it defines the ARGV array for the program call. If # specified as a string, it will be expanded in a shell-like # manner. Each string will be formatted using str.format with a # Device object as the first argument. For a list of device # attributes, see below. The two special string values # "builtin:gui" and "builtin:tty" signify to use udiskie's # builtin password prompt. mount_options: # List of mount option rules. Only the first matching entry is # used. Each item can match any combination of device attributes # (see below). Additionally, it must define an 'options' list. An # item without any device attributes serves as a catch-all rule. - id_type: vfat # match file system type options: ro # list of mount options - id_uuid: 9d53-13ba # match by device UUID options: [noexec, nodev] # mount options can be given as list ignore_device: # Customize udiskie's ignore rules. This config entry has the # same structure as *mount_options*, the only difference being # that the action is defined by the 'ignore' field which is a # boolean defaulting to true. The rules defined here are simply # prepended to the builtin ignore rules, so that it is possible # to completely overwrite the defaults by specifying a catch-all # rule. - id_uuid: abcd-ef01 # ignore this device - device_file: /dev/dm-5 ignore: false # never ignore this device notifications: # Customize which notifications are shown for how long. Possible # values are: # positive number timeout in seconds # false disable # -1 use the libnotify default timeout timeout: 1.5 # set the default for all notifications # Specify only if you want to overwrite the the default: device_mounted: 5 # mount notification device_unmounted: false # unmount notification device_added: false # device has appeared device_removed: false # device has disappeared device_unlocked: -1 # encrypted device was unlocked device_locked: -1 # encrypted device was locked job_failed: -1 # mount/unlock/.. has failed notification_actions: # Define which actions should be shown on notifications. Note that there # are currently only a limited set of actions available for each # notification. Events that are not explicitly specified show the default # set of actions. Specify an empty list if you don't want to see any # notification for the specified event: device_mounted: [browse] device_added: [mount] icon_names: # Customize the icon set used by the tray widget. Each entry # specifies a list of icon names. The first installed icon from # that list will be used. media: [drive-removable-media, media-optical] browse: [document-open, folder-open] mount: [udiskie-mount] unmount: [udiskie-unmount] unlock: [udiskie-unlock] lock: [udiskie-lock] eject: [udiskie-eject, media-eject] detach: [udiskie-detach] quit: [application-exit] ---------------------------------------------------------------------- All keys are optional. Reasonable defaults are used if you leave them unspecified. Device attributes ----------------- Some of the config entries make use of Device attributes. The following list of attributes is currently available, but there is no guarantee that they will remain available: Attribute Hint/Example is_drive is_block is_partition_table is_partition is_filesystem is_luks is_loop is_toplevel is_detachable is_ejectable has_media device_file block device path, e.g. "/dev/sdb1" device_presentation display string, e.g. "/dev/sdb1" device_id unique, persistent device identifer id_usage E.g. "filesystem" or "crypto" is_crypto is_ignored id_type E.g. "ext4" or "crypto_LUKS" id_label device label id_uuid device UUID is_luks_cleartext is_external udisks flag HintSystem=false is_systeminternal udisks flag HintSystem=true is_mounted mount_paths list of mount paths is_unlocked in_use device or any of its children mounted loop_file file backing the loop device See Also -------- linkman:udisks[1] http://www.freedesktop.org/wiki/Software/udisks/ Contact ------- You can use the github issues to report any issues you encounter, ask general questions or suggest new features. There is also a public mailing list on sourceforge if you prefer email: https://github.com/coldfix/udiskie/issues http://lists.coldfix.de/mailman/listinfo/udiskie udiskie-1.4.9/doc/asciidoc.conf0000644000175000017500000000316512221132411017156 0ustar thomasthomas00000000000000## linkman: macro # Inspired by/borrowed from the GIT source tree at Documentation/asciidoc.conf # # Usage: linkman:command[manpage-section] # # Note, {0} is the manpage section, while {target} is the command. # # Show man link as: (
); if section is defined, else just show # the command. [macros] (?su)[\\]?(?Plinkman):(?P\S*?)\[(?P.*?)\]= [attributes] asterisk=* plus=+ caret=^ startsb=[ endsb=] tilde=~ ifdef::backend-docbook[] [linkman-inlinemacro] {0%{target}} {0#} {0#{target}{0}} {0#} endif::backend-docbook[] ifdef::backend-docbook[] ifndef::docbook-xsl-172[] # "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this. # v1.72 breaks with this because it replaces dots not in roff requests. [listingblock] {title} | {title#} endif::docbook-xsl-172[] endif::backend-docbook[] ifdef::doctype-manpage[] ifdef::backend-docbook[] [header] template::[header-declarations] {pacman_date} {mantitle} {manvolnum} udiskie udiskie {manname} {manpurpose} endif::backend-docbook[] endif::doctype-manpage[] ifdef::backend-xhtml11[] [linkman-inlinemacro] {target}{0?({0})} endif::backend-xhtml11[] udiskie-1.4.9/PKG-INFO0000644000175000001440000003376212677741056014763 0ustar thomasusers00000000000000Metadata-Version: 1.1 Name: udiskie Version: 1.4.9 Summary: Removable disk automounter for udisks Home-page: https://github.com/coldfix/udiskie Author: Thomas Gläßle Author-email: t_glaessle@gmx.de License: MIT Description: ======= udiskie ======= |Version| |Downloads| |License| *udiskie* is a UDisks_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. Its features include: - automount removable media when inserted - notifications (on insertion, mount, unmount, …) - GTK tray icon to manage all available devices - command line tools for manual un-/mounting - support for LUKS encrypted devices - password caching - works with either udisks1 or udisks2 - an extensible code base (python) - a maintainer who is open for suggestions;) All features can be indidually enabled or disabled (yes, you can submit unmaintainable code and make me salty!) .. _UDisks: http://www.freedesktop.org/wiki/Software/udisks Documentation ~~~~~~~~~~~~~ - Usage_ - Permissions_ - Installation_ Miscellaneous: - `Custom mount paths`_ - `Acquiring debug information`_ .. _Usage: https://github.com/coldfix/udiskie/wiki/Usage .. _Permissions: https://github.com/coldfix/udiskie/wiki/Permissions .. _Installation: https://github.com/coldfix/udiskie/wiki/Installation .. _Custom mount paths: https://github.com/coldfix/udiskie/wiki/Custom-mount-paths .. _Acquiring debug information: https://github.com/coldfix/udiskie/wiki/Debugging-a-problem Project pages ~~~~~~~~~~~~~ The… - `Wiki`_ contains installation instructions and additional information. - `Man page`_ describes the command line options - `Source Code`_ is hosted on github. - `Latest Release`_ is available for download on PyPI. - `Issue Tracker`_ is the right place to report any issues you encounter, ask general questions or suggest new features. There is also a public `Mailing List`_ if you prefer email. .. _Wiki: https://github.com/coldfix/udiskie/wiki .. _Man Page: https://raw.githubusercontent.com/coldfix/udiskie/master/doc/udiskie.8.txt .. _Source Code: https://github.com/coldfix/udiskie .. _Latest Release: https://pypi.python.org/pypi/udiskie/ .. _Issue Tracker: https://github.com/coldfix/udiskie/issues .. _Mailing List: https://lists.coldfix.de/mailman/listinfo/udiskie Roadmap ~~~~~~~ For the next udiskie versions, I am mainly concerned with quality assurance and stability. For one this means reducing the number of supported runtime configurations and make the remaining easier to test, i.e.: - **drop support for python2** to avoid unicode issues and make use of the new asyncio module which provides better error handling (stack traces!) than the current solution. - **drop support for udisks1**. The udisks1 API is rather unfit for the asynchronous nature of the problem which has led to numerous bugs and problems (plenty more probably waiting to be discovered as we speak) - **add automated tests**. needed desperately… .. |Version| image:: http://coldfix.de:8080/v/udiskie/badge.svg :target: https://pypi.python.org/pypi/udiskie/ :alt: Latest Version .. |Downloads| image:: http://coldfix.de:8080/d/udiskie/badge.svg :target: https://pypi.python.org/pypi/udiskie#downloads :alt: Downloads .. |License| image:: http://coldfix.de:8080/license/udiskie/badge.svg :target: https://github.com/coldfix/udiskie/blob/master/COPYING :alt: License CHANGELOG --------- 1.4.9 ~~~~~ Date: 02.04.2016 - add is_loop and loop_file properties for devices - fix recursive mounting of crypto devices (udiskie-mount) - prevent empty submenus from showing 1.4.8 ~~~~~ Date: 09.02.2016 - fix problem with setupscript if utf8 is not the default encoding - fix crash when starting without X - basic support for loop devices (must be enabled explicitly at this time) - fix handling of 2 more error cases 1.4.7 ~~~~~ Date: 04.01.2016 - fix typo that prevents the yaml config file from being used - fix problem with glib/gio gir API on slackware (olders versions?) - fix bug when changing device state (e.g. when formatting existing device or burning ISO file to device) - improve handling of race conditions with udisks1 backend - fix notifications for devices without labels 1.4.6 ~~~~~ Date: 28.12.2015 - cleanup recent bugfixes - close some gates for more py2/unicode related bugs 1.4.5 ~~~~~ Date: 24.12.2015 - fix another bug with unicode data on command line (py2) - slightly improve stack traces in async code - further decrease verbosity while removing devices 1.4.4 ~~~~~ Date: 24.12.2015 - fix too narrow dependency enforcement - make udiskie slightly less verbose in default mode 1.4.3 ~~~~~ Date: 24.12.2015 - fix bug with unicode data on python2 - fix bug due to event ordering in udisks1 - fix bug due to inavailability of device data at specific time 1.4.2 ~~~~~ Date: 22.12.2015 - fix regression in get_password_tty 1.4.1 ~~~~~ Date: 19.12.2015 - fix problem in SmartTray due to recent transition to async 1.4.0 ~~~~~ Date: 19.12.2015 - go async (with self-made async module for now, until gbulb becomes ready) - specify GTK/Notify versions to be imported (hence fix warnings and a problem for the tray icon resulting from accidentally importing GTK2) - add optional password caching 1.3.2 ~~~~~ - revert "respect the automount flag for devices" - make dependency on Gtk optional 1.3.1 ~~~~~ - use icon hints from udev settings in notifications - respect the automount flag for devices - don't fail if libnotify is not available 1.3.0 ~~~~~ - add actions to "Device added" notification - allow to configure which actions should be added to notifications 1.2.1 ~~~~~ - fix unicode issue in setup script - update license/copyright notices 1.2.0 ~~~~~ - use UDisks2 by default - add --password-prompt command line argument and config file entry 1.1.3 ~~~~~ - fix password prompt for GTK2 (tray is still broken for GTK2) - fix minor documentation issues 1.1.2 ~~~~~ - add key ``device_id`` for matching devices rather than only file systems - improve documentation regarding dependencies 1.1.1 ~~~~~ - fix careless error in man page 1.1.0 ~~~~~ - implemented internationalization - added spanish translation - allow to choose icons from a configurable list 1.0.4 ~~~~~ - compatibility with older version of pygobject (e.g. in Slackware 14.1) 1.0.3 ~~~~~ - handle exception if no notification service is installed 1.0.2 ~~~~~ - fix crash when calling udiskie mount/unmount utilites without udisks1 installed 1.0.1 ~~~~~ - fix crash when calling udiskie without having udisks1 installed (regression) 1.0.0 ~~~~~ - port to PyGObject, removing dependencies on pygtk, zenity, dbus-python, python-notify - use a PyGObject based password dialog - remove --password-prompt parameter - rename command line parameters - add negations for all command line parameters 0.8.0 ~~~~~ - remove the '--filters' parameter for good - change config format to YAML - change default config path to $XDG_CONFIG_HOME/udiskie/config.yml - separate ignore filters from mount option filters - allow to match multiple attributes against a device (AND-wise) - allow to overwrite udiskies default handleability settings - raise exception if --config file doesn't exist - add --options parameter for udiskie-mount - simplify local installations 0.7.0 ~~~~~ There are some backward incompatible changes, hence the version break: - command line parameter '-f'/'--filters' renamed to '-C'/'--config' - add sections in config file to disable individual mount notifications and set defaults for some program options (udisks version, prompt, etc) - refactor ``udiskie.cli``, ``udiskie.config`` and ``udiskie.tray`` - revert 'make udiskie a namespace package' - add 'Browse folder' action to tray menu - add 'Browse folder' action button to mount notifications - add '--no-automounter' command line option to disable automounting - add '--auto-tray' command line option to use a tray icon that automatically disappears when no actions are available - show notifications when devices dis-/appear (can be disabled via config file) - show 'id_label' in tray menu, if available (instead of mount path or device path) - add 'Job failed' notifications - add 'Retry' button to failed notifications - remove automatic retries to unlock LUKS partitions - pass only device name to external password prompt - add '--quiet' command line option - ignore devices ignored by udev rules 0.6.4 ~~~~~ - fix logging in setup.py - more verbose log messages (with time) when having -v on - fix mounting devices that are added as 'external' and later changed to 'internal' [udisks1] (applies to LUKS devices that are opened by an udev rule for example) 0.6.3 (bug fix) ~~~~~~~~~~~~~~~ - fix exception in Mounter.detach_device if unable to detach - fix force-detach for UDisks2 backend - automatically use UDisks2 if UDisks1 is not available - mount unlocked devices only once, removes error message on UDisks2 - mention __ignore__ in man page 0.6.2 (aesthetic) ~~~~~~~~~~~~~~~~~ - add custom icons for the context menu of the system tray widget 0.6.1 (bug fix) ~~~~~~~~~~~~~~~ - fix udisks2 external device detection bug: all devices were considered external when using ``Sniffer`` (as done in the udiskie-mount and udiskie-umount tools) 0.6.0 (udisks2 support, bug fix) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - cache device states to avoid some race conditions - show filesystem label in mount/unmount notifications - retry to unlock LUKS devices when wrong password was entered twice - show 'eject' only if media is available (udisks1 ejects only in this case) - (un-) mount/lock notifications shown even when operations failed - refactor internal API - experimental support for udisks2 0.5.3 (feature, bug fix) ~~~~~~~~~~~~~~~~~~~~~~~~ - add '__ignore__' config file option to prevent handling specific devices - delay notifications until termination of long operations 0.5.2 (tray icon) ~~~~~~~~~~~~~~~~~ - add tray icon (pygtk based) - eject / detach drives from command line 0.5.1 (mainly internal changes) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - use setuptools entry points to create the executables - make udiskie a namespace package 0.5.0 (LUKS support) ~~~~~~~~~~~~~~~~~~~~ - support for LUKS devices (using zenity for password prompt) - major refactoring - use setuptools as installer Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: X11 Applications :: GTK Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Desktop Environment Classifier: Topic :: Software Development Classifier: Topic :: System :: Filesystems Classifier: Topic :: System :: Hardware Classifier: Topic :: Utilities udiskie-1.4.9/setup.py0000644000175000001440000001540412651462426015362 0ustar thomasusers00000000000000# encoding: utf-8 from setuptools import setup, Command from setuptools.command.install import install as orig_install from distutils.command.install_data import install_data as orig_install_data from distutils.command.build import build as orig_build from distutils.util import convert_path from subprocess import call import sys import logging from os import path, listdir from glob import glob import io # check availability of runtime dependencies def check_dependency(package, version): """Issue a warning if the package is not available.""" try: import gi gi.require_version(package.rsplit('.')[-1], version) __import__(package) except ImportError as e: # caused by either of the imports, probably the first logging.warn("Missing runtime dependencies:\n\t" + str(e)) except ValueError as e: # caused by the gi.require_version() statement logging.warn("Missing runtime dependencies:\n\t" + str(e)) except RuntimeError as e: # caused by the final __import__() statement logging.warn("Bad runtime dependency:\n\t" + str(e)) check_dependency('gi.repository.Gio', '2.0') check_dependency('gi.repository.GLib', '2.0') check_dependency('gi.repository.Gtk', '3.0') check_dependency('gi.repository.Notify', '0.7') # read long_description from README.rst long_description = None try: long_description = io.open('README.rst', encoding='utf-8').read() long_description += '\n' + io.open('CHANGES.rst', encoding='utf-8').read() except IOError: pass def exec_file(path): """Execute a python file and return the `globals` dictionary.""" namespace = {} with open(convert_path(path), 'rb') as f: exec(f.read(), namespace, namespace) return namespace metadata = exec_file('udiskie/__init__.py') # language files po_source_folder = 'lang' mo_build_prefix = path.join('build', 'locale') mo_install_prefix = path.join('share', 'locale') # menu icons theme_base = path.join('share', 'icons', 'hicolor') icon_names = ['mount', 'unmount', 'lock', 'unlock', 'eject', 'detach'] class build(orig_build): """Subclass build command to add a subcommand for building .mo files.""" sub_commands = orig_build.sub_commands + [('build_mo', None)] class build_mo(Command): """Create machine specific translation files (for i18n via gettext).""" description = 'Compile .po files into .mo files' def initialize_options(self): pass def finalize_options(self): pass def run(self): for po_filename in glob(path.join(po_source_folder, '*.po')): lang = path.splitext(path.split(po_filename)[1])[0] mo_filename = path.join(mo_build_prefix, lang, 'LC_MESSAGES', 'udiskie.mo') self.mkpath(path.dirname(mo_filename)) self.make_file( po_filename, mo_filename, self.make_mo, [po_filename, mo_filename]) def make_mo(self, po_filename, mo_filename): """Create a machine object (.mo) from a portable object (.po) file.""" try: call(['msgfmt', po_filename, '-o', mo_filename]) except OSError as e: # ignore failures since i18n support is optional: logging.warn(e) # NOTE: we want the install logic from *distutils* rather than the one from # *setuptools*. distutils does NOT automatically install dependencies. On the # other hand, setuptools fails to invoke the build commands properly before # trying to install and it puts the data files in the egg directory (we want # them in `sys.prefix` or similar). # NOTE: Subclassing the setuptools install command alters its behaviour to use # the distutils code. This is due to some really odd call-context checks in # the setuptools command. # NOTE: We need to subclass the setuptools install command rather than the # distutils command to make installing with pip from the source distribution # work. class install(orig_install): """Custom install command used to update the gtk icon cache.""" def run(self): """ Perform old-style (distutils) install, then update GTK icon cache. Extends ``distutils.command.install.install.run``. """ orig_install.run(self) try: call(['gtk-update-icon-cache', theme_base]) except OSError as e: # ignore failures since the tray icon is an optional component: logging.warn(e) class install_data(orig_install_data): def run(self): """Add built translation files and then install data files.""" self.data_files += [ (path.join(mo_install_prefix, lang, 'LC_MESSAGES'), [path.join(mo_build_prefix, lang, 'LC_MESSAGES', 'udiskie.mo')]) for lang in listdir(mo_build_prefix) ] orig_install_data.run(self) setup( name='udiskie', version=metadata['__version__'], description=metadata['__summary__'], long_description=long_description, author=metadata['__author__'], author_email=metadata['__author_email__'], maintainer=metadata['__maintainer__'], maintainer_email=metadata['__maintainer_email__'], url=metadata['__uri__'], license=metadata['__license__'], cmdclass={ 'install': install, 'install_data': install_data, 'build': build, 'build_mo': build_mo, }, packages=[ 'udiskie', ], data_files=[ (path.join(theme_base, 'scalable', 'actions'), [ path.join('icons', 'scalable', 'actions', 'udiskie-{0}.svg'.format(icon_name)) for icon_name in icon_names]) ], entry_points={ 'console_scripts': [ 'udiskie = udiskie.cli:Daemon.main', 'udiskie-mount = udiskie.cli:Mount.main', 'udiskie-umount = udiskie.cli:Umount.main', ], }, install_requires=[ 'PyYAML', 'docopt', # Currently not building out of the box: # 'PyGObject', ], extras_require={ 'password-cache': [ 'keyutils==0.3' ], }, tests_require=[ ], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: X11 Applications :: GTK', 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'License :: OSI Approved :: MIT License', 'Topic :: Desktop Environment', 'Topic :: Software Development', 'Topic :: System :: Filesystems', 'Topic :: System :: Hardware', 'Topic :: Utilities', ], ) udiskie-1.4.9/CONTRIBUTORS0000644000175000001440000000110312637112206015507 0ustar thomasusers00000000000000Byron Clark Peter Bui Massimiliano Torromeo Tasos Latsas Thomas Gläßle Eduard Bopp Steven Allen Jonas Große Sundrup Geoffrey Biggs Alejandro Pérez Andre-Patrick Bubel Israel Dahl Sven Schwedas Ben Boeckel Jan Staněk udiskie-1.4.9/lang/0000755000175000001440000000000012677741056014574 5ustar thomasusers00000000000000udiskie-1.4.9/lang/en_US.po0000644000175000001440000002631212642325627016143 0ustar thomasusers00000000000000msgid "" msgstr "" "Project-Id-Version: udiskie\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-01-03 23:56+0100\n" "PO-Revision-Date: 2014-07-17 19:27+0200\n" "Last-Translator: Thomas Gläßle \n" "Language-Team: English t_glaesle@gmx.de\n" "Language: en_US\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: ../udiskie/prompt.py:117 ../udiskie/prompt.py:128 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "Enter password for {0.device_presentation}: " #: ../udiskie/prompt.py:199 msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "b' option." msgstr "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "b' option." #: ../udiskie/udisks1.py:455 ../udiskie/udisks2.py:548 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "found device owning \"{0}\": \"{1}\"" #: ../udiskie/udisks1.py:458 ../udiskie/udisks2.py:551 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "no device found owning \"{0}\"" #: ../udiskie/udisks1.py:517 ../udiskie/udisks2.py:620 #, python-brace-format msgid "+++ {0}: {1}" msgstr "+++ {0}: {1}" #: ../udiskie/udisks1.py:633 #, python-brace-format msgid "{0} operation failed for device: {1}" msgstr "{0} operation failed for device: {1}" #: ../udiskie/config.py:91 msgid "Unknown matching attribute: {!r}" msgstr "Unknown matching attribute: {!r}" #: ../udiskie/config.py:94 #, python-brace-format msgid "{0} created" msgstr "{0} created" #: ../udiskie/config.py:97 msgid "{0}(match={1!r}, value={2!r})" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:120 #, python-brace-format msgid "{0} used for {1}" msgstr "{0} used for {1}" #: ../udiskie/config.py:217 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:220 #, fuzzy msgid "Failed to read {0!r}: {1}" msgstr "failed to {0} {1}: {2}" #: ../udiskie/depend.py:37 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:43 msgid "X server not connected!" msgstr "X server not connected!" #: ../udiskie/mount.py:131 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "failed to {0} {1}: {2}" #: ../udiskie/mount.py:147 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "not browsing {0}: not mounted" #: ../udiskie/mount.py:150 #, python-brace-format msgid "not browsing {0}: no program" msgstr "not browsing {0}: no program" #: ../udiskie/mount.py:152 msgid "opening {0} on {0.mount_paths[0]}" msgstr "opening {0} on {0.mount_paths[0]}" #: ../udiskie/mount.py:154 msgid "opened {0} on {0.mount_paths[0]}" msgstr "opened {0} on {0.mount_paths[0]}" #: ../udiskie/mount.py:169 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "not mounting {0}: unhandled device" #: ../udiskie/mount.py:172 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "not mounting {0}: already mounted" #: ../udiskie/mount.py:177 #, python-brace-format msgid "mounting {0} with {1}" msgstr "mounting {0} with {1}" #: ../udiskie/mount.py:179 #, python-brace-format msgid "mounted {0} on {1}" msgstr "mounted {0} on {1}" #: ../udiskie/mount.py:193 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "not unmounting {0}: unhandled device" #: ../udiskie/mount.py:196 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "not unmounting {0}: not mounted" #: ../udiskie/mount.py:198 #, python-brace-format msgid "unmounting {0}" msgstr "unmounting {0}" #: ../udiskie/mount.py:200 #, python-brace-format msgid "unmounted {0}" msgstr "unmounted {0}" #: ../udiskie/mount.py:215 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "not unlocking {0}: unhandled device" #: ../udiskie/mount.py:218 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "not unlocking {0}: already unlocked" #: ../udiskie/mount.py:221 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:228 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "not unlocking {0}: cancelled by user" #: ../udiskie/mount.py:230 #, python-brace-format msgid "unlocking {0}" msgstr "unlocking {0}" #: ../udiskie/mount.py:233 #, python-brace-format msgid "unlocked {0}" msgstr "unlocked {0}" #: ../udiskie/mount.py:244 #, fuzzy, python-brace-format msgid "unlocking {0} using cached password" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:248 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:250 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:275 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "not locking {0}: unhandled device" #: ../udiskie/mount.py:278 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "not locking {0}: not unlocked" #: ../udiskie/mount.py:280 #, python-brace-format msgid "locking {0}" msgstr "locking {0}" #: ../udiskie/mount.py:282 #, python-brace-format msgid "locked {0}" msgstr "locked {0}" #: ../udiskie/mount.py:314 ../udiskie/mount.py:352 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "not adding {0}: unhandled device" #: ../udiskie/mount.py:389 ../udiskie/mount.py:440 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "not removing {0}: unhandled device" #: ../udiskie/mount.py:464 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "not ejecting {0}: unhandled device" #: ../udiskie/mount.py:468 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "not ejecting {0}: drive not ejectable" #: ../udiskie/mount.py:472 #, python-brace-format msgid "ejecting {0}" msgstr "ejecting {0}" #: ../udiskie/mount.py:474 #, python-brace-format msgid "ejected {0}" msgstr "ejected {0}" #: ../udiskie/mount.py:489 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "not detaching {0}: unhandled device" #: ../udiskie/mount.py:493 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "not detaching {0}: drive not detachable" #: ../udiskie/mount.py:497 #, python-brace-format msgid "detaching {0}" msgstr "detaching {0}" #: ../udiskie/mount.py:499 #, python-brace-format msgid "detached {0}" msgstr "detached {0}" #: ../udiskie/mount.py:605 #, python-brace-format msgid "Browse {0}" msgstr "Browse {0}" #: ../udiskie/mount.py:606 #, python-brace-format msgid "Mount {0}" msgstr "Mount {0}" #: ../udiskie/mount.py:607 #, python-brace-format msgid "Unmount {0}" msgstr "Unmount {0}" #: ../udiskie/mount.py:608 #, python-brace-format msgid "Unlock {0}" msgstr "Unlock {0}" #: ../udiskie/mount.py:609 #, python-brace-format msgid "Lock {0}" msgstr "Lock {0}" #: ../udiskie/mount.py:610 #, python-brace-format msgid "Eject {0}" msgstr "Eject {0}" #: ../udiskie/mount.py:611 #, python-brace-format msgid "Unpower {0}" msgstr "Unpower {0}" #: ../udiskie/mount.py:612 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/tray.py:84 msgid "Quit" msgstr "Quit" #: ../udiskie/tray.py:174 msgid "Invalid node!" msgstr "Invalid node!" #: ../udiskie/tray.py:295 msgid "udiskie" msgstr "udiskie" #: ../udiskie/notify.py:63 msgid "Browse directory" msgstr "Browse directory" #: ../udiskie/notify.py:67 msgid "Device mounted" msgstr "Device mounted" #: ../udiskie/notify.py:68 #, fuzzy msgid "{0.ui_label} mounted on {0.mount_paths[0]}" msgstr "{0.id_label} mounted on {0.mount_paths[0]}" #: ../udiskie/notify.py:80 msgid "Device unmounted" msgstr "Device unmounted" #: ../udiskie/notify.py:81 #, fuzzy, python-brace-format msgid "{0.ui_label} unmounted" msgstr "{0.id_label} unmounted" #: ../udiskie/notify.py:92 msgid "Device locked" msgstr "Device locked" #: ../udiskie/notify.py:93 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "{0.device_presentation} locked" #: ../udiskie/notify.py:104 msgid "Device unlocked" msgstr "Device unlocked" #: ../udiskie/notify.py:105 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "{0.device_presentation} unlocked" #: ../udiskie/notify.py:140 msgid "Device added" msgstr "Device added" #: ../udiskie/notify.py:141 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "device appeared on {0.device_presentation}" #: ../udiskie/notify.py:162 msgid "Device removed" msgstr "Device removed" #: ../udiskie/notify.py:163 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "device disappeared on {0.device_presentation}" #: ../udiskie/notify.py:174 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" "failed to {0} {1}:\n" "{2}" #: ../udiskie/notify.py:176 #, python-brace-format msgid "failed to {0} device {1}." msgstr "failed to {0} device {1}." #: ../udiskie/notify.py:182 msgid "Retry" msgstr "Retry" #: ../udiskie/notify.py:185 msgid "Job failed" msgstr "Job failed" #: ../udiskie/notify.py:216 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" #: ../udiskie/cli.py:55 #, fuzzy msgid "" "Failed to connect UDisks2 dbus service..\n" "Falling back to UDisks1." msgstr "" "Failed to connect UDisks1 dbus service..\n" "Falling back to UDisks2 [experimental]." #: ../udiskie/cli.py:65 #, python-brace-format msgid "UDisks version not supported: {0}!" msgstr "UDisks version not supported: {0}!" #: ../udiskie/cli.py:80 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "These options are mutually exclusive: {0}" #: ../udiskie/cli.py:148 msgid "" "\n" " Note, that the options in the individual groups are mutually exclusive.\n" "\n" " The config file can be a JSON or preferrably a YAML file. For an\n" " example, see the MAN page (or doc/udiskie.8.txt in the repository).\n" " " msgstr "" "\n" " Note, that the options in the individual groups are mutually exclusive.\n" "\n" " The config file can be a JSON or preferrably a YAML file. For an\n" " example, see the MAN page (or doc/udiskie.8.txt in the repository).\n" " " #: ../udiskie/cli.py:168 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "%(levelname)s [%(asctime)s] %(name)s: %(message)s" #: ../udiskie/cli.py:170 #, python-format msgid "%(message)s" msgstr "%(message)s" #: ../udiskie/cli.py:358 msgid "" "Typelib for 'libnotify' is not available. Possible causes include:\n" "\t- libnotify is not installed\n" "\t- the typelib is provided by a separate package\n" "\t- libnotify was built with introspection disabled\n" "\n" "Starting udiskie without notifications." msgstr "" #: ../udiskie/cli.py:368 msgid "" "Typelib for 'Gtk 3.0' is not available. Possible causes include:\n" "\t- GTK3 is not installed\n" "\t- the typelib is provided by a separate package\n" "\t- GTK3 was built with introspection disabled\n" "\n" "Starting udiskie without tray icon." msgstr "" #~ msgid "Device not found: {0}" #~ msgstr "Device not found: {0}" #~ msgid "Interface {0!r} not available for {1}" #~ msgstr "Interface {0!r} not available for {1}" udiskie-1.4.9/lang/udiskie.pot0000644000175000001440000002174312642325627016756 0ustar thomasusers00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-01-03 23:56+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../udiskie/prompt.py:117 ../udiskie/prompt.py:128 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "" #: ../udiskie/prompt.py:199 msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "b' option." msgstr "" #: ../udiskie/udisks1.py:455 ../udiskie/udisks2.py:548 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "" #: ../udiskie/udisks1.py:458 ../udiskie/udisks2.py:551 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "" #: ../udiskie/udisks1.py:517 ../udiskie/udisks2.py:620 #, python-brace-format msgid "+++ {0}: {1}" msgstr "" #: ../udiskie/udisks1.py:633 #, python-brace-format msgid "{0} operation failed for device: {1}" msgstr "" #: ../udiskie/config.py:91 msgid "Unknown matching attribute: {!r}" msgstr "" #: ../udiskie/config.py:94 #, python-brace-format msgid "{0} created" msgstr "" #: ../udiskie/config.py:97 msgid "{0}(match={1!r}, value={2!r})" msgstr "" #: ../udiskie/config.py:120 #, python-brace-format msgid "{0} used for {1}" msgstr "" #: ../udiskie/config.py:217 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:220 msgid "Failed to read {0!r}: {1}" msgstr "" #: ../udiskie/depend.py:37 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:43 msgid "X server not connected!" msgstr "" #: ../udiskie/mount.py:131 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "" #: ../udiskie/mount.py:147 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "" #: ../udiskie/mount.py:150 #, python-brace-format msgid "not browsing {0}: no program" msgstr "" #: ../udiskie/mount.py:152 msgid "opening {0} on {0.mount_paths[0]}" msgstr "" #: ../udiskie/mount.py:154 msgid "opened {0} on {0.mount_paths[0]}" msgstr "" #: ../udiskie/mount.py:169 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:172 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "" #: ../udiskie/mount.py:177 #, python-brace-format msgid "mounting {0} with {1}" msgstr "" #: ../udiskie/mount.py:179 #, python-brace-format msgid "mounted {0} on {1}" msgstr "" #: ../udiskie/mount.py:193 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:196 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "" #: ../udiskie/mount.py:198 #, python-brace-format msgid "unmounting {0}" msgstr "" #: ../udiskie/mount.py:200 #, python-brace-format msgid "unmounted {0}" msgstr "" #: ../udiskie/mount.py:215 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:218 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "" #: ../udiskie/mount.py:221 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "" #: ../udiskie/mount.py:228 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "" #: ../udiskie/mount.py:230 #, python-brace-format msgid "unlocking {0}" msgstr "" #: ../udiskie/mount.py:233 #, python-brace-format msgid "unlocked {0}" msgstr "" #: ../udiskie/mount.py:244 #, python-brace-format msgid "unlocking {0} using cached password" msgstr "" #: ../udiskie/mount.py:248 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:250 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:275 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:278 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "" #: ../udiskie/mount.py:280 #, python-brace-format msgid "locking {0}" msgstr "" #: ../udiskie/mount.py:282 #, python-brace-format msgid "locked {0}" msgstr "" #: ../udiskie/mount.py:314 ../udiskie/mount.py:352 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:389 ../udiskie/mount.py:440 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:464 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:468 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "" #: ../udiskie/mount.py:472 #, python-brace-format msgid "ejecting {0}" msgstr "" #: ../udiskie/mount.py:474 #, python-brace-format msgid "ejected {0}" msgstr "" #: ../udiskie/mount.py:489 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:493 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "" #: ../udiskie/mount.py:497 #, python-brace-format msgid "detaching {0}" msgstr "" #: ../udiskie/mount.py:499 #, python-brace-format msgid "detached {0}" msgstr "" #: ../udiskie/mount.py:605 #, python-brace-format msgid "Browse {0}" msgstr "" #: ../udiskie/mount.py:606 #, python-brace-format msgid "Mount {0}" msgstr "" #: ../udiskie/mount.py:607 #, python-brace-format msgid "Unmount {0}" msgstr "" #: ../udiskie/mount.py:608 #, python-brace-format msgid "Unlock {0}" msgstr "" #: ../udiskie/mount.py:609 #, python-brace-format msgid "Lock {0}" msgstr "" #: ../udiskie/mount.py:610 #, python-brace-format msgid "Eject {0}" msgstr "" #: ../udiskie/mount.py:611 #, python-brace-format msgid "Unpower {0}" msgstr "" #: ../udiskie/mount.py:612 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/tray.py:84 msgid "Quit" msgstr "" #: ../udiskie/tray.py:174 msgid "Invalid node!" msgstr "" #: ../udiskie/tray.py:295 msgid "udiskie" msgstr "" #: ../udiskie/notify.py:63 msgid "Browse directory" msgstr "" #: ../udiskie/notify.py:67 msgid "Device mounted" msgstr "" #: ../udiskie/notify.py:68 msgid "{0.ui_label} mounted on {0.mount_paths[0]}" msgstr "" #: ../udiskie/notify.py:80 msgid "Device unmounted" msgstr "" #: ../udiskie/notify.py:81 #, python-brace-format msgid "{0.ui_label} unmounted" msgstr "" #: ../udiskie/notify.py:92 msgid "Device locked" msgstr "" #: ../udiskie/notify.py:93 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "" #: ../udiskie/notify.py:104 msgid "Device unlocked" msgstr "" #: ../udiskie/notify.py:105 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "" #: ../udiskie/notify.py:140 msgid "Device added" msgstr "" #: ../udiskie/notify.py:141 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "" #: ../udiskie/notify.py:162 msgid "Device removed" msgstr "" #: ../udiskie/notify.py:163 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "" #: ../udiskie/notify.py:174 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" #: ../udiskie/notify.py:176 #, python-brace-format msgid "failed to {0} device {1}." msgstr "" #: ../udiskie/notify.py:182 msgid "Retry" msgstr "" #: ../udiskie/notify.py:185 msgid "Job failed" msgstr "" #: ../udiskie/notify.py:216 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" #: ../udiskie/cli.py:55 msgid "" "Failed to connect UDisks2 dbus service..\n" "Falling back to UDisks1." msgstr "" #: ../udiskie/cli.py:65 #, python-brace-format msgid "UDisks version not supported: {0}!" msgstr "" #: ../udiskie/cli.py:80 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "" #: ../udiskie/cli.py:148 msgid "" "\n" " Note, that the options in the individual groups are mutually exclusive.\n" "\n" " The config file can be a JSON or preferrably a YAML file. For an\n" " example, see the MAN page (or doc/udiskie.8.txt in the repository).\n" " " msgstr "" #: ../udiskie/cli.py:168 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "" #: ../udiskie/cli.py:170 #, python-format msgid "%(message)s" msgstr "" #: ../udiskie/cli.py:358 msgid "" "Typelib for 'libnotify' is not available. Possible causes include:\n" "\t- libnotify is not installed\n" "\t- the typelib is provided by a separate package\n" "\t- libnotify was built with introspection disabled\n" "\n" "Starting udiskie without notifications." msgstr "" #: ../udiskie/cli.py:368 msgid "" "Typelib for 'Gtk 3.0' is not available. Possible causes include:\n" "\t- GTK3 is not installed\n" "\t- the typelib is provided by a separate package\n" "\t- GTK3 was built with introspection disabled\n" "\n" "Starting udiskie without tray icon." msgstr "" udiskie-1.4.9/lang/es_ES.po0000644000175000001440000002730712642325627016135 0ustar thomasusers00000000000000msgid "" msgstr "" "Project-Id-Version: udiskie\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-01-03 23:56+0100\n" "PO-Revision-Date: 2014-07-23 18:43+0100\n" "Last-Translator: Alejandro Pérez \n" "Language-Team: Spanish alejandro.perez.mendez@gmail.com\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.5\n" #: ../udiskie/prompt.py:117 ../udiskie/prompt.py:128 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "Introduce la clave para {0.device_presentation}: " #: ../udiskie/prompt.py:199 msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "b' option." msgstr "" "No se encontró el gestor de ficheros: {0!r}. Puede que necesites cambiar el " "valor de la opción '-b'." #: ../udiskie/udisks1.py:455 ../udiskie/udisks2.py:548 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "Se encontró el dispositivo maestro \"{0}\": \"{1}\"" #: ../udiskie/udisks1.py:458 ../udiskie/udisks2.py:551 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "no se encontró dispositivo maestro para \"{0}\"" #: ../udiskie/udisks1.py:517 ../udiskie/udisks2.py:620 #, python-brace-format msgid "+++ {0}: {1}" msgstr "+++ {0}: {1}" #: ../udiskie/udisks1.py:633 #, python-brace-format msgid "{0} operation failed for device: {1}" msgstr "Falló la operación {0} para el dispositovo: {1}" #: ../udiskie/config.py:91 msgid "Unknown matching attribute: {!r}" msgstr "Atributo de filtrado desconocido: {!r}" #: ../udiskie/config.py:94 #, python-brace-format msgid "{0} created" msgstr "{0} creado" #: ../udiskie/config.py:97 msgid "{0}(match={1!r}, value={2!r})" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:120 #, python-brace-format msgid "{0} used for {1}" msgstr "{0} usado para {1}" #: ../udiskie/config.py:217 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:220 #, fuzzy msgid "Failed to read {0!r}: {1}" msgstr "Fallo al {0} {1}: {2}" #: ../udiskie/depend.py:37 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:43 msgid "X server not connected!" msgstr "¡Servidor X no conectado!" #: ../udiskie/mount.py:131 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "Fallo al {0} {1}: {2}" #: ../udiskie/mount.py:147 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "no se exploró {0}: no está montado" #: ../udiskie/mount.py:150 #, python-brace-format msgid "not browsing {0}: no program" msgstr "no se exploró {0}: no hay programa configurado" #: ../udiskie/mount.py:152 msgid "opening {0} on {0.mount_paths[0]}" msgstr "abriendo {0} en {0.mount_paths[0]}" #: ../udiskie/mount.py:154 msgid "opened {0} on {0.mount_paths[0]}" msgstr "se abrió {0} en {0.mount_paths[0]}" #: ../udiskie/mount.py:169 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "no se montó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:172 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "no se montó {0}: ya está montado" #: ../udiskie/mount.py:177 #, python-brace-format msgid "mounting {0} with {1}" msgstr "montando {0} en {1}" #: ../udiskie/mount.py:179 #, python-brace-format msgid "mounted {0} on {1}" msgstr "montado {0} en {1}" #: ../udiskie/mount.py:193 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "no se desmontó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:196 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "no se desmontó {0}: no estaba montado" #: ../udiskie/mount.py:198 #, python-brace-format msgid "unmounting {0}" msgstr "desmontando {0}" #: ../udiskie/mount.py:200 #, python-brace-format msgid "unmounted {0}" msgstr "desmontado {0}" #: ../udiskie/mount.py:215 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "no se desbloqueó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:218 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "no se desbloqueó {0}: ya está desbloqueado" #: ../udiskie/mount.py:221 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:228 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "no se desbloqueó {0}: cancellado por el usuario" #: ../udiskie/mount.py:230 #, python-brace-format msgid "unlocking {0}" msgstr "desbloqueando {0}" #: ../udiskie/mount.py:233 #, python-brace-format msgid "unlocked {0}" msgstr "desbloqueado {0}" #: ../udiskie/mount.py:244 #, fuzzy, python-brace-format msgid "unlocking {0} using cached password" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:248 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:250 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:275 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "no se bloqueó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:278 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "no se bloqueó {0}: no estaba desbloqueado" #: ../udiskie/mount.py:280 #, python-brace-format msgid "locking {0}" msgstr "bloqueando {0}" #: ../udiskie/mount.py:282 #, python-brace-format msgid "locked {0}" msgstr "bloqueado {0}" #: ../udiskie/mount.py:314 ../udiskie/mount.py:352 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "no se añadió {0}: dispositivo no gestionado" #: ../udiskie/mount.py:389 ../udiskie/mount.py:440 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "no se eliminó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:464 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "no se expulsó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:468 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "no se expulsó {0}: dispositivo no expulsable" #: ../udiskie/mount.py:472 #, python-brace-format msgid "ejecting {0}" msgstr "expulsando {0}" #: ../udiskie/mount.py:474 #, python-brace-format msgid "ejected {0}" msgstr "expulsado {0}" #: ../udiskie/mount.py:489 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "no se desconectó {0}: unhandled device" #: ../udiskie/mount.py:493 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "no se desconectó {0}: dispositivo no desconectable" #: ../udiskie/mount.py:497 #, python-brace-format msgid "detaching {0}" msgstr "desconectando {0}" #: ../udiskie/mount.py:499 #, python-brace-format msgid "detached {0}" msgstr "desconectado {0}" #: ../udiskie/mount.py:605 #, python-brace-format msgid "Browse {0}" msgstr "Explorar {0}" #: ../udiskie/mount.py:606 #, python-brace-format msgid "Mount {0}" msgstr "Montar {0}" #: ../udiskie/mount.py:607 #, python-brace-format msgid "Unmount {0}" msgstr "Desmontar {0}" #: ../udiskie/mount.py:608 #, python-brace-format msgid "Unlock {0}" msgstr "Desbloquear {0}" #: ../udiskie/mount.py:609 #, python-brace-format msgid "Lock {0}" msgstr "Bloquear {0}" #: ../udiskie/mount.py:610 #, python-brace-format msgid "Eject {0}" msgstr "Expulsar {0}" #: ../udiskie/mount.py:611 #, python-brace-format msgid "Unpower {0}" msgstr "Apagar {0}" #: ../udiskie/mount.py:612 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/tray.py:84 msgid "Quit" msgstr "Salir" #: ../udiskie/tray.py:174 msgid "Invalid node!" msgstr "¡Nodo inválido!" #: ../udiskie/tray.py:295 msgid "udiskie" msgstr "udiskie" #: ../udiskie/notify.py:63 msgid "Browse directory" msgstr "Navegar directorio" #: ../udiskie/notify.py:67 msgid "Device mounted" msgstr "Dispositivo montado" #: ../udiskie/notify.py:68 #, fuzzy msgid "{0.ui_label} mounted on {0.mount_paths[0]}" msgstr "{0.ui_label} montado en {0.mount_paths[0]}" #: ../udiskie/notify.py:80 msgid "Device unmounted" msgstr "Dispositivo desmontado" #: ../udiskie/notify.py:81 #, fuzzy, python-brace-format msgid "{0.ui_label} unmounted" msgstr "{0.ui_label} desmontado" #: ../udiskie/notify.py:92 msgid "Device locked" msgstr "Dispositivo bloqueado" #: ../udiskie/notify.py:93 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "{0.device_presentation} bloqueado" #: ../udiskie/notify.py:104 msgid "Device unlocked" msgstr "Dispositivo desbloqueado" #: ../udiskie/notify.py:105 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "{0.device_presentation} desbloqueado" #: ../udiskie/notify.py:140 msgid "Device added" msgstr "Dispositivo añadido" #: ../udiskie/notify.py:141 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "Dispositivo apareció en {0.device_presentation}" #: ../udiskie/notify.py:162 msgid "Device removed" msgstr "Dispositivo retirado" #: ../udiskie/notify.py:163 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "el dispositivo despareció en {0.device_presentation} " #: ../udiskie/notify.py:174 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" "fallo al {0} {1}:\n" "{2}" #: ../udiskie/notify.py:176 #, python-brace-format msgid "failed to {0} device {1}." msgstr "fallo al {0} el dispositivo {1}." #: ../udiskie/notify.py:182 msgid "Retry" msgstr "Reintentar" #: ../udiskie/notify.py:185 msgid "Job failed" msgstr "Falló la tarea." #: ../udiskie/notify.py:216 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" #: ../udiskie/cli.py:55 #, fuzzy msgid "" "Failed to connect UDisks2 dbus service..\n" "Falling back to UDisks1." msgstr "" "Fallo al conectar al servicio dbus UDisks1.\n" "Usando UDisk2 [experimental]." #: ../udiskie/cli.py:65 #, python-brace-format msgid "UDisks version not supported: {0}!" msgstr "¡Versión de UDisks no soportada: {0}!" #: ../udiskie/cli.py:80 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "Estas opciones son excluyentes: {0}" #: ../udiskie/cli.py:148 #, fuzzy msgid "" "\n" " Note, that the options in the individual groups are mutually exclusive.\n" "\n" " The config file can be a JSON or preferrably a YAML file. For an\n" " example, see the MAN page (or doc/udiskie.8.txt in the repository).\n" " " msgstr "" "\n" " Nótese que las opciones de los grupos individuales son excluyentes.\n" "\n" " El fichero de configuración puede ser un fichero JSON o YAML " "(preferiblemente).\n" " Para un ejemplo, refiérase a la pagína del manual (o doc/udiskie.8.txt " "in the repository).\n" #: ../udiskie/cli.py:168 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "%(levelname)s [%(asctime)s] %(name)s: %(message)s" #: ../udiskie/cli.py:170 #, python-format msgid "%(message)s" msgstr "%(message)s" #: ../udiskie/cli.py:358 msgid "" "Typelib for 'libnotify' is not available. Possible causes include:\n" "\t- libnotify is not installed\n" "\t- the typelib is provided by a separate package\n" "\t- libnotify was built with introspection disabled\n" "\n" "Starting udiskie without notifications." msgstr "" #: ../udiskie/cli.py:368 msgid "" "Typelib for 'Gtk 3.0' is not available. Possible causes include:\n" "\t- GTK3 is not installed\n" "\t- the typelib is provided by a separate package\n" "\t- GTK3 was built with introspection disabled\n" "\n" "Starting udiskie without tray icon." msgstr "" #~ msgid "Device not found: {0}" #~ msgstr "Dispositivo no encontrado: {0}" #~ msgid "Interface {0!r} not available for {1}" #~ msgstr "Interfaz {0!r} no disponible para {1}" udiskie-1.4.9/README.rst0000644000175000001440000000616012651464174015340 0ustar thomasusers00000000000000======= udiskie ======= |Version| |Downloads| |License| *udiskie* is a UDisks_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. Its features include: - automount removable media when inserted - notifications (on insertion, mount, unmount, …) - GTK tray icon to manage all available devices - command line tools for manual un-/mounting - support for LUKS encrypted devices - password caching - works with either udisks1 or udisks2 - an extensible code base (python) - a maintainer who is open for suggestions;) All features can be indidually enabled or disabled (yes, you can submit unmaintainable code and make me salty!) .. _UDisks: http://www.freedesktop.org/wiki/Software/udisks Documentation ~~~~~~~~~~~~~ - Usage_ - Permissions_ - Installation_ Miscellaneous: - `Custom mount paths`_ - `Acquiring debug information`_ .. _Usage: https://github.com/coldfix/udiskie/wiki/Usage .. _Permissions: https://github.com/coldfix/udiskie/wiki/Permissions .. _Installation: https://github.com/coldfix/udiskie/wiki/Installation .. _Custom mount paths: https://github.com/coldfix/udiskie/wiki/Custom-mount-paths .. _Acquiring debug information: https://github.com/coldfix/udiskie/wiki/Debugging-a-problem Project pages ~~~~~~~~~~~~~ The… - `Wiki`_ contains installation instructions and additional information. - `Man page`_ describes the command line options - `Source Code`_ is hosted on github. - `Latest Release`_ is available for download on PyPI. - `Issue Tracker`_ is the right place to report any issues you encounter, ask general questions or suggest new features. There is also a public `Mailing List`_ if you prefer email. .. _Wiki: https://github.com/coldfix/udiskie/wiki .. _Man Page: https://raw.githubusercontent.com/coldfix/udiskie/master/doc/udiskie.8.txt .. _Source Code: https://github.com/coldfix/udiskie .. _Latest Release: https://pypi.python.org/pypi/udiskie/ .. _Issue Tracker: https://github.com/coldfix/udiskie/issues .. _Mailing List: https://lists.coldfix.de/mailman/listinfo/udiskie Roadmap ~~~~~~~ For the next udiskie versions, I am mainly concerned with quality assurance and stability. For one this means reducing the number of supported runtime configurations and make the remaining easier to test, i.e.: - **drop support for python2** to avoid unicode issues and make use of the new asyncio module which provides better error handling (stack traces!) than the current solution. - **drop support for udisks1**. The udisks1 API is rather unfit for the asynchronous nature of the problem which has led to numerous bugs and problems (plenty more probably waiting to be discovered as we speak) - **add automated tests**. needed desperately… .. |Version| image:: http://coldfix.de:8080/v/udiskie/badge.svg :target: https://pypi.python.org/pypi/udiskie/ :alt: Latest Version .. |Downloads| image:: http://coldfix.de:8080/d/udiskie/badge.svg :target: https://pypi.python.org/pypi/udiskie#downloads :alt: Downloads .. |License| image:: http://coldfix.de:8080/license/udiskie/badge.svg :target: https://github.com/coldfix/udiskie/blob/master/COPYING :alt: License udiskie-1.4.9/COPYING0000644000175000017500000000214012642325724015031 0ustar thomasthomas00000000000000Copyright (c) 2010-2012 Byron Clark (c) 2013-2016 Thomas Gläßle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. udiskie-1.4.9/icons/0000755000175000001440000000000012677741056014766 5ustar thomasusers00000000000000udiskie-1.4.9/icons/scalable/0000755000175000001440000000000012677741056016534 5ustar thomasusers00000000000000udiskie-1.4.9/icons/scalable/actions/0000755000175000001440000000000012677741056020174 5ustar thomasusers00000000000000udiskie-1.4.9/icons/scalable/actions/udiskie-unlock.svg0000644000175000017500000002626512266540464024006 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.4.9/icons/scalable/actions/udiskie-eject.svg0000644000175000017500000002244512266540464023601 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.4.9/icons/scalable/actions/udiskie-detach.svg0000644000175000017500000001325712266540464023740 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.4.9/icons/scalable/actions/udiskie-lock.svg0000644000175000017500000002625412266540464023441 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.4.9/icons/scalable/actions/udiskie-unmount.svg0000644000175000017500000010147312266540464024213 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.4.9/icons/scalable/actions/udiskie-mount.svg0000644000175000017500000007163212266540464023653 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.4.9/udiskie.egg-info/0000755000175000001440000000000012677741056017002 5ustar thomasusers00000000000000udiskie-1.4.9/udiskie.egg-info/SOURCES.txt0000644000175000017500000000165112677741056021027 0ustar thomasthomas00000000000000CHANGES.rst CONTRIBUTORS COPYING MANIFEST.in README.rst setup.py doc/Makefile doc/asciidoc.conf doc/udiskie.8.txt icons/scalable/actions/udiskie-detach.svg icons/scalable/actions/udiskie-eject.svg icons/scalable/actions/udiskie-lock.svg icons/scalable/actions/udiskie-mount.svg icons/scalable/actions/udiskie-unlock.svg icons/scalable/actions/udiskie-unmount.svg lang/en_US.po lang/es_ES.po lang/udiskie.pot test/test_cache.py test/test_match.py udiskie/__init__.py udiskie/async_.py udiskie/automount.py udiskie/cache.py udiskie/cli.py udiskie/common.py udiskie/compat.py udiskie/config.py udiskie/dbus.py udiskie/depend.py udiskie/locale.py udiskie/mount.py udiskie/notify.py udiskie/prompt.py udiskie/tray.py udiskie/udisks1.py udiskie/udisks2.py udiskie.egg-info/PKG-INFO udiskie.egg-info/SOURCES.txt udiskie.egg-info/dependency_links.txt udiskie.egg-info/entry_points.txt udiskie.egg-info/requires.txt udiskie.egg-info/top_level.txtudiskie-1.4.9/udiskie.egg-info/dependency_links.txt0000644000175000017500000000000112677741053023203 0ustar thomasthomas00000000000000 udiskie-1.4.9/udiskie.egg-info/top_level.txt0000644000175000017500000000001012677741053021656 0ustar thomasthomas00000000000000udiskie udiskie-1.4.9/udiskie.egg-info/requires.txt0000644000175000017500000000005612677741053021536 0ustar thomasthomas00000000000000PyYAML docopt [password-cache] keyutils==0.3 udiskie-1.4.9/udiskie.egg-info/PKG-INFO0000644000175000017500000003376212677741053020245 0ustar thomasthomas00000000000000Metadata-Version: 1.1 Name: udiskie Version: 1.4.9 Summary: Removable disk automounter for udisks Home-page: https://github.com/coldfix/udiskie Author: Thomas Gläßle Author-email: t_glaessle@gmx.de License: MIT Description: ======= udiskie ======= |Version| |Downloads| |License| *udiskie* is a UDisks_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. Its features include: - automount removable media when inserted - notifications (on insertion, mount, unmount, …) - GTK tray icon to manage all available devices - command line tools for manual un-/mounting - support for LUKS encrypted devices - password caching - works with either udisks1 or udisks2 - an extensible code base (python) - a maintainer who is open for suggestions;) All features can be indidually enabled or disabled (yes, you can submit unmaintainable code and make me salty!) .. _UDisks: http://www.freedesktop.org/wiki/Software/udisks Documentation ~~~~~~~~~~~~~ - Usage_ - Permissions_ - Installation_ Miscellaneous: - `Custom mount paths`_ - `Acquiring debug information`_ .. _Usage: https://github.com/coldfix/udiskie/wiki/Usage .. _Permissions: https://github.com/coldfix/udiskie/wiki/Permissions .. _Installation: https://github.com/coldfix/udiskie/wiki/Installation .. _Custom mount paths: https://github.com/coldfix/udiskie/wiki/Custom-mount-paths .. _Acquiring debug information: https://github.com/coldfix/udiskie/wiki/Debugging-a-problem Project pages ~~~~~~~~~~~~~ The… - `Wiki`_ contains installation instructions and additional information. - `Man page`_ describes the command line options - `Source Code`_ is hosted on github. - `Latest Release`_ is available for download on PyPI. - `Issue Tracker`_ is the right place to report any issues you encounter, ask general questions or suggest new features. There is also a public `Mailing List`_ if you prefer email. .. _Wiki: https://github.com/coldfix/udiskie/wiki .. _Man Page: https://raw.githubusercontent.com/coldfix/udiskie/master/doc/udiskie.8.txt .. _Source Code: https://github.com/coldfix/udiskie .. _Latest Release: https://pypi.python.org/pypi/udiskie/ .. _Issue Tracker: https://github.com/coldfix/udiskie/issues .. _Mailing List: https://lists.coldfix.de/mailman/listinfo/udiskie Roadmap ~~~~~~~ For the next udiskie versions, I am mainly concerned with quality assurance and stability. For one this means reducing the number of supported runtime configurations and make the remaining easier to test, i.e.: - **drop support for python2** to avoid unicode issues and make use of the new asyncio module which provides better error handling (stack traces!) than the current solution. - **drop support for udisks1**. The udisks1 API is rather unfit for the asynchronous nature of the problem which has led to numerous bugs and problems (plenty more probably waiting to be discovered as we speak) - **add automated tests**. needed desperately… .. |Version| image:: http://coldfix.de:8080/v/udiskie/badge.svg :target: https://pypi.python.org/pypi/udiskie/ :alt: Latest Version .. |Downloads| image:: http://coldfix.de:8080/d/udiskie/badge.svg :target: https://pypi.python.org/pypi/udiskie#downloads :alt: Downloads .. |License| image:: http://coldfix.de:8080/license/udiskie/badge.svg :target: https://github.com/coldfix/udiskie/blob/master/COPYING :alt: License CHANGELOG --------- 1.4.9 ~~~~~ Date: 02.04.2016 - add is_loop and loop_file properties for devices - fix recursive mounting of crypto devices (udiskie-mount) - prevent empty submenus from showing 1.4.8 ~~~~~ Date: 09.02.2016 - fix problem with setupscript if utf8 is not the default encoding - fix crash when starting without X - basic support for loop devices (must be enabled explicitly at this time) - fix handling of 2 more error cases 1.4.7 ~~~~~ Date: 04.01.2016 - fix typo that prevents the yaml config file from being used - fix problem with glib/gio gir API on slackware (olders versions?) - fix bug when changing device state (e.g. when formatting existing device or burning ISO file to device) - improve handling of race conditions with udisks1 backend - fix notifications for devices without labels 1.4.6 ~~~~~ Date: 28.12.2015 - cleanup recent bugfixes - close some gates for more py2/unicode related bugs 1.4.5 ~~~~~ Date: 24.12.2015 - fix another bug with unicode data on command line (py2) - slightly improve stack traces in async code - further decrease verbosity while removing devices 1.4.4 ~~~~~ Date: 24.12.2015 - fix too narrow dependency enforcement - make udiskie slightly less verbose in default mode 1.4.3 ~~~~~ Date: 24.12.2015 - fix bug with unicode data on python2 - fix bug due to event ordering in udisks1 - fix bug due to inavailability of device data at specific time 1.4.2 ~~~~~ Date: 22.12.2015 - fix regression in get_password_tty 1.4.1 ~~~~~ Date: 19.12.2015 - fix problem in SmartTray due to recent transition to async 1.4.0 ~~~~~ Date: 19.12.2015 - go async (with self-made async module for now, until gbulb becomes ready) - specify GTK/Notify versions to be imported (hence fix warnings and a problem for the tray icon resulting from accidentally importing GTK2) - add optional password caching 1.3.2 ~~~~~ - revert "respect the automount flag for devices" - make dependency on Gtk optional 1.3.1 ~~~~~ - use icon hints from udev settings in notifications - respect the automount flag for devices - don't fail if libnotify is not available 1.3.0 ~~~~~ - add actions to "Device added" notification - allow to configure which actions should be added to notifications 1.2.1 ~~~~~ - fix unicode issue in setup script - update license/copyright notices 1.2.0 ~~~~~ - use UDisks2 by default - add --password-prompt command line argument and config file entry 1.1.3 ~~~~~ - fix password prompt for GTK2 (tray is still broken for GTK2) - fix minor documentation issues 1.1.2 ~~~~~ - add key ``device_id`` for matching devices rather than only file systems - improve documentation regarding dependencies 1.1.1 ~~~~~ - fix careless error in man page 1.1.0 ~~~~~ - implemented internationalization - added spanish translation - allow to choose icons from a configurable list 1.0.4 ~~~~~ - compatibility with older version of pygobject (e.g. in Slackware 14.1) 1.0.3 ~~~~~ - handle exception if no notification service is installed 1.0.2 ~~~~~ - fix crash when calling udiskie mount/unmount utilites without udisks1 installed 1.0.1 ~~~~~ - fix crash when calling udiskie without having udisks1 installed (regression) 1.0.0 ~~~~~ - port to PyGObject, removing dependencies on pygtk, zenity, dbus-python, python-notify - use a PyGObject based password dialog - remove --password-prompt parameter - rename command line parameters - add negations for all command line parameters 0.8.0 ~~~~~ - remove the '--filters' parameter for good - change config format to YAML - change default config path to $XDG_CONFIG_HOME/udiskie/config.yml - separate ignore filters from mount option filters - allow to match multiple attributes against a device (AND-wise) - allow to overwrite udiskies default handleability settings - raise exception if --config file doesn't exist - add --options parameter for udiskie-mount - simplify local installations 0.7.0 ~~~~~ There are some backward incompatible changes, hence the version break: - command line parameter '-f'/'--filters' renamed to '-C'/'--config' - add sections in config file to disable individual mount notifications and set defaults for some program options (udisks version, prompt, etc) - refactor ``udiskie.cli``, ``udiskie.config`` and ``udiskie.tray`` - revert 'make udiskie a namespace package' - add 'Browse folder' action to tray menu - add 'Browse folder' action button to mount notifications - add '--no-automounter' command line option to disable automounting - add '--auto-tray' command line option to use a tray icon that automatically disappears when no actions are available - show notifications when devices dis-/appear (can be disabled via config file) - show 'id_label' in tray menu, if available (instead of mount path or device path) - add 'Job failed' notifications - add 'Retry' button to failed notifications - remove automatic retries to unlock LUKS partitions - pass only device name to external password prompt - add '--quiet' command line option - ignore devices ignored by udev rules 0.6.4 ~~~~~ - fix logging in setup.py - more verbose log messages (with time) when having -v on - fix mounting devices that are added as 'external' and later changed to 'internal' [udisks1] (applies to LUKS devices that are opened by an udev rule for example) 0.6.3 (bug fix) ~~~~~~~~~~~~~~~ - fix exception in Mounter.detach_device if unable to detach - fix force-detach for UDisks2 backend - automatically use UDisks2 if UDisks1 is not available - mount unlocked devices only once, removes error message on UDisks2 - mention __ignore__ in man page 0.6.2 (aesthetic) ~~~~~~~~~~~~~~~~~ - add custom icons for the context menu of the system tray widget 0.6.1 (bug fix) ~~~~~~~~~~~~~~~ - fix udisks2 external device detection bug: all devices were considered external when using ``Sniffer`` (as done in the udiskie-mount and udiskie-umount tools) 0.6.0 (udisks2 support, bug fix) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - cache device states to avoid some race conditions - show filesystem label in mount/unmount notifications - retry to unlock LUKS devices when wrong password was entered twice - show 'eject' only if media is available (udisks1 ejects only in this case) - (un-) mount/lock notifications shown even when operations failed - refactor internal API - experimental support for udisks2 0.5.3 (feature, bug fix) ~~~~~~~~~~~~~~~~~~~~~~~~ - add '__ignore__' config file option to prevent handling specific devices - delay notifications until termination of long operations 0.5.2 (tray icon) ~~~~~~~~~~~~~~~~~ - add tray icon (pygtk based) - eject / detach drives from command line 0.5.1 (mainly internal changes) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - use setuptools entry points to create the executables - make udiskie a namespace package 0.5.0 (LUKS support) ~~~~~~~~~~~~~~~~~~~~ - support for LUKS devices (using zenity for password prompt) - major refactoring - use setuptools as installer Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: X11 Applications :: GTK Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Desktop Environment Classifier: Topic :: Software Development Classifier: Topic :: System :: Filesystems Classifier: Topic :: System :: Hardware Classifier: Topic :: Utilities udiskie-1.4.9/udiskie.egg-info/entry_points.txt0000644000175000017500000000020512677741053022430 0ustar thomasthomas00000000000000[console_scripts] udiskie = udiskie.cli:Daemon.main udiskie-mount = udiskie.cli:Mount.main udiskie-umount = udiskie.cli:Umount.main udiskie-1.4.9/udiskie/0000755000175000001440000000000012677741056015310 5ustar thomasusers00000000000000udiskie-1.4.9/udiskie/cli.py0000644000175000001440000004457512640243465016437 0ustar thomasusers00000000000000""" Command line interface logic. The application classes in this module are installed as executables via setuptools entry points. """ from __future__ import absolute_import from __future__ import unicode_literals # import udiskie.depend first - for side effects! from .depend import has_Notify, has_Gtk import inspect import logging import traceback from docopt import docopt, DocoptExit from gi.repository import GLib import udiskie import udiskie.config import udiskie.mount from .async_ import AsyncList, Coroutine, Return, RunForever from .common import extend, str2unicode from .locale import _ __all__ = [ 'Daemon', 'Mount', 'Umount', ] @Coroutine.from_generator_function def get_backend(version=None): """ Return UDisks service. :param int version: requested UDisks backend version :returns: UDisks service wrapper object :raises GLib.GError: if unable to connect to UDisks dbus service. :raises ValueError: if the version is invalid If ``version`` has a false truth value, try to connect to UDisks1 and fall back to UDisks2 if not available. """ if not version: try: daemon = yield get_backend(2) except GLib.GError: log = logging.getLogger(__name__) log.warning(_('Failed to connect UDisks2 dbus service..\n' 'Falling back to UDisks1.')) daemon = yield get_backend(1) elif version == 1: import udiskie.udisks1 daemon = yield udiskie.udisks1.Daemon.create() elif version == 2: import udiskie.udisks2 daemon = yield udiskie.udisks2.Daemon.create() else: raise ValueError(_("UDisks version not supported: {0}!", version)) yield Return(daemon) class Choice(object): """Mapping of command line arguments to option values.""" def __init__(self, mapping): """Set mapping between arguments and values.""" self._mapping = mapping def _check(self, args): """Exit in case of multiple exclusive arguments.""" if sum(bool(args[arg]) for arg in self._mapping) > 1: raise DocoptExit(_('These options are mutually exclusive: {0}', ', '.join(self._mapping))) def __call__(self, args): """Get the option value from the parsed arguments.""" self._check(args) for arg, val in self._mapping.items(): if args[arg] not in (None, False): return val def Switch(name): """Negatable option.""" return Choice({'--' + name: True, '--no-' + name: False}) class Value(object): """Option which is given as value of a command line argument.""" def __init__(self, name): """Set argument name.""" self._name = name def __call__(self, args): """Get the value of the command line argument.""" return str2unicode(args[self._name]) class OptionalValue(object): def __init__(self, name): """Set argument name.""" self._name = name self._choice = Switch(name.lstrip('-')) def __call__(self, args): """Get the value of the command line argument.""" return self._choice(args) and str2unicode(args[self._name]) class _EntryPoint(object): """ Abstract base class for program entry points. Concrete implementations need to implement :meth:`run` and extend :meth:`finalize_options` to be usable with :meth:`main`. Furthermore the docstring of any concrete implementation must be usable with docopt. :ivar:`name` must be set to the name of the CLI utility. """ option_defaults = { 'log_level': logging.INFO, 'udisks_version': None, } option_rules = { 'log_level': Choice({ '--verbose': logging.DEBUG, '--quiet': logging.ERROR}), 'udisks_version': Choice({ '--udisks-auto': 0, '--use-udisks1': 1, '--use-udisks2': 2}), } usage_remarks = _(""" Note, that the options in the individual groups are mutually exclusive. The config file can be a JSON or preferrably a YAML file. For an example, see the MAN page (or doc/udiskie.8.txt in the repository). """) def __init__(self, argv=None): """ Parse command line options, read config and initialize members. :param list argv: command line parameters """ # parse program options (retrieve log level and config file name): args = docopt(self.usage, version=self.name + ' ' + self.version) default_opts = self.option_defaults program_opts = self.program_options(args) # initialize logging configuration: log_level = program_opts.get('log_level', default_opts['log_level']) if log_level <= logging.DEBUG: fmt = _('%(levelname)s [%(asctime)s] %(name)s: %(message)s') else: fmt = _('%(message)s') logging.basicConfig(level=log_level, format=fmt) # parse config options config_file = OptionalValue('--config')(args) config = udiskie.config.Config.from_file(config_file) options = {} options.update(default_opts) options.update(config.program_options) options.update(program_opts) # initialize instance variables self.config = config self.options = options self.exit_status = 0 def program_options(self, args): """ Fully initialize Daemon object. :param dict args: arguments as parsed by docopt :returns: options from command line :rtype: dict """ options = {} for name, rule in self.option_rules.items(): val = rule(args) if val is not None: options[name] = val return options @classmethod def main(cls, argv=None): """ Run program. :param list argv: command line parameters :returns: program exit code :rtype: int """ return cls(argv).run() @property def version(self): """Get the version from setuptools metadata.""" return udiskie.__version__ @property def usage(self): """Get the full usage string.""" return inspect.cleandoc(self.__doc__ + self.usage_remarks) @property def name(self): """Get the name of the CLI utility.""" raise NotImplementedError() def _init(self): """ Fully initialize Daemon object. :returns: the main application task :rtype: Async """ raise NotImplementedError() def run(self): """ Run the main loop. :returns: exit code :rtype: int """ self.mainloop = GLib.MainLoop() self._start_async_tasks() try: self.mainloop.run() return self.exit_status except KeyboardInterrupt: return 1 @Coroutine.from_generator_function def _start_async_tasks(self): """Start asynchronous operations.""" try: self.udisks = yield get_backend(self.options['udisks_version']) yield self._init() except Exception: self.exit_status = 1 # Print the stack trace only up to the current level: traceback.print_exc() self.mainloop.quit() class Daemon(_EntryPoint): """ udiskie: a user-level daemon for auto-mounting. Usage: udiskie [options] udiskie (--help | --version) General options: -c FILE, --config=FILE Set config file -C, --no-config Don't use config file -v, --verbose Increase verbosity (DEBUG) -q, --quiet Decrease verbosity -0, --udisks-auto Auto discover UDisks version -1, --use-udisks1 Use UDisks1 as backend -2, --use-udisks2 Use UDisks2 as backend -h, --help Show this help -V, --version Show version information Daemon options: -a, --automount Automount new devices -A, --no-automount Disable automounting -n, --notify Show popup notifications -N, --no-notify Disable notifications -t, --tray Show tray icon -s, --smart-tray Auto hide tray icon -T, --no-tray Disable tray icon --password-cache MINUTES Set password cache timeout --no-password-cache Disable password cache -p COMMAND, --password-prompt COMMAND Command for password retrieval -P, --no-password-prompt Disable unlocking Deprecated options: -f PROGRAM, --file-manager PROGRAM Set program for browsing -F, --no-file-manager Disable browsing """ name = 'udiskie' option_defaults = extend(_EntryPoint.option_defaults, { 'automount': True, 'notify': True, 'tray': False, 'file_manager': 'xdg-open', 'password_prompt': 'builtin:gui', 'password_cache': False, }) option_rules = extend(_EntryPoint.option_rules, { 'automount': Switch('automount'), 'notify': Switch('notify'), 'tray': Choice({ '--tray': True, '--no-tray': False, '--smart-tray': 'auto'}), 'file_manager': OptionalValue('--file-manager'), 'password_prompt': OptionalValue('--password-prompt'), 'password_cache': OptionalValue('--password-cache'), }) def _init(self): """Implements _EntryPoint._init.""" import udiskie.prompt config = self.config options = self.options prompt = udiskie.prompt.password(options['password_prompt']) browser = udiskie.prompt.browser(options['file_manager']) cache = None if options['password_cache'] is not False: import udiskie.cache timeout = int(options['password_cache']) * 60 cache = udiskie.cache.PasswordCache(timeout) mounter = udiskie.mount.Mounter( mount_options=config.mount_options, ignore_device=config.ignore_device, prompt=prompt, browser=browser, cache=cache, udisks=self.udisks) if options['notify'] and not has_Notify(): libnotify_not_available = _( "Typelib for 'libnotify' is not available. Possible causes include:" "\n\t- libnotify is not installed" "\n\t- the typelib is provided by a separate package" "\n\t- libnotify was built with introspection disabled" "\n\nStarting udiskie without notifications.") logging.getLogger(__name__).error(libnotify_not_available) options['notify'] = False if options['tray'] and not has_Gtk(3): gtk3_not_available = _( "Typelib for 'Gtk 3.0' is not available. Possible causes include:" "\n\t- GTK3 is not installed" "\n\t- the typelib is provided by a separate package" "\n\t- GTK3 was built with introspection disabled" "\n\nStarting udiskie without tray icon.") logging.getLogger(__name__).error(gtk3_not_available) options['tray'] = False tasks = [] # notifications (optional): if options['notify']: import udiskie.notify from gi.repository import Notify Notify.init('udiskie') aconfig = config.notification_actions if options['automount']: aconfig.setdefault('device_added', []) else: aconfig.setdefault('device_added', ['mount']) udiskie.notify.Notify(Notify.Notification.new, mounter=mounter, timeout=config.notifications, aconfig=aconfig) # tray icon (optional): if options['tray']: import udiskie.tray tray_classes = {True: udiskie.tray.TrayIcon, 'auto': udiskie.tray.AutoTray} if options['tray'] not in tray_classes: raise ValueError("Invalid tray: %s" % (options['tray'],)) icons = udiskie.tray.Icons(config.icon_names) actions = udiskie.mount.DeviceActions(mounter) menu_maker = udiskie.tray.SmartUdiskieMenu( mounter, icons, actions, quit_action=self.mainloop.quit) TrayIcon = tray_classes[options['tray']] statusicon = TrayIcon(menu_maker, icons) tasks.append(statusicon.task) else: statusicon = None tasks.append(RunForever) # automounter if options['automount']: import udiskie.automount udiskie.automount.AutoMounter(mounter) tasks.append(mounter.add_all()) # Note: mounter and statusicon are saved so these are kept alive: self.mounter = mounter self.statusicon = statusicon return AsyncList(tasks) class Mount(_EntryPoint): """ udiskie-mount: a user-level command line utility for mounting. Usage: udiskie-mount [options] (-a | DEVICE...) udiskie-mount (--help | --version) General options: -c FILE, --config=FILE Set config file -C, --no-config Don't use config file -v, --verbose Increase verbosity (DEBUG) -q, --quiet Decrease verbosity -0, --udisks-auto Auto discover UDisks version -1, --use-udisks1 Use UDisks1 as backend -2, --use-udisks2 Use UDisks2 as backend -h, --help Show this help -V, --version Show version information Mount options: -a, --all Mount all handleable devices -r, --recursive Recursively mount partitions -R, --no-recursive Disable recursive mounting -o OPTIONS, --options OPTIONS Mount option list -p COMMAND, --password-prompt COMMAND Command for password retrieval -P, --no-password-prompt Disable unlocking """ name = 'udiskie-mount' option_defaults = extend(_EntryPoint.option_defaults, { 'recursive': False, 'options': None, '': None, 'password_prompt': 'builtin:tty', }) option_rules = extend(_EntryPoint.option_rules, { 'recursive': Switch('recursive'), 'options': Value('--options'), '': Value('DEVICE'), 'password_prompt': OptionalValue('--password-prompt'), }) def _init(self): """Implements _EntryPoint._init.""" import udiskie.prompt config = self.config options = self.options if options['options']: opts = [o.strip() for o in options['options'].split(',')] mount_options = lambda dev: opts else: mount_options = config.mount_options prompt = udiskie.prompt.password(options['password_prompt']) mounter = udiskie.mount.Mounter( mount_options=mount_options, ignore_device=config.ignore_device, prompt=prompt, udisks=self.udisks) recursive = options['recursive'] if options['']: tasks = [mounter.add(path, recursive=recursive) for path in options['']] # TODO: AND results return AsyncList(tasks) else: return mounter.add_all(recursive=recursive) class Umount(_EntryPoint): """ udiskie-umount: a user-level command line utility for unmounting. Usage: udiskie-umount [options] (-a | DEVICE...) udiskie-umount (--help | --version) General options: -c FILE, --config=FILE Set config file -C, --no-config Don't use config file -v, --verbose Increase verbosity (DEBUG) -q, --quiet Decrease verbosity -0, --udisks-auto Auto discover UDisks version -1, --use-udisks1 Use UDisks1 as backend -2, --use-udisks2 Use UDisks2 as backend -h, --help Show this help -V, --version Show version information Unmount options: -a, --all Unmount all handleable devices -d, --detach Power off drive if possible -D, --no-detach Don't power off drive -e, --eject Eject media from device if possible -E, --no-eject Don't eject media -f, --force Force removal (recursive unmounting) -F, --no-force Don't force removal -l, --lock Lock device after unmounting -L, --no-lock Don't lock device """ name = 'udiskie-umount' option_defaults = extend(_EntryPoint.option_defaults, { 'detach': False, 'eject': False, 'force': False, 'lock': True, '': None, }) option_rules = extend(_EntryPoint.option_rules, { 'detach': Switch('detach'), 'eject': Switch('eject'), 'force': Switch('force'), 'lock': Switch('lock'), '': Value('DEVICE'), }) def _init(self): """Implements _EntryPoint._init.""" options = self.options mounter = udiskie.mount.Mounter(self.udisks) strategy = dict(detach=options['detach'], eject=options['eject'], lock=options['lock']) if options['']: strategy['force'] = options['force'] tasks = [mounter.remove(path, **strategy) for path in options['']] # TODO: AND results return AsyncList(tasks) else: return mounter.remove_all(**strategy) udiskie-1.4.9/udiskie/compat.py0000644000175000001440000000123612640243465017136 0ustar thomasusers00000000000000""" Compatibility layer for python2/python3. """ from __future__ import absolute_import from __future__ import unicode_literals import sys try: # python2 basestring = basestring unicode = unicode except NameError: # python3 basestring = str unicode = str def fix_str_conversions(cls): """Enable python2/3 compatible behaviour for __str__.""" def __bytes__(self): return self.__unicode__().encode('utf-8') cls.__unicode__ = __unicode__ = cls.__str__ cls.__bytes__ = __bytes__ if sys.version_info[0] == 2: cls.__str__ = __bytes__ else: cls.__str__ = __unicode__ return cls udiskie-1.4.9/udiskie/notify.py0000644000175000001440000002303312656353676017177 0ustar thomasusers00000000000000""" Notification utility. """ from __future__ import absolute_import from __future__ import unicode_literals import logging from gi.repository import GLib from .common import exc_message from .mount import DeviceActions from .locale import _ __all__ = ['Notify'] class Notify(object): """ Notification tool. Can be connected to udisks daemon in order to automatically issue notifications when system status has changed. NOTE: the action buttons in the notifications don't work with all notification services. """ def __init__(self, notify, mounter, timeout=None, aconfig=None): """ Initialize notifier and connect to service. :param notify: notification service module (pynotify or notify2) :param mounter: Mounter object :param dict timeout: timeouts """ self._notify = notify self._mounter = mounter self._actions = DeviceActions(mounter) self._timeout = timeout or {} self._aconfig = aconfig or {} self._default = self._timeout.get('timeout', -1) self._log = logging.getLogger(__name__) self._notifications = [] # Subscribe all enabled events to the daemon: udisks = mounter.udisks for event in ['device_mounted', 'device_unmounted', 'device_locked', 'device_unlocked', 'device_added', 'device_removed', 'job_failed']: if self._enabled(event): udisks.connect(event, getattr(self, event)) # event handlers: def device_mounted(self, device): """ Show 'Device mounted' notification with 'Browse directory' button. :param device: device object """ browse_action = ('browse', _('Browse directory'), self._mounter.browse, device) self._show_notification( 'device_mounted', _('Device mounted'), _('{0.ui_label} mounted on {0.mount_paths[0]}', device), device.icon_name, self._mounter._browser and browse_action) def device_unmounted(self, device): """ Show 'Device unmounted' notification. :param device: device object """ self._show_notification( 'device_unmounted', _('Device unmounted'), _('{0.ui_label} unmounted', device), device.icon_name) def device_locked(self, device): """ Show 'Device locked' notification. :param device: device object """ self._show_notification( 'device_locked', _('Device locked'), _('{0.device_presentation} locked', device), device.icon_name) def device_unlocked(self, device): """ Show 'Device unlocked' notification. :param device: device object """ self._show_notification( 'device_unlocked', _('Device unlocked'), _('{0.device_presentation} unlocked', device), device.icon_name) def device_added(self, device): """ Show 'Device added' notification. :param device: device object """ if self._has_actions('device_added'): # wait for partitions etc to be reported to udiskie, otherwise we # can't discover the actions GLib.timeout_add(500, self._device_added, device) else: self._device_added(device) def _device_added(self, device): device_file = device.device_presentation if (device.is_drive or device.is_toplevel) and device_file: # On UDisks1: cannot invoke self._actions.detect() for newly added # LUKS devices. It should be okay if we had waited for the actions # to be added, though. if self._has_actions('device_added'): node_tree = self._actions.detect(device.object_path) flat_actions = self._flatten_node(node_tree) actions = [ (action.method, action.label.format(action.device.ui_label), action.action) for action in flat_actions ] else: actions = () self._show_notification( 'device_added', _('Device added'), _('device appeared on {0.device_presentation}', device), device.icon_name, *actions) def _flatten_node(self, node): actions = [action for branch in node.branches for action in self._flatten_node(branch)] actions += node.methods return actions def device_removed(self, device): """ Show 'Device removed' notification. :param device: device object """ device_file = device.device_presentation if (device.is_drive or device.is_toplevel) and device_file: self._show_notification( 'device_removed', _('Device removed'), _('device disappeared on {0.device_presentation}', device), device.icon_name) def job_failed(self, device, action, message): """ Show 'Job failed' notification with 'Retry' button. :param device: device object """ device_file = device.device_presentation or device.object_path if message: text = _('failed to {0} {1}:\n{2}', action, device_file, message) else: text = _('failed to {0} device {1}.', action, device_file) try: retry = getattr(self._mounter, action) except AttributeError: retry_action = None else: retry_action = ('retry', _('Retry'), retry, device) self._show_notification( 'job_failed', _('Job failed'), text, device.icon_name, retry_action) def _show_notification(self, event, summary, message, icon, *actions): """ Show a notification. :param str event: event name :param str summary: notification title :param str message: notification body :param str icon: icon name :param dict action: parameters to :meth:`_add_action` """ notification = self._notify(summary, message, icon) timeout = self._get_timeout(event) if timeout != -1: notification.set_timeout(int(timeout * 1000)) for action in actions: if self._action_enabled(event, action[0]): self._add_action(notification, *action) try: notification.show() except GLib.GError as exc: # Catch and log the exception. Starting udiskie with notifications # enabled while not having a notification service installed is a # mistake too easy to be made, but it shoud not render the rest of # udiskie's logic useless by raising an exception before the # automount handler gets invoked. self._log.error(_("Failed to show notification: {0}", exc_message(exc))) def _add_action(self, notification, action, label, callback, *args): """ Show an action button button in mount notifications. Note, this only works with some libnotify services. """ def on_action_click(notification, action, *user_data): callback(*args) try: # this is the correct signature for Notify-0.7, the last argument # being 'user_data': notification.add_action(action, label, on_action_click, None) except TypeError: # this is the signature for some older version, I don't know what # the last argument is for. notification.add_action(action, label, on_action_click, None, None) # pynotify does not store hard references to the notification # objects. When a signal is received and the notification does not # exist anymore, no handller will be called. Therefore, we need to # prevent these notifications from being destroyed by storing # references (note, notify2 doesn't need this): notification.connect('closed', self._notifications.remove) self._notifications.append(notification) def _enabled(self, event): """ Check if the notification for an event is enabled. :param str event: event name :returns: if the event notification is enabled :rtype: bool """ return self._get_timeout(event) not in (None, False) def _get_timeout(self, event): """ Get the timeout for an event from the config. :param str event: event name :returns: timeout in seconds :rtype: int, float or NoneType """ return self._timeout.get(event, self._default) def _action_enabled(self, event, action): """ Check if an action for a notification is enabled. :param str event: event name :param str action: action name :rtype: bool """ event_actions = self._aconfig.get(event) if event_actions is None: return True if event_actions is False: return False return action in event_actions def _has_actions(self, event): """ Check if a notification type has any enabled actions. """ event_actions = self._aconfig.get(event) return event_actions is None or bool(event_actions) udiskie-1.4.9/udiskie/common.py0000644000175000001440000001611212656516144017146 0ustar thomasusers00000000000000""" Common DBus utilities. """ from __future__ import absolute_import from __future__ import unicode_literals import os.path import traceback from .compat import fix_str_conversions __all__ = [ 'wraps', 'check_call', 'Emitter', 'samefile', 'setdefault', 'extend', 'cachedproperty', 'decode_ay', # dealing with py2 strings: 'str2unicode', 'exc_message', 'format_exc', ] try: from black_magic.decorator import wraps except ImportError: from functools import wraps def check_call(exc_type, func, *args): try: func(*args) return True except exc_type: return False class Emitter(object): """ Event emitter class. Provides a simple event engine featuring a known finite set of events. """ def __init__(self, event_names=(), *args, **kwargs): """ Initialize with empty lists of event handlers. :param iterable event_names: names of known events. """ super(Emitter, self).__init__(*args, **kwargs) self._event_handlers = {} for evt in event_names: self._event_handlers[evt] = [] def trigger(self, event, *args): """ Trigger event handlers. :param str event: event name :param *args: event parameters """ for handler in self._event_handlers[event]: handler(*args) def connect(self, event, handler): """ Connect an event handler. :param str event: event name :param callable handler: event handler """ self._event_handlers[event].append(handler) def disconnect(self, event, handler): """ Disconnect an event handler. :param str event: event name :param callable handler: event handler """ self._event_handlers[event].remove(handler) def samefile(a, b): """Check if two pathes represent the same file.""" try: return os.path.samefile(a, b) except OSError: return os.path.normpath(a) == os.path.normpath(b) def setdefault(self, other): """ Merge two dictionaries like .update() but don't overwrite values. :param dict self: updated dict :param dict other: default values to be inserted """ for k, v in other.items(): self.setdefault(k, v) def extend(a, b): """Merge two dicts and return a new dict. Much like subclassing works.""" res = a.copy() res.update(b) return res def cachedproperty(func): """A memoize decorator for class properties.""" key = '_' + func.__name__ @wraps(func) def get(self): try: return getattr(self, key) except AttributeError: val = func(self) setattr(self, key, val) return val return property(get) # ---------------------------------------- # udisks.Device helper classes # ---------------------------------------- class AttrDictView(object): """Provide attribute access view to a dictionary.""" def __init__(self, data): self.__data = data def __getattr__(self, key): try: return self.__data[key] except KeyError: raise AttributeError @fix_str_conversions class NullDevice(object): """ Invalid object. Evaluates to False in boolean context, but allows arbitrary attribute access by returning another Null. """ object_path = '/' def __init__(self, **properties): """Initialize an instance with the given DBus proxy object.""" self.__dict__.update(properties) def __bool__(self): return False __nonzero__ = __bool__ def __str__(self): """Display as object path.""" return self.object_path def __eq__(self, other): """Comparison by object path.""" return self.object_path == str(other) def __ne__(self, other): """Comparison by object path.""" return not (self == other) def is_file(self, path): """Comparison by mount and device file path.""" return False # availability of interfaces is_drive = False is_block = False is_partition_table = False is_partition = False is_filesystem = False is_luks = False is_loop = False # Drive is_toplevel = is_drive is_detachable = False is_ejectable = False has_media = False def eject(self, unmount=False): raise RuntimeError("Cannot call methods on invalid device!") def detach(self): raise RuntimeError("Cannot call methods on invalid device!") # Block device_file = '' device_presentation = '' device_size = 0 id_usage = '' is_crypto = False is_ignored = None device_id = '' id_type = '' id_label = '' id_uuid = '' @property def luks_cleartext_slave(self): raise AttributeError('Invalid device has no cleartext slave.') is_luks_cleartext = False is_external = None is_systeminternal = None @property def drive(self): raise AttributeError('Invalid device has no drive.') root = drive should_automount = False icon_name = '' symbolic_icon_name = icon_name # Partition @property def partition_slave(self): raise AttributeError('Invalid device has no partition slave.') # Filesystem is_mounted = False mount_paths = () def mount(self, fstype=None, options=None, auth_no_user_interaction=False): raise RuntimeError("Cannot call methods on invalid device!") def unmount(self, force=False): raise RuntimeError("Cannot call methods on invalid device!") # Encrypted @property def luks_cleartext_holder(self): raise AttributeError('Invalid device has no cleartext holder.') is_unlocked = None def unlock(self, password): raise RuntimeError("Cannot call methods on invalid device!") def lock(self): raise RuntimeError("Cannot call methods on invalid device!") # Loop loop_file = '' # derived properties in_use = False parent_object_path = '/' ui_label = '(invalid device)' # ---------------------------------------- # byte array to string conversion # ---------------------------------------- try: unicode except NameError: unicode = str def decode_ay(ay): """Convert binary blob from DBus queries to strings.""" if ay is None: return '' elif isinstance(ay, unicode): return ay elif isinstance(ay, bytes): return ay.decode('utf-8') else: # dbus.Array([dbus.Byte]) or any similar sequence type: return bytearray(ay).rstrip(bytearray((0,))).decode('utf-8') def str2unicode(arg): """Decode python2 strings (bytes) to unicode.""" if isinstance(arg, list): return [str2unicode(s) for s in arg] if isinstance(arg, bytes): return arg.decode('utf-8') return arg def exc_message(exc): """Get an exception message.""" message = getattr(exc, 'message', None) return str2unicode(message or str(exc)) def format_exc(): return str2unicode(traceback.format_exc()) udiskie-1.4.9/udiskie/udisks2.py0000644000175000001440000006435212656516204017250 0ustar thomasusers00000000000000""" UDisks2 wrapper utilities. These act as a convenience abstraction layer on the UDisks2 DBus service. Requires UDisks2 2.1.1 as described here: http://udisks.freedesktop.org/docs/latest This wraps the DBus API of Udisks2 providing a common interface with the udisks1 module. """ from __future__ import absolute_import from __future__ import unicode_literals from copy import copy, deepcopy import logging from gi.repository import GLib from .common import Emitter, samefile, AttrDictView, decode_ay from .compat import fix_str_conversions from .dbus import connect_service, MethodsProxy from .locale import _ from .async_ import Coroutine, Return __all__ = ['Daemon'] def object_kind(object_path): """ Parse the kind of object from an UDisks2 object path. Example: /org/freedesktop/UDisks2/block_devices/sdb1 => device """ try: return { 'block_devices': 'device', 'drives': 'drive', 'jobs': 'job', }.get(object_path.split('/')[4]) except IndexError: return None def filter_opt(opt): """Remove ``None`` values from a dictionary.""" return {k: GLib.Variant(*v) for k, v in opt.items() if v[1] is not None} Interface = { 'Manager': 'org.freedesktop.UDisks2.Manager', 'Drive': 'org.freedesktop.UDisks2.Drive', 'DriveAta': 'org.freedesktop.UDisks2.Drive.Ata', 'MDRaid': 'org.freedesktop.UDisks2.MDRaid', 'Block': 'org.freedesktop.UDisks2.Block', 'Partition': 'org.freedesktop.UDisks2.Partition', 'PartitionTable': 'org.freedesktop.UDisks2.PartitionTable', 'Filesystem': 'org.freedesktop.UDisks2.Filesystem', 'Swapspace': 'org.freedesktop.UDisks2.Swapspace', 'Encrypted': 'org.freedesktop.UDisks2.Encrypted', 'Loop': 'org.freedesktop.UDisks2.Loop', 'Job': 'org.freedesktop.UDisks2.Job', 'ObjectManager': 'org.freedesktop.DBus.ObjectManager', 'Properties': 'org.freedesktop.DBus.Properties', } # ---------------------------------------- # Internal helper classes # ---------------------------------------- class MethodHub(object): """Provide MethodsProxies for queried interfaces of a DBus object.""" def __init__(self, object_proxy): """Initialize from (ObjectProxy).""" self._object_proxy = object_proxy def __getattr__(self, key): """Return a MethodsProxy for the requested interface.""" return MethodsProxy(self._object_proxy, Interface[key]) class PropertyHub(object): """Provide attribute accessors for queried interfaces of a DBus object.""" def __init__(self, interfaces_and_properties): """Initialize from (dict).""" self._interfaces_and_properties = interfaces_and_properties def __getattr__(self, key): """Return an AttrDictView for properties on the requested interface.""" interface = Interface[key] try: return AttrDictView(self._interfaces_and_properties[interface]) except KeyError: return PropertiesNotAvailable() class PropertiesNotAvailable(object): """Null class for properties of an unavailable interface.""" def __nonzero__(self): # python2 return False __bool__ = __nonzero__ # python3 def __getattr__(self, key): """Return None when asked for any attribute.""" return None # ---------------------------------------- # Device wrapper # ---------------------------------------- @fix_str_conversions class Device(object): """ Proxy class for UDisks2 devices. Properties are read from the cached values retrieved by the Daemon class. Methods are executed asynchronously, and hence return Asyncs instead of returning the result directly. """ def __init__(self, daemon, object_path, property_hub, method_hub): """Initialize from (Daemon, str, PropertyHub, MethodHub).""" self._daemon = daemon self.object_path = object_path self._P = property_hub self._M = method_hub def __str__(self): """Show as object_path.""" return self.object_path def __eq__(self, other): """Comparison by object_path.""" return self.object_path == str(other) def __ne__(self, other): """Comparison by object_path.""" return not (self == other) def is_file(self, path): """Comparison by mount and device file path.""" return (samefile(path, self.device_file) or samefile(path, self.loop_file) or any(samefile(path, mp) for mp in self.mount_paths)) # availability of interfaces @property def is_drive(self): """Check if the device is a drive.""" return bool(self._P.Drive) @property def is_block(self): """Check if the device is a block device.""" return bool(self._P.Block) @property def is_partition_table(self): """Check if the device is a partition table.""" return bool(self._P.PartitionTable) @property def is_partition(self): """Check if the device has a partition slave.""" # Sometimes udisks2 empties the Partition interface before removing # the device. In this case, we want to report .is_partition=False, so # properties like .partition_slave will not be used. return bool(self._P.Partition and self.partition_slave) @property def is_filesystem(self): """Check if the device is a filesystem.""" return bool(self._P.Filesystem) @property def is_luks(self): """Check if the device is a LUKS container.""" return bool(self._P.Encrypted) @property def is_loop(self): """Check if the device is a loop device.""" return bool(self._P.Loop) # ---------------------------------------- # Drive # ---------------------------------------- # Drive properties @property def is_toplevel(self): """Check if the device is not a child device.""" return not self.is_partition and not self.is_luks_cleartext @property def _assocdrive(self): """ Return associated drive if this is a top level block device. This method is used internally to unify the behaviour of top level devices in udisks1 and udisks2. """ return self.drive if self.is_toplevel and not self.is_loop else self @property def is_detachable(self): """Check if the drive that owns this device can be detached.""" return bool(self._assocdrive._P.Drive.CanPowerOff) @property def is_ejectable(self): """Check if the drive that owns this device can be ejected.""" return bool(self._assocdrive._P.Drive.Ejectable) @property def has_media(self): """Check if there is media available in the drive.""" return bool(self._assocdrive._P.Drive.MediaAvailable) # Drive methods def eject(self, auth_no_user_interaction=None): """Eject media from the device.""" return self._assocdrive._M.Drive.Eject( '(a{sv})', filter_opt({ 'auth.no_user_interaction': ('b', auth_no_user_interaction), }) ) def detach(self, auth_no_user_interaction=None): """Detach the device by e.g. powering down the physical port.""" return self._assocdrive._M.Drive.PowerOff( '(a{sv})', filter_opt({ 'auth.no_user_interaction': ('b', auth_no_user_interaction), }) ) # ---------------------------------------- # Block # ---------------------------------------- # Block properties @property def device_file(self): """The filesystem path of the device block file.""" return decode_ay(self._P.Block.Device) @property def device_presentation(self): """The device file path to present to the user.""" return decode_ay(self._P.Block.PreferredDevice) @property def device_size(self): """The size of the device in bytes.""" return self._P.Block.Size @property def id_usage(self): """Device usage class, for example 'filesystem' or 'crypto'.""" return self._P.Block.IdUsage @property def is_crypto(self): """Check if the device is a crypto device.""" return self.id_usage == 'crypto' @property def is_ignored(self): """Check if the device should be ignored.""" return self._P.Block.HintIgnore @property def device_id(self): """ Return a unique and persistent identifier for the device. This is the basename (last path component) of the symlink in `/dev/disk/by-id/`. """ if self.is_block: for filename in self._P.Block.Symlinks: parts = decode_ay(filename).split('/') if parts[-2] == 'by-id': return parts[-1] elif self.is_drive: return self._assocdrive._P.Drive.Id return '' @property def id_type(self): """" Return IdType property. This field provides further detail on IdUsage, for example: IdUsage 'filesystem' 'crypto' IdType 'ext4' 'crypto_LUKS' """ return self._P.Block.IdType @property def id_label(self): """Label of the device if available.""" return self._P.Block.IdLabel @property def id_uuid(self): """Device UUID.""" return self._P.Block.IdUUID @property def luks_cleartext_slave(self): """Get wrapper to the LUKS crypto device.""" return self._daemon[self._P.Block.CryptoBackingDevice] @property def is_luks_cleartext(self): """Check whether this is a luks cleartext device.""" return bool(self.luks_cleartext_slave) @property def is_external(self): """Check if the device is external.""" # NOTE: Checking for equality HintSystem==False returns False if the # property is resolved to a None value (interface not available). if self._P.Block.HintSystem == False: return True # NOTE: udisks2 seems to guess incorrectly in some cases. This # leads to HintSystem=True for unlocked devices. In order to show # the device anyway, it needs to be recursively checked if any # parent device is recognized as external. if self.is_luks_cleartext and self.luks_cleartext_slave.is_external: return True if self.is_partition and self.partition_slave.is_external: return True return False @property def is_systeminternal(self): """Check if the device is internal.""" return not self.is_external @property def drive(self): """Get wrapper to the drive containing this device.""" if self.is_drive: return self cleartext = self.luks_cleartext_slave if cleartext: return cleartext.drive if self.is_block: return self._daemon[self._P.Block.Drive] return None @property def root(self): """ Get the top level block device in the ancestry of this device. """ drive = self.drive for device in self._daemon: if device.is_drive: continue if device.is_toplevel and device.drive == drive: return device return None @property def should_automount(self): """Check if the device should be automounted.""" return bool(self._P.Block.HintAuto) @property def icon_name(self): """Return the recommended device icon name.""" return self._P.Block.HintIconName or 'drive-removable-media' @property def symbolic_icon_name(self): """Return the recommended device symbolic icon name.""" return self._P.Block.HintSymbolicIconName or 'drive-removable-media' # ---------------------------------------- # Partition # ---------------------------------------- # Partition properties @property def partition_slave(self): """Get the partition slave (container).""" return self._daemon[self._P.Partition.Table] # ---------------------------------------- # Filesystem # ---------------------------------------- # Filesystem properties @property def is_mounted(self): """Check if the device is mounted.""" return bool(self._P.Filesystem.MountPoints) @property def mount_paths(self): """Return list of active mount paths.""" return list(map(decode_ay, self._P.Filesystem.MountPoints or ())) # Filesystem methods def mount(self, fstype=None, options=None, auth_no_user_interaction=None): """Mount filesystem.""" return self._M.Filesystem.Mount( '(a{sv})', filter_opt({ 'fstype': ('s', fstype), 'options': ('s', ','.join(options or [])), 'auth.no_user_interaction': ('b', auth_no_user_interaction), }) ) def unmount(self, force=None, auth_no_user_interaction=None): """Unmount filesystem.""" return self._M.Filesystem.Unmount( '(a{sv})', filter_opt({ 'force': ('b', force), 'auth.no_user_interaction': ('b', auth_no_user_interaction), }) ) # ---------------------------------------- # Encrypted # ---------------------------------------- # Encrypted properties @property def luks_cleartext_holder(self): """Get wrapper to the unlocked luks cleartext device.""" if not self.is_luks: return None for device in self._daemon: if device.luks_cleartext_slave == self: return device return None @property def is_unlocked(self): """Check if device is already unlocked.""" return bool(self.luks_cleartext_holder) # Encrypted methods def unlock(self, password, auth_no_user_interaction=None): """Unlock Luks device.""" return self._M.Encrypted.Unlock( '(sa{sv})', password, filter_opt({ 'auth.no_user_interaction': ('b', auth_no_user_interaction), }) ) def lock(self, auth_no_user_interaction=None): """Lock Luks device.""" return self._M.Encrypted.Lock( '(a{sv})', filter_opt({ 'auth.no_user_interaction': ('b', auth_no_user_interaction), }) ) # ---------------------------------------- # Loop # ---------------------------------------- @property def loop_file(self): """Get the file backing the loop device.""" return decode_ay(self._P.Loop.BackingFile) # ---------------------------------------- # derived properties # ---------------------------------------- @property def in_use(self): """Check whether this device is in use, i.e. mounted or unlocked.""" if self.is_mounted or self.is_unlocked: return True if self.is_partition_table: for device in self._daemon: if device.partition_slave == self and device.in_use: return True return False @property def parent_object_path(self): return (self._P.Partition.Table or self._P.Block.CryptoBackingDevice or '/') @property def ui_label(self): return self.id_label or self.id_uuid or self.device_presentation # ---------------------------------------- # UDisks2 service wrapper # ---------------------------------------- class Daemon(Emitter): """ Listen to state changes to provide automatic synchronization. Listens to UDisks2 events. When a change occurs this class detects what has changed and triggers an appropriate event. Valid events are: - device_added / device_removed - device_unlocked / device_locked - device_mounted / device_unmounted - media_added / media_removed - device_changed / job_failed """ BusName = 'org.freedesktop.UDisks2' ObjectPath = '/org/freedesktop/UDisks2' Interface = Interface['ObjectManager'] def __iter__(self): """Iterate over all devices.""" return (self[path] for path in self.paths() if object_kind(path) in ('device', 'drive')) def __getitem__(self, object_path): return self.get(object_path) def find(self, path): """ Get a device proxy by device name or any mount path of the device. This searches through all accessible devices and compares device path as well as mount pathes. """ if isinstance(path, Device): return path for device in self: if device.is_file(path): self._log.debug(_('found device owning "{0}": "{1}"', path, device)) return device raise ValueError(_('no device found owning "{0}"', path)) def __init__(self, proxy): """Initialize object and start listening to UDisks2 events.""" event_names = ['device_added', 'device_removed', 'device_mounted', 'device_unmounted', 'media_added', 'media_removed', 'device_unlocked', 'device_locked', 'device_changed', 'job_failed'] super(Daemon, self).__init__(event_names) self._proxy = proxy self._log = logging.getLogger(__name__) self._objects = {} proxy.connect('InterfacesAdded', self._interfaces_added) proxy.connect('InterfacesRemoved', self._interfaces_removed) bus = proxy.object.bus bus.connect(Interface['Properties'], 'PropertiesChanged', None, self._properties_changed) bus.connect(Interface['Job'], 'Completed', None, self._job_completed) def _sync(self): """Synchronize state.""" def update_objects(objects): self._objects = objects update = self._proxy.call('GetManagedObjects', '()') update.callbacks.append(update_objects) return update @classmethod @Coroutine.from_generator_function def create(cls): proxy = yield connect_service(cls) daemon = cls(proxy) yield daemon._sync() yield Return(daemon) # UDisks2 interface def paths(self): return self._objects.keys() def get(self, object_path, interfaces_and_properties=None): """Create a Device instance from object path.""" # check this before creating the DBus object for more # controlled behaviour: if not interfaces_and_properties: interfaces_and_properties = self._objects.get(object_path) if not interfaces_and_properties: return None property_hub = PropertyHub(interfaces_and_properties) method_hub = MethodHub( self._proxy.object.bus.get_object(object_path)) return Device(self, object_path, property_hub, method_hub) def trigger(self, event, device, *args): self._log.debug(_("+++ {0}: {1}", event, device)) super(Daemon, self).trigger(event, device, *args) # add objects / interfaces def _interfaces_added(self, object_path, interfaces_and_properties): """Internal method.""" added = object_path not in self._objects self._objects.setdefault(object_path, {}) old_state = copy(self._objects[object_path]) self._objects[object_path].update(interfaces_and_properties) new_state = self._objects[object_path] if added: kind = object_kind(object_path) if kind in ('device', 'drive'): self.trigger('device_added', self[object_path]) if Interface['Block'] in interfaces_and_properties: slave = self[object_path].luks_cleartext_slave if slave: if not self._has_job(slave.object_path, 'device_unlocked'): self.trigger('device_unlocked', slave) if not added: self.trigger('device_changed', self.get(object_path, old_state), self.get(object_path, new_state)) # remove objects / interfaces def _detect_toggle(self, property_name, old, new, add_name, del_name): old_valid = old and bool(getattr(old, property_name)) new_valid = new and bool(getattr(new, property_name)) if add_name and new_valid and not old_valid: if not self._has_job(old.object_path, add_name): self.trigger(add_name, new) elif del_name and old_valid and not new_valid: if not self._has_job(old.object_path, del_name): self.trigger(del_name, new) def _has_job(self, device_path, event_name): job_interface = Interface['Job'] for path, interfaces in self._objects.items(): try: job = interfaces[job_interface] job_objects = job['Objects'] job_operation = job['Operation'] job_action = self._action_by_operation[job_operation] job_event = self._event_by_action[job_action] if event_name == job_event and device_path in job_objects: return True except KeyError: pass return False def _interfaces_removed(self, object_path, interfaces): """Internal method.""" old_state = copy(self._objects[object_path]) for interface in interfaces: del self._objects[object_path][interface] new_state = self._objects[object_path] if Interface['Drive'] in interfaces: self._detect_toggle( 'has_media', self.get(object_path, old_state), self.get(object_path, new_state), None, 'media_removed') if Interface['Block'] in interfaces: slave = self.get(object_path, old_state).luks_cleartext_slave if slave: if not self._has_job(slave.object_path, 'device_locked'): self.trigger('device_locked', slave) if self._objects[object_path]: self.trigger('device_changed', self.get(object_path, old_state), self.get(object_path, new_state)) else: del self._objects[object_path] if object_kind(object_path) in ('device', 'drive'): self.trigger( 'device_removed', self.get(object_path, old_state)) # change interface properties def _properties_changed(self, object_path, interface_name, changed_properties, invalidated_properties): """ Internal method. Called when a DBusProperty of any managed object changes. """ # update device state: old_state = deepcopy(self._objects[object_path]) for property_name in invalidated_properties: del self._objects[object_path][interface_name][property_name] for key, value in changed_properties.items(): self._objects[object_path][interface_name][key] = value new_state = self._objects[object_path] # detect changes and trigger events: if interface_name == Interface['Drive']: self._detect_toggle( 'has_media', self.get(object_path, old_state), self.get(object_path, new_state), 'media_added', 'media_removed') elif interface_name == Interface['Filesystem']: self._detect_toggle( 'is_mounted', self.get(object_path, old_state), self.get(object_path, new_state), 'device_mounted', 'device_unmounted') self.trigger('device_changed', self.get(object_path, old_state), self.get(object_path, new_state)) # There is no PropertiesChanged for the crypto device when it is # unlocked/locked in UDisks2. Instead, this is handled by the # InterfaceAdded/Removed handlers. # jobs _action_by_operation = { 'filesystem-mount': 'mount', 'filesystem-unmount': 'unmount', 'encrypted-unlock': 'unlock', 'encrypted-lock': 'lock', 'power-off-drive': 'detach', 'eject-media': 'eject', } _event_by_action = { 'mount': 'device_mounted', 'unmount': 'device_unmounted', 'unlock': 'device_unlocked', 'lock': 'device_locked', 'eject': 'media_removed', 'detach': 'device_removed', } _check_action_success = { 'mount': lambda dev: dev.is_mounted, 'unmount': lambda dev: not dev or not dev.is_mounted, 'unlock': lambda dev: dev.is_unlocked, 'lock': lambda dev: not dev or not dev.is_unlocked, 'detach': lambda dev: not dev, 'eject': lambda dev: not dev or not dev.has_media, } def _job_completed(self, job_name, success, message): """ Internal method. Called when a job of a long running task completes. """ job = self._objects[job_name][Interface['Job']] action = self._action_by_operation.get(job['Operation']) if not action: return # We only handle events, which are associated to exactly one object: object_path, = job['Objects'] device = self[object_path] if success: # It rarely happens, but sometimes UDisks posts the # Job.Completed event before PropertiesChanged, so we have to # check if the operation has been carried out yet: if self._check_action_success[action](device): event_name = self._event_by_action[action] self.trigger(event_name, device) else: self.trigger('job_failed', device, action, message) udiskie-1.4.9/udiskie/cache.py0000644000175000001440000000273612640243465016724 0ustar thomasusers00000000000000""" Utility for temporarily caching passwords. """ from __future__ import absolute_import from __future__ import unicode_literals import keyutils class PasswordCache(object): def __init__(self, timeout): self.timeout = timeout self.keyring = keyutils.KEY_SPEC_PROCESS_KEYRING def _key(self, device): return device.id_uuid.encode('utf-8') def _key_id(self, device): key = self._key(device) try: key_id = keyutils.request_key(key, self.keyring) except keyutils.Error: raise KeyError("Key has been revoked!") if key_id is None: raise KeyError("Key not cached!") return key_id def __contains__(self, device): try: self._key_id(device) return True except KeyError: return False def __getitem__(self, device): key_id = self._key_id(device) self._touch(key_id) try: return keyutils.read_key(key_id).decode('utf-8') except keyutils.Error: raise KeyError("Key not cached!") def __setitem__(self, device, value): key = self._key(device) key_id = keyutils.add_key(key, value.encode('utf-8'), self.keyring) self._touch(key_id) def __delitem__(self, device): key_id = self._key_id(device) keyutils.revoke(key_id) def _touch(self, key_id): if self.timeout > 0: keyutils.set_timeout(key_id, self.timeout) udiskie-1.4.9/udiskie/locale.py0000644000175000001440000000224112640243465017107 0ustar thomasusers00000000000000""" I18n utilities. """ from __future__ import absolute_import from __future__ import unicode_literals import gettext __all__ = ['_'] class Translator(object): """ Simple translation and message formatting utility. """ @classmethod def create(cls, domain, localedir=None, languages=None): """ Create a new translator for the given domain. Arguments are as in ``gettext.translation``. """ t = gettext.translation(domain, localedir, languages, fallback=True) try: # on python2 we want the unicode version: g = t.ugettext except AttributeError: # which is the default in python3: g = t.gettext return cls(g) def __init__(self, gettext): """Initialize a translator with the given gettext function.""" self._gettext = gettext def __call__(self, text, *args, **kwargs): """Translate and then and format the text with ``str.format``.""" msg = self._gettext(text) if args or kwargs: return msg.format(*args, **kwargs) else: return msg _ = Translator.create('udiskie') udiskie-1.4.9/udiskie/__init__.py0000644000175000001440000000070612677734423017424 0ustar thomasusers00000000000000# encoding: utf-8 from __future__ import unicode_literals __title__ = 'udiskie' __version__ = '1.4.9' __summary__ = 'Removable disk automounter for udisks' __uri__ = 'https://github.com/coldfix/udiskie' __author__ = 'Byron Clark' __author_email__ = 'byron@theclarkfamily.name' __maintainer__ = 'Thomas Gläßle' __maintainer_email__ = 't_glaessle@gmx.de' __license__ = 'MIT' __copyright__ = '(c) 2010-2012 Byron Clark, (c) 2013-2016 Thomas Gläßle' udiskie-1.4.9/udiskie/depend.py0000644000175000001440000000325012656353417017116 0ustar thomasusers00000000000000""" Make sure that the correct versions of gobject introspection dependencies are installed. """ from __future__ import absolute_import from __future__ import unicode_literals import logging from gi import require_version from .common import check_call from .locale import _ require_version('Gio', '2.0') require_version('GLib', '2.0') def check_version(package, version): return check_call(ValueError, require_version, package, version) _has_Gtk = (3 if check_version('Gtk', '3.0') else 2 if check_version('Gtk', '2.0') else 0) _has_Notify = check_version('Notify', '0.7') def require_Gtk(min_version=2): """ Make sure Gtk is properly initialized. :raises RuntimeError: if Gtk can not be properly initialized """ if _has_Gtk < min_version: raise RuntimeError('Module gi.repository.Gtk not available!') if _has_Gtk == 2: logging.getLogger(__name__).warn( _("Missing runtime dependency GTK 3. Falling back to GTK 2 " "for password prompt")) from gi.repository import Gtk # if we attempt to create any GUI elements with no X server running the # program will just crash, so let's make a way to catch this case: if not Gtk.init_check(None)[0]: raise RuntimeError(_("X server not connected!")) return Gtk def require_Notify(): if not _has_Notify: raise RuntimeError('Module gi.repository.Notify not available!') from gi.repository import Notify return Notify def has_Notify(): return check_call((RuntimeError, ImportError), require_Notify) def has_Gtk(min_version=2): return check_call((RuntimeError, ImportError), require_Gtk, min_version) udiskie-1.4.9/udiskie/mount.py0000644000175000001440000006141112677614541017025 0ustar thomasusers00000000000000""" Mount utilities. """ from __future__ import absolute_import from __future__ import unicode_literals from collections import namedtuple from functools import partial import logging from .async_ import AsyncList, Coroutine, Return from .common import wraps, setdefault, exc_message from .config import IgnoreDevice, FilterMatcher from .locale import _ __all__ = ['Mounter'] # TODO: add / remove / XXX_all should make proper use of the asynchronous # execution. @Coroutine.from_generator_function def _False(): yield Return(False) def _find_device(fn, set_error=False): """ Decorator for Mounter methods taking a Device as their first argument. Enables to pass the path name as first argument and does some common error handling (logging). """ @wraps(fn) def wrapper(self, device_or_path, *args, **kwargs): try: device = self.udisks.find(device_or_path) except ValueError as e: self._log.error(exc_message(e)) return _False() return Coroutine(fn(self, device, *args, **kwargs)) return wrapper def _sets_async_error(fn): @wraps(fn) def wrapper(self, device, *args, **kwargs): async_ = fn(self, device, *args, **kwargs) async_.errbacks.append(partial(self._error, fn, device)) return async_ return wrapper def _suppress_error(fn): """ Prevent errors in this function from being shown. This is OK, since all errors happen in sub-functions in which errors ARE logged. """ @wraps(fn) def wrapper(self, device, *args, **kwargs): async_ = fn(self, device, *args, **kwargs) async_.errbacks.append(lambda *args: True) return async_ return wrapper def _is_parent_of(parent, child): """Check whether the first device is the parent of the second device.""" if child.is_partition: return child.partition_slave == parent if child.is_toplevel: return child.drive == parent and child != parent return False class Mounter(object): """ Mount utility. Stores environment variables (filter, prompt, browser, udisks) to use across multiple mount operations. :ivar udisks: adapter to the udisks service NOTE: The optional parameters are not guaranteed to keep their order and should always be passed as keyword arguments. """ def __init__(self, udisks, mount_options=None, ignore_device=None, prompt=None, browser=None, cache=None): """ Initialize mounter with the given defaults. :param udisks: udisks service object. May be a Sniffer or a Daemon. :param FilterMatcher filter: customize mount options and handleability :param callable prompt: retrieve passwords for devices :param callable browser: open devices If prompt is None, device unlocking will not work. If browser is None, browse will not work. """ self.udisks = udisks self._mount_options = mount_options or (lambda device: None) self._ignore_device = ignore_device or FilterMatcher([], False) self._ignore_device._filters += [ IgnoreDevice({'is_block': False, 'ignore': True}), IgnoreDevice({'is_external': False, 'ignore': True}), IgnoreDevice({'is_ignored': True, 'ignore': True})] self._prompt = prompt self._browser = browser self._cache = cache self._log = logging.getLogger(__name__) try: # propagate error messages to UDisks1 daemon for 'Job failed' # notifications. self._set_error = self.udisks.set_error except AttributeError: self._set_error = lambda device, action, message: None def _error(self, fn, device, err, fmt): message = exc_message(err) self._log.error(_('failed to {0} {1}: {2}', fn.__name__, device, message)) self._set_error(device, fn.__name__, message) return True @_sets_async_error @_find_device def browse(self, device): """ Browse device. :param device: device object, block device path or mount path :returns: success :rtype: bool """ if not device.is_mounted: self._log.error(_("not browsing {0}: not mounted", device)) yield Return(False) if not self._browser: self._log.error(_("not browsing {0}: no program", device)) yield Return(False) self._log.debug(_('opening {0} on {0.mount_paths[0]}', device)) self._browser(device.mount_paths[0]) self._log.info(_('opened {0} on {0.mount_paths[0]}', device)) yield Return(True) # mount/unmount @_sets_async_error @_find_device def mount(self, device): """ Mount the device if not already mounted. :param device: device object, block device path or mount path :returns: whether the device is mounted. :rtype: bool """ if not self.is_handleable(device) or not device.is_filesystem: self._log.warn(_('not mounting {0}: unhandled device', device)) yield Return(False) if device.is_mounted: self._log.info(_('not mounting {0}: already mounted', device)) yield Return(True) fstype = str(device.id_type) options = self._mount_options(device) kwargs = dict(fstype=fstype, options=options) self._log.debug(_('mounting {0} with {1}', device, kwargs)) mount_path = yield device.mount(**kwargs) self._log.info(_('mounted {0} on {1}', device, mount_path)) yield Return(True) @_sets_async_error @_find_device def unmount(self, device): """ Unmount a Device if mounted. :param device: device object, block device path or mount path :returns: whether the device is unmounted :rtype: bool """ if not self.is_handleable(device) or not device.is_filesystem: self._log.warn(_('not unmounting {0}: unhandled device', device)) yield Return(False) if not device.is_mounted: self._log.info(_('not unmounting {0}: not mounted', device)) yield Return(True) self._log.debug(_('unmounting {0}', device)) yield device.unmount() self._log.info(_('unmounted {0}', device)) yield Return(True) # unlock/lock (LUKS) @_sets_async_error @_find_device def unlock(self, device): """ Unlock the device if not already unlocked. :param device: device object, block device path or mount path :returns: whether the device is unlocked :rtype: bool """ if not self.is_handleable(device) or not device.is_crypto: self._log.warn(_('not unlocking {0}: unhandled device', device)) yield Return(False) if device.is_unlocked: self._log.info(_('not unlocking {0}: already unlocked', device)) yield Return(True) if not self._prompt: self._log.error(_('not unlocking {0}: no password prompt', device)) yield Return(False) unlocked = yield self._unlock_from_cache(device) if unlocked: yield Return(True) password = yield self._prompt(device) if password is None: self._log.debug(_('not unlocking {0}: cancelled by user', device)) yield Return(False) self._log.debug(_('unlocking {0}', device)) yield device.unlock(password) self._update_cache(device, password) self._log.info(_('unlocked {0}', device)) yield Return(True) @Coroutine.from_generator_function def _unlock_from_cache(self, device): if not self._cache: yield Return(False) try: password = self._cache[device] except KeyError: yield Return(False) self._log.debug(_('unlocking {0} using cached password', device)) try: yield device.unlock(password) except Exception: self._log.debug(_('failed to unlock {0} using cached password', device)) yield Return(False) self._log.debug(_('unlocked {0} using cached password', device)) yield Return(True) def _update_cache(self, device, password): if not self._cache: return self._cache[device] = password def forget_password(self, device): try: del self._cache[device] except KeyError: pass @_sets_async_error @_find_device def lock(self, device): """ Lock device if unlocked. :param device: device object, block device path or mount path :returns: whether the device is locked :rtype: bool """ if not self.is_handleable(device) or not device.is_crypto: self._log.warn(_('not locking {0}: unhandled device', device)) yield Return(False) if not device.is_unlocked: self._log.info(_('not locking {0}: not unlocked', device)) yield Return(True) self._log.debug(_('locking {0}', device)) yield device.lock() self._log.info(_('locked {0}', device)) yield Return(True) # add/remove (unlock/lock or mount/unmount) @_suppress_error @_find_device def add(self, device, recursive=False): """ Mount or unlock the device depending on its type. :param device: device object, block device path or mount path :param bool recursive: recursively mount and unlock child devices :returns: whether all attempted operations succeeded :rtype: bool """ if device.is_filesystem: success = yield self.mount(device) elif device.is_crypto: success = yield self.unlock(device) if success and recursive: self.udisks._sync() device = self.udisks[device.object_path] success = yield self.add( device.luks_cleartext_holder, recursive=True) elif recursive and device.is_partition_table: tasks = [] for dev in self.get_all_handleable(): if dev.is_partition and dev.partition_slave == device: tasks.append(self.add(dev, recursive=True)) # TODO: AND results success = yield AsyncList(tasks) else: self._log.info(_('not adding {0}: unhandled device', device)) yield Return(False) yield Return(success) @_suppress_error @_find_device def auto_add(self, device, recursive=False): """ Automatically attempt to mount or unlock a device, but be quiet if the device is not supported. :param device: device object, block device path or mount path :param bool recursive: recursively mount and unlock child devices :returns: whether all attempted operations succeeded :rtype: bool """ success = True if not self.is_handleable(device): pass elif device.is_filesystem: if not device.is_mounted: success = yield self.mount(device) elif device.is_crypto: if self._prompt and not device.is_unlocked: success = yield self.unlock(device) if success and recursive: self.udisks._sync() device = self.udisks[device.object_path] success = yield self.auto_add( device.luks_cleartext_holder, recursive=True) elif recursive and device.is_partition_table: tasks = [] for dev in self.get_all_handleable(): if dev.is_partition and dev.partition_slave == device: tasks.append(self.auto_add(dev, recursive=True)) # TODO: AND results success = yield AsyncList(tasks) else: self._log.debug(_('not adding {0}: unhandled device', device)) yield Return(success) @_suppress_error @_find_device def remove(self, device, force=False, detach=False, eject=False, lock=False): """ Unmount or lock the device depending on device type. :param device: device object, block device path or mount path :param bool force: recursively remove all child devices :param bool detach: detach the root drive :param bool eject: remove media from the root drive :param bool lock: lock the associated LUKS cleartext slave :returns: whether all attempted operations succeeded :rtype: bool """ if device.is_filesystem: success = yield self.unmount(device) elif device.is_crypto: if force and device.is_unlocked: yield self.auto_remove(device.luks_cleartext_holder, force=True) success = yield self.lock(device) elif force and (device.is_partition_table or device.is_drive): tasks = [] for child in self.get_all_handleable(): if _is_parent_of(device, child): tasks.append(self.auto_remove( child, force=True, detach=detach, eject=eject, lock=lock)) # TODO: AND results success = yield AsyncList(tasks) else: self._log.info(_('not removing {0}: unhandled device', device)) success = False # if these operations work, everything is fine, we can return True: if lock and device.is_luks_cleartext: device = device.luks_cleartext_slave success = yield self.lock(device) if eject: success = yield self.eject(device) if detach: success = yield self.detach(device) yield Return(success) @_suppress_error @_find_device def auto_remove(self, device, force=False, detach=False, eject=False, lock=False): """ Unmount or lock the device depending on device type. :param device: device object, block device path or mount path :param bool force: recursively remove all child devices :param bool detach: detach the root drive :param bool eject: remove media from the root drive :param bool lock: lock the associated LUKS cleartext slave :returns: whether all attempted operations succeeded :rtype: bool """ success = True if not self.is_handleable(device): pass elif device.is_filesystem: if device.is_mounted: success = yield self.unmount(device) elif device.is_crypto: if force and device.is_unlocked: yield self.auto_remove(device.luks_cleartext_holder, force=True) if device.is_unlocked: success = yield self.lock(device) elif force and (device.is_partition_table or device.is_drive): tasks = [] for child in self.get_all_handleable(): if _is_parent_of(device, child): tasks.append(self.auto_remove( child, force=True, detach=detach, eject=eject, lock=lock)) # TODO: AND results success = yield AsyncList(tasks) else: self._log.debug(_('not removing {0}: unhandled device', device)) # if these operations work, everything is fine, we can return True: if lock and device.is_luks_cleartext: device = device.luks_cleartext_slave success = yield self.lock(device) if eject and device.has_media: success = yield self.eject(device) if detach and device.is_detachable: success = yield self.detach(device) yield Return(success) # eject/detach device @_sets_async_error @_find_device def eject(self, device, force=False): """ Eject a device after unmounting all its mounted filesystems. :param device: device object, block device path or mount path :param bool force: remove child devices before trying to eject :returns: whether the operation succeeded :rtype: bool """ if not self.is_handleable(device): self._log.warn(_('not ejecting {0}: unhandled device')) yield Return(False) drive = device.drive if not (drive.is_drive and drive.is_ejectable): self._log.warn(_('not ejecting {0}: drive not ejectable', drive)) yield Return(False) if force: yield self.auto_remove(drive, force=True) self._log.debug(_('ejecting {0}', device)) yield drive.eject() self._log.info(_('ejected {0}', device)) yield Return(True) @_sets_async_error @_find_device def detach(self, device, force=False): """ Detach a device after unmounting all its mounted filesystems. :param device: device object, block device path or mount path :param bool force: remove child devices before trying to detach :returns: whether the operation succeeded :rtype: bool """ if not self.is_handleable(device): self._log.warn(_('not detaching {0}: unhandled device', device)) yield Return(False) drive = device.root if not drive.is_detachable: self._log.warn(_('not detaching {0}: drive not detachable', drive)) yield Return(False) if force: yield self.auto_remove(drive, force=True) self._log.debug(_('detaching {0}', device)) yield drive.detach() self._log.info(_('detached {0}', device)) yield Return(True) # mount_all/unmount_all def add_all(self, recursive=False): """ Add all handleable devices that available at start. :param bool recursive: recursively mount and unlock child devices :returns: whether all attempted operations succeeded :rtype: bool """ tasks = [] for device in self.udisks: tasks.append(self.auto_add(device, recursive=recursive)) # TODO: AND results return AsyncList(tasks) def remove_all(self, detach=False, eject=False, lock=False): """ Remove all filesystems handleable by udiskie. :param bool detach: detach the root drive :param bool eject: remove media from the root drive :param bool lock: lock the associated LUKS cleartext slave :returns: whether all attempted operations succeeded :rtype: bool """ tasks = [] remove_args = dict(force=True, detach=detach, eject=eject, lock=lock) for device in self.get_all_handleable(): if device.parent_object_path != '/': continue tasks.append(self.auto_remove(device, **remove_args)) # TODO: AND results return AsyncList(tasks) # iterate devices def is_handleable(self, device): # TODO: handle pathes in first argument """ Check whether this device should be handled by udiskie. :param device: device object, block device path or mount path :returns: handleability :rtype: bool Currently this just means that the device is removable and holds a filesystem or the device is a LUKS encrypted volume. """ return not self._ignore_device(device) def is_addable(self, device): """ Check if device can be added with ``auto_add``. """ if not self.is_handleable(device): return False if device.is_filesystem: return not device.is_mounted if device.is_crypto: return self._prompt and not device.is_unlocked if device.is_partition_table: return any(self.is_addable(dev) for dev in self.get_all_handleable() if dev.partition_slave == device) return False def is_removable(self, device): """ Check if device can be removed with ``auto_remove``. """ if not self.is_handleable(device): return False if device.is_filesystem: return device.is_mounted if device.is_crypto: return device.is_unlocked if device.is_partition_table or device.is_drive: return any(self.is_removable(dev) for dev in self.get_all_handleable() if _is_parent_of(device, dev)) return False def get_all_handleable(self): """ Enumerate all handleable devices currently known to udisks. :returns: handleable devices :rtype: iterable NOTE: returns only devices that are still valid. This protects from race conditions inside udiskie. """ return filter(self.is_handleable, self.udisks) # data structs containing the menu hierarchy: Device = namedtuple('Device', ['root', 'branches', 'device', 'label', 'methods']) Action = namedtuple('Action', ['method', 'device', 'label', 'action']) Branch = namedtuple('Branch', ['label', 'groups']) class DeviceActions(object): _labels = { 'browse': _('Browse {0}'), 'mount': _('Mount {0}'), 'unmount': _('Unmount {0}'), 'unlock': _('Unlock {0}'), 'lock': _('Lock {0}'), 'eject': _('Eject {0}'), 'detach': _('Unpower {0}'), 'forget_password': _('Clear password for {0}'), } def __init__(self, mounter, actions={}): self._mounter = mounter self._actions = _actions = actions.copy() setdefault(_actions, { 'browse': mounter.browse, 'mount': mounter.mount, 'unmount': mounter.unmount, 'unlock': mounter.unlock, 'lock': partial(mounter.remove, force=True), 'eject': partial(mounter.eject, force=True), 'detach': partial(mounter.detach, force=True), 'forget_password': mounter.forget_password, }) def detect(self, root_device=''): """ Detect all currently known devices. :param str root_device: object path of root device to return :returns: root of device hierarchy :rtype: Device """ root = Device(None, [], None, "", []) device_nodes = dict(map(self._device_node, self._mounter.get_all_handleable())) # insert child devices as branches into their roots: for object_path, node in device_nodes.items(): device_nodes.get(node.root, root).branches.append(node) if not root_device: return root return device_nodes[root_device] def _get_device_methods(self, device): """Return an iterable over all available methods the device has.""" if device.is_filesystem: if device.is_mounted: yield 'browse' yield 'unmount' else: yield 'mount' elif device.is_crypto: if device.is_unlocked: yield 'lock' else: yield 'unlock' cache = self._mounter._cache if cache and device in cache: yield 'forget_password' if device.is_ejectable and device.has_media: yield 'eject' if device.is_detachable: yield 'detach' def _device_node(self, device): """Create an empty menu node for the specified device.""" label = device.ui_label # determine available methods methods = [Action(method, device, self._labels[method].format(label), partial(self._actions[method], device)) for method in self._get_device_methods(device)] # find the root device: if device.is_partition: root = device.partition_slave.object_path elif device.is_luks_cleartext: root = device.luks_cleartext_slave.object_path else: root = None # in this first step leave branches empty return device.object_path, Device(root, [], device, label, methods) def prune_empty_node(node, seen): """ Recursively remove empty branches and return whether this makes the node itself empty. The ``seen`` parameter is used to avoid infinite recursion due to cycles (you never know). """ if node.methods: return False if id(node) in seen: return True seen = seen | {id(node)} for branch in list(node.branches): if prune_empty_node(branch, seen): node.branches.remove(branch) else: return False return True udiskie-1.4.9/udiskie/automount.py0000644000175000001440000000261212642325627017710 0ustar thomasusers00000000000000""" Automount utility. """ from __future__ import absolute_import from __future__ import unicode_literals __all__ = ['AutoMounter'] class AutoMounter(object): """ Automount utility. Being connected to the udiskie daemon, this component automatically mounts newly discovered external devices. Instances are constructed with a Mounter object, like so: >>> AutoMounter(Mounter(udisks=Daemon())) """ def __init__(self, mounter): """ Store mounter as member variable and connect to the underlying udisks. :param Mounter mounter: mounter object """ self._mounter = mounter mounter.udisks.connect('device_changed', self.device_changed) mounter.udisks.connect('device_added', mounter.auto_add) mounter.udisks.connect('media_added', mounter.auto_add) def device_changed(self, old_state, new_state): """ Mount newly mountable devices. :param Device old_state: before change :param Device new_state: after change """ # udisks2 sometimes adds empty devices and later updates them which # makes is_external become true not at device_added time: if (self._mounter.is_addable(new_state) and not self._mounter.is_addable(old_state) and not self._mounter.is_removable(old_state)): self._mounter.auto_add(new_state) udiskie-1.4.9/udiskie/config.py0000644000175000001440000001672012656353375017135 0ustar thomasusers00000000000000""" Config utilities. For an example config file, see the manual. If you don't have the man page installed, a raw version is available in doc/udiskie.8.txt. """ from __future__ import absolute_import from __future__ import unicode_literals import logging import os from .common import exc_message from .compat import basestring, fix_str_conversions from .locale import _ __all__ = ['DeviceFilter', 'FilterMatcher', 'Config'] def lower(s): try: return s.lower() except AttributeError: return s def yaml_load(stream): """Load YAML document, but load all strings as unicode on py2.""" import yaml class UnicodeLoader(yaml.SafeLoader): pass UnicodeLoader.add_constructor( yaml.resolver.BaseResolver.DEFAULT_SCALAR_TAG, UnicodeLoader.construct_scalar) return yaml.load(stream, UnicodeLoader) @fix_str_conversions class DeviceFilter(object): """Associate a certain value to matching devices.""" VALID_PARAMETERS = [ 'is_drive', 'is_block', 'is_partition_table', 'is_partition', 'is_filesystem', 'is_luks', 'is_loop', 'is_toplevel', 'is_detachable', 'is_ejectable', 'has_media', 'device_file', 'device_presentation', 'device_id', 'id_usage', 'is_crypto', 'is_ignored', 'id_type', 'id_label', 'id_uuid', 'is_luks_cleartext', 'is_external', 'is_systeminternal', 'is_mounted', 'mount_paths', 'is_unlocked', 'in_use', 'should_automount', 'ui_label', 'loop_filename', ] def __init__(self, match, value): """ Construct an instance. :param dict match: device attributes :param list value: value """ self._log = logging.getLogger(__name__) self._match = match.copy() # the use of keys() makes deletion inside the loop safe: for k in self._match.keys(): if k not in self.VALID_PARAMETERS: self._log.warn(_('Unknown matching attribute: {!r}', k)) del self._match[k] self._value = value self._log.debug(_('{0} created', self)) def __str__(self): return _('{0}(match={1!r}, value={2!r})', self.__class__.__name__, self._match, self._value) def match(self, device): """ Check if the device matches this filter. :param Device device: device to be checked """ return all(lower(getattr(device, k)) == lower(v) for k, v in self._match.items()) def value(self, device): """ Get the associated value. :param Device device: matched device If :meth:`match` is False for the device, the return value of this method is undefined. """ self._log.debug(_('{0} used for {1}', self, device.object_path)) return self._value class MountOptions(DeviceFilter): """Associate a list of mount options to matched devices.""" def __init__(self, config_item): """Parse the MountOptions filter from the config item.""" config_item = config_item.copy() options = config_item.pop('options') if isinstance(options, basestring): options = [o.strip() for o in options.split(',')] super(MountOptions, self).__init__(config_item, options) class IgnoreDevice(DeviceFilter): """Associate a boolean ignore flag to matched devices.""" def __init__(self, config_item): """Parse the IgnoreDevice filter from the config item.""" config_item = config_item.copy() ignore = config_item.pop('ignore', True) super(IgnoreDevice, self).__init__(config_item, ignore) class FilterMatcher(object): """Matches devices against multiple `DeviceFilter`s.""" def __init__(self, filters, default): """ Construct a FilterMatcher instance from list of DeviceFilter. :param list filters: """ self._filters = list(filters) self._default = default def __call__(self, device): """ Matches devices against multiple :class:`DeviceFilter`s. :param default: default value :param list filters: device filters :param Device device: device to be mounted :returns: value of the first matching filter """ matches = (f.value(device) for f in self._filters if f.match(device)) return next(matches, self._default) class Config(object): """Udiskie config in memory representation.""" def __init__(self, data): """ Initialize with preparsed data object. :param ConfigParser data: config file accessor """ self._data = data @classmethod def default_pathes(cls): """ Return the default config file pathes. :rtype: list """ try: from xdg.BaseDirectory import xdg_config_home as config_home except ImportError: config_home = os.path.expanduser('~/.config') return [os.path.join(config_home, 'udiskie', 'config.yml'), os.path.join(config_home, 'udiskie', 'config.json')] @classmethod def from_file(cls, path=None): """ Read config file. :param str path: YAML config file name :returns: configuration object :rtype: Config :raises IOError: if the path does not exist """ # None => use default if path is None: for path in cls.default_pathes(): try: return cls.from_file(path) except IOError as e: logging.getLogger(__name__).debug( _("Failed to read config file: {0}", exc_message(e))) except ImportError as e: logging.getLogger(__name__).warn( _("Failed to read {0!r}: {1}", path, exc_message(e))) return cls({}) # False/'' => no config if not path: return cls({}) if os.path.splitext(path)[1].lower() == '.json': from json import load else: load = yaml_load with open(path) as f: return cls(load(f)) @property def mount_options(self): """Get a MountOptions filter list from the config data.""" config_list = self._data.get('mount_options', []) return FilterMatcher(map(MountOptions, config_list), None) @property def ignore_device(self): """Get a IgnoreDevice filter list from the config data""" config_list = self._data.get('ignore_device', []) return FilterMatcher(map(IgnoreDevice, config_list), False) @property def program_options(self): """Get the program options dictionary from the config file.""" return self._data.get('program_options', {}).copy() @property def notifications(self): """Get the notification timeouts dictionary from the config file.""" return self._data.get('notifications', {}).copy() @property def icon_names(self): """Get the icon names dictionary from the config file.""" return self._data.get('icon_names', {}).copy() @property def notification_actions(self): """Get the notification actions dictionary from the config file.""" return self._data.get('notification_actions', {}).copy() udiskie-1.4.9/udiskie/dbus.py0000644000175000001440000003175312642325627016622 0ustar thomasusers00000000000000""" Common DBus utilities. """ from __future__ import absolute_import from __future__ import unicode_literals import sys from functools import partial from gi.repository import Gio from gi.repository import GLib from .async_ import Async, Coroutine, Return from .common import format_exc __all__ = [ 'InterfaceProxy', 'PropertiesProxy', 'ObjectProxy', 'BusProxy', 'connect_service', 'MethodsProxy', ] if sys.version_info >= (3,): unpack_variant = GLib.Variant.unpack else: dict_type = GLib.VariantType.new('a{?*}') def unpack_variant(v): """Unpack a GLib.Variant.""" if v.get_type_string() in 'sog': return v.get_string().decode('utf-8') t = v.get_type() if t.is_basic(): return v.unpack() elems = [v.get_child_value(i) for i in range(v.n_children())] if t.is_subtype_of(dict_type): return dict(map(unpack_variant, elems)) if t.is_tuple(): return tuple(map(unpack_variant, elems)) if t.is_array(): return list(map(unpack_variant, elems)) if t.is_dict_entry(): return list(map(unpack_variant, elems)) # The following cases should never occur for DBus data: if t.is_variant(): return unpack_variant(elems[0]) if t.is_maybe(): return unpack_variant(elems[0]) if elems else None raise ValueError("Unknown variant type: {}!".format(v.get_type_string())) class DBusCall(Async): """ Asynchronously call a DBus method. """ def __init__(self, proxy, method_name, signature, args, flags=0, timeout_msec=-1): """ Asynchronously call the specified method on a DBus proxy object. :param Gio.DBusProxy proxy: :param str method_name: :param str signature: :param tuple args: :param int flags: :param int timeout_msec: """ cancellable = None user_data = None proxy.call( method_name, GLib.Variant(signature, tuple(args)), flags, timeout_msec, cancellable, self._callback, user_data, ) def _callback(self, proxy, result, user_data): """ Handle call result. :param Gio.DBusProxy proxy: :param Gio.AsyncResult result: :param user_data: unused """ try: value = proxy.call_finish(result) except Exception as e: self.errback(e, format_exc()) else: self.callback(*unpack_variant(value)) class InterfaceProxy(object): """ DBus proxy object for a specific interface. Provides attribute accessors to properties and methods of a DBus interface on a DBus object. :ivar str object_path: object path of the DBus object :ivar PropertiesProxy property: attribute access to DBus properties :ivar Gio.DBusProxy method: attribute access to DBus methods :ivar Gio.DBusProxy _proxy: underlying proxy object """ def __init__(self, proxy): """ Initialize property and method attribute accessors for the interface. :param Gio.DBusProxy proxy: accessed object :param str interface: accessed interface """ self._proxy = proxy self.object_path = proxy.get_object_path() @property def object(self): """ Get a proxy for the underlying object. :rtype: ObjectProxy """ proxy = self._proxy return ObjectProxy(proxy.get_connection(), proxy.get_name(), proxy.get_object_path()) def connect(self, event, handler): """ Connect to a DBus signal. :param str event: event name :param handler: callback :returns: subscription id :rtype: int """ interface = self._proxy.get_interface_name() return self.object.connect(interface, event, handler) def call(self, method_name, signature='()', *args): return DBusCall(self._proxy, method_name, signature, args) class PropertiesProxy(InterfaceProxy): Interface = 'org.freedesktop.DBus.Properties' def __init__(self, proxy, interface_name=None): super(PropertiesProxy, self).__init__(proxy) self.interface_name = interface_name def GetAll(self, interface_name=None): return self.call('GetAll', '(s)', interface_name or self.interface_name) class ObjectProxy(object): """ Simple proxy class for a DBus object. :param Gio.DBusConnection connection: :param str bus_name: :param str object_path: """ def __init__(self, connection, bus_name, object_path): """ Initialize member variables. :ivar Gio.DBusConnection connection: :ivar str bus_name: :ivar str object_path: This performs no IO at all. """ self.connection = connection self.bus_name = bus_name self.object_path = object_path def _get_interface(self, name): """ Get a Gio native interface proxy for this Dbus object. :param str name: interface name :returns: a proxy object for the other interface :rtype: Gio.DBusProxy """ return DBusProxyNew( self.connection, Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, info=None, name=self.bus_name, object_path=self.object_path, interface_name=name, ) @Coroutine.from_generator_function def get_interface(self, name): """ Get an interface proxy for this Dbus object. :param str name: interface name :returns: a proxy object for the other interface :rtype: InterfaceProxy """ proxy = yield self._get_interface(name) yield Return(InterfaceProxy(proxy)) @Coroutine.from_generator_function def get_property_interface(self, interface_name=None): proxy = yield self._get_interface(PropertiesProxy.Interface) yield Return(PropertiesProxy(proxy, interface_name)) @property def bus(self): """ Get a proxy object for the underlying bus. :rtype: BusProxy """ return BusProxy(self.connection, self.bus_name) def connect(self, interface, event, handler): """ Connect to a DBus signal. :param str interface: interface name :param str event: event name :param handler: callback :returns: subscription id :rtype: int """ object_path = self.object_path return self.bus.connect(interface, event, object_path, handler) @Coroutine.from_generator_function def call(self, interface_name, method_name, signature='()', *args): proxy = yield self.get_interface(interface_name) result = yield proxy.call(method_name, signature, *args) yield Return(result) class DBusCallback(object): def __init__(self, handler): """Store reference to handler.""" self._handler = handler def __call__(self, connection, sender_name, object_path, interface_name, signal_name, parameters, *user_data): """Call handler unpacked signal parameters.""" return self._handler(*unpack_variant(parameters)) class DBusCallbackWithObjectPath(object): def __init__(self, handler): """Store reference to handler.""" self._handler = handler def __call__(self, connection, sender_name, object_path, interface_name, signal_name, parameters, *user_data): """Call handler with object_path and unpacked signal parameters.""" return self._handler(object_path, *unpack_variant(parameters)) class BusProxy(object): """ Simple proxy class for a connected bus. :ivar Gio.DBusConnection connection: :ivar str bus_name: """ def __init__(self, connection, bus_name): """ Initialize member variables. :param Gio.DBusConnection connection: :param str bus_name: This performs IO at all. """ self.connection = connection self.bus_name = bus_name def get_object(self, object_path): """ Get a object representing the specified object. :param str object_path: object path :returns: a simple representative for the object :rtype: ObjectProxy """ return ObjectProxy(self.connection, self.bus_name, object_path) def connect(self, interface, event, object_path, handler): """ Connect to a DBus signal. :param str interface: interface name :param str event: event name :param str object_path: object path or ``None`` :param handler: callback """ if object_path: callback = DBusCallback(handler) else: callback = DBusCallbackWithObjectPath(handler) return self.connection.signal_subscribe( self.bus_name, interface, event, object_path, None, Gio.DBusSignalFlags.NONE, callback, None, ) def disconnect(self, subscription_id): """ Disconnect a DBus signal subscription. """ self.connection.signal_unsubscribe(subscription_id) class DBusProxyNew(Async): """ Asynchronously call a DBus method. """ def __init__(self, connection, flags, info, name, object_path, interface_name): """ Asynchronously call the specified method on a DBus proxy object. """ cancellable = None user_data = None Gio.DBusProxy.new( connection, flags, info, name, object_path, interface_name, cancellable, self._callback, user_data, ) def _callback(self, proxy, result, user_data): """ Handle call result. :param Gio.DBusProxy proxy: :param Gio.AsyncResult result: :param user_data: unused """ try: value = Gio.DBusProxy.new_finish(result) if value is None: raise RuntimeError("Failed to connect DBus object!") except Exception as e: self.errback(e, format_exc()) else: self.callback(value) class DBusProxyNewForBus(Async): """ Asynchronously call a DBus method. """ def __init__(self, bus_type, flags, info, name, object_path, interface_name): """ Asynchronously call the specified method on a DBus proxy object. """ cancellable = None user_data = None Gio.DBusProxy.new_for_bus( bus_type, flags, info, name, object_path, interface_name, cancellable, self._callback, user_data, ) def _callback(self, proxy, result, user_data): """ Handle call result. :param Gio.DBusProxy proxy: :param Gio.AsyncResult result: :param user_data: unused """ try: value = Gio.DBusProxy.new_for_bus_finish(result) if value is None: raise RuntimeError("Failed to connect DBus object!") except Exception as e: self.errback(e, format_exc()) else: self.callback(value) @Coroutine.from_generator_function def connect_service(cls): """ Connect to the service object on DBus. :returns: new proxy object for the service :rtype: InterfaceProxy :raises BusException: if unable to connect to service. """ proxy = yield DBusProxyNewForBus( Gio.BusType.SYSTEM, Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, info=None, name=cls.BusName, object_path=cls.ObjectPath, interface_name=cls.Interface, ) yield Return(InterfaceProxy(proxy)) class MethodsProxy(object): """Provide methods as attributes for one interface of a DBus object.""" def __init__(self, object_proxy, interface_name): """Initialize from (ObjectProxy, str).""" self._object_proxy = object_proxy self._interface_name = interface_name def __getattr__(self, name): """Get a proxy for the specified method on this interface.""" return partial(self._object_proxy.call, self._interface_name, name) udiskie-1.4.9/udiskie/tray.py0000644000175000001440000002742712677614541016653 0ustar thomasusers00000000000000""" Tray icon for udiskie. """ from __future__ import absolute_import from __future__ import unicode_literals from gi.repository import Gio from gi.repository import Gtk from .async_ import Async from .common import setdefault from .compat import basestring from .locale import _ from .mount import Action, Branch, prune_empty_node __all__ = ['UdiskieMenu', 'SmartUdiskieMenu', 'TrayIcon'] class Icons(object): """Encapsulates the responsibility to load icons.""" _icon_names = { 'media': [ 'drive-removable-media-usb-pendrive', 'drive-removable-media-usb', 'drive-removable-media', 'media-optical', 'media-flash', ], 'browse': ['document-open', 'folder-open'], 'mount': ['udiskie-mount'], 'unmount': ['udiskie-unmount'], 'unlock': ['udiskie-unlock'], 'lock': ['udiskie-lock'], 'eject': ['udiskie-eject', 'media-eject'], 'detach': ['udiskie-detach'], 'quit': ['application-exit'], 'forget_password': ['edit-delete'], } def __init__(self, icon_names={}): """Merge ``icon_names`` into default icon names.""" _icon_names = icon_names.copy() setdefault(_icon_names, self.__class__._icon_names) self._icon_names = _icon_names for k, v in _icon_names.items(): if isinstance(v, basestring): self._icon_names[k] = [v] def get_icon(self, icon_id, size): """ Load icon dynamically. :param str icon_id: udiskie internal icon id :param GtkIconSize size: requested size :returns: the loaded icon :rtype: Gtk.Image """ return Gtk.Image.new_from_gicon(self.get_gicon(icon_id), size) def get_gicon(self, icon_id): """ Lookup the GTK icon name corresponding to the specified internal id. :param str icon_id: udiskie internal icon id :param GtkIconSize size: requested size :returns: the loaded icon :rtype: Gio.Icon """ return Gio.ThemedIcon.new_from_names(self._icon_names[icon_id]) class UdiskieMenu(object): """ Builder for udiskie menus. Objects of this class generate action menus when being called. """ _quit_label = _('Quit') def __init__(self, mounter, icons, actions, quit_action=None): """ Initialize a new menu maker. :param object mounter: mount operation provider :param Icons icons: icon provider :param DeviceActions actions: device actions discovery :returns: a new menu maker :rtype: cls Required keys for the ``_labels``, ``_menu_icons`` and ``actions`` dictionaries are: - browse Open mount location - mount Mount a device - unmount Unmount a device - unlock Unlock a LUKS device - lock Lock a LUKS device - eject Eject a drive - detach Detach (power down) a drive - quit Exit the application NOTE: If using a main loop other than ``Gtk.main`` the 'quit' action must be customized. """ self._icons = icons self._mounter = mounter self._actions = actions self._quit_action = quit_action def __call__(self): """ Create menu for udiskie mount operations. :returns: a new menu :rtype: Gtk.Menu """ # create actions items menu = self._branchmenu(self._prepare_menu(self.detect()).groups) # append menu item for closing the application if self._quit_action: if len(menu) > 0: menu.append(Gtk.SeparatorMenuItem()) menu.append(self._menuitem( self._quit_label, self._icons.get_icon('quit', Gtk.IconSize.MENU), lambda _: self._quit_action() )) return menu def detect(self): """ Detect all currently known devices. :returns: root of device hierarchy :rtype: Device """ root = self._actions.detect() prune_empty_node(root, set()) return root def _branchmenu(self, groups): """ Create a menu from the given node. :param Branch groups: contains information about the menu :returns: a new menu object holding all groups of the node :rtype: Gtk.Menu """ def make_action_callback(node): return lambda _: node.action() menu = Gtk.Menu() separate = False for group in groups: if len(group) > 0: if separate: menu.append(Gtk.SeparatorMenuItem()) separate = True for node in group: if isinstance(node, Action): menu.append(self._menuitem( node.label, self._icons.get_icon(node.method, Gtk.IconSize.MENU), make_action_callback(node))) elif isinstance(node, Branch): menu.append(self._menuitem( node.label, icon=None, onclick=self._branchmenu(node.groups))) else: raise ValueError(_("Invalid node!")) return menu def _menuitem(self, label, icon, onclick): """ Create a generic menu item. :param str label: text :param Gtk.Image icon: icon (may be ``None``) :param onclick: onclick handler, either a callable or Gtk.Menu :returns: the menu item object :rtype: Gtk.MenuItem """ if icon is None: item = Gtk.MenuItem() else: item = Gtk.ImageMenuItem() item.set_image(icon) # I don't really care for the "show icons only for nouns, not # for verbs" policy: item.set_always_show_image(True) if label is not None: item.set_label(label) if isinstance(onclick, Gtk.Menu): item.set_submenu(onclick) else: item.connect('activate', onclick) return item def _prepare_menu(self, node): """ Prepare the menu hierarchy from the given device tree. :param Device node: root node of device hierarchy :returns: menu hierarchy :rtype: Branch """ return Branch( label=node.label, groups=[ [self._prepare_menu(branch) for branch in node.branches if branch.methods or branch.branches], node.methods, ]) class SmartUdiskieMenu(UdiskieMenu): def _actions_group(self, node, presentation): """ Create the actions group for the specified device node. :param Device node: device :param str presentation: node label """ labels = self._actions._labels return [Action(action.method, action.device, labels[action.method].format(presentation), action.action) for action in node.methods] def _collapse_device(self, node, presentation=""): """Collapse device hierarchy into a flat folder.""" if (not presentation or node.device.is_mounted or not node.device.is_luks_cleartext): presentation = node.label groups = [group for branch in node.branches for group in self._collapse_device(branch, presentation) if group] groups.append(self._actions_group(node, presentation)) return groups def _prepare_menu(self, node): """Overrides UdiskieMenu._prepare_menu.""" return Branch( label=node.label, groups=[ [Branch(branch.label, self._collapse_device(branch)) for branch in node.branches if branch.methods or branch.branches], ]) class TrayIcon(object): """Default TrayIcon class.""" def __init__(self, menumaker, icons, statusicon=None, show=True): """ Create an object managing a tray icon. The actual Gtk.StatusIcon is only created as soon as you call show() for the first time. The reason to delay its creation is that the GTK icon will be initially visible, which results in a perceptable flickering. :param UdiskieMenu menumaker: menu factory :param Gtk.StatusIcon statusicon: status icon """ self._icons = icons self._icon = statusicon self._menu = menumaker self._conn_left = None self._conn_right = None self.task = Async() menumaker._quit_action = self.destroy if show: self.show() def destroy(self): self.show(False) self.task.callback() def _create_statusicon(self): """Return a new Gtk.StatusIcon.""" statusicon = Gtk.StatusIcon() statusicon.set_from_gicon(self._icons.get_gicon('media')) statusicon.set_tooltip_text(_("udiskie")) return statusicon @property def visible(self): """Return visibility state of icon.""" return bool(self._conn_left) def show(self, show=True): """Show or hide the tray icon.""" if show and not self.visible: self._show() if not show and self.visible: self._hide() def _show(self): """Show the tray icon.""" if not self._icon: self._icon = self._create_statusicon() widget = self._icon widget.set_visible(True) self._conn_left = widget.connect("activate", self._activate) self._conn_right = widget.connect("popup-menu", self._popup_menu) def _hide(self): """Hide the tray icon.""" self._icon.set_visible(False) self._icon.disconnect(self._conn_left) self._icon.disconnect(self._conn_right) self._conn_left = None self._conn_right = None def create_context_menu(self): """Create the context menu.""" return self._menu() def _activate(self, icon): """Handle a left click event (show the menu).""" self._popup_menu(icon, button=0, time=Gtk.get_current_event_time()) def _popup_menu(self, icon, button, time): """Handle a right click event (show the menu).""" m = self.create_context_menu() m.show_all() m.popup(parent_menu_shell=None, parent_menu_item=None, func=icon.position_menu, data=icon, button=button, activate_time=time) # need to store reference or menu will be destroyed before showing: self._m = m class AutoTray(TrayIcon): """ TrayIcon that automatically hides. The menu has no 'Quit' item, and the tray icon will automatically hide if there is no action available. """ def __init__(self, menumaker, icons): """ Create and automatically set visibility of a new status icon. Overrides TrayIcon.__init__. """ super(AutoTray, self).__init__(menumaker, icons, show=False) # Okay, the following is BAD: menumaker._quit_action = None udisks = menumaker._mounter.udisks udisks.connect('device_changed', self.update) udisks.connect('device_added', self.update) udisks.connect('device_removed', self.update) self.update() def has_menu(self): """Check if a menu action is available.""" return any(self._menu._prepare_menu(self._menu.detect()).groups) def update(self, *args): """Show/hide icon depending on whether there are devices.""" self.show(self.has_menu()) udiskie-1.4.9/udiskie/udisks1.py0000644000175000001440000005264512656516062017253 0ustar thomasusers00000000000000""" UDisks wrapper utilities. These act as a convenience abstraction layer on the UDisks DBus service. Requires UDisks 1.0.5 as described here: http://udisks.freedesktop.org/docs/1.0.5/ This wraps the DBus API of Udisks2 providing a common interface with the udisks2 module. :class:`Daemon` caches all device states and listens to UDisks events to guarantee the accessibilityy of device properties in between operations. """ from __future__ import absolute_import from __future__ import unicode_literals from collections import defaultdict import logging import os.path from gi.repository import GLib from .async_ import AsyncList, Coroutine, Return from .common import Emitter, samefile, AttrDictView, wraps, NullDevice from .compat import fix_str_conversions from .dbus import connect_service, MethodsProxy from .locale import _ __all__ = [ 'Daemon', ] def filter_opt(opt): """Remove ``None`` values from a dictionary.""" return [k for k, v in opt.items() if v] @fix_str_conversions class Device(object): """Helper base class for devices.""" Interface = 'org.freedesktop.UDisks.Device' def __init__(self, daemon, object_path, property_proxy, method_proxy): """ Initialize an instance with the given DBus proxy object. :param dbus.ObjectProxy object: """ self._daemon = daemon self.object_path = object_path self._P = property_proxy self._M = method_proxy def __str__(self): """Display as object path.""" return self.object_path def __eq__(self, other): """Comparison by object path.""" return self.object_path == str(other) def __ne__(self, other): """Comparison by object path.""" return not (self == other) def is_file(self, path): """Comparison by mount and device file path.""" return (samefile(path, self.device_file) or samefile(path, self.loop_file) or any(samefile(path, mp) for mp in self.mount_paths)) # availability of interfaces @property def is_drive(self): """Check if the device is a drive.""" return self._P.DeviceIsDrive @property def is_block(self): """Check if the device is a block device.""" return True @property def is_partition_table(self): """Check if the device is a partition table.""" return self._P.DeviceIsPartitionTable @property def is_partition(self): """Check if the device has a partition slave.""" return self._P.DeviceIsPartition @property def is_filesystem(self): """Check if the device is a filesystem.""" return self.id_usage == 'filesystem' @property def is_luks(self): """Check if the device is a LUKS container.""" return self._P.DeviceIsLuks @property def is_loop(self): """Check if the device is a loop device.""" return self._P.DeviceIsLinuxLoop # ---------------------------------------- # Drive # ---------------------------------------- # Drive properties is_toplevel = is_drive @property def is_detachable(self): """Check if the drive that owns this device can be detached.""" if not self.is_drive: return None return self._P.DriveCanDetach @property def is_ejectable(self): """Check if the drive that owns this device can be ejected.""" if not self.is_drive: return None return self._P.DriveIsMediaEjectable @property def has_media(self): """Check if there is media available in the drive.""" return self._P.DeviceIsMediaAvailable # Drive methods def eject(self, unmount=False): """Eject media from the device.""" return self._M.DriveEject( '(as)', filter_opt({'unmount': unmount})) def detach(self): """Detach the device by e.g. powering down the physical port.""" return self._M.DriveDetach('(as)', []) # ---------------------------------------- # Block # ---------------------------------------- # Block properties @property def device_file(self): """The filesystem path of the device block file.""" return os.path.normpath(self._P.DeviceFile) @property def device_presentation(self): """The device file path to present to the user.""" return self._P.DeviceFilePresentation # TODO: device_size missing @property def id_usage(self): """Device usage class, for example 'filesystem' or 'crypto'.""" return self._P.IdUsage @property def is_crypto(self): """Check if the device is a crypto device.""" return self.id_usage == 'crypto' @property def is_ignored(self): """Check if the device should be ignored.""" return self._P.DevicePresentationHide @property def device_id(self): """ Return a unique and persistent identifier for the device. This is the basename (last path component) of the symlink in `/dev/disk/by-id/`. """ for filename in self._P.DeviceFileById: parts = filename.split('/') if parts[-2] == 'by-id': return parts[-1] return '' @property def id_type(self): """" Return IdType property. This field provides further detail on IdUsage, for example: IdUsage 'filesystem' 'crypto' IdType 'ext4' 'crypto_LUKS' """ return self._P.IdType @property def id_label(self): """Label of the device if available.""" return self._P.IdLabel @property def id_uuid(self): """Device UUID.""" return self._P.IdUuid @property def luks_cleartext_slave(self): """Get luks crypto device.""" if not self.is_luks_cleartext: return None return self._daemon[self._P.LuksCleartextSlave] @property def is_luks_cleartext(self): """Check whether this is a luks cleartext device.""" return self._P.DeviceIsLuksCleartext @property def is_external(self): """Check if the device is external.""" return not self.is_systeminternal @property def is_systeminternal(self): """Check if the device is internal.""" return self._P.DeviceIsSystemInternal @property def drive(self): """ Get the drive containing this device. The returned Device object is not guaranteed to be a drive. """ if self.is_partition: return self.partition_slave.drive elif self.is_luks_cleartext: return self.luks_cleartext_slave.drive else: return self root = drive @property def should_automount(self): """Check if the device should be automounted.""" return self._P.DeviceAutomountHint != 'never' @property def icon_name(self): """Return the recommended device icon name.""" return self._P.DevicePresentationIconName or 'drive-removable-media' symbolic_icon_name = icon_name # ---------------------------------------- # Partition # ---------------------------------------- # Partition properties @property def partition_slave(self): """Get the partition slave (container).""" if not self.is_partition: return None return self._daemon[self._P.PartitionSlave] # ---------------------------------------- # Filesystem # ---------------------------------------- # Filesystem properties @property def is_mounted(self): """Check if the device is mounted.""" return self._P.DeviceIsMounted @property def mount_paths(self): """Return list of active mount paths.""" if not self.is_mounted: return [] raw_paths = self._P.DeviceMountPaths return [os.path.normpath(path) for path in raw_paths] # Filesystem methods def mount(self, fstype=None, options=None, auth_no_user_interaction=False): """Mount filesystem.""" options = (options or []) + filter_opt({ 'auth_no_user_interaction': auth_no_user_interaction }) return self._M.FilesystemMount( '(sas)', fstype or self.id_type, options) def unmount(self, force=False): """Unmount filesystem.""" return self._M.FilesystemUnmount( '(as)', filter_opt({'force': force})) # ---------------------------------------- # Encrypted # ---------------------------------------- # Encrypted properties @property def luks_cleartext_holder(self): """Get unlocked luks cleartext device.""" if not self.is_luks: return None return self._daemon[self._P.LuksHolder] @property def is_unlocked(self): """Check if device is already unlocked.""" if not self.is_luks: return None return self.luks_cleartext_holder # Encrypted methods def unlock(self, password): """Unlock Luks device.""" return self._M.LuksUnlock( '(sas)', password, []) def lock(self): """Lock Luks device.""" return self._M.LuksLock('(as)', []) # ---------------------------------------- # Loop # ---------------------------------------- @property def loop_file(self): """Get the file backing the loop device.""" return self._P.LinuxLoopFilename # ---------------------------------------- # derived properties # ---------------------------------------- @property def in_use(self): """Check whether this device is in use, i.e. mounted or unlocked.""" if self.is_mounted or self.is_unlocked: return True if self.is_partition_table: for device in self._daemon: if device.partition_slave == self and device.in_use: return True return False @property def parent_object_path(self): if self.is_partition: return self._P.PartitionSlave elif self.is_luks_cleartext: return self._P.LuksCleartextSlave else: return '/' @property def ui_label(self): return self.id_label or self.id_uuid or self.device_presentation def _keep_async_event_order(func): """ Decorator that ensures that event handlers for a device will be called in the order that the events have been generated. """ @wraps(func) def wrapper(self, object_path, *args, **kwargs): self._event_queue[object_path].push(func, self, object_path, *args, **kwargs) return wrapper class EventQueue(object): """ Execute asynchronous event handlers in the order they are pushed. This is necessary because the :class:`Daemon` is stateful; it which assumes that event handlers are executed in the order they were generated by udisks. """ def __init__(self): self._waiting = [] self._execing = False def push(self, func, *args, **kwargs): """Schedule another event handler for execution.""" self._waiting.append((func, args, kwargs)) if not self._execing: self.pop() def pop(self, *_ignored): """Start executing the next event handler.""" if not self._waiting: self._execing = False return self._execing = True func, args, kwargs = self._waiting.pop() coroutine = Coroutine(func(*args, **kwargs)) coroutine.errbacks.append(self.pop) coroutine.callbacks.append(self.pop) class Daemon(Emitter): """ UDisks listener daemon. Listens to UDisks events. When a change occurs this class detects what has changed and triggers an appropriate event. Valid events are: - device_added / device_removed - device_unlocked / device_locked - device_mounted / device_unmounted - media_added / media_removed - device_changed / job_failed A very primitive mechanism that gets along without external dependencies is used for event dispatching. The methods `connect` and `disconnect` can be used to add or remove event handlers. """ BusName = 'org.freedesktop.UDisks' Interface = 'org.freedesktop.UDisks' ObjectPath = '/org/freedesktop/UDisks' def __iter__(self): """Iterate over all devices.""" return (self[object_path] for object_path in self.paths()) def __getitem__(self, object_path): return self.get(object_path) def find(self, path): """ Get a device proxy by device name or any mount path of the device. This searches through all accessible devices and compares device path as well as mount pathes. """ if isinstance(path, Device): return path for device in self: if device.is_file(path): self._log.debug(_('found device owning "{0}": "{1}"', path, device)) return device raise ValueError(_('no device found owning "{0}"', path)) def __init__(self, proxy): """ Create a Daemon object and start listening to DBus events. :param dbus.InterfaceProxy proxy: proxy to the dbus service object A default proxy will be created if set to ``None``. """ event_names = ['device_added', 'device_removed', 'device_mounted', 'device_unmounted', 'media_added', 'media_removed', 'device_unlocked', 'device_locked', 'device_changed', 'job_failed'] super(Daemon, self).__init__(event_names) self._proxy = proxy self._log = logging.getLogger(__name__) self._jobs = {} self._devices = {} self._property_proxy = {} self._errors = {'mount': {}, 'unmount': {}, 'unlock': {}, 'lock': {}, 'eject': {}, 'detach': {}} # DBus events self._event_queue = defaultdict(EventQueue) proxy.connect('DeviceAdded', self._device_changed) proxy.connect('DeviceRemoved', self._device_changed) proxy.connect('DeviceChanged', self._device_changed) proxy.connect('DeviceJobChanged', self._device_job_changed) @classmethod @Coroutine.from_generator_function def create(cls): proxy = yield connect_service(cls) udisks = cls(proxy) yield udisks._sync() yield Return(udisks) # Sniffer overrides def paths(self): """Iterate over all valid cached devices.""" return (object_path for object_path, device in self._devices.items() if device) def get(self, object_path): """Return the current cached state of the device.""" return self._devices.get(object_path) def trigger(self, event, device, *args): self._log.debug(_("+++ {0}: {1}", event, device)) super(Daemon, self).trigger(event, device, *args) @Coroutine.from_generator_function def update(self, object_path): device = yield self._get_updated_device(object_path) if device is not None: self._devices[object_path] = device yield Return(device) @Coroutine.from_generator_function def _get_updated_device(self, object_path): obj = self._proxy.object.bus.get_object(object_path) try: prop_prox = self._property_proxy[object_path] except KeyError: prop_prox = yield obj.get_property_interface(Device.Interface) self._property_proxy[object_path] = prop_prox try: properties = yield prop_prox.GetAll() except GLib.GError: self._invalidate(object_path) yield Return(None) # TODO: return something useful? (NullDevice) else: itfc_prox = MethodsProxy(obj, Device.Interface) device = Device(self, object_path, AttrDictView(properties), itfc_prox) yield Return(device) # special methods def set_error(self, device, action, message): if action in self._errors: self._errors[action][device.object_path] = message # events def _detect_toggle(self, property_name, old, new, add_name, del_name): old_valid = old and bool(getattr(old, property_name)) new_valid = new and bool(getattr(new, property_name)) # If we were notified about a started job we don't want to trigger # an event when the device is changed, but when the job is # completed. Otherwise we would show unmount notifications too # early (when it's not yet safe to remove the drive). # On the other hand, if the unmount operation is not issued via # UDisks1, there will be no corresponding job. cached_job = self._jobs.get(old.object_path) action_name = self._event_by_action.get(cached_job) if add_name and new_valid and not old_valid: if add_name != action_name: self.trigger(add_name, new) elif del_name and old_valid and not new_valid: if del_name != action_name: self.trigger(del_name, old) # UDisks event listeners @_keep_async_event_order def _device_changed(self, object_path): """Internal method.""" old_state = self[object_path] new_state = yield self.update(object_path) if not old_state: if new_state: self.trigger('device_added', new_state) return new_state = new_state or NullDevice(object_path=object_path) self._detect_toggle('has_media', old_state, new_state, 'media_added', 'media_removed') self._detect_toggle('is_mounted', old_state, new_state, 'device_mounted', 'device_unmounted') self._detect_toggle('is_unlocked', old_state, new_state, 'device_unlocked', 'device_locked') self.trigger('device_changed', old_state, new_state) if not new_state: self._invalidate(object_path) self.trigger('device_removed', old_state) # NOTE: it seems the UDisks1 documentation for DeviceJobChanged is # fatally incorrect! @_keep_async_event_order def _device_job_changed(self, object_path, job_in_progress, job_id, job_initiated_by_user, job_is_cancellable, job_percentage): """ Detect type of event and trigger appropriate event handlers. Internal method. """ try: if job_id: action = self._action_by_operation[job_id] else: action = self._jobs[object_path] except KeyError: # this can happen # a) at startup, when we only see the completion of a job # b) when we get notified about a job, which we don't handle return # NOTE: The here used heuristic is prone to raise conditions. if job_in_progress: # Cache the action name for later use: self._jobs[object_path] = action else: del self._jobs[object_path] device = yield self._get_updated_device(object_path) if self._check_action_success[action](device): event = self._event_by_action[action] self.trigger(event, device) else: # get and delete message, if available: message = self._errors[action].pop(object_path, "") self.trigger('job_failed', device, action, message) self._log.info(_('{0} operation failed for device: {1}', action, object_path)) # used internally by _device_job_changed: _action_by_operation = { 'FilesystemMount': 'mount', 'FilesystemUnmount': 'unmount', 'LuksUnlock': 'unlock', 'LuksLock': 'lock', 'DriveDetach': 'detach', 'DriveEject': 'eject', } _event_by_action = { 'mount': 'device_mounted', 'unmount': 'device_unmounted', 'unlock': 'device_unlocked', 'lock': 'device_locked', 'eject': 'media_removed', 'detach': 'device_removed', } _check_action_success = { 'mount': lambda dev: dev.is_mounted, 'unmount': lambda dev: not dev or not dev.is_mounted, 'unlock': lambda dev: dev.is_unlocked, 'lock': lambda dev: not dev or not dev.is_unlocked, 'detach': lambda dev: not dev, 'eject': lambda dev: not dev or not dev.has_media, } # internal state keeping @Coroutine.from_generator_function def _sync(self): """Cache all device states.""" object_pathes = yield self._proxy.call('EnumerateDevices', '()') yield AsyncList(map(self.update, object_pathes)) yield Return() def _invalidate(self, object_path): """Flag the device invalid. This removes it from the iteration.""" if object_path in self._devices: del self._devices[object_path] del self._property_proxy[object_path] udiskie-1.4.9/udiskie/prompt.py0000644000175000001440000001362212640243465017176 0ustar thomasusers00000000000000""" User prompt utility. """ from __future__ import absolute_import from __future__ import unicode_literals from udiskie.depend import has_Gtk, require_Gtk from distutils.spawn import find_executable import getpass import logging import shlex import subprocess import sys from .async_ import Async, Coroutine, Return, Subprocess from .locale import _ from .compat import basestring __all__ = ['password', 'browser'] dialog_definition = r""" 5 center dialog 6 6 0 False True gtk-cancel True gtk-ok True True True cancel_button ok_button """ class Dialog(Async): def __init__(self, dialog): self._dialog = dialog self._dialog.connect("response", self._result_handler) self._dialog.show() def _result_handler(self, dialog, response): self.callback(response) dialog.destroy() @Coroutine.from_generator_function def password_dialog(title, message): """ Show a Gtk password dialog. :param str title: :param str message: :returns: the password or ``None`` if the user aborted the operation :rtype: str :raises RuntimeError: if Gtk can not be properly initialized """ Gtk = require_Gtk() builder = Gtk.Builder.new() builder.add_from_string(dialog_definition) dialog = builder.get_object('entry_dialog') label = builder.get_object('message') entry = builder.get_object('entry') dialog.set_title(title) label.set_label(message) dialog.show_all() response = yield Dialog(dialog) dialog.hide() if response == Gtk.ResponseType.OK: yield Return(entry.get_text()) else: yield Return(None) def get_password_gui(device): """Get the password to unlock a device from GUI.""" text = _('Enter password for {0.device_presentation}: ', device) try: return password_dialog('udiskie', text) except RuntimeError: return None @Coroutine.from_generator_function def get_password_tty(device): """Get the password to unlock a device from terminal.""" # TODO: make this a TRUE async text = _('Enter password for {0.device_presentation}: ', device) try: yield Return(getpass.getpass(text)) except EOFError: print("") yield Return(None) class DeviceCommand(object): """ Launcher that starts user-defined password prompts. The command can be specified in terms of a command line template. """ def __init__(self, argv): """Create the launcher object from the command line template.""" if isinstance(argv, basestring): self.argv = shlex.split(argv) else: self.argv = argv @Coroutine.from_generator_function def __call__(self, device): """ Invoke the subprocess to ask the user to enter a password for unlocking the specified device. """ argv = [arg.format(device) for arg in self.argv] try: stdout = yield Subprocess(argv) except subprocess.CalledProcessError: yield Return(None) yield Return(stdout.rstrip('\n')) def password(password_command): """ Create a password prompt function. :param bool hint_gui: whether a GUI input dialog should be preferred """ gui = lambda: has_Gtk() and get_password_gui tty = lambda: sys.stdin.isatty() and get_password_tty if password_command == 'builtin:gui': return gui() or tty() elif password_command == 'builtin:tty': return tty() or gui() elif password_command: return DeviceCommand(password_command) else: return None def browser(browser_name='xdg-open'): """ Create a browse-directory function. :param str browser_name: file manager program name :returns: one-parameter open function :rtype: callable """ if not browser_name: return None executable = find_executable(browser_name) if executable is None: # Why not raise an exception? -I think it is more convenient (for # end users) to have a reasonable default, without enforcing it. logging.getLogger(__name__).warn( _("Can't find file browser: {0!r}. " "You may want to change the value for the '-b' option.", browser_name)) return None def browse(path): return subprocess.Popen([executable, path]) return browse udiskie-1.4.9/udiskie/async_.py0000644000175000001440000002464012642325627017136 0ustar thomasusers00000000000000""" Lightweight asynchronous framework. This module defines the protocol used for asynchronous operations in udiskie. It is based on ideas from "Twisted" and the "yield from" expression in python3, but more lightweight (incomplete) and compatible with python2. """ from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from functools import partial from subprocess import CalledProcessError import sys from gi.repository import GLib from gi.repository import Gio from .common import cachedproperty, wraps, format_exc __all__ = [ 'Async', 'AsyncList', 'Return', 'Coroutine', ] class Async(object): """ Base class for asynchronous operations. One `Async' object represents an asynchronous operation. It allows for separate result and error handlers which can be set by appending to the `callbacks` and `errbacks` lists. Implementations must conform to the following very lightweight protocol: The task is started on initialization, but most not finish immediately. Tasks must take care to increase their reference count on their own in order not to be deleted until completion. Success/error exit is signaled to the observer by calling exactly one of `self.callback(value)` or `self.errback(exception)` when the operation finishes. For implementations, see :class:`Coroutine` and :class:`DBusCall`. """ done = False @cachedproperty def callbacks(self): """Functions to be called on successful completion.""" return [] @cachedproperty def errbacks(self): """Functions to be called on error completion.""" return [] def _finish(self, callbacks, *args): """Set finished state and invoke specified callbacks [internal].""" if self.done: # TODO: more output raise RuntimeError("Async already finished!") self.done = True # TODO: handle Async callbacks: return [fn(*args) for fn in callbacks] def callback(self, *values): """Signal successful completion.""" self._finish(self.callbacks, *values) def errback(self, exception, formatted): """Signal unsuccessful completion.""" was_handled = self._finish(self.errbacks, exception, formatted) if not any(was_handled): print(formatted, file=sys.stderr) # Making this object a global is important to prevent garbage collection which # could cause the waiting function to be de-reference-counted and henceforth # destroyed as well. RunForever = Async() class AsyncList(Async): """ Manages a collection of asynchronous tasks. The callbacks are executed when all of the subtasks have completed. """ def __init__(self, tasks): """Create an AsyncList from a list of Asyncs.""" tasks = list(tasks) total = len(tasks) self._results = [None] * total self._completed = 0 self._total = total if total == 0: run_soon(self.callback) else: for idx, task in enumerate(tasks): task.callbacks.append(partial(self._subtask_result, idx)) task.errbacks.append(partial(self._subtask_error, idx)) def _subtask_result(self, idx, *args): """Receive a result from a single subtask.""" self._results[idx] = (True, args) self._completed += 1 if self._completed == self._total: self.callback(self._results) def _subtask_error(self, idx, error, fmt): """Receive an error from a single subtask.""" self._results[idx] = (False, error) self._completed += 1 if self._completed == self._total: self.callback(self._results) class Return(object): """Wraps a return value from a coroutine.""" def __init__(self, *values): self.values = values def call_func(fn, *args): """ Call the function with the specified arguments but return None. This rather boring helper function is used by run_soon to make sure the function is executed only once. """ # NOTE: Apparently, idle_add does not re-execute its argument if an # exception is raised. So it's okay to let exceptions propagate. fn(*args) def run_soon(fn, *args): """Run the function once.""" GLib.idle_add(call_func, fn, *args) class Coroutine(Async): """ A coroutine processes a sequence of asynchronous tasks. Coroutines resemble non-atomic asynchronous operations. They merely aggregate and operate on the results of zero or more asynchronous subtasks. Coroutines are scheduled for execution by just calling them. In that regard, they behave very similar to normal functions. The difference is, that they return an Async object rather than a result. This object can then be used to add result handler callbacks. The coroutine's code block will first be entered in a separate main loop iteration. Coroutines are implemented as generators using `yield` expressions to transfer control flow when performing asynchronous tasks. Coroutines may yield zero or more `Async` tasks and one final `Return` value. The code after a `yield` expression is executed only after the yielded `Async` has finished. In case of successful completion, the result of the asynchronous operation is returned. In case of an error, the exception is raised inside the generator. For example: >>> @Coroutine.from_generator_function ... def foo(*args): ... # perform synchronous calculations here: ... other_args = f(args) ... try: ... # Invoke another asynchronous routine. Potentialy passes ... # control flow to main loop: ... result = yield subroutine(other_args) ... except ValueError: ... # Handle errors raised by the asynchronous subroutine. These ... # are sent here from the callback function. ... pass ... # `result` now contains the `Return` value of the sub-routine and ... # can be used for further calculations: ... value = g(result) ... # Set our own `Return` value. This must be the last statement: ... yield Return(value) """ @classmethod def from_generator_function(cls, generator_function): """Turn a generator function into a coroutine function.""" @wraps(generator_function) def coroutine_function(*args, **kwargs): return cls(generator_function(*args, **kwargs)) coroutine_function.__func__ = generator_function return coroutine_function def __init__(self, generator): """ Create and start a `Coroutine` task from the specified generator. """ self._generator = generator # TODO: cancellable tasks (generator.close() -> GeneratorExit)? run_soon(self._interact, next, self._generator) # TODO: shorten stack traces by inlining _recv / _interact ? def _recv(self, thing): """ Handle a value received from (yielded by) the generator. This function is called immediately after the generator suspends its own control flow by yielding a value. """ if isinstance(thing, Async): thing.callbacks.append(self._send) thing.errbacks.append(self._throw) elif isinstance(thing, Return): self._generator.close() # self.callback(*thing.values) # to shorten stack trace use instead: run_soon(self.callback, *thing.values) else: # the protocol is easy to do wrong, therefore we better do not # silently ignore any errors! raise NotImplementedError( ("Unexpected return value from function {!r}: {!r}.\n" "Expecting either an Async or a Return.") .format(self._generator, thing)) def _send(self, *values): """ Interact with the coroutine by sending a value. Set the return value of the current `yield` expression to the specified value and resume control flow inside the coroutine. """ self._interact(self._generator.send, self._pack(*values)) def _pack(self, *values): """Unpack a return tuple to a yield expression return value.""" # Schizophrenic returns from yield expressions. Inspired by # gi.overrides.Gio.DBusProxy. if len(values) == 0: return None elif len(values) == 1: return values[0] else: return values def _throw(self, exc, fmt): """ Interact with the coroutine by raising an exception. Transfer the control flow back to the coroutine by raising an exception from the `yield` expression. """ self._interact(self._generator.throw, exc) return True def _interact(self, func, *args): """ Interact with the coroutine by performing the specified operation. """ try: value = func(*args) except StopIteration: self._generator.close() self.callback() except Exception as e: self._generator.close() self.errback(e, format_exc()) else: self._recv(value) class Subprocess(Async): """ An Async task that represents a subprocess. If successful, the task's result is set to the collected STDOUT of the subprocess. :raises subprocess.CalledProcessError: if the subprocess returns a non-zero exit code """ def __init__(self, argv): self.p = Gio.Subprocess.new(argv, Gio.SubprocessFlags.STDOUT_PIPE) stdin_buf = None cancellable = None user_data = None self.p.communicate_utf8_async( stdin_buf, cancellable, self._callback, user_data) def _callback(self, source_object, result, user_data): try: success, stdout, stderr = self.p.communicate_utf8_finish(result) if not success: raise RuntimeError("Subprocess did not exit normally!") exit_code = self.p.get_exit_status() if exit_code != 0: raise CalledProcessError( "Subprocess returned a non-zero exit-status!", exit_code, stdout) except Exception as e: self.errback(e, format_exc()) else: self.callback(stdout)