From 9ece93b5d825237c1a68e3f18ab338cbc8f403d5 Mon Sep 17 00:00:00 2001 From: Roman Dobosz Date: Sat, 5 Sep 2015 12:39:20 +0200 Subject: [PATCH] Added proper symlink treatment --- README.rst | 20 +++++ adbfs | 214 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 159 insertions(+), 75 deletions(-) diff --git a/README.rst b/README.rst index e26d69a..d0e16dc 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,7 @@ Rquirements =========== * Python 2.7 +* ``adb`` installed and in ``$PATH`` * An Android device or emulator preferably rooted * Busybox installed and available in the path on the device @@ -18,6 +19,17 @@ Make sure, that issuing from command line:: should display files from root directory on the device. +Features +======== + +* Listing the directory with (default), or without skipping system dirs + (``acct``, ``dev``, ``proc``, etc) +* Copying files from and to the device +* Creating directories +* Removing files and directories +* Symbolic links in lists are corrected to be relative to the file system +* Symbolic links also point to the right target, skipping intermediate links + Installation ============ @@ -36,6 +48,14 @@ 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 :) +Limitations +=========== + +* Initial listing might be slow. Depending on how fast the device is, how many + files are on the device and so on +* Some filenames might be still inaccessible for operating +* All files operations which needs root privileges will fail (for now) + License ======= diff --git a/adbfs b/adbfs index 894efd4..0610ad2 100755 --- a/adbfs +++ b/adbfs @@ -4,6 +4,8 @@ adbfs Virtual filesystem for Midnight Commander * Copyright (c) 2015, Roman Dobosz, * Published under 3-clause BSD-style license (see LICENSE file) + +Version: 0.7 """ from datetime import datetime @@ -13,31 +15,41 @@ 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.*)') +class File(object): + """Item in filesystem representation""" + def __init__(self, perms=None, links=1, uid=0, gid=0, size=0, + date_time=None, name=None): + """initialize file""" + self.perms = perms + self.links = links + self.uid = uid + self.gid = gid + self.size = size + self.date_time = date_time # as string + self.name = name - current_re = re.compile(r"^(\./)?(?P.+):$") - as_root = os.getenv("ADBFS_AS_ROOT", False) - verbose = os.getenv("ADBFS_VERBOSE", False) + self.dirname = "" + self.type = None + self.string = None + self.link_target = None + self.filepath = None - def __init__(self): - """Prepare archive content for operations""" - super(Adb, self).__init__() - self._entries = [] + def _correct_link(self): + """Canonize filename and fill the link attr""" + try: + name, target = self.name.split(" -> ") + except ValueError: + return - def _correct_entry(self, entry, current_dir): - """parse date string, append current_dir to the entry""" + self.name = name + if target.startswith("/"): + self.link_target = target + else: + self.link_target = os.path.abspath(os.path.join(self.dirname, + target)) + + def update(self, dirname): + """update object fields""" month_num = {"Jan": 1, "Feb": 2, "Mar": 3, @@ -50,41 +62,94 @@ class Adb(object): "Oct": 10, "Nov": 11, "Dec": 12} - entry["dir"] = current_dir - entry["fullname"] = os.path.join(current_dir, entry['name']) - date = entry["datetime"].split() + self.dirname = dirname + date = self.date_time.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") + self.date_time = date.strftime("%m/%d/%Y %H:%M:01") + self.type = self.perms[0] if self.perms else None - def _mk_rel_links(self, entry): - """Convert links to relative, if needed""" - fname, target = entry['name'].split(" -> ") + if self.type == "l" and " -> " in self.name: + self._correct_link() - if not target.startswith("/"): - return + self.filepath = os.path.join(self.dirname, self.name) - dir_ = entry["dir"] if entry["dir"] else "/" - target = os.path.relpath(os.path.join(dir_, target), dir_) - entry['name'] = fname + " -> " + target + def mk_link_relative(self, target_type): + """Convert links to relative""" + rel_path = self.dirname + # if target_type == "d": + # rel_path = self.filepath + self.link_target = os.path.relpath(self.link_target, rel_path) + + def __repr__(self): + """represent the file/entire node""" + fullname = os.path.join(self.dirname, self.name) + if self.link_target: + fullname += " -> " + self.link_target + return "".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") + + if not self.name: + return "" + + fullname = os.path.join(self.dirname, self.name) + if self.link_target: + fullname += " -> " + self.link_target + + return template.format(perms=self.perms, + links=self.links, + uid=self.uid, + gid=self.gid, + size=self.size, + date_time=self.date_time, + fullname=fullname) + + +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.+):$") + as_root = os.getenv("ADBFS_AS_ROOT", False) + verbose = os.getenv("ADBFS_VERBOSE", False) + + def __init__(self): + """Prepare archive content for operations""" + super(Adb, self).__init__() + self._entries = [] + self._links = {} def _find_target(self, needle): """Find link target""" + if needle in self._links: + elem = self._links[needle] + target = os.path.abspath(os.path.join(elem.dirname, + elem.link_target)) + return self._find_target(target) + for entry in self._entries: - if ' -> ' in entry["name"] and entry['perms'].startswith("l"): - fullname, target = entry["fullname"].split(" -> ") - if fullname == needle: - dir_ = entry["dir"] if entry["dir"] else "/" - target = os.path.join(os.path.join(dir_, target), dir_) - target = os.path.abspath(target) - return self._find_target(target) - else: - if entry['fullname'] == needle: - return entry['fullname'] + if entry.filepath == needle: + return entry return None def _normalize_links(self): @@ -95,26 +160,18 @@ class Adb(object): /bar -> /foo If one want to follow such 'bar' link - MC in extfs mode will fail to - figure out the right target. This helper will correct the thing and - remove dead links. + figure out the right target. This helper will correct the thing. """ + elems_to_remove = [] + for entry in self._links.values(): + target_entry = self._find_target(entry.link_target) + if target_entry: + entry.link_target = target_entry.filepath + entry.mk_link_relative(target_entry.type) + else: + elems_to_remove.append(self._entries.index(entry)) - elems_to_rm = [] - for entry in self._entries: - if not entry["perms"].startswith("l"): - continue - - fname, target = entry['name'].split(" -> ") - target = self._find_target(target) - - if not target: - elems_to_rm.append(self._entries.index(entry)) - continue - - entry['name'] = fname + " -> " + target - self._mk_rel_links(entry) - - for idx in sorted(elems_to_rm, reverse=True): + for idx in sorted(elems_to_remove, reverse=True): del self._entries[idx] def _retrieve_file_list(self, root=None): @@ -124,39 +181,43 @@ class Adb(object): if not root: command.append("'busybox ls -anel'") else: - command.append("'busybox ls -Ranel {dir}/{name}'".format(**root)) + command.append("'busybox ls -Ranel {}'".format(root.filepath)) - lines = subprocess.check_output(command) + try: + lines = subprocess.check_output(command) + except subprocess.CalledProcessError: + sys.stderr.write("Cannot read directory. Is device connected?\n") + return 1 - current_dir = root["dir"] if root else "" + 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 = reg_match.groupdict() - if entry["name"] in (".", ".."): + entry = File(**reg_match.groupdict()) + if entry.name in (".", ".."): continue - if Adb.skip_system_dir and entry['name'] in Adb.dirs_to_skip: + if Adb.verbose and entry.name in Adb.dirs_to_skip: continue - self._correct_entry(entry, current_dir) - - entry["str"] = ("{perms} {links:>4} {uid:<8} {gid:<8} {size:>8} " - "{datetime} {dir}/{name}\n".format(**entry)) + entry.update(current_dir) self._entries.append(entry) - if root is None and entry["perms"].startswith("d"): + if root is None and entry.type == "d": self._retrieve_file_list(entry) - self._normalize_links() + if entry.type == "l": + self._links[entry.filepath] = entry def run(self, fname): """Not supported""" @@ -167,7 +228,10 @@ class Adb(object): def list(self): """Output list contents directory""" self._retrieve_file_list() - sys.stdout.write("".join([entry["str"] for entry in self._entries])) + # self._retrieve_file_list_from_pickle() + # self._save_file_list_to_pickle() + self._normalize_links() + sys.stdout.write("".join([str(entry) for entry in self._entries])) return 0 def copyout(self, src, dst):