1
0
mirror of https://github.com/gryf/mc_adbfs.git synced 2025-12-18 12:00:19 +01:00

Added proper symlink treatment

This commit is contained in:
2015-09-05 12:39:20 +02:00
parent 5da8792dcf
commit 9ece93b5d8
2 changed files with 159 additions and 75 deletions

View File

@@ -9,6 +9,7 @@ Rquirements
=========== ===========
* Python 2.7 * Python 2.7
* ``adb`` installed and in ``$PATH``
* 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 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. 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 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 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 :)
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 License
======= =======

214
adbfs
View File

@@ -4,6 +4,8 @@ adbfs Virtual filesystem for Midnight Commander
* Copyright (c) 2015, Roman Dobosz, * Copyright (c) 2015, 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
""" """
from datetime import datetime from datetime import datetime
@@ -13,31 +15,41 @@ import re
import sys import sys
class Adb(object): class File(object):
"""Class for interact with android rooted device through adb""" """Item in filesystem representation"""
adb = "/opt/android-sdk-update-manager/platform-tools/adb" def __init__(self, perms=None, links=1, uid=0, gid=0, size=0,
skip_system_dir = os.getenv("ADBFS_SKIP_SYSTEM_DIR", True) date_time=None, name=None):
dirs_to_skip = ["acct", "charger", "d", "dev", "proc", "sys"] """initialize file"""
file_re = re.compile(r'^(?P<perms>[-bcdlps][-rwxsStT]{9})\s+' self.perms = perms
r'(?P<links>\d+)\s' self.links = links
r'(?P<uid>\d+)\s+' self.uid = uid
r'(?P<gid>\d+)\s+' self.gid = gid
r'(?P<size>\d+)\s[A-Z,a-z]{3}\s' self.size = size
r'(?P<datetime>[A-Z,a-z]{3}\s+' self.date_time = date_time # as string
r'\d+\s\d{2}:\d{2}:\d{2}\s+\d{4})\s' self.name = name
r'(?P<name>.*)')
current_re = re.compile(r"^(\./)?(?P<dir>.+):$") self.dirname = ""
as_root = os.getenv("ADBFS_AS_ROOT", False) self.type = None
verbose = os.getenv("ADBFS_VERBOSE", False) self.string = None
self.link_target = None
self.filepath = None
def __init__(self): def _correct_link(self):
"""Prepare archive content for operations""" """Canonize filename and fill the link attr"""
super(Adb, self).__init__() try:
self._entries = [] name, target = self.name.split(" -> ")
except ValueError:
return
def _correct_entry(self, entry, current_dir): self.name = name
"""parse date string, append current_dir to the entry""" 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, month_num = {"Jan": 1,
"Feb": 2, "Feb": 2,
"Mar": 3, "Mar": 3,
@@ -50,41 +62,94 @@ class Adb(object):
"Oct": 10, "Oct": 10,
"Nov": 11, "Nov": 11,
"Dec": 12} "Dec": 12}
entry["dir"] = current_dir self.dirname = dirname
entry["fullname"] = os.path.join(current_dir, entry['name']) date = self.date_time.split()
date = entry["datetime"].split()
date = "%s-%02d-%s %s" % (date[1], date = "%s-%02d-%s %s" % (date[1],
month_num[date[0]], month_num[date[0]],
date[3], date[3],
date[2]) date[2])
date = datetime.strptime(date, "%d-%m-%Y %H:%M:%S") 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): if self.type == "l" and " -> " in self.name:
"""Convert links to relative, if needed""" self._correct_link()
fname, target = entry['name'].split(" -> ")
if not target.startswith("/"): self.filepath = os.path.join(self.dirname, self.name)
return
dir_ = entry["dir"] if entry["dir"] else "/" def mk_link_relative(self, target_type):
target = os.path.relpath(os.path.join(dir_, target), dir_) """Convert links to relative"""
entry['name'] = fname + " -> " + target 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 "<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")
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<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>.*)')
current_re = re.compile(r"^(\./)?(?P<dir>.+):$")
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): def _find_target(self, needle):
"""Find link target""" """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: for entry in self._entries:
if ' -> ' in entry["name"] and entry['perms'].startswith("l"): if entry.filepath == needle:
fullname, target = entry["fullname"].split(" -> ") return entry
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']
return None return None
def _normalize_links(self): def _normalize_links(self):
@@ -95,26 +160,18 @@ class Adb(object):
/bar -> /foo /bar -> /foo
If one want to follow such 'bar' link - MC in extfs mode will fail to 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 figure out the right target. This helper will correct the thing.
remove dead links.
""" """
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 idx in sorted(elems_to_remove, reverse=True):
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):
del self._entries[idx] del self._entries[idx]
def _retrieve_file_list(self, root=None): def _retrieve_file_list(self, root=None):
@@ -124,39 +181,43 @@ class Adb(object):
if not root: if not root:
command.append("'busybox ls -anel'") command.append("'busybox ls -anel'")
else: 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"): 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:
current_dir = "/"
continue continue
reg_match = self.file_re.match(line) reg_match = self.file_re.match(line)
if not reg_match: if not reg_match:
continue continue
entry = reg_match.groupdict() entry = File(**reg_match.groupdict())
if entry["name"] in (".", ".."): if entry.name in (".", ".."):
continue 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 continue
self._correct_entry(entry, current_dir) entry.update(current_dir)
entry["str"] = ("{perms} {links:>4} {uid:<8} {gid:<8} {size:>8} "
"{datetime} {dir}/{name}\n".format(**entry))
self._entries.append(entry) 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._retrieve_file_list(entry)
self._normalize_links() if entry.type == "l":
self._links[entry.filepath] = entry
def run(self, fname): def run(self, fname):
"""Not supported""" """Not supported"""
@@ -167,7 +228,10 @@ class Adb(object):
def list(self): def list(self):
"""Output list contents directory""" """Output list contents directory"""
self._retrieve_file_list() 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 return 0
def copyout(self, src, dst): def copyout(self, src, dst):