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

Add --copy-links, use stat instead of lstat

To properly sync symlinks, use stat instead of lstat, otherwise we get the size
of the link (instead of the file) and we end up constantly re-pushing
everything. Also add a --copy-links/-L option that enables syncing of symlinks,
similarly to rsync.
This commit is contained in:
Kostas Chatzikokolakis
2018-11-27 13:58:06 -08:00
committed by Rudolf Polzer
parent e6d9ffdbb0
commit b0a2a10852

View File

@@ -36,6 +36,9 @@ class OSLike(object):
def lstat(self, path: bytes) -> os.stat_result: # os's name, so pylint: disable=g-bad-name
raise NotImplementedError('Abstract')
def stat(self, path: bytes) -> os.stat_result: # os's name, so pylint: disable=g-bad-name
raise NotImplementedError('Abstract')
def unlink(self, path: bytes) -> None: # os's name, so pylint: disable=g-bad-name
raise NotImplementedError('Abstract')
@@ -259,6 +262,23 @@ class AdbFileSystem(GlobLike, OSLike):
return statdata
raise OSError('No such file or directory')
def stat(self, path: bytes) -> os.stat_result: # os's name, so pylint: disable=g-bad-name
"""Stat a file."""
if path in self.stat_cache and not stat.S_ISLNK(
self.stat_cache[path].st_mode):
return self.stat_cache[path]
with Stdout(
self.adb +
[b'shell', b'ls -aldL %s' % (self.QuoteArgument(path),)]) as stdout:
for line in stdout:
if line.startswith(b'total '):
continue
line = line.rstrip(b'\r\n')
statdata, _ = self.LsToStat(line)
self.stat_cache[path] = statdata
return statdata
raise OSError('No such file or directory')
def unlink(self, path: bytes) -> None: # os's name, so pylint: disable=g-bad-name
"""Delete a file."""
if subprocess.call(
@@ -316,13 +336,15 @@ class AdbFileSystem(GlobLike, OSLike):
raise OSError('pull failed')
def BuildFileList(fs: OSLike, path: bytes,
def BuildFileList(fs: OSLike, path: bytes, follow_links: bool,
prefix: bytes) -> Iterable[Tuple[bytes, os.stat_result]]:
"""Builds a file list.
Args:
fs: File system provider (can be os or AdbFileSystem()).
path: Initial path.
follow_links: Whether to follow symlinks while iterating. May recurse
endlessly.
prefix: Path prefix for output file names.
Yields:
@@ -330,6 +352,9 @@ def BuildFileList(fs: OSLike, path: bytes,
Directories are yielded before their contents.
"""
try:
if follow_links:
statresult = fs.stat(path)
else:
statresult = fs.lstat(path)
except OSError:
return
@@ -342,9 +367,12 @@ def BuildFileList(fs: OSLike, path: bytes,
for n in files:
if n == b'.' or n == b'..':
continue
for t in BuildFileList(fs, path + b'/' + n, prefix + b'/' + n):
for t in BuildFileList(fs, path + b'/' + n, follow_links,
prefix + b'/' + n):
yield t
elif stat.S_ISREG(statresult.st_mode) or stat.S_ISLNK(statresult.st_mode):
elif stat.S_ISREG(statresult.st_mode):
yield prefix, statresult
elif stat.S_ISLNK(statresult.st_mode) and not follow_links:
yield prefix, statresult
else:
logging.info('Unsupported file: %r.', path)
@@ -444,7 +472,7 @@ class FileSyncer(object):
def __init__(self, adb: AdbFileSystem, local_path: bytes, remote_path: bytes,
local_to_remote: bool, remote_to_local: bool,
preserve_times: bool, delete_missing: bool,
allow_overwrite: bool, allow_replace: bool,
allow_overwrite: bool, allow_replace: bool, copy_links: bool,
dry_run: bool) -> None:
self.local = local_path
self.remote = remote_path
@@ -455,6 +483,7 @@ class FileSyncer(object):
self.delete_missing = delete_missing
self.allow_overwrite = allow_overwrite
self.allow_replace = allow_replace
self.copy_links = copy_links
self.dry_run = dry_run
self.num_bytes = 0
self.start_time = time.time()
@@ -480,8 +509,9 @@ class FileSyncer(object):
def ScanAndDiff(self) -> None:
"""Scans the local and remote locations and identifies differences."""
logging.info('Scanning and diffing...')
locallist = BuildFileList(cast(OSLike, os), self.local, b'')
remotelist = BuildFileList(self.adb, self.remote, b'')
locallist = BuildFileList(
cast(OSLike, os), self.local, self.copy_links, b'')
remotelist = BuildFileList(self.adb, self.remote, self.copy_links, b'')
self.local_only, self.both, self.remote_only = DiffLists(
locallist, remotelist)
if not self.local_only and not self.both and not self.remote_only:
@@ -757,6 +787,11 @@ def main() -> None:
action='store_true',
help='Do not ever overwrite any '
'existing files. Mutually exclusive with -f.')
parser.add_argument(
'-L',
'--copy-links',
action='store_true',
help='transform symlink into referent file/dir')
parser.add_argument(
'--dry-run',
action='store_true',
@@ -797,6 +832,7 @@ def main() -> None:
delete_missing = args.delete
allow_replace = args.force
allow_overwrite = not args.no_clobber
copy_links = args.copy_links
dry_run = args.dry_run
local_to_remote = True
remote_to_local = False
@@ -830,7 +866,7 @@ def main() -> None:
logging.info('Sync: local %r, remote %r', localpaths[i], remotepaths[i])
syncer = FileSyncer(adb, localpaths[i], remotepaths[i], local_to_remote,
remote_to_local, preserve_times, delete_missing,
allow_overwrite, allow_replace, dry_run)
allow_overwrite, allow_replace, copy_links, dry_run)
if not syncer.IsWorking():
logging.error('Device not connected or not working.')
return