diff --git a/pyadb/internal/__init__.py b/pyadb/internal/__init__.py index db68be8..f842a4c 100644 --- a/pyadb/internal/__init__.py +++ b/pyadb/internal/__init__.py @@ -1,5 +1,144 @@ +import os +import posixpath +import shutil +from send2trash import send2trash +from pyadb.internal import config +from pyadb.internal.directory import Directory, get_unmodified from pyadb.internal.cli_wrap import AdbWrapper +def merge(src, dst, log=False): + if not os.path.exists(dst): + return False + ok = True + for path, dirs, files in os.walk(src): + relPath = os.path.relpath(path, src) + destPath = os.path.join(dst, relPath) + if not os.path.exists(destPath): + os.makedirs(destPath) + for file in files: + destFile = os.path.join(destPath, file) + if os.path.isfile(destFile): + if log: + print("Skipping existing file: " + + os.path.join(relPath, file)) + ok = False + continue + srcFile = os.path.join(path, file) + shutil.move(srcFile, destFile) + for path, dirs, files in os.walk(src, False): + if len(files) == 0 and len(dirs) == 0: + os.rmdir(path) + return ok + + class FileSystem(AdbWrapper): + STAT_TYPE = 1 + + def stat(self, file): + """Returns information on file""" + # %a Access bits (octal) |%A Access bits (flags)|%b Size/512 + # %B Bytes per %b (512) |%d Device ID (dec) |%D Device ID (hex) + # %f All mode bits (hex) |%F File type |%g Group ID + # %G Group name |%h Hard links |%i Inode + # %m Mount point |%n Filename |%N Long filename + # %o I/O block size |%s Size (bytes) |%t Devtype major + # %T Devtype minor (hex) |%u User ID |%U User name + # %x Access time |%X Access unix time |%y File write time + # %Y File write unix time|%z Dir change time |%Z Dir change time + + # The valid format escape sequences for filesystems: + # %a Available blocks |%b Total blocks |%c Total inodes + # %d Free inodes |%f Free blocks |%i File system ID + # %l Max filename length |%n File name |%s Fragment size + # %S Best transfer size |%t FS type (hex) |%T FS type + command = 'stat -c "%A;%F;%U;%G" {};echo $?'.format(file) + res = self.sudo(command, output="out") + output, res = res.split('\n') + if res == '0': + return output.split(';') + + def exists(self, file): + return self.stat(file) is not None + + def isfile(self, file): + return self.stat(file)[FileSystem.STAT_TYPE] == 'file' + + def isdir(self, file): + return self.stat(file)[FileSystem.STAT_TYPE] == 'directory' + + def islink(self, file): + return self.stat(file)[FileSystem.STAT_TYPE] == 'symbolic link' + + def delete(self, path): + return self.sudo(["rm", "-rf", path], output="out") + + def pull(self, remote, local, override=False): + parent = os.path.dirname(local) + if not os.path.exists(parent): + os.makedirs(parent) + if os.path.exists(local): + if override: + if os.path.isdir(local): + shutil.rmtree(local) + else: + os.remove(local) + else: + raise FileExistsError + + self.execute(['pull', remote, local]) + + def push(self, local, remote): + self.execute(['push', remote, local]) + + def merge_local_with_remote(self, remote, local, dry=True, delete=False): + """Merge local directory with directory on phone, optionally deleting""" # noqa + stdout, stderr = self.execute( + ['cd', remote, ';', 'find', '.', '-type', 'f'], + output_streams=True + ) + listing = stdout.read().decode().rstrip().split(config.LINEFEED) + remote_dirtree = Directory.from_dir_listing( + listing, is_listening=True) + prev_working = os.getcwd() + os.chdir(local) + for root, dirs, files in os.walk("."): + new_root = root.split(os.sep) + parent = remote_dirtree.traverse( + new_root, create_intermediate=False) + for file in files: + fp = os.path.join(root, file) + try: + parent.remove_child(file) + except FileNotFoundError: + if delete: + if dry: + print("Removing:", fp) + else: + send2trash(fp) + + os.chdir(prev_working) + for file in get_unmodified(remote_dirtree): + if config.IS_WINDOWS: + local_path = os.path.join(local, file.replace('/', os.sep)) + else: + local_path = os.path.join(local, file) + remote_path = posixpath.join(remote, file) + if dry: + print(remote_path, local_path, sep='->') + else: + self.pull(remote_path, local_path) + + +class NormalDevice(FileSystem): + """Device model that represents when the device is booted in an os + + """ + pass + + +class RecoveryMode(FileSystem): + """Device model that represents wehn the device is in recovery mode + + """ pass diff --git a/pyadb/internal/directory.py b/pyadb/internal/directory.py index 0c24817..b53ee00 100644 --- a/pyadb/internal/directory.py +++ b/pyadb/internal/directory.py @@ -30,7 +30,10 @@ class Directory(File): self.modified = False def __index__(self, key): - return self.children[key] + try: + return self.children[key] + except KeyError: + raise FileNotFoundError def mark_modified(self): if self.is_listening: @@ -44,10 +47,7 @@ class Directory(File): path = path.split('/') if len(path) == 1: - try: - return self[path[0]] - except KeyError: - raise FileNotFoundError + return self[path[0]] _next, *rest = path if _next == "..": @@ -60,13 +60,13 @@ class Directory(File): else: try: selected = self[_next] - except KeyError: + except FileNotFoundError as e: if create_intermediate: new = Directory(_next, self.is_listening) self.add_child(new, notify) selected = new else: - raise FileNotFoundError + raise e return selected.traverse( rest, create_intermediate=create_intermediate, notify=notify @@ -107,3 +107,14 @@ class Directory(File): file = File(file) parent.add_child(file, notify=False) return root + + +def get_unmodified(dirtree: Directory): + if dirtree.modified: + for child in dirtree.children.values(): + if isinstance(child, Directory): + yield from get_unmodified(child) + else: + yield child.get_fullpath() + else: + yield dirtree.get_fullpath()+'/'