1
0
mirror of https://github.com/gryf/mc_adbfs.git synced 2026-03-27 05:43:33 +01:00

14 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
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
4 changed files with 77 additions and 92 deletions

View File

@@ -1,22 +1,17 @@
# simple makefile for running tests for the adbfs plugin # simple makefile for running tests for the adbfs plugin
all: test_dir py2 py3 flake8 all: test_dir py3 flake8
TEST_DIR='.test' TEST_DIR='.test'
PY2_VENV=$(TEST_DIR)/py2
PY3_VENV=$(TEST_DIR)/py3 PY3_VENV=$(TEST_DIR)/py3
FL8_VENV=$(TEST_DIR)/flake8 FL8_VENV=$(TEST_DIR)/flake8
TST_EXISTS=$(shell [ -e $(TEST_DIR) ] && echo 1 || echo 0) TST_EXISTS=$(shell [ -e $(TEST_DIR) ] && echo 1 || echo 0)
PY2_EXISTS=$(shell [ -e $(PY2_VENV) ] && echo 1 || echo 0)
PY3_EXISTS=$(shell [ -e $(PY3_VENV) ] && echo 1 || echo 0) PY3_EXISTS=$(shell [ -e $(PY3_VENV) ] && echo 1 || echo 0)
FL8_EXISTS=$(shell [ -e $(FL8_VENV) ] && echo 1 || echo 0) FL8_EXISTS=$(shell [ -e $(FL8_VENV) ] && echo 1 || echo 0)
py3: test_dir virtualenv3 py3: test_dir virtualenv3
.test/py3/bin/python test_adbfs.py .test/py3/bin/python test_adbfs.py
py2: test_dir virtualenv2
.test/py2/bin/python test_adbfs.py
flake8: test_dir virtualenv_flake8 flake8: test_dir virtualenv_flake8
.test/flake8/bin/flake8 adbfs test_adbfs.py .test/flake8/bin/flake8 adbfs test_adbfs.py
@@ -35,14 +30,6 @@ else
virtualenv3: virtualenv3:
endif endif
ifeq ($(PY2_EXISTS), 0)
virtualenv2:
virtualenv -p python2 $(PY2_VENV)
$(PY2_VENV)/bin/pip install mock
else
virtualenv2:
endif
ifeq ($(FL8_EXISTS), 0) ifeq ($(FL8_EXISTS), 0)
virtualenv_flake8: virtualenv_flake8:
virtualenv -p python2 $(FL8_VENV) virtualenv -p python2 $(FL8_VENV)

View File

@@ -9,7 +9,7 @@ This is Midnight Commander extfs plugin for browsing Android device through
Rquirements Rquirements
=========== ===========
* Python 2.7 or 3.x (tested on 3.5.4) * Python 3.x (tested on 3.5.4, 3.6 and 3.7)
* ``adb`` installed and in ``$PATH`` or provided via the config file * ``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`` (``toolbox``, ``toybox``) installed and available in the path on * ``busybox`` (``toolbox``, ``toybox``) installed and available in the path on
@@ -76,6 +76,7 @@ You can configure behaviour of this plugin using ``.ini`` file located under
root = root =
adb_command = adb adb_command = adb
adb_connect = adb_connect =
try_su = false
where: where:
@@ -90,12 +91,14 @@ where:
* ``root`` root directory to read. Everything outside of that directory will be * ``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 omitted. That would be the fastest way to access certain location on the
device. Note, that ``dirs_to_skip`` still apply inside this directory. device. Note, that ``dirs_to_skip`` still apply inside this directory.
* ``adb_command`` absolute or relative path to ``adb`` command. `~/` or * ``adb_command`` absolute or relative path to ``adb`` command. ``~/`` or
environment variables are allowed. environment variables are allowed.
* ``adb_connect`` specifies if connection to specific device needs to be * ``adb_connect`` specifies if connection to specific device needs to be
performed before accessing shell. It is useful for *adb over network* performed before accessing shell. It is useful for *adb over network*
feature. Typical value here is a device IP address with optional port, which feature. Typical value here is a device IP address with optional port, which
defaults to 5555. defaults to 5555.
* ``try_su`` specifies whether or not to try to detect if ``su`` command is
available and usable.
Contribution Contribution
@@ -103,27 +106,24 @@ Contribution
There is a ``Makefile`` in the top directory, which is basic helper for running 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 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`` besides Python in It requires GNU ``make`` program, and also ``virtualenv``. Using it is simple
version 2 and 3. Using it is simple as running following command: as running following command:
.. code:: shell-session .. code:: shell-session
$ make $ make
it will run `py2`, `py3` and `flake8` jobs to check it against the code. For it will run `py3` and `flake8` jobs to check it against the code. For
running tests against Python 3: running tests against Python 3:
.. code:: shell-session .. code:: shell-session
$ make py3 $ make py3
or Python 2:
.. code:: shell-session
$ make py2
or flake 8: or flake 8:
.. code:: shell-session .. code:: shell-session
@@ -150,3 +150,5 @@ 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

99
adbfs
View File

@@ -1,14 +1,11 @@
#! /usr/bin/env python #! /usr/bin/env python3
""" """
adbfs Virtual filesystem for Midnight Commander adbfs Virtual filesystem for Midnight Commander
* Copyright (c) 2016, 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)
""" """
try: import configparser
import ConfigParser as configparser
except ImportError:
import configparser
import argparse import argparse
from datetime import datetime from datetime import datetime
import json import json
@@ -16,8 +13,9 @@ import os
import re import re
import subprocess import subprocess
import sys import sys
import shlex
__version__ = 0.12 __version__ = 0.14
XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
@@ -82,6 +80,7 @@ class Conf(object):
self.suppress_colors = False self.suppress_colors = False
self.adb_command = 'adb' self.adb_command = 'adb'
self.adb_connect = '' self.adb_connect = ''
self.try_su = False
self.read() self.read()
self.connect() self.connect()
@@ -189,13 +188,14 @@ class Conf(object):
if not os.path.exists(conf_fname): if not os.path.exists(conf_fname):
return return
cfg = configparser.SafeConfigParser() cfg = configparser.ConfigParser()
cfg_map = {'debug': (cfg.getboolean, 'debug'), cfg_map = {'debug': (cfg.getboolean, 'debug'),
'dirs_to_skip': (cfg.get, 'dirs_to_skip'), 'dirs_to_skip': (cfg.get, 'dirs_to_skip'),
'suppress_colors': (cfg.get, 'suppress_colors'), 'suppress_colors': (cfg.get, 'suppress_colors'),
'root': (cfg.get, 'root'), 'root': (cfg.get, 'root'),
'adb_command': (cfg.get, 'adb_command'), 'adb_command': (cfg.get, 'adb_command'),
'adb_connect': (cfg.get, 'adb_connect')} 'adb_connect': (cfg.get, 'adb_connect'),
'try_su': (cfg.getboolean, 'try_su')}
cfg.read(conf_fname) cfg.read(conf_fname)
for key, (function, attr) in cfg_map.items(): for key, (function, attr) in cfg_map.items():
@@ -205,7 +205,7 @@ class Conf(object):
pass pass
if self.dirs_to_skip and 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)
self.dirs_to_skip = [x.encode('utf-8') for x in self.dirs_to_skip] self.dirs_to_skip = [x.encode('utf-8') for x in self.dirs_to_skip]
else: else:
self.dirs_to_skip = [] self.dirs_to_skip = []
@@ -335,11 +335,23 @@ class Adb(object):
self._links = {} self._links = {}
self._got_root = False self._got_root = False
self.__su_check() 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): 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() cmd = self._shell_cmd(False, 'su -c whoami')
try: try:
with open(os.devnull, 'w') as fnull: with open(os.devnull, 'w') as fnull:
result = check_output(cmd, stderr=fnull) result = check_output(cmd, stderr=fnull)
@@ -389,10 +401,8 @@ class Adb(object):
def _retrieve_single_dir_list(self, dir_): def _retrieve_single_dir_list(self, dir_):
"""Retrieve file list using adb""" """Retrieve file list using adb"""
lscmd = self.conf.box['rls'].format(dir_) lscmd = self.conf.box['rls'].format(shlex.quote(dir_))
if self._got_root: command = self._shell_cmd(True, *shlex.split(lscmd))
lscmd = 'su -c "{}"'.format(lscmd)
command = [self.conf.adb_command, 'shell', lscmd]
try: try:
if self.conf.debug: if self.conf.debug:
@@ -449,12 +459,9 @@ class Adb(object):
if not root: if not root:
lscmd = self.conf.box['ls'] lscmd = self.conf.box['ls']
else: else:
lscmd = self.conf.box['rls'].format(root.filepath) lscmd = self.conf.box['rls'].format(shlex.quote(root.filepath))
if self._got_root: command = self._shell_cmd(True, *shlex.split(lscmd))
lscmd = 'su -c "{}"'.format(lscmd)
command = [self.conf.adb_command, 'shell', lscmd]
try: try:
if self.conf.debug: if self.conf.debug:
@@ -463,7 +470,7 @@ class Adb(object):
lines = check_output(command) lines = 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 2
current_dir = root.dirname if root else '/' current_dir = root.dirname if root else '/'
for line in lines.split('\n'): for line in lines.split('\n'):
@@ -499,13 +506,13 @@ class Adb(object):
"""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"""
if self.error: if self.error:
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 4
if self.conf.root: if self.conf.root:
self._retrieve_single_dir_list(self.conf.root) self._retrieve_single_dir_list(self.conf.root)
@@ -520,7 +527,7 @@ class Adb(object):
"""Copy file form the device using adb.""" """Copy file form the device using adb."""
if self.error: if self.error:
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 5
cmd = [self.conf.adb_command, 'pull', src, dst] cmd = [self.conf.adb_command, 'pull', src, dst]
if self.conf.debug: if self.conf.debug:
@@ -531,7 +538,7 @@ class Adb(object):
err = subprocess.call(cmd, stdout=fnull, stderr=fnull) err = subprocess.call(cmd, stdout=fnull, stderr=fnull)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell') sys.stderr.write('Error executing adb shell')
return 1 return 6
return err return err
@@ -539,7 +546,7 @@ class Adb(object):
"""Copy file to the device through adb.""" """Copy file to the device through adb."""
if self.error: if self.error:
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 7
if not dst.startswith('/'): if not dst.startswith('/'):
dst = '/' + dst dst = '/' + dst
@@ -552,69 +559,69 @@ class Adb(object):
err = subprocess.call(cmd, stdout=fnull, stderr=fnull) err = subprocess.call(cmd, stdout=fnull, stderr=fnull)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell') sys.stderr.write('Error executing adb shell')
return 1 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."""
if self.error: if self.error:
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 10
cmd = [self.conf.adb_command, 'shell', 'rm', dst] cmd = self._shell_cmd(False, 'rm %s' % shlex.quote(dst))
try: try:
err = check_output(cmd) err = check_output(cmd).strip()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell') sys.stderr.write('Error executing adb shell')
return 1 return 11
if err != '': 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."""
if self.error: if self.error:
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 13
cmd = [self.conf.adb_command, 'shell', 'rm', '-r', dst] cmd = self._shell_cmd(False, 'rm -r %s' % shlex.quote(dst))
try: try:
err = check_output(cmd) err = check_output(cmd).strip()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell') sys.stderr.write('Error executing adb shell')
return 1 return 14
if err != '': 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."""
if self.error: if self.error:
sys.stderr.write(self.error) sys.stderr.write(self.error)
return 1 return 16
if not dst.startswith('/'): if not dst.startswith('/'):
dst = '/' + dst dst = '/' + dst
cmd = [self.conf.adb_command, 'shell', 'mkdir', dst] cmd = self._shell_cmd(False, 'mkdir %s' % shlex.quote(dst))
try: try:
err = check_output(cmd) err = check_output(cmd).strip()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
sys.stderr.write('Error executing adb shell') sys.stderr.write('Error executing adb shell')
return 1 return 17
if err != '': if err != '':
sys.stderr.write(err) sys.stderr.write(err)
return 1 return 18
return 0 return 0
@@ -673,7 +680,11 @@ def main():
args = parser.parse_args() args = parser.parse_args()
return args.func(args) try:
return args.func(args)
except AttributeError:
parser.print_help()
parser.exit()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,24 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import six from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
import unittest import unittest
try: from unittest import mock
from unittest import mock
except ImportError:
import mock
FILE = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'adbfs')
try: module_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'adbfs')
from importlib.util import spec_from_loader, module_from_spec spec = spec_from_loader("adbfs", SourceFileLoader("adbfs", module_path))
from importlib.machinery import SourceFileLoader adbfs = module_from_spec(spec)
spec = spec_from_loader("adbfs", SourceFileLoader("adbfs", FILE)) spec.loader.exec_module(adbfs)
adbfs = module_from_spec(spec)
spec.loader.exec_module(adbfs)
except ImportError:
# py27
import imp
adbfs = imp.load_source('adbfs', FILE)
LISTING = '''\ 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/Grüß Gott
@@ -44,10 +35,7 @@ class TestCheckOutput(unittest.TestCase):
Python3 treats string as unicode objects, but subprocess.check_output Python3 treats string as unicode objects, but subprocess.check_output
returns bytes object, which is equvalend for py2 string… annoying. returns bytes object, which is equvalend for py2 string… annoying.
""" """
if six.PY3: out.return_value = bytes(LISTING, 'utf-8')
out.return_value = bytes(LISTING, 'utf-8')
else:
out.return_value = LISTING
result = adbfs.check_output(None) result = adbfs.check_output(None)
self.assertEqual(result, LISTING) self.assertEqual(result, LISTING)
@@ -57,9 +45,6 @@ class TestCheckOutput(unittest.TestCase):
Special case for py3. We have bytes with some weird character - like Special case for py3. We have bytes with some weird character - like
some system write something with codepage, instead of utf8. some system write something with codepage, instead of utf8.
""" """
if six.PY2:
# doesn't affect Python 2
return
line = (b'-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 ' line = (b'-rw-rw---- 1 0 1015 0 01/01/2010 22:11:01 '
b'/storage/emulated/0/\xe2\n') # Latin 1 char â b'/storage/emulated/0/\xe2\n') # Latin 1 char â
out.return_value = bytes(line) out.return_value = bytes(line)