%PDF- %PDF-
| Direktori : /proc/thread-self/root/usr/lib/calibre/calibre/devices/mtp/windows/ |
| Current File : //proc/thread-self/root/usr/lib/calibre/calibre/devices/mtp/windows/driver.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import time, threading, traceback
from functools import wraps, partial
from polyglot.builtins import iteritems, itervalues
from itertools import chain
from calibre import as_unicode, prints, force_unicode
from calibre.constants import __appname__, numeric_version, isxp
from calibre.ptempfile import SpooledTemporaryFile
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
from calibre.devices.mtp.base import MTPDeviceBase, debug
null = object()
class ThreadingViolation(Exception):
def __init__(self):
Exception.__init__(self,
'You cannot use the MTP driver from a thread other than the '
' thread in which startup() was called')
def same_thread(func):
@wraps(func)
def check_thread(self, *args, **kwargs):
if self.start_thread is not threading.current_thread():
raise ThreadingViolation()
return func(self, *args, **kwargs)
return check_thread
class MTP_DEVICE(MTPDeviceBase):
supported_platforms = ['windows']
def __init__(self, *args, **kwargs):
MTPDeviceBase.__init__(self, *args, **kwargs)
self.dev = None
self.blacklisted_devices = set()
self.ejected_devices = set()
self.currently_connected_pnp_id = None
self.detected_devices = {}
self.previous_devices_on_system = frozenset()
self.last_refresh_devices_time = time.time()
self.wpd = self.wpd_error = None
self._main_id = self._carda_id = self._cardb_id = None
self.start_thread = None
self._filesystem_cache = None
self.eject_dev_on_next_scan = False
self.current_device_data = {}
def startup(self):
self.start_thread = threading.current_thread()
if isxp:
self.wpd = None
self.wpd_error = _('MTP devices are not supported on Windows XP')
else:
try:
from calibre_extensions import wpd
self.wpd = wpd
except Exception as err:
self.wpd = None
self.wpd_error = as_unicode(err)
if self.wpd is not None:
try:
self.wpd.init(__appname__, *(numeric_version[:3]))
except self.wpd.NoWPD:
self.wpd_error = _(
'The Windows Portable Devices service is not available'
' on your computer. You may need to install Windows'
' Media Player 11 or newer and/or restart your computer')
except Exception as e:
self.wpd_error = as_unicode(e)
@same_thread
def shutdown(self):
self.dev = self._filesystem_cache = self.start_thread = None
if self.wpd is not None:
self.wpd.uninit()
@same_thread
def detect_managed_devices(self, devices_on_system, force_refresh=False):
if self.wpd is None:
return None
if self.eject_dev_on_next_scan:
self.eject_dev_on_next_scan = False
if self.currently_connected_pnp_id is not None:
self.do_eject()
devices_on_system = frozenset(devices_on_system)
if (force_refresh or
devices_on_system != self.previous_devices_on_system or
time.time() - self.last_refresh_devices_time > 10):
self.previous_devices_on_system = devices_on_system
self.last_refresh_devices_time = time.time()
try:
pnp_ids = frozenset(self.wpd.enumerate_devices())
except:
return None
self.detected_devices = {dev:self.detected_devices.get(dev, None)
for dev in pnp_ids}
# Get device data for detected devices. If there is an error, we will
# try again for that device the next time this method is called.
for dev in tuple(self.detected_devices):
data = self.detected_devices.get(dev, None)
if data is None or data is False:
try:
data = self.wpd.device_info(dev)
except Exception as e:
prints('Failed to get device info for device:', dev,
as_unicode(e))
data = {} if data is False else False
self.detected_devices[dev] = data
# Remove devices that have been disconnected from ejected
# devices and blacklisted devices
self.ejected_devices = set(self.detected_devices).intersection(
self.ejected_devices)
self.blacklisted_devices = set(self.detected_devices).intersection(
self.blacklisted_devices)
if self.currently_connected_pnp_id is not None:
return (self.currently_connected_pnp_id if
self.currently_connected_pnp_id in self.detected_devices
else None)
for dev, data in iteritems(self.detected_devices):
if dev in self.blacklisted_devices or dev in self.ejected_devices:
# Ignore blacklisted and ejected devices
continue
if data and self.is_suitable_wpd_device(data):
return dev
return None
@same_thread
def debug_managed_device_detection(self, devices_on_system, output):
import pprint
p = partial(prints, file=output)
if self.currently_connected_pnp_id is not None:
return True
if self.wpd_error:
p('Cannot detect MTP devices')
p(force_unicode(self.wpd_error))
return False
try:
pnp_ids = frozenset(self.wpd.enumerate_devices())
except:
p("Failed to get list of PNP ids on system")
p(traceback.format_exc())
return False
if not pnp_ids:
p('The Windows WPD service says there are no portable devices connected')
return False
p('List of WPD PNP ids:')
p(pprint.pformat(list(pnp_ids)))
for pnp_id in pnp_ids:
try:
data = self.wpd.device_info(pnp_id)
except:
p('Failed to get data for device:', pnp_id)
p(traceback.format_exc())
continue
protocol = data.get('protocol', '').lower()
if not protocol.startswith('mtp:'):
continue
p('MTP device:', pnp_id)
p(pprint.pformat(data))
if not self.is_suitable_wpd_device(data):
p('Not a suitable MTP device, ignoring\n')
continue
p('\nTrying to open:', pnp_id)
try:
self.open(pnp_id, 'debug-detection')
except BlacklistedDevice:
p('This device has been blacklisted by the user')
continue
except:
p('Open failed:')
p(traceback.format_exc())
continue
break
if self.currently_connected_pnp_id:
p('Opened', self.current_friendly_name, 'successfully')
p('Device info:')
p(pprint.pformat(self.dev.data))
self.post_yank_cleanup()
return True
p('No suitable MTP devices found')
return False
def is_suitable_wpd_device(self, devdata):
# Check that protocol is MTP
protocol = devdata.get('protocol', '').lower()
if not protocol.startswith('mtp:'):
return False
# Check that the device has some read-write storage
if not devdata.get('has_storage', False):
return False
has_rw_storage = False
for s in devdata.get('storage', []):
if s.get('filesystem', None) == 'DCF':
# DCF filesystem indicates a camera or an iPhone
# See https://bugs.launchpad.net/calibre/+bug/1054562
continue
if s.get('type', 'unknown_unknown').split('_')[-1] == 'rom':
continue # Read only storage
if s.get('rw', False):
has_rw_storage = True
break
if not has_rw_storage:
return False
return True
def _filesystem_callback(self, fs_map, obj, level):
name = obj.get('name', '')
self.filesystem_callback(_('Found object: %s')%name)
if not obj.get('is_folder', False):
return False
fs_map[obj.get('id', null)] = obj
path = [name]
pid = obj.get('parent_id', 0)
while pid != 0 and pid in fs_map:
parent = fs_map[pid]
path.append(parent.get('name', ''))
pid = parent.get('parent_id', 0)
if fs_map.get(pid, None) is parent:
break # An object is its own parent
path = tuple(reversed(path))
ok = not self.is_folder_ignored(self._currently_getting_sid, path)
if not ok:
debug('Ignored object: %s' % '/'.join(path))
return ok
@property
def filesystem_cache(self):
if self._filesystem_cache is None:
debug('Loading filesystem metadata...')
st = time.time()
from calibre.devices.mtp.filesystem_cache import FilesystemCache
ts = self.total_space()
all_storage = []
items = []
for storage_id, capacity in zip([self._main_id, self._carda_id,
self._cardb_id], ts):
if storage_id is None:
continue
name = _('Unknown')
for s in self.dev.data['storage']:
if s['id'] == storage_id:
name = s['name']
break
storage = {'id':storage_id, 'size':capacity, 'name':name,
'is_folder':True, 'can_delete':False, 'is_system':True}
self._currently_getting_sid = str(storage_id)
id_map = self.dev.get_filesystem(storage_id, partial(
self._filesystem_callback, {}))
for x in itervalues(id_map):
x['storage_id'] = storage_id
all_storage.append(storage)
items.append(itervalues(id_map))
self._filesystem_cache = FilesystemCache(all_storage, chain(*items))
debug('Filesystem metadata loaded in %g seconds (%d objects)'%(
time.time()-st, len(self._filesystem_cache)))
return self._filesystem_cache
@same_thread
def do_eject(self):
if self.currently_connected_pnp_id is None:
return
self.ejected_devices.add(self.currently_connected_pnp_id)
self.currently_connected_pnp_id = self.current_friendly_name = None
self._main_id = self._carda_id = self._cardb_id = None
self.dev = self._filesystem_cache = None
@same_thread
def post_yank_cleanup(self):
self.currently_connected_pnp_id = self.current_friendly_name = None
self._main_id = self._carda_id = self._cardb_id = None
self.dev = self._filesystem_cache = None
self.current_serial_num = None
@property
def is_mtp_device_connected(self):
return self.currently_connected_pnp_id is not None
def eject(self):
if self.currently_connected_pnp_id is None:
return
self.eject_dev_on_next_scan = True
self.current_serial_num = None
@same_thread
def open(self, connected_device, library_uuid):
self.dev = self._filesystem_cache = None
try:
self.dev = self.wpd.Device(connected_device)
except self.wpd.WPDError:
time.sleep(2)
try:
self.dev = self.wpd.Device(connected_device)
except self.wpd.WPDError as e:
self.blacklisted_devices.add(connected_device)
raise OpenFailed('Failed to open %s with error: %s'%(
connected_device, as_unicode(e)))
devdata = self.dev.data
storage = [s for s in devdata.get('storage', []) if s.get('rw', False)]
if not storage:
self.blacklisted_devices.add(connected_device)
raise OpenFailed('No storage found for device %s'%(connected_device,))
snum = devdata.get('serial_number', None)
if snum in self.prefs.get('blacklist', []):
self.blacklisted_devices.add(connected_device)
self.dev = None
raise BlacklistedDevice(
'The %s device has been blacklisted by the user'%(connected_device,))
storage.sort(key=lambda x:x.get('id', 'zzzzz'))
self._main_id = storage[0]['id']
if len(storage) > 1:
self._carda_id = storage[1]['id']
if len(storage) > 2:
self._cardb_id = storage[2]['id']
self.current_friendly_name = devdata.get('friendly_name', '')
if not self.current_friendly_name:
self.current_friendly_name = devdata.get('model_name',
_('Unknown MTP device'))
self.currently_connected_pnp_id = connected_device
self.current_serial_num = snum
self.current_device_data = devdata.copy()
def device_debug_info(self):
import pprint
return pprint.pformat(self.current_device_data)
@same_thread
def get_basic_device_information(self):
d = self.dev.data
dv = d.get('device_version', '')
return (self.current_friendly_name, dv, dv, '')
@same_thread
def total_space(self, end_session=True):
ans = [0, 0, 0]
dd = self.dev.data
for s in dd.get('storage', []):
i = {self._main_id:0, self._carda_id:1,
self._cardb_id:2}.get(s.get('id', -1), None)
if i is not None:
ans[i] = s['capacity']
return tuple(ans)
@same_thread
def free_space(self, end_session=True):
self.dev.update_data()
ans = [0, 0, 0]
dd = self.dev.data
for s in dd.get('storage', []):
i = {self._main_id:0, self._carda_id:1,
self._cardb_id:2}.get(s.get('id', -1), None)
if i is not None:
ans[i] = s['free_space']
return tuple(ans)
@same_thread
def get_mtp_file(self, f, stream=None, callback=None):
if f.is_folder:
raise ValueError('%s if a folder'%(f.full_path,))
set_name = stream is None
if stream is None:
stream = SpooledTemporaryFile(5*1024*1024, '_wpd_receive_file.dat')
try:
try:
self.dev.get_file(f.object_id, stream, callback)
except self.wpd.WPDFileBusy:
time.sleep(2)
self.dev.get_file(f.object_id, stream, callback)
except Exception as e:
raise DeviceError('Failed to fetch the file %s with error: %s'%
(f.full_path, as_unicode(e)))
stream.seek(0)
if set_name:
stream.name = f.name
return stream
@same_thread
def create_folder(self, parent, name):
if not parent.is_folder:
raise ValueError('%s is not a folder'%(parent.full_path,))
e = parent.folder_named(name)
if e is not None:
return e
ans = self.dev.create_folder(parent.object_id, name)
ans['storage_id'] = parent.storage_id
return parent.add_child(ans)
@same_thread
def delete_file_or_folder(self, obj):
if obj.deleted:
return
if not obj.can_delete:
raise ValueError('Cannot delete %s as deletion not allowed'%
(obj.full_path,))
if obj.is_system:
raise ValueError('Cannot delete %s as it is a system object'%
(obj.full_path,))
if obj.files or obj.folders:
raise ValueError('Cannot delete %s as it is not empty'%
(obj.full_path,))
parent = obj.parent
self.dev.delete_object(obj.object_id)
parent.remove_child(obj)
return parent
@same_thread
def put_file(self, parent, name, stream, size, callback=None, replace=True):
e = parent.folder_named(name)
if e is not None:
raise ValueError('Cannot upload file, %s already has a folder named: %s'%(
parent.full_path, e.name))
e = parent.file_named(name)
if e is not None:
if not replace:
raise ValueError('Cannot upload file %s, it already exists'%(
e.full_path,))
self.delete_file_or_folder(e)
sid, pid = parent.storage_id, parent.object_id
ans = self.dev.put_file(pid, name, stream, size, callback)
ans['storage_id'] = sid
return parent.add_child(ans)