udiskie-2.1.0/0000775000372000037200000000000013615574740014025 5ustar travistravis00000000000000udiskie-2.1.0/COPYING0000664000372000037200000000214013615574740015055 0ustar travistravis00000000000000Copyright (c) 2010-2012 Byron Clark (c) 2013-2019 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-2.1.0/MANIFEST.in0000664000372000037200000000036613615574740015570 0ustar travistravis00000000000000include doc/*.txt include doc/asciidoc.conf include doc/Makefile graft completions recursive-include icons *.svg recursive-include lang *.pot *.po recursive-include udiskie *.ui include CONTRIBUTORS COPYING LICENSE include README.rst CHANGES.rst udiskie-2.1.0/CHANGES.rst0000664000372000037200000003016013615574740015627 0ustar travistravis00000000000000CHANGELOG --------- 2.1.0 ~~~~~ Date: 02.02.2020 - fix some typos (thanks @torstehu, #197) - change how device rules are evaluated: lookup undecided rules on parent device (fixes issue with filters not applying to subdevices of a matched device, see #198) - change builtin rules to not show loop devices with ``HintIgnore``, see #181 - change how is_external attribute is compute: use the value from udisks directly (fixes issue with is_external property not behaving as expected, see #185) - add 'skip' keyword for rules to skip evaluation of further rules on this device, and continue directly on the parent 2.0.4 ~~~~~ Date: 21.01.2020 - fix user commands that output non-utf8 data 2.0.3 ~~~~~ Date: 20.01.2020 - fix exception when using non-device parameters with DeviceCommand (e.g. in --notify-command) 2.0.2 ~~~~~ Date: 30.12.2019 - hotfix for automounting being broken since 2.0.0 2.0.1 ~~~~~ Date: 28.12.2019 - use ``importlib.resources`` directly on py3.7 and above, rather than requiring ``importlib_resources`` as additional dependency 2.0.0 ~~~~~ Date: 26.12.2019 - require python >= 3.5 - drop python2 support - drop udisks1 support - drop command line options corresponding to udisks version selection (-1, -2) - use py35's ``async def`` functions -- improving stack traces upon exception - internal refactoring and simplifications - add "show password" checkbox in password dialog 1.7.7 ~~~~~ Date: 17.02.2019 - keep password dialog always on top - fix stdin-based password prompts 1.7.6 ~~~~~ Date: 17.02.2019 - add russian translations (thanks @mr-GreyWolf) - fixed deprecation warnings in setup.py (thanks @sealj553) 1.7.5 ~~~~~ Date: 24.05.2018 - fix "NameError: 'Async' is not defined" when starting without tray icon 1.7.4 ~~~~~ Date: 17.05.2018 - fix attribute error when using options in udiskie-mount (#159) - fix tray in appindicator mode (#156) - possibly fix non-deterministic bugs (due to garbage collection) by keeping global reference to all active asyncs 1.7.3 ~~~~~ Date: 13.12.2017 - temporary workaround for udisks2.7 requiring ``filesystem-mount-system`` when trying to mount a LUKS cleartext device diretcly after unlocking 1.7.2 ~~~~~ Date: 18.10.2017 - officially deprecate udisks1 - officially deprecate python2 (want python >= 3.5) - fix startup crash on py2 - fix exception when inserting LUKS device if ``--password-prompt`` or udisks1 is used - fix minor problem with zsh autocompletion 1.7.1 ~~~~~ Date: 02.10.2017 - add an "open keyfile" button to the password dialog - add warning if mounting device without ntfs-3g (#143) - fix problem with LVM devices 1.7.0 ~~~~~ Date: 26.03.2017 - add joined ``device_config`` list in the config file - deprecate ``mount_options`` and ``ignore_device`` in favor of ``device_config`` - can configure ``automount`` per device using the new ``device_config`` [#107] - can configure keyfiles (requires udisks 2.6.4) [#66] - remove mailing list 1.6.2 ~~~~~ Date: 06.03.2017 - Show losetup/quit actions only in ex-menu - Show note in menu if no devices are found 1.6.1 ~~~~~ Date: 24.02.2017 - add format strings for the undocumented ``udiskie-info`` utility - speed up autocompletion times, for ``udiskie-mount`` by about a factor three, for ``udiskie-umount`` by about a factor 10 1.6.0 ~~~~~ Date: 22.02.2017 - fix crash on startup if config file is empty - add ``--notify-command`` to notify external programs (@jgraef) [#127] - can enable/disable automounting via special right-click menu [#98] - do not explicitly specify filesystem when mounting [#131] 1.5.1 ~~~~~ Date: 03.06.2016 - fix unicode issue that occurs on python2 when stdout is redirected (in particular for zsh autocompletion) 1.5.0 ~~~~~ Date: 03.06.2016 - make systray menu flat (use ``udiskie --tray --menu smart`` to request the old menu) [#119] - extend support for loop devices (requires UDisks2) [#101] - support ubuntu/unity AppIndicator backend for status icon [#59] - add basic utility to obtain info on block devices [#122] - add zsh completions [#26] - improve UI menu labels for devices - fix error when force-ejecting device [#121] - respect configured ignore-rules in ``udiskie-umount`` - fix error message for empty task lists [#123] 1.4.12 ~~~~~~ Date: 15.05.2016 - log INFO events to STDOUT (#112) - fix exception in notifications when action is not available. This concerns the retry button in the ``job_failed`` notification, as well as the browse action in the ``device_mounted`` notification (#117) - don't show 'browse' action in tray menu if unavailable 1.4.11 ~~~~~~ Date: 13.05.2016 - protect password dialog against garbage collection (which makes the invoking coroutine hang up and not unlock the device) - fix add_all/remove_all operations: only consider leaf/root devices within the handleable devices hierarchy: - avoid considering the same device twice (#114) - makes sure every handleable device is considered at all in remove_all 1.4.10 ~~~~~~ Date: 11.05.2016 - signal failing mount/unmount operations with non-zero exit codes (#110) - suppress notifications for unhandled devices - add rules for docker devices marking them unhandled to avoid excessive notifications (#113) - allow mounting/unmounting using UUID (#90) - prevent warning when starting without X session (#102) - can now match against wildcards in config rules (#49) 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-2.1.0/PKG-INFO0000664000372000037200000005010213615574740015120 0ustar travistravis00000000000000Metadata-Version: 2.1 Name: udiskie Version: 2.1.0 Summary: Removable disk automounter for udisks Home-page: 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 Description: ======= udiskie ======= |Version| |License| *udiskie* is a udisks2_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. |Screenshot| Its features include: - automount removable media - notifications - tray icon - command line tools for manual un-/mounting - LUKS encrypted devices - unlocking with keyfiles (requires udisks 2.6.4) - loop devices (mounting iso archives) - password caching (requires python keyutils 0.3) All features can be individually enabled or disabled. **NOTE:** support for python2 and udisks1 have been removed. If you need a version of udiskie that supports python2, please check out the ``1.7.X`` releases or the ``maint-1.7`` branch. .. _udisks2: http://www.freedesktop.org/wiki/Software/udisks - `Documentation`_ - Usage_ - Installation_ - `Debug Info`_ - Troubleshooting_ - FAQ_ - `Man page`_ - `Source Code`_ - `Latest Release`_ - `Issue Tracker`_ .. _Documentation: https://github.com/coldfix/udiskie/wiki .. _Usage: https://github.com/coldfix/udiskie/wiki/Usage .. _Installation: https://github.com/coldfix/udiskie/wiki/Installation .. _Debug Info: https://github.com/coldfix/udiskie/wiki/Debug-Info .. _Troubleshooting: https://github.com/coldfix/udiskie/wiki/Troubleshooting .. _FAQ: https://github.com/coldfix/udiskie/wiki/FAQ .. _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 .. _Roadmap: https://github.com/coldfix/udiskie/blob/master/HACKING.rst#roadmap .. Badges: .. |Version| image:: https://img.shields.io/pypi/v/udiskie.svg :target: https://pypi.python.org/pypi/udiskie :alt: Version .. |License| image:: https://img.shields.io/pypi/l/udiskie.svg :target: https://github.com/coldfix/udiskie/blob/master/COPYING :alt: License: MIT .. |Screenshot| image:: https://raw.githubusercontent.com/coldfix/udiskie/master/screenshot.png :target: https://raw.githubusercontent.com/coldfix/udiskie/master/screenshot.png :alt: Screenshot CHANGELOG --------- 2.1.0 ~~~~~ Date: 02.02.2020 - fix some typos (thanks @torstehu, #197) - change how device rules are evaluated: lookup undecided rules on parent device (fixes issue with filters not applying to subdevices of a matched device, see #198) - change builtin rules to not show loop devices with ``HintIgnore``, see #181 - change how is_external attribute is compute: use the value from udisks directly (fixes issue with is_external property not behaving as expected, see #185) - add 'skip' keyword for rules to skip evaluation of further rules on this device, and continue directly on the parent 2.0.4 ~~~~~ Date: 21.01.2020 - fix user commands that output non-utf8 data 2.0.3 ~~~~~ Date: 20.01.2020 - fix exception when using non-device parameters with DeviceCommand (e.g. in --notify-command) 2.0.2 ~~~~~ Date: 30.12.2019 - hotfix for automounting being broken since 2.0.0 2.0.1 ~~~~~ Date: 28.12.2019 - use ``importlib.resources`` directly on py3.7 and above, rather than requiring ``importlib_resources`` as additional dependency 2.0.0 ~~~~~ Date: 26.12.2019 - require python >= 3.5 - drop python2 support - drop udisks1 support - drop command line options corresponding to udisks version selection (-1, -2) - use py35's ``async def`` functions -- improving stack traces upon exception - internal refactoring and simplifications - add "show password" checkbox in password dialog 1.7.7 ~~~~~ Date: 17.02.2019 - keep password dialog always on top - fix stdin-based password prompts 1.7.6 ~~~~~ Date: 17.02.2019 - add russian translations (thanks @mr-GreyWolf) - fixed deprecation warnings in setup.py (thanks @sealj553) 1.7.5 ~~~~~ Date: 24.05.2018 - fix "NameError: 'Async' is not defined" when starting without tray icon 1.7.4 ~~~~~ Date: 17.05.2018 - fix attribute error when using options in udiskie-mount (#159) - fix tray in appindicator mode (#156) - possibly fix non-deterministic bugs (due to garbage collection) by keeping global reference to all active asyncs 1.7.3 ~~~~~ Date: 13.12.2017 - temporary workaround for udisks2.7 requiring ``filesystem-mount-system`` when trying to mount a LUKS cleartext device diretcly after unlocking 1.7.2 ~~~~~ Date: 18.10.2017 - officially deprecate udisks1 - officially deprecate python2 (want python >= 3.5) - fix startup crash on py2 - fix exception when inserting LUKS device if ``--password-prompt`` or udisks1 is used - fix minor problem with zsh autocompletion 1.7.1 ~~~~~ Date: 02.10.2017 - add an "open keyfile" button to the password dialog - add warning if mounting device without ntfs-3g (#143) - fix problem with LVM devices 1.7.0 ~~~~~ Date: 26.03.2017 - add joined ``device_config`` list in the config file - deprecate ``mount_options`` and ``ignore_device`` in favor of ``device_config`` - can configure ``automount`` per device using the new ``device_config`` [#107] - can configure keyfiles (requires udisks 2.6.4) [#66] - remove mailing list 1.6.2 ~~~~~ Date: 06.03.2017 - Show losetup/quit actions only in ex-menu - Show note in menu if no devices are found 1.6.1 ~~~~~ Date: 24.02.2017 - add format strings for the undocumented ``udiskie-info`` utility - speed up autocompletion times, for ``udiskie-mount`` by about a factor three, for ``udiskie-umount`` by about a factor 10 1.6.0 ~~~~~ Date: 22.02.2017 - fix crash on startup if config file is empty - add ``--notify-command`` to notify external programs (@jgraef) [#127] - can enable/disable automounting via special right-click menu [#98] - do not explicitly specify filesystem when mounting [#131] 1.5.1 ~~~~~ Date: 03.06.2016 - fix unicode issue that occurs on python2 when stdout is redirected (in particular for zsh autocompletion) 1.5.0 ~~~~~ Date: 03.06.2016 - make systray menu flat (use ``udiskie --tray --menu smart`` to request the old menu) [#119] - extend support for loop devices (requires UDisks2) [#101] - support ubuntu/unity AppIndicator backend for status icon [#59] - add basic utility to obtain info on block devices [#122] - add zsh completions [#26] - improve UI menu labels for devices - fix error when force-ejecting device [#121] - respect configured ignore-rules in ``udiskie-umount`` - fix error message for empty task lists [#123] 1.4.12 ~~~~~~ Date: 15.05.2016 - log INFO events to STDOUT (#112) - fix exception in notifications when action is not available. This concerns the retry button in the ``job_failed`` notification, as well as the browse action in the ``device_mounted`` notification (#117) - don't show 'browse' action in tray menu if unavailable 1.4.11 ~~~~~~ Date: 13.05.2016 - protect password dialog against garbage collection (which makes the invoking coroutine hang up and not unlock the device) - fix add_all/remove_all operations: only consider leaf/root devices within the handleable devices hierarchy: - avoid considering the same device twice (#114) - makes sure every handleable device is considered at all in remove_all 1.4.10 ~~~~~~ Date: 11.05.2016 - signal failing mount/unmount operations with non-zero exit codes (#110) - suppress notifications for unhandled devices - add rules for docker devices marking them unhandled to avoid excessive notifications (#113) - allow mounting/unmounting using UUID (#90) - prevent warning when starting without X session (#102) - can now match against wildcards in config rules (#49) 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 :: 3.5 Classifier: Programming Language :: Python :: 3.6 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 Requires-Python: >=3.5 Description-Content-Type: text/x-rst Provides-Extra: password_cache Provides-Extra: config udiskie-2.1.0/doc/0000775000372000037200000000000013615574740014572 5ustar travistravis00000000000000udiskie-2.1.0/doc/asciidoc.conf0000664000372000037200000000316513615574740017224 0ustar travistravis00000000000000## 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-2.1.0/doc/Makefile0000664000372000037200000000022213615574740016226 0ustar travistravis00000000000000udiskie.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-2.1.0/doc/udiskie.8.txt0000664000372000037200000002705313615574740017145 0ustar travistravis00000000000000///// 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 an udisks2 front-end 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. *-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 the device attributes as keyword arguments, e.g.: -p "zenity --entry --hide-text --text 'Enter password for {device_presentation}:'" *-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, \--no-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. *-F, \--no-file-manager*:: Disable browsing. *--terminal=PROGRAM*:: Set terminal command line to open mounted directories. Default is none! Pass an empty string to disable this feature. *--no-terminal*:: Disable terminal action. *--appindicator*:: Use AppIndicator3 for the status icon. Use this on Ubuntu/Unity if no icon is shown. *--no-appindicator*:: Use Gtk.StatusIcon for the status icon (default). *--password-cache MINUTES*:: Cache passwords for LUKS partitions and set the timeout. *--no-password-cache*:: Never cache passwords (default). *--notify-command COMMAND*:: Command to execute on device events. Command string be formatted using the event name and the list of device attributes (see below), e.g.: --notify-command "zenity --info --text '{event}: {device_presentation}'" *--no-notify-command*:: No notify command (default). 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 tray: auto # [bool] Enable the tray icon. "auto" # means auto-hide the tray icon when # there are no handled devices. menu: flat # ["flat" | "nested"] Set the # systray menu behaviour. 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 last command line argument. terminal: 'termite -d' # [string] Set terminal command line to open directories. It will be # invoked with thefolder path as its last command line 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`. 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. notify_command: "zenity --info --text '{event}: {device_presentation}'" # [string|list] Set command to be executed on any device event. # This is specified like `password_prompt`. device_config: # List of device option rules. Each item can match any combination of device # attributes. Additionally, it defines the resulting action (see below). # Any rule can contain multiple filters (AND) and multiple actions. # Only the first matching rule that defines a given action is used. # The rules defined here are simply prepended to the builtin device rules, # so that it is possible to completely overwrite the defaults by specifying # a catch-all rule (i.e. a rule without device attributes). - device_file: /dev/dm-5 # [filter] ignore: false # [action] never ignore this device - id_uuid: 9d53-13ba # [filter] match by device UUID options: [noexec, nodev] # [action] mount options can be given as list ignore: false # [action] never ignore this device (even if fs=FAT) automount: false # [action] do not automount this device - id_type: vfat # [filter] match file system type ignore: true # [action] ignore all FAT devices - id_type: ntfs # [filter] (optional) skip: true # [action] skip all further (even builtin) rules # for all matched devices, and resolve action result # on parent device - ignore: True # never mount/unmount or even show this in the GUI automount: False # show but do not automount this device options: [] # additional options to be passed when mounting mount_options: # [deprecated] do not use ignore_device: # [deprecated] do not use 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 quickmenu_actions: [mount, unmount, unlock, terminal, detach, delete] # List of actions to be shown in the quickmenu or the special value 'all'. # The quickmenu is shown on left-click if using flat menu type. 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] terminal: [terminal, terminator, xfce-terminal] mount: [udiskie-mount] unmount: [udiskie-unmount] unlock: [udiskie-unlock] lock: [udiskie-lock] eject: [udiskie-eject, media-eject] detach: [udiskie-detach] delete: [udiskie-eject] 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_size block device size device_id unique, persistent device identifier 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 mount_path any mount path is_unlocked in_use device or any of its children mounted should_automount ui_label loop_file file backing the loop device setup_by_uid user that setup the loop device autoclear automatically delete loop device after use symlinks drive_model drive_vendor drive_label ui_device_label ui_device_presentation ui_id_label ui_id_uuid 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. If you don't have or like github, you can contact me by email: https://github.com/coldfix/udiskie/issues thomas@coldfix.de udiskie-2.1.0/udiskie/0000775000372000037200000000000013615574740015462 5ustar travistravis00000000000000udiskie-2.1.0/udiskie/notify.py0000664000372000037200000002270713615574740017354 0ustar travistravis00000000000000""" Notification utility. """ import logging from gi.repository import GLib from .async_ import run_bg from .common import exc_message, DaemonBase, format_exc from .mount import DeviceActions from .locale import _ __all__ = ['Notify'] class Notify(DaemonBase): """ 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. """ EVENTS = ['device_mounted', 'device_unmounted', 'device_locked', 'device_unlocked', 'device_added', 'device_removed', 'job_failed'] def __init__(self, notify, mounter, timeout=None, aconfig=None): """ Initialize notifier and connect to service. :param notify: notification service module (gi.repository.Notify) :param mounter: Mounter object :param dict timeout: dictionary with timeouts for notifications """ 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 = [] self.events = { event: getattr(self, event) for event in self.EVENTS if self._enabled(event) } # event handlers: def device_mounted(self, device): """Show mount notification for specified device object.""" if not self._mounter.is_handleable(device): return browse_action = ('browse', _('Browse directory'), self._mounter.browse, device) terminal_action = ('terminal', _('Open terminal'), self._mounter.terminal, 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, self._mounter._terminal and terminal_action) def device_unmounted(self, device): """Show unmount notification for specified device object.""" if not self._mounter.is_handleable(device): return self._show_notification( 'device_unmounted', _('Device unmounted'), _('{0.ui_label} unmounted', device), device.icon_name) def device_locked(self, device): """Show lock notification for specified device object.""" if not self._mounter.is_handleable(device): return self._show_notification( 'device_locked', _('Device locked'), _('{0.device_presentation} locked', device), device.icon_name) def device_unlocked(self, device): """Show unlock notification for specified device object.""" if not self._mounter.is_handleable(device): return self._show_notification( 'device_unlocked', _('Device unlocked'), _('{0.device_presentation} unlocked', device), device.icon_name) def device_added(self, device): """Show discovery notification for specified device object.""" if not self._mounter.is_handleable(device): return 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 removal notification for specified device object.""" if not self._mounter.is_handleable(device): return 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.""" if not self._mounter.is_handleable(device): return 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 actions: each item is a tuple with parameters for _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 action and 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 should 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))) self._log.debug(format_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. """ on_action_click = run_bg(lambda *_: 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) # gi.Notify does not store hard references to the notification # objects. When a signal is received and the notification does not # exist anymore, no handler will be called. Therefore, we need to # prevent these notifications from being destroyed by storing # references: notification.connect('closed', self._notifications.remove) self._notifications.append(notification) def _enabled(self, event): """Check if the notification for an event is enabled.""" return self._get_timeout(event) not in (None, False) def _get_timeout(self, event): """Get the timeout for an event from the config or None.""" return self._timeout.get(event, self._default) def _action_enabled(self, event, action): """Check if an action for a notification is enabled.""" 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-2.1.0/udiskie/cache.py0000664000372000037200000000266313615574740017106 0ustar travistravis00000000000000""" Utility for temporarily caching passwords. """ import keyutils class PasswordCache: 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) except keyutils.Error: raise KeyError("Key not cached!") def __setitem__(self, device, value): key = self._key(device) if isinstance(value, str): value = value.encode('utf-8') key_id = keyutils.add_key(key, value, 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-2.1.0/udiskie/prompt.py0000664000372000037200000002164013615574740017360 0ustar travistravis00000000000000""" User prompt utility. """ from udiskie.depend import has_Gtk, require_Gtk from udiskie.common import is_utf8 from distutils.spawn import find_executable import getpass import logging import shlex import string import subprocess import sys try: from importlib.resources import read_text except ImportError: # for Python<3.7 from importlib_resources import read_text from .async_ import exec_subprocess, run_bg, Future from .locale import _ from .config import DeviceFilter Gtk = None __all__ = ['password', 'browser'] dialog_definition = read_text(__package__, 'password_dialog.ui') class Dialog(Future): def __init__(self, window): self._enter_count = 0 self.window = window self.window.connect("response", self._result_handler) def _result_handler(self, window, response): self.set_result(response) def __enter__(self): self._enter_count += 1 self._awaken() return self def __exit__(self, *exc_info): self._enter_count -= 1 if self._enter_count == 0: self._cleanup() def _awaken(self): self.window.present() def _cleanup(self): self.window.hide() self.window.destroy() class PasswordResult: def __init__(self, password=None, cache_hint=None): self.password = password self.cache_hint = cache_hint class PasswordDialog(Dialog): INSTANCES = {} content = None @classmethod def create(cls, key, title, message, options): if key in cls.INSTANCES: return cls.INSTANCES[key] return cls(key, title, message, options) def _awaken(self): self.INSTANCES[self.key] = self super()._awaken() def _cleanup(self): del self.INSTANCES[self.key] super()._cleanup() def __init__(self, key, title, message, options): self.key = key global Gtk Gtk = require_Gtk() builder = Gtk.Builder.new() builder.add_from_string(dialog_definition) window = builder.get_object('entry_dialog') self.entry = builder.get_object('entry') show_password = builder.get_object('show_password') show_password.set_label(_('Show password')) show_password.connect('clicked', self.on_show_password) allow_keyfile = options.get('allow_keyfile') keyfile_button = builder.get_object('keyfile_button') keyfile_button.set_label(_('Open keyfile…')) keyfile_button.set_visible(allow_keyfile) keyfile_button.connect('clicked', run_bg(self.on_open_keyfile)) allow_cache = options.get('allow_cache') cache_hint = options.get('cache_hint') self.use_cache = builder.get_object('remember') self.use_cache.set_label(_('Remember password')) self.use_cache.set_visible(allow_cache) self.use_cache.set_active(cache_hint) label = builder.get_object('message') label.set_label(message) window.set_title(title) window.set_keep_above(True) super().__init__(window) def on_show_password(self, button): self.entry.set_visibility(button.get_active()) async def on_open_keyfile(self, button): gtk_dialog = Gtk.FileChooserDialog( _("Open a keyfile to unlock the LUKS device"), self.window, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) with Dialog(gtk_dialog) as dialog: response = await dialog if response == Gtk.ResponseType.OK: with open(dialog.window.get_filename(), 'rb') as f: self.content = f.read() self.window.response(response) def get_text(self): if self.content is not None: return self.content return self.entry.get_text() async def password_dialog(key, title, message, options): """ Show a Gtk password dialog. :returns: the password or ``None`` if the user aborted the operation :raises RuntimeError: if Gtk can not be properly initialized """ with PasswordDialog.create(key, title, message, options) as dialog: response = await dialog if response == Gtk.ResponseType.OK: return PasswordResult(dialog.get_text(), dialog.use_cache.get_active()) return None def get_password_gui(device, options): """Get the password to unlock a device from GUI.""" text = _('Enter password for {0.device_presentation}: ', device) try: return password_dialog(device.id_uuid, 'udiskie', text, options) except RuntimeError: return None async def get_password_tty(device, options): """Get the password to unlock a device from terminal.""" # TODO: make this a TRUE async text = _('Enter password for {0.device_presentation}: ', device) try: return PasswordResult(getpass.getpass(text)) except EOFError: print("") return None class DeviceCommand: """ Launcher that starts user-defined password prompts. The command can be specified in terms of a command line template. """ def __init__(self, argv, **extra): """Create the launcher object from the command line template.""" if isinstance(argv, str): self.argv = shlex.split(argv) else: self.argv = argv self.extra = extra.copy() # obtain a list of used fields names formatter = string.Formatter() self.used_attrs = set() for arg in self.argv: for text, kwd, spec, conv in formatter.parse(arg): if kwd is None: continue if kwd in DeviceFilter.VALID_PARAMETERS: self.used_attrs.add(kwd) if kwd not in DeviceFilter.VALID_PARAMETERS and \ kwd not in self.extra: self.extra[kwd] = None logging.getLogger(__name__).error(_( 'Unknown device attribute {!r} in format string: {!r}', kwd, arg)) async def __call__(self, device): """ Invoke the subprocess to ask the user to enter a password for unlocking the specified device. """ attrs = {attr: getattr(device, attr) for attr in self.used_attrs} attrs.update(self.extra) argv = [arg.format(**attrs) for arg in self.argv] try: stdout = await exec_subprocess(argv) except subprocess.CalledProcessError: return None # Remove trailing newline for text answers, but not for binary # keyfiles. This logic is a guess that may cause bugs for some users:( if stdout.endswith(b'\n') and is_utf8(stdout): stdout = stdout[:-1] return stdout async def password(self, device, options): text = await self(device) return PasswordResult(text) def password(password_command): """Create a password prompt function.""" 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).password else: return None def browser(browser_name='xdg-open'): """Create a browse-directory function.""" if not browser_name: return None argv = shlex.split(browser_name) executable = find_executable(argv[0]) 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 '-f' option.", browser_name)) return None def browse(path): return subprocess.Popen(argv + [path]) return browse def notify_command(command_format, mounter): """ Command notification tool. This works similar to Notify, but will issue command instead of showing the notifications on the desktop. This can then be used to react to events from shell scripts. The command can contain modern pythonic format placeholders like: {device_file}. The following placeholders are supported: event, device_file, device_id, device_size, drive, drive_label, id_label, id_type, id_usage, id_uuid, mount_path, root :param str command_format: command to run when an event occurs. :param mounter: Mounter object """ udisks = mounter.udisks for event in ['device_mounted', 'device_unmounted', 'device_locked', 'device_unlocked', 'device_added', 'device_removed', 'job_failed']: udisks.connect(event, run_bg(DeviceCommand(command_format, event=event))) udiskie-2.1.0/udiskie/udisks2.py0000664000372000037200000007413713615574740017434 0ustar travistravis00000000000000""" 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. """ from copy import copy, deepcopy import logging from gi.repository import GLib import udiskie.dbus as dbus from .common import Emitter, AttrDictView, decode_ay, samefile, sameuuid from .locale import _ __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 /org/freedesktop/UDisks2/drives/WDC_WD... => drive /org/freedesktop/UDisks2/jobs/5 => job """ 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: """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 dbus.MethodsProxy(self._object_proxy, Interface[key]) class PropertyHub: """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: """Null class for properties of an unavailable interface.""" def __bool__(self): return False def __getattr__(self, key): """Return None when asked for any attribute.""" return None # ---------------------------------------- # Device wrapper # ---------------------------------------- class Device: """ 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) # 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. """ # NOTE: always fallback to `self` because udisks2 doesn't report # CryptoBackingDevice nor Drive for logical volumes: return self.is_toplevel and not self.is_loop and self.drive or 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) @property def drive_vendor(self): """Return drive vendor.""" return self._assocdrive._P.Drive.Vendor @property def drive_model(self): """Return drive model.""" return self._assocdrive._P.Drive.Model # 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.""" return not self.is_systeminternal @property def is_systeminternal(self): """Check if the device is internal.""" return self._P.Block.HintSystem @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' @property def symlinks(self): """Known symlinks of the block device.""" if not self._P.Block.Symlinks: return [] return [decode_ay(path) for path in self._P.Block.Symlinks] # ---------------------------------------- # Partition # ---------------------------------------- # Partition properties @property def partition_slave(self): """Get the partition slave (container).""" return self._daemon[self._P.Partition.Table] @property def partition_uuid(self): """Get the partition UUID.""" return self._P.Partition.UUID # ---------------------------------------- # 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 unlock_keyfile(self, password, auth_no_user_interaction=None): return self._M.Encrypted.Unlock( '(sa{sv})', '', filter_opt({ 'keyfile_contents': ('ay', password), '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) @property def setup_by_uid(self): """Get the ID of the user who set up the loop device.""" return self._P.Loop.SetupByUID @property def autoclear(self): """If True the loop device will be deleted after unmounting.""" return self._P.Loop.Autoclear def delete(self, auth_no_user_interaction=None): """Delete loop partition.""" return self._M.Loop.Delete( '(a{sv})', filter_opt({ 'auth.no_user_interaction': ('b', auth_no_user_interaction), }) ) def set_autoclear(self, value, auth_no_user_interaction=None): """Set autoclear flag for loop partition.""" return self._M.Loop.SetAutoclear( '(ba{sv})', value, filter_opt({ 'auth.no_user_interaction': ('b', auth_no_user_interaction), }) ) # ---------------------------------------- # derived properties # ---------------------------------------- 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) or sameuuid(path, self.id_uuid) or sameuuid(path, self.partition_uuid)) @property def parent_object_path(self): return (self._P.Partition.Table or self._P.Block.CryptoBackingDevice or '/') @property def mount_path(self): """Return any mount path.""" try: return self.mount_paths[0] except IndexError: return '' @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 ui_id_label(self): """Label of the unlocked partition or the device itself.""" return (self.luks_cleartext_holder or self).id_label @property def ui_id_uuid(self): """UUID of the unlocked partition or the device itself.""" return (self.luks_cleartext_holder or self).id_uuid @property def ui_device_presentation(self): """Path of the crypto backing device or the device itself.""" return (self.luks_cleartext_slave or self).device_presentation @property def ui_label(self): """UI string identifying the partition if possible.""" return ': '.join(filter(None, [ self.ui_device_presentation, self.ui_id_label or self.ui_id_uuid or self.drive_label ])) @property def ui_device_label(self): """UI string identifying the device (drive) if toplevel.""" return ': '.join(filter(None, [ self.ui_device_presentation, self.loop_file or self.drive_label or self.ui_id_label or self.ui_id_uuid ])) @property def drive_label(self): """Return drive label.""" return ' '.join(filter(None, [ self.drive_vendor, self.drive_model, ])) # ---------------------------------------- # 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 paths. """ 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 FileNotFoundError(_('no device found owning "{0}"', path)) def __init__(self, proxy, version): """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().__init__(event_names) self._log = logging.getLogger(__name__) self._log.debug(_('Daemon version: {0}', version)) self.version = version self.version_info = tuple(map(int, version.split('.'))) self.keyfile_support = self.version_info >= (2, 6, 4) self._log.debug(_('Keyfile support: {0}', self.keyfile_support)) self._proxy = proxy 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) async def _sync(self): """Synchronize state.""" self._objects = await self._proxy.call('GetManagedObjects', '()') @classmethod async def create(cls): service = (cls.BusName, cls.ObjectPath, cls.Interface) proxy = await dbus.connect_service(*service) version = await cls.get_version() daemon = cls(proxy, version) await daemon._sync() return daemon @classmethod async def get_version(cls): service = (cls.BusName, '/org/freedesktop/UDisks2/Manager', Interface['Properties']) manager = await dbus.connect_service(*service) version = await dbus.call(manager._proxy, 'Get', '(ss)', ( Interface['Manager'], 'Version')) return version async def loop_setup(self, fd, options): service = (self.BusName, '/org/freedesktop/UDisks2/Manager', Interface['Manager']) manager = await dbus.connect_service(*service) object_path = await dbus.call_with_fd_list( manager._proxy, 'LoopSetup', '(ha{sv})', (0, filter_opt({ 'auth.no_user_interaction': ( 'b', options.get('auth.no_user_interaction')), 'offset': ('t', options.get('offset')), 'size': ('t', options.get('size')), 'read-only': ('b', options.get('read-only')), 'no-part-scan': ('b', options.get('no-part-scan')), })), [fd], ) await self._sync() return self[object_path] # 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().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-2.1.0/udiskie/automount.py0000664000372000037200000000273313615574740020074 0ustar travistravis00000000000000""" Automount utility. """ from .common import DaemonBase from .async_ import run_bg __all__ = ['AutoMounter'] class AutoMounter(DaemonBase): """ 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 = AutoMounter(Mounter(udisks=Daemon())) >>> automounter.activate() """ def __init__(self, mounter, automount=True): """Store mounter as member variable.""" self._mounter = mounter self._automount = automount self.events = { 'device_changed': self.device_changed, 'device_added': self.auto_add, 'media_added': self.auto_add, } def is_on(self): return self._automount def toggle_on(self): self._automount = not self._automount def device_changed(self, old_state, new_state): """Mount newly mountable devices.""" # udisks2 sometimes adds empty devices and later updates them - which # makes is_external become true at a time later than device_added: if (self._mounter.is_addable(new_state) and not self._mounter.is_addable(old_state) and not self._mounter.is_removable(old_state)): self.auto_add(new_state) @run_bg def auto_add(self, device): return self._mounter.auto_add(device, automount=self._automount) udiskie-2.1.0/udiskie/depend.py0000664000372000037200000000346413615574740017302 0ustar travistravis00000000000000""" Make sure that the correct versions of gobject introspection dependencies are installed. """ import os import logging from gi import require_version from .locale import _ require_version('Gio', '2.0') require_version('GLib', '2.0') def check_call(exc_type, func, *args): try: func(*args) return True except exc_type: return False def check_version(package, version): return check_call(ValueError, require_version, package, version) _in_X = bool(os.environ.get('DISPLAY')) _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 not _in_X: raise RuntimeError('Not in X session.') 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-2.1.0/udiskie/common.py0000664000372000037200000001053313615574740017326 0ustar travistravis00000000000000""" Common utilities. """ import os.path import sys import traceback __all__ = [ 'wraps', 'Emitter', 'samefile', 'sameuuid', 'setdefault', 'extend', 'cachedproperty', 'decode_ay', 'exc_message', 'format_exc', ] try: from black_magic.decorator import wraps except ImportError: from functools import wraps class Emitter: """Simple event emitter for a known finite set of events.""" def __init__(self, event_names=(), *args, **kwargs): """Initialize with empty lists of event handlers.""" super().__init__(*args, **kwargs) self._event_handlers = {} for evt in event_names: self._event_handlers[evt] = [] def trigger(self, event, *args): """Trigger event by name.""" for handler in self._event_handlers[event]: handler(*args) def connect(self, event, handler): """Connect an event handler.""" self._event_handlers[event].append(handler) def disconnect(self, event, handler): """Disconnect an event handler.""" self._event_handlers[event].remove(handler) def samefile(a: str, b: str) -> bool: """Check if two paths represent the same file.""" try: return os.path.samefile(a, b) except OSError: return os.path.normpath(a) == os.path.normpath(b) def sameuuid(a: str, b: str) -> bool: """Compare two UUIDs.""" return a and b and a.lower() == b.lower() def setdefault(self: dict, other: dict): """Like .update() but values in self take priority.""" for k, v in other.items(): self.setdefault(k, v) def extend(a: dict, b: dict) -> dict: """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: """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 class ObjDictView: """Provide dict-like access view to the attributes of an object.""" def __init__(self, object, valid=None): self._object = object self._valid = valid def __getitem__(self, key): if self._valid is None or key in self._valid: try: return getattr(self._object, key) except AttributeError: raise KeyError(key) raise KeyError("Unknown key: {}".format(key)) class DaemonBase: active = False def activate(self): udisks = self._mounter.udisks for event, handler in self.events.items(): udisks.connect(event, handler) self.active = True def deactivate(self): udisks = self._mounter.udisks for event, handler in self.events.items(): udisks.disconnect(event, handler) self.active = False # ---------------------------------------- # byte array to string conversion # ---------------------------------------- def decode_ay(ay): """Convert binary blob from DBus queries to strings.""" if ay is None: return '' elif isinstance(ay, str): 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 is_utf8(bs): """Check if the given bytes string is utf-8 decodable.""" try: bs.decode('utf-8') return True except UnicodeDecodeError: return False def exc_message(exc): """Get an exception message.""" message = getattr(exc, 'message', None) return message or str(exc) def format_exc(*exc_info): """Show exception with traceback.""" typ, exc, tb = exc_info or sys.exc_info() error = traceback.format_exception(typ, exc, tb) return "".join(error) udiskie-2.1.0/udiskie/cli.py0000664000372000037200000005446013615574740016614 0ustar travistravis00000000000000""" Command line interface logic. The application classes in this module are installed as executables via setuptools entry points. """ # import udiskie.depend first - for side effects! from .depend import has_Notify, has_Gtk, _in_X import inspect import logging.config import traceback from gi.repository import GLib from docopt import docopt, DocoptExit import udiskie import udiskie.config import udiskie.mount import udiskie.udisks2 from .common import extend, ObjDictView from .locale import _ from .async_ import Future, ensure_future, gather __all__ = [ 'Daemon', 'Mount', 'Umount', ] class Choice: """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: """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 args[self._name] class OptionalValue: 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 args[self._name] class SelectLevel(logging.Filter): def __init__(self, level): self.level = level def filter(self, record): return record.levelno == self.level class _EntryPoint: """ Abstract base class for program entry points. Implementations need to - implement :meth:`_init` - provide a docstring - extend :cvar:`option_defaults` and :cvar:`option_rules`. """ option_defaults = { 'log_level': logging.INFO, } option_rules = { 'log_level': Choice({ '--verbose': logging.DEBUG, '--quiet': logging.ERROR}), } usage_remarks = _(""" Note, that the options in the individual groups are mutually exclusive. The config file can be a JSON or preferably 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.""" # parse program options (retrieve log level and config file name): args = docopt(self.usage, version='udiskie ' + 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']) debug = log_level <= logging.DEBUG logging.config.dictConfig({ 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'plain': {'format': _('%(message)s')}, 'detail': {'format': _( '%(levelname)s [%(asctime)s] %(name)s: %(message)s')}, }, 'filters': { 'info': {'()': 'udiskie.cli.SelectLevel', 'level': logging.INFO}, }, 'handlers': { 'info': {'class': 'logging.StreamHandler', 'stream': 'ext://sys.stdout', 'formatter': 'plain', 'filters': ['info']}, 'error': {'class': 'logging.StreamHandler', 'stream': 'ext://sys.stderr', 'formatter': 'plain', 'level': 'WARNING'}, 'debug': {'class': 'logging.StreamHandler', 'stream': 'ext://sys.stderr', 'formatter': 'detail'}, }, # configure root logger: 'root': { 'handlers': ['info', 'debug' if debug else 'error'], 'level': log_level, }, }) # 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 def program_options(self, args): """Get program options from docopt parsed options.""" 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. Returns program exit code.""" return cls(argv).run() @property def version(self): """Version from setuptools metadata.""" return udiskie.__version__ @property def usage(self): """Full usage string.""" return inspect.cleandoc(self.__doc__ + self.usage_remarks) def _init(self): """Return the application main task as Future.""" raise NotImplementedError() def run(self): """Run the main loop. Returns exit code.""" self.exit_code = 1 self.mainloop = GLib.MainLoop() try: future = ensure_future(self._start_async_tasks()) future.callbacks.append(self.set_exit_code) self.mainloop.run() return self.exit_code except KeyboardInterrupt: return 1 def set_exit_code(self, exit_code): self.exit_code = exit_code async def _start_async_tasks(self): """Start asynchronous operations.""" try: self.udisks = await udiskie.udisks2.Daemon.create() results = await self._init() return 0 if all(results) else 1 except Exception: traceback.print_exc() return 1 finally: self.mainloop.quit() class Component: def __init__(self, create): self.create = create self.instance = None @property def active(self): return self.instance is not None and self.instance.active def activate(self): if self.instance is None: self.instance = self.create() if not self.instance.active: self.instance.activate() def deactivate(self): if self.active: self.instance.deactivate() def toggle(self): if self.active: self.deactivate() else: self.activate() 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 -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 -m MENU, --menu MENU Tray menu [flat/nested] --appindicator Use appindicator for status icon --no-appindicator Don't use appindicator --password-cache MINUTES Set password cache timeout --no-password-cache Disable password cache -f PROGRAM, --file-manager PROGRAM Set program for browsing -F, --no-file-manager Disable browsing --terminal COMMAND Set terminal command line (e.g. "termite -d") --no-terminal Disable terminal action -p COMMAND, --password-prompt COMMAND Command for password retrieval -P, --no-password-prompt Disable unlocking --notify-command COMMAND Command to execute on events --no-notify-command Disable command notifications """ option_defaults = extend(_EntryPoint.option_defaults, { 'automount': True, 'notify': True, 'tray': False, 'menu': 'flat', 'appindicator': False, 'file_manager': 'xdg-open', 'terminal': '', 'password_prompt': 'builtin:gui', 'password_cache': False, 'notify_command': None, }) option_rules = extend(_EntryPoint.option_rules, { 'automount': Switch('automount'), 'notify': Switch('notify'), 'tray': Choice({ '--tray': True, '--no-tray': False, '--smart-tray': 'auto'}), 'menu': Value('--menu'), 'appindicator': Switch('appindicator'), 'file_manager': OptionalValue('--file-manager'), 'password_prompt': OptionalValue('--password-prompt'), 'password_cache': OptionalValue('--password-cache'), 'notify_command': OptionalValue('--notify-command'), }) def _init(self): import udiskie.prompt config = self.config options = self.options # prepare mounter object prompt = udiskie.prompt.password(options['password_prompt']) browser = udiskie.prompt.browser(options['file_manager']) terminal = udiskie.prompt.browser(options['terminal']) cache = None try: import udiskie.cache timeout = int(options['password_cache']) * 60 cache = udiskie.cache.PasswordCache(timeout) except ImportError: cache = None self.mounter = udiskie.mount.Mounter( config=config.device_config, prompt=prompt, browser=browser, terminal=terminal, cache=cache, cache_hint=options['password_cache'], udisks=self.udisks) # check component availability 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 _in_X: no_X_session = _( "Not run within X session. " "\nStarting udiskie without tray icon.\n") logging.getLogger(__name__).error(no_X_session) options['tray'] = 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" "\nStarting udiskie without tray icon.\n") logging.getLogger(__name__).error(gtk3_not_available) options['tray'] = False # start components tasks = [] self.notify = Component(self._load_notify) self.statusicon = Component(self._load_statusicon) self.automounter = self._load_automounter(options['automount']) self.automounter.activate() if options['notify']: self.notify.activate() if options['notify_command']: # is currently enabled/disabled statically only once: self.notify_command() if options['tray']: self.statusicon.activate() tasks.append(self.statusicon.instance._icon.task) else: tasks.append(Future()) if options['automount']: tasks.append(self.mounter.add_all()) return gather(*tasks) def _load_notify(self): import udiskie.notify from gi.repository import Notify Notify.init('udiskie') aconfig = self.config.notification_actions if self.options['automount']: aconfig.setdefault('device_added', []) else: aconfig.setdefault('device_added', ['mount']) return udiskie.notify.Notify( Notify.Notification.new, mounter=self.mounter, timeout=self.config.notifications, aconfig=aconfig) def notify_command(self): import udiskie.prompt return udiskie.prompt.notify_command( self.options['notify_command'], self.mounter) def _load_statusicon(self): import udiskie.tray options = self.options config = self.config if options['tray'] == 'auto': smart = True elif options['tray'] is True: smart = False else: raise ValueError("Invalid tray: %s" % (options['tray'],)) icons = udiskie.tray.Icons(self.config.icon_names) actions = udiskie.mount.DeviceActions(self.mounter) if options['menu'] == 'flat': flat = True # dropped legacy 'nested' mode: elif options['menu'] in ('smart', 'nested'): flat = False else: raise ValueError("Invalid menu: %s" % (options['menu'],)) menu_maker = udiskie.tray.UdiskieMenu(self, icons, actions, flat, config.quickmenu_actions) if options['appindicator']: import udiskie.appindicator TrayIcon = udiskie.appindicator.AppIndicatorIcon else: TrayIcon = udiskie.tray.TrayIcon trayicon = TrayIcon(menu_maker, icons) return udiskie.tray.UdiskieStatusIcon(trayicon, menu_maker, smart) def _load_automounter(self, automount): import udiskie.automount return udiskie.automount.AutoMounter(self.mounter, automount) 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 -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 """ option_defaults = extend(_EntryPoint.option_defaults, { 'recursive': None, '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): import udiskie.prompt config = self.config options = self.options device_config = config.device_config if options['options']: device_config.insert(0, udiskie.config.MountOptions({ 'options': [o.strip() for o in options['options'].split(',')], })) prompt = udiskie.prompt.password(options['password_prompt']) mounter = udiskie.mount.Mounter( config=device_config, prompt=prompt, udisks=self.udisks) recursive = options['recursive'] if options['']: tasks = [mounter.add(path, recursive=recursive) for path in options['']] else: tasks = [mounter.add_all(recursive=recursive)] return gather(*tasks) 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 -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 """ option_defaults = extend(_EntryPoint.option_defaults, { 'detach': None, '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): config = self.config options = self.options mounter = udiskie.mount.Mounter( self.udisks, config=config.device_config) 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['']] else: tasks = [mounter.remove_all(**strategy)] return gather(*tasks) def _parse_filter(spec): try: key, val = spec.split('=', 1) except ValueError: if spec.startswith('!'): val = False key = spec[1:] else: val = True key = spec return key, val class Info(_EntryPoint): """ udiskie-info: get information about handleable devices. Usage: udiskie-info [options] [-o OUTPUT] [-f FILTER]... (-a | DEVICE...) udiskie-info (--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 -h, --help Show this help -V, --version Show version information Unmount options: -a, --all List all handleable devices -o COL, --output COL Specify output columns in a format string containing the allowed device attributes, e.g.: "{ui_label} {is_luks}" [default: device_presentation]. -f FILT, --filter FILT Print only devices that match the given filter. """ option_defaults = extend(_EntryPoint.option_defaults, { 'output': '', 'filter': '', '': None, }) option_rules = extend(_EntryPoint.option_rules, { 'output': Value('--output'), 'filter': Value('--filter'), '': Value('DEVICE'), }) def _init(self): config = self.config options = self.options mounter = udiskie.mount.Mounter( self.udisks, config=config.device_config) if options['']: devices = [self.udisks.find(path) for path in options['']] else: devices = mounter.get_all_handleable() DeviceFilter = udiskie.config.DeviceFilter output = options['output'] # old behaviour: single attribute if output in DeviceFilter.VALID_PARAMETERS: def format_output(device): return getattr(device, output) # new behaviour: format string else: def format_output(device): view = ObjDictView(device, DeviceFilter.VALID_PARAMETERS) return output.format_map(view) filters = [_parse_filter(spec) for spec in options['filter']] matcher = DeviceFilter(dict(filters)) for device in devices: if matcher.match(device): print(format_output(device)) return gather() udiskie-2.1.0/udiskie/tray.py0000664000372000037200000003424613615574740017024 0ustar travistravis00000000000000""" Tray icon for udiskie. """ from gi.repository import Gio from gi.repository import Gtk from .async_ import run_bg, Future from .common import setdefault, DaemonBase from .locale import _ from .mount import Action, prune_empty_node from .prompt import Dialog __all__ = ['UdiskieMenu', 'TrayIcon'] class MenuFolder: def __init__(self, label, items): self.label = label self.items = items def __bool__(self): return bool(self.items) __nonzero__ = __bool__ class MenuSection(MenuFolder): pass class SubMenu(MenuFolder): pass class Icons: """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'], 'terminal': ['terminal', 'utilities-terminal'], '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'], 'delete': ['udiskie-eject'], 'losetup': ['udiskie-mount'], } 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, str): self._icon_names[k] = [v] def get_icon_name(self, icon_id: str) -> str: """Lookup the system icon name from udisie-internal id.""" icon_theme = Gtk.IconTheme.get_default() for name in self._icon_names[icon_id]: if icon_theme.has_icon(name): return name return 'not-available' def get_icon(self, icon_id: str, size: "Gtk.IconSize") -> "Gtk.Image": """Load Gtk.Image from udiskie-internal id.""" return Gtk.Image.new_from_gicon(self.get_gicon(icon_id), size) def get_gicon(self, icon_id: str) -> "Gio.Icon": """Lookup Gio.Icon from udiskie-internal id.""" return Gio.ThemedIcon.new_from_names(self._icon_names[icon_id]) class UdiskieMenu: """ Builder for udiskie menus. Objects of this class generate action menus when being called. """ def __init__(self, daemon, icons, actions, flat=True, quickmenu_actions=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 - terminal Open mount location in terminal - 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._daemon = daemon self._mounter = daemon.mounter self._actions = actions self._quit_action = daemon.mainloop.quit self.flat = flat # actions shown in the quick-menu ("flat", left-click): self._quickmenu_actions = quickmenu_actions or [ 'mount', 'browse', 'terminal', 'unlock', 'detach', 'delete', # suppressed: # 'unmount', # 'lock', # 'eject', # 'forget_password', ] def __call__(self, menu, extended=True): """Populate the Gtk.Menu with udiskie mount operations.""" # create actions items flat = self.flat and not extended self._create_menu_items(menu, self._prepare_menu(self.detect(), flat)) if extended: self._insert_options(menu) return menu def _insert_options(self, menu): """Add configuration options to menu.""" menu.append(Gtk.SeparatorMenuItem()) menu.append(self._menuitem( _('Mount disc image'), self._icons.get_icon('losetup', Gtk.IconSize.MENU), run_bg(lambda _: self._losetup()) )) menu.append(Gtk.SeparatorMenuItem()) menu.append(self._menuitem( _("Enable automounting"), icon=None, onclick=lambda _: self._daemon.automounter.toggle_on(), checked=self._daemon.automounter.is_on(), )) menu.append(self._menuitem( _("Enable notifications"), icon=None, onclick=lambda _: self._daemon.notify.toggle(), checked=self._daemon.notify.active, )) # append menu item for closing the application if self._quit_action: menu.append(Gtk.SeparatorMenuItem()) menu.append(self._menuitem( _('Quit'), self._icons.get_icon('quit', Gtk.IconSize.MENU), lambda _: self._quit_action() )) async def _losetup(self): gtk_dialog = Gtk.FileChooserDialog( _('Open disc image'), None, Gtk.FileChooserAction.OPEN, (_('Open'), Gtk.ResponseType.OK, _('Cancel'), Gtk.ResponseType.CANCEL)) with Dialog(gtk_dialog) as dialog: response = await dialog if response == Gtk.ResponseType.OK: await self._mounter.losetup(dialog.window.get_filename()) def detect(self): """Detect all currently known devices. Returns the root device.""" root = self._actions.detect() prune_empty_node(root, set()) return root def _create_menu(self, items): """ Create a menu from the given node. :param list items: list of menu items :returns: a new Gtk.Menu object holding all items of the node """ menu = Gtk.Menu() self._create_menu_items(menu, items) return menu def _create_menu_items(self, menu, items): def make_action_callback(node): return run_bg(lambda _: node.action()) for node in items: 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, SubMenu): menu.append(self._menuitem( node.label, icon=None, onclick=self._create_menu(node.items))) elif isinstance(node, MenuSection): self._create_menu_section(menu, node) else: raise ValueError(_("Invalid node!")) if len(menu) == 0: mi = self._menuitem(_("No external devices"), None, None) mi.set_sensitive(False) menu.append(mi) def _create_menu_section(self, menu, section): if len(menu) > 0: menu.append(Gtk.SeparatorMenuItem()) if section.label: mi = self._menuitem(section.label, None, None) mi.set_sensitive(False) menu.append(mi) self._create_menu_items(menu, section.items) def _menuitem(self, label, icon, onclick, checked=None): """ 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 checked is not None: item = Gtk.CheckMenuItem() item.set_active(checked) elif 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) elif onclick is not None: item.connect('activate', onclick) return item def _prepare_menu(self, node, flat=None): """ Prepare the menu hierarchy from the given device tree. :param Device node: root node of device hierarchy :returns: menu hierarchy as list """ if flat is None: flat = self.flat ItemGroup = MenuSection if flat else SubMenu return [ ItemGroup(branch.label, self._collapse_device(branch, flat)) for branch in node.branches if branch.methods or branch.branches ] def _collapse_device(self, node, flat): """Collapse device hierarchy into a flat folder.""" items = [item for branch in node.branches for item in self._collapse_device(branch, flat) if item] show_all = not flat or self._quickmenu_actions == 'all' methods = node.methods if show_all else [ method for method in node.methods if method.method in self._quickmenu_actions ] if flat: items.extend(methods) else: items.append(MenuSection(None, methods)) return items class TrayIcon: """Default TrayIcon class.""" def __init__(self, menumaker, icons, statusicon=None): """ 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 = Future() menumaker._quit_action = self.destroy def destroy(self): self.show(False) self.task.set_result(True) 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, extended): """Create the context menu.""" menu = Gtk.Menu() self._menu(menu, extended) return 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(), extended=False) def _popup_menu(self, icon, button, time, extended=True): """Handle a right click event (show the menu).""" m = self.create_context_menu(extended) 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 UdiskieStatusIcon(DaemonBase): """ Manage a status icon. When `smart` is on, the icon will automatically hide if there is no action available and the menu will have no 'Quit' item. """ def __init__(self, icon, menumaker, smart=False): self._icon = icon self._menumaker = menumaker self._mounter = menumaker._mounter self._quit_action = menumaker._quit_action self.smart = smart self.active = False self.events = { 'device_changed': self.update, 'device_added': self.update, 'device_removed': self.update, } def activate(self): super().activate() self.update() def deactivate(self): super().deactivate() self._icon.show(False) @property def smart(self): return getattr(self, '_smart', None) @smart.setter def smart(self, smart): if smart == self.smart: return if smart: self._menumaker._quit_action = None else: self._menumaker._quit_action = self._quit_action self._smart = smart self.update() def has_menu(self): """Check if a menu action is available.""" return any(self._menumaker._prepare_menu(self._menumaker.detect())) def update(self, *args): """Show/hide icon depending on whether there are devices.""" if self.smart: self._icon.show(self.has_menu()) else: self._icon.show(True) udiskie-2.1.0/udiskie/config.py0000664000372000037200000002011413615574740017277 0ustar travistravis00000000000000""" 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. """ import logging import os import fnmatch from .common import exc_message from .locale import _ __all__ = ['DeviceFilter', 'match_config', 'Config'] def lower(s): try: return s.lower() except AttributeError: return s def match_value(value, pattern): if isinstance(value, (list, tuple)): return any(match_value(v, pattern) for v in value) if isinstance(value, str) and isinstance(pattern, str): return fnmatch.fnmatch(value.lower(), pattern.lower()) return lower(value) == lower(pattern) class DeviceFilter: """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_size', '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', 'mount_path', 'is_unlocked', 'in_use', 'should_automount', 'ui_label', 'loop_file', 'setup_by_uid', 'autoclear', 'symlinks', 'drive_model', 'drive_vendor', 'drive_label', 'ui_device_label', 'ui_device_presentation', 'ui_id_label', 'ui_id_uuid', ] def __init__(self, match): """Construct from dict of device matching attributes.""" self._log = logging.getLogger(__name__) self._match = match = match.copy() self._values = {} # mount options: if 'options' in match: options = match.pop('options') if isinstance(options, str): options = [o.strip() for o in options.split(',')] self._values['options'] = options # ignore device: if 'ignore' in match: self._values['ignore'] = match.pop('ignore') # automount: if 'automount' in match: self._values['automount'] = match.pop('automount') # keyfile: if 'keyfile' in match: keyfile = match.pop('keyfile') keyfile = os.path.expandvars(keyfile) keyfile = os.path.expanduser(keyfile) self._values['keyfile'] = keyfile if 'skip' in match: self._values['skip'] = match.pop('skip') # the use of list() makes deletion inside the loop safe: for k in list(self._match): if k not in self.VALID_PARAMETERS: self._log.error(_('Unknown matching attribute: {!r}', k)) del self._match[k] self._log.debug(_('{0} created', self)) def __str__(self): return _('{0}(match={1!r}, value={2!r})', self.__class__.__name__, self._match, self._values) def match(self, device): """Check if the device object matches this filter.""" return all(match_value(getattr(device, k), v) for k, v in self._match.items()) def has_value(self, kind): return kind in self._values def value(self, kind, device): """ Get the value for the device object associated with this filter. If :meth:`match` is False for the device, the return value of this method is undefined. """ self._log.debug(_('{0}(match={1!r}, {2}={3!r}) used for {4}', self.__class__.__name__, self._match, kind, self._values[kind], device.object_path)) return self._values[kind] class MountOptions(DeviceFilter): """Associate a list of mount options to matched devices.""" def __init__(self, config_item): config_item.setdefault('options', None) super().__init__(config_item) class IgnoreDevice(DeviceFilter): """Associate a boolean ignore flag to matched devices.""" def __init__(self, config_item): config_item.setdefault('ignore', True) super().__init__(config_item) def match_config(filters, device, kind, default): """ Matches devices against multiple :class:`DeviceFilter`s. :param list filters: device filters :param Device device: device to be mounted :param str kind: value kind :param default: default value :returns: value of the first matching filter """ while device is not None: for f in filters: if f.has_value(kind) and f.match(device): return f.value(kind, device) # 'skip' allows skipping further rules and directly moving on # lookup on the parent device: if f.has_value('skip') and f.match(device) and ( f.value('skip', device) in (True, 'all', kind)): break device = device.partition_slave or device.luks_cleartext_slave return default class Config: """Udiskie config in memory representation.""" def __init__(self, data): """Initialize with preparsed data dict.""" self._data = data or {} @classmethod def default_pathes(cls): """Return the default config file paths as a 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 YAML config file. Returns Config object. :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: from yaml import safe_load as load with open(path) as f: return cls(load(f)) @property def device_config(self): device_config = map(DeviceFilter, self._data.get('device_config', [])) mount_options = map(MountOptions, self._data.get('mount_options', [])) ignore_device = map(IgnoreDevice, self._data.get('ignore_device', [])) return list(device_config) + list(mount_options) + list(ignore_device) @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() @property def quickmenu_actions(self): """Get the set of actions to be shown in the quickmenu (left-click).""" return self._data.get('quickmenu_actions', None) udiskie-2.1.0/udiskie/dbus.py0000664000372000037200000002137313615574740016777 0ustar travistravis00000000000000""" Common DBus utilities. """ from functools import partial from gi.repository import Gio from gi.repository import GLib from .async_ import gio_callback, pack, Future __all__ = [ 'InterfaceProxy', 'PropertiesProxy', 'ObjectProxy', 'BusProxy', 'connect_service', 'MethodsProxy', ] unpack_variant = GLib.Variant.unpack async def call(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: """ future = Future() cancellable = None proxy.call( method_name, GLib.Variant(signature, tuple(args)), flags, timeout_msec, cancellable, gio_callback, future, ) result = await future value = proxy.call_finish(result) return pack(*unpack_variant(value)) async def call_with_fd_list(proxy, method_name, signature, args, fds, 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 list fds: :param int flags: :param int timeout_msec: """ future = Future() cancellable = None fd_list = Gio.UnixFDList.new_from_array(fds) proxy.call_with_unix_fd_list( method_name, GLib.Variant(signature, tuple(args)), flags, timeout_msec, fd_list, cancellable, gio_callback, future, ) result = await future value, fds = proxy.call_with_unix_fd_list_finish(result) return pack(*unpack_variant(value)) class InterfaceProxy: """ DBus proxy object for a specific interface. :ivar str object_path: object path of the DBus object :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 """ self._proxy = proxy self.object_path = proxy.get_object_path() @property def object(self): """Get an ObjectProxy instanec for the underlying object.""" 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, returns subscription id (int).""" interface = self._proxy.get_interface_name() return self.object.connect(interface, event, handler) def call(self, method_name, signature='()', *args): return call(self._proxy, method_name, signature, args) class PropertiesProxy(InterfaceProxy): Interface = 'org.freedesktop.DBus.Properties' def __init__(self, proxy, interface_name=None): super().__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: """Simple proxy class for a DBus object.""" 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 Future(Gio.DBusProxy) for the specified interface.""" return proxy_new( 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, ) async def get_interface(self, name): """Get an InterfaceProxy for the specified interface.""" proxy = await self._get_interface(name) return InterfaceProxy(proxy) async def get_property_interface(self, interface_name=None): proxy = await self._get_interface(PropertiesProxy.Interface) return PropertiesProxy(proxy, interface_name) @property def bus(self): """Get a BusProxy for the underlying bus.""" return BusProxy(self.connection, self.bus_name) def connect(self, interface, event, handler): """Connect to a DBus signal. Returns subscription id (int).""" object_path = self.object_path return self.bus.connect(interface, event, object_path, handler) async def call(self, interface_name, method_name, signature='()', *args): proxy = await self.get_interface(interface_name) result = await proxy.call(method_name, signature, *args) return result class BusProxy: """ 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 an ObjectProxy representing the specified object.""" return ObjectProxy(self.connection, self.bus_name, object_path) def connect(self, interface, event, object_path, handler): """ Connect to a DBus signal. If ``object_path`` is None, subscribe for all objects and invoke the callback with the object_path as its first argument. """ if object_path: def callback(connection, sender_name, object_path, interface_name, signal_name, parameters): return handler(*unpack_variant(parameters)) else: def callback(connection, sender_name, object_path, interface_name, signal_name, parameters): return handler(object_path, *unpack_variant(parameters)) return self.connection.signal_subscribe( self.bus_name, interface, event, object_path, None, Gio.DBusSignalFlags.NONE, callback, ) def disconnect(self, subscription_id): """Disconnect a DBus signal subscription.""" self.connection.signal_unsubscribe(subscription_id) async def proxy_new(connection, flags, info, name, object_path, interface_name): """Asynchronously call the specified method on a DBus proxy object.""" future = Future() cancellable = None Gio.DBusProxy.new( connection, flags, info, name, object_path, interface_name, cancellable, gio_callback, future, ) result = await future value = Gio.DBusProxy.new_finish(result) if value is None: raise RuntimeError("Failed to connect DBus object!") return value async def proxy_new_for_bus(bus_type, flags, info, name, object_path, interface_name): """Asynchronously call the specified method on a DBus proxy object.""" future = Future() cancellable = None Gio.DBusProxy.new_for_bus( bus_type, flags, info, name, object_path, interface_name, cancellable, gio_callback, future, ) result = await future value = Gio.DBusProxy.new_for_bus_finish(result) if value is None: raise RuntimeError("Failed to connect DBus object!") return value async def connect_service(bus_name, object_path, interface): """Connect to the service object on DBus, return InterfaceProxy.""" proxy = await proxy_new_for_bus( Gio.BusType.SYSTEM, Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, info=None, name=bus_name, object_path=object_path, interface_name=interface, ) return InterfaceProxy(proxy) class MethodsProxy: """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-2.1.0/udiskie/async_.py0000664000372000037200000001503113615574740017310 0ustar travistravis00000000000000""" 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. """ import traceback from functools import partial from subprocess import CalledProcessError from gi.repository import GLib from gi.repository import Gio from .common import cachedproperty, wraps __all__ = [ 'pack', 'to_coro', 'run_bg', 'Future', 'gather', 'Task', ] ACTIVE_TASKS = set() def pack(*values): """Unpack a return tuple to a yield expression return value.""" # Schizophrenic returns from asyncs. Inspired by # gi.overrides.Gio.DBusProxy. if len(values) == 0: return None elif len(values) == 1: return values[0] else: return values class Future: """ Base class for asynchronous operations. One `Future' 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. Success/error exit is signaled to the observer by calling exactly one of `self.set_result(value)` or `self.set_exception(exception)` when the operation finishes. For implementations, see :class:`Task` or :class:`Dialog`. """ @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].""" return [fn(*args) for fn in callbacks] def set_result(self, value): """Signal successful completion.""" self._finish(self.callbacks, value) def set_exception(self, exception): """Signal unsuccessful completion.""" was_handled = self._finish(self.errbacks, exception) if not was_handled: traceback.print_exception( type(exception), exception, exception.__traceback__) def __await__(self): ACTIVE_TASKS.add(self) try: return (yield self) finally: ACTIVE_TASKS.remove(self) def to_coro(func): @wraps(func) async def coro(*args, **kwargs): return func(*args, **kwargs) return coro def run_bg(func): @wraps(func) def runner(*args, **kwargs): return ensure_future(func(*args, **kwargs)) return runner class gather(Future): """ Manages a collection of asynchronous tasks. The callbacks are executed when all of the subtasks have completed. """ def __init__(self, *tasks): """Create from a list of `Future`-s.""" tasks = list(tasks) self._done = False self._results = {} self._num_tasks = len(tasks) if not tasks: run_soon(self.set_result, []) for idx, task in enumerate(tasks): task = ensure_future(task) task.callbacks.append(partial(self._subtask_result, idx)) task.errbacks.append(partial(self._subtask_error, idx)) def _subtask_result(self, idx, value): """Receive a result from a single subtask.""" self._results[idx] = value if len(self._results) == self._num_tasks: self.set_result([ self._results[i] for i in range(self._num_tasks) ]) def _subtask_error(self, idx, error): """Receive an error from a single subtask.""" self.set_exception(error) self.errbacks.clear() 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) def sleep(seconds): future = Future() GLib.timeout_add(int(seconds*1000), future.set_result, True) return future def ensure_future(awaitable): if isinstance(awaitable, Future): return awaitable return Task(iter(awaitable.__await__())) class Task(Future): """Turns a generator into a Future.""" def __init__(self, generator): """Create and start a ``Task`` from the specified generator.""" self._generator = generator run_soon(self._resume, next, self._generator) def _resume(self, func, *args): """Resume the coroutine by throwing a value or returning a value from the ``await`` and handle further awaits.""" try: value = func(*args) except StopIteration: self._generator.close() self.set_result(None) except Exception as e: self._generator.close() self.set_exception(e) else: assert isinstance(value, Future) value.callbacks.append(partial(self._resume, self._generator.send)) value.errbacks.append(partial(self._resume, self._generator.throw)) def gio_callback(proxy, result, future): future.set_result(result) async def exec_subprocess(argv): """ An Future 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 """ future = Future() process = Gio.Subprocess.new( argv, Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDIN_INHERIT) stdin_buf = None cancellable = None process.communicate_async( stdin_buf, cancellable, gio_callback, future) result = await future success, stdout, stderr = process.communicate_finish(result) stdout = stdout.get_data() # GLib.Bytes -> bytes if not success: raise RuntimeError("Subprocess did not exit normally!") exit_code = process.get_exit_status() if exit_code != 0: raise CalledProcessError( "Subprocess returned a non-zero exit-status!", exit_code, stdout) return stdout udiskie-2.1.0/udiskie/locale.py0000664000372000037200000000054613615574740017300 0ustar travistravis00000000000000""" I18n utilities. """ from gettext import translation _t = translation('udiskie', localedir=None, languages=None, fallback=True) def _(text, *args, **kwargs): """Translate and then and format the text with ``str.format``.""" msg = _t.gettext(text) if args or kwargs: return msg.format(*args, **kwargs) else: return msg udiskie-2.1.0/udiskie/mount.py0000664000372000037200000007775213615574740017220 0ustar travistravis00000000000000""" Mount utilities. """ from distutils.spawn import find_executable from collections import namedtuple from functools import partial import logging import os from .async_ import to_coro, gather, sleep from .common import wraps, setdefault, exc_message, format_exc from .config import IgnoreDevice, match_config from .locale import _ __all__ = ['Mounter'] # TODO: add / remove / XXX_all should make proper use of the asynchronous # execution. def _error_boundary(fn): @wraps(fn) async def wrapper(self, device, *args, **kwargs): try: return await fn(self, device, *args, **kwargs) except Exception as e: self._log.error(_('failed to {0} {1}: {2}', fn.__name__, device, exc_message(e))) self._log.debug(format_exc()) return False 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: """ 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, config=None, prompt=None, browser=None, terminal=None, cache=None, cache_hint=False): """ Initialize mounter with the given defaults. :param udisks: udisks service object. May be a Sniffer or a Daemon. :param list config: list of :class:`DeviceFilter` :param callable prompt: retrieve passwords for devices :param callable browser: open devices :param callable terminal: open devices in terminal If prompt is None, device unlocking will not work. If browser is None, browse will not work. """ self.udisks = udisks self._config = (config or []) + [ IgnoreDevice({'symlinks': '/dev/mapper/docker-*', 'ignore': True}), IgnoreDevice({'symlinks': '/dev/disk/by-id/dm-name-docker-*', 'ignore': True}), IgnoreDevice({'is_loop': True, 'is_ignored': False, 'loop_file': '/*', 'ignore': False}), IgnoreDevice({'is_block': False, 'ignore': True}), IgnoreDevice({'is_external': False, 'is_toplevel': True, 'ignore': True}), IgnoreDevice({'is_ignored': True, 'ignore': True})] self._prompt = prompt self._browser = browser self._terminal = terminal self._cache = cache self._cache_hint = cache_hint self._log = logging.getLogger(__name__) def _find_device(self, device_or_path): """Find device object from path.""" return self.udisks.find(device_or_path) async def _find_device_losetup(self, device_or_path): try: device = self.udisks.find(device_or_path) return device, False except FileNotFoundError: if not os.path.isfile(device_or_path): raise device = await self.losetup(device_or_path) return device, True @_error_boundary async def browse(self, device): """ Launch file manager on the mount path of the specified device. :param device: device object, block device path or mount path :returns: whether the program was successfully launched. """ device = self._find_device(device) if not device.is_mounted: self._log.error(_("not browsing {0}: not mounted", device)) return False if not self._browser: self._log.error(_("not browsing {0}: no program", device)) 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)) return True @_error_boundary async def terminal(self, device): """ Launch terminal on the mount path of the specified device. :param device: device object, block device path or mount path :returns: whether the program was successfully launched. """ device = self._find_device(device) if not device.is_mounted: self._log.error(_("not opening terminal {0}: not mounted", device)) return False if not self._terminal: self._log.error(_("not opening terminal {0}: no program", device)) return False self._log.debug(_('opening {0} on {0.mount_paths[0]}', device)) self._terminal(device.mount_paths[0]) self._log.info(_('opened {0} on {0.mount_paths[0]}', device)) return True # mount/unmount @_error_boundary async 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. """ device = self._find_device(device) if not self.is_handleable(device) or not device.is_filesystem: self._log.warn(_('not mounting {0}: unhandled device', device)) return False if device.is_mounted: self._log.info(_('not mounting {0}: already mounted', device)) return True options = match_config(self._config, device, 'options', None) kwargs = dict(options=options) self._log.debug(_('mounting {0} with {1}', device, kwargs)) self._check_device_before_mount(device) mount_path = await device.mount(**kwargs) self._log.info(_('mounted {0} on {1}', device, mount_path)) return True def _check_device_before_mount(self, device): if device.id_type == 'ntfs' and not find_executable('ntfs-3g'): self._log.warn(_( "Mounting NTFS device with default driver.\n" "Please install 'ntfs-3g' if you experience problems or the " "device is readonly.")) @_error_boundary async 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 """ device = self._find_device(device) if not self.is_handleable(device) or not device.is_filesystem: self._log.warn(_('not unmounting {0}: unhandled device', device)) return False if not device.is_mounted: self._log.info(_('not unmounting {0}: not mounted', device)) return True self._log.debug(_('unmounting {0}', device)) await device.unmount() self._log.info(_('unmounted {0}', device)) return True # unlock/lock (LUKS) @_error_boundary async 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 """ device = self._find_device(device) if not self.is_handleable(device) or not device.is_crypto: self._log.warn(_('not unlocking {0}: unhandled device', device)) return False if device.is_unlocked: self._log.info(_('not unlocking {0}: already unlocked', device)) return True if not self._prompt: self._log.error(_('not unlocking {0}: no password prompt', device)) return False unlocked = await self._unlock_from_cache(device) if unlocked: return True unlocked = await self._unlock_from_keyfile(device) if unlocked: return True options = dict(allow_keyfile=self.udisks.keyfile_support, allow_cache=self._cache is not None, cache_hint=self._cache_hint) password = await self._prompt(device, options) # password is either None or udiskie.prompt.PasswordResult: if password is None: self._log.debug(_('not unlocking {0}: cancelled by user', device)) return False cache_hint = password.cache_hint password = password.password if isinstance(password, bytes): self._log.debug(_('unlocking {0} using keyfile', device)) await device.unlock_keyfile(password) else: self._log.debug(_('unlocking {0}', device)) await device.unlock(password) self._update_cache(device, password, cache_hint) self._log.info(_('unlocked {0}', device)) return True async def _unlock_from_cache(self, device): if not self._cache: return False try: password = self._cache[device] except KeyError: self._log.debug(_("no cached key for {0}", device)) return False self._log.debug(_('unlocking {0} using cached password', device)) try: await device.unlock_keyfile(password) except Exception: self._log.debug(_('failed to unlock {0} using cached password', device)) self._log.debug(format_exc()) return False self._log.info(_('unlocked {0} using cached password', device)) return True async def _unlock_from_keyfile(self, device): if not self.udisks.keyfile_support: return False filename = match_config(self._config, device, 'keyfile', None) if filename is None: self._log.debug(_('No matching keyfile rule for {}.', device)) return False try: with open(filename, 'rb') as f: keyfile = f.read() except IOError: self._log.warn(_('keyfile for {0} not found: {1}', device, filename)) return False self._log.debug(_('unlocking {0} using keyfile {1}', device, filename)) try: await device.unlock_keyfile(keyfile) except Exception: self._log.debug(_('failed to unlock {0} using keyfile', device)) self._log.debug(format_exc()) return False self._log.info(_('unlocked {0} using keyfile', device)) return True def _update_cache(self, device, password, cache_hint): if not self._cache: return # TODO: could allow numeric cache_hint (=timeout)… if cache_hint or cache_hint is None: self._cache[device] = password def forget_password(self, device): try: del self._cache[device] except KeyError: pass @_error_boundary async def lock(self, device): """ Lock device if unlocked. :param device: device object, block device path or mount path :returns: whether the device is locked """ device = self._find_device(device) if not self.is_handleable(device) or not device.is_crypto: self._log.warn(_('not locking {0}: unhandled device', device)) return False if not device.is_unlocked: self._log.info(_('not locking {0}: not unlocked', device)) return True self._log.debug(_('locking {0}', device)) await device.lock() self._log.info(_('locked {0}', device)) return True # add/remove (unlock/lock or mount/unmount) @_error_boundary async def add(self, device, recursive=None): """ 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 """ device, created = await self._find_device_losetup(device) if created and recursive is False: return device if device.is_filesystem: success = await self.mount(device) elif device.is_crypto: success = await self.unlock(device) if success and recursive: await self.udisks._sync() device = self.udisks[device.object_path] success = await self.add( device.luks_cleartext_holder, recursive=True) elif (recursive and device.is_partition_table and self.is_handleable(device)): tasks = [ self.add(dev, recursive=True) for dev in self.get_all_handleable() if dev.is_partition and dev.partition_slave == device ] results = await gather(*tasks) success = all(results) else: self._log.info(_('not adding {0}: unhandled device', device)) return False return success @_error_boundary async def auto_add(self, device, recursive=None, automount=True): """ 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 """ device, created = await self._find_device_losetup(device) if created and recursive is False: return device if device.is_luks_cleartext and self.udisks.version_info >= (2, 7, 0): await sleep(1.5) # temporary workaround for #153, unreliable success = True if not self.is_automount(device, automount): pass elif device.is_filesystem: if not device.is_mounted: success = await self.mount(device) elif device.is_crypto: if self._prompt and not device.is_unlocked: success = await self.unlock(device) if success and recursive: await self.udisks._sync() device = self.udisks[device.object_path] success = await self.auto_add( device.luks_cleartext_holder, recursive=True) elif recursive and device.is_partition_table: tasks = [ self.auto_add(dev, recursive=True) for dev in self.get_all_handleable() if dev.is_partition and dev.partition_slave == device ] results = await gather(*tasks) success = all(results) else: self._log.debug(_('not adding {0}: unhandled device', device)) return success @_error_boundary async 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 """ device = self._find_device(device) if device.is_filesystem: if device.is_mounted or not device.is_loop or detach is False: success = await self.unmount(device) elif device.is_crypto: if force and device.is_unlocked: await self.auto_remove(device.luks_cleartext_holder, force=True) success = await self.lock(device) elif (force and (device.is_partition_table or device.is_drive) and self.is_handleable(device)): kw = dict(force=True, detach=detach, eject=eject, lock=lock) tasks = [ self.auto_remove(child, **kw) for child in self.get_all_handleable() if _is_parent_of(device, child) ] results = await gather(*tasks) success = all(results) 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 if self.is_handleable(device): success = await self.lock(device) if eject: success = await self.eject(device) if (detach or detach is None) and device.is_loop: success = await self.delete(device, remove=False) elif detach: success = await self.detach(device) return success @_error_boundary async 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 """ device = self._find_device(device) success = True if not self.is_handleable(device): pass elif device.is_filesystem: if device.is_mounted: success = await self.unmount(device) elif device.is_crypto: if force and device.is_unlocked: await self.auto_remove(device.luks_cleartext_holder, force=True) if device.is_unlocked: success = await self.lock(device) elif force and (device.is_partition_table or device.is_drive): kw = dict(force=True, detach=detach, eject=eject, lock=lock) tasks = [ self.auto_remove(child, **kw) for child in self.get_all_handleable() if _is_parent_of(device, child) ] results = await gather(*tasks) success = all(results) 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 = await self.lock(device) if eject and device.has_media: success = await self.eject(device) if (detach or detach is None) and device.is_loop: success = await self.delete(device, remove=False) elif detach and device.is_detachable: success = await self.detach(device) return success # eject/detach device @_error_boundary async 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 """ device = self._find_device(device) if not self.is_handleable(device): self._log.warn(_('not ejecting {0}: unhandled device')) return False drive = device.drive if not (drive.is_drive and drive.is_ejectable): self._log.warn(_('not ejecting {0}: drive not ejectable', drive)) return False if force: # Can't autoremove 'device.drive', because that will be filtered # due to block=False: await self.auto_remove(device.root, force=True) self._log.debug(_('ejecting {0}', device)) await drive.eject() self._log.info(_('ejected {0}', device)) return True @_error_boundary async 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 """ device = self._find_device(device) if not self.is_handleable(device): self._log.warn(_('not detaching {0}: unhandled device', device)) return False drive = device.root if not drive.is_detachable: self._log.warn(_('not detaching {0}: drive not detachable', drive)) return False if force: await self.auto_remove(drive, force=True) self._log.debug(_('detaching {0}', device)) await drive.detach() self._log.info(_('detached {0}', device)) return True # mount_all/unmount_all async 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 """ tasks = [self.auto_add(device, recursive=recursive) for device in self.get_all_handleable_leaves()] results = await gather(*tasks) success = all(results) return success async 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 """ kw = dict(force=True, detach=detach, eject=eject, lock=lock) tasks = [self.auto_remove(device, **kw) for device in self.get_all_handleable_roots()] results = await gather(*tasks) success = all(results) return success # loop devices async def losetup(self, image, read_only=True, offset=None, size=None, no_part_scan=None): """ Setup a loop device. :param str image: path of the image file :param bool read_only: :param int offset: :param int size: :param bool no_part_scan: :returns: the device object for the loop device """ try: device = self.udisks.find(image) except FileNotFoundError: pass else: self._log.info(_('not setting up {0}: already up', device)) return device if not os.path.isfile(image): self._log.error(_('not setting up {0}: not a file', image)) return None self._log.debug(_('setting up {0}', image)) fd = os.open(image, os.O_RDONLY) device = await self.udisks.loop_setup(fd, { 'offset': offset, 'size': size, 'read-only': read_only, 'no-part-scan': no_part_scan, }) self._log.info(_('set up {0} as {1}', image, device.device_presentation)) return device @_error_boundary async def delete(self, device, remove=True): """ Detach the loop device. :param device: device object, block device path or mount path :param bool remove: whether to unmount the partition etc. :returns: whether the loop device is deleted """ device = self._find_device(device) if not self.is_handleable(device) or not device.is_loop: self._log.warn(_('not deleting {0}: unhandled device', device)) return False if remove: await self.auto_remove(device, force=True) self._log.debug(_('deleting {0}', device)) await device.delete() self._log.info(_('deleted {0}', device)) return True # iterate devices def is_handleable(self, device): # TODO: handle paths in first argument """ Check whether this device should be handled by udiskie. :param device: device object, block device path or mount path :returns: handleability 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_automount(self, device, default=True): if not self.is_handleable(device): return False return match_config(self._config, device, 'automount', default) def _ignore_device(self, device): return match_config(self._config, device, 'ignore', False) def is_addable(self, device, automount=True): """Check if device can be added with ``auto_add``.""" if not self.is_automount(device, automount): 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): """Get list of all known handleable devices.""" nodes = self.get_device_tree() return [node.device for node in sorted(nodes.values(), key=DevNode._sort_key) if not node.ignored and node.device] def get_all_handleable_roots(self): """ Get list of all handleable devices, return only those that represent root nodes within the filtered device tree. """ nodes = self.get_device_tree() return [node.device for node in sorted(nodes.values(), key=DevNode._sort_key) if not node.ignored and node.device and (node.root == '/' or nodes[node.root].ignored)] def get_all_handleable_leaves(self): """ Get list of all handleable devices, return only those that represent leaf nodes within the filtered device tree. """ nodes = self.get_device_tree() return [node.device for node in sorted(nodes.values(), key=DevNode._sort_key) if not node.ignored and node.device and all(child.ignored for child in node.children)] def get_device_tree(self): """Get a tree of all devices.""" root = DevNode(None, None, [], None) device_nodes = { dev.object_path: DevNode(dev, dev.parent_object_path, [], self._ignore_device(dev)) for dev in self.udisks } for node in device_nodes.values(): device_nodes.get(node.root, root).children.append(node) device_nodes['/'] = root for node in device_nodes.values(): node.children.sort(key=DevNode._sort_key) # use parent as fallback, update top->down: def propagate_ignored(node): for child in node.children: if child.ignored is None: child.ignored = node.ignored propagate_ignored(child) propagate_ignored(root) return device_nodes class DevNode: def __init__(self, device, root, children, ignored): self.device = device self.root = root self.children = children self.ignored = ignored def _sort_key(self): return self.device.device_presentation if self.device else '' # data structs containing the menu hierarchy: Device = namedtuple('Device', ['root', 'branches', 'device', 'label', 'methods']) Action = namedtuple('Action', ['method', 'device', 'label', 'action']) class DeviceActions: _labels = { 'browse': _('Browse {0}'), 'terminal': _('Hack on {0}'), 'mount': _('Mount {0}'), 'unmount': _('Unmount {0}'), 'unlock': _('Unlock {0}'), 'lock': _('Lock {0}'), 'eject': _('Eject {1}'), 'detach': _('Unpower {1}'), 'forget_password': _('Clear password for {0}'), 'delete': _('Detach {0}'), } def __init__(self, mounter, actions={}): self._mounter = mounter self._actions = _actions = actions.copy() setdefault(_actions, { 'browse': mounter.browse, 'terminal': mounter.terminal, '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': to_coro(mounter.forget_password), 'delete': mounter.delete, }) def detect(self, root_device='/'): """ Detect all currently known devices. :param str root_device: object path of root device to return :returns: root node of device hierarchy """ 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 node in device_nodes.values(): device_nodes.get(node.root, root).branches.append(node) device_nodes['/'] = root for node in device_nodes.values(): node.branches.sort(key=lambda node: node.label) 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: if self._mounter._browser: yield 'browse' if self._mounter._terminal: yield 'terminal' 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' if device.is_loop: yield 'delete' def _device_node(self, device): """Create an empty menu node for the specified device.""" label = device.ui_label dev_label = device.ui_device_label # determine available methods methods = [Action(method, device, self._labels[method].format(label, dev_label), partial(self._actions[method], device)) for method in self._get_device_methods(device)] # find the root device: root = device.parent_object_path # in this first step leave branches empty return device.object_path, Device(root, [], device, dev_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-2.1.0/udiskie/__init__.py0000664000372000037200000000002613615574740017571 0ustar travistravis00000000000000__version__ = '2.1.0' udiskie-2.1.0/udiskie/appindicator.py0000664000372000037200000000371313615574740020515 0ustar travistravis00000000000000""" Status icon using AppIndicator3. """ from gi.repository import Gtk from gi.repository import AppIndicator3 from .async_ import Future class AppIndicatorIcon: """ Show status icon using AppIndicator as backend. Replaces `udiskie.tray.StatusIcon` on ubuntu/unity. """ def __init__(self, menumaker, _icons): self._maker = menumaker self._menu = Gtk.Menu() self._indicator = AppIndicator3.Indicator.new( 'udiskie', _icons.get_icon_name('media'), AppIndicator3.IndicatorCategory.HARDWARE) self._indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE) self._indicator.set_menu(self._menu) # Get notified before menu is shown, see: # https://bugs.launchpad.net/screenlets/+bug/522152/comments/15 dbusmenuserver = self._indicator.get_property('dbus-menu-server') self._dbusmenuitem = dbusmenuserver.get_property('root-node') self._conn = self._dbusmenuitem.connect('about-to-show', self._on_show) self.task = Future() menumaker._quit_action = self.destroy # Populate menu initially, so libdbusmenu does not ignore the # 'about-to-show': self._maker(self._menu) def destroy(self): self.show(False) self._dbusmenuitem.disconnect(self._conn) self.task.set_result(True) @property def visible(self): status = self._indicator.get_status() return status == AppIndicator3.IndicatorStatus.ACTIVE def show(self, show=True): if show == self.visible: return status = (AppIndicator3.IndicatorStatus.ACTIVE if show else AppIndicator3.IndicatorStatus.PASSIVE) self._indicator.set_status(status) def _on_show(self, menu): # clear menu: for item in self._menu.get_children(): self._menu.remove(item) # repopulate: self._maker(self._menu) self._menu.show_all() udiskie-2.1.0/udiskie/password_dialog.ui0000664000372000037200000000536113615574740021207 0ustar travistravis00000000000000 5 center dialog 6 6 True 0 True False True True Show password False True Remember password False True gtk-cancel True True gtk-ok True True True True Open keyfile… False cancel_button ok_button udiskie-2.1.0/CONTRIBUTORS0000664000372000037200000000115113615574740015703 0ustar travistravis00000000000000Byron 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 Janosch Gräf udiskie-2.1.0/setup.cfg0000664000372000037200000000302513615574740015646 0ustar travistravis00000000000000[metadata] name = udiskie version = attr: udiskie.__version__ description = Removable disk automounter for udisks url = https://github.com/coldfix/udiskie long_description = file: README.rst, CHANGES.rst author = Byron Clark author_email = byron@theclarkfamily.name maintainer = Thomas Gläßle maintainer_email = t_glaessle@gmx.de license = MIT license_file = COPYING 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 :: 3.5 Programming Language :: Python :: 3.6 License :: OSI Approved :: MIT License Topic :: Desktop Environment Topic :: Software Development Topic :: System :: Filesystems Topic :: System :: Hardware Topic :: Utilities long_description_content_type = text/x-rst [options] packages = udiskie zip_safe = true include_package_data = true python_requires = >=3.5 install_requires = PyYAML docopt importlib_resources;python_version<'3.7' PyGObject [options.extras_require] password-cache = keyutils==0.3 config = xdg [options.entry_points] console_scripts = udiskie = udiskie.cli:Daemon.main udiskie-mount = udiskie.cli:Mount.main udiskie-umount = udiskie.cli:Umount.main udiskie-info = udiskie.cli:Info.main [flake8] ignore = E126,E221,E226,E241,E731,E741,W503,W504 max-line-length = 84 max-complexity = 12 exclude = docs,.git,build,__pycache__,dist,hit_models [egg_info] tag_build = tag_date = 0 udiskie-2.1.0/udiskie.egg-info/0000775000372000037200000000000013615574740017154 5ustar travistravis00000000000000udiskie-2.1.0/udiskie.egg-info/PKG-INFO0000664000372000037200000005010213615574740020247 0ustar travistravis00000000000000Metadata-Version: 2.1 Name: udiskie Version: 2.1.0 Summary: Removable disk automounter for udisks Home-page: 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 Description: ======= udiskie ======= |Version| |License| *udiskie* is a udisks2_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. |Screenshot| Its features include: - automount removable media - notifications - tray icon - command line tools for manual un-/mounting - LUKS encrypted devices - unlocking with keyfiles (requires udisks 2.6.4) - loop devices (mounting iso archives) - password caching (requires python keyutils 0.3) All features can be individually enabled or disabled. **NOTE:** support for python2 and udisks1 have been removed. If you need a version of udiskie that supports python2, please check out the ``1.7.X`` releases or the ``maint-1.7`` branch. .. _udisks2: http://www.freedesktop.org/wiki/Software/udisks - `Documentation`_ - Usage_ - Installation_ - `Debug Info`_ - Troubleshooting_ - FAQ_ - `Man page`_ - `Source Code`_ - `Latest Release`_ - `Issue Tracker`_ .. _Documentation: https://github.com/coldfix/udiskie/wiki .. _Usage: https://github.com/coldfix/udiskie/wiki/Usage .. _Installation: https://github.com/coldfix/udiskie/wiki/Installation .. _Debug Info: https://github.com/coldfix/udiskie/wiki/Debug-Info .. _Troubleshooting: https://github.com/coldfix/udiskie/wiki/Troubleshooting .. _FAQ: https://github.com/coldfix/udiskie/wiki/FAQ .. _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 .. _Roadmap: https://github.com/coldfix/udiskie/blob/master/HACKING.rst#roadmap .. Badges: .. |Version| image:: https://img.shields.io/pypi/v/udiskie.svg :target: https://pypi.python.org/pypi/udiskie :alt: Version .. |License| image:: https://img.shields.io/pypi/l/udiskie.svg :target: https://github.com/coldfix/udiskie/blob/master/COPYING :alt: License: MIT .. |Screenshot| image:: https://raw.githubusercontent.com/coldfix/udiskie/master/screenshot.png :target: https://raw.githubusercontent.com/coldfix/udiskie/master/screenshot.png :alt: Screenshot CHANGELOG --------- 2.1.0 ~~~~~ Date: 02.02.2020 - fix some typos (thanks @torstehu, #197) - change how device rules are evaluated: lookup undecided rules on parent device (fixes issue with filters not applying to subdevices of a matched device, see #198) - change builtin rules to not show loop devices with ``HintIgnore``, see #181 - change how is_external attribute is compute: use the value from udisks directly (fixes issue with is_external property not behaving as expected, see #185) - add 'skip' keyword for rules to skip evaluation of further rules on this device, and continue directly on the parent 2.0.4 ~~~~~ Date: 21.01.2020 - fix user commands that output non-utf8 data 2.0.3 ~~~~~ Date: 20.01.2020 - fix exception when using non-device parameters with DeviceCommand (e.g. in --notify-command) 2.0.2 ~~~~~ Date: 30.12.2019 - hotfix for automounting being broken since 2.0.0 2.0.1 ~~~~~ Date: 28.12.2019 - use ``importlib.resources`` directly on py3.7 and above, rather than requiring ``importlib_resources`` as additional dependency 2.0.0 ~~~~~ Date: 26.12.2019 - require python >= 3.5 - drop python2 support - drop udisks1 support - drop command line options corresponding to udisks version selection (-1, -2) - use py35's ``async def`` functions -- improving stack traces upon exception - internal refactoring and simplifications - add "show password" checkbox in password dialog 1.7.7 ~~~~~ Date: 17.02.2019 - keep password dialog always on top - fix stdin-based password prompts 1.7.6 ~~~~~ Date: 17.02.2019 - add russian translations (thanks @mr-GreyWolf) - fixed deprecation warnings in setup.py (thanks @sealj553) 1.7.5 ~~~~~ Date: 24.05.2018 - fix "NameError: 'Async' is not defined" when starting without tray icon 1.7.4 ~~~~~ Date: 17.05.2018 - fix attribute error when using options in udiskie-mount (#159) - fix tray in appindicator mode (#156) - possibly fix non-deterministic bugs (due to garbage collection) by keeping global reference to all active asyncs 1.7.3 ~~~~~ Date: 13.12.2017 - temporary workaround for udisks2.7 requiring ``filesystem-mount-system`` when trying to mount a LUKS cleartext device diretcly after unlocking 1.7.2 ~~~~~ Date: 18.10.2017 - officially deprecate udisks1 - officially deprecate python2 (want python >= 3.5) - fix startup crash on py2 - fix exception when inserting LUKS device if ``--password-prompt`` or udisks1 is used - fix minor problem with zsh autocompletion 1.7.1 ~~~~~ Date: 02.10.2017 - add an "open keyfile" button to the password dialog - add warning if mounting device without ntfs-3g (#143) - fix problem with LVM devices 1.7.0 ~~~~~ Date: 26.03.2017 - add joined ``device_config`` list in the config file - deprecate ``mount_options`` and ``ignore_device`` in favor of ``device_config`` - can configure ``automount`` per device using the new ``device_config`` [#107] - can configure keyfiles (requires udisks 2.6.4) [#66] - remove mailing list 1.6.2 ~~~~~ Date: 06.03.2017 - Show losetup/quit actions only in ex-menu - Show note in menu if no devices are found 1.6.1 ~~~~~ Date: 24.02.2017 - add format strings for the undocumented ``udiskie-info`` utility - speed up autocompletion times, for ``udiskie-mount`` by about a factor three, for ``udiskie-umount`` by about a factor 10 1.6.0 ~~~~~ Date: 22.02.2017 - fix crash on startup if config file is empty - add ``--notify-command`` to notify external programs (@jgraef) [#127] - can enable/disable automounting via special right-click menu [#98] - do not explicitly specify filesystem when mounting [#131] 1.5.1 ~~~~~ Date: 03.06.2016 - fix unicode issue that occurs on python2 when stdout is redirected (in particular for zsh autocompletion) 1.5.0 ~~~~~ Date: 03.06.2016 - make systray menu flat (use ``udiskie --tray --menu smart`` to request the old menu) [#119] - extend support for loop devices (requires UDisks2) [#101] - support ubuntu/unity AppIndicator backend for status icon [#59] - add basic utility to obtain info on block devices [#122] - add zsh completions [#26] - improve UI menu labels for devices - fix error when force-ejecting device [#121] - respect configured ignore-rules in ``udiskie-umount`` - fix error message for empty task lists [#123] 1.4.12 ~~~~~~ Date: 15.05.2016 - log INFO events to STDOUT (#112) - fix exception in notifications when action is not available. This concerns the retry button in the ``job_failed`` notification, as well as the browse action in the ``device_mounted`` notification (#117) - don't show 'browse' action in tray menu if unavailable 1.4.11 ~~~~~~ Date: 13.05.2016 - protect password dialog against garbage collection (which makes the invoking coroutine hang up and not unlock the device) - fix add_all/remove_all operations: only consider leaf/root devices within the handleable devices hierarchy: - avoid considering the same device twice (#114) - makes sure every handleable device is considered at all in remove_all 1.4.10 ~~~~~~ Date: 11.05.2016 - signal failing mount/unmount operations with non-zero exit codes (#110) - suppress notifications for unhandled devices - add rules for docker devices marking them unhandled to avoid excessive notifications (#113) - allow mounting/unmounting using UUID (#90) - prevent warning when starting without X session (#102) - can now match against wildcards in config rules (#49) 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 :: 3.5 Classifier: Programming Language :: Python :: 3.6 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 Requires-Python: >=3.5 Description-Content-Type: text/x-rst Provides-Extra: password_cache Provides-Extra: config udiskie-2.1.0/udiskie.egg-info/zip-safe0000664000372000037200000000000113615574740020604 0ustar travistravis00000000000000 udiskie-2.1.0/udiskie.egg-info/dependency_links.txt0000664000372000037200000000000113615574740023222 0ustar travistravis00000000000000 udiskie-2.1.0/udiskie.egg-info/top_level.txt0000664000372000037200000000001013615574740021675 0ustar travistravis00000000000000udiskie udiskie-2.1.0/udiskie.egg-info/SOURCES.txt0000664000372000037200000000211713615574740021041 0ustar travistravis00000000000000CHANGES.rst CONTRIBUTORS COPYING MANIFEST.in README.rst setup.cfg setup.py completions/zsh/_udiskie completions/zsh/_udiskie-mount completions/zsh/_udiskie-umount 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/ru_RU.po lang/sk_SK.po lang/udiskie.pot test/test_cache.py test/test_match.py udiskie/__init__.py udiskie/appindicator.py udiskie/async_.py udiskie/automount.py udiskie/cache.py udiskie/cli.py udiskie/common.py udiskie/config.py udiskie/dbus.py udiskie/depend.py udiskie/locale.py udiskie/mount.py udiskie/notify.py udiskie/password_dialog.ui udiskie/prompt.py udiskie/tray.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.txt udiskie.egg-info/zip-safeudiskie-2.1.0/udiskie.egg-info/requires.txt0000664000372000037200000000016513615574740021556 0ustar travistravis00000000000000PyYAML docopt PyGObject [:python_version < "3.7"] importlib_resources [config] xdg [password_cache] keyutils==0.3 udiskie-2.1.0/udiskie.egg-info/entry_points.txt0000664000372000037200000000025213615574740022451 0ustar travistravis00000000000000[console_scripts] udiskie = udiskie.cli:Daemon.main udiskie-info = udiskie.cli:Info.main udiskie-mount = udiskie.cli:Mount.main udiskie-umount = udiskie.cli:Umount.main udiskie-2.1.0/icons/0000775000372000037200000000000013615574740015140 5ustar travistravis00000000000000udiskie-2.1.0/icons/scalable/0000775000372000037200000000000013615574740016706 5ustar travistravis00000000000000udiskie-2.1.0/icons/scalable/actions/0000775000372000037200000000000013615574740020346 5ustar travistravis00000000000000udiskie-2.1.0/icons/scalable/actions/udiskie-unlock.svg0000664000372000037200000002626513615574740024030 0ustar travistravis00000000000000 image/svg+xml udiskie-2.1.0/icons/scalable/actions/udiskie-mount.svg0000664000372000037200000007163213615574740023675 0ustar travistravis00000000000000 image/svg+xml udiskie-2.1.0/icons/scalable/actions/udiskie-lock.svg0000664000372000037200000002625413615574740023463 0ustar travistravis00000000000000 image/svg+xml udiskie-2.1.0/icons/scalable/actions/udiskie-detach.svg0000664000372000037200000001325713615574740023762 0ustar travistravis00000000000000 image/svg+xml udiskie-2.1.0/icons/scalable/actions/udiskie-unmount.svg0000664000372000037200000010147313615574740024235 0ustar travistravis00000000000000 image/svg+xml udiskie-2.1.0/icons/scalable/actions/udiskie-eject.svg0000664000372000037200000002244513615574740023623 0ustar travistravis00000000000000 image/svg+xml udiskie-2.1.0/completions/0000775000372000037200000000000013615574740016361 5ustar travistravis00000000000000udiskie-2.1.0/completions/zsh/0000775000372000037200000000000013615574740017165 5ustar travistravis00000000000000udiskie-2.1.0/completions/zsh/_udiskie-mount0000664000372000037200000000250513615574740022046 0ustar travistravis00000000000000#compdef udiskie-mount # vim: ft=zsh sts=2 sw=2 ts=2 function _udiskie-mount { local context curcontext="$curcontext" line state ret=1 local args tmp typeset -A opt_args args=( '(- *)'{-h,--help}"[show help]" '(- *)'{-V,--version}"[show version]" '(-q)'{-v,--verbose}"[more output]" '(-v)'{-q,--quiet}"[less output]" '(-C)'{-c,--config}"[set config file]:file:_files" '(-c)'{-C,--no-config}"[don't use config file]" '(*)'{-a,--all}"[unmount all devices]" '(-R)'{-r,--recursive}"[recursively add devices]" '(-r)'{-R,--no-recursive}"[disable recursive mounting]" {-o,--options}"[set filesystem options]:file system option" '(-P)'{-p,--password-prompt}"[Command for password retrieval]:passwordialog:->pprompt" '(-p)'{-P,--no-password-prompt}"[Disable unlocking]" '*:dev or dir:->udevordir' ) _arguments -C -s "$args[@]" && ret=0 case "$state" in pprompt) _alternative \ 'builtins:builtin prompt:(builtin:tty builtin:gui)' \ 'commands:command name:_path_commands' \ && ret=0 ;; udevordir) local dev_tmp mp_tmp dev_tmp=( $(udiskie-info -a) ) _alternative \ 'device-paths: device path:_canonical_paths -A dev_tmp -N device-paths device\ path' \ && ret=0 ;; esac return ret } _udiskie-mount "$@" udiskie-2.1.0/completions/zsh/_udiskie-umount0000664000372000037200000000370313615574740022234 0ustar travistravis00000000000000#compdef udiskie-umount # vim: ft=zsh sts=2 sw=2 ts=2 function _udiskie-umount { local context curcontext="$curcontext" line state ret=1 typeset -A opt_args args=( '(- *)'{-h,--help}"[show help]" '(- *)'{-V,--version}"[show version]" '(-q)'{-v,--verbose}"[more output]" '(-v)'{-q,--quiet}"[less output]" '(-C)'{-c,--config}"[set config file]:file:_files" '(-c)'{-C,--no-config}"[don't use config file]" '(*)'{-a,--all}"[unmount all devices]" '(-D)'{-d,--detach}"[detach device]" '(-d)'{-D,--no-detach}"[don't detach device]" '(-E)'{-e,--eject}"[eject device]" '(-e)'{-E,--no-eject}"[don't eject device]" '(-F)'{-f,--force}"[recursive unmounting]" '(-f)'{-F,--no-force}"[no recursive unmountinng]" '(-L)'{-l,--lock}"[lock device after unmounting]" '(-l)'{-L,--no-lock}"[don't lock device]" '*:dev or dir:->udevordir' ) _arguments -C -s "$args[@]" && ret=0 case "$state" in udevordir) local dev_tmp mp_tmp loop_tmp dev_detail # "${(@f)X}" means to use lines as separators dev_detail=( "${(@f)$(udiskie-info -a -o '{device_presentation}<:1:>{mount_path}<:2:>{is_filesystem}<:3:>{is_mounted}<:4:>{is_loop}<:5:>{loop_file}')}" ) # select: 'device_presentation' dev_tmp=( ${dev_detail%%<:1:>*} ) # filter: 'is_filesystem' and 'is_mounted' mp_tmp=( ${(M)dev_detail:#*<:2:>True<:3:>True<:4:>*} ) # select: 'mount_path' mp_tmp=( ${mp_tmp##*<:1:>} ) mp_tmp=( ${mp_tmp%%<:2:>*} ) # filter: 'is_loop' loop_tmp=( ${(M)dev_detail:#*<:3:>True<:5:>*} ) # select: 'mount_path' loop_tmp=( ${loop_tmp##*<:5:>} ) _alternative \ 'directories:mount point:_canonical_paths -A mp_tmp -N directories mount\ point' \ 'device-paths: device path:_canonical_paths -A dev_tmp -N device-paths device\ path' \ 'loop-files: loop file:_canonical_paths -A loop_tmp -N loop-files loop\ file' \ && ret=0 ;; esac return ret } _udiskie-umount "$@" udiskie-2.1.0/completions/zsh/_udiskie0000664000372000037200000000362513615574740020712 0ustar travistravis00000000000000#compdef udiskie # vim: ft=zsh sts=2 sw=2 ts=2 function _udiskie { local context curcontext="$curcontext" line state ret=1 local args tmp typeset -A opt_args args=( '(- *)'{-h,--help}"[show help]" '(- *)'{-V,--version}"[show version]" '(-q)'{-v,--verbose}"[more output]" '(-v)'{-q,--quiet}"[less output]" '(-C)'{-c,--config}"[set config file]:file:_files" '(-c)'{-C,--no-config}"[don't use config file]" '(-A)'{-a,--automount}"[automount new devices]" '(-a)'{-A,--no-automount}"[disable automounting]" '(-N)'{-n,--notify}"[show popup notifications]" '(-n)'{-N,--no-notify}"[disable notifications]" '(--no-appindicator)'--appindicator"[use appindicator for status icon]" '(--appindicator)'--no-appindicator"[don't use appindicator]" '(-T -s)'{-t,--tray}"[show tray icon]" '(-T -t)'{-s,--smart-tray}"[auto hide tray icon]" '(-t -s)'{-T,--no-tray}"[disable tray icon]" {-m,--menu}"[set behaviour for tray menu]:traymenu:(flat nested)" '(--no-password-cache)'--password-cache"[set timeout for passwords of encrypted devices to N minutes]:minutes" '(--password-cache)'--no-password-cache"[don't cache passwords for encrypted devices]" '(-P)'{-p,--password-prompt}"[Command for password retrieval]:passwordialog:->pprompt" '(-p)'{-P,--no-password-prompt}"[Disable unlocking]" '(-F)'{-f,--file-manager}"[set program for browsing directories]:filemanager:_path_commands" '(-f)'{-F,--no-file-manager}"[disable browsing]" '(--no-notify-command)'--notify-command"[execute this command on events]:minutes" '(--notify-command)'--no-notify-command"[don't execute event handler]" ) _arguments -C -s "$args[@]" && ret=0 case $state in pprompt) _alternative \ 'builtins:builtin prompt:(builtin:tty builtin:gui)' \ 'commands:command name:_path_commands' \ && ret=0 ;; esac return ret } _udiskie "$@" udiskie-2.1.0/test/0000775000372000037200000000000013615574740015004 5ustar travistravis00000000000000udiskie-2.1.0/test/test_cache.py0000664000372000037200000000420013615574740017454 0ustar travistravis00000000000000""" Tests for the udiskie.cache module. """ import unittest import time from udiskie.cache import PasswordCache class TestDev: 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.encode('utf-8')) 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.encode('utf-8')) time.sleep(2) self.assertEqual(cache[device], password.encode('utf-8')) 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.encode('utf-8')) 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.encode('utf-8')) cache[device] = password * 2 self.assertEqual(cache[device], password.encode('utf-8')*2) del cache[device] with self.assertRaises(KeyError): cache[device] if __name__ == '__main__': unittest.main() udiskie-2.1.0/test/test_match.py0000664000372000037200000000517013615574740017514 0ustar travistravis00000000000000""" 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, match_config class TestDev: def __init__(self, object_path, id_type, id_uuid): self.object_path = object_path self.id_type = id_type self.id_uuid = id_uuid self.partition_slave = None self.luks_cleartext_slave = None 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 ''') self.filters = Config.from_file(self.config_file).device_config def mount_options(self, device): return match_config(self.filters, device, 'options', None) def ignore_device(self, device): return match_config(self.filters, device, 'ignore', False) 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'))) if __name__ == '__main__': unittest.main() udiskie-2.1.0/README.rst0000664000372000037200000000440613615574740015520 0ustar travistravis00000000000000======= udiskie ======= |Version| |License| *udiskie* is a udisks2_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. |Screenshot| Its features include: - automount removable media - notifications - tray icon - command line tools for manual un-/mounting - LUKS encrypted devices - unlocking with keyfiles (requires udisks 2.6.4) - loop devices (mounting iso archives) - password caching (requires python keyutils 0.3) All features can be individually enabled or disabled. **NOTE:** support for python2 and udisks1 have been removed. If you need a version of udiskie that supports python2, please check out the ``1.7.X`` releases or the ``maint-1.7`` branch. .. _udisks2: http://www.freedesktop.org/wiki/Software/udisks - `Documentation`_ - Usage_ - Installation_ - `Debug Info`_ - Troubleshooting_ - FAQ_ - `Man page`_ - `Source Code`_ - `Latest Release`_ - `Issue Tracker`_ .. _Documentation: https://github.com/coldfix/udiskie/wiki .. _Usage: https://github.com/coldfix/udiskie/wiki/Usage .. _Installation: https://github.com/coldfix/udiskie/wiki/Installation .. _Debug Info: https://github.com/coldfix/udiskie/wiki/Debug-Info .. _Troubleshooting: https://github.com/coldfix/udiskie/wiki/Troubleshooting .. _FAQ: https://github.com/coldfix/udiskie/wiki/FAQ .. _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 .. _Roadmap: https://github.com/coldfix/udiskie/blob/master/HACKING.rst#roadmap .. Badges: .. |Version| image:: https://img.shields.io/pypi/v/udiskie.svg :target: https://pypi.python.org/pypi/udiskie :alt: Version .. |License| image:: https://img.shields.io/pypi/l/udiskie.svg :target: https://github.com/coldfix/udiskie/blob/master/COPYING :alt: License: MIT .. |Screenshot| image:: https://raw.githubusercontent.com/coldfix/udiskie/master/screenshot.png :target: https://raw.githubusercontent.com/coldfix/udiskie/master/screenshot.png :alt: Screenshot udiskie-2.1.0/setup.py0000664000372000037200000000777713615574740015561 0ustar travistravis00000000000000from setuptools import setup, Command from setuptools.command.easy_install import ScriptWriter from setuptools.command.install import install as orig_install from distutils.command.build import build as orig_build from textwrap import dedent from subprocess import call import logging from os import path from glob import glob comp_files = glob('completions/zsh/_*') icon_files = glob('icons/scalable/actions/udiskie-*.svg') languages = [path.splitext(path.split(po_file)[1])[0] for po_file in glob('lang/*.po')] 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 lang in languages: po_file = 'lang/{}.po'.format(lang) mo_file = 'build/locale/{}/LC_MESSAGES/udiskie.mo'.format(lang) self.mkpath(path.dirname(mo_file)) self.make_file( po_file, mo_file, self.make_mo, [po_file, mo_file]) 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.warning(e) # 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. # # In fact this is desirable because distutils (correctly) installs data files # to `sys.prefix` whereas setuptools by default installs to the egg folder # (which is pretty much useless) and doesn't invoke build commands before # install. The only real drawback with the distutils behaviour is that it does # not automatically install dependencies, but we can easily live with that. # # Note further that we need to subclass the *setuptools* install command # rather than the *distutils* one to prevent errors when installing with pip # from the source distribution. class install(orig_install): """Custom install command used to update the gtk icon cache.""" def run(self): """Perform distutils-style install, then update GTK icon cache.""" orig_install.run(self) try: call(['gtk-update-icon-cache', 'share/icons/hicolor']) except OSError as e: # ignore failures since the tray icon is an optional component: logging.warning(e) def fast_entrypoint_script_template(): """ Replacement for ``easy_install.ScriptWriter.template`` to generate faster entry points that don't depend on and import pkg_resources. NOTE: `pip install` already does the right thing (at least for pip 19.0) without our help, but this is still needed for setuptools install, i.e. ``python setup.py install`` or develop. """ SCRIPT_TEMPLATE = dedent(r''' # encoding: utf-8 import sys from {ep.module_name} import {ep.attrs[0]} if __name__ == '__main__': sys.exit({func}()) ''').lstrip() class ScriptTemplate(str): def __mod__(self, context): func = '.'.join(context['ep'].attrs) return self.format(func=func, **context) return ScriptTemplate(SCRIPT_TEMPLATE) ScriptWriter.template = fast_entrypoint_script_template() setup( cmdclass={ 'install': install, 'build': build, 'build_mo': build_mo, }, data_files=[ ('share/icons/hicolor/scalable/actions', icon_files), ('share/zsh/site-functions', comp_files), *[('share/locale/{}/LC_MESSAGES'.format(lang), ['build/locale/{}/LC_MESSAGES/udiskie.mo'.format(lang)]) for lang in languages], ], ) udiskie-2.1.0/lang/0000775000372000037200000000000013615574740014746 5ustar travistravis00000000000000udiskie-2.1.0/lang/udiskie.pot0000664000372000037200000002705413615574740017137 0ustar travistravis00000000000000# 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: 2019-12-26 13:57+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=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: ../udiskie/cli.py:46 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "" #: ../udiskie/cli.py:119 msgid "" "\n" " Note, that the options in the individual groups are mutually exclusive.\n" "\n" " The config file can be a JSON or preferably a YAML file. For an\n" " example, see the MAN page (or doc/udiskie.8.txt in the repository).\n" " " msgstr "" #: ../udiskie/cli.py:139 #, python-format msgid "%(message)s" msgstr "" #: ../udiskie/cli.py:141 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "" #: ../udiskie/cli.py:370 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:380 msgid "" "Not run within X session. \n" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/cli.py:387 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" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/depend.py:50 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:56 msgid "X server not connected!" msgstr "" #: ../udiskie/prompt.py:92 msgid "Show password" msgstr "" #: ../udiskie/prompt.py:97 msgid "Open keyfile…" msgstr "" #: ../udiskie/prompt.py:104 msgid "Remember password" msgstr "" #: ../udiskie/prompt.py:119 msgid "Open a keyfile to unlock the LUKS device" msgstr "" #: ../udiskie/prompt.py:153 ../udiskie/prompt.py:163 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "" #: ../udiskie/prompt.py:197 msgid "Unknown device attribute {!r} in format string: {!r}" msgstr "" #: ../udiskie/prompt.py:245 msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "f' option." msgstr "" #: ../udiskie/mount.py:29 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "" #: ../udiskie/mount.py:118 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "" #: ../udiskie/mount.py:121 #, python-brace-format msgid "not browsing {0}: no program" msgstr "" #: ../udiskie/mount.py:123 ../udiskie/mount.py:143 #, python-brace-format msgid "opening {0} on {0.mount_paths[0]}" msgstr "" #: ../udiskie/mount.py:125 ../udiskie/mount.py:145 #, python-brace-format msgid "opened {0} on {0.mount_paths[0]}" msgstr "" #: ../udiskie/mount.py:138 #, python-brace-format msgid "not opening terminal {0}: not mounted" msgstr "" #: ../udiskie/mount.py:141 #, python-brace-format msgid "not opening terminal {0}: no program" msgstr "" #: ../udiskie/mount.py:159 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:162 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "" #: ../udiskie/mount.py:166 #, python-brace-format msgid "mounting {0} with {1}" msgstr "" #: ../udiskie/mount.py:169 #, python-brace-format msgid "mounted {0} on {1}" msgstr "" #: ../udiskie/mount.py:175 msgid "" "Mounting NTFS device with default driver.\n" "Please install 'ntfs-3g' if you experience problems or the device is " "readonly." msgstr "" #: ../udiskie/mount.py:189 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:192 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "" #: ../udiskie/mount.py:194 #, python-brace-format msgid "unmounting {0}" msgstr "" #: ../udiskie/mount.py:196 #, python-brace-format msgid "unmounted {0}" msgstr "" #: ../udiskie/mount.py:210 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:213 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "" #: ../udiskie/mount.py:216 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "" #: ../udiskie/mount.py:230 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "" #: ../udiskie/mount.py:235 #, python-brace-format msgid "unlocking {0} using keyfile" msgstr "" #: ../udiskie/mount.py:238 #, python-brace-format msgid "unlocking {0}" msgstr "" #: ../udiskie/mount.py:241 #, python-brace-format msgid "unlocked {0}" msgstr "" #: ../udiskie/mount.py:252 #, python-brace-format msgid "unlocking {0} using cached password" msgstr "" #: ../udiskie/mount.py:256 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:259 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:267 msgid "No matching keyfile rule for {}." msgstr "" #: ../udiskie/mount.py:273 #, python-brace-format msgid "keyfile for {0} not found: {1}" msgstr "" #: ../udiskie/mount.py:275 #, python-brace-format msgid "unlocking {0} using keyfile {1}" msgstr "" #: ../udiskie/mount.py:279 #, python-brace-format msgid "failed to unlock {0} using keyfile" msgstr "" #: ../udiskie/mount.py:282 #, python-brace-format msgid "unlocked {0} using keyfile" msgstr "" #: ../udiskie/mount.py:308 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:311 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "" #: ../udiskie/mount.py:313 #, python-brace-format msgid "locking {0}" msgstr "" #: ../udiskie/mount.py:315 #, python-brace-format msgid "locked {0}" msgstr "" #: ../udiskie/mount.py:352 ../udiskie/mount.py:395 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:431 ../udiskie/mount.py:481 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:506 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:510 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "" #: ../udiskie/mount.py:516 #, python-brace-format msgid "ejecting {0}" msgstr "" #: ../udiskie/mount.py:518 #, python-brace-format msgid "ejected {0}" msgstr "" #: ../udiskie/mount.py:532 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:536 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "" #: ../udiskie/mount.py:540 #, python-brace-format msgid "detaching {0}" msgstr "" #: ../udiskie/mount.py:542 #, python-brace-format msgid "detached {0}" msgstr "" #: ../udiskie/mount.py:593 #, python-brace-format msgid "not setting up {0}: already up" msgstr "" #: ../udiskie/mount.py:596 #, python-brace-format msgid "not setting up {0}: not a file" msgstr "" #: ../udiskie/mount.py:598 #, python-brace-format msgid "setting up {0}" msgstr "" #: ../udiskie/mount.py:606 #, python-brace-format msgid "set up {0} as {1}" msgstr "" #: ../udiskie/mount.py:621 #, python-brace-format msgid "not deleting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:625 #, python-brace-format msgid "deleting {0}" msgstr "" #: ../udiskie/mount.py:627 #, python-brace-format msgid "deleted {0}" msgstr "" #: ../udiskie/mount.py:757 #, python-brace-format msgid "Browse {0}" msgstr "" #: ../udiskie/mount.py:758 #, python-brace-format msgid "Hack on {0}" msgstr "" #: ../udiskie/mount.py:759 #, python-brace-format msgid "Mount {0}" msgstr "" #: ../udiskie/mount.py:760 #, python-brace-format msgid "Unmount {0}" msgstr "" #: ../udiskie/mount.py:761 #, python-brace-format msgid "Unlock {0}" msgstr "" #: ../udiskie/mount.py:762 #, python-brace-format msgid "Lock {0}" msgstr "" #: ../udiskie/mount.py:763 #, python-brace-format msgid "Eject {1}" msgstr "" #: ../udiskie/mount.py:764 #, python-brace-format msgid "Unpower {1}" msgstr "" #: ../udiskie/mount.py:765 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/mount.py:766 #, python-brace-format msgid "Detach {0}" msgstr "" #: ../udiskie/udisks2.py:662 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "" #: ../udiskie/udisks2.py:665 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "" #: ../udiskie/udisks2.py:684 #, python-brace-format msgid "Daemon version: {0}" msgstr "" #: ../udiskie/udisks2.py:689 #, python-brace-format msgid "Keyfile support: {0}" msgstr "" #: ../udiskie/udisks2.py:768 #, python-brace-format msgid "+++ {0}: {1}" msgstr "" #: ../udiskie/tray.py:159 msgid "Mount disc image" msgstr "" #: ../udiskie/tray.py:165 msgid "Enable automounting" msgstr "" #: ../udiskie/tray.py:171 msgid "Enable notifications" msgstr "" #: ../udiskie/tray.py:180 msgid "Quit" msgstr "" #: ../udiskie/tray.py:187 msgid "Open disc image" msgstr "" #: ../udiskie/tray.py:189 msgid "Open" msgstr "" #: ../udiskie/tray.py:190 msgid "Cancel" msgstr "" #: ../udiskie/tray.py:230 msgid "Invalid node!" msgstr "" #: ../udiskie/tray.py:232 msgid "No external devices" msgstr "" #: ../udiskie/tray.py:341 msgid "udiskie" msgstr "" #: ../udiskie/config.py:111 msgid "Unknown matching attribute: {!r}" msgstr "" #: ../udiskie/config.py:113 #, python-brace-format msgid "{0} created" msgstr "" #: ../udiskie/config.py:116 msgid "{0}(match={1!r}, value={2!r})" msgstr "" #: ../udiskie/config.py:136 msgid "{0}(match={1!r}, {2}={3!r}) used for {4}" msgstr "" #: ../udiskie/config.py:212 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:215 msgid "Failed to read {0!r}: {1}" msgstr "" #: ../udiskie/notify.py:62 msgid "Browse directory" msgstr "" #: ../udiskie/notify.py:64 msgid "Open terminal" msgstr "" #: ../udiskie/notify.py:68 msgid "Device mounted" msgstr "" #: ../udiskie/notify.py:69 #, python-brace-format 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:90 msgid "Device locked" msgstr "" #: ../udiskie/notify.py:91 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "" #: ../udiskie/notify.py:100 msgid "Device unlocked" msgstr "" #: ../udiskie/notify.py:101 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "" #: ../udiskie/notify.py:134 msgid "Device added" msgstr "" #: ../udiskie/notify.py:135 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "" #: ../udiskie/notify.py:154 msgid "Device removed" msgstr "" #: ../udiskie/notify.py:155 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "" #: ../udiskie/notify.py:164 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" #: ../udiskie/notify.py:166 #, python-brace-format msgid "failed to {0} device {1}." msgstr "" #: ../udiskie/notify.py:172 msgid "Retry" msgstr "" #: ../udiskie/notify.py:175 msgid "Job failed" msgstr "" #: ../udiskie/notify.py:206 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" udiskie-2.1.0/lang/en_US.po0000664000372000037200000003471113615574740016325 0ustar travistravis00000000000000msgid "" msgstr "" "Project-Id-Version: udiskie\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-12-26 13:57+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/cli.py:46 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "These options are mutually exclusive: {0}" #: ../udiskie/cli.py:119 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:139 #, python-format msgid "%(message)s" msgstr "%(message)s" #: ../udiskie/cli.py:141 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "%(levelname)s [%(asctime)s] %(name)s: %(message)s" #: ../udiskie/cli.py:370 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:380 msgid "" "Not run within X session. \n" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/cli.py:387 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" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/depend.py:50 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:56 msgid "X server not connected!" msgstr "X server not connected!" #: ../udiskie/prompt.py:92 msgid "Show password" msgstr "" #: ../udiskie/prompt.py:97 msgid "Open keyfile…" msgstr "" #: ../udiskie/prompt.py:104 msgid "Remember password" msgstr "" #: ../udiskie/prompt.py:119 msgid "Open a keyfile to unlock the LUKS device" msgstr "" #: ../udiskie/prompt.py:153 ../udiskie/prompt.py:163 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "Enter password for {0.device_presentation}: " #: ../udiskie/prompt.py:197 msgid "Unknown device attribute {!r} in format string: {!r}" msgstr "" #: ../udiskie/prompt.py:245 #, fuzzy msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "f' option." msgstr "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "b' option." #: ../udiskie/mount.py:29 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "failed to {0} {1}: {2}" #: ../udiskie/mount.py:118 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "not browsing {0}: not mounted" #: ../udiskie/mount.py:121 #, python-brace-format msgid "not browsing {0}: no program" msgstr "not browsing {0}: no program" #: ../udiskie/mount.py:123 ../udiskie/mount.py:143 #, python-brace-format msgid "opening {0} on {0.mount_paths[0]}" msgstr "opening {0} on {0.mount_paths[0]}" #: ../udiskie/mount.py:125 ../udiskie/mount.py:145 #, python-brace-format msgid "opened {0} on {0.mount_paths[0]}" msgstr "opened {0} on {0.mount_paths[0]}" #: ../udiskie/mount.py:138 #, fuzzy, python-brace-format msgid "not opening terminal {0}: not mounted" msgstr "not unmounting {0}: not mounted" #: ../udiskie/mount.py:141 #, fuzzy, python-brace-format msgid "not opening terminal {0}: no program" msgstr "not browsing {0}: no program" #: ../udiskie/mount.py:159 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "not mounting {0}: unhandled device" #: ../udiskie/mount.py:162 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "not mounting {0}: already mounted" #: ../udiskie/mount.py:166 #, python-brace-format msgid "mounting {0} with {1}" msgstr "mounting {0} with {1}" #: ../udiskie/mount.py:169 #, python-brace-format msgid "mounted {0} on {1}" msgstr "mounted {0} on {1}" #: ../udiskie/mount.py:175 msgid "" "Mounting NTFS device with default driver.\n" "Please install 'ntfs-3g' if you experience problems or the device is " "readonly." msgstr "" #: ../udiskie/mount.py:189 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "not unmounting {0}: unhandled device" #: ../udiskie/mount.py:192 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "not unmounting {0}: not mounted" #: ../udiskie/mount.py:194 #, python-brace-format msgid "unmounting {0}" msgstr "unmounting {0}" #: ../udiskie/mount.py:196 #, python-brace-format msgid "unmounted {0}" msgstr "unmounted {0}" #: ../udiskie/mount.py:210 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "not unlocking {0}: unhandled device" #: ../udiskie/mount.py:213 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "not unlocking {0}: already unlocked" #: ../udiskie/mount.py:216 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:230 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "not unlocking {0}: cancelled by user" #: ../udiskie/mount.py:235 #, fuzzy, python-brace-format msgid "unlocking {0} using keyfile" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:238 #, python-brace-format msgid "unlocking {0}" msgstr "unlocking {0}" #: ../udiskie/mount.py:241 #, python-brace-format msgid "unlocked {0}" msgstr "unlocked {0}" #: ../udiskie/mount.py:252 #, fuzzy, python-brace-format msgid "unlocking {0} using cached password" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:256 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:259 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:267 msgid "No matching keyfile rule for {}." msgstr "" #: ../udiskie/mount.py:273 #, fuzzy, python-brace-format msgid "keyfile for {0} not found: {1}" msgstr "Device not found: {0}" #: ../udiskie/mount.py:275 #, fuzzy, python-brace-format msgid "unlocking {0} using keyfile {1}" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:279 #, python-brace-format msgid "failed to unlock {0} using keyfile" msgstr "" #: ../udiskie/mount.py:282 #, fuzzy, python-brace-format msgid "unlocked {0} using keyfile" msgstr "unlocked {0}" #: ../udiskie/mount.py:308 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "not locking {0}: unhandled device" #: ../udiskie/mount.py:311 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "not locking {0}: not unlocked" #: ../udiskie/mount.py:313 #, python-brace-format msgid "locking {0}" msgstr "locking {0}" #: ../udiskie/mount.py:315 #, python-brace-format msgid "locked {0}" msgstr "locked {0}" #: ../udiskie/mount.py:352 ../udiskie/mount.py:395 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "not adding {0}: unhandled device" #: ../udiskie/mount.py:431 ../udiskie/mount.py:481 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "not removing {0}: unhandled device" #: ../udiskie/mount.py:506 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "not ejecting {0}: unhandled device" #: ../udiskie/mount.py:510 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "not ejecting {0}: drive not ejectable" #: ../udiskie/mount.py:516 #, python-brace-format msgid "ejecting {0}" msgstr "ejecting {0}" #: ../udiskie/mount.py:518 #, python-brace-format msgid "ejected {0}" msgstr "ejected {0}" #: ../udiskie/mount.py:532 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "not detaching {0}: unhandled device" #: ../udiskie/mount.py:536 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "not detaching {0}: drive not detachable" #: ../udiskie/mount.py:540 #, python-brace-format msgid "detaching {0}" msgstr "detaching {0}" #: ../udiskie/mount.py:542 #, python-brace-format msgid "detached {0}" msgstr "detached {0}" #: ../udiskie/mount.py:593 #, fuzzy, python-brace-format msgid "not setting up {0}: already up" msgstr "not mounting {0}: already mounted" #: ../udiskie/mount.py:596 #, fuzzy, python-brace-format msgid "not setting up {0}: not a file" msgstr "not ejecting {0}: drive not ejectable" #: ../udiskie/mount.py:598 #, fuzzy, python-brace-format msgid "setting up {0}" msgstr "ejecting {0}" #: ../udiskie/mount.py:606 #, python-brace-format msgid "set up {0} as {1}" msgstr "" #: ../udiskie/mount.py:621 #, fuzzy, python-brace-format msgid "not deleting {0}: unhandled device" msgstr "not ejecting {0}: unhandled device" #: ../udiskie/mount.py:625 #, fuzzy, python-brace-format msgid "deleting {0}" msgstr "ejecting {0}" #: ../udiskie/mount.py:627 #, fuzzy, python-brace-format msgid "deleted {0}" msgstr "ejected {0}" #: ../udiskie/mount.py:757 #, python-brace-format msgid "Browse {0}" msgstr "Browse {0}" #: ../udiskie/mount.py:758 #, fuzzy, python-brace-format msgid "Hack on {0}" msgstr "locking {0}" #: ../udiskie/mount.py:759 #, python-brace-format msgid "Mount {0}" msgstr "Mount {0}" #: ../udiskie/mount.py:760 #, python-brace-format msgid "Unmount {0}" msgstr "Unmount {0}" #: ../udiskie/mount.py:761 #, python-brace-format msgid "Unlock {0}" msgstr "Unlock {0}" #: ../udiskie/mount.py:762 #, python-brace-format msgid "Lock {0}" msgstr "Lock {0}" #: ../udiskie/mount.py:763 #, fuzzy, python-brace-format msgid "Eject {1}" msgstr "Eject {0}" #: ../udiskie/mount.py:764 #, fuzzy, python-brace-format msgid "Unpower {1}" msgstr "Unpower {0}" #: ../udiskie/mount.py:765 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/mount.py:766 #, fuzzy, python-brace-format msgid "Detach {0}" msgstr "detached {0}" #: ../udiskie/udisks2.py:662 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "found device owning \"{0}\": \"{1}\"" #: ../udiskie/udisks2.py:665 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "no device found owning \"{0}\"" #: ../udiskie/udisks2.py:684 #, python-brace-format msgid "Daemon version: {0}" msgstr "" #: ../udiskie/udisks2.py:689 #, python-brace-format msgid "Keyfile support: {0}" msgstr "" #: ../udiskie/udisks2.py:768 #, python-brace-format msgid "+++ {0}: {1}" msgstr "+++ {0}: {1}" #: ../udiskie/tray.py:159 msgid "Mount disc image" msgstr "" #: ../udiskie/tray.py:165 msgid "Enable automounting" msgstr "" #: ../udiskie/tray.py:171 msgid "Enable notifications" msgstr "" #: ../udiskie/tray.py:180 msgid "Quit" msgstr "Quit" #: ../udiskie/tray.py:187 msgid "Open disc image" msgstr "" #: ../udiskie/tray.py:189 msgid "Open" msgstr "" #: ../udiskie/tray.py:190 msgid "Cancel" msgstr "" #: ../udiskie/tray.py:230 msgid "Invalid node!" msgstr "Invalid node!" #: ../udiskie/tray.py:232 msgid "No external devices" msgstr "" #: ../udiskie/tray.py:341 msgid "udiskie" msgstr "udiskie" #: ../udiskie/config.py:111 msgid "Unknown matching attribute: {!r}" msgstr "Unknown matching attribute: {!r}" #: ../udiskie/config.py:113 #, python-brace-format msgid "{0} created" msgstr "{0} created" #: ../udiskie/config.py:116 msgid "{0}(match={1!r}, value={2!r})" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:136 #, fuzzy msgid "{0}(match={1!r}, {2}={3!r}) used for {4}" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:212 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:215 #, fuzzy msgid "Failed to read {0!r}: {1}" msgstr "failed to {0} {1}: {2}" #: ../udiskie/notify.py:62 msgid "Browse directory" msgstr "Browse directory" #: ../udiskie/notify.py:64 msgid "Open terminal" msgstr "" #: ../udiskie/notify.py:68 msgid "Device mounted" msgstr "Device mounted" #: ../udiskie/notify.py:69 #, fuzzy, python-brace-format 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:90 msgid "Device locked" msgstr "Device locked" #: ../udiskie/notify.py:91 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "{0.device_presentation} locked" #: ../udiskie/notify.py:100 msgid "Device unlocked" msgstr "Device unlocked" #: ../udiskie/notify.py:101 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "{0.device_presentation} unlocked" #: ../udiskie/notify.py:134 msgid "Device added" msgstr "Device added" #: ../udiskie/notify.py:135 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "device appeared on {0.device_presentation}" #: ../udiskie/notify.py:154 msgid "Device removed" msgstr "Device removed" #: ../udiskie/notify.py:155 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "device disappeared on {0.device_presentation}" #: ../udiskie/notify.py:164 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" "failed to {0} {1}:\n" "{2}" #: ../udiskie/notify.py:166 #, python-brace-format msgid "failed to {0} device {1}." msgstr "failed to {0} device {1}." #: ../udiskie/notify.py:172 msgid "Retry" msgstr "Retry" #: ../udiskie/notify.py:175 msgid "Job failed" msgstr "Job failed" #: ../udiskie/notify.py:206 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" #~ msgid "{0} operation failed for device: {1}" #~ msgstr "{0} operation failed for device: {1}" #, 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]." #~ msgid "UDisks version not supported: {0}!" #~ msgstr "UDisks version not supported: {0}!" #~ msgid "{0} used for {1}" #~ msgstr "{0} used for {1}" #~ msgid "Interface {0!r} not available for {1}" #~ msgstr "Interface {0!r} not available for {1}" udiskie-2.1.0/lang/ru_RU.po0000664000372000037200000004375013615574740016353 0ustar travistravis00000000000000# 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. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-02-04 20:08+0300\n" "PO-Revision-Date: 2019-02-16 21:18+0300\n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.8.11\n" "Last-Translator: \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "Language: ru_RU\n" #: ../udiskie/prompt.py:168 ../udiskie/prompt.py:178 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "Введите пароль для {0.device_presentation}:" #: ../udiskie/prompt.py:211 msgid "Positional field in format string {!r} is deprecated." msgstr "Позиционное поле в формате строки {!r} является устаревшим." #: ../udiskie/prompt.py:220 msgid "Unknown device attribute {!r} in format string: {!r}" msgstr "Неизвестный атрибут устройства {!r} в формате строки: {!r}" #: ../udiskie/prompt.py:276 msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "f' option." msgstr "" "Не найдена программа для работы с файлами: {0!r}. Вы можете изменить " "значение указанное после '-f'." #: ../udiskie/config.py:112 msgid "Unknown matching attribute: {!r}" msgstr "Неизвестный атрибут соответствия: {!r}" #: ../udiskie/config.py:114 #, python-brace-format msgid "{0} created" msgstr "{0} создан(о)" #: ../udiskie/config.py:117 msgid "{0}(match={1!r}, value={2!r})" msgstr "{0}(соответствие={1!r}, значение={2!r})" #: ../udiskie/config.py:143 msgid "{0}(match={1!r}, {2}={3!r}) used for {4}" msgstr "{0}(соответствие={1!r}, {2}={3!r}) использовано в {4}" #: ../udiskie/config.py:231 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "Невозможно прочитать файл настроек: {0}" #: ../udiskie/config.py:234 msgid "Failed to read {0!r}: {1}" msgstr "Невозможно прочитать {0!r}: {1}" #: ../udiskie/depend.py:43 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "Отсутствуют библиотеки GTK 3. Возврат к GTK 2 для запроса пароля" #: ../udiskie/depend.py:49 msgid "X server not connected!" msgstr "X-сервер не подключен!" #: ../udiskie/mount.py:31 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "Невозможно {0} {1}: {2}" #: ../udiskie/mount.py:120 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "обзор недоступен т.к. {0}: не смонтирован" #: ../udiskie/mount.py:123 #, python-brace-format msgid "not browsing {0}: no program" msgstr "обзор {0} недоступен т.к. нет программы" #: ../udiskie/mount.py:125 #, python-brace-format msgid "opening {0} on {0.mount_paths[0]}" msgstr "открытие {0} на {0.mount_paths[0]}" #: ../udiskie/mount.py:127 #, python-brace-format msgid "opened {0} on {0.mount_paths[0]}" msgstr "открыто {0} on {0.mount_paths[0]}" #: ../udiskie/mount.py:142 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "не смонтировано {0}: необработанное устройство" #: ../udiskie/mount.py:145 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "не смонтировано {0}: уже смонтировано" #: ../udiskie/mount.py:149 #, python-brace-format msgid "mounting {0} with {1}" msgstr "монтируется {0} с {1}" #: ../udiskie/mount.py:152 #, python-brace-format msgid "mounted {0} on {1}" msgstr "смонтировано {0} в {1}" #: ../udiskie/mount.py:158 msgid "" "Mounting NTFS device with default driver.\n" "Please install 'ntfs-3g' if you experience problems or the device is " "readonly." msgstr "" "Устройство с NTFS смонтировано используя драйвер по умолчанию.\n" "Пожалуйста установите 'ntfs-3g' при возникновении проблем или устройство " "доступно только для чтения." #: ../udiskie/mount.py:173 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "не размонтировано {0}: необработанное устройство" #: ../udiskie/mount.py:176 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "не размонтировано {0}: не смонтировано" #: ../udiskie/mount.py:178 #, python-brace-format msgid "unmounting {0}" msgstr "размонтируется {0}" #: ../udiskie/mount.py:180 #, python-brace-format msgid "unmounted {0}" msgstr "размонтировно {0}" #: ../udiskie/mount.py:195 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "не разблокировано {0}: необработанное устройство" #: ../udiskie/mount.py:198 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "не разблокировано {0}: уже разблокировано" #: ../udiskie/mount.py:201 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "не разблокировано {0}: нет пароля" #: ../udiskie/mount.py:211 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "не разблокировано {0}: отменено пользователем" #: ../udiskie/mount.py:214 #, python-brace-format msgid "unlocking {0} using keyfile" msgstr "разблокировано {0} с помощью ключевого файла" #: ../udiskie/mount.py:217 #, python-brace-format msgid "unlocking {0}" msgstr "разблокировка {0}" #: ../udiskie/mount.py:220 #, python-brace-format msgid "unlocked {0}" msgstr "разблокировано {0}" #: ../udiskie/mount.py:231 #, python-brace-format msgid "unlocking {0} using cached password" msgstr "разблокировка {0} с помощью кэшированного пароля" #: ../udiskie/mount.py:235 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "не удалось разблокировать {0} с помощью кэшированного пароля" #: ../udiskie/mount.py:238 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "разблокировано {0} с помощью кэшированного пароля" #: ../udiskie/mount.py:246 msgid "No matching keyfile rule for {}." msgstr "Нет соответствующего правила ключевого файла для {}." #: ../udiskie/mount.py:252 #, python-brace-format msgid "keyfile for {0} not found: {1}" msgstr "ключевой файл для {0} не найден: {1}" #: ../udiskie/mount.py:254 #, python-brace-format msgid "unlocking {0} using keyfile {1}" msgstr "разблокируется {0} с помощью ключевого файла {1}" #: ../udiskie/mount.py:258 #, python-brace-format msgid "failed to unlock {0} using keyfile" msgstr "не удалось разблокировать {0} с помощью ключевого файла" #: ../udiskie/mount.py:261 #, python-brace-format msgid "unlocked {0} using keyfile" msgstr "разблокировно {0} с помощью ключевого файла" #: ../udiskie/mount.py:286 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "не блокировано {0}: необработанное устройство" #: ../udiskie/mount.py:289 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "разблокирован {0} с помощью ключевого файла" #: ../udiskie/mount.py:291 #, python-brace-format msgid "locking {0}" msgstr "заперто {0}" #: ../udiskie/mount.py:293 #, python-brace-format msgid "locked {0}" msgstr "заблокировано {0}" #: ../udiskie/mount.py:331 ../udiskie/mount.py:373 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "не добавлено {0}: необработанное устройство" #: ../udiskie/mount.py:410 ../udiskie/mount.py:460 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "не удалено {0}: необработанное устройство" #: ../udiskie/mount.py:486 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "не извлечено {0}: необработанное устройство" #: ../udiskie/mount.py:490 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "не извлечено {0}: лоток привод не извлекается" #: ../udiskie/mount.py:496 #, python-brace-format msgid "ejecting {0}" msgstr "выбросить {0}" #: ../udiskie/mount.py:498 #, python-brace-format msgid "ejected {0}" msgstr "выброшено {0}" #: ../udiskie/mount.py:513 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "не отсоединено {0}: необработанное устройство" #: ../udiskie/mount.py:517 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "не отсоединено{0}: устройство не отсоединяемое" #: ../udiskie/mount.py:521 #, python-brace-format msgid "detaching {0}" msgstr "отсоединение {0}" #: ../udiskie/mount.py:523 #, python-brace-format msgid "detached {0}" msgstr "отсоединено {0}" #: ../udiskie/mount.py:576 #, python-brace-format msgid "not setting up {0}: already up" msgstr "не включено {0}: уже включено" #: ../udiskie/mount.py:579 #, python-brace-format msgid "not setting up {0}: not a file" msgstr "не включено {0}: это не файл" #: ../udiskie/mount.py:581 #, python-brace-format msgid "setting up {0}" msgstr "настройка {0}" #: ../udiskie/mount.py:589 #, python-brace-format msgid "set up {0} as {1}" msgstr "установлено {0} как {1}" #: ../udiskie/mount.py:605 #, python-brace-format msgid "not deleting {0}: unhandled device" msgstr "не удаляется {0}: необработанное устройство" #: ../udiskie/mount.py:609 #, python-brace-format msgid "deleting {0}" msgstr "удаление {0}" #: ../udiskie/mount.py:611 #, python-brace-format msgid "deleted {0}" msgstr "удалён {0}" #: ../udiskie/mount.py:750 #, python-brace-format msgid "Browse {0}" msgstr "Обзор {0}" #: ../udiskie/mount.py:751 #, python-brace-format msgid "Mount {0}" msgstr "Монтировать {0}" #: ../udiskie/mount.py:752 #, python-brace-format msgid "Unmount {0}" msgstr "Размонтировать {0}" #: ../udiskie/mount.py:753 #, python-brace-format msgid "Unlock {0}" msgstr "Разблокировать {0}" #: ../udiskie/mount.py:754 #, python-brace-format msgid "Lock {0}" msgstr "Заблокировать {0}" #: ../udiskie/mount.py:755 #, python-brace-format msgid "Eject {1}" msgstr "Извлечь {1}" #: ../udiskie/mount.py:756 #, python-brace-format msgid "Unpower {1}" msgstr "Отключить питание {1}" #: ../udiskie/mount.py:757 #, python-brace-format msgid "Clear password for {0}" msgstr "Очистить пароль для {0}" #: ../udiskie/mount.py:758 #, python-brace-format msgid "Detach {0}" msgstr "Отсоединить {0}" #: ../udiskie/udisks2.py:661 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "найдено устройство, владеющее \"{0}\": \"{1}\"" #: ../udiskie/udisks2.py:664 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "не найдены устройства, владеющие \"{0}\"" #: ../udiskie/udisks2.py:683 #, python-brace-format msgid "Daemon version: {0}" msgstr "Версия демона: {0}" #: ../udiskie/udisks2.py:688 #, python-brace-format msgid "Keyfile support: {0}" msgstr "Поддержка ключевых файлов: {0}" #: ../udiskie/udisks2.py:771 #, python-brace-format msgid "+++ {0}: {1}" msgstr "+++ {0}: {1}" #: ../udiskie/tray.py:164 msgid "Mount disc image" msgstr "Монтировать образ диска" #: ../udiskie/tray.py:170 msgid "Enable automounting" msgstr "Включить автоматическое монтирование" #: ../udiskie/tray.py:176 msgid "Enable notifications" msgstr "Включить уведомления" #: ../udiskie/tray.py:185 msgid "Quit" msgstr "Выход" #: ../udiskie/tray.py:192 msgid "Open disc image" msgstr "Открыть образ диска" #: ../udiskie/tray.py:194 msgid "Open" msgstr "Открыть" #: ../udiskie/tray.py:195 msgid "Cancel" msgstr "Отмена" #: ../udiskie/tray.py:244 msgid "No external devices" msgstr "Нет внешних устройств" #: ../udiskie/tray.py:348 msgid "udiskie" msgstr "udiskie" #: ../udiskie/notify.py:66 msgid "Browse directory" msgstr "Обзор папки" #: ../udiskie/notify.py:70 msgid "Device mounted" msgstr "Устройство смонтировано" #: ../udiskie/notify.py:71 #, python-brace-format msgid "{0.ui_label} mounted on {0.mount_paths[0]}" msgstr "{0.ui_label} смонтировано в {0.mount_paths[0]}" #: ../udiskie/notify.py:85 msgid "Device unmounted" msgstr "Устройство размонтировано" #: ../udiskie/notify.py:86 #, python-brace-format msgid "{0.ui_label} unmounted" msgstr "{0.ui_label} размонтировано" #: ../udiskie/notify.py:99 msgid "Device locked" msgstr "Устройство заблокировано" #: ../udiskie/notify.py:100 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "{0.device_presentation} заблокированно" #: ../udiskie/notify.py:113 msgid "Device unlocked" msgstr "Устройство разблокировано" #: ../udiskie/notify.py:114 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "{0.device_presentation} разблокированно" #: ../udiskie/notify.py:151 msgid "Device added" msgstr "Устройство добавлено" #: ../udiskie/notify.py:152 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "устройство появилось в {0.device_presentation}" #: ../udiskie/notify.py:175 msgid "Device removed" msgstr "Устройство удалено" #: ../udiskie/notify.py:176 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "устройство исчезло в {0.device_presentation}" #: ../udiskie/notify.py:189 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" "неудачно {0} {1}:\n" "{2}" #: ../udiskie/notify.py:191 #, python-brace-format msgid "failed to {0} device {1}." msgstr "неудачно {0} устройство {1}." #: ../udiskie/notify.py:197 msgid "Retry" msgstr "Повторить попытку" #: ../udiskie/notify.py:200 msgid "Job failed" msgstr "Задание не выполнено" #: ../udiskie/notify.py:231 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "Не удалось показать уведомление: {0}" #: ../udiskie/cli.py:46 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "Эти параметры являются взаимоисключающими: {0}" #: ../udiskie/cli.py:116 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" "\n" " Файл конфигурации может быть JSON или, предпочтительно, YAML-файлом\n" " Примеры приведены в документации.\n" " " #: ../udiskie/cli.py:141 #, python-format msgid "%(message)s" msgstr "%(message)s" #: ../udiskie/cli.py:142 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "%(levelname)s [%(asctime)s] %(name)s: %(message)s" #: ../udiskie/cli.py:383 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 "" "Typelib для 'libnotify' недоступна. Возможные причины:\n" "\t- libnotify не установлена\n" "\t- typelib поставляется в виде отдельного пакета\n" "\t- libnotify была собрана с отключенным самоанализом\n" "\n" "Запуск udiskie выполнен без уведомлений." #: ../udiskie/cli.py:393 msgid "" "Not run within X session. \n" "Starting udiskie without tray icon.\n" msgstr "" "Не запущено в сессии X-Windows. \n" "Запустите udiskie без иконки в трее.\n" #: ../udiskie/cli.py:400 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" "Starting udiskie without tray icon.\n" msgstr "" "Typelib для GTK 3.0 недоступен. Возможные причины:\n" "\t- GTK3 не установлен\n" "\t- typelib поставляется в виде отдельного пакета\n" "\t- GTK3 был собран с отключенным самоанализом\n" "Запуск udiskie выполнен без иконки в трее.\n" udiskie-2.1.0/lang/sk_SK.po0000664000372000037200000003562313615574740016331 0ustar travistravis00000000000000# Slovak translations for PACKAGE package # Slovenské preklady pre balík PACKAGE. # Copyright (C) 2019 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # oli , 2019. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-12-26 13:57+0100\n" "PO-Revision-Date: 2019-05-02 14:20+0200\n" "Last-Translator: Jose Riha \n" "Language-Team: Slovak\n" "Language: sk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Poedit 2.2.1\n" #: ../udiskie/cli.py:46 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "Tieto voľby sa navzájom vylučujú: {0}" #: ../udiskie/cli.py:119 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:139 #, python-format msgid "%(message)s" msgstr "%(message)s" #: ../udiskie/cli.py:141 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "%(levelname)s [%(asctime)s] %(name)s: %(message)s" #: ../udiskie/cli.py:370 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:380 msgid "" "Not run within X session. \n" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/cli.py:387 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" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/depend.py:50 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:56 msgid "X server not connected!" msgstr "X server nie je pripojený!" #: ../udiskie/prompt.py:92 msgid "Show password" msgstr "" #: ../udiskie/prompt.py:97 msgid "Open keyfile…" msgstr "" #: ../udiskie/prompt.py:104 msgid "Remember password" msgstr "" #: ../udiskie/prompt.py:119 msgid "Open a keyfile to unlock the LUKS device" msgstr "" #: ../udiskie/prompt.py:153 ../udiskie/prompt.py:163 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "Zadajte heslo pre {0.device_presentation}: " #: ../udiskie/prompt.py:197 msgid "Unknown device attribute {!r} in format string: {!r}" msgstr "Neznámy atribút zariadenia {!r} vo formátovacom reťazci: {!r}" #: ../udiskie/prompt.py:245 msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "f' option." msgstr "" "Nepodarilo sa nájsť prehliadač súborov: {0!r}. Možno budete chcieť zmeniť " "hodnotu pre voľbu '-f'." #: ../udiskie/mount.py:29 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "chyba pri {0} {1}: {2}" #: ../udiskie/mount.py:118 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "neprezerám {0}: nepripojené" #: ../udiskie/mount.py:121 #, python-brace-format msgid "not browsing {0}: no program" msgstr "neprezerám {0}: chýba program" #: ../udiskie/mount.py:123 ../udiskie/mount.py:143 #, python-brace-format msgid "opening {0} on {0.mount_paths[0]}" msgstr "otváram {0} na {0.mount_paths[0]}" #: ../udiskie/mount.py:125 ../udiskie/mount.py:145 #, python-brace-format msgid "opened {0} on {0.mount_paths[0]}" msgstr "otvorené {0} na {0.mount_paths[0]}" #: ../udiskie/mount.py:138 #, fuzzy, python-brace-format msgid "not opening terminal {0}: not mounted" msgstr "neodpájam {0}: nepripojené" #: ../udiskie/mount.py:141 #, fuzzy, python-brace-format msgid "not opening terminal {0}: no program" msgstr "neprezerám {0}: chýba program" #: ../udiskie/mount.py:159 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "nepripám {0}: nepodporované zariadenie" #: ../udiskie/mount.py:162 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "nepripájam {0}: už pripojené" #: ../udiskie/mount.py:166 #, python-brace-format msgid "mounting {0} with {1}" msgstr "pripájam {0} s {1}" #: ../udiskie/mount.py:169 #, python-brace-format msgid "mounted {0} on {1}" msgstr "pripojené {0} na {1}" #: ../udiskie/mount.py:175 msgid "" "Mounting NTFS device with default driver.\n" "Please install 'ntfs-3g' if you experience problems or the device is " "readonly." msgstr "" "Pripájam zariadenie NTFS s východzím ovládačom.\n" "Prosím, nainštalujte 'ntfs-3g', ak narazíte na problémy alebo nemôžete na " "zariadenie zapisovať." #: ../udiskie/mount.py:189 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "neodpájam {0}: nepodporované zariadenie" #: ../udiskie/mount.py:192 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "neodpájam {0}: nepripojené" #: ../udiskie/mount.py:194 #, python-brace-format msgid "unmounting {0}" msgstr "odpájam {0}" #: ../udiskie/mount.py:196 #, python-brace-format msgid "unmounted {0}" msgstr "odpojené {0}" #: ../udiskie/mount.py:210 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "nezamykám {0}: nepodporované zariadenie" #: ../udiskie/mount.py:213 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "nezamykám {0}: už odomknuté" #: ../udiskie/mount.py:216 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "nezamykám {0}: chýba dialógové okno pre heslo" #: ../udiskie/mount.py:230 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "nezamykám {0}: zrušené používateľom" #: ../udiskie/mount.py:235 #, python-brace-format msgid "unlocking {0} using keyfile" msgstr "odomykám {0} použitím kľúča" #: ../udiskie/mount.py:238 #, python-brace-format msgid "unlocking {0}" msgstr "odomykám {0}" #: ../udiskie/mount.py:241 #, python-brace-format msgid "unlocked {0}" msgstr "odomknuté {0}" #: ../udiskie/mount.py:252 #, python-brace-format msgid "unlocking {0} using cached password" msgstr "odomykám {0} použitím kešovaného hesla" #: ../udiskie/mount.py:256 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "nepodarilo sa odomknúť {0} použitím kešovaného hesla" #: ../udiskie/mount.py:259 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "odomknuté {0} použitím kešovaného hesla" #: ../udiskie/mount.py:267 msgid "No matching keyfile rule for {}." msgstr "Nepodarilo sa nájsť zodpovedajúce pravidlo pri kľúči pre {}." #: ../udiskie/mount.py:273 #, python-brace-format msgid "keyfile for {0} not found: {1}" msgstr "kľúč pre {0} nebol nájdený: {1}" #: ../udiskie/mount.py:275 #, python-brace-format msgid "unlocking {0} using keyfile {1}" msgstr "odomykám {0} použitím kľúča {1}" #: ../udiskie/mount.py:279 #, python-brace-format msgid "failed to unlock {0} using keyfile" msgstr "nepodarilo sa odomknúť {0} použitím kľúča" #: ../udiskie/mount.py:282 #, python-brace-format msgid "unlocked {0} using keyfile" msgstr "odomknuté {0} použitím kľúča" #: ../udiskie/mount.py:308 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "neodomykám {0}: nepodporované zariadenie" #: ../udiskie/mount.py:311 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "neodomykám {0}: nie je zamknuté" #: ../udiskie/mount.py:313 #, python-brace-format msgid "locking {0}" msgstr "zamykám {0}" #: ../udiskie/mount.py:315 #, python-brace-format msgid "locked {0}" msgstr "zamknuté {0}" #: ../udiskie/mount.py:352 ../udiskie/mount.py:395 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "nepridávam {0}: nepodporované zariadenie" #: ../udiskie/mount.py:431 ../udiskie/mount.py:481 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "neodstraňujem {0}: nepodporované zariadenie" #: ../udiskie/mount.py:506 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "nevysúvam {0}: nepodporované zariadenie" #: ../udiskie/mount.py:510 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "nevysúvam {0}: zariadenie to nepodporuje" #: ../udiskie/mount.py:516 #, python-brace-format msgid "ejecting {0}" msgstr "vysúvam {0}" #: ../udiskie/mount.py:518 #, python-brace-format msgid "ejected {0}" msgstr "vysunuté {0}" #: ../udiskie/mount.py:532 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:536 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "" #: ../udiskie/mount.py:540 #, python-brace-format msgid "detaching {0}" msgstr "" #: ../udiskie/mount.py:542 #, python-brace-format msgid "detached {0}" msgstr "" #: ../udiskie/mount.py:593 #, python-brace-format msgid "not setting up {0}: already up" msgstr "nenastavujem {0}: už je nastavené" #: ../udiskie/mount.py:596 #, python-brace-format msgid "not setting up {0}: not a file" msgstr "nenastavujem {0}: nie je to súbor" #: ../udiskie/mount.py:598 #, python-brace-format msgid "setting up {0}" msgstr "nastavujem {0}" #: ../udiskie/mount.py:606 #, python-brace-format msgid "set up {0} as {1}" msgstr "nastaviť {0} ako {1}" #: ../udiskie/mount.py:621 #, python-brace-format msgid "not deleting {0}: unhandled device" msgstr "nemažem {0}: nepodporované zariadenie" #: ../udiskie/mount.py:625 #, python-brace-format msgid "deleting {0}" msgstr "mažem {0}" #: ../udiskie/mount.py:627 #, python-brace-format msgid "deleted {0}" msgstr "vymazané {0}" #: ../udiskie/mount.py:757 #, python-brace-format msgid "Browse {0}" msgstr "Prehliadať {0}" #: ../udiskie/mount.py:758 #, fuzzy, python-brace-format msgid "Hack on {0}" msgstr "zamykám {0}" #: ../udiskie/mount.py:759 #, python-brace-format msgid "Mount {0}" msgstr "Pripojiť {0}" #: ../udiskie/mount.py:760 #, python-brace-format msgid "Unmount {0}" msgstr "Odpojiť {0}" #: ../udiskie/mount.py:761 #, python-brace-format msgid "Unlock {0}" msgstr "Odomknúť {0}" #: ../udiskie/mount.py:762 #, python-brace-format msgid "Lock {0}" msgstr "Uzamknúť {0}" #: ../udiskie/mount.py:763 #, python-brace-format msgid "Eject {1}" msgstr "Vysunúť {1}" #: ../udiskie/mount.py:764 #, python-brace-format msgid "Unpower {1}" msgstr "Vypnúť {1}" #: ../udiskie/mount.py:765 #, python-brace-format msgid "Clear password for {0}" msgstr "Vymazať heslo pre {0}" #: ../udiskie/mount.py:766 #, python-brace-format msgid "Detach {0}" msgstr "" #: ../udiskie/udisks2.py:662 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "našiel som zariadenie vlastniace \"{0}\": \"{1}\"" #: ../udiskie/udisks2.py:665 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "nenašiel som zariadenie vlastniace \"{0}\"" #: ../udiskie/udisks2.py:684 #, python-brace-format msgid "Daemon version: {0}" msgstr "Verzia daemona: {0}" #: ../udiskie/udisks2.py:689 #, python-brace-format msgid "Keyfile support: {0}" msgstr "Podpora pre kľúč (keyfile): {0}" #: ../udiskie/udisks2.py:768 #, python-brace-format msgid "+++ {0}: {1}" msgstr "+++ {0}: {1}" #: ../udiskie/tray.py:159 msgid "Mount disc image" msgstr "Pripojiť obraz disku" #: ../udiskie/tray.py:165 msgid "Enable automounting" msgstr "Povoliť automatické pripájanie" #: ../udiskie/tray.py:171 msgid "Enable notifications" msgstr "Povoliť notifikácie" #: ../udiskie/tray.py:180 msgid "Quit" msgstr "Ukončiť" #: ../udiskie/tray.py:187 msgid "Open disc image" msgstr "Otvoriť obraz disku" #: ../udiskie/tray.py:189 msgid "Open" msgstr "Otvoriť" #: ../udiskie/tray.py:190 msgid "Cancel" msgstr "Zrušiť" #: ../udiskie/tray.py:230 msgid "Invalid node!" msgstr "" #: ../udiskie/tray.py:232 msgid "No external devices" msgstr "Žiadne externé zariadenia" #: ../udiskie/tray.py:341 msgid "udiskie" msgstr "udiskie" #: ../udiskie/config.py:111 msgid "Unknown matching attribute: {!r}" msgstr "Neznámy atribút pre vyhľadávanie: {!r}" #: ../udiskie/config.py:113 #, python-brace-format msgid "{0} created" msgstr "{0} vytvorené" #: ../udiskie/config.py:116 msgid "{0}(match={1!r}, value={2!r})" msgstr "{0}(zhoda={1!r}, hodnota={2!r})" #: ../udiskie/config.py:136 msgid "{0}(match={1!r}, {2}={3!r}) used for {4}" msgstr "{0}(zhoda={1!r}, {2}={3!r}) použité pre {4}" #: ../udiskie/config.py:212 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "Nepodarilo sa načítať konfiguračný súbor: {0}" #: ../udiskie/config.py:215 msgid "Failed to read {0!r}: {1}" msgstr "Nepodarilo sa načítať {0!r}: {1}" #: ../udiskie/notify.py:62 msgid "Browse directory" msgstr "Prehliadať adresár" #: ../udiskie/notify.py:64 msgid "Open terminal" msgstr "" #: ../udiskie/notify.py:68 msgid "Device mounted" msgstr "Zariadenie pripojené" #: ../udiskie/notify.py:69 #, python-brace-format msgid "{0.ui_label} mounted on {0.mount_paths[0]}" msgstr "{0.ui_label} pripojené na {0.mount_paths[0]}" #: ../udiskie/notify.py:80 msgid "Device unmounted" msgstr "Zariadenie odpojené" #: ../udiskie/notify.py:81 #, python-brace-format msgid "{0.ui_label} unmounted" msgstr "{0.ui_label} odpojené" #: ../udiskie/notify.py:90 msgid "Device locked" msgstr "Zariadenie zamknuté" #: ../udiskie/notify.py:91 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "{0.device_presentation} zamknuté" #: ../udiskie/notify.py:100 msgid "Device unlocked" msgstr "Zariadenie odomknuté" #: ../udiskie/notify.py:101 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "{0.device_presentation} odomknuté" #: ../udiskie/notify.py:134 msgid "Device added" msgstr "Zariadenie pridané" #: ../udiskie/notify.py:135 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "zariadenie sa objavilo na {0.device_presentation}" #: ../udiskie/notify.py:154 msgid "Device removed" msgstr "Zariadenie odstránené" #: ../udiskie/notify.py:155 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "zariadenie zmizlo z {0.device_presentation}" #: ../udiskie/notify.py:164 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" "nepodarilo sa {0} {1}:\n" "{2}" #: ../udiskie/notify.py:166 #, python-brace-format msgid "failed to {0} device {1}." msgstr "chyba pri {0} zariadenia {1}." #: ../udiskie/notify.py:172 msgid "Retry" msgstr "Skúsiť znova" #: ../udiskie/notify.py:175 msgid "Job failed" msgstr "Úloha skončila s chybou" #: ../udiskie/notify.py:206 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "Nepodarilo sa zobraziť notifikáciu: {0}" #~ msgid "Positional field in format string {!r} is deprecated." #~ msgstr "Pozičné pole vo formátovacom reťazci {!r} je zastaralé." udiskie-2.1.0/lang/es_ES.po0000664000372000037200000003603113615574740016307 0ustar travistravis00000000000000msgid "" msgstr "" "Project-Id-Version: udiskie\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-12-26 13:57+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/cli.py:46 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "Estas opciones son excluyentes: {0}" #: ../udiskie/cli.py:119 #, 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:139 #, python-format msgid "%(message)s" msgstr "%(message)s" #: ../udiskie/cli.py:141 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "%(levelname)s [%(asctime)s] %(name)s: %(message)s" #: ../udiskie/cli.py:370 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:380 msgid "" "Not run within X session. \n" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/cli.py:387 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" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/depend.py:50 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:56 msgid "X server not connected!" msgstr "¡Servidor X no conectado!" #: ../udiskie/prompt.py:92 msgid "Show password" msgstr "" #: ../udiskie/prompt.py:97 msgid "Open keyfile…" msgstr "" #: ../udiskie/prompt.py:104 msgid "Remember password" msgstr "" #: ../udiskie/prompt.py:119 msgid "Open a keyfile to unlock the LUKS device" msgstr "" #: ../udiskie/prompt.py:153 ../udiskie/prompt.py:163 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "Introduce la clave para {0.device_presentation}: " #: ../udiskie/prompt.py:197 msgid "Unknown device attribute {!r} in format string: {!r}" msgstr "" #: ../udiskie/prompt.py:245 #, fuzzy msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "f' option." msgstr "" "No se encontró el gestor de ficheros: {0!r}. Puede que necesites cambiar el " "valor de la opción '-b'." #: ../udiskie/mount.py:29 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "Fallo al {0} {1}: {2}" #: ../udiskie/mount.py:118 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "no se exploró {0}: no está montado" #: ../udiskie/mount.py:121 #, python-brace-format msgid "not browsing {0}: no program" msgstr "no se exploró {0}: no hay programa configurado" #: ../udiskie/mount.py:123 ../udiskie/mount.py:143 #, python-brace-format msgid "opening {0} on {0.mount_paths[0]}" msgstr "abriendo {0} en {0.mount_paths[0]}" #: ../udiskie/mount.py:125 ../udiskie/mount.py:145 #, python-brace-format msgid "opened {0} on {0.mount_paths[0]}" msgstr "se abrió {0} en {0.mount_paths[0]}" #: ../udiskie/mount.py:138 #, fuzzy, python-brace-format msgid "not opening terminal {0}: not mounted" msgstr "no se desmontó {0}: no estaba montado" #: ../udiskie/mount.py:141 #, fuzzy, python-brace-format msgid "not opening terminal {0}: no program" msgstr "no se exploró {0}: no hay programa configurado" #: ../udiskie/mount.py:159 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "no se montó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:162 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "no se montó {0}: ya está montado" #: ../udiskie/mount.py:166 #, python-brace-format msgid "mounting {0} with {1}" msgstr "montando {0} en {1}" #: ../udiskie/mount.py:169 #, python-brace-format msgid "mounted {0} on {1}" msgstr "montado {0} en {1}" #: ../udiskie/mount.py:175 msgid "" "Mounting NTFS device with default driver.\n" "Please install 'ntfs-3g' if you experience problems or the device is " "readonly." msgstr "" #: ../udiskie/mount.py:189 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "no se desmontó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:192 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "no se desmontó {0}: no estaba montado" #: ../udiskie/mount.py:194 #, python-brace-format msgid "unmounting {0}" msgstr "desmontando {0}" #: ../udiskie/mount.py:196 #, python-brace-format msgid "unmounted {0}" msgstr "desmontado {0}" #: ../udiskie/mount.py:210 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "no se desbloqueó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:213 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "no se desbloqueó {0}: ya está desbloqueado" #: ../udiskie/mount.py:216 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:230 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "no se desbloqueó {0}: cancellado por el usuario" #: ../udiskie/mount.py:235 #, fuzzy, python-brace-format msgid "unlocking {0} using keyfile" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:238 #, python-brace-format msgid "unlocking {0}" msgstr "desbloqueando {0}" #: ../udiskie/mount.py:241 #, python-brace-format msgid "unlocked {0}" msgstr "desbloqueado {0}" #: ../udiskie/mount.py:252 #, fuzzy, python-brace-format msgid "unlocking {0} using cached password" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:256 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:259 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:267 msgid "No matching keyfile rule for {}." msgstr "" #: ../udiskie/mount.py:273 #, fuzzy, python-brace-format msgid "keyfile for {0} not found: {1}" msgstr "Dispositivo no encontrado: {0}" #: ../udiskie/mount.py:275 #, fuzzy, python-brace-format msgid "unlocking {0} using keyfile {1}" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:279 #, python-brace-format msgid "failed to unlock {0} using keyfile" msgstr "" #: ../udiskie/mount.py:282 #, fuzzy, python-brace-format msgid "unlocked {0} using keyfile" msgstr "desbloqueado {0}" #: ../udiskie/mount.py:308 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "no se bloqueó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:311 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "no se bloqueó {0}: no estaba desbloqueado" #: ../udiskie/mount.py:313 #, python-brace-format msgid "locking {0}" msgstr "bloqueando {0}" #: ../udiskie/mount.py:315 #, python-brace-format msgid "locked {0}" msgstr "bloqueado {0}" #: ../udiskie/mount.py:352 ../udiskie/mount.py:395 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "no se añadió {0}: dispositivo no gestionado" #: ../udiskie/mount.py:431 ../udiskie/mount.py:481 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "no se eliminó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:506 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "no se expulsó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:510 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "no se expulsó {0}: dispositivo no expulsable" #: ../udiskie/mount.py:516 #, python-brace-format msgid "ejecting {0}" msgstr "expulsando {0}" #: ../udiskie/mount.py:518 #, python-brace-format msgid "ejected {0}" msgstr "expulsado {0}" #: ../udiskie/mount.py:532 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "no se desconectó {0}: unhandled device" #: ../udiskie/mount.py:536 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "no se desconectó {0}: dispositivo no desconectable" #: ../udiskie/mount.py:540 #, python-brace-format msgid "detaching {0}" msgstr "desconectando {0}" #: ../udiskie/mount.py:542 #, python-brace-format msgid "detached {0}" msgstr "desconectado {0}" #: ../udiskie/mount.py:593 #, fuzzy, python-brace-format msgid "not setting up {0}: already up" msgstr "no se montó {0}: ya está montado" #: ../udiskie/mount.py:596 #, fuzzy, python-brace-format msgid "not setting up {0}: not a file" msgstr "no se expulsó {0}: dispositivo no expulsable" #: ../udiskie/mount.py:598 #, fuzzy, python-brace-format msgid "setting up {0}" msgstr "expulsando {0}" #: ../udiskie/mount.py:606 #, python-brace-format msgid "set up {0} as {1}" msgstr "" #: ../udiskie/mount.py:621 #, fuzzy, python-brace-format msgid "not deleting {0}: unhandled device" msgstr "no se expulsó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:625 #, fuzzy, python-brace-format msgid "deleting {0}" msgstr "expulsando {0}" #: ../udiskie/mount.py:627 #, fuzzy, python-brace-format msgid "deleted {0}" msgstr "expulsado {0}" #: ../udiskie/mount.py:757 #, python-brace-format msgid "Browse {0}" msgstr "Explorar {0}" #: ../udiskie/mount.py:758 #, fuzzy, python-brace-format msgid "Hack on {0}" msgstr "bloqueando {0}" #: ../udiskie/mount.py:759 #, python-brace-format msgid "Mount {0}" msgstr "Montar {0}" #: ../udiskie/mount.py:760 #, python-brace-format msgid "Unmount {0}" msgstr "Desmontar {0}" #: ../udiskie/mount.py:761 #, python-brace-format msgid "Unlock {0}" msgstr "Desbloquear {0}" #: ../udiskie/mount.py:762 #, python-brace-format msgid "Lock {0}" msgstr "Bloquear {0}" #: ../udiskie/mount.py:763 #, fuzzy, python-brace-format msgid "Eject {1}" msgstr "Expulsar {1}" #: ../udiskie/mount.py:764 #, fuzzy, python-brace-format msgid "Unpower {1}" msgstr "Apagar {1}" #: ../udiskie/mount.py:765 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/mount.py:766 #, fuzzy, python-brace-format msgid "Detach {0}" msgstr "desconectado {0}" #: ../udiskie/udisks2.py:662 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "Se encontró el dispositivo maestro \"{0}\": \"{1}\"" #: ../udiskie/udisks2.py:665 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "no se encontró dispositivo maestro para \"{0}\"" #: ../udiskie/udisks2.py:684 #, python-brace-format msgid "Daemon version: {0}" msgstr "" #: ../udiskie/udisks2.py:689 #, python-brace-format msgid "Keyfile support: {0}" msgstr "" #: ../udiskie/udisks2.py:768 #, python-brace-format msgid "+++ {0}: {1}" msgstr "+++ {0}: {1}" #: ../udiskie/tray.py:159 msgid "Mount disc image" msgstr "" #: ../udiskie/tray.py:165 msgid "Enable automounting" msgstr "" #: ../udiskie/tray.py:171 msgid "Enable notifications" msgstr "" #: ../udiskie/tray.py:180 msgid "Quit" msgstr "Salir" #: ../udiskie/tray.py:187 msgid "Open disc image" msgstr "" #: ../udiskie/tray.py:189 msgid "Open" msgstr "" #: ../udiskie/tray.py:190 msgid "Cancel" msgstr "" #: ../udiskie/tray.py:230 msgid "Invalid node!" msgstr "¡Nodo inválido!" #: ../udiskie/tray.py:232 msgid "No external devices" msgstr "" #: ../udiskie/tray.py:341 msgid "udiskie" msgstr "udiskie" #: ../udiskie/config.py:111 msgid "Unknown matching attribute: {!r}" msgstr "Atributo de filtrado desconocido: {!r}" #: ../udiskie/config.py:113 #, python-brace-format msgid "{0} created" msgstr "{0} creado" #: ../udiskie/config.py:116 msgid "{0}(match={1!r}, value={2!r})" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:136 #, fuzzy msgid "{0}(match={1!r}, {2}={3!r}) used for {4}" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:212 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:215 #, fuzzy msgid "Failed to read {0!r}: {1}" msgstr "Fallo al {0} {1}: {2}" #: ../udiskie/notify.py:62 msgid "Browse directory" msgstr "Navegar directorio" #: ../udiskie/notify.py:64 msgid "Open terminal" msgstr "" #: ../udiskie/notify.py:68 msgid "Device mounted" msgstr "Dispositivo montado" #: ../udiskie/notify.py:69 #, fuzzy, python-brace-format 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:90 msgid "Device locked" msgstr "Dispositivo bloqueado" #: ../udiskie/notify.py:91 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "{0.device_presentation} bloqueado" #: ../udiskie/notify.py:100 msgid "Device unlocked" msgstr "Dispositivo desbloqueado" #: ../udiskie/notify.py:101 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "{0.device_presentation} desbloqueado" #: ../udiskie/notify.py:134 msgid "Device added" msgstr "Dispositivo añadido" #: ../udiskie/notify.py:135 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "Dispositivo apareció en {0.device_presentation}" #: ../udiskie/notify.py:154 msgid "Device removed" msgstr "Dispositivo retirado" #: ../udiskie/notify.py:155 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "el dispositivo despareció en {0.device_presentation} " #: ../udiskie/notify.py:164 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" "fallo al {0} {1}:\n" "{2}" #: ../udiskie/notify.py:166 #, python-brace-format msgid "failed to {0} device {1}." msgstr "fallo al {0} el dispositivo {1}." #: ../udiskie/notify.py:172 msgid "Retry" msgstr "Reintentar" #: ../udiskie/notify.py:175 msgid "Job failed" msgstr "Falló la tarea." #: ../udiskie/notify.py:206 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" #~ msgid "{0} operation failed for device: {1}" #~ msgstr "Falló la operación {0} para el dispositovo: {1}" #, 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]." #~ msgid "UDisks version not supported: {0}!" #~ msgstr "¡Versión de UDisks no soportada: {0}!" #~ msgid "{0} used for {1}" #~ msgstr "{0} usado para {1}" #~ msgid "Interface {0!r} not available for {1}" #~ msgstr "Interfaz {0!r} no disponible para {1}"