1
0
mirror of https://github.com/gryf/mc_adbfs.git synced 2026-03-27 13:53:35 +01:00

8 Commits
0.8 ... 0.10

Author SHA1 Message Date
1c6a6cfdf8 Added new option for selecting adb executable 2017-04-29 17:49:19 +02:00
7ce2dd2568 Fix for new version of adb command
Previously it was possible (most probably unintentionally) to perform
command which gives listing of directory contents on the output:

$ adb shell su -c toolbox ls /
acct
cache
charger
config
...

Using such syntax in newer versions of adb, return an error:

$ adb shell su -c toolbox ls /
Unknown id: ls

It is needed to quote argument passed to the 'shell' parameter on adb,
like:

$ adb shell 'su -c "toolbox ls /"'
acct
cache
charger
config
...

This patch fixes this issue for both adb versions.
2017-04-29 17:44:57 +02:00
11f980beb1 Minor code style fixes 2017-04-29 17:30:18 +02:00
bc742ccdf6 Merge remote-tracking branch 'github/master' 2017-03-06 17:12:58 +01:00
7a6a8a499b Added basic support for not rooted devices 2017-03-06 17:10:15 +01:00
b2163e0fba Update README.rst 2016-10-06 18:51:52 +02:00
57509eaac0 Added option for suppressing colors for ls command from busybox 2016-10-06 14:14:28 +02:00
210d7f2962 Added a functionality for accessing single directory 2016-09-20 20:40:14 +02:00
2 changed files with 208 additions and 104 deletions

View File

@@ -9,13 +9,17 @@ Rquirements
=========== ===========
* Python 2.7 * Python 2.7
* ``adb`` installed and in ``$PATH`` * ``adb`` installed and in ``$PATH`` or provided via the config file
* An Android device or emulator preferably rooted * An Android device or emulator preferably rooted
* Busybox installed and available in the path on the device * Busybox installed and available in the path on the device
Make sure, that issuing from command line:: Make sure, that issuing from command line:
.. code:: shell-session
$ adb shell busybox ls $ adb shell busybox ls
$ # or in case of no PATH adb placement
$ /path/to/adb shell busybox ls
it should display files from root directory on the device. it should display files from root directory on the device.
@@ -39,7 +43,9 @@ if needed.
Usage Usage
===== =====
To use it, just issue:: To use it, just issue:
.. code:: shell-session
cd adbfs:// cd adbfs://
@@ -48,6 +54,38 @@ device. For convenience you can add a bookmark (accessible under CTRL+\) for
fast access. The time is depended on how many files and directories you have on fast access. The time is depended on how many files and directories you have on
your device and how fast it is :) your device and how fast it is :)
Configuration
=============
You can configure behaviour of this plugin using ``.ini`` file located under
``$XDG_CONFIG_HOME/mc/adbfs.ini`` (which usually is located under
``~/.config/mc/adbfs.ini``), and have default values, like:
.. code:: ini
[adbfs]
debug = false
skip_dirs = true
dirs_to_skip = ["acct", "charger", "d", "dev", "proc", "sys"]
suppress_colors = false
root =
adb_command = adb
where:
* ``debug`` will provide a little bit more verbose information, useful for
debugging
* ``dirs_to_skip`` list of paths to directories which will be skipped during
reading. If leaved empty, or setted to empty list (``[]``) will read
everything (slow!)
* ``suppress_colors`` this option will make ``busybox`` not to display colors,
helpful, if ``busybox ls`` is configured to display colors by default. Does
not affect ``toolbox``.
* ``root`` root directory to read. Everything outside of that directory will be
omitted. That would be the fastest way to access certain location on the
device. Note, that ``dirs_to_skip`` still apply inside this directory.
* ``adb_command`` = path to ``adb`` command.
Limitations Limitations
=========== ===========

266
adbfs
View File

@@ -16,9 +16,9 @@ import re
import subprocess import subprocess
import sys import sys
__version__ = 0.8 __version__ = 0.10
XDG_CONFIG_HOME = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
class NoBoxFoundException(OSError): class NoBoxFoundException(OSError):
@@ -28,6 +28,7 @@ class NoBoxFoundException(OSError):
""" """
pass pass
class Conf(object): class Conf(object):
"""Simple config parser""" """Simple config parser"""
boxes = {'busybox': {'ls': 'busybox ls -anel', boxes = {'busybox': {'ls': 'busybox ls -anel',
@@ -48,37 +49,40 @@ class Conf(object):
r'(?P<size>\d+)?\s' r'(?P<size>\d+)?\s'
r'(?P<date>\d{4}-\d{2}-\d{2}\s' r'(?P<date>\d{4}-\d{2}-\d{2}\s'
r'\d{2}:\d{2})\s' r'\d{2}:\d{2})\s'
r'(?P<name>.*)'} r'(?P<name>.*)'}}
}
def __init__(self): def __init__(self):
self.box = None self.box = None
self.debug = False self.debug = False
self.skip_dirs = True self.dirs_to_skip = ['acct', 'charger', 'd', 'dev', 'proc', 'sys']
self.dirs_to_skip = ["acct", "charger", "d", "dev", "proc", "sys"] self.root = None
self.suppress_colors = False
self.adb_command = 'adb'
self.get_the_box()
self.read() self.read()
self.get_the_box()
def get_the_box(self): def get_the_box(self):
"""Detect if we dealing with busybox or toolbox""" """Detect if we dealing with busybox or toolbox"""
cmd = [self.adb_command] + 'shell which'.split()
try: try:
with open(os.devnull, "w") as fnull: with open(os.devnull, 'w') as fnull:
result = subprocess.check_output('adb shell which ' result = subprocess.check_output(cmd + ['busybox'],
'busybox'.split(),
stderr=fnull) stderr=fnull)
if 'busybox' in result: if 'busybox' in result:
self.box = Conf.boxes['busybox'] self.box = Conf.boxes['busybox']
if self.suppress_colors:
self.box.update({'ls': 'busybox ls --color=none -anel',
'rls': 'busybox ls --color=none '
'-Ranel {}'})
Adb.file_re = re.compile(self.box['file_re']) Adb.file_re = re.compile(self.box['file_re'])
return return
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
pass pass
try: try:
with open(os.devnull, "w") as fnull: with open(os.devnull, 'w') as fnull:
result = subprocess.check_output('adb shell which ' result = subprocess.check_output(cmd + ['toolbox'],
'toolbox'.split(),
stderr=fnull) stderr=fnull)
if 'toolbox' in result: if 'toolbox' in result:
@@ -89,7 +93,7 @@ class Conf(object):
pass pass
raise NoBoxFoundException(errno.ENOENT, raise NoBoxFoundException(errno.ENOENT,
"There is no toolbox or busybox available") 'There is no toolbox or busybox available')
def read(self): def read(self):
""" """
@@ -105,8 +109,10 @@ class Conf(object):
cfg = ConfigParser.SafeConfigParser() cfg = ConfigParser.SafeConfigParser()
cfg_map = {'debug': (cfg.getboolean, 'debug'), cfg_map = {'debug': (cfg.getboolean, 'debug'),
'skip_dirs': (cfg.getboolean, 'skip_dirs'), 'dirs_to_skip': (cfg.get, 'dirs_to_skip'),
'dirs_to_skip': (cfg.get, 'dirs_to_skip')} 'suppress_colors': (cfg.get, 'suppress_colors'),
'root': (cfg.get, 'root'),
'adb_command': (cfg.get, 'adb')}
cfg.read(conf_fname) cfg.read(conf_fname)
for key, (function, attr) in cfg_map.items(): for key, (function, attr) in cfg_map.items():
@@ -115,8 +121,15 @@ class Conf(object):
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
pass pass
if isinstance(self.dirs_to_skip, str): if self.dirs_to_skip and isinstance(self.dirs_to_skip, str):
self.dirs_to_skip = json.loads(self.dirs_to_skip, encoding="ascii") self.dirs_to_skip = json.loads(self.dirs_to_skip, encoding='ascii')
self.dirs_to_skip = [x.encode('utf-8') for x in self.dirs_to_skip]
else:
self.dirs_to_skip = []
if self.adb_command:
self.adb_command = os.path.expandvars(self.adb_command)
self.adb_command = os.path.expanduser(self.adb_command)
class File(object): class File(object):
@@ -133,7 +146,7 @@ class File(object):
self.name = name self.name = name
self.date = date # as string self.date = date # as string
self.dirname = "" self.dirname = ''
self.type = None self.type = None
self.string = None self.string = None
self.link_target = None self.link_target = None
@@ -142,7 +155,7 @@ class File(object):
def _correct_link(self): def _correct_link(self):
"""Canonize filename and fill the link attr""" """Canonize filename and fill the link attr"""
try: try:
name, target = self.name.split(" -> ") name, target = self.name.split(' -> ')
except ValueError: except ValueError:
return return
@@ -151,7 +164,7 @@ class File(object):
if not self.size: if not self.size:
self.size = 0 self.size = 0
if target.startswith("/"): if target.startswith('/'):
self.link_target = target self.link_target = target
else: else:
self.link_target = os.path.abspath(os.path.join(self.dirname, self.link_target = os.path.abspath(os.path.join(self.dirname,
@@ -159,34 +172,34 @@ class File(object):
def update(self, dirname): def update(self, dirname):
"""update object fields""" """update object fields"""
month_num = {"Jan": 1, month_num = {'Jan': 1,
"Feb": 2, 'Feb': 2,
"Mar": 3, 'Mar': 3,
"Apr": 4, 'Apr': 4,
"May": 5, 'May': 5,
"Jun": 6, 'Jun': 6,
"Jul": 7, 'Jul': 7,
"Aug": 8, 'Aug': 8,
"Sep": 9, 'Sep': 9,
"Oct": 10, 'Oct': 10,
"Nov": 11, 'Nov': 11,
"Dec": 12} 'Dec': 12}
self.dirname = dirname self.dirname = dirname
if self.date_time: if self.date_time:
date = self.date_time.split() date = self.date_time.split()
date = "%s-%02d-%s %s" % (date[1], date = '%s-%02d-%s %s' % (date[1],
month_num[date[0]], month_num[date[0]],
date[3], date[3],
date[2]) date[2])
date = datetime.strptime(date, "%d-%m-%Y %H:%M:%S") date = datetime.strptime(date, '%d-%m-%Y %H:%M:%S')
elif self.date: elif self.date:
date = datetime.strptime(self.date, "%Y-%m-%d %H:%M") date = datetime.strptime(self.date, '%Y-%m-%d %H:%M')
self.date_time = date.strftime("%m/%d/%Y %H:%M:01") self.date_time = date.strftime('%m/%d/%Y %H:%M:01')
self.type = self.perms[0] if self.perms else None self.type = self.perms[0] if self.perms else None
if self.type == "l" and " -> " in self.name: if self.type == 'l' and ' -> ' in self.name:
self._correct_link() self._correct_link()
self.filepath = os.path.join(self.dirname, self.name) self.filepath = os.path.join(self.dirname, self.name)
@@ -199,22 +212,22 @@ class File(object):
"""represent the file/entire node""" """represent the file/entire node"""
fullname = os.path.join(self.dirname, self.name) fullname = os.path.join(self.dirname, self.name)
if self.link_target: if self.link_target:
fullname += " -> " + self.link_target fullname += ' -> ' + self.link_target
return "<File {type} {name} {id}>".format(type=self.type, return '<File {type} {name} {id}>'.format(type=self.type,
name=fullname, name=fullname,
id=hex(id(self))) id=hex(id(self)))
def __str__(self): def __str__(self):
"""display the file/entire node""" """display the file/entire node"""
template = ("{perms} {links:>4} {uid:<8} {gid:<8} {size:>8} " template = ('{perms} {links:>4} {uid:<8} {gid:<8} {size:>8} '
"{date_time} {fullname}\n") '{date_time} {fullname}\n')
if not self.name: if not self.name:
return "" return ''
fullname = os.path.join(self.dirname, self.name) fullname = os.path.join(self.dirname, self.name)
if self.link_target: if self.link_target:
fullname += " -> " + self.link_target fullname += ' -> ' + self.link_target
return template.format(perms=self.perms, return template.format(perms=self.perms,
links=self.links, links=self.links,
@@ -227,9 +240,8 @@ class File(object):
class Adb(object): class Adb(object):
"""Class for interact with android rooted device through adb""" """Class for interact with android rooted device through adb"""
dirs_to_skip = ["acct", "charger", "d", "dev", "proc", "sys"]
file_re = None file_re = None
current_re = re.compile(r"^(\./)?(?P<dir>.+):$") current_re = re.compile(r'^(\./)?(?P<dir>.+):$')
def __init__(self): def __init__(self):
"""Prepare archive content for operations""" """Prepare archive content for operations"""
@@ -244,18 +256,16 @@ class Adb(object):
def __su_check(self): def __su_check(self):
"""Check if we are able to get elevated privileges""" """Check if we are able to get elevated privileges"""
cmd = [self.conf.adb_command] + 'shell su -c whoami'.split()
try: try:
with open(os.devnull, "w") as fnull: with open(os.devnull, 'w') as fnull:
result = subprocess.check_output('adb shell su -c ' result = subprocess.check_output(cmd, stderr=fnull)
'whoami'.split(),
stderr=fnull)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return return
if 'root' in result: if 'root' in result:
self._got_root = True self._got_root = True
return
def _find_target(self, needle): def _find_target(self, needle):
"""Find link target""" """Find link target"""
@@ -294,33 +304,42 @@ class Adb(object):
for idx in sorted(elems_to_remove, reverse=True): for idx in sorted(elems_to_remove, reverse=True):
del self._entries[idx] del self._entries[idx]
def _retrieve_file_list(self, root=None): def _retrieve_single_dir_list(self, dir_):
"""Retrieve file list using adb""" """Retrieve file list using adb"""
command = ["adb", "shell", "su", "-c"] lscmd = self.conf.box['rls'].format(dir_)
skip_dirs = self.conf.skip_dirs if self._got_root:
lscmd = 'su -c "{}"'.format(lscmd)
if not root: command = [self.conf.adb_command, 'shell', lscmd]
command.append(self.conf.box['ls'])
else:
command.append(self.conf.box['rls'].format(root.filepath))
try: try:
if self.conf.debug: if self.conf.debug:
print "executing", " ".join(command) print 'executing', ' '.join(command)
lines = subprocess.check_output(command) lines = subprocess.check_output(command)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write("Cannot read directory. Is device connected?\n") sys.stderr.write('Cannot read directory. Is device connected?\n')
return 1 return 1
current_dir = root.dirname if root else "/" lines = [l.strip() for l in lines.split('\n') if l.strip()]
for line in lines.split("\n"): if len(lines) == 1:
line = line.strip() reg_match = self.file_re.match(lines[0])
entry = File(**reg_match.groupdict())
entry.update('/')
if entry.filepath in self.conf.dirs_to_skip:
return
self._entries.append(entry)
if entry.type == 'l':
self._links[entry.filepath] = entry
self._retrieve_single_dir_list(entry.link_target)
else:
for line in lines:
current_dir_re = self.current_re.match(line) current_dir_re = self.current_re.match(line)
if current_dir_re: if current_dir_re:
current_dir = current_dir_re.groupdict()["dir"] current_dir = current_dir_re.groupdict()['dir']
if not current_dir: if not current_dir:
current_dir = "/" current_dir = '/'
continue continue
reg_match = self.file_re.match(line) reg_match = self.file_re.match(line)
@@ -328,25 +347,75 @@ class Adb(object):
continue continue
entry = File(**reg_match.groupdict()) entry = File(**reg_match.groupdict())
if entry.name in (".", ".."): if entry.name in ('.', '..'):
continue continue
entry.update(current_dir) entry.update(current_dir)
if skip_dirs and entry.filepath in self.conf.dirs_to_skip: if entry.filepath in self.conf.dirs_to_skip:
continue continue
self._entries.append(entry) self._entries.append(entry)
if root is None and entry.type == "d":
if entry.type == 'l':
self._links[entry.filepath] = entry
def _retrieve_file_list(self, root=None):
"""Retrieve file list using adb"""
if not root:
lscmd = self.conf.box['ls']
else:
lscmd = self.conf.box['rls'].format(root.filepath)
if self._got_root:
lscmd = 'su -c "{}"'.format(lscmd)
command = [self.conf.adb_command, 'shell', lscmd]
try:
if self.conf.debug:
print 'executing', ' '.join(command)
lines = subprocess.check_output(command)
except subprocess.CalledProcessError:
sys.stderr.write('Cannot read directory. Is device connected?\n')
return 1
current_dir = root.dirname if root else '/'
for line in lines.split('\n'):
line = line.strip()
current_dir_re = self.current_re.match(line)
if current_dir_re:
current_dir = current_dir_re.groupdict()['dir']
if not current_dir:
current_dir = '/'
continue
reg_match = self.file_re.match(line)
if not reg_match:
continue
entry = File(**reg_match.groupdict())
if entry.name in ('.', '..'):
continue
entry.update(current_dir)
if entry.filepath in self.conf.dirs_to_skip:
continue
self._entries.append(entry)
if root is None and entry.type == 'd':
self._retrieve_file_list(entry) self._retrieve_file_list(entry)
if entry.type == "l": if entry.type == 'l':
self._links[entry.filepath] = entry self._links[entry.filepath] = entry
def run(self, fname): def run(self, fname):
"""Not supported""" """Not supported"""
sys.stderr.write("Not supported - or maybe you are on compatible " sys.stderr.write('Not supported - or maybe you are on compatible '
"architecture?\n") 'architecture?\n')
return 1 return 1
def list(self): def list(self):
@@ -355,15 +424,13 @@ class Adb(object):
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 1
if self.conf.root:
self._retrieve_single_dir_list(self.conf.root)
else:
self._retrieve_file_list() self._retrieve_file_list()
self._normalize_links() self._normalize_links()
# with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), sys.stdout.write(''.join([str(entry) for entry in self._entries]))
# # "list.pcl"), "w") as fob:
# "list.pcl")) as fob:
# import cPickle
# # cPickle.dump(self._entries, fob)
# self._entries = cPickle.load(fob)
sys.stdout.write("".join([str(entry) for entry in self._entries]))
return 0 return 0
def copyout(self, src, dst): def copyout(self, src, dst):
@@ -372,11 +439,11 @@ class Adb(object):
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 1
cmd = ["adb", "pull", src, dst] cmd = [self.conf.adb_command, 'pull', src, dst]
if self.conf.debug: if self.conf.debug:
sys.stderr.write(" ".join(cmd) + "\n") sys.stderr.write(' '.join(cmd) + '\n')
with open(os.devnull, "w") as fnull: with open(os.devnull, 'w') as fnull:
try: try:
err = subprocess.call(cmd, stdout=fnull, stderr=fnull) err = subprocess.call(cmd, stdout=fnull, stderr=fnull)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
@@ -390,15 +457,14 @@ class Adb(object):
if self.error: if self.error:
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 1
if not dst.startswith("/"): if not dst.startswith('/'):
dst = "/" + dst dst = '/' + dst
# cmd = ["adb", "push", pipes.quote(src), pipes.quote(dst)] cmd = [self.conf.adb_command, 'push', src, dst]
cmd = ["adb", "push", src, dst]
if self.conf.debug: if self.conf.debug:
sys.stderr.write(" ".join(cmd) + "\n") sys.stderr.write(' '.join(cmd) + '\n')
with open(os.devnull, "w") as fnull: with open(os.devnull, 'w') as fnull:
try: try:
err = subprocess.call(cmd, stdout=fnull, stderr=fnull) err = subprocess.call(cmd, stdout=fnull, stderr=fnull)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
@@ -406,8 +472,8 @@ class Adb(object):
return 1 return 1
if err != 0: if err != 0:
sys.stderr.write("Cannot push the file, " sys.stderr.write('Cannot push the file, '
"%s, error %d" % (dst, err)) '%s, error %d' % (dst, err))
return 1 return 1
return 0 return 0
@@ -417,14 +483,14 @@ class Adb(object):
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 1
cmd = ["adb", "shell", "rm", dst] cmd = [self.conf.adb_command, 'shell', 'rm', dst]
try: try:
err = subprocess.check_output(cmd) err = subprocess.check_output(cmd)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell') sys.stderr.write('Error executing adb shell')
return 1 return 1
if err != "": if err != '':
sys.stderr.write(err) sys.stderr.write(err)
return 1 return 1
return 0 return 0
@@ -435,14 +501,14 @@ class Adb(object):
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 1
cmd = ["adb", "shell", "rm", "-r", dst] cmd = [self.conf.adb_command, 'shell', 'rm', '-r', dst]
try: try:
err = subprocess.check_output(cmd) err = subprocess.check_output(cmd)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell') sys.stderr.write('Error executing adb shell')
return 1 return 1
if err != "": if err != '':
sys.stderr.write(err) sys.stderr.write(err)
return 1 return 1
return 0 return 0
@@ -453,17 +519,17 @@ class Adb(object):
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 1
if not dst.startswith("/"): if not dst.startswith('/'):
dst = "/" + dst dst = '/' + dst
cmd = ["adb", "shell", "mkdir", dst] cmd = [self.conf.adb_command, 'shell', 'mkdir', dst]
try: try:
err = subprocess.check_output(cmd) err = subprocess.check_output(cmd)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell') sys.stderr.write('Error executing adb shell')
return 1 return 1
if err != "": if err != '':
sys.stderr.write(err) sys.stderr.write(err)
return 1 return 1
return 0 return 0
@@ -527,5 +593,5 @@ def main():
return args.func(args) return args.func(args)
if __name__ == "__main__": if __name__ == '__main__':
sys.exit(main()) sys.exit(main())