1
0
mirror of https://github.com/gryf/mc_adbfs.git synced 2026-03-26 21:43:30 +01:00

34 Commits

Author SHA1 Message Date
cbef13ccea Merge pull request #5 from fanick1/master
fix typo: quite->quote
2022-12-29 19:54:58 +01:00
fanick1
9cfa834604 fix typo: quite->quote 2022-12-28 23:31:02 +01:00
b1a6219d21 Removed deprecated encoding argument from json.loads. 2020-12-07 19:08:29 +01:00
d16f0f06b8 Merge branch 'thp-various-fixes' 2020-06-09 19:55:21 +02:00
c2f07f5516 Merge branch 'various-fixes' of https://github.com/thp/mc_adbfs into thp-various-fixes 2020-06-09 19:55:06 +02:00
e9b196eaf8 Change SafeConfigParser to ConfigParser 2020-05-21 10:10:43 +02:00
039c078a35 Fix missing attribute on conf object 2020-04-26 13:48:21 +02:00
2776668913 Fix for removing files with space/parentheses.
Also, some further py2 cleanup.
2020-04-26 09:32:59 +02:00
390f1b1112 Drop Python2 support. 2020-04-26 09:16:59 +02:00
f7a6b145fd Added new option for trying su command. 2020-04-26 09:06:28 +02:00
9f1a51fdbf Added wrapper on adb shell commands 2020-04-26 09:04:28 +02:00
5ece2d579c Fix bad filenames for Python3.
In Python3, if there are filenames encoded with 8-bit encodings, there
might be an issues with converting them into unicode objects. This is a
workaround on this subject. Python2 is not affected.

Other than that, there was tests added to cover this case, appropriate
Makefile which automate creating venvs for both: Python 2 and 3, and
also there is a check against pep8 rules using flake8.
2019-05-14 21:14:24 +02:00
63fdc2c605 Removed unused elements
There was some items unused:

- In readme there was misleading entry, unused `skip_dirs` option.
- There was unused Exception class.
2019-05-14 20:43:28 +02:00
Thomas Perl
b088c45d3f Fix quoting for 'adb shell' 2019-05-09 11:58:29 +02:00
Thomas Perl
c5559f7d41 Python 3's ConfigParser is already the safe one 2019-05-09 11:38:21 +02:00
Thomas Perl
9ffa1a13af Default to Python 3 2019-05-09 11:38:00 +02:00
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
ae08a7329a Added ability to use config file 2016-09-18 19:22:50 +02:00
ebef125f38 Added rmmdir command 2016-09-14 18:50:49 +02:00
de5793c672 Fixed error handling, replacing manual commandline parsing with argparse 2016-06-04 18:02:33 +02:00
d1e8f42429 Removing quoting where it is unnecessary 2016-06-04 16:46:06 +02:00
89c783e9cd Added checking for root 2016-06-04 16:19:33 +02:00
7cb2a09282 Added support for toolbox 2016-06-04 16:16:51 +02:00
5 changed files with 689 additions and 145 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.test
__pycache__
adbfsc

42
Makefile Normal file
View File

@@ -0,0 +1,42 @@
# simple makefile for running tests for the adbfs plugin
all: test_dir py3 flake8
TEST_DIR='.test'
PY3_VENV=$(TEST_DIR)/py3
FL8_VENV=$(TEST_DIR)/flake8
TST_EXISTS=$(shell [ -e $(TEST_DIR) ] && echo 1 || echo 0)
PY3_EXISTS=$(shell [ -e $(PY3_VENV) ] && echo 1 || echo 0)
FL8_EXISTS=$(shell [ -e $(FL8_VENV) ] && echo 1 || echo 0)
py3: test_dir virtualenv3
.test/py3/bin/python test_adbfs.py
flake8: test_dir virtualenv_flake8
.test/flake8/bin/flake8 adbfs test_adbfs.py
ifeq ($(TST_EXISTS), 0)
test_dir:
mkdir -p .test
else
test_dir:
endif
ifeq ($(PY3_EXISTS), 0)
virtualenv3:
virtualenv -p python3 $(PY3_VENV)
$(PY3_VENV)/bin/pip install six
else
virtualenv3:
endif
ifeq ($(FL8_EXISTS), 0)
virtualenv_flake8:
virtualenv -p python2 $(FL8_VENV)
$(FL8_VENV)/bin/pip install flake8
else
virtualenv_flake8:
endif
clean:
rm -fr $(TEST_DIR) __pycache__ adbfsc

View File

@@ -5,19 +5,26 @@ Midnight Commander adbfs external fs plugin
This is Midnight Commander extfs plugin for browsing Android device through This is Midnight Commander extfs plugin for browsing Android device through
``adb`` interface written in Python. ``adb`` interface written in Python.
Rquirements Rquirements
=========== ===========
* Python 2.7 * Python 3.x (tested on 3.5.4, 3.6 and 3.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`` (``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 $ 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.
should display files from root directory on the device.
Features Features
======== ========
@@ -30,24 +37,103 @@ Features
* Symbolic links in lists are corrected to be relative to the file system * Symbolic links in lists are corrected to be relative to the file system
* Symbolic links also point to the right target, skipping intermediate links * Symbolic links also point to the right target, skipping intermediate links
Installation Installation
============ ============
Copy adbfs into ``~/.local/share/mc/extfs.d/`` directory and make it executable Copy adbfs into ``~/.local/share/mc/extfs.d/`` directory and make it executable
if needed. if needed.
Usage 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 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 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
dirs_to_skip = ["acct", "charger", "d", "dev", "proc", "sys"]
suppress_colors = false
root =
adb_command = adb
adb_connect =
try_su = false
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.
* ``try_su`` specifies whether or not to try to detect if ``su`` command is
available and usable.
Contribution
============
There is a ``Makefile`` in the top directory, which is basic helper for running
the tests. Please use it, and adapt/add tests for provided fixes/functionality.
The reason why `tox`_ wasn't used is, that there is no ``setup.py`` file, and
it's difficult to install simple script, which isn't a python module (python
interpreter will refuse to import module without ``.py`` extension).
It requires GNU ``make`` program, and also ``virtualenv``. Using it is simple
as running following command:
.. code:: shell-session
$ make
it will run `py3` and `flake8` jobs to check it against the code. For
running tests against Python 3:
.. code:: shell-session
$ make py3
or flake 8:
.. code:: shell-session
$ make flake8
Exit status on any of those means that test fail. Appropriate message/traceback
will also be visible.
Limitations Limitations
=========== ===========
@@ -55,9 +141,14 @@ Limitations
files are on the device and so on files are on the device and so on
* Some filenames might be still inaccessible for operating * Some filenames might be still inaccessible for operating
* All files operations which needs root privileges will fail (for now) * All files operations which needs root privileges will fail (for now)
* The implementation is experimental and it's by now working with mine device;
while it might not work with yours
License License
======= =======
This software is licensed under 3-clause BSD license. See LICENSE file for This software is licensed under 3-clause BSD license. See LICENSE file for
details. details.
.. _tox: https://tox.readthedocs.io

626
adbfs
View File

@@ -1,39 +1,235 @@
#! /usr/bin/env python #! /usr/bin/env python3
""" """
adbfs Virtual filesystem for Midnight Commander adbfs Virtual filesystem for Midnight Commander
* Copyright (c) 2015, Roman Dobosz, * Copyright (c) 2016, Roman Dobosz,
* Published under 3-clause BSD-style license (see LICENSE file) * Published under 3-clause BSD-style license (see LICENSE file)
Version: 0.7
""" """
import configparser
import argparse
from datetime import datetime from datetime import datetime
import json
import os import os
import pipes
import re import re
import subprocess import subprocess
import sys import sys
import shlex
__version__ = 0.14
XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
DEBUG = os.getenv("ADBFS_DEBUG", False) def check_output(command_list, stderr=None):
SKIP_SYSTEM_DIR = os.getenv("ADBFS_SKIP_SYSTEM_DIR", True) """
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 = []
for t in result.split(b'\n'):
if not t:
continue
try:
_result.append(t.decode('utf-8'))
except UnicodeDecodeError:
_result.append(t.decode('iso-8859-1'))
result = '\n'.join(_result) + '\n'
return result
class Conf(object):
"""Simple config parser"""
boxes = {'busybox': {'ls': 'busybox ls -anel',
'rls': 'busybox ls -Ranel {}',
'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[A-Z,a-z]{3}\s'
r'(?P<date_time>[A-Z,a-z]{3}\s+'
r'\d+\s\d{2}:\d{2}:\d{2}\s+\d{4})\s'
r'(?P<name>.*)'},
'toolbox': {'ls': 'toolbox ls -anl',
'rls': 'toolbox ls -Ranl {}',
'file_re': r'^(?P<perms>[-bcdlps][-rwxsStT]{9})\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>.*)'},
'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.dirs_to_skip = ['acct', 'charger', 'd', 'dev', 'proc', 'sys']
self.root = None
self.suppress_colors = False
self.adb_command = 'adb'
self.adb_connect = ''
self.try_su = False
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 = 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 = 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']
Adb.file_re = re.compile(self.box['file_re'])
return
except subprocess.CalledProcessError:
pass
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):
"""
Read config file and change the options according to values from that
file.
"""
if not os.path.exists(XDG_CONFIG_HOME):
return
conf_fname = os.path.join(XDG_CONFIG_HOME, 'mc', 'adbfs.ini')
if not os.path.exists(conf_fname):
return
cfg = configparser.ConfigParser()
cfg_map = {'debug': (cfg.getboolean, 'debug'),
'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'),
'try_su': (cfg.getboolean, 'try_su')}
cfg.read(conf_fname)
for key, (function, attr) in cfg_map.items():
try:
setattr(self, attr, function('adbfs', key))
except (configparser.NoSectionError, configparser.NoOptionError):
pass
if self.dirs_to_skip and isinstance(self.dirs_to_skip, str):
self.dirs_to_skip = json.loads(self.dirs_to_skip)
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):
"""Item in filesystem representation""" """Item in filesystem representation"""
def __init__(self, perms=None, links=1, uid=0, gid=0, size=0, def __init__(self, perms=None, links=1, uid=0, gid=0, size=None,
date_time=None, name=None): date_time=None, date=None, name=None):
"""initialize file""" """initialize file"""
self.perms = perms self.perms = perms
self.links = links self.links = links
self.uid = uid self.uid = uid
self.gid = gid self.gid = gid
self.size = size self.size = size if size else 0
self.date_time = date_time # as string self.date_time = date_time # as string
self.name = name self.name = name
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
@@ -42,12 +238,16 @@ 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
self.name = name self.name = name
if target.startswith("/"):
if not self.size:
self.size = 0
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,
@@ -55,60 +255,62 @@ 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
date = self.date_time.split() if self.date_time:
date = "%s-%02d-%s %s" % (date[1], date = self.date_time.split()
month_num[date[0]], date = '%s-%02d-%s %s' % (date[1],
date[3], month_num[date[0]],
date[2]) date[3],
date = datetime.strptime(date, "%d-%m-%Y %H:%M:%S") date[2])
self.date_time = date.strftime("%m/%d/%Y %H:%M:01") date = datetime.strptime(date, '%d-%m-%Y %H:%M:%S')
elif self.date:
date = datetime.strptime(self.date, '%Y-%m-%d %H:%M')
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)
def mk_link_relative(self, target_type): def mk_link_relative(self):
"""Convert links to relative""" """Convert links to relative"""
rel_path = self.dirname self.link_target = os.path.relpath(self.link_target, self.dirname)
# if target_type == "d":
# rel_path = self.filepath
self.link_target = os.path.relpath(self.link_target, rel_path)
def __repr__(self): def __repr__(self):
"""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,
@@ -121,25 +323,44 @@ 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 = re.compile(r'^(?P<perms>[-bcdlps][-rwxsStT]{9})\s+' current_re = re.compile(r'^(\./)?(?P<dir>.+):$')
r'(?P<links>\d+)\s'
r'(?P<uid>\d+)\s+'
r'(?P<gid>\d+)\s+'
r'(?P<size>\d+)\s[A-Z,a-z]{3}\s'
r'(?P<date_time>[A-Z,a-z]{3}\s+'
r'\d+\s\d{2}:\d{2}:\d{2}\s+\d{4})\s'
r'(?P<name>.*)')
current_re = re.compile(r"^(\./)?(?P<dir>.+):$")
as_root = os.getenv("ADBFS_AS_ROOT", False)
verbose = os.getenv("ADBFS_VERBOSE", False)
def __init__(self): def __init__(self):
"""Prepare archive content for operations""" """Prepare archive content for operations"""
super(Adb, self).__init__() super(Adb, self).__init__()
self.conf = Conf()
self.error = ''
self._entries = [] self._entries = []
self._links = {} self._links = {}
self._got_root = False
if self.conf.try_su:
self.__su_check()
def _shell_cmd(self, with_root, *args):
cmd = [self.conf.adb_command, 'shell']
if with_root and self._got_root:
_args = [shlex.quote(x) for x in args]
cmd += ['su', '-c', shlex.quote(' '.join(_args))]
else:
cmd += args
return cmd
def __su_check(self):
"""Check if we are able to get elevated privileges"""
cmd = self._shell_cmd(False, 'su -c whoami')
try:
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
def _find_target(self, needle): def _find_target(self, needle):
"""Find link target""" """Find link target"""
@@ -153,6 +374,7 @@ class Adb(object):
for entry in self._entries: for entry in self._entries:
if entry.filepath == needle: if entry.filepath == needle:
return entry return entry
return None return None
def _normalize_links(self): def _normalize_links(self):
@@ -170,38 +392,94 @@ class Adb(object):
target_entry = self._find_target(entry.link_target) target_entry = self._find_target(entry.link_target)
if target_entry: if target_entry:
entry.link_target = target_entry.filepath entry.link_target = target_entry.filepath
entry.mk_link_relative(target_entry.type) entry.mk_link_relative()
else: else:
elems_to_remove.append(self._entries.index(entry)) elems_to_remove.append(self._entries.index(entry))
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_single_dir_list(self, dir_):
"""Retrieve file list using adb"""
lscmd = self.conf.box['rls'].format(shlex.quote(dir_))
command = self._shell_cmd(True, *shlex.split(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
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): def _retrieve_file_list(self, root=None):
"""Retrieve file list using adb""" """Retrieve file list using adb"""
# if root:
# print "retrieve for %s" % root.filepath
command = ["adb", "shell", "su", "-c"]
if not root: if not root:
command.append("'busybox ls -anel'") lscmd = self.conf.box['ls']
else: else:
command.append("'busybox ls -Ranel {}'".format(root.filepath)) lscmd = self.conf.box['rls'].format(shlex.quote(root.filepath))
command = self._shell_cmd(True, *shlex.split(lscmd))
try: try:
lines = subprocess.check_output(command) if self.conf.debug:
except subprocess.CalledProcessError: print('executing', ' '.join(command))
sys.stderr.write("Cannot read directory. Is device connected?\n")
return 1
current_dir = root.dirname if root else "/" lines = check_output(command)
for line in lines.split("\n"): except subprocess.CalledProcessError:
sys.stderr.write('Cannot read directory. Is device connected?\n')
return 2
current_dir = root.dirname if root else '/'
for line in lines.split('\n'):
line = line.strip() line = line.strip()
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)
@@ -209,87 +487,141 @@ class Adb(object):
continue continue
entry = File(**reg_match.groupdict()) entry = File(**reg_match.groupdict())
if entry.name in (".", ".."): if entry.name in ('.', '..'):
continue
if SKIP_SYSTEM_DIR and entry.name in Adb.dirs_to_skip:
continue continue
entry.update(current_dir) entry.update(current_dir)
if entry.filepath in self.conf.dirs_to_skip:
continue
self._entries.append(entry) 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) 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 3
def list(self): def list(self):
"""Output list contents directory""" """Output list contents directory"""
self._retrieve_file_list() if self.error:
# self._retrieve_file_list_from_pickle() sys.stderr.write(self.error)
# self._save_file_list_to_pickle() return 4
if self.conf.root:
self._retrieve_single_dir_list(self.conf.root)
else:
self._retrieve_file_list()
self._normalize_links() self._normalize_links()
sys.stdout.write("".join([str(entry) for entry in self._entries])) 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):
"""Copy file form the device using adb.""" """Copy file form the device using adb."""
with open(os.devnull, "w") as fnull: if self.error:
return subprocess.call(["adb", "pull", pipes.quote(src), sys.stderr.write(self.error)
pipes.quote(dst)], return 5
stdout=fnull, stderr=fnull)
cmd = [self.conf.adb_command, 'pull', src, dst]
if self.conf.debug:
sys.stderr.write(' '.join(cmd) + '\n')
with open(os.devnull, 'w') as fnull:
try:
err = subprocess.call(cmd, stdout=fnull, stderr=fnull)
except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell')
return 6
return err
def copyin(self, src, dst): def copyin(self, src, dst):
"""Copy file to the device through adb.""" """Copy file to the device through adb."""
if not dst.startswith("/"): if self.error:
dst = "/" + dst sys.stderr.write(self.error)
return 7
if not dst.startswith('/'):
dst = '/' + dst
with open(os.devnull, "w") as fnull: cmd = [self.conf.adb_command, 'push', src, dst]
err = subprocess.call(["adb", "push", pipes.quote(src), if self.conf.debug:
pipes.quote(dst)], sys.stderr.write(' '.join(cmd) + '\n')
stdout=fnull, stderr=fnull)
with open(os.devnull, 'w') as fnull:
try:
err = subprocess.call(cmd, stdout=fnull, stderr=fnull)
except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell')
return 8
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 9
return 0 return 0
def rm(self, dst): def rm(self, dst):
"""Remove file from device.""" """Remove file from device."""
cmd = ["adb", "shell", "rm", pipes.quote(dst)] if self.error:
err = subprocess.check_output(cmd) sys.stderr.write(self.error)
return 10
if err != "": cmd = self._shell_cmd(False, 'rm %s' % shlex.quote(dst))
try:
err = check_output(cmd).strip()
except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell')
return 11
if err != '':
sys.stderr.write(err) sys.stderr.write(err)
return 1 return 12
return 0 return 0
def rmdir(self, dst): def rmdir(self, dst):
"""Remove directory from device.""" """Remove directory from device."""
cmd = ["adb", "shell", "rm", "-r", pipes.quote(dst)] if self.error:
err = subprocess.check_output(cmd) sys.stderr.write(self.error)
return 13
if err != "": cmd = self._shell_cmd(False, 'rm -r %s' % shlex.quote(dst))
try:
err = check_output(cmd).strip()
except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell')
return 14
if err != '':
sys.stderr.write(err) sys.stderr.write(err)
return 1 return 15
return 0 return 0
def mkdir(self, dst): def mkdir(self, dst):
"""Make directory on the device through adb.""" """Make directory on the device through adb."""
cmd = ["adb", "shell", "mkdir", pipes.quote(dst)] if self.error:
err = subprocess.check_output(cmd) sys.stderr.write(self.error)
return 16
if err != "": if not dst.startswith('/'):
dst = '/' + dst
cmd = self._shell_cmd(False, 'mkdir %s' % shlex.quote(dst))
try:
err = check_output(cmd).strip()
except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell')
return 17
if err != '':
sys.stderr.write(err) sys.stderr.write(err)
return 1 return 18
return 0 return 0
@@ -301,39 +633,59 @@ CALL_MAP = {'list': lambda a: Adb().list(),
'rm': lambda a: Adb().rm(a.dst), 'rm': lambda a: Adb().rm(a.dst),
'run': lambda a: Adb().run(a.dst)} 'run': lambda a: Adb().run(a.dst)}
def main(): def main():
"""parse commandline""" """parse commandline"""
try: parser = argparse.ArgumentParser()
if DEBUG: subparsers = parser.add_subparsers(help='supported commands')
sys.stderr.write("commandline: %s\n" % " ".join(sys.argv)) parser_list = subparsers.add_parser('list')
if sys.argv[1] not in ('list', 'copyin', 'copyout', 'rm', "rmdir", parser_copyin = subparsers.add_parser('copyin')
'mkdir', "run"): parser_copyout = subparsers.add_parser('copyout')
sys.exit(2) parser_rm = subparsers.add_parser('rm')
except IndexError: parser_mkdir = subparsers.add_parser('mkdir')
sys.exit(2) parser_rmdir = subparsers.add_parser('rmdir')
parser_run = subparsers.add_parser('run')
class Arg(object): parser_list.add_argument('arch')
"""Mimic argparse/optparse object""" parser_list.set_defaults(func=CALL_MAP['list'])
dst = None
src = None
arch = None
arg = Arg() parser_copyin.add_argument('arch')
parser_copyin.add_argument('dst')
parser_copyin.add_argument('src')
parser_copyin.set_defaults(func=CALL_MAP['copyin'])
parser_copyout.add_argument('arch')
parser_copyout.add_argument('src')
parser_copyout.add_argument('dst')
parser_copyout.set_defaults(func=CALL_MAP['copyout'])
parser_rm.add_argument('arch')
parser_rm.add_argument('dst')
parser_rm.set_defaults(func=CALL_MAP['rm'])
parser_mkdir.add_argument('arch')
parser_mkdir.add_argument('dst')
parser_mkdir.set_defaults(func=CALL_MAP['mkdir'])
parser_rmdir.add_argument('arch')
parser_rmdir.add_argument('dst')
parser_rmdir.set_defaults(func=CALL_MAP['rmdir'])
parser_run.add_argument('arch')
parser_run.add_argument('dst')
parser_run.set_defaults(func=CALL_MAP['run'])
parser.add_argument('--version', action='version',
version='%(prog)s ' + str(__version__))
args = parser.parse_args()
try: try:
arg.arch = sys.argv[2] return args.func(args)
if sys.argv[1] == 'copyin': except AttributeError:
arg.src = sys.argv[4] parser.print_help()
arg.dst = sys.argv[3] parser.exit()
if sys.argv[1] == 'copyout':
arg.src = sys.argv[3]
arg.dst = sys.argv[4]
elif sys.argv[1] in ('rm', 'rmdir', 'run', 'mkdir'):
arg.dst = sys.argv[3]
except IndexError:
sys.exit(2)
return CALL_MAP[sys.argv[1]](arg)
if __name__ == "__main__": if __name__ == '__main__':
sys.exit(main()) sys.exit(main())

56
test_adbfs.py Normal file
View File

@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
import os
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
import unittest
from unittest import mock
module_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'adbfs')
spec = spec_from_loader("adbfs", SourceFileLoader("adbfs", module_path))
adbfs = module_from_spec(spec)
spec.loader.exec_module(adbfs)
LISTING = '''\
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/Grüß Gott
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/\x80
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/Γεια σας
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/Здравствуйте
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/שָׁלוֹם
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/السَّلامُ عَلَيْكُمْ
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/გამარჯობა
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/こんにちは。
-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 /storage/emulated/0/안녕하십니까
''' # noqa
class TestCheckOutput(unittest.TestCase):
@mock.patch('subprocess.check_output')
def test_check_output(self, out):
"""
As for Python2 (and its last version: 2.7), subprocess.check_output
always return string like objects, contrary to bytes - no conversion
to string is needed.
Python3 treats string as unicode objects, but subprocess.check_output
returns bytes object, which is equvalend for py2 string… annoying.
"""
out.return_value = bytes(LISTING, 'utf-8')
result = adbfs.check_output(None)
self.assertEqual(result, LISTING)
@mock.patch('subprocess.check_output')
def test_check_output_py3_invalid_char(self, out):
"""
Special case for py3. We have bytes with some weird character - like
some system write something with codepage, instead of utf8.
"""
line = (b'-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 '
b'/storage/emulated/0/\xe2\n') # Latin 1 char â
out.return_value = bytes(line)
result = adbfs.check_output(None)
self.assertEqual(result, line.decode('iso-8859-1'))
if __name__ == "__main__":
unittest.main()