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:
304
adb-sync
304
adb-sync
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user