From 3d00fcb57a8696df83d83c87ec20049e030e43e9 Mon Sep 17 00:00:00 2001 From: Roman Dobosz Date: Wed, 2 Sep 2015 21:15:13 +0200 Subject: [PATCH] Initial commit --- LICENSE | 24 ++++++ README.rst | 43 ++++++++++ adbfs | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 294 insertions(+) create mode 100644 LICENSE create mode 100644 README.rst create mode 100755 adbfs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d992403 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2015, Roman Dobosz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the organization nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ROMAN DOBOSZ BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e26d69a --- /dev/null +++ b/README.rst @@ -0,0 +1,43 @@ +=========================================== +Midnight Commander adbfs external fs plugin +=========================================== + +This is Midnight Commander extfs plugin for browsing Android device through +``adb`` interface written in Python. + +Rquirements +=========== + +* Python 2.7 +* An Android device or emulator preferably rooted +* Busybox installed and available in the path on the device + +Make sure, that issuing from command line:: + + $ adb shell busybox ls + +should display files from root directory on the device. + +Installation +============ + +Copy adbfs into ``~/.local/share/mc/extfs.d/`` directory and make it executable +if needed. + +Usage +===== + +To use it, just issue:: + + 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 :) + +License +======= + +This software is licensed under 3-clause BSD license. See LICENSE file for +details. diff --git a/adbfs b/adbfs new file mode 100755 index 0000000..aecb0c4 --- /dev/null +++ b/adbfs @@ -0,0 +1,227 @@ +#! /usr/bin/env python +""" +adbfs Virtual filesystem for Midnight Commander + + +* Copyright (c) 2015, Roman Dobosz, +* Published under 3-clause BSD-style license (see LICENSE file) +""" + +from argparse import ArgumentParser +from datetime import datetime +import subprocess +import os +import re +import sys + + + +class Adb(object): + """Class for interact with android rooted device through adb""" + adb = "/opt/android-sdk-update-manager/platform-tools/adb" + skip_system_dir = os.getenv("ADBFS_SKIP_SYSTEM_DIR", True) + dirs_to_skip = ["acct", "charger", "d", "dev", "proc", "sys"] + file_re = re.compile(r'^(?P[-bcdlps][-rwxsStT]{9})\s+' + r'(?P\d+)\s' + r'(?P\d+)\s+' + r'(?P\d+)\s+' + r'(?P\d+)\s[A-Z,a-z]{3}\s' + r'(?P[A-Z,a-z]{3}\s+' + r'\d+\s\d{2}:\d{2}:\d{2}\s+\d{4})\s' + r'(?P.*)') + + current_re = re.compile(r"^(\./)?(?P.+):$") + verbose = os.getenv("ADBFS_VERBOSE", False) + + def __init__(self): + """Prepare archive content for operations""" + super(Adb, self).__init__() + self._entries = [] + self._str_ls = [] + self._err = None + + def correct_entry(self, entry, current_dir): + """parse date string, append current_dir to the entry""" + 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} + entry["dir"] = current_dir + date = entry["datetime"].split() + 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") + entry["datetime"] = date.strftime("%m/%d/%Y %H:%M:01") + + def scan_subdirectory(self, entry): + """Recursively scan directory""" + lines = subprocess.check_output([Adb.adb, "shell", "su", "-c", + asdjhsfudh.format(**entry)]) + current_dir = entry["dir"] + 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"] + continue + + reg_match = self.file_re.match(line) + if not reg_match: + continue + + entry = reg_match.groupdict() + if entry["name"] in (".", ".."): + continue + self.correct_entry(entry, current_dir) + + if Adb.skip_system_dir and entry['name'] in Adb.dirs_to_skip: + continue + + self._entries.append(entry) + self._str_ls.append("{perms} {links:>4} {uid:<8} " + "{gid:<8} {size:>8} " + "{datetime} {dir}/{name}\n".format(**entry)) + + def retrieve_file_list(self, root=None): + """Retrieve file list using adb""" + + command = [Adb.adb, "shell", "su", "-c"] + if not root: + command.append("'busybox ls -anel'") + else: + command.append("'busybox ls -Ranel {dir}/{name}'".format(**root)) + + lines = subprocess.check_output(command) + + current_dir = root["dir"] 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"] + continue + + reg_match = self.file_re.match(line) + if not reg_match: + continue + + entry = reg_match.groupdict() + if entry["name"] in (".", ".."): + continue + self.correct_entry(entry, current_dir) + + if Adb.skip_system_dir and entry['name'] in Adb.dirs_to_skip: + continue + + self._entries.append(entry) + self._str_ls.append("{perms} {links:>4} {uid:<8} " + "{gid:<8} {size:>8} " + "{datetime} {dir}/{name}\n".format(**entry)) + + if root is None and entry["perms"].startswith("d"): + self.retrieve_file_list(entry) + + def run(self, fname): + """Not supported""" + self.err = ("Not supported - or maybe you are on compatible " + "architecture?") + return self._show_error() + + def list(self): + """Output list contents directory""" + self.retrieve_file_list() + sys.stdout.write("".join(self._str_ls)) + return 0 + + def copyout(self, src, dst): + """Copy file form the device using adb.""" + with open(os.devnull, "w") as fnull: + return subprocess.call([Adb.adb, "pull", src, dst], + stdout=fnull, stderr=fnull) + + def copyin(self, src, dst): + """Copy file to the device through adb.""" + if not dst.startswith("/"): + dst = "/" + dst + + with open(os.devnull, "w") as fnull: + err = subprocess.call([Adb.adb, "push", src, dst], + stdout=fnull, stderr=fnull) + + if err != 0: + sys.stderr.write("Cannot push the file, " + "%s, error %d" % (dst, err)) + return 1 + return 0 + + def rm(self, dst): + """Remove file from device through adb.""" + sys.stderr.write("not implemented\n") + return 1 + + def mkdir(self, dst): + """Make directory on the device through adb.""" + sys.stderr.write("not implemented\n") + return 1 + + def _show_error(self): + """ + Display an error, if configured, otherwise try to not annoy an user + """ + if Adb.verbose: + return self.err + else: + return 1 + + +CALL_MAP = {'list': lambda a: Adb().list(), + 'copyin': lambda a: Adb().copyin(a.src, a.dst), + 'copyout': lambda a: Adb().copyout(a.src, a.dst), + 'mkdir': lambda a: Adb().mkdir(a.dst), + 'rm': lambda a: Adb().rm(a.dst), + 'run': lambda a: Adb().run(a.dst)} + +def main(): + """parse commandline""" + try: + if sys.argv[1] not in ('list', 'copyin', 'copyout', 'rm', 'mkdir', + "run"): + sys.exit(2) + except IndexError: + sys.exit(2) + + class Arg(object): + """Mimic argparse/optparse object""" + dst = None + src = None + arch = None + + arg = Arg() + + try: + arg.arch = sys.argv[2] + if sys.argv[1] == 'copyin': + arg.src = sys.argv[4] + arg.dst = sys.argv[3] + if sys.argv[1] == 'copyout': + arg.src = sys.argv[3] + arg.dst = sys.argv[4] + elif sys.argv[1] in ('rm', 'run', 'mkdir'): + arg.dst = sys.argv[3] + except IndexError: + sys.exit(2) + + return CALL_MAP[sys.argv[1]](arg) + +if __name__ == "__main__": + sys.exit(main())