1
0
mirror of https://github.com/gryf/mc_adbfs.git synced 2025-12-18 20:10:21 +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
* ``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
=======

214
adbfs
View File

@@ -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<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<datetime>[A-Z,a-z]{3}\s+'
r'\d+\s\d{2}:\d{2}:\d{2}\s+\d{4})\s'
r'(?P<name>.*)')
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<dir>.+):$")
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 "<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):
"""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):