1
0
mirror of https://github.com/google/adb-sync.git synced 2026-01-03 09:58:01 +00:00

Add annotations to pass both mypy and pytype.

This commit is contained in:
Rudolf Polzer
2018-11-27 09:57:41 -08:00
parent 6dde901f5a
commit 384a485224

304
adb-sync
View File

@@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python3
# Copyright 2014 Google Inc. All rights reserved. # Copyright 2014 Google Inc. All rights reserved.
# #
@@ -26,13 +26,65 @@ import stat
import subprocess import subprocess
import sys import sys
import time import time
from typing import Callable, cast, Dict, List, IO, Iterable, Tuple
class AdbFileSystem(object): class OSLike(object):
def listdir(self, path: bytes) -> Iterable[bytes]:
raise NotImplementedError('Abstract')
def lstat(self, path: bytes) -> os.stat_result:
raise NotImplementedError('Abstract')
def unlink(self, path: bytes) -> None:
raise NotImplementedError('Abstract')
def rmdir(self, path: bytes) -> None:
raise NotImplementedError('Abstract')
def makedirs(self, path: bytes) -> None:
raise NotImplementedError('Abstract')
def utime(self, path: bytes, times: Tuple[float, float]) -> None:
raise NotImplementedError('Abstract')
def glob(self, path: bytes) -> Iterable[bytes]:
raise NotImplementedError('Abstract')
class Stdout(object):
def __init__(self, args: List[bytes]) -> None:
"""Closes the process's stdout when done.
Usage:
with Stdout(...) as stdout:
DoSomething(stdout)
Args:
args: Which program to run.
Returns:
An object for use by 'with'.
"""
self.popen = subprocess.Popen(args, stdout=subprocess.PIPE)
def __enter__(self) -> IO:
return self.popen.stdout
def __exit__(self, exc_type, exc_value, traceback) -> bool:
self.popen.stdout.close()
if self.popen.wait() != 0:
raise OSError('Subprocess exited with nonzero status.')
return False
class AdbFileSystem(OSLike):
"""Mimics os's file interface but uses the adb utility.""" """Mimics os's file interface but uses the adb utility."""
def __init__(self, adb): def __init__(self, adb: List[bytes]) -> None:
self.stat_cache = {} self.stat_cache = {} # type: Dict[bytes, os.stat_result]
self.adb = adb self.adb = adb
# Regarding parsing stat results, we only care for the following fields: # Regarding parsing stat results, we only care for the following fields:
@@ -78,7 +130,7 @@ class AdbFileSystem(object):
(?(S_IFLNK) .* | (?P<filename> .*)) (?(S_IFLNK) .* | (?P<filename> .*))
$""", re.DOTALL | re.VERBOSE) $""", re.DOTALL | re.VERBOSE)
def LsToStat(self, line): def LsToStat(self, line) -> Tuple[os.stat_result, bytes]:
"""Convert a line from 'ls -l' output to a stat result. """Convert a line from 'ls -l' output to a stat result.
Args: Args:
@@ -116,12 +168,11 @@ class AdbFileSystem(object):
st_mode |= stat.S_IFLNK st_mode |= stat.S_IFLNK
if groups['S_IFSOCK']: if groups['S_IFSOCK']:
st_mode |= stat.S_IFSOCK st_mode |= stat.S_IFSOCK
st_size = groups['st_size'] st_size = None if groups['st_size'] is None else int(groups['st_size'])
if st_size is not None: st_mtime = int(
st_size = int(st_size) time.mktime(
st_mtime = time.mktime( time.strptime(
time.strptime( match.group('st_mtime').decode('ascii'), '%Y-%m-%d %H:%M')))
match.group('st_mtime').decode('utf-8'), '%Y-%m-%d %H:%M'))
# Fill the rest with dummy values. # Fill the rest with dummy values.
st_ino = 1 st_ino = 1
@@ -136,38 +187,7 @@ class AdbFileSystem(object):
filename = groups['filename'] filename = groups['filename']
return stbuf, filename return stbuf, filename
def Stdout(self, *popen_args): def QuoteArgument(self, arg: bytes) -> bytes:
"""Closes the process's stdout when done.
Usage:
with Stdout(...) as stdout:
DoSomething(stdout)
Args:
popen_args: Arguments for subprocess.Popen; stdout=PIPE is implicitly
added.
Returns:
An object for use by 'with'.
"""
class Stdout(object):
def __init__(self, popen):
self.popen = popen
def __enter__(self):
return self.popen.stdout
def __exit__(self, exc_type, exc_value, traceback):
self.popen.stdout.close()
if self.popen.wait() != 0:
raise OSError('Subprocess exited with nonzero status.')
return False
return Stdout(subprocess.Popen(*popen_args, stdout=subprocess.PIPE))
def QuoteArgument(self, arg):
# Quotes an argument for use by adb shell. # Quotes an argument for use by adb shell.
# Usually, arguments in 'adb shell' use are put in double quotes by adb, # Usually, arguments in 'adb shell' use are put in double quotes by adb,
# but not in any way escaped. # but not in any way escaped.
@@ -178,7 +198,7 @@ class AdbFileSystem(object):
arg = b'"' + arg + b'"' arg = b'"' + arg + b'"'
return arg return arg
def IsWorking(self): def IsWorking(self) -> bool:
"""Tests the adb connection.""" """Tests the adb connection."""
# This string should contain all possible evil, but no percent signs. # This string should contain all possible evil, but no percent signs.
# Note this code uses 'date' and not 'echo', as date just calls strftime # Note this code uses 'date' and not 'echo', as date just calls strftime
@@ -189,10 +209,9 @@ class AdbFileSystem(object):
] ]
for test_string in test_strings: for test_string in test_strings:
good = False good = False
with self.Stdout( with Stdout(self.adb +
self.adb + [b'shell',
[b'shell', b'date +%s' % b'date +%s' % (self.QuoteArgument(test_string),)]) as stdout:
(self.QuoteArgument(test_string),)]) as stdout:
for line in stdout: for line in stdout:
line = line.rstrip(b'\r\n') line = line.rstrip(b'\r\n')
if line == test_string: if line == test_string:
@@ -201,12 +220,11 @@ class AdbFileSystem(object):
return False return False
return True return True
def listdir(self, path): # os's name, so pylint: disable=g-bad-name def listdir(self, path: bytes) -> Iterable[bytes]: # os's name, so pylint: disable=g-bad-name
"""List the contents of a directory, caching them for later lstat calls.""" """List the contents of a directory, caching them for later lstat calls."""
with self.Stdout( with Stdout(self.adb +
self.adb + [b'shell',
[b'shell', b'ls -al %s' % b'ls -al %s' % (self.QuoteArgument(path + b'/'),)]) as stdout:
(self.QuoteArgument(path + b'/'),)]) as stdout:
for line in stdout: for line in stdout:
if line.startswith(b'total '): if line.startswith(b'total '):
continue continue
@@ -221,11 +239,11 @@ class AdbFileSystem(object):
self.stat_cache[path + b'/' + filename] = statdata self.stat_cache[path + b'/' + filename] = statdata
yield filename yield filename
def lstat(self, path): # os's name, so pylint: disable=g-bad-name def lstat(self, path: bytes) -> os.stat_result: # os's name, so pylint: disable=g-bad-name
"""Stat a file.""" """Stat a file."""
if path in self.stat_cache: if path in self.stat_cache:
return self.stat_cache[path] return self.stat_cache[path]
with self.Stdout( with Stdout(
self.adb + self.adb +
[b'shell', b'ls -ald %s' % (self.QuoteArgument(path),)]) as stdout: [b'shell', b'ls -ald %s' % (self.QuoteArgument(path),)]) as stdout:
for line in stdout: for line in stdout:
@@ -237,62 +255,65 @@ class AdbFileSystem(object):
return statdata return statdata
raise OSError('No such file or directory') raise OSError('No such file or directory')
def unlink(self, path): # os's name, so pylint: disable=g-bad-name def unlink(self, path: bytes) -> None: # os's name, so pylint: disable=g-bad-name
"""Delete a file.""" """Delete a file."""
if subprocess.call( if subprocess.call(
self.adb + [b'shell', b'rm %s' % (self.QuoteArgument(path),)]) != 0: self.adb + [b'shell', b'rm %s' % (self.QuoteArgument(path),)]) != 0:
raise OSError('unlink failed') raise OSError('unlink failed')
def rmdir(self, path): # os's name, so pylint: disable=g-bad-name def rmdir(self, path: bytes) -> None: # os's name, so pylint: disable=g-bad-name
"""Delete a directory.""" """Delete a directory."""
if subprocess.call( if subprocess.call(
self.adb + self.adb +
[b'shell', b'rmdir %s' % (self.QuoteArgument(path),)]) != 0: [b'shell', b'rmdir %s' % (self.QuoteArgument(path),)]) != 0:
raise OSError('rmdir failed') raise OSError('rmdir failed')
def makedirs(self, path): # os's name, so pylint: disable=g-bad-name def makedirs(self, path: bytes) -> None: # os's name, so pylint: disable=g-bad-name
"""Create a directory.""" """Create a directory."""
if subprocess.call( if subprocess.call(
self.adb + self.adb +
[b'shell', b'mkdir -p %s' % (self.QuoteArgument(path),)]) != 0: [b'shell', b'mkdir -p %s' % (self.QuoteArgument(path),)]) != 0:
raise OSError('mkdir failed') raise OSError('mkdir failed')
def utime(self, path, times): def utime(self, path: bytes, times: Tuple[float, float]) -> None:
# TODO(rpolzer): Find out why this does not work (returns status 255). # TODO(rpolzer): Find out why this does not work (returns status 255).
"""Set the time of a file to a specified unix time.""" """Set the time of a file to a specified unix time."""
atime, mtime = times atime, mtime = times
timestr = time.strftime(b'%Y%m%d.%H%M%S', time.localtime(mtime)) timestr = time.strftime('%Y%m%d.%H%M%S',
time.localtime(mtime)).encode('ascii')
if subprocess.call( if subprocess.call(
self.adb + self.adb +
[b'shell', [b'shell',
b'touch -mt %s %s' % (timestr, self.QuoteArgument(path))]) != 0: b'touch -mt %s %s' % (timestr, self.QuoteArgument(path))]) != 0:
raise OSError('touch failed') raise OSError('touch failed')
timestr = time.strftime(b'%Y%m%d.%H%M%S', time.localtime(atime)) timestr = time.strftime('%Y%m%d.%H%M%S',
time.localtime(atime)).encode('ascii')
if subprocess.call( if subprocess.call(
self.adb + self.adb +
[b'shell', [b'shell',
b'touch -at %s %s' % (timestr, self.QuoteArgument(path))]) != 0: b'touch -at %s %s' % (timestr, self.QuoteArgument(path))]) != 0:
raise OSError('touch failed') raise OSError('touch failed')
def glob(self, path): def glob(self, path) -> Iterable[bytes]:
with self.Stdout( with Stdout(
self.adb + self.adb +
[b'shell', b'for p in %s; do echo "$p"; done' % (path,)]) as stdout: [b'shell', b'for p in %s; do echo "$p"; done' % (path,)]) as stdout:
for line in stdout: for line in stdout:
yield line.rstrip(b'\r\n') yield line.rstrip(b'\r\n')
def Push(self, src, dst): def Push(self, src: bytes, dst: bytes):
"""Push a file from the local file system to the Android device.""" """Push a file from the local file system to the Android device."""
if subprocess.call(self.adb + [b'push', src, dst]) != 0: if subprocess.call(self.adb + [b'push', src, dst]) != 0:
raise OSError('push failed') raise OSError('push failed')
def Pull(self, src, dst): def Pull(self, src: bytes, dst: bytes):
"""Pull a file from the Android device to the local file system.""" """Pull a file from the Android device to the local file system."""
if subprocess.call(self.adb + [b'pull', src, dst]) != 0: if subprocess.call(self.adb + [b'pull', src, dst]) != 0:
raise OSError('pull failed') raise OSError('pull failed')
def BuildFileList(fs, path, prefix=b''): def BuildFileList(fs, path: bytes,
prefix: bytes) -> Iterable[Tuple[bytes, os.stat_result]]:
"""Builds a file list. """Builds a file list.
Args: Args:
@@ -311,10 +332,9 @@ def BuildFileList(fs, path, prefix=b''):
if stat.S_ISDIR(statresult.st_mode): if stat.S_ISDIR(statresult.st_mode):
yield prefix, statresult yield prefix, statresult
try: try:
files = list(fs.listdir(path)) files = fs.listdir(path)
except OSError: except OSError:
return return
files.sort()
for n in files: for n in files:
if n == b'.' or n == b'..': if n == b'.' or n == b'..':
continue continue
@@ -326,7 +346,11 @@ def BuildFileList(fs, path, prefix=b''):
logging.info('Unsupported file: %r.', path) logging.info('Unsupported file: %r.', path)
def DiffLists(a, b): def DiffLists(a: Iterable[Tuple[bytes, os.stat_result]],
b: Iterable[Tuple[bytes, os.stat_result]]
) -> Tuple[List[Tuple[bytes, os.stat_result]], List[
Tuple[bytes, os.stat_result, os
.stat_result]], List[Tuple[bytes, os.stat_result]]]:
"""Compares two lists. """Compares two lists.
Args: Args:
@@ -338,12 +362,12 @@ def DiffLists(a, b):
both: the items from both list, with the remaining tuple items combined. both: the items from both list, with the remaining tuple items combined.
b_only: the items from list b. b_only: the items from list b.
""" """
a_only = [] a_only = [] # type: List[Tuple[bytes, os.stat_result]]
b_only = [] b_only = [] # type: List[Tuple[bytes, os.stat_result]]
both = [] both = [] # type: List[Tuple[bytes, os.stat_result, os.stat_result]]
a_iter = iter(a) a_iter = iter(sorted(a))
b_iter = iter(b) b_iter = iter(sorted(b))
a_active = True a_active = True
b_active = True b_active = True
a_available = False a_available = False
@@ -367,7 +391,7 @@ def DiffLists(a, b):
b_active = False b_active = False
break break
if a_item[0] == b_item[0]: if a_item[0] == b_item[0]:
both.append(tuple([a_item[0]] + list(a_item[1:]) + list(b_item[1:]))) both.append((a_item[0], a_item[1], b_item[1]))
a_available = False a_available = False
b_available = False b_available = False
elif a_item[0] < b_item[0]: elif a_item[0] < b_item[0]:
@@ -381,24 +405,62 @@ def DiffLists(a, b):
if a_active: if a_active:
if a_available: if a_available:
a_only.append(a_item) a_only.append(cast(Tuple[bytes, os.stat_result], a_item))
for item in a_iter: for item in a_iter:
a_only.append(item) a_only.append(item)
if b_active: if b_active:
if b_available: if b_available:
b_only.append(b_item) b_only.append(cast(Tuple[bytes, os.stat_result], b_item))
for item in b_iter: for item in b_iter:
b_only.append(item) b_only.append(item)
return a_only, both, b_only return a_only, both, b_only
class DeleteInterruptedFile(object):
def __init__(self, dry_run: bool, fs: OSLike, name: bytes) -> None:
"""Sets up interrupt protection.
Usage:
with DeleteInterruptedFile(False, fs, name):
DoSomething()
If DoSomething() should get interrupted, the file 'name' will be deleted.
The exception otherwise will be passed on.
Args:
dry_run: If true, we don't actually delete.
fs: File system object.
name: File name to delete.
Returns:
An object for use by 'with'.
"""
self.dry_run = dry_run
self.fs = fs
self.name = name
def __enter__(self) -> None:
pass
def __exit__(self, exc_type, exc_value, traceback) -> bool:
if exc_type is not None:
logging.info('Interrupted-%s-Delete: %r',
'Pull' if self.fs == os else 'Push', self.name)
if not self.dry_run:
self.fs.unlink(self.name)
return False
class FileSyncer(object): class FileSyncer(object):
"""File synchronizer.""" """File synchronizer."""
def __init__(self, adb, local_path, remote_path, local_to_remote, def __init__(self, adb: AdbFileSystem, local_path: bytes, remote_path: bytes,
remote_to_local, preserve_times, delete_missing, allow_overwrite, local_to_remote: bool, remote_to_local: bool,
allow_replace, dry_run): preserve_times: bool, delete_missing: bool,
allow_overwrite: bool, allow_replace: bool,
dry_run: bool) -> None:
self.local = local_path self.local = local_path
self.remote = remote_path self.remote = remote_path
self.adb = adb self.adb = adb
@@ -409,21 +471,32 @@ class FileSyncer(object):
self.allow_overwrite = allow_overwrite self.allow_overwrite = allow_overwrite
self.allow_replace = allow_replace self.allow_replace = allow_replace
self.dry_run = dry_run self.dry_run = dry_run
self.local_only = None
self.both = None
self.remote_only = None
self.num_bytes = 0 self.num_bytes = 0
self.start_time = time.time() self.start_time = time.time()
def IsWorking(self): # Attributes filled in later.
local_only = None # type: List[Tuple[bytes, os.stat_result]]
both = None # type: List[Tuple[bytes, os.stat_result, os.stat_result]]
remote_only = None # type: List[Tuple[bytes, os.stat_result]]
src_to_dst = None # type: Tuple[bool, bool]
dst_to_src = None # type: Tuple[bool, bool]
src_only = None # type: Tuple[List[Tuple[bytes, os.stat_result]], List[Tuple[bytes, os.stat_result]]]
dst_only = None # type: Tuple[List[Tuple[bytes, os.stat_result]], List[Tuple[bytes, os.stat_result]]]
src = None # type: Tuple[bytes, bytes]
dst = None # type: Tuple[bytes, bytes]
dst_fs = None # type: Tuple[OSLike, OSLike]
push = None # type: Tuple[str, str]
copy = None # type: Tuple[Callable[[bytes, bytes], None], Callable[[bytes, bytes], None]]
def IsWorking(self) -> bool:
"""Tests the adb connection.""" """Tests the adb connection."""
return self.adb.IsWorking() return self.adb.IsWorking()
def ScanAndDiff(self): def ScanAndDiff(self) -> None:
"""Scans the local and remote locations and identifies differences.""" """Scans the local and remote locations and identifies differences."""
logging.info('Scanning and diffing...') logging.info('Scanning and diffing...')
locallist = BuildFileList(os, self.local) locallist = BuildFileList(os, self.local, b'')
remotelist = BuildFileList(self.adb, self.remote) remotelist = BuildFileList(self.adb, self.remote, b'')
self.local_only, self.both, self.remote_only = DiffLists( self.local_only, self.both, self.remote_only = DiffLists(
locallist, remotelist) locallist, remotelist)
if not self.local_only and not self.both and not self.remote_only: if not self.local_only and not self.both and not self.remote_only:
@@ -434,46 +507,11 @@ class FileSyncer(object):
self.dst_only = (self.remote_only, self.local_only) self.dst_only = (self.remote_only, self.local_only)
self.src = (self.local, self.remote) self.src = (self.local, self.remote)
self.dst = (self.remote, self.local) self.dst = (self.remote, self.local)
self.dst_fs = (self.adb, os) self.dst_fs = (self.adb, cast(OSLike, os))
self.push = ('Push', 'Pull') self.push = ('Push', 'Pull')
self.copy = (self.adb.Push, self.adb.Pull) self.copy = (self.adb.Push, self.adb.Pull)
def InterruptProtection(self, fs, name): def PerformDeletions(self) -> None:
"""Sets up interrupt protection.
Usage:
with self.InterruptProtection(fs, name):
DoSomething()
If DoSomething() should get interrupted, the file 'name' will be deleted.
The exception otherwise will be passed on.
Args:
fs: File system object.
name: File name to delete.
Returns:
An object for use by 'with'.
"""
dry_run = self.dry_run
class DeleteInterruptedFile(object):
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None:
logging.info(b'Interrupted-%s-Delete: %r',
'Pull' if fs == os else 'Push', name)
if not dry_run:
fs.unlink(name)
return False
return DeleteInterruptedFile()
def PerformDeletions(self):
"""Perform all deleting necessary for the file sync operation.""" """Perform all deleting necessary for the file sync operation."""
if not self.delete_missing: if not self.delete_missing:
return return
@@ -493,9 +531,11 @@ class FileSyncer(object):
self.dst_fs[i].unlink(dst_name) self.dst_fs[i].unlink(dst_name)
del self.dst_only[i][:] del self.dst_only[i][:]
def PerformOverwrites(self): def PerformOverwrites(self) -> None:
"""Delete files/directories that are in the way for overwriting.""" """Delete files/directories that are in the way for overwriting."""
src_only_prepend = ([], []) src_only_prepend = (
[], []
) # type: Tuple[List[Tuple[bytes, os.stat_result]], List[Tuple[bytes, os.stat_result]]]
for name, localstat, remotestat in self.both: for name, localstat, remotestat in self.both:
if stat.S_ISDIR(localstat.st_mode) and stat.S_ISDIR(remotestat.st_mode): if stat.S_ISDIR(localstat.st_mode) and stat.S_ISDIR(remotestat.st_mode):
# A dir is a dir is a dir. # A dir is a dir is a dir.
@@ -566,7 +606,7 @@ class FileSyncer(object):
for i in [0, 1]: for i in [0, 1]:
self.src_only[i][:0] = src_only_prepend[i] self.src_only[i][:0] = src_only_prepend[i]
def PerformCopies(self): def PerformCopies(self) -> None:
"""Perform all copying necessary for the file sync operation.""" """Perform all copying necessary for the file sync operation."""
for i in [0, 1]: for i in [0, 1]:
if self.src_to_dst[i]: if self.src_to_dst[i]:
@@ -578,7 +618,7 @@ class FileSyncer(object):
if not self.dry_run: if not self.dry_run:
self.dst_fs[i].makedirs(dst_name) self.dst_fs[i].makedirs(dst_name)
else: else:
with self.InterruptProtection(self.dst_fs[i], dst_name): with DeleteInterruptedFile(self.dry_run, self.dst_fs[i], dst_name):
if not self.dry_run: if not self.dry_run:
self.copy[i](src_name, dst_name) self.copy[i](src_name, dst_name)
if stat.S_ISREG(s.st_mode): if stat.S_ISREG(s.st_mode):
@@ -590,7 +630,7 @@ class FileSyncer(object):
time.asctime(time.localtime(s.st_mtime))) time.asctime(time.localtime(s.st_mtime)))
self.dst_fs[i].utime(dst_name, (s.st_atime, s.st_mtime)) self.dst_fs[i].utime(dst_name, (s.st_atime, s.st_mtime))
def TimeReport(self): def TimeReport(self) -> None:
"""Report time and amount of data transferred.""" """Report time and amount of data transferred."""
if self.dry_run: if self.dry_run:
logging.info('Total: %d bytes', self.num_bytes) logging.info('Total: %d bytes', self.num_bytes)
@@ -602,13 +642,13 @@ class FileSyncer(object):
dt) dt)
def ExpandWildcards(globber, path): def ExpandWildcards(globber: OSLike, path: bytes) -> Iterable[bytes]:
if path.find(b'?') == -1 and path.find(b'*') == -1 and path.find(b'[') == -1: if path.find(b'?') == -1 and path.find(b'*') == -1 and path.find(b'[') == -1:
return [path] return [path]
return globber.glob(path) return globber.glob(path)
def FixPath(src, dst): def FixPath(src: bytes, dst: bytes) -> Tuple[bytes, bytes]:
# rsync-like path munging to make remote specifications shorter. # rsync-like path munging to make remote specifications shorter.
append = b'' append = b''
pos = src.rfind(b'/') pos = src.rfind(b'/')
@@ -629,7 +669,7 @@ def FixPath(src, dst):
return (src, dst) return (src, dst)
def main(*args): def main(*unused_args) -> None:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Synchronize a directory between an Android device and the ' + description='Synchronize a directory between an Android device and the ' +
'local file system') 'local file system')