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

12 Commits
0.8 ... 0.11

Author SHA1 Message Date
e4a4aa8974 Added Python3 compatibility 2018-03-11 16:09:28 +01:00
755ce62321 Added toybox support 2017-05-24 21:57:19 +02:00
d44050118c Added adb_connect feature 2017-05-21 20:52:56 +02:00
a216b31ef1 Fix for adb_command 2017-05-21 20:47:18 +02:00
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 313 additions and 118 deletions

View File

@@ -8,14 +8,19 @@ This is Midnight Commander extfs plugin for browsing Android device through
Rquirements
===========
* Python 2.7
* ``adb`` installed and in ``$PATH``
* Python 2.7 or 3.x (tested on 3.5.4)
* ``adb`` installed and in ``$PATH`` or provided via the config file
* An Android device or emulator preferably rooted
* Busybox installed and available in the path on the device
* ``busybox`` (``toolbox``, ``toybox``) 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
$ # or in case of no PATH adb placement
$ /path/to/adb shell busybox ls
it should display files from root directory on the device.
@@ -39,15 +44,56 @@ if needed.
Usage
=====
To use it, just issue::
To use it, just issue:
cd adbfs://
.. code:: shell-session
cd adbfs://
under MC - after some time you should see the files and directories on your
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
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
adb_connect =
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`` or ``toybox``.
* ``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`` absolute or relative path to ``adb`` command. `~/` or
environment variables are allowed.
* ``adb_connect`` specifies if connection to specific device needs to be
performed before accessing shell. It is useful for *adb over network*
feature. Typical value here is a device IP address with optional port, which
defaults to 5555.
Limitations
===========

373
adbfs
View File

@@ -5,20 +5,33 @@ adbfs Virtual filesystem for Midnight Commander
* Copyright (c) 2016, Roman Dobosz,
* Published under 3-clause BSD-style license (see LICENSE file)
"""
import ConfigParser
try:
import ConfigParser as configparser
except ImportError:
import configparser
import argparse
from datetime import datetime
import errno
import json
import os
import re
import subprocess
import sys
__version__ = 0.8
__version__ = 0.11
XDG_CONFIG_HOME = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
def check_output(command_list, stderr=None):
"""
For some reason, in py3 it was decided that command output should be bytes
instead of string. This little function will check if we have string or
bytes and in case of bytes it will convert it to string.
"""
result = subprocess.check_output(command_list, stderr=stderr)
if not isinstance(result, str):
result = result.decode('utf-8')
return result
class NoBoxFoundException(OSError):
@@ -28,6 +41,7 @@ class NoBoxFoundException(OSError):
"""
pass
class Conf(object):
"""Simple config parser"""
boxes = {'busybox': {'ls': 'busybox ls -anel',
@@ -48,38 +62,62 @@ class Conf(object):
r'(?P<size>\d+)?\s'
r'(?P<date>\d{4}-\d{2}-\d{2}\s'
r'\d{2}:\d{2})\s'
r'(?P<name>.*)'}
}
r'(?P<name>.*)'},
'toybox': {'ls': 'toybox ls -anl',
'rls': 'toybox ls -Ranl {}',
'file_re': r'^(?P<perms>[-bcdlps][-rwxsStT]{9})\s+'
r'(?P<links>\d+)\s+'
r'(?P<uid>\d+)\s+'
r'(?P<gid>\d+)\s+'
r'(?P<size>\d+)?\s'
r'(?P<date>\d{4}-\d{2}-\d{2}\s'
r'\d{2}:\d{2})\s'
r'(?P<name>.*)'}}
def __init__(self):
self.box = None
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.adb_connect = ''
self.get_the_box()
self.read()
self.connect()
self.get_the_box()
def get_the_box(self):
"""Detect if we dealing with busybox or toolbox"""
cmd = [self.adb_command] + 'shell which'.split()
try:
with open(os.devnull, "w") as fnull:
result = subprocess.check_output('adb shell which '
'busybox'.split(),
stderr=fnull)
with open(os.devnull, 'w') as fnull:
result = check_output(cmd + ['busybox'], stderr=fnull)
if 'busybox' in result:
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'])
return
except subprocess.CalledProcessError:
pass
try:
with open(os.devnull, "w") as fnull:
result = subprocess.check_output('adb shell which '
'toolbox'.split(),
stderr=fnull)
with open(os.devnull, 'w') as fnull:
result = check_output(cmd + ['toybox'], stderr=fnull)
if 'toybox' in result:
self.box = Conf.boxes['toybox']
Adb.file_re = re.compile(self.box['file_re'])
return
except subprocess.CalledProcessError:
pass
try:
with open(os.devnull, 'w') as fnull:
result = check_output(cmd + ['toolbox'], stderr=fnull)
if 'toolbox' in result:
self.box = Conf.boxes['toolbox']
@@ -88,8 +126,56 @@ class Conf(object):
except subprocess.CalledProcessError:
pass
raise NoBoxFoundException(errno.ENOENT,
"There is no toolbox or busybox available")
sys.stderr.write('There is no toolbox or busybox available.\n')
sys.exit(1)
def get_attached_devices(self):
"""Return a list of attached devices"""
cmd = [self.adb_command, 'devices']
devices = []
try:
with open(os.devnull, 'w') as fnull:
result = check_output(cmd, stderr=fnull)
except subprocess.CalledProcessError:
result = ''
for line in result.split('\n'):
if line.startswith('*'):
continue
if line.strip() == 'List of devices attached':
continue
if line.strip() == '':
continue
identifier, _ = line.split()
devices.append(identifier)
return devices
def connect(self):
"""
If adb_connect is non empty string, perform connecting to specified
device over network using an address (or hostname).
"""
if not self.adb_connect:
return
devices = self.get_attached_devices()
for device in devices:
if self.adb_connect in device:
return # already connected, no need to reconnect
cmd = [self.adb_command, 'connect', self.adb_connect]
with open(os.devnull, 'w') as fnull:
result = check_output(cmd, stderr=fnull)
if result.split()[0] == 'connected':
subprocess.call([self.adb_command, 'wait-for-device'])
return
sys.stderr.write('Unable to connect to `%s\'. Is adb over network '
'enabled on device?\n' % self.adb_connect)
sys.exit(2)
def read(self):
"""
@@ -103,20 +189,30 @@ class Conf(object):
if not os.path.exists(conf_fname):
return
cfg = ConfigParser.SafeConfigParser()
cfg = configparser.SafeConfigParser()
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_command'),
'adb_connect': (cfg.get, 'adb_connect')}
cfg.read(conf_fname)
for key, (function, attr) in cfg_map.items():
try:
setattr(self, attr, function('adbfs', key))
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
except (configparser.NoSectionError, configparser.NoOptionError):
pass
if isinstance(self.dirs_to_skip, str):
self.dirs_to_skip = json.loads(self.dirs_to_skip, encoding="ascii")
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 = [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):
@@ -133,7 +229,7 @@ class File(object):
self.name = name
self.date = date # as string
self.dirname = ""
self.dirname = ''
self.type = None
self.string = None
self.link_target = None
@@ -142,7 +238,7 @@ class File(object):
def _correct_link(self):
"""Canonize filename and fill the link attr"""
try:
name, target = self.name.split(" -> ")
name, target = self.name.split(' -> ')
except ValueError:
return
@@ -151,7 +247,7 @@ class File(object):
if not self.size:
self.size = 0
if target.startswith("/"):
if target.startswith('/'):
self.link_target = target
else:
self.link_target = os.path.abspath(os.path.join(self.dirname,
@@ -159,34 +255,34 @@ class File(object):
def update(self, dirname):
"""update object fields"""
month_num = {"Jan": 1,
"Feb": 2,
"Mar": 3,
"Apr": 4,
"May": 5,
"Jun": 6,
"Jul": 7,
"Aug": 8,
"Sep": 9,
"Oct": 10,
"Nov": 11,
"Dec": 12}
month_num = {'Jan': 1,
'Feb': 2,
'Mar': 3,
'Apr': 4,
'May': 5,
'Jun': 6,
'Jul': 7,
'Aug': 8,
'Sep': 9,
'Oct': 10,
'Nov': 11,
'Dec': 12}
self.dirname = dirname
if self.date_time:
date = self.date_time.split()
date = "%s-%02d-%s %s" % (date[1],
date = '%s-%02d-%s %s' % (date[1],
month_num[date[0]],
date[3],
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:
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
if self.type == "l" and " -> " in self.name:
if self.type == 'l' and ' -> ' in self.name:
self._correct_link()
self.filepath = os.path.join(self.dirname, self.name)
@@ -199,22 +295,22 @@ class File(object):
"""represent the file/entire node"""
fullname = os.path.join(self.dirname, self.name)
if self.link_target:
fullname += " -> " + self.link_target
return "<File {type} {name} {id}>".format(type=self.type,
fullname += ' -> ' + self.link_target
return '<File {type} {name} {id}>'.format(type=self.type,
name=fullname,
id=hex(id(self)))
def __str__(self):
"""display the file/entire node"""
template = ("{perms} {links:>4} {uid:<8} {gid:<8} {size:>8} "
"{date_time} {fullname}\n")
template = ('{perms} {links:>4} {uid:<8} {gid:<8} {size:>8} '
'{date_time} {fullname}\n')
if not self.name:
return ""
return ''
fullname = os.path.join(self.dirname, self.name)
if self.link_target:
fullname += " -> " + self.link_target
fullname += ' -> ' + self.link_target
return template.format(perms=self.perms,
links=self.links,
@@ -227,9 +323,8 @@ class File(object):
class Adb(object):
"""Class for interact with android rooted device through adb"""
dirs_to_skip = ["acct", "charger", "d", "dev", "proc", "sys"]
file_re = None
current_re = re.compile(r"^(\./)?(?P<dir>.+):$")
current_re = re.compile(r'^(\./)?(?P<dir>.+):$')
def __init__(self):
"""Prepare archive content for operations"""
@@ -244,18 +339,16 @@ class Adb(object):
def __su_check(self):
"""Check if we are able to get elevated privileges"""
cmd = [self.conf.adb_command] + 'shell su -c whoami'.split()
try:
with open(os.devnull, "w") as fnull:
result = subprocess.check_output('adb shell su -c '
'whoami'.split(),
stderr=fnull)
with open(os.devnull, 'w') as fnull:
result = check_output(cmd, stderr=fnull)
except subprocess.CalledProcessError:
return
if 'root' in result:
self._got_root = True
return
def _find_target(self, needle):
"""Find link target"""
@@ -294,33 +387,92 @@ class Adb(object):
for idx in sorted(elems_to_remove, reverse=True):
del self._entries[idx]
def _retrieve_file_list(self, root=None):
def _retrieve_single_dir_list(self, dir_):
"""Retrieve file list using adb"""
command = ["adb", "shell", "su", "-c"]
skip_dirs = self.conf.skip_dirs
if not root:
command.append(self.conf.box['ls'])
else:
command.append(self.conf.box['rls'].format(root.filepath))
lscmd = self.conf.box['rls'].format(dir_)
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)
print('executing', ' '.join(command))
lines = subprocess.check_output(command)
lines = check_output(command)
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
current_dir = root.dirname if root else "/"
for line in lines.split("\n"):
lines = [l.strip() for l in lines.split('\n') if l.strip()]
if len(lines) == 1:
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)
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 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 = 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"]
current_dir = current_dir_re.groupdict()['dir']
if not current_dir:
current_dir = "/"
current_dir = '/'
continue
reg_match = self.file_re.match(line)
@@ -328,25 +480,25 @@ class Adb(object):
continue
entry = File(**reg_match.groupdict())
if entry.name in (".", ".."):
if entry.name in ('.', '..'):
continue
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
self._entries.append(entry)
if root is None and entry.type == "d":
if root is None and entry.type == 'd':
self._retrieve_file_list(entry)
if entry.type == "l":
if entry.type == 'l':
self._links[entry.filepath] = entry
def run(self, fname):
"""Not supported"""
sys.stderr.write("Not supported - or maybe you are on compatible "
"architecture?\n")
sys.stderr.write('Not supported - or maybe you are on compatible '
'architecture?\n')
return 1
def list(self):
@@ -355,15 +507,13 @@ class Adb(object):
sys.stderr.write(self.error)
return 1
self._retrieve_file_list()
if self.conf.root:
self._retrieve_single_dir_list(self.conf.root)
else:
self._retrieve_file_list()
self._normalize_links()
# with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
# # "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]))
sys.stdout.write(''.join([str(entry) for entry in self._entries]))
return 0
def copyout(self, src, dst):
@@ -372,11 +522,11 @@ class Adb(object):
sys.stderr.write(self.error)
return 1
cmd = ["adb", "pull", src, dst]
cmd = [self.conf.adb_command, 'pull', src, dst]
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:
err = subprocess.call(cmd, stdout=fnull, stderr=fnull)
except subprocess.CalledProcessError:
@@ -390,15 +540,14 @@ class Adb(object):
if self.error:
sys.stderr.write(self.error)
return 1
if not dst.startswith("/"):
dst = "/" + dst
if not dst.startswith('/'):
dst = '/' + dst
# cmd = ["adb", "push", pipes.quote(src), pipes.quote(dst)]
cmd = ["adb", "push", src, dst]
cmd = [self.conf.adb_command, 'push', src, dst]
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:
err = subprocess.call(cmd, stdout=fnull, stderr=fnull)
except subprocess.CalledProcessError:
@@ -406,8 +555,8 @@ class Adb(object):
return 1
if err != 0:
sys.stderr.write("Cannot push the file, "
"%s, error %d" % (dst, err))
sys.stderr.write('Cannot push the file, '
'%s, error %d' % (dst, err))
return 1
return 0
@@ -417,14 +566,14 @@ class Adb(object):
sys.stderr.write(self.error)
return 1
cmd = ["adb", "shell", "rm", dst]
cmd = [self.conf.adb_command, 'shell', 'rm', dst]
try:
err = subprocess.check_output(cmd)
err = check_output(cmd)
except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell')
return 1
if err != "":
if err != '':
sys.stderr.write(err)
return 1
return 0
@@ -435,14 +584,14 @@ class Adb(object):
sys.stderr.write(self.error)
return 1
cmd = ["adb", "shell", "rm", "-r", dst]
cmd = [self.conf.adb_command, 'shell', 'rm', '-r', dst]
try:
err = subprocess.check_output(cmd)
err = check_output(cmd)
except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell')
return 1
if err != "":
if err != '':
sys.stderr.write(err)
return 1
return 0
@@ -453,17 +602,17 @@ class Adb(object):
sys.stderr.write(self.error)
return 1
if not dst.startswith("/"):
dst = "/" + dst
if not dst.startswith('/'):
dst = '/' + dst
cmd = ["adb", "shell", "mkdir", dst]
cmd = [self.conf.adb_command, 'shell', 'mkdir', dst]
try:
err = subprocess.check_output(cmd)
err = check_output(cmd)
except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell')
return 1
if err != "":
if err != '':
sys.stderr.write(err)
return 1
return 0
@@ -527,5 +676,5 @@ def main():
return args.func(args)
if __name__ == "__main__":
if __name__ == '__main__':
sys.exit(main())