mirror of
https://github.com/google/adb-sync.git
synced 2026-01-03 09:58:01 +00:00
Implement wildcard support on the source side. Now we're rsync
equivalent!
This commit is contained in:
12
README.md
12
README.md
@@ -90,18 +90,6 @@ To copy all downloads from your device to your PC, type:
|
|||||||
adb-sync --reverse /sdcard/Download/ ~/Downloads
|
adb-sync --reverse /sdcard/Download/ ~/Downloads
|
||||||
```
|
```
|
||||||
|
|
||||||
TODO
|
|
||||||
====
|
|
||||||
|
|
||||||
Patches for the following features would be very welcome:
|
|
||||||
|
|
||||||
- Supporting wildcards on the SRC side. This has to take place before appending
|
|
||||||
the last path component of SRC to DST; for example, this should work:
|
|
||||||
`adb-sync /sdcard/* ~/mnt`. The best way to implement this is probably to
|
|
||||||
expand the wildcards first, then run multiple AdbFileSyncer instances; this
|
|
||||||
could even allow for somewhat supporting two-way mode with this (however,
|
|
||||||
two-way mode should then be blocked if SRC ends with a slash!).
|
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
============
|
============
|
||||||
|
|
||||||
|
|||||||
135
adb-sync
135
adb-sync
@@ -19,6 +19,7 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import argparse
|
import argparse
|
||||||
|
import glob
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import stat
|
import stat
|
||||||
@@ -157,11 +158,7 @@ class AdbFileSystem(object):
|
|||||||
arg = arg.replace(b'"', b'\\"')
|
arg = arg.replace(b'"', b'\\"')
|
||||||
arg = arg.replace(b'$', b'\\$')
|
arg = arg.replace(b'$', b'\\$')
|
||||||
arg = arg.replace(b'`', b'\\`')
|
arg = arg.replace(b'`', b'\\`')
|
||||||
# Sometimes adb is evil and puts us NOT in double quotes.
|
arg = b'"' + arg + b'"'
|
||||||
# So here's a horrible hack to always force double quote mode.
|
|
||||||
# Thanks to autoconf guys for describing this method in portable shell.
|
|
||||||
# $PATH is assumed to always be set, even on Android.
|
|
||||||
arg = b'${PATH+"' + arg + b'"}'
|
|
||||||
return arg
|
return arg
|
||||||
|
|
||||||
def IsWorking(self):
|
def IsWorking(self):
|
||||||
@@ -176,8 +173,8 @@ class AdbFileSystem(object):
|
|||||||
]
|
]
|
||||||
for test_string in test_strings:
|
for test_string in test_strings:
|
||||||
good = False
|
good = False
|
||||||
with self.Stdout(self.adb + [b'shell', b'date',
|
with self.Stdout(self.adb + [b'shell', b'date +%s' %
|
||||||
b'+' + 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:
|
||||||
@@ -188,14 +185,14 @@ class AdbFileSystem(object):
|
|||||||
|
|
||||||
def listdir(self, path): # os's name, so pylint: disable=g-bad-name
|
def listdir(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
"""List the contents of a directory."""
|
"""List the contents of a directory."""
|
||||||
with self.Stdout(self.adb + [b'shell', b'ls', b'-a',
|
with self.Stdout(self.adb + [b'shell', b'ls -a %s' %
|
||||||
self.QuoteArgument(path)]) as stdout:
|
self.QuoteArgument(path)]) as stdout:
|
||||||
for line in stdout:
|
for line in stdout:
|
||||||
yield line.rstrip(b'\r\n')
|
yield line.rstrip(b'\r\n')
|
||||||
|
|
||||||
def CacheDirectoryLstat(self, path):
|
def CacheDirectoryLstat(self, path):
|
||||||
"""Cache lstat for a directory."""
|
"""Cache lstat for a directory."""
|
||||||
with self.Stdout(self.adb + [b'shell', b'ls', b'-al',
|
with self.Stdout(self.adb + [b'shell', b'ls -al %s' %
|
||||||
self.QuoteArgument(path + b'/')]) as stdout:
|
self.QuoteArgument(path + b'/')]) as stdout:
|
||||||
for line in stdout:
|
for line in stdout:
|
||||||
line = line.rstrip(b'\r\n')
|
line = line.rstrip(b'\r\n')
|
||||||
@@ -213,7 +210,7 @@ class AdbFileSystem(object):
|
|||||||
"""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(self.adb + [b'shell', b'ls', b'-ald',
|
with self.Stdout(self.adb + [b'shell', b'ls -ald %s' %
|
||||||
self.QuoteArgument(path)]) as stdout:
|
self.QuoteArgument(path)]) as stdout:
|
||||||
for line in stdout:
|
for line in stdout:
|
||||||
line = line.rstrip(b'\r\n')
|
line = line.rstrip(b'\r\n')
|
||||||
@@ -224,17 +221,20 @@ class AdbFileSystem(object):
|
|||||||
|
|
||||||
def unlink(self, path): # os's name, so pylint: disable=g-bad-name
|
def unlink(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
"""Delete a file."""
|
"""Delete a file."""
|
||||||
if subprocess.call(self.adb + [b'shell', b'rm', path]) != 0:
|
if subprocess.call(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): # os's name, so pylint: disable=g-bad-name
|
||||||
"""Delete a directory."""
|
"""Delete a directory."""
|
||||||
if subprocess.call(self.adb + [b'shell', b'rmdir', path]) != 0:
|
if subprocess.call(self.adb + [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): # os's name, so pylint: disable=g-bad-name
|
||||||
"""Create a directory."""
|
"""Create a directory."""
|
||||||
if subprocess.call(self.adb + [b'shell', b'mkdir', b'-p', path]) != 0:
|
if subprocess.call(self.adb + [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, times):
|
||||||
@@ -242,14 +242,20 @@ class AdbFileSystem(object):
|
|||||||
"""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(b'%Y%m%d.%H%M%S', time.localtime(mtime))
|
||||||
if subprocess.call(self.adb + [b'shell', b'touch', b'-mt',
|
if subprocess.call(self.adb + [b'shell', b'touch -mt %s %s' %
|
||||||
timestr, path]) != 0:
|
(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(b'%Y%m%d.%H%M%S', time.localtime(atime))
|
||||||
if subprocess.call(self.adb + [b'shell', b'touch', b'-at',
|
if subprocess.call(self.adb + [b'shell', b'touch -at %s %s' %
|
||||||
timestr, path]) != 0:
|
(timestr, self.QuoteArgument(path))]) != 0:
|
||||||
raise OSError('touch failed')
|
raise OSError('touch failed')
|
||||||
|
|
||||||
|
def glob(self, path):
|
||||||
|
with self.Stdout(self.adb + [b'shell', b'for p in %s; do echo "$p"; done' %
|
||||||
|
path]) as stdout:
|
||||||
|
for line in stdout:
|
||||||
|
yield line.rstrip(b'\r\n')
|
||||||
|
|
||||||
def Push(self, src, dst):
|
def Push(self, src, dst):
|
||||||
"""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:
|
||||||
@@ -374,7 +380,7 @@ class FileSyncer(object):
|
|||||||
allow_replace, dry_run):
|
allow_replace, dry_run):
|
||||||
self.local = local_path
|
self.local = local_path
|
||||||
self.remote = remote_path
|
self.remote = remote_path
|
||||||
self.adb = AdbFileSystem(adb)
|
self.adb = adb
|
||||||
self.local_to_remote = local_to_remote
|
self.local_to_remote = local_to_remote
|
||||||
self.remote_to_local = remote_to_local
|
self.remote_to_local = remote_to_local
|
||||||
self.preserve_times = preserve_times
|
self.preserve_times = preserve_times
|
||||||
@@ -493,7 +499,7 @@ class FileSyncer(object):
|
|||||||
elif localminute < remoteminute:
|
elif localminute < remoteminute:
|
||||||
l2r = False
|
l2r = False
|
||||||
if l2r and r2l:
|
if l2r and r2l:
|
||||||
print('Unresolvable: $%s' % name.decode('utf-8', errors='replace'))
|
print('Unresolvable: %s' % name.decode('utf-8', errors='replace'))
|
||||||
continue
|
continue
|
||||||
if l2r:
|
if l2r:
|
||||||
i = 0 # Local to remote operation.
|
i = 0 # Local to remote operation.
|
||||||
@@ -573,11 +579,30 @@ class FileSyncer(object):
|
|||||||
print('Total: %d KB/s (%d bytes in %.3fs)' % (rate, self.num_bytes, dt))
|
print('Total: %d KB/s (%d bytes in %.3fs)' % (rate, self.num_bytes, dt))
|
||||||
|
|
||||||
|
|
||||||
|
def ExpandWildcards(globber, path):
|
||||||
|
if path.find('?') == -1 and path.find('*') == -1 and path.find('[') == -1:
|
||||||
|
return [path]
|
||||||
|
return globber.glob(path)
|
||||||
|
|
||||||
|
|
||||||
|
def FixPath(src, dst):
|
||||||
|
# rsync-like path munging to make remote specifications shorter.
|
||||||
|
pos = src.rfind(b'/')
|
||||||
|
if pos >= 0:
|
||||||
|
if pos == len(src)-1:
|
||||||
|
# Final slash: copy to the destination "as is".
|
||||||
|
src = src[:-1]
|
||||||
|
else:
|
||||||
|
# No final slash: destination name == source name.
|
||||||
|
dst += src[pos:]
|
||||||
|
return (src, dst)
|
||||||
|
|
||||||
|
|
||||||
def main(*args):
|
def main(*args):
|
||||||
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')
|
||||||
parser.add_argument('source', metavar='SRC', type=str,
|
parser.add_argument('source', metavar='SRC', type=str, nargs='+',
|
||||||
help='The directory to read files/directories from. '+
|
help='The directory to read files/directories from. '+
|
||||||
'This must be a local path if -R is not specified, '+
|
'This must be a local path if -R is not specified, '+
|
||||||
'and an Android path if -R is specified. If SRC does '+
|
'and an Android path if -R is specified. If SRC does '+
|
||||||
@@ -635,17 +660,8 @@ def main(*args):
|
|||||||
'be done.')
|
'be done.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
local = args.source.encode('utf-8')
|
localpatterns = [x.encode('utf-8') for x in args.source]
|
||||||
remote = args.destination.encode('utf-8')
|
remotepath = args.destination.encode('utf-8')
|
||||||
# rsync-like path munging to make remote specifications shorter.
|
|
||||||
pos = local.rfind(b'/')
|
|
||||||
if pos >= 0:
|
|
||||||
if pos == len(local)-1:
|
|
||||||
# Final slash: copy to the destination "as is".
|
|
||||||
local = local[:-1]
|
|
||||||
else:
|
|
||||||
# No final slash: destination name == source name.
|
|
||||||
remote += local[pos:]
|
|
||||||
adb = args.adb.encode('utf-8').split(b' ')
|
adb = args.adb.encode('utf-8').split(b' ')
|
||||||
if args.device:
|
if args.device:
|
||||||
adb += [b'-d']
|
adb += [b'-d']
|
||||||
@@ -657,6 +673,23 @@ def main(*args):
|
|||||||
adb += [b'-H', args.host.encode('utf-8')]
|
adb += [b'-H', args.host.encode('utf-8')]
|
||||||
if args.port != None:
|
if args.port != None:
|
||||||
adb += [b'-P', args.port.encode('utf-8')]
|
adb += [b'-P', args.port.encode('utf-8')]
|
||||||
|
adb = AdbFileSystem(adb)
|
||||||
|
|
||||||
|
# Expand wildcards.
|
||||||
|
localpaths = []
|
||||||
|
remotepaths = []
|
||||||
|
if args.reverse:
|
||||||
|
for pattern in localpatterns:
|
||||||
|
for src in ExpandWildcards(adb, pattern):
|
||||||
|
src, dst = FixPath(src, remotepath)
|
||||||
|
localpaths.append(src)
|
||||||
|
remotepaths.append(dst)
|
||||||
|
else:
|
||||||
|
for src in localpatterns:
|
||||||
|
src, dst = FixPath(src, remotepath)
|
||||||
|
localpaths.append(src)
|
||||||
|
remotepaths.append(dst)
|
||||||
|
|
||||||
preserve_times = False # args.times
|
preserve_times = False # args.times
|
||||||
delete_missing = args.delete
|
delete_missing = args.delete
|
||||||
allow_replace = args.force
|
allow_replace = args.force
|
||||||
@@ -669,7 +702,7 @@ def main(*args):
|
|||||||
remote_to_local = True
|
remote_to_local = True
|
||||||
if args.reverse:
|
if args.reverse:
|
||||||
local_to_remote, remote_to_local = remote_to_local, local_to_remote
|
local_to_remote, remote_to_local = remote_to_local, local_to_remote
|
||||||
local, remote = remote, local
|
localpaths, remotepaths = remotepaths, localpaths
|
||||||
if allow_replace and not allow_overwrite:
|
if allow_replace and not allow_overwrite:
|
||||||
print('--no-clobber and --force are mutually exclusive.')
|
print('--no-clobber and --force are mutually exclusive.')
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
@@ -679,19 +712,31 @@ def main(*args):
|
|||||||
parser.print_help()
|
parser.print_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
syncer = FileSyncer(adb, local, remote,
|
# Two-way sync is only allowed with disjoint remote and local path sets.
|
||||||
local_to_remote, remote_to_local, preserve_times,
|
if (remote_to_local and local_to_remote) or delete_missing:
|
||||||
delete_missing, allow_overwrite, allow_replace, dry_run)
|
if ((remote_to_local and len(localpaths) != len(set(localpaths))) or
|
||||||
if not syncer.IsWorking():
|
(local_to_remote and len(remotepaths) != len(set(remotepaths)))):
|
||||||
print('Device not connected or not working.')
|
print('--two-way and --delete are only supported for disjoint sets of '+
|
||||||
return
|
'source and destination paths (in other words, all SRC must '+
|
||||||
try:
|
'differ in basename).')
|
||||||
syncer.ScanAndDiff()
|
parser.print_help()
|
||||||
syncer.PerformDeletions()
|
return
|
||||||
syncer.PerformOverwrites()
|
|
||||||
syncer.PerformCopies()
|
for i in range(len(localpaths)):
|
||||||
finally:
|
print('Sync: local %s, remote %s' % (localpaths[i], remotepaths[i]))
|
||||||
syncer.TimeReport()
|
syncer = FileSyncer(adb, localpaths[i], remotepaths[i],
|
||||||
|
local_to_remote, remote_to_local, preserve_times,
|
||||||
|
delete_missing, allow_overwrite, allow_replace, dry_run)
|
||||||
|
if not syncer.IsWorking():
|
||||||
|
print('Device not connected or not working.')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
syncer.ScanAndDiff()
|
||||||
|
syncer.PerformDeletions()
|
||||||
|
syncer.PerformOverwrites()
|
||||||
|
syncer.PerformCopies()
|
||||||
|
finally:
|
||||||
|
syncer.TimeReport()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main(*sys.argv)
|
main(*sys.argv)
|
||||||
|
|||||||
Reference in New Issue
Block a user