udiskie-1.7.3/0000755000175000017500000000000013214317676014001 5ustar thomasthomas00000000000000udiskie-1.7.3/completions/0000755000175000017500000000000013214317676016335 5ustar thomasthomas00000000000000udiskie-1.7.3/completions/_udiskie0000644000175000017500000000414513214314444020046 0ustar thomasthomas00000000000000#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]" '(--use-udisks1 --use-udisks2)'--udisks-auto"[autodetect UDisks version]" '(--udisks-auto --use-udisks2)'--use-udisks1"[use UDisks 1]" '(--udisks-auto --use-udisks1)'--use-udisks2"[use UDisks 2]" '(-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:cli builtin:gui)' \ 'commands:command name:_path_commands' \ && ret=0 ;; esac return ret } _udiskie "$@" udiskie-1.7.3/completions/_udiskie-mount0000644000175000017500000000302513214314444021202 0ustar thomasthomas00000000000000#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]" '(--use-udisks1 --use-udisks2)'--udisks-auto"[autodetect UDisks version]" '(--udisks-auto --use-udisks2)'--use-udisks1"[use UDisks 1]" '(--udisks-auto --use-udisks1)'--use-udisks2"[use UDisks 2]" '(-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:cli 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-1.7.3/completions/_udiskie-umount0000644000175000017500000000422313214314444021370 0ustar thomasthomas00000000000000#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]" '(--use-udisks1 --use-udisks2)'--udisks-auto"[autodetect UDisks version]" '(--udisks-auto --use-udisks2)'--use-udisks1"[use UDisks 1]" '(--udisks-auto --use-udisks1)'--use-udisks2"[use UDisks 2]" '(-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-1.7.3/setup.cfg0000644000175000017500000000004613214317676015622 0ustar thomasthomas00000000000000[egg_info] tag_build = tag_date = 0 udiskie-1.7.3/test/0000755000175000017500000000000013214317676014760 5ustar thomasthomas00000000000000udiskie-1.7.3/test/test_match.py0000644000175000017500000000510513214314444017454 0ustar thomasthomas00000000000000# encoding: utf-8 """ Tests for the udiskie.match module. These tests are intended to demonstrate and ensure the correct usage of the config file used by udiskie for custom device options. """ import unittest import tempfile import shutil import os.path import gc from udiskie.config import Config, match_config class TestDev(object): def __init__(self, object_path, id_type, id_uuid): self.object_path = object_path self.id_type = id_type self.id_uuid = id_uuid class TestFilterMatcher(unittest.TestCase): """ Tests for the udiskie.match.FilterMatcher class. """ def setUp(self): """Create a temporary config file.""" self.base = tempfile.mkdtemp() self.config_file = os.path.join(self.base, 'filters.conf') with open(self.config_file, 'wt') as f: f.write(''' mount_options: - id_uuid: device-with-options options: noatime,nouser - id_type: vfat options: ro,nouser ignore_device: - id_uuid: ignored-DEVICE ''') 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-1.7.3/test/test_cache.py0000644000175000017500000000416313214314444017426 0ustar thomasthomas00000000000000# encoding: utf-8 """ Tests for the udiskie.cache module. """ from __future__ import unicode_literals import unittest import time from udiskie.cache import PasswordCache class TestDev(object): def __init__(self, id_uuid): self.id_uuid = id_uuid class TestPasswordCache(unittest.TestCase): """ Tests for the udiskie.cache.PasswordCache class. """ # NOTE: The device names are different in each test so that they do not # interfere accidentally. def test_timeout(self): """The cached password expires after the specified timeout.""" device = TestDev('ALPHA') password = '{<}hëllo ωορλδ!{>}' cache = PasswordCache(1) cache[device] = password self.assertEqual(cache[device], password) time.sleep(1.5) with self.assertRaises(KeyError): _ = cache[device] def test_touch(self): """Key access refreshes the timeout.""" device = TestDev('BETA') password = '{<}hëllo ωορλδ!{>}' cache = PasswordCache(3) cache[device] = password time.sleep(2) self.assertEqual(cache[device], password) time.sleep(2) self.assertEqual(cache[device], password) time.sleep(4) with self.assertRaises(KeyError): _ = cache[device] def test_revoke(self): """A key can be deleted manually.""" device = TestDev('GAMMA') password = '{<}hëllo ωορλδ!{>}' cache = PasswordCache(0) cache[device] = password self.assertEqual(cache[device], password) del cache[device] with self.assertRaises(KeyError): _ = cache[device] def test_update(self): device = TestDev('DELTA') password = '{<}hëllo ωορλδ!{>}' cache = PasswordCache(0) cache[device] = password self.assertEqual(cache[device], password) cache[device] = password * 2 self.assertEqual(cache[device], password*2) del cache[device] with self.assertRaises(KeyError): _ = cache[device] if __name__ == '__main__': unittest.main() udiskie-1.7.3/README.rst0000644000175000017500000000373613214314444015467 0ustar thomasthomas00000000000000======= udiskie ======= |Version| |License| *udiskie* is a UDisks_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. Its features include: - automount removable media - 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, requires UDisks2) - password caching (requires python keyutils 0.3) All features can be indidually enabled or disabled. **NOTE:** support for udisks1 and python2 is deprecated and will be discontinued in the next major version of udiskie. .. _UDisks: 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 udiskie-1.7.3/CHANGES.rst0000644000175000017500000002421713214317537015605 0ustar thomasthomas00000000000000CHANGELOG --------- 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-1.7.3/MANIFEST.in0000644000175000017500000000036213173451745015540 0ustar thomasthomas00000000000000include doc/*.txt include doc/asciidoc.conf include doc/Makefile graft completions recursive-include icons *.svg recursive-include lang *.pot *.po include CONTRIBUTORS COPYING LICENSE include README.rst CHANGES.rst include fastentrypoints.py udiskie-1.7.3/doc/0000755000175000017500000000000013214317676014546 5ustar thomasthomas00000000000000udiskie-1.7.3/doc/asciidoc.conf0000644000175000017500000000316512221132411017153 0ustar thomasthomas00000000000000## linkman: macro # Inspired by/borrowed from the GIT source tree at Documentation/asciidoc.conf # # Usage: linkman:command[manpage-section] # # Note, {0} is the manpage section, while {target} is the command. # # Show man link as: (
); if section is defined, else just show # the command. [macros] (?su)[\\]?(?Plinkman):(?P\S*?)\[(?P.*?)\]= [attributes] asterisk=* plus=+ caret=^ startsb=[ endsb=] tilde=~ ifdef::backend-docbook[] [linkman-inlinemacro] {0%{target}} {0#} {0#{target}{0}} {0#} endif::backend-docbook[] ifdef::backend-docbook[] ifndef::docbook-xsl-172[] # "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this. # v1.72 breaks with this because it replaces dots not in roff requests. [listingblock] {title} | {title#} endif::docbook-xsl-172[] endif::backend-docbook[] ifdef::doctype-manpage[] ifdef::backend-docbook[] [header] template::[header-declarations] {pacman_date} {mantitle} {manvolnum} udiskie udiskie {manname} {manpurpose} endif::backend-docbook[] endif::doctype-manpage[] ifdef::backend-xhtml11[] [linkman-inlinemacro] {target}{0?({0})} endif::backend-xhtml11[] udiskie-1.7.3/doc/Makefile0000644000175000017500000000022212522134406016167 0ustar thomasthomas00000000000000udiskie.8: udiskie.8.txt asciidoc.conf a2x --asciidoc-opts="-f asciidoc.conf" -f manpage -L udiskie.8.txt .PHONY: clean clean: rm -f udiskie.8 udiskie-1.7.3/doc/udiskie.8.txt0000644000175000017500000002614313214314444017106 0ustar thomasthomas00000000000000///// vim:set ts=4 sw=4 syntax=asciidoc noet: ///// udiskie(8) ========== Name ---- udiskie - automounter for removable media Synopsis -------- 'udiskie' [OPTIONS] 'udiskie-mount' [OPTIONS] (-a | DEVICE...) 'udiskie-umount' [OPTIONS] (-a | PATH...) Description ----------- *udiskie* is a front-end for UDisks written in python. Its main purpose is automatically mounting removable media, such as CDs or flash drives. It has optional mount notifications, a GTK tray icon and user level CLIs for manual mount and unmount operations. The media will be mounted in a new directory under '/media' or '/run/media/USER/', using the device name if possible. Common options -------------- *-h, \--help*:: Show help message and exit. *-V, \--version*:: Show help message and exit. *-v, \--verbose*:: Verbose output. *-q, \--quiet*:: Quiet output. *-0, \--udisks-auto*:: Auto discover UDisks version (default). Prefers UDisks2 if both are available. *-1, \--use-udisks1*:: Use UDisks1 as DBus backend. *-2, \--use-udisks2*:: Use UDisks2 as DBus backend. *-c FILE, \--config=FILE*:: Specify config file. *-C, \--no-config*:: Don't use any config file. Shared Mount and Daemon options ------------------------------- *-p COMMAND, \--password-prompt=COMMAND*:: Password retrieval command. The string is formatted with 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. This option is deprecated and will probably be replaced by a python commands file. *-F, \--no-file-manager*:: Disable browsing. *-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 udisks_version: 2 # [int] Specify the version of udisks # to be used. Set to 0 to use automatic # discovery. tray: auto # [bool] Enable the tray icon. "auto" # means auto-hide the tray icon when # there are no handled devices. 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 first 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 multliple 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 - 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 notification_actions: # Define which actions should be shown on notifications. Note that there # are currently only a limited set of actions available for each # notification. Events that are not explicitly specified show the default # set of actions. Specify an empty list if you don't want to see any # notification for the specified event: device_mounted: [browse] device_added: [mount] icon_names: # Customize the icon set used by the tray widget. Each entry # specifies a list of icon names. The first installed icon from # that list will be used. media: [drive-removable-media, media-optical] browse: [document-open, folder-open] mount: [udiskie-mount] unmount: [udiskie-unmount] unlock: [udiskie-unlock] lock: [udiskie-lock] eject: [udiskie-eject, media-eject] detach: [udiskie-detach] 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_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-1.7.3/COPYING0000644000175000017500000000214013173451745015031 0ustar thomasthomas00000000000000Copyright (c) 2010-2012 Byron Clark (c) 2013-2017 Thomas Gläßle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. udiskie-1.7.3/udiskie/0000755000175000017500000000000013214317676015436 5ustar thomasthomas00000000000000udiskie-1.7.3/udiskie/compat.py0000644000175000017500000000230113214314444017255 0ustar thomasthomas00000000000000""" Compatibility layer for python2/python3. """ from __future__ import absolute_import from __future__ import unicode_literals import sys try: # python2 basestring = basestring unicode = unicode except NameError: # python3 basestring = str unicode = str def fix_str_conversions(cls): """Enable python2/3 compatible behaviour for __str__.""" def __bytes__(self): return self.__unicode__().encode('utf-8') cls.__unicode__ = __unicode__ = cls.__str__ cls.__bytes__ = __bytes__ if sys.version_info[0] == 2: cls.__str__ = __bytes__ else: cls.__str__ = __unicode__ return cls def patch_print_unicode(): if sys.version_info[0] == 2: # When STDOUT is redirected, printing unicode on python2 will result # in a UnicodeEncodeError, because python only uses UTF-8 if printing # to a terminal. See: # - https://pythonhosted.org/kitchen/unicode-frustrations.html # - https://wiki.python.org/moin/PrintFails import locale import codecs encoding = locale.getpreferredencoding() writer = codecs.getwriter(encoding) sys.stdout = writer(sys.stdout) udiskie-1.7.3/udiskie/udisks2.py0000644000175000017500000007141313214314444017370 0ustar thomasthomas00000000000000""" UDisks2 wrapper utilities. These act as a convenience abstraction layer on the UDisks2 DBus service. Requires UDisks2 2.1.1 as described here: http://udisks.freedesktop.org/docs/latest This wraps the DBus API of Udisks2 providing a common interface with the udisks1 module. """ from __future__ import absolute_import from __future__ import unicode_literals from copy import copy, deepcopy import logging from gi.repository import GLib from .common import (Emitter, samefile, sameuuid, AttrDictView, decode_ay, BaseDevice) from .dbus import connect_service, MethodsProxy, DBusCallWithFdList, DBusCall from .locale import _ from .async_ import Coroutine, Return __all__ = ['Daemon'] def object_kind(object_path): """ Parse the kind of object from an UDisks2 object path. Example: /org/freedesktop/UDisks2/block_devices/sdb1 => device """ try: return { 'block_devices': 'device', 'drives': 'drive', 'jobs': 'job', }.get(object_path.split('/')[4]) except IndexError: return None def filter_opt(opt): """Remove ``None`` values from a dictionary.""" return {k: GLib.Variant(*v) for k, v in opt.items() if v[1] is not None} Interface = { 'Manager': 'org.freedesktop.UDisks2.Manager', 'Drive': 'org.freedesktop.UDisks2.Drive', 'DriveAta': 'org.freedesktop.UDisks2.Drive.Ata', 'MDRaid': 'org.freedesktop.UDisks2.MDRaid', 'Block': 'org.freedesktop.UDisks2.Block', 'Partition': 'org.freedesktop.UDisks2.Partition', 'PartitionTable': 'org.freedesktop.UDisks2.PartitionTable', 'Filesystem': 'org.freedesktop.UDisks2.Filesystem', 'Swapspace': 'org.freedesktop.UDisks2.Swapspace', 'Encrypted': 'org.freedesktop.UDisks2.Encrypted', 'Loop': 'org.freedesktop.UDisks2.Loop', 'Job': 'org.freedesktop.UDisks2.Job', 'ObjectManager': 'org.freedesktop.DBus.ObjectManager', 'Properties': 'org.freedesktop.DBus.Properties', } # ---------------------------------------- # Internal helper classes # ---------------------------------------- class MethodHub(object): """Provide MethodsProxies for queried interfaces of a DBus object.""" def __init__(self, object_proxy): """Initialize from (ObjectProxy).""" self._object_proxy = object_proxy def __getattr__(self, key): """Return a MethodsProxy for the requested interface.""" return MethodsProxy(self._object_proxy, Interface[key]) class PropertyHub(object): """Provide attribute accessors for queried interfaces of a DBus object.""" def __init__(self, interfaces_and_properties): """Initialize from (dict).""" self._interfaces_and_properties = interfaces_and_properties def __getattr__(self, key): """Return an AttrDictView for properties on the requested interface.""" interface = Interface[key] try: return AttrDictView(self._interfaces_and_properties[interface]) except KeyError: return PropertiesNotAvailable() class PropertiesNotAvailable(object): """Null class for properties of an unavailable interface.""" def __nonzero__(self): # python2 return False __bool__ = __nonzero__ # python3 def __getattr__(self, key): """Return None when asked for any attribute.""" return None # ---------------------------------------- # Device wrapper # ---------------------------------------- class Device(BaseDevice): """ 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 # 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.""" # NOTE: Checking for equality HintSystem==False returns False if the # property is resolved to a None value (interface not available). if self._P.Block.HintSystem == False: return True # NOTE: udisks2 seems to guess incorrectly in some cases. This # leads to HintSystem=True for unlocked devices. In order to show # the device anyway, it needs to be recursively checked if any # parent device is recognized as external. if self.is_luks_cleartext and self.luks_cleartext_slave.is_external: return True if self.is_partition and self.partition_slave.is_external: return True return False @property def is_systeminternal(self): """Check if the device is internal.""" return not self.is_external @property def drive(self): """Get wrapper to the drive containing this device.""" if self.is_drive: return self cleartext = self.luks_cleartext_slave if cleartext: return cleartext.drive if self.is_block: return self._daemon[self._P.Block.Drive] return None @property def root(self): """ Get the top level block device in the ancestry of this device. """ drive = self.drive for device in self._daemon: if device.is_drive: continue if device.is_toplevel and device.drive == drive: return device return None @property def should_automount(self): """Check if the device should be automounted.""" return bool(self._P.Block.HintAuto) @property def icon_name(self): """Return the recommended device icon name.""" return self._P.Block.HintIconName or 'drive-removable-media' @property def symbolic_icon_name(self): """Return the recommended device symbolic icon name.""" return self._P.Block.HintSymbolicIconName or 'drive-removable-media' @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), }) ) loop_support = True # ---------------------------------------- # derived properties # ---------------------------------------- @property def parent_object_path(self): return (self._P.Partition.Table or self._P.Block.CryptoBackingDevice or '/') # ---------------------------------------- # UDisks2 service wrapper # ---------------------------------------- class Daemon(Emitter): """ Listen to state changes to provide automatic synchronization. Listens to UDisks2 events. When a change occurs this class detects what has changed and triggers an appropriate event. Valid events are: - device_added / device_removed - device_unlocked / device_locked - device_mounted / device_unmounted - media_added / media_removed - device_changed / job_failed """ BusName = 'org.freedesktop.UDisks2' ObjectPath = '/org/freedesktop/UDisks2' Interface = Interface['ObjectManager'] def __iter__(self): """Iterate over all devices.""" return (self[path] for path in self.paths() if object_kind(path) in ('device', 'drive')) def __getitem__(self, object_path): return self.get(object_path) def find(self, path): """ Get a device proxy by device name or any mount path of the device. This searches through all accessible devices and compares device path as well as mount pathes. """ if isinstance(path, Device): return path for device in self: if device.is_file(path): self._log.debug(_('found device owning "{0}": "{1}"', path, device)) return device raise ValueError(_('no device found owning "{0}"', path)) def __init__(self, proxy, 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(Daemon, self).__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) def _sync(self): """Synchronize state.""" def update_objects(objects): self._objects = objects update = self._proxy.call('GetManagedObjects', '()') update.callbacks.append(update_objects) return update @classmethod @Coroutine.from_generator_function def create(cls): service = (cls.BusName, cls.ObjectPath, cls.Interface) proxy = yield connect_service(*service) version = yield cls.get_version() daemon = cls(proxy, version) yield daemon._sync() yield Return(daemon) @classmethod @Coroutine.from_generator_function def get_version(cls): service = (cls.BusName, '/org/freedesktop/UDisks2/Manager', Interface['Properties']) manager = yield connect_service(*service) version = yield DBusCall(manager._proxy, 'Get', '(ss)', ( Interface['Manager'], 'Version')) yield Return(version) @Coroutine.from_generator_function def loop_setup(self, fd, options): service = (self.BusName, '/org/freedesktop/UDisks2/Manager', Interface['Manager']) manager = yield connect_service(*service) object_path = yield DBusCallWithFdList( 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], ) yield self._sync() yield Return(self[object_path]) loop_support = True # UDisks2 interface def paths(self): return self._objects.keys() def get(self, object_path, interfaces_and_properties=None): """Create a Device instance from object path.""" # check this before creating the DBus object for more # controlled behaviour: if not interfaces_and_properties: interfaces_and_properties = self._objects.get(object_path) if not interfaces_and_properties: return None property_hub = PropertyHub(interfaces_and_properties) method_hub = MethodHub( self._proxy.object.bus.get_object(object_path)) return Device(self, object_path, property_hub, method_hub) def trigger(self, event, device, *args): self._log.debug(_("+++ {0}: {1}", event, device)) super(Daemon, self).trigger(event, device, *args) # add objects / interfaces def _interfaces_added(self, object_path, interfaces_and_properties): """Internal method.""" added = object_path not in self._objects self._objects.setdefault(object_path, {}) old_state = copy(self._objects[object_path]) self._objects[object_path].update(interfaces_and_properties) new_state = self._objects[object_path] if added: kind = object_kind(object_path) if kind in ('device', 'drive'): self.trigger('device_added', self[object_path]) if Interface['Block'] in interfaces_and_properties: slave = self[object_path].luks_cleartext_slave if slave: if not self._has_job(slave.object_path, 'device_unlocked'): self.trigger('device_unlocked', slave) if not added: self.trigger('device_changed', self.get(object_path, old_state), self.get(object_path, new_state)) # remove objects / interfaces def _detect_toggle(self, property_name, old, new, add_name, del_name): old_valid = old and bool(getattr(old, property_name)) new_valid = new and bool(getattr(new, property_name)) if add_name and new_valid and not old_valid: if not self._has_job(old.object_path, add_name): self.trigger(add_name, new) elif del_name and old_valid and not new_valid: if not self._has_job(old.object_path, del_name): self.trigger(del_name, new) def _has_job(self, device_path, event_name): job_interface = Interface['Job'] for path, interfaces in self._objects.items(): try: job = interfaces[job_interface] job_objects = job['Objects'] job_operation = job['Operation'] job_action = self._action_by_operation[job_operation] job_event = self._event_by_action[job_action] if event_name == job_event and device_path in job_objects: return True except KeyError: pass return False def _interfaces_removed(self, object_path, interfaces): """Internal method.""" old_state = copy(self._objects[object_path]) for interface in interfaces: del self._objects[object_path][interface] new_state = self._objects[object_path] if Interface['Drive'] in interfaces: self._detect_toggle( 'has_media', self.get(object_path, old_state), self.get(object_path, new_state), None, 'media_removed') if Interface['Block'] in interfaces: slave = self.get(object_path, old_state).luks_cleartext_slave if slave: if not self._has_job(slave.object_path, 'device_locked'): self.trigger('device_locked', slave) if self._objects[object_path]: self.trigger('device_changed', self.get(object_path, old_state), self.get(object_path, new_state)) else: del self._objects[object_path] if object_kind(object_path) in ('device', 'drive'): self.trigger( 'device_removed', self.get(object_path, old_state)) # change interface properties def _properties_changed(self, object_path, interface_name, changed_properties, invalidated_properties): """ Internal method. Called when a DBusProperty of any managed object changes. """ # update device state: old_state = deepcopy(self._objects[object_path]) for property_name in invalidated_properties: del self._objects[object_path][interface_name][property_name] for key, value in changed_properties.items(): self._objects[object_path][interface_name][key] = value new_state = self._objects[object_path] # detect changes and trigger events: if interface_name == Interface['Drive']: self._detect_toggle( 'has_media', self.get(object_path, old_state), self.get(object_path, new_state), 'media_added', 'media_removed') elif interface_name == Interface['Filesystem']: self._detect_toggle( 'is_mounted', self.get(object_path, old_state), self.get(object_path, new_state), 'device_mounted', 'device_unmounted') self.trigger('device_changed', self.get(object_path, old_state), self.get(object_path, new_state)) # There is no PropertiesChanged for the crypto device when it is # unlocked/locked in UDisks2. Instead, this is handled by the # InterfaceAdded/Removed handlers. # jobs _action_by_operation = { 'filesystem-mount': 'mount', 'filesystem-unmount': 'unmount', 'encrypted-unlock': 'unlock', 'encrypted-lock': 'lock', 'power-off-drive': 'detach', 'eject-media': 'eject', } _event_by_action = { 'mount': 'device_mounted', 'unmount': 'device_unmounted', 'unlock': 'device_unlocked', 'lock': 'device_locked', 'eject': 'media_removed', 'detach': 'device_removed', } _check_action_success = { 'mount': lambda dev: dev.is_mounted, 'unmount': lambda dev: not dev or not dev.is_mounted, 'unlock': lambda dev: dev.is_unlocked, 'lock': lambda dev: not dev or not dev.is_unlocked, 'detach': lambda dev: not dev, 'eject': lambda dev: not dev or not dev.has_media, } def _job_completed(self, job_name, success, message): """ Internal method. Called when a job of a long running task completes. """ job = self._objects[job_name][Interface['Job']] action = self._action_by_operation.get(job['Operation']) if not action: return # We only handle events, which are associated to exactly one object: object_path, = job['Objects'] device = self[object_path] if success: # It rarely happens, but sometimes UDisks posts the # Job.Completed event before PropertiesChanged, so we have to # check if the operation has been carried out yet: if self._check_action_success[action](device): event_name = self._event_by_action[action] self.trigger(event_name, device) else: self.trigger('job_failed', device, action, message) udiskie-1.7.3/udiskie/__init__.py0000644000175000017500000000070613214317257017545 0ustar thomasthomas00000000000000# encoding: utf-8 from __future__ import unicode_literals __title__ = 'udiskie' __version__ = '1.7.3' __summary__ = 'Removable disk automounter for udisks' __uri__ = 'https://github.com/coldfix/udiskie' __author__ = 'Byron Clark' __author_email__ = 'byron@theclarkfamily.name' __maintainer__ = 'Thomas Gläßle' __maintainer_email__ = 't_glaessle@gmx.de' __license__ = 'MIT' __copyright__ = '(c) 2010-2012 Byron Clark, (c) 2013-2017 Thomas Gläßle' udiskie-1.7.3/udiskie/automount.py0000644000175000017500000000265613214314444020042 0ustar thomasthomas00000000000000""" Automount utility. """ from __future__ import absolute_import from __future__ import unicode_literals from .common import DaemonBase __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(Mounter(udisks=Daemon())) """ def __init__(self, mounter): """ Store mounter as member variable and connect to the underlying udisks. :param Mounter mounter: mounter object """ self._mounter = mounter self.events = { 'device_changed': self.device_changed, 'device_added': self._mounter.auto_add, 'media_added': self._mounter.auto_add, } def device_changed(self, old_state, new_state): """ Mount newly mountable devices. :param Device old_state: before change :param Device new_state: after change """ # udisks2 sometimes adds empty devices and later updates them - which # makes is_external become true 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._mounter.auto_add(new_state) udiskie-1.7.3/udiskie/dbus.py0000644000175000017500000003455113214314444016743 0ustar thomasthomas00000000000000""" Common DBus utilities. """ from __future__ import absolute_import from __future__ import unicode_literals import sys from functools import partial from gi.repository import Gio from gi.repository import GLib from .async_ import Async, Coroutine, Return from .common import format_exc __all__ = [ 'InterfaceProxy', 'PropertiesProxy', 'ObjectProxy', 'BusProxy', 'connect_service', 'MethodsProxy', ] if sys.version_info >= (3,): unpack_variant = GLib.Variant.unpack else: dict_type = GLib.VariantType.new('a{?*}') def unpack_variant(v): """Unpack a GLib.Variant.""" if v.get_type_string() in 'sog': return v.get_string().decode('utf-8') t = v.get_type() if t.is_basic(): return v.unpack() elems = [v.get_child_value(i) for i in range(v.n_children())] if t.is_subtype_of(dict_type): return dict(map(unpack_variant, elems)) if t.is_tuple(): return tuple(map(unpack_variant, elems)) if t.is_array(): return list(map(unpack_variant, elems)) if t.is_dict_entry(): return list(map(unpack_variant, elems)) # The following cases should never occur for DBus data: if t.is_variant(): return unpack_variant(elems[0]) if t.is_maybe(): return unpack_variant(elems[0]) if elems else None raise ValueError("Unknown variant type: {}!".format(v.get_type_string())) class DBusCall(Async): """ Asynchronously call a DBus method. """ def __init__(self, proxy, method_name, signature, args, flags=0, timeout_msec=-1): """ Asynchronously call the specified method on a DBus proxy object. :param Gio.DBusProxy proxy: :param str method_name: :param str signature: :param tuple args: :param int flags: :param int timeout_msec: """ cancellable = None user_data = None proxy.call( method_name, GLib.Variant(signature, tuple(args)), flags, timeout_msec, cancellable, self._callback, user_data, ) def _callback(self, proxy, result, user_data): """ Handle call result. :param Gio.DBusProxy proxy: :param Gio.AsyncResult result: :param user_data: unused """ try: value = proxy.call_finish(result) except Exception as e: self.errback(e, format_exc()) else: self.callback(*unpack_variant(value)) class DBusCallWithFdList(Async): """ Asynchronously call a DBus method. """ def __init__(self, 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 int flags: :param int timeout_msec: """ cancellable = None user_data = 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, self._callback, user_data, ) def _callback(self, proxy, result, user_data): """ Handle call result. :param Gio.DBusProxy proxy: :param Gio.AsyncResult result: :param user_data: unused """ try: value, fds = proxy.call_with_unix_fd_list_finish(result) except Exception as e: self.errback(e, format_exc()) else: self.callback(*unpack_variant(value)) class InterfaceProxy(object): """ DBus proxy object for a specific interface. Provides attribute accessors to properties and methods of a DBus interface on a DBus object. :ivar str object_path: object path of the DBus object :ivar PropertiesProxy property: attribute access to DBus properties :ivar Gio.DBusProxy method: attribute access to DBus methods :ivar Gio.DBusProxy _proxy: underlying proxy object """ def __init__(self, proxy): """ Initialize property and method attribute accessors for the interface. :param Gio.DBusProxy proxy: accessed object :param str interface: accessed interface """ self._proxy = proxy self.object_path = proxy.get_object_path() @property def object(self): """ Get a proxy for the underlying object. :rtype: ObjectProxy """ proxy = self._proxy return ObjectProxy(proxy.get_connection(), proxy.get_name(), proxy.get_object_path()) def connect(self, event, handler): """ Connect to a DBus signal. :param str event: event name :param handler: callback :returns: subscription id :rtype: int """ interface = self._proxy.get_interface_name() return self.object.connect(interface, event, handler) def call(self, method_name, signature='()', *args): return DBusCall(self._proxy, method_name, signature, args) class PropertiesProxy(InterfaceProxy): Interface = 'org.freedesktop.DBus.Properties' def __init__(self, proxy, interface_name=None): super(PropertiesProxy, self).__init__(proxy) self.interface_name = interface_name def GetAll(self, interface_name=None): return self.call('GetAll', '(s)', interface_name or self.interface_name) class ObjectProxy(object): """ Simple proxy class for a DBus object. :param Gio.DBusConnection connection: :param str bus_name: :param str object_path: """ def __init__(self, connection, bus_name, object_path): """ Initialize member variables. :ivar Gio.DBusConnection connection: :ivar str bus_name: :ivar str object_path: This performs no IO at all. """ self.connection = connection self.bus_name = bus_name self.object_path = object_path def _get_interface(self, name): """ Get a Gio native interface proxy for this Dbus object. :param str name: interface name :returns: a proxy object for the other interface :rtype: Gio.DBusProxy """ return DBusProxyNew( self.connection, Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, info=None, name=self.bus_name, object_path=self.object_path, interface_name=name, ) @Coroutine.from_generator_function def get_interface(self, name): """ Get an interface proxy for this Dbus object. :param str name: interface name :returns: a proxy object for the other interface :rtype: InterfaceProxy """ proxy = yield self._get_interface(name) yield Return(InterfaceProxy(proxy)) @Coroutine.from_generator_function def get_property_interface(self, interface_name=None): proxy = yield self._get_interface(PropertiesProxy.Interface) yield Return(PropertiesProxy(proxy, interface_name)) @property def bus(self): """ Get a proxy object for the underlying bus. :rtype: BusProxy """ return BusProxy(self.connection, self.bus_name) def connect(self, interface, event, handler): """ Connect to a DBus signal. :param str interface: interface name :param str event: event name :param handler: callback :returns: subscription id :rtype: int """ object_path = self.object_path return self.bus.connect(interface, event, object_path, handler) @Coroutine.from_generator_function def call(self, interface_name, method_name, signature='()', *args): proxy = yield self.get_interface(interface_name) result = yield proxy.call(method_name, signature, *args) yield Return(result) class DBusCallback(object): def __init__(self, handler): """Store reference to handler.""" self._handler = handler def __call__(self, connection, sender_name, object_path, interface_name, signal_name, parameters, *user_data): """Call handler unpacked signal parameters.""" return self._handler(*unpack_variant(parameters)) class DBusCallbackWithObjectPath(object): def __init__(self, handler): """Store reference to handler.""" self._handler = handler def __call__(self, connection, sender_name, object_path, interface_name, signal_name, parameters, *user_data): """Call handler with object_path and unpacked signal parameters.""" return self._handler(object_path, *unpack_variant(parameters)) class BusProxy(object): """ Simple proxy class for a connected bus. :ivar Gio.DBusConnection connection: :ivar str bus_name: """ def __init__(self, connection, bus_name): """ Initialize member variables. :param Gio.DBusConnection connection: :param str bus_name: This performs IO at all. """ self.connection = connection self.bus_name = bus_name def get_object(self, object_path): """ Get a object representing the specified object. :param str object_path: object path :returns: a simple representative for the object :rtype: ObjectProxy """ return ObjectProxy(self.connection, self.bus_name, object_path) def connect(self, interface, event, object_path, handler): """ Connect to a DBus signal. :param str interface: interface name :param str event: event name :param str object_path: object path or ``None`` :param handler: callback """ if object_path: callback = DBusCallback(handler) else: callback = DBusCallbackWithObjectPath(handler) return self.connection.signal_subscribe( self.bus_name, interface, event, object_path, None, Gio.DBusSignalFlags.NONE, callback, None, ) def disconnect(self, subscription_id): """ Disconnect a DBus signal subscription. """ self.connection.signal_unsubscribe(subscription_id) class DBusProxyNew(Async): """ Asynchronously call a DBus method. """ def __init__(self, connection, flags, info, name, object_path, interface_name): """ Asynchronously call the specified method on a DBus proxy object. """ cancellable = None user_data = None Gio.DBusProxy.new( connection, flags, info, name, object_path, interface_name, cancellable, self._callback, user_data, ) def _callback(self, proxy, result, user_data): """ Handle call result. :param Gio.DBusProxy proxy: :param Gio.AsyncResult result: :param user_data: unused """ try: value = Gio.DBusProxy.new_finish(result) if value is None: raise RuntimeError("Failed to connect DBus object!") except Exception as e: self.errback(e, format_exc()) else: self.callback(value) class DBusProxyNewForBus(Async): """ Asynchronously call a DBus method. """ def __init__(self, bus_type, flags, info, name, object_path, interface_name): """ Asynchronously call the specified method on a DBus proxy object. """ cancellable = None user_data = None Gio.DBusProxy.new_for_bus( bus_type, flags, info, name, object_path, interface_name, cancellable, self._callback, user_data, ) def _callback(self, proxy, result, user_data): """ Handle call result. :param Gio.DBusProxy proxy: :param Gio.AsyncResult result: :param user_data: unused """ try: value = Gio.DBusProxy.new_for_bus_finish(result) if value is None: raise RuntimeError("Failed to connect DBus object!") except Exception as e: self.errback(e, format_exc()) else: self.callback(value) @Coroutine.from_generator_function def connect_service(bus_name, object_path, interface): """ Connect to the service object on DBus. :returns: new proxy object for the service :rtype: InterfaceProxy :raises BusException: if unable to connect to service. """ proxy = yield DBusProxyNewForBus( Gio.BusType.SYSTEM, Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, info=None, name=bus_name, object_path=object_path, interface_name=interface, ) yield Return(InterfaceProxy(proxy)) class MethodsProxy(object): """Provide methods as attributes for one interface of a DBus object.""" def __init__(self, object_proxy, interface_name): """Initialize from (ObjectProxy, str).""" self._object_proxy = object_proxy self._interface_name = interface_name def __getattr__(self, name): """Get a proxy for the specified method on this interface.""" return partial(self._object_proxy.call, self._interface_name, name) udiskie-1.7.3/udiskie/locale.py0000644000175000017500000000224113214314444017234 0ustar thomasthomas00000000000000""" I18n utilities. """ from __future__ import absolute_import from __future__ import unicode_literals import gettext __all__ = ['_'] class Translator(object): """ Simple translation and message formatting utility. """ @classmethod def create(cls, domain, localedir=None, languages=None): """ Create a new translator for the given domain. Arguments are as in ``gettext.translation``. """ t = gettext.translation(domain, localedir, languages, fallback=True) try: # on python2 we want the unicode version: g = t.ugettext except AttributeError: # which is the default in python3: g = t.gettext return cls(g) def __init__(self, gettext): """Initialize a translator with the given gettext function.""" self._gettext = gettext def __call__(self, text, *args, **kwargs): """Translate and then and format the text with ``str.format``.""" msg = self._gettext(text) if args or kwargs: return msg.format(*args, **kwargs) else: return msg _ = Translator.create('udiskie') udiskie-1.7.3/udiskie/config.py0000644000175000017500000002050413214314444017244 0ustar thomasthomas00000000000000""" Config utilities. For an example config file, see the manual. If you don't have the man page installed, a raw version is available in doc/udiskie.8.txt. """ from __future__ import absolute_import from __future__ import unicode_literals import logging import os import fnmatch from .common import exc_message from .compat import basestring, fix_str_conversions 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, basestring) and isinstance(pattern, basestring): return fnmatch.fnmatch(value.lower(), pattern.lower()) return lower(value) == lower(pattern) def yaml_load(stream): """Load YAML document, but load all strings as unicode on py2.""" import yaml class UnicodeLoader(yaml.SafeLoader): pass UnicodeLoader.add_constructor( yaml.resolver.BaseResolver.DEFAULT_SCALAR_TAG, UnicodeLoader.construct_scalar) return yaml.load(stream, UnicodeLoader) @fix_str_conversions class DeviceFilter(object): """Associate a certain value to matching devices.""" VALID_PARAMETERS = [ 'is_drive', 'is_block', 'is_partition_table', 'is_partition', 'is_filesystem', 'is_luks', 'is_loop', 'is_toplevel', 'is_detachable', 'is_ejectable', 'has_media', 'device_file', 'device_presentation', 'device_id', 'id_usage', 'is_crypto', 'is_ignored', 'id_type', 'id_label', 'id_uuid', 'is_luks_cleartext', 'is_external', 'is_systeminternal', 'is_mounted', 'mount_paths', '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 an instance. :param dict match: device attributes :param list value: value """ 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, basestring): 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: self._values['keyfile'] = match.pop('keyfile') # 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 matches this filter. :param Device device: device to be checked """ 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 associated value. :param Device device: matched device If :meth:`match` is False for the device, the return value of this method is undefined. """ self._log.debug(_('{0}(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): """Parse the MountOptions filter from the config item.""" config_item.setdefault('options', None) super(MountOptions, self).__init__(config_item) class IgnoreDevice(DeviceFilter): """Associate a boolean ignore flag to matched devices.""" def __init__(self, config_item): """Parse the IgnoreDevice filter from the config item.""" config_item.setdefault('ignore', True) super(IgnoreDevice, self).__init__(config_item) def match_config(filters, device, kind, default): """ Matches devices against multiple :class:`DeviceFilter`s. :param default: default value :param list filters: device filters :param Device device: device to be mounted :returns: value of the first matching filter """ if device is None: return default matches = (f.value(kind, device) for f in filters if f.has_value(kind) and f.match(device)) return next(matches, default) class Config(object): """Udiskie config in memory representation.""" def __init__(self, data): """ Initialize with preparsed data object. :param ConfigParser data: config file accessor """ self._data = data or {} @classmethod def default_pathes(cls): """ Return the default config file pathes. :rtype: list """ try: from xdg.BaseDirectory import xdg_config_home as config_home except ImportError: config_home = os.path.expanduser('~/.config') return [os.path.join(config_home, 'udiskie', 'config.yml'), os.path.join(config_home, 'udiskie', 'config.json')] @classmethod def from_file(cls, path=None): """ Read config file. :param str path: YAML config file name :returns: configuration object :rtype: Config :raises IOError: if the path does not exist """ # None => use default if path is None: for path in cls.default_pathes(): try: return cls.from_file(path) except IOError as e: logging.getLogger(__name__).debug( _("Failed to read config file: {0}", exc_message(e))) except ImportError as e: logging.getLogger(__name__).warn( _("Failed to read {0!r}: {1}", path, exc_message(e))) return cls({}) # False/'' => no config if not path: return cls({}) if os.path.splitext(path)[1].lower() == '.json': from json import load else: load = yaml_load with open(path) as f: return cls(load(f)) @property def 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() udiskie-1.7.3/udiskie/depend.py0000644000175000017500000000343713214314444017244 0ustar thomasthomas00000000000000""" Make sure that the correct versions of gobject introspection dependencies are installed. """ from __future__ import absolute_import from __future__ import unicode_literals import os import logging from gi import require_version from .common import check_call from .locale import _ require_version('Gio', '2.0') require_version('GLib', '2.0') def check_version(package, version): return check_call(ValueError, require_version, package, version) _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-1.7.3/udiskie/cli.py0000644000175000017500000006363413214314444016561 0ustar thomasthomas00000000000000""" Command line interface logic. The application classes in this module are installed as executables via setuptools entry points. """ from __future__ import absolute_import from __future__ import unicode_literals from __future__ import print_function # import udiskie.depend first - for side effects! from .depend import has_Notify, has_Gtk, _in_X import sys import inspect import logging.config import traceback from docopt import docopt, DocoptExit from gi.repository import GLib import udiskie import udiskie.config import udiskie.mount import udiskie.compat from .async_ import AsyncList, Coroutine, Return, RunForever from .common import extend, str2unicode, ObjDictView from .locale import _ __all__ = [ 'Daemon', 'Mount', 'Umount', ] def deprecation_warning(text): """Show a deprecation warning.""" # NOTE: not using `warnings.warn(text, DeprecationWarning)`, because that # requires starting the python interpreter to be started with `-Wd`, which # in turn shows more warnings about removal of Gtk.StatusIcon. log = logging.getLogger("udiskie.DeprecationWarning") log.warning(_("Deprecation warning: {}", text)) @Coroutine.from_generator_function def get_backend(version=None): """ Return UDisks service. :param int version: requested UDisks backend version :returns: UDisks service wrapper object :raises GLib.GError: if unable to connect to UDisks dbus service. :raises ValueError: if the version is invalid If ``version`` has a false truth value, try to connect to UDisks2 and fall back to UDisks1 if not available. """ if not version: try: daemon = yield get_backend(2) except GLib.GError: log = logging.getLogger(__name__) log.warning(_('Failed to connect UDisks2 dbus service..\n' 'Falling back to UDisks1.')) daemon = yield get_backend(1) elif version == 1: import udiskie.udisks1 daemon = yield udiskie.udisks1.Daemon.create() deprecation_warning(_( 'Using UDisks1. Support will be discontinued ' 'in the next major version of udiskie.')) elif version == 2: import udiskie.udisks2 daemon = yield udiskie.udisks2.Daemon.create() else: raise ValueError(_("UDisks version not supported: {0}!", version)) yield Return(daemon) class Choice(object): """Mapping of command line arguments to option values.""" def __init__(self, mapping): """Set mapping between arguments and values.""" self._mapping = mapping def _check(self, args): """Exit in case of multiple exclusive arguments.""" if sum(bool(args[arg]) for arg in self._mapping) > 1: raise DocoptExit(_('These options are mutually exclusive: {0}', ', '.join(self._mapping))) def __call__(self, args): """Get the option value from the parsed arguments.""" self._check(args) for arg, val in self._mapping.items(): if args[arg] not in (None, False): return val def Switch(name): """Negatable option.""" return Choice({'--' + name: True, '--no-' + name: False}) class Value(object): """Option which is given as value of a command line argument.""" def __init__(self, name): """Set argument name.""" self._name = name def __call__(self, args): """Get the value of the command line argument.""" return str2unicode(args[self._name]) class OptionalValue(object): def __init__(self, name): """Set argument name.""" self._name = name self._choice = Switch(name.lstrip('-')) def __call__(self, args): """Get the value of the command line argument.""" return self._choice(args) and str2unicode(args[self._name]) class SelectLevel(logging.Filter): def __init__(self, level): self.level = level def filter(self, record): return record.levelno == self.level class _EntryPoint(object): """ Abstract base class for program entry points. Concrete implementations need to implement :meth:`run` and extend :meth:`finalize_options` to be usable with :meth:`main`. Furthermore the docstring of any concrete implementation must be usable with docopt. :ivar:`name` must be set to the name of the CLI utility. """ option_defaults = { 'log_level': logging.INFO, 'udisks_version': None, } option_rules = { 'log_level': Choice({ '--verbose': logging.DEBUG, '--quiet': logging.ERROR}), 'udisks_version': Choice({ '--udisks-auto': 0, '--use-udisks1': 1, '--use-udisks2': 2}), } usage_remarks = _(""" Note, that the options in the individual groups are mutually exclusive. The config file can be a JSON or preferrably a YAML file. For an example, see the MAN page (or doc/udiskie.8.txt in the repository). """) def __init__(self, argv=None): """ Parse command line options, read config and initialize members. :param list argv: command line parameters """ udiskie.compat.patch_print_unicode() # parse program options (retrieve log level and config file name): args = docopt(self.usage, version=self.name + ' ' + self.version) default_opts = self.option_defaults program_opts = self.program_options(args) # initialize logging configuration: log_level = program_opts.get('log_level', default_opts['log_level']) 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 self.exit_status = 0 if sys.version_info < (3,5): deprecation_warning(_( "Running on python {}.{}. The next major version of udiskie " "will require at least python 3.5!", *sys.version_info[:2])) def program_options(self, args): """ Fully initialize Daemon object. :param dict args: arguments as parsed by docopt :returns: options from command line :rtype: dict """ options = {} for name, rule in self.option_rules.items(): val = rule(args) if val is not None: options[name] = val return options @classmethod def main(cls, argv=None): """ Run program. :param list argv: command line parameters :returns: program exit code :rtype: int """ return cls(argv).run() @property def version(self): """Get the version from setuptools metadata.""" return udiskie.__version__ @property def usage(self): """Get the full usage string.""" return inspect.cleandoc(self.__doc__ + self.usage_remarks) @property def name(self): """Get the name of the CLI utility.""" raise NotImplementedError() def _init(self): """ Fully initialize Daemon object. :returns: the main application task :rtype: Async """ raise NotImplementedError() def run(self): """ Run the main loop. :returns: exit code :rtype: int """ self.mainloop = GLib.MainLoop() self._start_async_tasks() try: self.mainloop.run() return self.exit_status except KeyboardInterrupt: return 1 @Coroutine.from_generator_function def _start_async_tasks(self): """Start asynchronous operations.""" try: self.udisks = yield get_backend(self.options['udisks_version']) results = yield self._init() if not all(results): self.exit_status = 1 except Exception: self.exit_status = 1 # Print the stack trace only up to the current level: traceback.print_exc() self.mainloop.quit() class Component(object): 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 -0, --udisks-auto Auto discover UDisks version -1, --use-udisks1 Use UDisks1 as backend -2, --use-udisks2 Use UDisks2 as backend -h, --help Show this help -V, --version Show version information Daemon options: -a, --automount Automount new devices -A, --no-automount Disable automounting -n, --notify Show popup notifications -N, --no-notify Disable notifications -t, --tray Show tray icon -s, --smart-tray Auto hide tray icon -T, --no-tray Disable tray icon -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 -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 Deprecated options: -f PROGRAM, --file-manager PROGRAM Set program for browsing -F, --no-file-manager Disable browsing """ name = 'udiskie' option_defaults = extend(_EntryPoint.option_defaults, { 'automount': True, 'notify': True, 'tray': False, 'menu': 'flat', 'appindicator': False, 'file_manager': 'xdg-open', '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): """Implements _EntryPoint._init.""" 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']) cache = None if options['password_cache'] is not False: import udiskie.cache timeout = int(options['password_cache']) * 60 cache = udiskie.cache.PasswordCache(timeout) self.mounter = udiskie.mount.Mounter( config=config.device_config, prompt=prompt, browser=browser, cache=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 has_Gtk(3) 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 = Component(self._load_automounter) 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(RunForever) if options['automount']: self.automounter.activate() tasks.append(self.mounter.add_all()) return AsyncList(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 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) 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): import udiskie.automount return udiskie.automount.AutoMounter(self.mounter) class Mount(_EntryPoint): """ udiskie-mount: a user-level command line utility for mounting. Usage: udiskie-mount [options] (-a | DEVICE...) udiskie-mount (--help | --version) General options: -c FILE, --config=FILE Set config file -C, --no-config Don't use config file -v, --verbose Increase verbosity (DEBUG) -q, --quiet Decrease verbosity -0, --udisks-auto Auto discover UDisks version -1, --use-udisks1 Use UDisks1 as backend -2, --use-udisks2 Use UDisks2 as backend -h, --help Show this help -V, --version Show version information Mount options: -a, --all Mount all handleable devices -r, --recursive Recursively mount partitions -R, --no-recursive Disable recursive mounting -o OPTIONS, --options OPTIONS Mount option list -p COMMAND, --password-prompt COMMAND Command for password retrieval -P, --no-password-prompt Disable unlocking """ name = 'udiskie-mount' option_defaults = extend(_EntryPoint.option_defaults, { 'recursive': 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): """Implements _EntryPoint._init.""" import udiskie.prompt config = self.config options = self.options device_config = config.device_config if options['options']: device_config._filters.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=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 AsyncList(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 -0, --udisks-auto Auto discover UDisks version -1, --use-udisks1 Use UDisks1 as backend -2, --use-udisks2 Use UDisks2 as backend -h, --help Show this help -V, --version Show version information Unmount options: -a, --all Unmount all handleable devices -d, --detach Power off drive if possible -D, --no-detach Don't power off drive -e, --eject Eject media from device if possible -E, --no-eject Don't eject media -f, --force Force removal (recursive unmounting) -F, --no-force Don't force removal -l, --lock Lock device after unmounting -L, --no-lock Don't lock device """ name = 'udiskie-umount' option_defaults = extend(_EntryPoint.option_defaults, { 'detach': 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): """Implements _EntryPoint._init.""" 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 AsyncList(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 -0, --udisks-auto Auto discover UDisks version -1, --use-udisks1 Use UDisks1 as backend -2, --use-udisks2 Use UDisks2 as backend -h, --help Show this help -V, --version Show version information Unmount options: -a, --all 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. """ name = 'udiskie-info' 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): """Implements _EntryPoint._init.""" 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: from string import Formatter formatter = Formatter() def format_output(device): view = ObjDictView(device, DeviceFilter.VALID_PARAMETERS) return formatter.vformat(output, (), 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 AsyncList([]) udiskie-1.7.3/udiskie/tray.py0000644000175000017500000003436513214314444016770 0ustar thomasthomas00000000000000""" Tray icon for udiskie. """ from __future__ import absolute_import from __future__ import unicode_literals from gi.repository import Gio from gi.repository import Gtk from .async_ import Async, Coroutine, Return from .common import setdefault, DaemonBase from .compat import basestring from .locale import _ from .mount import Action, prune_empty_node from .prompt import Dialog __all__ = ['UdiskieMenu', 'TrayIcon'] class MenuFolder(object): 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(object): """Encapsulates the responsibility to load icons.""" _icon_names = { 'media': [ 'drive-removable-media-usb-pendrive', 'drive-removable-media-usb', 'drive-removable-media', 'media-optical', 'media-flash', ], 'browse': ['document-open', 'folder-open'], 'mount': ['udiskie-mount'], 'unmount': ['udiskie-unmount'], 'unlock': ['udiskie-unlock'], 'lock': ['udiskie-lock'], 'eject': ['udiskie-eject', 'media-eject'], 'detach': ['udiskie-detach'], 'quit': ['application-exit'], 'forget_password': ['edit-delete'], '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, basestring): self._icon_names[k] = [v] def get_icon_name(self, icon_id): """ Try to determine the name of the icon that should be used. :param str icon_id: udiskie internal icon id :returns: the icon name to be used :rtype: str """ 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, size): """ Load icon dynamically. :param str icon_id: udiskie internal icon id :param GtkIconSize size: requested size :returns: the loaded icon :rtype: Gtk.Image """ return Gtk.Image.new_from_gicon(self.get_gicon(icon_id), size) def get_gicon(self, icon_id): """ Lookup the GTK icon name corresponding to the specified internal id. :param str icon_id: udiskie internal icon id :param GtkIconSize size: requested size :returns: the loaded icon :rtype: Gio.Icon """ return Gio.ThemedIcon.new_from_names(self._icon_names[icon_id]) class UdiskieMenu(object): """ Builder for udiskie menus. Objects of this class generate action menus when being called. """ def __init__(self, daemon, icons, actions, flat=True): """ Initialize a new menu maker. :param object mounter: mount operation provider :param Icons icons: icon provider :param DeviceActions actions: device actions discovery :returns: a new menu maker :rtype: cls Required keys for the ``_labels``, ``_menu_icons`` and ``actions`` dictionaries are: - browse Open mount location - mount Mount a device - unmount Unmount a device - unlock Unlock a LUKS device - lock Lock a LUKS device - eject Eject a drive - detach Detach (power down) a drive - quit Exit the application NOTE: If using a main loop other than ``Gtk.main`` the 'quit' action must be customized. """ self._icons = icons self._daemon = daemon self._mounter = daemon.mounter self._actions = actions self._quit_action = daemon.mainloop.quit self.flat = flat def __call__(self, menu, extended): """ Populate the menu with udiskie mount operations. :param Gtk.Menu menu: """ # 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()) if self._mounter.udisks.loop_support: menu.append(self._menuitem( _('Mount disc image'), self._icons.get_icon('losetup', Gtk.IconSize.MENU), lambda _: self._losetup() )) menu.append(Gtk.SeparatorMenuItem()) menu.append(self._menuitem( _("Enable automounting"), icon=None, onclick=lambda _: self._daemon.automounter.toggle(), checked=self._daemon.automounter.active, )) 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() )) @Coroutine.from_generator_function def _losetup(self): dialog = Gtk.FileChooserDialog( _('Open disc image'), None, Gtk.FileChooserAction.OPEN, (_('Open'), Gtk.ResponseType.OK, _('Cancel'), Gtk.ResponseType.CANCEL)) dialog.show_all() response = yield Dialog(dialog) dialog.hide() if response != Gtk.ResponseType.OK: return yield self._mounter.losetup(dialog.get_filename()) def detect(self): """ Detect all currently known devices. :returns: root of device hierarchy :rtype: Device """ root = self._actions.detect() prune_empty_node(root, set()) return root def _create_menu(self, items): """ Create a menu from the given node. :param list items: list of menu items :returns: a new menu object holding all items of the node :rtype: Gtk.Menu """ menu = Gtk.Menu() self._create_menu_items(menu, items) return menu def _create_menu_items(self, menu, items): def make_action_callback(node): return 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 :rtype: 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] if flat: items.extend(node.methods) else: items.append(MenuSection(None, node.methods)) return items class TrayIcon(object): """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 = Async() menumaker._quit_action = self.destroy def destroy(self): self.show(False) self.task.callback() def _create_statusicon(self): """Return a new Gtk.StatusIcon.""" statusicon = Gtk.StatusIcon() statusicon.set_from_gicon(self._icons.get_gicon('media')) statusicon.set_tooltip_text(_("udiskie")) return statusicon @property def visible(self): """Return visibility state of icon.""" return bool(self._conn_left) def show(self, show=True): """Show or hide the tray icon.""" if show and not self.visible: self._show() if not show and self.visible: self._hide() def _show(self): """Show the tray icon.""" if not self._icon: self._icon = self._create_statusicon() widget = self._icon widget.set_visible(True) self._conn_left = widget.connect("activate", self._activate) self._conn_right = widget.connect("popup-menu", self._popup_menu) def _hide(self): """Hide the tray icon.""" self._icon.set_visible(False) self._icon.disconnect(self._conn_left) self._icon.disconnect(self._conn_right) self._conn_left = None self._conn_right = None def create_context_menu(self, 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(UdiskieStatusIcon, self).activate() self.update() def deactivate(self): super(UdiskieStatusIcon, self).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-1.7.3/udiskie/common.py0000644000175000017500000002633413214314444017276 0ustar thomasthomas00000000000000""" Common DBus utilities. """ from __future__ import absolute_import from __future__ import unicode_literals import os.path import traceback from .compat import fix_str_conversions __all__ = [ 'wraps', 'check_call', 'Emitter', 'samefile', 'sameuuid', 'setdefault', 'extend', 'cachedproperty', 'decode_ay', # dealing with py2 strings: 'str2unicode', 'exc_message', 'format_exc', ] try: from black_magic.decorator import wraps except ImportError: from functools import wraps def check_call(exc_type, func, *args): try: func(*args) return True except exc_type: return False class Emitter(object): """ Event emitter class. Provides a simple event engine featuring a known finite set of events. """ def __init__(self, event_names=(), *args, **kwargs): """ Initialize with empty lists of event handlers. :param iterable event_names: names of known events. """ super(Emitter, self).__init__(*args, **kwargs) self._event_handlers = {} for evt in event_names: self._event_handlers[evt] = [] def trigger(self, event, *args): """ Trigger event handlers. :param str event: event name :param *args: event parameters """ for handler in self._event_handlers[event]: handler(*args) def connect(self, event, handler): """ Connect an event handler. :param str event: event name :param callable handler: event handler """ self._event_handlers[event].append(handler) def disconnect(self, event, handler): """ Disconnect an event handler. :param str event: event name :param callable handler: event handler """ self._event_handlers[event].remove(handler) def samefile(a, b): """Check if two pathes represent the same file.""" try: return os.path.samefile(a, b) except OSError: return os.path.normpath(a) == os.path.normpath(b) def sameuuid(a, b): """Compare two UUIDs.""" return a and b and a.lower() == b.lower() def setdefault(self, other): """ Merge two dictionaries like .update() but don't overwrite values. :param dict self: updated dict :param dict other: default values to be inserted """ for k, v in other.items(): self.setdefault(k, v) def extend(a, b): """Merge two dicts and return a new dict. Much like subclassing works.""" res = a.copy() res.update(b) return res def cachedproperty(func): """A memoize decorator for class properties.""" key = '_' + func.__name__ @wraps(func) def get(self): try: return getattr(self, key) except AttributeError: val = func(self) setattr(self, key, val) return val return property(get) # ---------------------------------------- # udisks.Device helper classes # ---------------------------------------- class AttrDictView(object): """Provide attribute access view to a dictionary.""" def __init__(self, data): self.__data = data def __getattr__(self, key): try: return self.__data[key] except KeyError: raise AttributeError class ObjDictView(object): """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)) @fix_str_conversions class BaseDevice(object): def __str__(self): """Show as object_path.""" return self.object_path def __eq__(self, other): """Comparison by object_path.""" return self.object_path == str(other) def __ne__(self, other): """Comparison by object_path.""" return not (self == other) def is_file(self, path): """Comparison by mount and device file path.""" return (samefile(path, self.device_file) or samefile(path, self.loop_file) or any(samefile(path, mp) for mp in self.mount_paths) or sameuuid(path, self.id_uuid) or sameuuid(path, self.partition_uuid)) # ---------------------------------------- # derived properties # ---------------------------------------- @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, ])) @fix_str_conversions class NullDevice(object): """ Invalid object. Evaluates to False in boolean context, but allows arbitrary attribute access by returning another Null. """ object_path = '/' def __init__(self, **properties): """Initialize an instance with the given DBus proxy object.""" self.__dict__.update(properties) def __bool__(self): return False __nonzero__ = __bool__ def __str__(self): """Display as object path.""" return self.object_path def __eq__(self, other): """Comparison by object path.""" return self.object_path == str(other) def __ne__(self, other): """Comparison by object path.""" return not (self == other) def is_file(self, path): """Comparison by mount and device file path.""" return False # availability of interfaces is_drive = False is_block = False is_partition_table = False is_partition = False is_filesystem = False is_luks = False is_loop = False # Drive is_toplevel = is_drive is_detachable = False is_ejectable = False has_media = False def eject(self, unmount=False): raise RuntimeError("Cannot call methods on invalid device!") def detach(self): raise RuntimeError("Cannot call methods on invalid device!") # Block device_file = '' device_presentation = '' device_size = 0 id_usage = '' is_crypto = False is_ignored = None device_id = '' id_type = '' id_label = '' id_uuid = '' @property def luks_cleartext_slave(self): raise AttributeError('Invalid device has no cleartext slave.') is_luks_cleartext = False is_external = None is_systeminternal = None @property def drive(self): raise AttributeError('Invalid device has no drive.') root = drive should_automount = False icon_name = '' symbolic_icon_name = icon_name symlinks = [] # Partition @property def partition_slave(self): raise AttributeError('Invalid device has no partition slave.') # Filesystem is_mounted = False mount_paths = () def mount(self, fstype=None, options=None, auth_no_user_interaction=False): raise RuntimeError("Cannot call methods on invalid device!") def unmount(self, force=False): raise RuntimeError("Cannot call methods on invalid device!") # Encrypted @property def luks_cleartext_holder(self): raise AttributeError('Invalid device has no cleartext holder.') is_unlocked = None def unlock(self, password): raise RuntimeError("Cannot call methods on invalid device!") def lock(self): raise RuntimeError("Cannot call methods on invalid device!") # Loop loop_file = '' setup_by_uid = -1 autoclear = None def delete(self, auth_no_user_interaction=None): raise RuntimeError("Cannot call methods on invalid device!") def set_autoclear(self, value, auth_no_user_interaction=None): raise RuntimeError("Cannot call methods on invalid device!") loop_support = False # derived properties mount_path = '' in_use = False ui_id_label = '' ui_id_uuid = '' ui_device_presentation = '' ui_label = '(invalid device)' ui_device_label = '' drive_label = '' parent_object_path = '/' can_add = False can_remove = False class DaemonBase(object): 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 # ---------------------------------------- try: unicode except NameError: unicode = str def decode_ay(ay): """Convert binary blob from DBus queries to strings.""" if ay is None: return '' elif isinstance(ay, unicode): return ay elif isinstance(ay, bytes): return ay.decode('utf-8') else: # dbus.Array([dbus.Byte]) or any similar sequence type: return bytearray(ay).rstrip(bytearray((0,))).decode('utf-8') def str2unicode(arg): """Decode python2 strings (bytes) to unicode.""" if isinstance(arg, list): return [str2unicode(s) for s in arg] if isinstance(arg, bytes): return arg.decode('utf-8') return arg def exc_message(exc): """Get an exception message.""" message = getattr(exc, 'message', None) return str2unicode(message or str(exc)) def format_exc(): return str2unicode(traceback.format_exc()) udiskie-1.7.3/udiskie/cache.py0000644000175000017500000000273613214314444017051 0ustar thomasthomas00000000000000""" Utility for temporarily caching passwords. """ from __future__ import absolute_import from __future__ import unicode_literals import keyutils class PasswordCache(object): def __init__(self, timeout): self.timeout = timeout self.keyring = keyutils.KEY_SPEC_PROCESS_KEYRING def _key(self, device): return device.id_uuid.encode('utf-8') def _key_id(self, device): key = self._key(device) try: key_id = keyutils.request_key(key, self.keyring) except keyutils.Error: raise KeyError("Key has been revoked!") if key_id is None: raise KeyError("Key not cached!") return key_id def __contains__(self, device): try: self._key_id(device) return True except KeyError: return False def __getitem__(self, device): key_id = self._key_id(device) self._touch(key_id) try: return keyutils.read_key(key_id).decode('utf-8') except keyutils.Error: raise KeyError("Key not cached!") def __setitem__(self, device, value): key = self._key(device) key_id = keyutils.add_key(key, value.encode('utf-8'), self.keyring) self._touch(key_id) def __delitem__(self, device): key_id = self._key_id(device) keyutils.revoke(key_id) def _touch(self, key_id): if self.timeout > 0: keyutils.set_timeout(key_id, self.timeout) udiskie-1.7.3/udiskie/async_.py0000644000175000017500000002630613214316576017271 0ustar thomasthomas00000000000000""" 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. """ # NOTE: neither AsyncList nor Coroutine save references to the active tasks! # Although this would create a reference cycle (coro->task->callbacks->coro), # the garbage collector can generally detect the cycle and delete the involved # objects anyway (there is usually no independent reference to the coroutine). # So you must take care to increase the reference-count of all active tasks # manually. from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from functools import partial from subprocess import CalledProcessError import sys from gi.repository import GLib from gi.repository import Gio from .common import cachedproperty, wraps, format_exc __all__ = [ 'Async', 'AsyncList', 'Return', 'Coroutine', ] class Async(object): """ Base class for asynchronous operations. One `Async' object represents an asynchronous operation. It allows for separate result and error handlers which can be set by appending to the `callbacks` and `errbacks` lists. Implementations must conform to the following very lightweight protocol: The task is started on initialization, but most not finish immediately. Tasks must take care to increase their reference count on their own in order not to be deleted until completion. Success/error exit is signaled to the observer by calling exactly one of `self.callback(value)` or `self.errback(exception)` when the operation finishes. For implementations, see :class:`Coroutine` and :class:`DBusCall`. """ done = False @cachedproperty def callbacks(self): """Functions to be called on successful completion.""" return [] @cachedproperty def errbacks(self): """Functions to be called on error completion.""" return [] def _finish(self, callbacks, *args): """Set finished state and invoke specified callbacks [internal].""" if self.done: # TODO: more output raise RuntimeError("Async already finished!") self.done = True # TODO: handle Async callbacks: return [fn(*args) for fn in callbacks] def callback(self, *values): """Signal successful completion.""" self._finish(self.callbacks, *values) def errback(self, exception, formatted): """Signal unsuccessful completion.""" was_handled = self._finish(self.errbacks, exception, formatted) if not any(was_handled): print(formatted, file=sys.stderr) # Making this object a global is important to prevent garbage collection which # could cause the waiting function to be de-reference-counted and henceforth # destroyed as well. RunForever = Async() class AsyncList(Async): """ Manages a collection of asynchronous tasks. The callbacks are executed when all of the subtasks have completed. """ def __init__(self, tasks): """Create an AsyncList from a list of Asyncs.""" tasks = list(tasks) self._results = {} self._num_tasks = len(tasks) if not tasks: run_soon(self.callback, []) for idx, task in enumerate(tasks): task.callbacks.append(partial(self._subtask_result, idx)) task.errbacks.append(partial(self._subtask_error, idx)) def _set_subtask_result(self, idx, result): """Set result of a single subtask.""" self._results[idx] = result if len(self._results) == self._num_tasks: self.callback([ self._results[i] for i in range(self._num_tasks) ]) def _subtask_result(self, idx, *args): """Receive a result from a single subtask.""" self._set_subtask_result(idx, AsyncResult(True, *args)) def _subtask_error(self, idx, error, fmt): """Receive an error from a single subtask.""" self._set_subtask_result(idx, AsyncResult(False, error, fmt)) class AsyncResult(object): def __init__(self, success, *values): self.success = success self.values = values def __bool__(self): return self.success and all(self.values) __nonzero__ = __bool__ class Return(object): """Wraps a return value from a coroutine.""" def __init__(self, *values): self.values = values def call_func(fn, *args): """ Call the function with the specified arguments but return None. This rather boring helper function is used by run_soon to make sure the function is executed only once. """ # NOTE: Apparently, idle_add does not re-execute its argument if an # exception is raised. So it's okay to let exceptions propagate. fn(*args) def run_soon(fn, *args): """Run the function once.""" GLib.idle_add(call_func, fn, *args) def sleep(seconds): future = Async() GLib.timeout_add(int(seconds*1000), future.callback, True) return future class Coroutine(Async): """ A coroutine processes a sequence of asynchronous tasks. Coroutines resemble non-atomic asynchronous operations. They merely aggregate and operate on the results of zero or more asynchronous subtasks. Coroutines are scheduled for execution by just calling them. In that regard, they behave very similar to normal functions. The difference is, that they return an Async object rather than a result. This object can then be used to add result handler callbacks. The coroutine's code block will first be entered in a separate main loop iteration. Coroutines are implemented as generators using `yield` expressions to transfer control flow when performing asynchronous tasks. Coroutines may yield zero or more `Async` tasks and one final `Return` value. The code after a `yield` expression is executed only after the yielded `Async` has finished. In case of successful completion, the result of the asynchronous operation is returned. In case of an error, the exception is raised inside the generator. For example: >>> @Coroutine.from_generator_function ... def foo(*args): ... # perform synchronous calculations here: ... other_args = f(args) ... try: ... # Invoke another asynchronous routine. Potentialy passes ... # control flow to main loop: ... result = yield subroutine(other_args) ... except ValueError: ... # Handle errors raised by the asynchronous subroutine. These ... # are sent here from the callback function. ... pass ... # `result` now contains the `Return` value of the sub-routine and ... # can be used for further calculations: ... value = g(result) ... # Set our own `Return` value. This must be the last statement: ... yield Return(value) """ @classmethod def from_generator_function(cls, generator_function): """Turn a generator function into a coroutine function.""" @wraps(generator_function) def coroutine_function(*args, **kwargs): return cls(generator_function(*args, **kwargs)) coroutine_function.__func__ = generator_function return coroutine_function def __init__(self, generator): """ Create and start a `Coroutine` task from the specified generator. """ self._generator = generator # TODO: cancellable tasks (generator.close() -> GeneratorExit)? run_soon(self._interact, next, self._generator) # TODO: shorten stack traces by inlining _recv / _interact ? def _recv(self, thing): """ Handle a value received from (yielded by) the generator. This function is called immediately after the generator suspends its own control flow by yielding a value. """ if isinstance(thing, Async): thing.callbacks.append(self._send) thing.errbacks.append(self._throw) elif isinstance(thing, Return): self._generator.close() # self.callback(*thing.values) # to shorten stack trace use instead: run_soon(self.callback, *thing.values) else: # the protocol is easy to do wrong, therefore we better do not # silently ignore any errors! raise NotImplementedError( ("Unexpected return value from function {!r}: {!r}.\n" "Expecting either an Async or a Return.") .format(self._generator, thing)) def _send(self, *values): """ Interact with the coroutine by sending a value. Set the return value of the current `yield` expression to the specified value and resume control flow inside the coroutine. """ self._interact(self._generator.send, self._pack(*values)) def _pack(self, *values): """Unpack a return tuple to a yield expression return value.""" # Schizophrenic returns from yield expressions. Inspired by # gi.overrides.Gio.DBusProxy. if len(values) == 0: return None elif len(values) == 1: return values[0] else: return values def _throw(self, exc, fmt): """ Interact with the coroutine by raising an exception. Transfer the control flow back to the coroutine by raising an exception from the `yield` expression. """ self._interact(self._generator.throw, exc) return True def _interact(self, func, *args): """ Interact with the coroutine by performing the specified operation. """ try: value = func(*args) except StopIteration: self._generator.close() self.callback() except Exception as e: self._generator.close() self.errback(e, format_exc()) else: self._recv(value) class Subprocess(Async): """ An Async task that represents a subprocess. If successful, the task's result is set to the collected STDOUT of the subprocess. :raises subprocess.CalledProcessError: if the subprocess returns a non-zero exit code """ def __init__(self, argv): self.p = Gio.Subprocess.new(argv, Gio.SubprocessFlags.STDOUT_PIPE) stdin_buf = None cancellable = None user_data = None self.p.communicate_utf8_async( stdin_buf, cancellable, self._callback, user_data) def _callback(self, source_object, result, user_data): try: success, stdout, stderr = self.p.communicate_utf8_finish(result) if not success: raise RuntimeError("Subprocess did not exit normally!") exit_code = self.p.get_exit_status() if exit_code != 0: raise CalledProcessError( "Subprocess returned a non-zero exit-status!", exit_code, stdout) except Exception as e: self.errback(e, format_exc()) else: self.callback(stdout) udiskie-1.7.3/udiskie/appindicator.py0000644000175000017500000000372213214314444020457 0ustar thomasthomas00000000000000""" Status icon using AppIndicator3. """ from gi.repository import Gtk from gi.repository import AppIndicator3 from udiskie.async_ import Async class AppIndicatorIcon(object): """ 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 = Async() 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.callback() @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-1.7.3/udiskie/mount.py0000644000175000017500000010234113214317417017144 0ustar thomasthomas00000000000000""" Mount utilities. """ from __future__ import absolute_import from __future__ import unicode_literals from distutils.spawn import find_executable from collections import namedtuple from functools import partial import inspect import logging import os from .async_ import AsyncList, Coroutine, Return, sleep from .common import wraps, setdefault, exc_message from .config import IgnoreDevice, match_config from .locale import _ __all__ = ['Mounter'] # TODO: add / remove / XXX_all should make proper use of the asynchronous # execution. @Coroutine.from_generator_function def _False(): yield Return(False) def _find_device(fn): """ Decorator for Mounter methods taking a Device as their first argument. Enables to pass the path name as first argument and does some common error handling (logging). """ @wraps(fn) def wrapper(self, device_or_path, *args, **kwargs): try: device = self.udisks.find(device_or_path) except ValueError as e: self._log.error(exc_message(e)) return _False() return Coroutine(fn(self, device, *args, **kwargs)) return wrapper def _find_device_auto_losetup(fn): @Coroutine.from_generator_function def wrapper(self, device_or_path, *args, **kwargs): try: try: device = self.udisks.find(device_or_path) except ValueError as e: if not os.path.isfile(device_or_path): raise device = yield self.losetup(device_or_path) bound = inspect.getcallargs(fn, self, device, *args, **kwargs) if bound.get('recursive') is False: yield Return(device) except Exception as e: self._log.error(exc_message(e)) yield Return(False) result = yield Coroutine(fn(self, device, *args, **kwargs)) yield Return(result) return wrapper def _sets_async_error(fn): @wraps(fn) def wrapper(self, device, *args, **kwargs): async_ = fn(self, device, *args, **kwargs) async_.errbacks.append(partial(self._error, fn, device)) return async_ return wrapper def _suppress_error(fn): """ Prevent errors in this function from being shown. This is OK, since all errors happen in sub-functions in which errors ARE logged. """ @wraps(fn) def wrapper(self, device, *args, **kwargs): async_ = fn(self, device, *args, **kwargs) async_.errbacks.append(lambda *args: True) return async_ return wrapper def _is_parent_of(parent, child): """Check whether the first device is the parent of the second device.""" if child.is_partition: return child.partition_slave == parent if child.is_toplevel: return child.drive == parent and child != parent return False def _get_parent(device): """Return the container device or ``None``.""" return device.partition_slave or device.luks_cleartext_slave class Mounter(object): """ Mount utility. Stores environment variables (filter, prompt, browser, udisks) to use across multiple mount operations. :ivar udisks: adapter to the udisks service NOTE: The optional parameters are not guaranteed to keep their order and should always be passed as keyword arguments. """ def __init__(self, udisks, config=None, prompt=None, browser=None, cache=None): """ Initialize mounter with the given defaults. :param udisks: udisks service object. May be a Sniffer or a Daemon. :param list config: list of :class:`DeviceFilter` :param callable prompt: retrieve passwords for devices :param callable browser: open devices If prompt is None, device unlocking will not work. If browser is None, browse will not work. """ self.udisks = udisks self._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, 'loop_file': '/*', 'ignore': False}), IgnoreDevice({'is_block': False, 'ignore': True}), IgnoreDevice({'is_external': False, 'ignore': True}), IgnoreDevice({'is_ignored': True, 'ignore': True})] self._prompt = prompt self._browser = browser self._cache = cache self._log = logging.getLogger(__name__) try: # propagate error messages to UDisks1 daemon for 'Job failed' # notifications. self._set_error = self.udisks.set_error except AttributeError: self._set_error = lambda device, action, message: None def _error(self, fn, device, err, fmt): message = exc_message(err) self._log.error(_('failed to {0} {1}: {2}', fn.__name__, device, message)) self._set_error(device, fn.__name__, message) return True @_sets_async_error @_find_device def browse(self, device): """ Browse device. :param device: device object, block device path or mount path :returns: success :rtype: bool """ if not device.is_mounted: self._log.error(_("not browsing {0}: not mounted", device)) yield Return(False) if not self._browser: self._log.error(_("not browsing {0}: no program", device)) yield Return(False) self._log.debug(_('opening {0} on {0.mount_paths[0]}', device)) self._browser(device.mount_paths[0]) self._log.info(_('opened {0} on {0.mount_paths[0]}', device)) yield Return(True) # mount/unmount @_sets_async_error @_find_device def mount(self, device): """ Mount the device if not already mounted. :param device: device object, block device path or mount path :returns: whether the device is mounted. :rtype: bool """ if not self.is_handleable(device) or not device.is_filesystem: self._log.warn(_('not mounting {0}: unhandled device', device)) yield Return(False) if device.is_mounted: self._log.info(_('not mounting {0}: already mounted', device)) yield Return(True) 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 = yield device.mount(**kwargs) self._log.info(_('mounted {0} on {1}', device, mount_path)) yield 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.")) @_sets_async_error @_find_device def unmount(self, device): """ Unmount a Device if mounted. :param device: device object, block device path or mount path :returns: whether the device is unmounted :rtype: bool """ if not self.is_handleable(device) or not device.is_filesystem: self._log.warn(_('not unmounting {0}: unhandled device', device)) yield Return(False) if not device.is_mounted: self._log.info(_('not unmounting {0}: not mounted', device)) yield Return(True) self._log.debug(_('unmounting {0}', device)) yield device.unmount() self._log.info(_('unmounted {0}', device)) yield Return(True) # unlock/lock (LUKS) @_sets_async_error @_find_device def unlock(self, device): """ Unlock the device if not already unlocked. :param device: device object, block device path or mount path :returns: whether the device is unlocked :rtype: bool """ if not self.is_handleable(device) or not device.is_crypto: self._log.warn(_('not unlocking {0}: unhandled device', device)) yield Return(False) if device.is_unlocked: self._log.info(_('not unlocking {0}: already unlocked', device)) yield Return(True) if not self._prompt: self._log.error(_('not unlocking {0}: no password prompt', device)) yield Return(False) unlocked = yield self._unlock_from_cache(device) if unlocked: yield Return(True) unlocked = yield self._unlock_from_keyfile(device) if unlocked: yield Return(True) password = yield self._prompt(device, self.udisks.keyfile_support) if password is None: self._log.debug(_('not unlocking {0}: cancelled by user', device)) yield Return(False) if isinstance(password, bytes): self._log.debug(_('unlocking {0} using keyfile', device)) yield device.unlock_keyfile(password) else: self._log.debug(_('unlocking {0}', device)) yield device.unlock(password) self._update_cache(device, password) self._log.info(_('unlocked {0}', device)) yield Return(True) @Coroutine.from_generator_function def _unlock_from_cache(self, device): if not self._cache: yield Return(False) try: password = self._cache[device] except KeyError: yield Return(False) self._log.debug(_('unlocking {0} using cached password', device)) try: yield device.unlock(password) except Exception: self._log.debug(_('failed to unlock {0} using cached password', device)) yield Return(False) self._log.info(_('unlocked {0} using cached password', device)) yield Return(True) @Coroutine.from_generator_function def _unlock_from_keyfile(self, device): if not self.udisks.keyfile_support: yield Return(False) filename = match_config(self._config, device, 'keyfile', None) if filename is None: self._log.debug(_('No matching keyfile rule for {}.', device)) yield Return(False) try: with open(filename, 'rb') as f: keyfile = f.read() except IOError: self._log.warn(_('configured keyfile for {0} not found', device)) yield Return(False) self._log.debug(_('unlocking {0} using keyfile {1}', device, filename)) try: yield device.unlock_keyfile(keyfile) except Exception: self._log.debug(_('failed to unlock {0} using keyfile', device)) yield Return(False) self._log.info(_('unlocked {0} using keyfile', device)) yield Return(True) def _update_cache(self, device, password): if not self._cache: return self._cache[device] = password def forget_password(self, device): try: del self._cache[device] except KeyError: pass @_sets_async_error @_find_device def lock(self, device): """ Lock device if unlocked. :param device: device object, block device path or mount path :returns: whether the device is locked :rtype: bool """ if not self.is_handleable(device) or not device.is_crypto: self._log.warn(_('not locking {0}: unhandled device', device)) yield Return(False) if not device.is_unlocked: self._log.info(_('not locking {0}: not unlocked', device)) yield Return(True) self._log.debug(_('locking {0}', device)) yield device.lock() self._log.info(_('locked {0}', device)) yield Return(True) # add/remove (unlock/lock or mount/unmount) @_suppress_error @_find_device_auto_losetup 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 :rtype: bool """ if device.is_filesystem: success = yield self.mount(device) elif device.is_crypto: success = yield self.unlock(device) if success and recursive: yield self.udisks._sync() device = self.udisks[device.object_path] success = yield self.add( device.luks_cleartext_holder, recursive=True) elif (recursive and device.is_partition_table 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 = yield AsyncList(tasks) success = all(results) else: self._log.info(_('not adding {0}: unhandled device', device)) yield Return(False) yield Return(success) @_suppress_error @_find_device_auto_losetup def auto_add(self, device, recursive=None): """ Automatically attempt to mount or unlock a device, but be quiet if the device is not supported. :param device: device object, block device path or mount path :param bool recursive: recursively mount and unlock child devices :returns: whether all attempted operations succeeded :rtype: bool """ success = True if device.is_luks_cleartext and self.udisks.version_info >= (2,7,0): yield sleep(1.5) # temporary workaround for #153, unreliable... if not self.is_automount(device): pass elif device.is_filesystem: if not device.is_mounted: success = yield self.mount(device) elif device.is_crypto: if self._prompt and not device.is_unlocked: success = yield self.unlock(device) if success and recursive: yield self.udisks._sync() device = self.udisks[device.object_path] success = yield self.auto_add( device.luks_cleartext_holder, recursive=True) elif recursive and device.is_partition_table: tasks = [ self.auto_add(dev, recursive=True) for dev in self.get_all_handleable() if dev.is_partition and dev.partition_slave == device ] results = yield AsyncList(tasks) success = all(results) else: self._log.debug(_('not adding {0}: unhandled device', device)) yield Return(success) @_suppress_error @_find_device def remove(self, device, force=False, detach=False, eject=False, lock=False): """ Unmount or lock the device depending on device type. :param device: device object, block device path or mount path :param bool force: recursively remove all child devices :param bool detach: detach the root drive :param bool eject: remove media from the root drive :param bool lock: lock the associated LUKS cleartext slave :returns: whether all attempted operations succeeded :rtype: bool """ if device.is_filesystem: if device.is_mounted or not device.is_loop or detach is False: success = yield self.unmount(device) elif device.is_crypto: if force and device.is_unlocked: yield self.auto_remove(device.luks_cleartext_holder, force=True) success = yield self.lock(device) elif (force and (device.is_partition_table or device.is_drive) 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 = yield AsyncList(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 success = yield self.lock(device) if eject: success = yield self.eject(device) if (detach or detach is None) and device.is_loop: success = yield self.delete(device, remove=False) elif detach: success = yield self.detach(device) yield Return(success) @_suppress_error @_find_device def auto_remove(self, device, force=False, detach=False, eject=False, lock=False): """ Unmount or lock the device depending on device type. :param device: device object, block device path or mount path :param bool force: recursively remove all child devices :param bool detach: detach the root drive :param bool eject: remove media from the root drive :param bool lock: lock the associated LUKS cleartext slave :returns: whether all attempted operations succeeded :rtype: bool """ success = True if not self.is_handleable(device): pass elif device.is_filesystem: if device.is_mounted: success = yield self.unmount(device) elif device.is_crypto: if force and device.is_unlocked: yield self.auto_remove(device.luks_cleartext_holder, force=True) if device.is_unlocked: success = yield self.lock(device) elif force and (device.is_partition_table or device.is_drive): 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 = yield AsyncList(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 = yield self.lock(device) if eject and device.has_media: success = yield self.eject(device) if (detach or detach is None) and device.is_loop: success = yield self.delete(device, remove=False) elif detach and device.is_detachable: success = yield self.detach(device) yield Return(success) # eject/detach device @_sets_async_error @_find_device def eject(self, device, force=False): """ Eject a device after unmounting all its mounted filesystems. :param device: device object, block device path or mount path :param bool force: remove child devices before trying to eject :returns: whether the operation succeeded :rtype: bool """ if not self.is_handleable(device): self._log.warn(_('not ejecting {0}: unhandled device')) yield Return(False) drive = device.drive if not (drive.is_drive and drive.is_ejectable): self._log.warn(_('not ejecting {0}: drive not ejectable', drive)) yield Return(False) if force: # Can't autoremove 'device.drive', because that will be filtered # due to block=False: yield self.auto_remove(device.root, force=True) self._log.debug(_('ejecting {0}', device)) yield drive.eject() self._log.info(_('ejected {0}', device)) yield Return(True) @_sets_async_error @_find_device def detach(self, device, force=False): """ Detach a device after unmounting all its mounted filesystems. :param device: device object, block device path or mount path :param bool force: remove child devices before trying to detach :returns: whether the operation succeeded :rtype: bool """ if not self.is_handleable(device): self._log.warn(_('not detaching {0}: unhandled device', device)) yield Return(False) drive = device.root if not drive.is_detachable: self._log.warn(_('not detaching {0}: drive not detachable', drive)) yield Return(False) if force: yield self.auto_remove(drive, force=True) self._log.debug(_('detaching {0}', device)) yield drive.detach() self._log.info(_('detached {0}', device)) yield Return(True) # mount_all/unmount_all @Coroutine.from_generator_function def add_all(self, recursive=False): """ Add all handleable devices that available at start. :param bool recursive: recursively mount and unlock child devices :returns: whether all attempted operations succeeded :rtype: bool """ tasks = [self.auto_add(device, recursive=recursive) for device in self.get_all_handleable_leaves()] results = yield AsyncList(tasks) success = all(results) yield Return(success) @Coroutine.from_generator_function def remove_all(self, detach=False, eject=False, lock=False): """ Remove all filesystems handleable by udiskie. :param bool detach: detach the root drive :param bool eject: remove media from the root drive :param bool lock: lock the associated LUKS cleartext slave :returns: whether all attempted operations succeeded :rtype: bool """ 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 = yield AsyncList(tasks) success = all(results) yield Return(success) # loop devices @Coroutine.from_generator_function 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 ValueError: pass else: self._log.info(_('not setting up {0}: already up', device)) yield Return(device) if not os.path.isfile(image): self._log.error(_('not setting up {0}: not a file', image)) yield Return(None) if not self.udisks.loop_support: self._log.error(_('not setting up {0}: unsupported in UDisks1', image)) yield Return(None) self._log.debug(_('setting up {0}', image)) fd = os.open(image, os.O_RDONLY) device = yield 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)) yield Return(device) @_sets_async_error @_find_device 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 :rtype: bool """ if not self.is_handleable(device) or not device.is_loop: self._log.warn(_('not deleting {0}: unhandled device', device)) yield Return(False) if remove: yield self.auto_remove(device, force=True) self._log.debug(_('deleting {0}', device)) yield device.delete() self._log.info(_('deleted {0}', device)) yield Return(True) # iterate devices def is_handleable(self, device): # TODO: handle pathes in first argument """ Check whether this device should be handled by udiskie. :param device: device object, block device path or mount path :returns: handleability :rtype: bool Currently this just means that the device is removable and holds a filesystem or the device is a LUKS encrypted volume. """ ignored = self._ignore_device(device) # propagate handleability of parent devices: if ignored is None and device is not None: return self.is_handleable(_get_parent(device)) return not ignored def is_automount(self, device): if not self.is_handleable(device): return False return match_config(self._config, device, 'automount', True) def _ignore_device(self, device): return match_config(self._config, device, 'ignore', False) def is_addable(self, device): """ Check if device can be added with ``auto_add``. """ if not self.is_automount(device): return False if device.is_filesystem: return not device.is_mounted if device.is_crypto: return self._prompt and not device.is_unlocked if device.is_partition_table: return any(self.is_addable(dev) for dev in self.get_all_handleable() if dev.partition_slave == device) return False def is_removable(self, device): """ Check if device can be removed with ``auto_remove``. """ if not self.is_handleable(device): return False if device.is_filesystem: return device.is_mounted if device.is_crypto: return device.is_unlocked if device.is_partition_table or device.is_drive: return any(self.is_removable(dev) for dev in self.get_all_handleable() if _is_parent_of(device, dev)) return False def get_all_handleable(self): """ Enumerate all handleable devices currently known to udisks. :returns: handleable devices :rtype: list """ 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 def get_device_tree_filtered(self): """Get a tree of all handleable devices.""" root = self.get_device_tree()['/'] # remove ignored devices from the structure: def filter_handled(node): node.children[:] = [ descendant for child in node.children for descendant in filter_handled(child) ] if node.ignored: return node.children else: return [dev] filter_handled(root) return root 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(object): _labels = { 'browse': _('Browse {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, 'mount': mounter.mount, 'unmount': mounter.unmount, 'unlock': mounter.unlock, 'lock': partial(mounter.remove, force=True), 'eject': partial(mounter.eject, force=True), 'detach': partial(mounter.detach, force=True), 'forget_password': mounter.forget_password, '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 of device hierarchy :rtype: Device """ root = Device(None, [], None, "", []) device_nodes = dict(map(self._device_node, self._mounter.get_all_handleable())) # insert child devices as branches into their roots: for 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' 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 and device.loop_support: 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-1.7.3/udiskie/udisks1.py0000644000175000017500000005445513214314444017376 0ustar thomasthomas00000000000000""" UDisks wrapper utilities. These act as a convenience abstraction layer on the UDisks DBus service. Requires UDisks 1.0.5 as described here: http://udisks.freedesktop.org/docs/1.0.5/ This wraps the DBus API of Udisks2 providing a common interface with the udisks2 module. :class:`Daemon` caches all device states and listens to UDisks events to guarantee the accessibilityy of device properties in between operations. """ from __future__ import absolute_import from __future__ import unicode_literals from collections import defaultdict import logging import os.path from gi.repository import GLib from .async_ import AsyncList, Coroutine, Return from .common import (Emitter, samefile, sameuuid, AttrDictView, wraps, NullDevice, BaseDevice) from .compat import fix_str_conversions from .dbus import connect_service, MethodsProxy, DBusCall from .locale import _ __all__ = [ 'Daemon', ] def filter_opt(opt): """Remove ``None`` values from a dictionary.""" return [k for k, v in opt.items() if v] class Device(BaseDevice): """Helper base class for devices.""" Interface = 'org.freedesktop.UDisks.Device' def __init__(self, daemon, object_path, property_proxy, method_proxy): """ Initialize an instance with the given DBus proxy object. :param dbus.ObjectProxy object: """ self._daemon = daemon self.object_path = object_path self._P = property_proxy self._M = method_proxy # availability of interfaces @property def is_drive(self): """Check if the device is a drive.""" return self._P.DeviceIsDrive @property def is_block(self): """Check if the device is a block device.""" return True @property def is_partition_table(self): """Check if the device is a partition table.""" return self._P.DeviceIsPartitionTable @property def is_partition(self): """Check if the device has a partition slave.""" return self._P.DeviceIsPartition @property def is_filesystem(self): """Check if the device is a filesystem.""" return self.id_usage == 'filesystem' @property def is_luks(self): """Check if the device is a LUKS container.""" return self._P.DeviceIsLuks @property def is_loop(self): """Check if the device is a loop device.""" return self._P.DeviceIsLinuxLoop # ---------------------------------------- # Drive # ---------------------------------------- # Drive properties is_toplevel = is_drive @property def is_detachable(self): """Check if the drive that owns this device can be detached.""" if not self.is_drive: return None return self._P.DriveCanDetach @property def is_ejectable(self): """Check if the drive that owns this device can be ejected.""" if not self.is_drive: return None return self._P.DriveIsMediaEjectable @property def has_media(self): """Check if there is media available in the drive.""" return self._P.DeviceIsMediaAvailable @property def drive_vendor(self): """Return drive vendor.""" return self._P.DriveVendor @property def drive_model(self): """Return drive model.""" return self._P.DriveModel # Drive methods def eject(self, unmount=False): """Eject media from the device.""" return self._M.DriveEject( '(as)', filter_opt({'unmount': unmount})) def detach(self): """Detach the device by e.g. powering down the physical port.""" return self._M.DriveDetach('(as)', []) # ---------------------------------------- # Block # ---------------------------------------- # Block properties @property def device_file(self): """The filesystem path of the device block file.""" return os.path.normpath(self._P.DeviceFile) @property def device_presentation(self): """The device file path to present to the user.""" return self._P.DeviceFilePresentation # TODO: device_size missing @property def id_usage(self): """Device usage class, for example 'filesystem' or 'crypto'.""" return self._P.IdUsage @property def is_crypto(self): """Check if the device is a crypto device.""" return self.id_usage == 'crypto' @property def is_ignored(self): """Check if the device should be ignored.""" return self._P.DevicePresentationHide @property def device_id(self): """ Return a unique and persistent identifier for the device. This is the basename (last path component) of the symlink in `/dev/disk/by-id/`. """ for filename in self._P.DeviceFileById: parts = filename.split('/') if parts[-2] == 'by-id': return parts[-1] return '' @property def id_type(self): """" Return IdType property. This field provides further detail on IdUsage, for example: IdUsage 'filesystem' 'crypto' IdType 'ext4' 'crypto_LUKS' """ return self._P.IdType @property def id_label(self): """Label of the device if available.""" return self._P.IdLabel @property def id_uuid(self): """Device UUID.""" return self._P.IdUuid @property def luks_cleartext_slave(self): """Get luks crypto device.""" if not self.is_luks_cleartext: return None return self._daemon[self._P.LuksCleartextSlave] @property def is_luks_cleartext(self): """Check whether this is a luks cleartext device.""" return self._P.DeviceIsLuksCleartext @property def is_external(self): """Check if the device is external.""" return not self.is_systeminternal @property def is_systeminternal(self): """Check if the device is internal.""" return self._P.DeviceIsSystemInternal @property def drive(self): """ Get the drive containing this device. The returned Device object is not guaranteed to be a drive. """ if self.is_partition: return self.partition_slave.drive elif self.is_luks_cleartext: return self.luks_cleartext_slave.drive else: return self root = drive @property def should_automount(self): """Check if the device should be automounted.""" return self._P.DeviceAutomountHint != 'never' @property def icon_name(self): """Return the recommended device icon name.""" return self._P.DevicePresentationIconName or 'drive-removable-media' symbolic_icon_name = icon_name @property def symlinks(self): """Known symlinks of the block device.""" return list(filter(None, [ self._P.DeviceFile, self._P.DeviceFilePresentation, self._P.DeviceFileById, self._P.DeviceFileByPath, ])) # ---------------------------------------- # Partition # ---------------------------------------- # Partition properties @property def partition_slave(self): """Get the partition slave (container).""" if not self.is_partition: return None return self._daemon[self._P.PartitionSlave] @property def partition_uuid(self): """Get the partition UUID.""" return self._P.PartitionUuid # ---------------------------------------- # Filesystem # ---------------------------------------- # Filesystem properties @property def is_mounted(self): """Check if the device is mounted.""" return self._P.DeviceIsMounted @property def mount_paths(self): """Return list of active mount paths.""" if not self.is_mounted: return [] raw_paths = self._P.DeviceMountPaths return [os.path.normpath(path) for path in raw_paths] # Filesystem methods def mount(self, fstype=None, options=None, auth_no_user_interaction=False): """Mount filesystem.""" options = (options or []) + filter_opt({ 'auth_no_user_interaction': auth_no_user_interaction }) return self._M.FilesystemMount( '(sas)', fstype or self.id_type, options) def unmount(self, force=False): """Unmount filesystem.""" return self._M.FilesystemUnmount( '(as)', filter_opt({'force': force})) # ---------------------------------------- # Encrypted # ---------------------------------------- # Encrypted properties @property def luks_cleartext_holder(self): """Get unlocked luks cleartext device.""" if not self.is_luks: return None return self._daemon[self._P.LuksHolder] @property def is_unlocked(self): """Check if device is already unlocked.""" if not self.is_luks: return None return self.luks_cleartext_holder # Encrypted methods def unlock(self, password): """Unlock Luks device.""" return self._M.LuksUnlock( '(sas)', password, []) def lock(self): """Lock Luks device.""" return self._M.LuksLock('(as)', []) # ---------------------------------------- # Loop # ---------------------------------------- @property def loop_file(self): """Get the file backing the loop device.""" return self._P.LinuxLoopFilename @property def setup_by_uid(self): """[NOT IN UDISKS1] Get the ID of the user who set up the loop device.""" return None @property def autoclear(self): """[NOT IN UDISKS1] If True the loop device will be deleted after unmounting.""" return None def delete(self, *args, **kwargs): raise NotImplementedError("No support for manipulating loop devices in UDisks1!") set_autoclear = delete loop_support = False # ---------------------------------------- # derived properties # ---------------------------------------- @property def parent_object_path(self): if self.is_partition: return self._P.PartitionSlave elif self.is_luks_cleartext: return self._P.LuksCleartextSlave else: return '/' def _keep_async_event_order(func): """ Decorator that ensures that event handlers for a device will be called in the order that the events have been generated. """ @wraps(func) def wrapper(self, object_path, *args, **kwargs): self._event_queue[object_path].push(func, self, object_path, *args, **kwargs) return wrapper class EventQueue(object): """ Execute asynchronous event handlers in the order they are pushed. This is necessary because the :class:`Daemon` is stateful; it which assumes that event handlers are executed in the order they were generated by udisks. """ def __init__(self): self._waiting = [] self._execing = False def push(self, func, *args, **kwargs): """Schedule another event handler for execution.""" self._waiting.append((func, args, kwargs)) if not self._execing: self.pop() def pop(self, *_ignored): """Start executing the next event handler.""" if not self._waiting: self._execing = False return self._execing = True func, args, kwargs = self._waiting.pop() coroutine = Coroutine(func(*args, **kwargs)) coroutine.errbacks.append(self.pop) coroutine.callbacks.append(self.pop) class Daemon(Emitter): """ UDisks listener daemon. Listens to UDisks events. When a change occurs this class detects what has changed and triggers an appropriate event. Valid events are: - device_added / device_removed - device_unlocked / device_locked - device_mounted / device_unmounted - media_added / media_removed - device_changed / job_failed A very primitive mechanism that gets along without external dependencies is used for event dispatching. The methods `connect` and `disconnect` can be used to add or remove event handlers. """ BusName = 'org.freedesktop.UDisks' Interface = 'org.freedesktop.UDisks' ObjectPath = '/org/freedesktop/UDisks' def __iter__(self): """Iterate over all devices.""" return (self[object_path] for object_path in self.paths()) keyfile_support = False def __getitem__(self, object_path): return self.get(object_path) def find(self, path): """ Get a device proxy by device name or any mount path of the device. This searches through all accessible devices and compares device path as well as mount pathes. """ if isinstance(path, Device): return path for device in self: if device.is_file(path): self._log.debug(_('found device owning "{0}": "{1}"', path, device)) return device raise ValueError(_('no device found owning "{0}"', path)) def __init__(self, proxy, version): """ Create a Daemon object and start listening to DBus events. :param dbus.InterfaceProxy proxy: proxy to the dbus service object A default proxy will be created if set to ``None``. """ event_names = ['device_added', 'device_removed', 'device_mounted', 'device_unmounted', 'media_added', 'media_removed', 'device_unlocked', 'device_locked', 'device_changed', 'job_failed'] super(Daemon, self).__init__(event_names) self._log = logging.getLogger(__name__) self._log.debug(_('Daemon version: {0}', version)) self.version = version self.version_info = tuple(map(int, version.split('.'))) self._proxy = proxy self._jobs = {} self._devices = {} self._property_proxy = {} self._errors = {'mount': {}, 'unmount': {}, 'unlock': {}, 'lock': {}, 'eject': {}, 'detach': {}} # DBus events self._event_queue = defaultdict(EventQueue) proxy.connect('DeviceAdded', self._device_changed) proxy.connect('DeviceRemoved', self._device_changed) proxy.connect('DeviceChanged', self._device_changed) proxy.connect('DeviceJobChanged', self._device_job_changed) @classmethod @Coroutine.from_generator_function def create(cls): service = (cls.BusName, cls.ObjectPath, cls.Interface) proxy = yield connect_service(*service) version = yield cls.get_version() daemon = cls(proxy, version) yield daemon._sync() yield Return(daemon) @classmethod @Coroutine.from_generator_function def get_version(cls): service = (cls.BusName, cls.ObjectPath, 'org.freedesktop.DBus.Properties') manager = yield connect_service(*service) version = yield DBusCall(manager._proxy, 'Get', '(ss)', ( cls.Interface, 'DaemonVersion')) yield Return(version) def loop_setup(self, fd, options): raise NotImplementedError("UDisks1 does not support LoopSetup!") loop_support = False # Sniffer overrides def paths(self): """Iterate over all valid cached devices.""" return (object_path for object_path, device in self._devices.items() if device) def get(self, object_path): """Return the current cached state of the device.""" return self._devices.get(object_path) def trigger(self, event, device, *args): self._log.debug(_("+++ {0}: {1}", event, device)) super(Daemon, self).trigger(event, device, *args) @Coroutine.from_generator_function def update(self, object_path): device = yield self._get_updated_device(object_path) if device is not None: self._devices[object_path] = device yield Return(device) @Coroutine.from_generator_function def _get_updated_device(self, object_path): obj = self._proxy.object.bus.get_object(object_path) try: prop_prox = self._property_proxy[object_path] except KeyError: prop_prox = yield obj.get_property_interface(Device.Interface) self._property_proxy[object_path] = prop_prox try: properties = yield prop_prox.GetAll() except GLib.GError: self._invalidate(object_path) yield Return(None) # TODO: return something useful? (NullDevice) else: itfc_prox = MethodsProxy(obj, Device.Interface) device = Device(self, object_path, AttrDictView(properties), itfc_prox) yield Return(device) # special methods def set_error(self, device, action, message): if action in self._errors: self._errors[action][device.object_path] = message # events def _detect_toggle(self, property_name, old, new, add_name, del_name): old_valid = old and bool(getattr(old, property_name)) new_valid = new and bool(getattr(new, property_name)) # If we were notified about a started job we don't want to trigger # an event when the device is changed, but when the job is # completed. Otherwise we would show unmount notifications too # early (when it's not yet safe to remove the drive). # On the other hand, if the unmount operation is not issued via # UDisks1, there will be no corresponding job. cached_job = self._jobs.get(old.object_path) action_name = self._event_by_action.get(cached_job) if add_name and new_valid and not old_valid: if add_name != action_name: self.trigger(add_name, new) elif del_name and old_valid and not new_valid: if del_name != action_name: self.trigger(del_name, old) # UDisks event listeners @_keep_async_event_order def _device_changed(self, object_path): """Internal method.""" old_state = self[object_path] new_state = yield self.update(object_path) if not old_state: if new_state: self.trigger('device_added', new_state) return new_state = new_state or NullDevice(object_path=object_path) self._detect_toggle('has_media', old_state, new_state, 'media_added', 'media_removed') self._detect_toggle('is_mounted', old_state, new_state, 'device_mounted', 'device_unmounted') self._detect_toggle('is_unlocked', old_state, new_state, 'device_unlocked', 'device_locked') self.trigger('device_changed', old_state, new_state) if not new_state: self._invalidate(object_path) self.trigger('device_removed', old_state) # NOTE: it seems the UDisks1 documentation for DeviceJobChanged is # fatally incorrect! @_keep_async_event_order def _device_job_changed(self, object_path, job_in_progress, job_id, job_initiated_by_user, job_is_cancellable, job_percentage): """ Detect type of event and trigger appropriate event handlers. Internal method. """ try: if job_id: action = self._action_by_operation[job_id] else: action = self._jobs[object_path] except KeyError: # this can happen # a) at startup, when we only see the completion of a job # b) when we get notified about a job, which we don't handle return # NOTE: The here used heuristic is prone to raise conditions. if job_in_progress: # Cache the action name for later use: self._jobs[object_path] = action else: del self._jobs[object_path] device = yield self._get_updated_device(object_path) if self._check_action_success[action](device): event = self._event_by_action[action] self.trigger(event, device) else: # get and delete message, if available: message = self._errors[action].pop(object_path, "") self.trigger('job_failed', device, action, message) self._log.info(_('{0} operation failed for device: {1}', action, object_path)) # used internally by _device_job_changed: _action_by_operation = { 'FilesystemMount': 'mount', 'FilesystemUnmount': 'unmount', 'LuksUnlock': 'unlock', 'LuksLock': 'lock', 'DriveDetach': 'detach', 'DriveEject': 'eject', } _event_by_action = { 'mount': 'device_mounted', 'unmount': 'device_unmounted', 'unlock': 'device_unlocked', 'lock': 'device_locked', 'eject': 'media_removed', 'detach': 'device_removed', } _check_action_success = { 'mount': lambda dev: dev.is_mounted, 'unmount': lambda dev: not dev or not dev.is_mounted, 'unlock': lambda dev: dev.is_unlocked, 'lock': lambda dev: not dev or not dev.is_unlocked, 'detach': lambda dev: not dev, 'eject': lambda dev: not dev or not dev.has_media, } # internal state keeping @Coroutine.from_generator_function def _sync(self): """Cache all device states.""" object_pathes = yield self._proxy.call('EnumerateDevices', '()') yield AsyncList(map(self.update, object_pathes)) yield Return() def _invalidate(self, object_path): """Flag the device invalid. This removes it from the iteration.""" if object_path in self._devices: del self._devices[object_path] del self._property_proxy[object_path] udiskie-1.7.3/udiskie/prompt.py0000644000175000017500000002435513214314444017330 0ustar thomasthomas00000000000000# encoding: utf-8 """ User prompt utility. """ from __future__ import absolute_import from __future__ import unicode_literals from udiskie.depend import has_Gtk, require_Gtk from distutils.spawn import find_executable import getpass import logging import re import shlex import string import subprocess import sys from .async_ import Async, Coroutine, Return, Subprocess from .locale import _ from .common import AttrDictView from .compat import basestring from .config import DeviceFilter Gtk = None __all__ = ['password', 'browser'] dialog_definition = r""" 5 center dialog 6 6 0 False True gtk-cancel True gtk-ok True True True cancel_button ok_button """ class Dialog(Async): _ACTIVE_INSTANCES = [] def __init__(self, dialog): self._dialog = dialog self._dialog.connect("response", self._result_handler) self._dialog.show() # The connected signal is stored in the dialog, therefore creating a # reference cycle (self->dialog->handler->self) that does not protect # against garbage collection. Therefore, if the garbage collector gets # invoked, the `Dialog` instance and its members are deleted. When the # `_result_handler` is invoked, a new (empty) list of `callbacks` is # created - and the original handlers never get invoked. Hence, we # need to increase the reference count manually: self._ACTIVE_INSTANCES.append(self) def _result_handler(self, dialog, response): self.callback(response) dialog.hide() dialog.destroy() self._ACTIVE_INSTANCES.remove(self) class PasswordDialog(Dialog): content = None def __init__(self, title, message, allow_keyfile): global Gtk Gtk = require_Gtk() builder = Gtk.Builder.new() builder.add_from_string(dialog_definition) self.dialog = builder.get_object('entry_dialog') self.entry = builder.get_object('entry') if allow_keyfile: button = Gtk.Button('Open keyfile…') button.connect('clicked', self.on_open_keyfile) self.dialog.get_action_area().pack_end(button, False, False, 10) label = builder.get_object('message') label.set_label(message) self.dialog.set_title(title) self.dialog.show_all() super(PasswordDialog, self).__init__(self.dialog) def on_open_keyfile(self, button): dialog = Gtk.FileChooserDialog( "Open a keyfile to unlock the LUKS device", self.dialog, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.connect("response", self.on_select_keyfile) dialog.show_all() def on_select_keyfile(self, dialog, response): if response == Gtk.ResponseType.OK: with open(dialog.get_filename(), 'rb') as f: self.content = f.read() self.dialog.response(response) dialog.hide() dialog.destroy() def get_text(self): if self.content is not None: return self.content return self.entry.get_text() @Coroutine.from_generator_function def password_dialog(title, message, allow_keyfile): """ Show a Gtk password dialog. :param str title: :param str message: :returns: the password or ``None`` if the user aborted the operation :rtype: str :raises RuntimeError: if Gtk can not be properly initialized """ dialog = PasswordDialog(title, message, allow_keyfile) response = yield dialog if response == Gtk.ResponseType.OK: yield Return(dialog.get_text()) else: yield Return(None) def get_password_gui(device, allow_keyfile=False): """Get the password to unlock a device from GUI.""" text = _('Enter password for {0.device_presentation}: ', device) try: return password_dialog('udiskie', text, allow_keyfile) except RuntimeError: return None @Coroutine.from_generator_function def get_password_tty(device, allow_keyfile=False): """Get the password to unlock a device from terminal.""" # TODO: make this a TRUE async text = _('Enter password for {0.device_presentation}: ', device) try: yield Return(getpass.getpass(text)) except EOFError: print("") yield Return(None) class DeviceCommand(object): """ Launcher that starts user-defined password prompts. The command can be specified in terms of a command line template. """ def __init__(self, argv, **extra): """Create the launcher object from the command line template.""" if isinstance(argv, basestring): self.argv = shlex.split(argv) else: self.argv = argv self.extra = extra.copy() # obtain a list of used fields names formatter = string.Formatter() field_name = re.compile('(\d*\.)?(\w+)') self.used_attrs = [] for arg in self.argv: for text, name, spec, conv in formatter.parse(arg): if name is None: continue pos, kwd = field_name.match(name).groups() if pos is not None: logging.getLogger(__name__).warn( _('Positional field in format string {!r} is deprecated.', arg)) # check used field names if kwd in self.used_attrs or kwd in self.extra: continue if kwd in DeviceFilter.VALID_PARAMETERS: self.used_attrs.append(kwd) else: self.extra[kwd] = None logging.getLogger(__name__).error(_( 'Unknown device attribute {!r} in format string: {!r}', kwd, arg)) # NOTE: *ignored swallows `allow_keyfile` @Coroutine.from_generator_function def __call__(self, device, *ignored): """ 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) # for backward compatibility provide positional argument: fake_dev = AttrDictView(attrs) argv = [arg.format(fake_dev, **attrs) for arg in self.argv] try: stdout = yield Subprocess(argv) except subprocess.CalledProcessError: yield Return(None) yield Return(stdout.rstrip('\n')) def password(password_command): """ Create a password prompt function. :param bool hint_gui: whether a GUI input dialog should be preferred """ gui = lambda: has_Gtk() and get_password_gui tty = lambda: sys.stdin.isatty() and get_password_tty if password_command == 'builtin:gui': return gui() or tty() elif password_command == 'builtin:tty': return tty() or gui() elif password_command: return DeviceCommand(password_command) else: return None def browser(browser_name='xdg-open'): """ Create a browse-directory function. :param str browser_name: file manager program name :returns: one-parameter open function :rtype: callable """ if not browser_name: return None executable = find_executable(browser_name) if executable is None: # Why not raise an exception? -I think it is more convenient (for # end users) to have a reasonable default, without enforcing it. logging.getLogger(__name__).warn( _("Can't find file browser: {0!r}. " "You may want to change the value for the '-f' option.", browser_name)) return None def browse(path): return subprocess.Popen([executable, 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: The command format string 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, DeviceCommand(command_format, event=event)) udiskie-1.7.3/udiskie/notify.py0000644000175000017500000002374313214314444017317 0ustar thomasthomas00000000000000""" Notification utility. """ from __future__ import absolute_import from __future__ import unicode_literals import logging from gi.repository import GLib from .common import exc_message, DaemonBase 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 (pynotify or notify2) :param mounter: Mounter object :param dict timeout: timeouts """ self._notify = notify self._mounter = mounter self._actions = DeviceActions(mounter) self._timeout = timeout or {} self._aconfig = aconfig or {} self._default = self._timeout.get('timeout', -1) self._log = logging.getLogger(__name__) self._notifications = [] self.events = { event: getattr(self, event) for event in self.EVENTS if self._enabled(event) } # event handlers: def device_mounted(self, device): """ Show 'Device mounted' notification with 'Browse directory' button. :param device: device object """ if not self._mounter.is_handleable(device): return browse_action = ('browse', _('Browse directory'), self._mounter.browse, device) self._show_notification( 'device_mounted', _('Device mounted'), _('{0.ui_label} mounted on {0.mount_paths[0]}', device), device.icon_name, self._mounter._browser and browse_action) def device_unmounted(self, device): """ Show 'Device unmounted' notification. :param device: device object """ 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 'Device locked' notification. :param device: 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 'Device unlocked' notification. :param device: 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 'Device added' notification. :param device: 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 'Device removed' notification. :param device: 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. :param device: device object """ 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 dict action: parameters to :meth:`_add_action` """ notification = self._notify(summary, message, icon) timeout = self._get_timeout(event) if timeout != -1: notification.set_timeout(int(timeout * 1000)) for action in actions: if 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 shoud not render the rest of # udiskie's logic useless by raising an exception before the # automount handler gets invoked. self._log.error(_("Failed to show notification: {0}", exc_message(exc))) def _add_action(self, notification, action, label, callback, *args): """ Show an action button button in mount notifications. Note, this only works with some libnotify services. """ def on_action_click(notification, action, *user_data): callback(*args) try: # this is the correct signature for Notify-0.7, the last argument # being 'user_data': notification.add_action(action, label, on_action_click, None) except TypeError: # this is the signature for some older version, I don't know what # the last argument is for. notification.add_action(action, label, on_action_click, None, None) # pynotify does not store hard references to the notification # objects. When a signal is received and the notification does not # exist anymore, no handller will be called. Therefore, we need to # prevent these notifications from being destroyed by storing # references (note, notify2 doesn't need this): notification.connect('closed', self._notifications.remove) self._notifications.append(notification) def _enabled(self, event): """ Check if the notification for an event is enabled. :param str event: event name :returns: if the event notification is enabled :rtype: bool """ return self._get_timeout(event) not in (None, False) def _get_timeout(self, event): """ Get the timeout for an event from the config. :param str event: event name :returns: timeout in seconds :rtype: int, float or NoneType """ return self._timeout.get(event, self._default) def _action_enabled(self, event, action): """ Check if an action for a notification is enabled. :param str event: event name :param str action: action name :rtype: bool """ event_actions = self._aconfig.get(event) if event_actions is None: return True if event_actions is False: return False return action in event_actions def _has_actions(self, event): """ Check if a notification type has any enabled actions. """ event_actions = self._aconfig.get(event) return event_actions is None or bool(event_actions) udiskie-1.7.3/setup.py0000644000175000017500000001626613214314444015514 0ustar thomasthomas00000000000000# encoding: utf-8 from setuptools import setup, Command from setuptools.command.install import install as orig_install from distutils.command.install_data import install_data as orig_install_data from distutils.command.build import build as orig_build from distutils.util import convert_path # monkey-patch: use faster entry points! import fastentrypoints from subprocess import call import sys import logging from os import path, listdir from glob import glob import io # check availability of runtime dependencies def check_dependency(package, version): """Issue a warning if the package is not available.""" try: import gi gi.require_version(package.rsplit('.')[-1], version) __import__(package) except ImportError as e: # caused by either of the imports, probably the first logging.warn("Missing runtime dependencies:\n\t" + str(e)) except ValueError as e: # caused by the gi.require_version() statement logging.warn("Missing runtime dependencies:\n\t" + str(e)) except RuntimeError as e: # caused by the final __import__() statement logging.warn("Bad runtime dependency:\n\t" + str(e)) check_dependency('gi.repository.Gio', '2.0') check_dependency('gi.repository.GLib', '2.0') check_dependency('gi.repository.Gtk', '3.0') check_dependency('gi.repository.Notify', '0.7') # read long_description from README.rst long_description = None try: long_description = io.open('README.rst', encoding='utf-8').read() long_description += '\n' + io.open('CHANGES.rst', encoding='utf-8').read() except IOError: pass def exec_file(path): """Execute a python file and return the `globals` dictionary.""" namespace = {} with open(convert_path(path), 'rb') as f: exec(f.read(), namespace, namespace) return namespace metadata = exec_file('udiskie/__init__.py') # language files po_source_folder = 'lang' mo_build_prefix = path.join('build', 'locale') mo_install_prefix = path.join('share', 'locale') # completion files comp_source_folder = 'completions' comp_install_prefix = path.join('share', 'zsh', 'site-functions') # menu icons theme_base = path.join('share', 'icons', 'hicolor') icon_names = ['mount', 'unmount', 'lock', 'unlock', 'eject', 'detach'] class build(orig_build): """Subclass build command to add a subcommand for building .mo files.""" sub_commands = orig_build.sub_commands + [('build_mo', None)] class build_mo(Command): """Create machine specific translation files (for i18n via gettext).""" description = 'Compile .po files into .mo files' def initialize_options(self): pass def finalize_options(self): pass def run(self): for po_filename in glob(path.join(po_source_folder, '*.po')): lang = path.splitext(path.split(po_filename)[1])[0] mo_filename = path.join(mo_build_prefix, lang, 'LC_MESSAGES', 'udiskie.mo') self.mkpath(path.dirname(mo_filename)) self.make_file( po_filename, mo_filename, self.make_mo, [po_filename, mo_filename]) def make_mo(self, po_filename, mo_filename): """Create a machine object (.mo) from a portable object (.po) file.""" try: call(['msgfmt', po_filename, '-o', mo_filename]) except OSError as e: # ignore failures since i18n support is optional: logging.warn(e) # NOTE: we want the install logic from *distutils* rather than the one from # *setuptools*. distutils does NOT automatically install dependencies. On the # other hand, setuptools fails to invoke the build commands properly before # trying to install and it puts the data files in the egg directory (we want # them in `sys.prefix` or similar). # NOTE: Subclassing the setuptools install command alters its behaviour to use # the distutils code. This is due to some really odd call-context checks in # the setuptools command. # NOTE: We need to subclass the setuptools install command rather than the # distutils command to make installing with pip from the source distribution # work. class install(orig_install): """Custom install command used to update the gtk icon cache.""" def run(self): """ Perform old-style (distutils) install, then update GTK icon cache. Extends ``distutils.command.install.install.run``. """ orig_install.run(self) try: call(['gtk-update-icon-cache', theme_base]) except OSError as e: # ignore failures since the tray icon is an optional component: logging.warn(e) class install_data(orig_install_data): def run(self): """Add built translation files and then install data files.""" self.data_files += [ (path.join(mo_install_prefix, lang, 'LC_MESSAGES'), [path.join(mo_build_prefix, lang, 'LC_MESSAGES', 'udiskie.mo')]) for lang in listdir(mo_build_prefix) ] self.data_files += [ (comp_install_prefix, [ path.join(comp_source_folder, cmd) for cmd in listdir(comp_source_folder) ]) ] orig_install_data.run(self) setup( name='udiskie', version=metadata['__version__'], description=metadata['__summary__'], long_description=long_description, author=metadata['__author__'], author_email=metadata['__author_email__'], maintainer=metadata['__maintainer__'], maintainer_email=metadata['__maintainer_email__'], url=metadata['__uri__'], license=metadata['__license__'], cmdclass={ 'install': install, 'install_data': install_data, 'build': build, 'build_mo': build_mo, }, packages=[ 'udiskie', ], data_files=[ (path.join(theme_base, 'scalable', 'actions'), [ path.join('icons', 'scalable', 'actions', 'udiskie-{0}.svg'.format(icon_name)) for icon_name in icon_names]) ], entry_points={ 'console_scripts': [ 'udiskie = udiskie.cli:Daemon.main', 'udiskie-mount = udiskie.cli:Mount.main', 'udiskie-umount = udiskie.cli:Umount.main', 'udiskie-info = udiskie.cli:Info.main', ], }, install_requires=[ 'PyYAML', 'docopt', # Currently not building out of the box: # 'PyGObject', ], extras_require={ 'password-cache': [ 'keyutils==0.3' ], }, tests_require=[ ], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: X11 Applications :: GTK', 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'License :: OSI Approved :: MIT License', 'Topic :: Desktop Environment', 'Topic :: Software Development', 'Topic :: System :: Filesystems', 'Topic :: System :: Hardware', 'Topic :: Utilities', ], ) udiskie-1.7.3/udiskie.egg-info/0000755000175000017500000000000013214317676017130 5ustar thomasthomas00000000000000udiskie-1.7.3/udiskie.egg-info/entry_points.txt0000644000175000017500000000025213214317676022425 0ustar thomasthomas00000000000000[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-1.7.3/udiskie.egg-info/top_level.txt0000644000175000017500000000001013214317676021651 0ustar thomasthomas00000000000000udiskie udiskie-1.7.3/udiskie.egg-info/SOURCES.txt0000644000175000017500000000204013214317676021010 0ustar thomasthomas00000000000000CHANGES.rst CONTRIBUTORS COPYING MANIFEST.in README.rst fastentrypoints.py setup.py completions/_udiskie completions/_udiskie-mount completions/_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/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/compat.py udiskie/config.py udiskie/dbus.py udiskie/depend.py udiskie/locale.py udiskie/mount.py udiskie/notify.py udiskie/prompt.py udiskie/tray.py udiskie/udisks1.py udiskie/udisks2.py udiskie.egg-info/PKG-INFO udiskie.egg-info/SOURCES.txt udiskie.egg-info/dependency_links.txt udiskie.egg-info/entry_points.txt udiskie.egg-info/requires.txt udiskie.egg-info/top_level.txtudiskie-1.7.3/udiskie.egg-info/requires.txt0000644000175000017500000000005613214317676021531 0ustar thomasthomas00000000000000PyYAML docopt [password-cache] keyutils==0.3 udiskie-1.7.3/udiskie.egg-info/PKG-INFO0000644000175000017500000004161713214317676020236 0ustar thomasthomas00000000000000Metadata-Version: 1.1 Name: udiskie Version: 1.7.3 Summary: Removable disk automounter for udisks Home-page: https://github.com/coldfix/udiskie Author: Thomas Gläßle Author-email: t_glaessle@gmx.de License: MIT Description-Content-Type: UNKNOWN Description: ======= udiskie ======= |Version| |License| *udiskie* is a UDisks_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. Its features include: - automount removable media - 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, requires UDisks2) - password caching (requires python keyutils 0.3) All features can be indidually enabled or disabled. **NOTE:** support for udisks1 and python2 is deprecated and will be discontinued in the next major version of udiskie. .. _UDisks: 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 CHANGELOG --------- 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 :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Desktop Environment Classifier: Topic :: Software Development Classifier: Topic :: System :: Filesystems Classifier: Topic :: System :: Hardware Classifier: Topic :: Utilities udiskie-1.7.3/udiskie.egg-info/dependency_links.txt0000644000175000017500000000000113214317676023176 0ustar thomasthomas00000000000000 udiskie-1.7.3/fastentrypoints.py0000644000175000017500000000737513173451745017643 0ustar thomasthomas00000000000000# Copyright (c) 2016, Aaron Christianson # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ''' Monkey patch setuptools to write faster console_scripts with this format: import sys from mymodule import entry_function sys.exit(entry_function()) This is better. (c) 2016, Aaron Christianson http://github.com/ninjaaron/fast-entry_points ''' from setuptools.command import easy_install import re TEMPLATE = '''\ # -*- coding: utf-8 -*- import re import sys from {0} import {1} if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) sys.exit({2}())''' @classmethod def get_args(cls, dist, header=None): """ Yield write_script() argument tuples for a distribution's console_scripts and gui_scripts entry points. """ if header is None: header = cls.get_header() spec = str(dist.as_requirement()) for type_ in 'console', 'gui': group = type_ + '_scripts' for name, ep in dist.get_entry_map(group).items(): # ensure_safe_name if re.search(r'[\\/]', name): raise ValueError("Path separators not allowed in script names") script_text = TEMPLATE.format( ep.module_name, ep.attrs[0], '.'.join(ep.attrs)) args = cls._get_script_args(type_, name, header, script_text) for res in args: yield res easy_install.ScriptWriter.get_args = get_args def main(): import os import re import shutil import sys dests = sys.argv[1:] or ['.'] filename = re.sub('\.pyc$', '.py', __file__) for dst in dests: shutil.copy(filename, dst) manifest_path = os.path.join(dst, 'MANIFEST.in') setup_path = os.path.join(dst, 'setup.py') # Insert the include statement to MANIFEST.in if not present with open(manifest_path, 'a+') as manifest: manifest.seek(0) manifest_content = manifest.read() if not 'include fastentrypoints.py' in manifest_content: manifest.write(('\n' if manifest_content else '') + 'include fastentrypoints.py') # Insert the import statement to setup.py if not present with open(setup_path, 'a+') as setup: setup.seek(0) setup_content = setup.read() if not 'import fastentrypoints' in setup_content: setup.seek(0) setup.truncate() setup.write('import fastentrypoints\n' + setup_content) print(__name__) udiskie-1.7.3/icons/0000755000175000017500000000000013214317676015114 5ustar thomasthomas00000000000000udiskie-1.7.3/icons/scalable/0000755000175000017500000000000013214317676016662 5ustar thomasthomas00000000000000udiskie-1.7.3/icons/scalable/actions/0000755000175000017500000000000013214317676020322 5ustar thomasthomas00000000000000udiskie-1.7.3/icons/scalable/actions/udiskie-mount.svg0000644000175000017500000007163212266540464023650 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.7.3/icons/scalable/actions/udiskie-detach.svg0000644000175000017500000001325712266540464023735 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.7.3/icons/scalable/actions/udiskie-eject.svg0000644000175000017500000002244512266540464023576 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.7.3/icons/scalable/actions/udiskie-unmount.svg0000644000175000017500000010147312266540464024210 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.7.3/icons/scalable/actions/udiskie-unlock.svg0000644000175000017500000002626512266540464024003 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.7.3/icons/scalable/actions/udiskie-lock.svg0000644000175000017500000002625412266540464023436 0ustar thomasthomas00000000000000 image/svg+xml udiskie-1.7.3/CONTRIBUTORS0000644000175000017500000000115113173451745015657 0ustar thomasthomas00000000000000Byron 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-1.7.3/PKG-INFO0000644000175000017500000004161713214317676015107 0ustar thomasthomas00000000000000Metadata-Version: 1.1 Name: udiskie Version: 1.7.3 Summary: Removable disk automounter for udisks Home-page: https://github.com/coldfix/udiskie Author: Thomas Gläßle Author-email: t_glaessle@gmx.de License: MIT Description-Content-Type: UNKNOWN Description: ======= udiskie ======= |Version| |License| *udiskie* is a UDisks_ front-end that allows to manage removeable media such as CDs or flash drives from userspace. Its features include: - automount removable media - 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, requires UDisks2) - password caching (requires python keyutils 0.3) All features can be indidually enabled or disabled. **NOTE:** support for udisks1 and python2 is deprecated and will be discontinued in the next major version of udiskie. .. _UDisks: 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 CHANGELOG --------- 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 :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Desktop Environment Classifier: Topic :: Software Development Classifier: Topic :: System :: Filesystems Classifier: Topic :: System :: Hardware Classifier: Topic :: Utilities udiskie-1.7.3/lang/0000755000175000017500000000000013214317676014722 5ustar thomasthomas00000000000000udiskie-1.7.3/lang/es_ES.po0000644000175000017500000003432413214314444016254 0ustar thomasthomas00000000000000msgid "" msgstr "" "Project-Id-Version: udiskie\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-03-27 10:13+0200\n" "PO-Revision-Date: 2014-07-23 18:43+0100\n" "Last-Translator: Alejandro Pérez \n" "Language-Team: Spanish alejandro.perez.mendez@gmail.com\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.5\n" #: ../udiskie/prompt.py:132 ../udiskie/prompt.py:143 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "Introduce la clave para {0.device_presentation}: " #: ../udiskie/prompt.py:176 msgid "Positional field in format string {!r} is deprecated." msgstr "" #: ../udiskie/prompt.py:185 msgid "Unknown device attribute {!r} in format string: {!r}" msgstr "" #: ../udiskie/prompt.py:241 #, 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/udisks1.py:480 ../udiskie/udisks2.py:589 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "Se encontró el dispositivo maestro \"{0}\": \"{1}\"" #: ../udiskie/udisks1.py:483 ../udiskie/udisks2.py:592 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "no se encontró dispositivo maestro para \"{0}\"" #: ../udiskie/udisks1.py:506 ../udiskie/udisks2.py:611 #, python-brace-format msgid "Daemon version: {0}" msgstr "" #: ../udiskie/udisks1.py:564 ../udiskie/udisks2.py:702 #, python-brace-format msgid "+++ {0}: {1}" msgstr "+++ {0}: {1}" #: ../udiskie/udisks1.py:680 #, python-brace-format msgid "{0} operation failed for device: {1}" msgstr "Falló la operación {0} para el dispositovo: {1}" #: ../udiskie/config.py:128 msgid "Unknown matching attribute: {!r}" msgstr "Atributo de filtrado desconocido: {!r}" #: ../udiskie/config.py:130 #, python-brace-format msgid "{0} created" msgstr "{0} creado" #: ../udiskie/config.py:133 msgid "{0}(match={1!r}, value={2!r})" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:159 #, fuzzy msgid "{0}(match={1!r}, {2}={3!r}) used for {4}" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:247 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:250 #, fuzzy msgid "Failed to read {0!r}: {1}" msgstr "Fallo al {0} {1}: {2}" #: ../udiskie/depend.py:46 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:52 msgid "X server not connected!" msgstr "¡Servidor X no conectado!" #: ../udiskie/mount.py:158 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "Fallo al {0} {1}: {2}" #: ../udiskie/mount.py:174 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "no se exploró {0}: no está montado" #: ../udiskie/mount.py:177 #, python-brace-format msgid "not browsing {0}: no program" msgstr "no se exploró {0}: no hay programa configurado" #: ../udiskie/mount.py:179 #, python-brace-format msgid "opening {0} on {0.mount_paths[0]}" msgstr "abriendo {0} en {0.mount_paths[0]}" #: ../udiskie/mount.py:181 #, python-brace-format msgid "opened {0} on {0.mount_paths[0]}" msgstr "se abrió {0} en {0.mount_paths[0]}" #: ../udiskie/mount.py:196 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "no se montó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:199 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "no se montó {0}: ya está montado" #: ../udiskie/mount.py:203 #, python-brace-format msgid "mounting {0} with {1}" msgstr "montando {0} en {1}" #: ../udiskie/mount.py:205 #, python-brace-format msgid "mounted {0} on {1}" msgstr "montado {0} en {1}" #: ../udiskie/mount.py:219 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "no se desmontó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:222 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "no se desmontó {0}: no estaba montado" #: ../udiskie/mount.py:224 #, python-brace-format msgid "unmounting {0}" msgstr "desmontando {0}" #: ../udiskie/mount.py:226 #, python-brace-format msgid "unmounted {0}" msgstr "desmontado {0}" #: ../udiskie/mount.py:241 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "no se desbloqueó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:244 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "no se desbloqueó {0}: ya está desbloqueado" #: ../udiskie/mount.py:247 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:257 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "no se desbloqueó {0}: cancellado por el usuario" #: ../udiskie/mount.py:259 #, python-brace-format msgid "unlocking {0}" msgstr "desbloqueando {0}" #: ../udiskie/mount.py:262 #, python-brace-format msgid "unlocked {0}" msgstr "desbloqueado {0}" #: ../udiskie/mount.py:273 #, fuzzy, python-brace-format msgid "unlocking {0} using cached password" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:277 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:279 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:293 #, python-brace-format msgid "configured keyfile for {0} not found" msgstr "" #: ../udiskie/mount.py:295 #, fuzzy, python-brace-format msgid "unlocking {0} using keyfile {1}" msgstr "no se desbloqueó {0}: no se introdujo la clave" #: ../udiskie/mount.py:299 #, python-brace-format msgid "failed to unlock {0} using keyfile" msgstr "" #: ../udiskie/mount.py:301 #, fuzzy, python-brace-format msgid "unlocked {0} using keyfile" msgstr "desbloqueado {0}" #: ../udiskie/mount.py:326 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "no se bloqueó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:329 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "no se bloqueó {0}: no estaba desbloqueado" #: ../udiskie/mount.py:331 #, python-brace-format msgid "locking {0}" msgstr "bloqueando {0}" #: ../udiskie/mount.py:333 #, python-brace-format msgid "locked {0}" msgstr "bloqueado {0}" #: ../udiskie/mount.py:369 ../udiskie/mount.py:409 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "no se añadió {0}: dispositivo no gestionado" #: ../udiskie/mount.py:446 ../udiskie/mount.py:496 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "no se eliminó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:522 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "no se expulsó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:526 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "no se expulsó {0}: dispositivo no expulsable" #: ../udiskie/mount.py:532 #, python-brace-format msgid "ejecting {0}" msgstr "expulsando {0}" #: ../udiskie/mount.py:534 #, python-brace-format msgid "ejected {0}" msgstr "expulsado {0}" #: ../udiskie/mount.py:549 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "no se desconectó {0}: unhandled device" #: ../udiskie/mount.py:553 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "no se desconectó {0}: dispositivo no desconectable" #: ../udiskie/mount.py:557 #, python-brace-format msgid "detaching {0}" msgstr "desconectando {0}" #: ../udiskie/mount.py:559 #, python-brace-format msgid "detached {0}" msgstr "desconectado {0}" #: ../udiskie/mount.py:615 #, fuzzy, python-brace-format msgid "not setting up {0}: already up" msgstr "no se montó {0}: ya está montado" #: ../udiskie/mount.py:618 #, fuzzy, python-brace-format msgid "not setting up {0}: not a file" msgstr "no se expulsó {0}: dispositivo no expulsable" #: ../udiskie/mount.py:621 #, python-brace-format msgid "not setting up {0}: unsupported in UDisks1" msgstr "" #: ../udiskie/mount.py:623 #, fuzzy, python-brace-format msgid "setting up {0}" msgstr "expulsando {0}" #: ../udiskie/mount.py:631 #, python-brace-format msgid "set up {0} as {1}" msgstr "" #: ../udiskie/mount.py:647 #, fuzzy, python-brace-format msgid "not deleting {0}: unhandled device" msgstr "no se expulsó {0}: dispositivo no gestionado" #: ../udiskie/mount.py:651 #, fuzzy, python-brace-format msgid "deleting {0}" msgstr "expulsando {0}" #: ../udiskie/mount.py:653 #, fuzzy, python-brace-format msgid "deleted {0}" msgstr "expulsado {0}" #: ../udiskie/mount.py:809 #, python-brace-format msgid "Browse {0}" msgstr "Explorar {0}" #: ../udiskie/mount.py:810 #, python-brace-format msgid "Mount {0}" msgstr "Montar {0}" #: ../udiskie/mount.py:811 #, python-brace-format msgid "Unmount {0}" msgstr "Desmontar {0}" #: ../udiskie/mount.py:812 #, python-brace-format msgid "Unlock {0}" msgstr "Desbloquear {0}" #: ../udiskie/mount.py:813 #, python-brace-format msgid "Lock {0}" msgstr "Bloquear {0}" #: ../udiskie/mount.py:814 #, fuzzy, python-brace-format msgid "Eject {1}" msgstr "Expulsar {1}" #: ../udiskie/mount.py:815 #, fuzzy, python-brace-format msgid "Unpower {1}" msgstr "Apagar {1}" #: ../udiskie/mount.py:816 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/mount.py:817 #, fuzzy, python-brace-format msgid "Detach {0}" msgstr "desconectado {0}" #: ../udiskie/tray.py:167 msgid "Mount disc image" msgstr "" #: ../udiskie/tray.py:173 msgid "Enable automounting" msgstr "" #: ../udiskie/tray.py:179 msgid "Enable notifications" msgstr "" #: ../udiskie/tray.py:188 msgid "Quit" msgstr "Salir" #: ../udiskie/tray.py:196 msgid "Open disc image" msgstr "" #: ../udiskie/tray.py:198 msgid "Open" msgstr "" #: ../udiskie/tray.py:199 msgid "Cancel" msgstr "" #: ../udiskie/tray.py:247 msgid "Invalid node!" msgstr "¡Nodo inválido!" #: ../udiskie/tray.py:249 msgid "No external devices" msgstr "" #: ../udiskie/tray.py:353 msgid "udiskie" msgstr "udiskie" #: ../udiskie/notify.py:68 msgid "Browse directory" msgstr "Navegar directorio" #: ../udiskie/notify.py:72 msgid "Device mounted" msgstr "Dispositivo montado" #: ../udiskie/notify.py:73 #, 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:87 msgid "Device unmounted" msgstr "Dispositivo desmontado" #: ../udiskie/notify.py:88 #, fuzzy, python-brace-format msgid "{0.ui_label} unmounted" msgstr "{0.ui_label} desmontado" #: ../udiskie/notify.py:101 msgid "Device locked" msgstr "Dispositivo bloqueado" #: ../udiskie/notify.py:102 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "{0.device_presentation} bloqueado" #: ../udiskie/notify.py:115 msgid "Device unlocked" msgstr "Dispositivo desbloqueado" #: ../udiskie/notify.py:116 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "{0.device_presentation} desbloqueado" #: ../udiskie/notify.py:153 msgid "Device added" msgstr "Dispositivo añadido" #: ../udiskie/notify.py:154 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "Dispositivo apareció en {0.device_presentation}" #: ../udiskie/notify.py:177 msgid "Device removed" msgstr "Dispositivo retirado" #: ../udiskie/notify.py:178 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "el dispositivo despareció en {0.device_presentation} " #: ../udiskie/notify.py:191 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" "fallo al {0} {1}:\n" "{2}" #: ../udiskie/notify.py:193 #, python-brace-format msgid "failed to {0} device {1}." msgstr "fallo al {0} el dispositivo {1}." #: ../udiskie/notify.py:199 msgid "Retry" msgstr "Reintentar" #: ../udiskie/notify.py:202 msgid "Job failed" msgstr "Falló la tarea." #: ../udiskie/notify.py:233 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" #: ../udiskie/cli.py:57 #, fuzzy msgid "" "Failed to connect UDisks2 dbus service..\n" "Falling back to UDisks1." msgstr "" "Fallo al conectar al servicio dbus UDisks1.\n" "Usando UDisk2 [experimental]." #: ../udiskie/cli.py:67 #, python-brace-format msgid "UDisks version not supported: {0}!" msgstr "¡Versión de UDisks no soportada: {0}!" #: ../udiskie/cli.py:82 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "Estas opciones son excluyentes: {0}" #: ../udiskie/cli.py:157 #, 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:182 #, python-format msgid "%(message)s" msgstr "%(message)s" #: ../udiskie/cli.py:183 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "%(levelname)s [%(asctime)s] %(name)s: %(message)s" #: ../udiskie/cli.py:436 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:446 msgid "" "Not run within X session. \n" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/cli.py:453 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 "" #~ msgid "{0} used for {1}" #~ msgstr "{0} usado para {1}" #~ msgid "Device not found: {0}" #~ msgstr "Dispositivo no encontrado: {0}" #~ msgid "Interface {0!r} not available for {1}" #~ msgstr "Interfaz {0!r} no disponible para {1}" udiskie-1.7.3/lang/udiskie.pot0000644000175000017500000002613013214314444017073 0ustar thomasthomas00000000000000# 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: 2017-03-27 10:13+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../udiskie/prompt.py:132 ../udiskie/prompt.py:143 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "" #: ../udiskie/prompt.py:176 msgid "Positional field in format string {!r} is deprecated." msgstr "" #: ../udiskie/prompt.py:185 msgid "Unknown device attribute {!r} in format string: {!r}" msgstr "" #: ../udiskie/prompt.py:241 msgid "" "Can't find file browser: {0!r}. You may want to change the value for the '-" "f' option." msgstr "" #: ../udiskie/udisks1.py:480 ../udiskie/udisks2.py:589 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "" #: ../udiskie/udisks1.py:483 ../udiskie/udisks2.py:592 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "" #: ../udiskie/udisks1.py:506 ../udiskie/udisks2.py:611 #, python-brace-format msgid "Daemon version: {0}" msgstr "" #: ../udiskie/udisks1.py:564 ../udiskie/udisks2.py:702 #, python-brace-format msgid "+++ {0}: {1}" msgstr "" #: ../udiskie/udisks1.py:680 #, python-brace-format msgid "{0} operation failed for device: {1}" msgstr "" #: ../udiskie/config.py:128 msgid "Unknown matching attribute: {!r}" msgstr "" #: ../udiskie/config.py:130 #, python-brace-format msgid "{0} created" msgstr "" #: ../udiskie/config.py:133 msgid "{0}(match={1!r}, value={2!r})" msgstr "" #: ../udiskie/config.py:159 msgid "{0}(match={1!r}, {2}={3!r}) used for {4}" msgstr "" #: ../udiskie/config.py:247 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:250 msgid "Failed to read {0!r}: {1}" msgstr "" #: ../udiskie/depend.py:46 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:52 msgid "X server not connected!" msgstr "" #: ../udiskie/mount.py:158 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "" #: ../udiskie/mount.py:174 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "" #: ../udiskie/mount.py:177 #, python-brace-format msgid "not browsing {0}: no program" msgstr "" #: ../udiskie/mount.py:179 #, python-brace-format msgid "opening {0} on {0.mount_paths[0]}" msgstr "" #: ../udiskie/mount.py:181 #, python-brace-format msgid "opened {0} on {0.mount_paths[0]}" msgstr "" #: ../udiskie/mount.py:196 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:199 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "" #: ../udiskie/mount.py:203 #, python-brace-format msgid "mounting {0} with {1}" msgstr "" #: ../udiskie/mount.py:205 #, python-brace-format msgid "mounted {0} on {1}" msgstr "" #: ../udiskie/mount.py:219 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:222 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "" #: ../udiskie/mount.py:224 #, python-brace-format msgid "unmounting {0}" msgstr "" #: ../udiskie/mount.py:226 #, python-brace-format msgid "unmounted {0}" msgstr "" #: ../udiskie/mount.py:241 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:244 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "" #: ../udiskie/mount.py:247 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "" #: ../udiskie/mount.py:257 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "" #: ../udiskie/mount.py:259 #, python-brace-format msgid "unlocking {0}" msgstr "" #: ../udiskie/mount.py:262 #, python-brace-format msgid "unlocked {0}" msgstr "" #: ../udiskie/mount.py:273 #, python-brace-format msgid "unlocking {0} using cached password" msgstr "" #: ../udiskie/mount.py:277 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:279 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:293 #, python-brace-format msgid "configured keyfile for {0} not found" msgstr "" #: ../udiskie/mount.py:295 #, python-brace-format msgid "unlocking {0} using keyfile {1}" msgstr "" #: ../udiskie/mount.py:299 #, python-brace-format msgid "failed to unlock {0} using keyfile" msgstr "" #: ../udiskie/mount.py:301 #, python-brace-format msgid "unlocked {0} using keyfile" msgstr "" #: ../udiskie/mount.py:326 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:329 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "" #: ../udiskie/mount.py:331 #, python-brace-format msgid "locking {0}" msgstr "" #: ../udiskie/mount.py:333 #, python-brace-format msgid "locked {0}" msgstr "" #: ../udiskie/mount.py:369 ../udiskie/mount.py:409 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:446 ../udiskie/mount.py:496 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:522 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:526 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "" #: ../udiskie/mount.py:532 #, python-brace-format msgid "ejecting {0}" msgstr "" #: ../udiskie/mount.py:534 #, python-brace-format msgid "ejected {0}" msgstr "" #: ../udiskie/mount.py:549 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:553 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "" #: ../udiskie/mount.py:557 #, python-brace-format msgid "detaching {0}" msgstr "" #: ../udiskie/mount.py:559 #, python-brace-format msgid "detached {0}" msgstr "" #: ../udiskie/mount.py:615 #, python-brace-format msgid "not setting up {0}: already up" msgstr "" #: ../udiskie/mount.py:618 #, python-brace-format msgid "not setting up {0}: not a file" msgstr "" #: ../udiskie/mount.py:621 #, python-brace-format msgid "not setting up {0}: unsupported in UDisks1" msgstr "" #: ../udiskie/mount.py:623 #, python-brace-format msgid "setting up {0}" msgstr "" #: ../udiskie/mount.py:631 #, python-brace-format msgid "set up {0} as {1}" msgstr "" #: ../udiskie/mount.py:647 #, python-brace-format msgid "not deleting {0}: unhandled device" msgstr "" #: ../udiskie/mount.py:651 #, python-brace-format msgid "deleting {0}" msgstr "" #: ../udiskie/mount.py:653 #, python-brace-format msgid "deleted {0}" msgstr "" #: ../udiskie/mount.py:809 #, python-brace-format msgid "Browse {0}" msgstr "" #: ../udiskie/mount.py:810 #, python-brace-format msgid "Mount {0}" msgstr "" #: ../udiskie/mount.py:811 #, python-brace-format msgid "Unmount {0}" msgstr "" #: ../udiskie/mount.py:812 #, python-brace-format msgid "Unlock {0}" msgstr "" #: ../udiskie/mount.py:813 #, python-brace-format msgid "Lock {0}" msgstr "" #: ../udiskie/mount.py:814 #, python-brace-format msgid "Eject {1}" msgstr "" #: ../udiskie/mount.py:815 #, python-brace-format msgid "Unpower {1}" msgstr "" #: ../udiskie/mount.py:816 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/mount.py:817 #, python-brace-format msgid "Detach {0}" msgstr "" #: ../udiskie/tray.py:167 msgid "Mount disc image" msgstr "" #: ../udiskie/tray.py:173 msgid "Enable automounting" msgstr "" #: ../udiskie/tray.py:179 msgid "Enable notifications" msgstr "" #: ../udiskie/tray.py:188 msgid "Quit" msgstr "" #: ../udiskie/tray.py:196 msgid "Open disc image" msgstr "" #: ../udiskie/tray.py:198 msgid "Open" msgstr "" #: ../udiskie/tray.py:199 msgid "Cancel" msgstr "" #: ../udiskie/tray.py:247 msgid "Invalid node!" msgstr "" #: ../udiskie/tray.py:249 msgid "No external devices" msgstr "" #: ../udiskie/tray.py:353 msgid "udiskie" msgstr "" #: ../udiskie/notify.py:68 msgid "Browse directory" msgstr "" #: ../udiskie/notify.py:72 msgid "Device mounted" msgstr "" #: ../udiskie/notify.py:73 #, python-brace-format msgid "{0.ui_label} mounted on {0.mount_paths[0]}" msgstr "" #: ../udiskie/notify.py:87 msgid "Device unmounted" msgstr "" #: ../udiskie/notify.py:88 #, python-brace-format msgid "{0.ui_label} unmounted" msgstr "" #: ../udiskie/notify.py:101 msgid "Device locked" msgstr "" #: ../udiskie/notify.py:102 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "" #: ../udiskie/notify.py:115 msgid "Device unlocked" msgstr "" #: ../udiskie/notify.py:116 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "" #: ../udiskie/notify.py:153 msgid "Device added" msgstr "" #: ../udiskie/notify.py:154 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "" #: ../udiskie/notify.py:177 msgid "Device removed" msgstr "" #: ../udiskie/notify.py:178 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "" #: ../udiskie/notify.py:191 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" #: ../udiskie/notify.py:193 #, python-brace-format msgid "failed to {0} device {1}." msgstr "" #: ../udiskie/notify.py:199 msgid "Retry" msgstr "" #: ../udiskie/notify.py:202 msgid "Job failed" msgstr "" #: ../udiskie/notify.py:233 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" #: ../udiskie/cli.py:57 msgid "" "Failed to connect UDisks2 dbus service..\n" "Falling back to UDisks1." msgstr "" #: ../udiskie/cli.py:67 #, python-brace-format msgid "UDisks version not supported: {0}!" msgstr "" #: ../udiskie/cli.py:82 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "" #: ../udiskie/cli.py:157 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:182 #, python-format msgid "%(message)s" msgstr "" #: ../udiskie/cli.py:183 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "" #: ../udiskie/cli.py:436 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:446 msgid "" "Not run within X session. \n" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/cli.py:453 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-1.7.3/lang/en_US.po0000644000175000017500000003325313214314444016267 0ustar thomasthomas00000000000000msgid "" msgstr "" "Project-Id-Version: udiskie\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-03-27 10:13+0200\n" "PO-Revision-Date: 2014-07-17 19:27+0200\n" "Last-Translator: Thomas Gläßle \n" "Language-Team: English t_glaesle@gmx.de\n" "Language: en_US\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: ../udiskie/prompt.py:132 ../udiskie/prompt.py:143 #, python-brace-format msgid "Enter password for {0.device_presentation}: " msgstr "Enter password for {0.device_presentation}: " #: ../udiskie/prompt.py:176 msgid "Positional field in format string {!r} is deprecated." msgstr "" #: ../udiskie/prompt.py:185 msgid "Unknown device attribute {!r} in format string: {!r}" msgstr "" #: ../udiskie/prompt.py:241 #, 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/udisks1.py:480 ../udiskie/udisks2.py:589 #, python-brace-format msgid "found device owning \"{0}\": \"{1}\"" msgstr "found device owning \"{0}\": \"{1}\"" #: ../udiskie/udisks1.py:483 ../udiskie/udisks2.py:592 #, python-brace-format msgid "no device found owning \"{0}\"" msgstr "no device found owning \"{0}\"" #: ../udiskie/udisks1.py:506 ../udiskie/udisks2.py:611 #, python-brace-format msgid "Daemon version: {0}" msgstr "" #: ../udiskie/udisks1.py:564 ../udiskie/udisks2.py:702 #, python-brace-format msgid "+++ {0}: {1}" msgstr "+++ {0}: {1}" #: ../udiskie/udisks1.py:680 #, python-brace-format msgid "{0} operation failed for device: {1}" msgstr "{0} operation failed for device: {1}" #: ../udiskie/config.py:128 msgid "Unknown matching attribute: {!r}" msgstr "Unknown matching attribute: {!r}" #: ../udiskie/config.py:130 #, python-brace-format msgid "{0} created" msgstr "{0} created" #: ../udiskie/config.py:133 msgid "{0}(match={1!r}, value={2!r})" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:159 #, fuzzy msgid "{0}(match={1!r}, {2}={3!r}) used for {4}" msgstr "{0}(match={1!r}, value={2!r})" #: ../udiskie/config.py:247 #, python-brace-format msgid "Failed to read config file: {0}" msgstr "" #: ../udiskie/config.py:250 #, fuzzy msgid "Failed to read {0!r}: {1}" msgstr "failed to {0} {1}: {2}" #: ../udiskie/depend.py:46 msgid "" "Missing runtime dependency GTK 3. Falling back to GTK 2 for password prompt" msgstr "" #: ../udiskie/depend.py:52 msgid "X server not connected!" msgstr "X server not connected!" #: ../udiskie/mount.py:158 #, python-brace-format msgid "failed to {0} {1}: {2}" msgstr "failed to {0} {1}: {2}" #: ../udiskie/mount.py:174 #, python-brace-format msgid "not browsing {0}: not mounted" msgstr "not browsing {0}: not mounted" #: ../udiskie/mount.py:177 #, python-brace-format msgid "not browsing {0}: no program" msgstr "not browsing {0}: no program" #: ../udiskie/mount.py:179 #, python-brace-format msgid "opening {0} on {0.mount_paths[0]}" msgstr "opening {0} on {0.mount_paths[0]}" #: ../udiskie/mount.py:181 #, python-brace-format msgid "opened {0} on {0.mount_paths[0]}" msgstr "opened {0} on {0.mount_paths[0]}" #: ../udiskie/mount.py:196 #, python-brace-format msgid "not mounting {0}: unhandled device" msgstr "not mounting {0}: unhandled device" #: ../udiskie/mount.py:199 #, python-brace-format msgid "not mounting {0}: already mounted" msgstr "not mounting {0}: already mounted" #: ../udiskie/mount.py:203 #, python-brace-format msgid "mounting {0} with {1}" msgstr "mounting {0} with {1}" #: ../udiskie/mount.py:205 #, python-brace-format msgid "mounted {0} on {1}" msgstr "mounted {0} on {1}" #: ../udiskie/mount.py:219 #, python-brace-format msgid "not unmounting {0}: unhandled device" msgstr "not unmounting {0}: unhandled device" #: ../udiskie/mount.py:222 #, python-brace-format msgid "not unmounting {0}: not mounted" msgstr "not unmounting {0}: not mounted" #: ../udiskie/mount.py:224 #, python-brace-format msgid "unmounting {0}" msgstr "unmounting {0}" #: ../udiskie/mount.py:226 #, python-brace-format msgid "unmounted {0}" msgstr "unmounted {0}" #: ../udiskie/mount.py:241 #, python-brace-format msgid "not unlocking {0}: unhandled device" msgstr "not unlocking {0}: unhandled device" #: ../udiskie/mount.py:244 #, python-brace-format msgid "not unlocking {0}: already unlocked" msgstr "not unlocking {0}: already unlocked" #: ../udiskie/mount.py:247 #, python-brace-format msgid "not unlocking {0}: no password prompt" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:257 #, python-brace-format msgid "not unlocking {0}: cancelled by user" msgstr "not unlocking {0}: cancelled by user" #: ../udiskie/mount.py:259 #, python-brace-format msgid "unlocking {0}" msgstr "unlocking {0}" #: ../udiskie/mount.py:262 #, python-brace-format msgid "unlocked {0}" msgstr "unlocked {0}" #: ../udiskie/mount.py:273 #, fuzzy, python-brace-format msgid "unlocking {0} using cached password" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:277 #, python-brace-format msgid "failed to unlock {0} using cached password" msgstr "" #: ../udiskie/mount.py:279 #, python-brace-format msgid "unlocked {0} using cached password" msgstr "" #: ../udiskie/mount.py:293 #, python-brace-format msgid "configured keyfile for {0} not found" msgstr "" #: ../udiskie/mount.py:295 #, fuzzy, python-brace-format msgid "unlocking {0} using keyfile {1}" msgstr "not unlocking {0}: no password prompt" #: ../udiskie/mount.py:299 #, python-brace-format msgid "failed to unlock {0} using keyfile" msgstr "" #: ../udiskie/mount.py:301 #, fuzzy, python-brace-format msgid "unlocked {0} using keyfile" msgstr "unlocked {0}" #: ../udiskie/mount.py:326 #, python-brace-format msgid "not locking {0}: unhandled device" msgstr "not locking {0}: unhandled device" #: ../udiskie/mount.py:329 #, python-brace-format msgid "not locking {0}: not unlocked" msgstr "not locking {0}: not unlocked" #: ../udiskie/mount.py:331 #, python-brace-format msgid "locking {0}" msgstr "locking {0}" #: ../udiskie/mount.py:333 #, python-brace-format msgid "locked {0}" msgstr "locked {0}" #: ../udiskie/mount.py:369 ../udiskie/mount.py:409 #, python-brace-format msgid "not adding {0}: unhandled device" msgstr "not adding {0}: unhandled device" #: ../udiskie/mount.py:446 ../udiskie/mount.py:496 #, python-brace-format msgid "not removing {0}: unhandled device" msgstr "not removing {0}: unhandled device" #: ../udiskie/mount.py:522 #, python-brace-format msgid "not ejecting {0}: unhandled device" msgstr "not ejecting {0}: unhandled device" #: ../udiskie/mount.py:526 #, python-brace-format msgid "not ejecting {0}: drive not ejectable" msgstr "not ejecting {0}: drive not ejectable" #: ../udiskie/mount.py:532 #, python-brace-format msgid "ejecting {0}" msgstr "ejecting {0}" #: ../udiskie/mount.py:534 #, python-brace-format msgid "ejected {0}" msgstr "ejected {0}" #: ../udiskie/mount.py:549 #, python-brace-format msgid "not detaching {0}: unhandled device" msgstr "not detaching {0}: unhandled device" #: ../udiskie/mount.py:553 #, python-brace-format msgid "not detaching {0}: drive not detachable" msgstr "not detaching {0}: drive not detachable" #: ../udiskie/mount.py:557 #, python-brace-format msgid "detaching {0}" msgstr "detaching {0}" #: ../udiskie/mount.py:559 #, python-brace-format msgid "detached {0}" msgstr "detached {0}" #: ../udiskie/mount.py:615 #, fuzzy, python-brace-format msgid "not setting up {0}: already up" msgstr "not mounting {0}: already mounted" #: ../udiskie/mount.py:618 #, fuzzy, python-brace-format msgid "not setting up {0}: not a file" msgstr "not ejecting {0}: drive not ejectable" #: ../udiskie/mount.py:621 #, python-brace-format msgid "not setting up {0}: unsupported in UDisks1" msgstr "" #: ../udiskie/mount.py:623 #, fuzzy, python-brace-format msgid "setting up {0}" msgstr "ejecting {0}" #: ../udiskie/mount.py:631 #, python-brace-format msgid "set up {0} as {1}" msgstr "" #: ../udiskie/mount.py:647 #, fuzzy, python-brace-format msgid "not deleting {0}: unhandled device" msgstr "not ejecting {0}: unhandled device" #: ../udiskie/mount.py:651 #, fuzzy, python-brace-format msgid "deleting {0}" msgstr "ejecting {0}" #: ../udiskie/mount.py:653 #, fuzzy, python-brace-format msgid "deleted {0}" msgstr "ejected {0}" #: ../udiskie/mount.py:809 #, python-brace-format msgid "Browse {0}" msgstr "Browse {0}" #: ../udiskie/mount.py:810 #, python-brace-format msgid "Mount {0}" msgstr "Mount {0}" #: ../udiskie/mount.py:811 #, python-brace-format msgid "Unmount {0}" msgstr "Unmount {0}" #: ../udiskie/mount.py:812 #, python-brace-format msgid "Unlock {0}" msgstr "Unlock {0}" #: ../udiskie/mount.py:813 #, python-brace-format msgid "Lock {0}" msgstr "Lock {0}" #: ../udiskie/mount.py:814 #, fuzzy, python-brace-format msgid "Eject {1}" msgstr "Eject {0}" #: ../udiskie/mount.py:815 #, fuzzy, python-brace-format msgid "Unpower {1}" msgstr "Unpower {0}" #: ../udiskie/mount.py:816 #, python-brace-format msgid "Clear password for {0}" msgstr "" #: ../udiskie/mount.py:817 #, fuzzy, python-brace-format msgid "Detach {0}" msgstr "detached {0}" #: ../udiskie/tray.py:167 msgid "Mount disc image" msgstr "" #: ../udiskie/tray.py:173 msgid "Enable automounting" msgstr "" #: ../udiskie/tray.py:179 msgid "Enable notifications" msgstr "" #: ../udiskie/tray.py:188 msgid "Quit" msgstr "Quit" #: ../udiskie/tray.py:196 msgid "Open disc image" msgstr "" #: ../udiskie/tray.py:198 msgid "Open" msgstr "" #: ../udiskie/tray.py:199 msgid "Cancel" msgstr "" #: ../udiskie/tray.py:247 msgid "Invalid node!" msgstr "Invalid node!" #: ../udiskie/tray.py:249 msgid "No external devices" msgstr "" #: ../udiskie/tray.py:353 msgid "udiskie" msgstr "udiskie" #: ../udiskie/notify.py:68 msgid "Browse directory" msgstr "Browse directory" #: ../udiskie/notify.py:72 msgid "Device mounted" msgstr "Device mounted" #: ../udiskie/notify.py:73 #, 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:87 msgid "Device unmounted" msgstr "Device unmounted" #: ../udiskie/notify.py:88 #, fuzzy, python-brace-format msgid "{0.ui_label} unmounted" msgstr "{0.id_label} unmounted" #: ../udiskie/notify.py:101 msgid "Device locked" msgstr "Device locked" #: ../udiskie/notify.py:102 #, python-brace-format msgid "{0.device_presentation} locked" msgstr "{0.device_presentation} locked" #: ../udiskie/notify.py:115 msgid "Device unlocked" msgstr "Device unlocked" #: ../udiskie/notify.py:116 #, python-brace-format msgid "{0.device_presentation} unlocked" msgstr "{0.device_presentation} unlocked" #: ../udiskie/notify.py:153 msgid "Device added" msgstr "Device added" #: ../udiskie/notify.py:154 #, python-brace-format msgid "device appeared on {0.device_presentation}" msgstr "device appeared on {0.device_presentation}" #: ../udiskie/notify.py:177 msgid "Device removed" msgstr "Device removed" #: ../udiskie/notify.py:178 #, python-brace-format msgid "device disappeared on {0.device_presentation}" msgstr "device disappeared on {0.device_presentation}" #: ../udiskie/notify.py:191 #, python-brace-format msgid "" "failed to {0} {1}:\n" "{2}" msgstr "" "failed to {0} {1}:\n" "{2}" #: ../udiskie/notify.py:193 #, python-brace-format msgid "failed to {0} device {1}." msgstr "failed to {0} device {1}." #: ../udiskie/notify.py:199 msgid "Retry" msgstr "Retry" #: ../udiskie/notify.py:202 msgid "Job failed" msgstr "Job failed" #: ../udiskie/notify.py:233 #, python-brace-format msgid "Failed to show notification: {0}" msgstr "" #: ../udiskie/cli.py:57 #, fuzzy msgid "" "Failed to connect UDisks2 dbus service..\n" "Falling back to UDisks1." msgstr "" "Failed to connect UDisks1 dbus service..\n" "Falling back to UDisks2 [experimental]." #: ../udiskie/cli.py:67 #, python-brace-format msgid "UDisks version not supported: {0}!" msgstr "UDisks version not supported: {0}!" #: ../udiskie/cli.py:82 #, python-brace-format msgid "These options are mutually exclusive: {0}" msgstr "These options are mutually exclusive: {0}" #: ../udiskie/cli.py:157 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:182 #, python-format msgid "%(message)s" msgstr "%(message)s" #: ../udiskie/cli.py:183 #, python-format msgid "%(levelname)s [%(asctime)s] %(name)s: %(message)s" msgstr "%(levelname)s [%(asctime)s] %(name)s: %(message)s" #: ../udiskie/cli.py:436 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:446 msgid "" "Not run within X session. \n" "Starting udiskie without tray icon.\n" msgstr "" #: ../udiskie/cli.py:453 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 "" #~ msgid "{0} used for {1}" #~ msgstr "{0} used for {1}" #~ msgid "Device not found: {0}" #~ msgstr "Device not found: {0}" #~ msgid "Interface {0!r} not available for {1}" #~ msgstr "Interface {0!r} not available for {1}"