adb-sync.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. #!/usr/bin/python
  2. # Copyright 2014 Google Inc. All rights reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Sync files from/to an Android device."""
  16. from __future__ import unicode_literals
  17. import sys
  18. reload(sys)
  19. sys.setdefaultencoding("utf-8")
  20. import argparse
  21. import glob
  22. import locale
  23. import os
  24. import re
  25. import stat
  26. import subprocess
  27. import sys
  28. import time
  29. import fnmatch
  30. def _sprintf(s, *args):
  31. # To be able to use string formatting, we first have to covert to
  32. # unicode strings; however, we must do so in a way that preserves all
  33. # bytes, and convert back at the end. An encoding that maps all byte
  34. # values to different Unicode codepoints is cp437.
  35. return (s.decode('cp437') % tuple([
  36. (x.decode('cp437') if type(x) == bytes else x) for x in args
  37. ])).encode('cp437')
  38. def _print(s, *args):
  39. """Writes a binary string to stdout.
  40. Args:
  41. s: The binary format string to write.
  42. args: The args for the format string.
  43. """
  44. if hasattr(sys.stdout, 'buffer'):
  45. # Python 3.
  46. sys.stdout.buffer.write(_sprintf(s, *args) + b'\n')
  47. sys.stdout.buffer.flush()
  48. else:
  49. # Python 2.
  50. sys.stdout.write(_sprintf(s, *args) + b'\n')
  51. class AdbFileSystem(object):
  52. """Mimics os's file interface but uses the adb utility."""
  53. def __init__(self, adb):
  54. self.stat_cache = {}
  55. self.adb = adb
  56. # Regarding parsing stat results, we only care for the following fields:
  57. # - st_size
  58. # - st_mtime
  59. # - st_mode (but only about S_ISDIR and S_ISREG properties)
  60. # Therefore, we only capture parts of 'ls -l' output that we actually use.
  61. # The other fields will be filled with dummy values.
  62. LS_TO_STAT_RE = re.compile(br'''^
  63. (?:
  64. (?P<S_IFREG> -) |
  65. (?P<S_IFBLK> b) |
  66. (?P<S_IFCHR> c) |
  67. (?P<S_IFDIR> d) |
  68. (?P<S_IFLNK> l) |
  69. (?P<S_IFIFO> p) |
  70. (?P<S_IFSOCK> s))
  71. [-r][-w][-xsS]
  72. [-r][-w][-xsS]
  73. [-r][-w][-xtT] # Mode string.
  74. [ ]+
  75. (?:
  76. [0-9]+ # number of hard links
  77. [ ]+
  78. )?
  79. [^ ]+ # User name/ID.
  80. [ ]+
  81. [^ ]+ # Group name/ID.
  82. [ ]+
  83. (?(S_IFBLK) [^ ]+[ ]+[^ ]+[ ]+) # Device numbers.
  84. (?(S_IFCHR) [^ ]+[ ]+[^ ]+[ ]+) # Device numbers.
  85. (?(S_IFDIR) [0-9]+ [ ]+)? # directory Size.
  86. (?(S_IFREG)
  87. (?P<st_size> [0-9]+) # Size.
  88. [ ]+)
  89. (?P<st_mtime>
  90. [0-9]{4}-[0-9]{2}-[0-9]{2} # Date.
  91. [ ]
  92. [0-9]{2}:[0-9]{2}) # Time.
  93. [ ]
  94. # Don't capture filename for symlinks (ambiguous).
  95. (?(S_IFLNK) .* | (?P<filename> .*))
  96. $''', re.DOTALL | re.VERBOSE)
  97. def LsToStat(self, line):
  98. """Convert a line from 'ls -l' output to a stat result.
  99. Args:
  100. line: Output line of 'ls -l' on Android.
  101. Returns:
  102. os.stat_result for the line.
  103. Raises:
  104. OSError: if the given string is not a 'ls -l' output line (but maybe an
  105. error message instead).
  106. """
  107. match = self.LS_TO_STAT_RE.match(line)
  108. if match is None:
  109. _print(b'Warning: could not parse %r.', line)
  110. raise OSError('Unparseable ls -al result.')
  111. groups = match.groupdict()
  112. # Get the values we're interested in.
  113. st_mode = ( # 0755
  114. stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
  115. if groups['S_IFREG']: st_mode |= stat.S_IFREG
  116. if groups['S_IFBLK']: st_mode |= stat.S_IFBLK
  117. if groups['S_IFCHR']: st_mode |= stat.S_IFCHR
  118. if groups['S_IFDIR']: st_mode |= stat.S_IFDIR
  119. if groups['S_IFIFO']: st_mode |= stat.S_IFIFO
  120. if groups['S_IFLNK']: st_mode |= stat.S_IFLNK
  121. if groups['S_IFSOCK']: st_mode |= stat.S_IFSOCK
  122. st_size = groups['st_size']
  123. if st_size is not None:
  124. st_size = int(st_size)
  125. st_mtime = time.mktime(time.strptime(match.group('st_mtime').decode('utf-8'),
  126. '%Y-%m-%d %H:%M'))
  127. # Fill the rest with dummy values.
  128. st_ino = 1
  129. st_rdev = 0
  130. st_nlink = 1
  131. st_uid = -2 # Nobody.
  132. st_gid = -2 # Nobody.
  133. st_atime = st_ctime = st_mtime
  134. stbuf = os.stat_result((st_mode, st_ino, st_rdev, st_nlink, st_uid, st_gid,
  135. st_size, st_atime, st_mtime, st_ctime))
  136. filename = groups['filename']
  137. return stbuf, filename
  138. def Stdout(self, *popen_args):
  139. """Closes the process's stdout when done.
  140. Usage:
  141. with Stdout(...) as stdout:
  142. DoSomething(stdout)
  143. Args:
  144. popen_args: Arguments for subprocess.Popen; stdout=PIPE is implicitly
  145. added.
  146. Returns:
  147. An object for use by 'with'.
  148. """
  149. class Stdout(object):
  150. def __init__(self, popen):
  151. self.popen = popen
  152. def __enter__(self):
  153. return self.popen.stdout
  154. def __exit__(self, exc_type, exc_value, traceback):
  155. self.popen.stdout.close()
  156. if self.popen.wait() != 0:
  157. raise OSError('Subprocess exited with nonzero status.')
  158. return False
  159. return Stdout(subprocess.Popen(*popen_args, stdout=subprocess.PIPE))
  160. def QuoteArgument(self, arg):
  161. # Quotes an argument for use by adb shell.
  162. # Usually, arguments in 'adb shell' use are put in double quotes by adb,
  163. # but not in any way escaped.
  164. arg = arg.replace(b'\\', b'\\\\')
  165. arg = arg.replace(b'"', b'\\"')
  166. arg = arg.replace(b'$', b'\\$')
  167. arg = arg.replace(b'`', b'\\`')
  168. arg = b'"' + arg + b'"'
  169. return arg
  170. def IsWorking(self):
  171. """Tests the adb connection."""
  172. # This string should contain all possible evil, but no percent signs.
  173. # Note this code uses 'date' and not 'echo', as date just calls strftime
  174. # while echo does its own backslash escape handling additionally to the
  175. # shell's. Too bad printf "%s\n" is not available.
  176. test_strings = [
  177. b'(',
  178. b'(; #`ls`$PATH\'"(\\\\\\\\){};!\xc0\xaf\xff\xc2\xbf'
  179. ]
  180. for test_string in test_strings:
  181. good = False
  182. with self.Stdout(self.adb + [b'shell', _sprintf(b'date +%s',
  183. self.QuoteArgument(test_string))]) as stdout:
  184. for line in stdout:
  185. line = line.rstrip(b'\r\n')
  186. if line == test_string:
  187. good = True
  188. if not good:
  189. return False
  190. return True
  191. def listdir(self, path): # os's name, so pylint: disable=g-bad-name
  192. """List the contents of a directory, caching them for later lstat calls."""
  193. with self.Stdout(self.adb + [b'shell', _sprintf(b'ls -al %s',
  194. self.QuoteArgument(path + b'/'))]) as stdout:
  195. for line in stdout:
  196. if line.startswith(b'total '):
  197. continue
  198. line = line.rstrip(b'\r\n')
  199. try:
  200. statdata, filename = self.LsToStat(line)
  201. except OSError:
  202. continue
  203. if filename is None:
  204. _print(b'Warning: could not parse %s', line)
  205. else:
  206. self.stat_cache[path + b'/' + filename] = statdata
  207. yield filename
  208. def lstat(self, path): # os's name, so pylint: disable=g-bad-name
  209. """Stat a file."""
  210. if path in self.stat_cache:
  211. return self.stat_cache[path]
  212. with self.Stdout(self.adb + [b'shell', _sprintf(b'ls -ald %s',
  213. self.QuoteArgument(path))]) as stdout:
  214. for line in stdout:
  215. if line.startswith(b'total '):
  216. continue
  217. line = line.rstrip(b'\r\n')
  218. statdata, filename = self.LsToStat(line)
  219. self.stat_cache[path] = statdata
  220. return statdata
  221. raise OSError('No such file or directory')
  222. def unlink(self, path): # os's name, so pylint: disable=g-bad-name
  223. """Delete a file."""
  224. if subprocess.call(self.adb + [b'shell', _sprintf(b'rm %s',
  225. self.QuoteArgument(path))]) != 0:
  226. raise OSError('unlink failed')
  227. def rmdir(self, path): # os's name, so pylint: disable=g-bad-name
  228. """Delete a directory."""
  229. if subprocess.call(self.adb + [b'shell', _sprintf(b'rmdir %s',
  230. self.QuoteArgument(path))]) != 0:
  231. raise OSError('rmdir failed')
  232. def makedirs(self, path): # os's name, so pylint: disable=g-bad-name
  233. """Create a directory."""
  234. if subprocess.call(self.adb + [b'shell', _sprintf(b'mkdir -p %s',
  235. self.QuoteArgument(path))]) != 0:
  236. raise OSError('mkdir failed')
  237. def utime(self, path, times):
  238. # TODO(rpolzer): Find out why this does not work (returns status 255).
  239. """Set the time of a file to a specified unix time."""
  240. atime, mtime = times
  241. timestr = time.strftime(b'%Y%m%d.%H%M%S', time.localtime(mtime))
  242. if subprocess.call(self.adb + [b'shell', _sprintf(b'touch -mt %s %s',
  243. timestr, self.QuoteArgument(path))]) != 0:
  244. raise OSError('touch failed')
  245. timestr = time.strftime(b'%Y%m%d.%H%M%S', time.localtime(atime))
  246. if subprocess.call(self.adb + [b'shell',_sprintf( b'touch -at %s %s',
  247. timestr, self.QuoteArgument(path))]) != 0:
  248. raise OSError('touch failed')
  249. def glob(self, path):
  250. with self.Stdout(self.adb + [b'shell',
  251. _sprintf(b'for p in %s; do echo "$p"; done',
  252. path)]) as stdout:
  253. for line in stdout:
  254. yield line.rstrip(b'\r\n')
  255. def Push(self, src, dst):
  256. """Push a file from the local file system to the Android device."""
  257. if subprocess.call(self.adb + [b'push', src, dst]) != 0:
  258. raise OSError('push failed')
  259. def Pull(self, src, dst):
  260. """Pull a file from the Android device to the local file system."""
  261. if subprocess.call(self.adb + [b'pull', src, dst]) != 0:
  262. raise OSError('pull failed')
  263. def BuildFileList(fs, path, exclude = [], prefix=b''):
  264. """Builds a file list.
  265. Args:
  266. fs: File system provider (can be os or AdbFileSystem()).
  267. path: Initial path.
  268. prefix: Path prefix for output file names.
  269. Yields:
  270. File names from path (prefixed by prefix).
  271. Directories are yielded before their contents.
  272. """
  273. if not exclude:
  274. exclude = []
  275. try:
  276. statresult = fs.lstat(path)
  277. except OSError:
  278. return
  279. if stat.S_ISDIR(statresult.st_mode):
  280. yield prefix, statresult
  281. try:
  282. files = [f for f in fs.listdir(path) if not any([fnmatch.fnmatch(f, pattern) for pattern in exclude])]
  283. except OSError:
  284. return
  285. files.sort()
  286. for n in files:
  287. if n == b'.' or n == b'..':
  288. continue
  289. for t in BuildFileList(fs, path + b'/' + n, exclude, prefix + b'/' + n):
  290. yield t
  291. elif stat.S_ISREG(statresult.st_mode):
  292. yield prefix, statresult
  293. else:
  294. _print(b'Note: unsupported file: %s', path)
  295. def DiffLists(a, b):
  296. """Compares two lists.
  297. Args:
  298. a: the first list.
  299. b: the second list.
  300. Returns:
  301. a_only: the items from list a.
  302. both: the items from both list, with the remaining tuple items combined.
  303. b_only: the items from list b.
  304. """
  305. a_only = []
  306. b_only = []
  307. both = []
  308. a_iter = iter(a)
  309. b_iter = iter(b)
  310. a_active = True
  311. b_active = True
  312. a_available = False
  313. b_available = False
  314. a_item = None
  315. b_item = None
  316. while a_active and b_active:
  317. if not a_available:
  318. try:
  319. a_item = next(a_iter)
  320. a_available = True
  321. except StopIteration:
  322. a_active = False
  323. break
  324. if not b_available:
  325. try:
  326. b_item = next(b_iter)
  327. b_available = True
  328. except StopIteration:
  329. b_active = False
  330. break
  331. if a_item[0] == b_item[0]:
  332. both.append(tuple([a_item[0]] + list(a_item[1:]) + list(b_item[1:])))
  333. a_available = False
  334. b_available = False
  335. elif a_item[0] < b_item[0]:
  336. a_only.append(a_item)
  337. a_available = False
  338. elif a_item[0] > b_item[0]:
  339. b_only.append(b_item)
  340. b_available = False
  341. else:
  342. raise
  343. if a_active:
  344. if a_available:
  345. a_only.append(a_item)
  346. for item in a_iter:
  347. a_only.append(item)
  348. if b_active:
  349. if b_available:
  350. b_only.append(b_item)
  351. for item in b_iter:
  352. b_only.append(item)
  353. return a_only, both, b_only
  354. class FileSyncer(object):
  355. """File synchronizer."""
  356. def __init__(self, adb, local_path, remote_path, exclude, local_to_remote,
  357. remote_to_local, preserve_times, delete_missing, allow_overwrite,
  358. allow_replace, dry_run):
  359. self.local = local_path
  360. self.remote = remote_path
  361. self.exclude = exclude
  362. self.adb = adb
  363. self.local_to_remote = local_to_remote
  364. self.remote_to_local = remote_to_local
  365. self.preserve_times = preserve_times
  366. self.delete_missing = delete_missing
  367. self.allow_overwrite = allow_overwrite
  368. self.allow_replace = allow_replace
  369. self.dry_run = dry_run
  370. self.local_only = None
  371. self.both = None
  372. self.remote_only = None
  373. self.num_bytes = 0
  374. self.start_time = time.time()
  375. def IsWorking(self):
  376. """Tests the adb connection."""
  377. return self.adb.IsWorking()
  378. def ScanAndDiff(self):
  379. """Scans the local and remote locations and identifies differences."""
  380. _print(b'Scanning and diffing...')
  381. locallist = BuildFileList(os, self.local, self.exclude)
  382. remotelist = BuildFileList(self.adb, self.remote, self.exclude)
  383. self.local_only, self.both, self.remote_only = DiffLists(locallist,
  384. remotelist)
  385. if not self.local_only and not self.both and not self.remote_only:
  386. _print(b'No files seen. User error?')
  387. self.src_to_dst = (self.local_to_remote, self.remote_to_local)
  388. self.dst_to_src = (self.remote_to_local, self.local_to_remote)
  389. self.src_only = (self.local_only, self.remote_only)
  390. self.dst_only = (self.remote_only, self.local_only)
  391. self.src = (self.local, self.remote)
  392. self.dst = (self.remote, self.local)
  393. self.dst_fs = (self.adb, os)
  394. self.push = (b'Push', b'Pull')
  395. self.copy = (self.adb.Push, self.adb.Pull)
  396. def InterruptProtection(self, fs, name):
  397. """Sets up interrupt protection.
  398. Usage:
  399. with self.InterruptProtection(fs, name):
  400. DoSomething()
  401. If DoSomething() should get interrupted, the file 'name' will be deleted.
  402. The exception otherwise will be passed on.
  403. Args:
  404. fs: File system object.
  405. name: File name to delete.
  406. Returns:
  407. An object for use by 'with'.
  408. """
  409. dry_run = self.dry_run
  410. class DeleteInterruptedFile(object):
  411. def __enter__(self):
  412. pass
  413. def __exit__(self, exc_type, exc_value, traceback):
  414. if exc_type is not None:
  415. _print(b'Interrupted-%s-Delete: %s',
  416. b'Pull' if fs == os else b'Push', name)
  417. if not dry_run:
  418. fs.unlink(name)
  419. return False
  420. return DeleteInterruptedFile()
  421. def PerformDeletions(self):
  422. """Perform all deleting necessary for the file sync operation."""
  423. if not self.delete_missing:
  424. return
  425. for i in [0, 1]:
  426. if self.src_to_dst[i] and not self.dst_to_src[i]:
  427. if not self.src_only[i] and not self.both:
  428. _print(b'Cowardly refusing to delete everything.')
  429. else:
  430. for name, s in reversed(self.dst_only[i]):
  431. dst_name = self.dst[i] + name
  432. _print(b'%s-Delete: %s', self.push[i], dst_name)
  433. if stat.S_ISDIR(s.st_mode):
  434. if not self.dry_run:
  435. self.dst_fs[i].rmdir(dst_name)
  436. else:
  437. if not self.dry_run:
  438. self.dst_fs[i].unlink(dst_name)
  439. del self.dst_only[i][:]
  440. def PerformOverwrites(self):
  441. """Delete files/directories that are in the way for overwriting."""
  442. src_only_prepend = ([], [])
  443. for name, localstat, remotestat in self.both:
  444. if stat.S_ISDIR(localstat.st_mode) and stat.S_ISDIR(remotestat.st_mode):
  445. # A dir is a dir is a dir.
  446. continue
  447. elif stat.S_ISDIR(localstat.st_mode) or stat.S_ISDIR(remotestat.st_mode):
  448. # Dir vs file? Nothing to do here yet.
  449. pass
  450. else:
  451. # File vs file? Compare sizes.
  452. if localstat.st_size == remotestat.st_size:
  453. continue
  454. l2r = self.local_to_remote
  455. r2l = self.remote_to_local
  456. if l2r and r2l:
  457. # Truncate times to full minutes, as Android's "ls" only outputs minute
  458. # accuracy.
  459. localminute = int(localstat.st_mtime / 60)
  460. remoteminute = int(remotestat.st_mtime / 60)
  461. if localminute > remoteminute:
  462. r2l = False
  463. elif localminute < remoteminute:
  464. l2r = False
  465. if l2r and r2l:
  466. _print(b'Unresolvable: %s', name)
  467. continue
  468. if l2r:
  469. i = 0 # Local to remote operation.
  470. src_stat = localstat
  471. dst_stat = remotestat
  472. else:
  473. i = 1 # Remote to local operation.
  474. src_stat = remotestat
  475. dst_stat = localstat
  476. dst_name = self.dst[i] + name
  477. _print(b'%s-Delete-Conflicting: %s', self.push[i], dst_name)
  478. if stat.S_ISDIR(localstat.st_mode) or stat.S_ISDIR(remotestat.st_mode):
  479. if not self.allow_replace:
  480. _print(b'Would have to replace to do this. '
  481. b'Use --force to allow this.')
  482. continue
  483. if not self.allow_overwrite:
  484. _print(b'Would have to overwrite to do this, '
  485. b'which --no-clobber forbids.')
  486. continue
  487. if stat.S_ISDIR(dst_stat.st_mode):
  488. kill_files = [x for x in self.dst_only[i]
  489. if x[0][:len(name) + 1] == name + b'/']
  490. self.dst_only[i][:] = [x for x in self.dst_only[i]
  491. if x[0][:len(name) + 1] != name + b'/']
  492. for l, s in reversed(kill_files):
  493. if stat.S_ISDIR(s.st_mode):
  494. if not self.dry_run:
  495. self.dst_fs[i].rmdir(self.dst[i] + l)
  496. else:
  497. if not self.dry_run:
  498. self.dst_fs[i].unlink(self.dst[i] + l)
  499. if not self.dry_run:
  500. self.dst_fs[i].rmdir(dst_name)
  501. elif stat.S_ISDIR(src_stat.st_mode):
  502. if not self.dry_run:
  503. self.dst_fs[i].unlink(dst_name)
  504. else:
  505. if not self.dry_run:
  506. self.dst_fs[i].unlink(dst_name)
  507. src_only_prepend[i].append((name, src_stat))
  508. for i in [0, 1]:
  509. self.src_only[i][:0] = src_only_prepend[i]
  510. def PerformCopies(self):
  511. """Perform all copying necessary for the file sync operation."""
  512. for i in [0, 1]:
  513. if self.src_to_dst[i]:
  514. for name, s in self.src_only[i]:
  515. src_name = self.src[i] + name
  516. dst_name = self.dst[i] + name
  517. _print(b'%s: %s', self.push[i], dst_name)
  518. if stat.S_ISDIR(s.st_mode):
  519. if not self.dry_run:
  520. self.dst_fs[i].makedirs(dst_name)
  521. else:
  522. with self.InterruptProtection(self.dst_fs[i], dst_name):
  523. if not self.dry_run:
  524. self.copy[i](src_name, dst_name)
  525. self.num_bytes += s.st_size
  526. if not self.dry_run:
  527. if self.preserve_times:
  528. _print(b'%s-Times: accessed %s, modified %s',
  529. self.push[i],
  530. time.asctime(time.localtime(s.st_atime)).encode('utf-8'),
  531. time.asctime(time.localtime(s.st_mtime)).encode('utf-8'))
  532. self.dst_fs[i].utime(dst_name, (s.st_atime, s.st_mtime))
  533. def TimeReport(self):
  534. """Report time and amount of data transferred."""
  535. if self.dry_run:
  536. _print(b'Total: %d bytes', self.num_bytes)
  537. else:
  538. end_time = time.time()
  539. dt = end_time - self.start_time
  540. rate = self.num_bytes / 1024.0 / dt
  541. _print(b'Total: %d KB/s (%d bytes in %.3fs)', rate, self.num_bytes, dt)
  542. def ExpandWildcards(globber, path):
  543. if path.find(b'?') == -1 and path.find(b'*') == -1 and path.find(b'[') == -1:
  544. return [path]
  545. return globber.glob(path)
  546. def FixPath(src, dst):
  547. # rsync-like path munging to make remote specifications shorter.
  548. append = b''
  549. pos = src.rfind(b'/')
  550. if pos >= 0:
  551. if src.endswith(b'/'):
  552. # Final slash: copy to the destination "as is".
  553. src = src[:-1]
  554. else:
  555. # No final slash: destination name == source name.
  556. append = src[pos:]
  557. else:
  558. # No slash at all - use same name at destination.
  559. append = b'/' + src
  560. # Append the destination file name if any.
  561. # BUT: do not append "." or ".." components!
  562. if append != b'/.' and append != b'/..':
  563. dst += append
  564. return (src, dst)
  565. def main(*args):
  566. parser = argparse.ArgumentParser(
  567. description='Synchronize a directory between an Android device and the '+
  568. 'local file system')
  569. parser.add_argument('source', metavar='SRC', type=str, nargs='+',
  570. help='The directory to read files/directories from. '+
  571. 'This must be a local path if -R is not specified, '+
  572. 'and an Android path if -R is specified. If SRC does '+
  573. 'not end with a final slash, its last path component '+
  574. 'is appended to DST (like rsync does).')
  575. parser.add_argument('destination', metavar='DST', type=str,
  576. help='The directory to write files/directories to. '+
  577. 'This must be an Android path if -R is not specified, '+
  578. 'and a local path if -R is specified.')
  579. parser.add_argument('-e', '--adb', metavar='COMMAND', default='adb', type=str,
  580. help='Use the given adb binary and arguments.')
  581. parser.add_argument('--device', action='store_true',
  582. help='Directs command to the only connected USB device; '+
  583. 'returns an error if more than one USB device is '+
  584. 'present. '+
  585. 'Corresponds to the "-d" option of adb.')
  586. parser.add_argument('--emulator', action='store_true',
  587. help='Directs command to the only running emulator; '+
  588. 'returns an error if more than one emulator is running. '+
  589. 'Corresponds to the "-e" option of adb.')
  590. parser.add_argument('-s', '--serial', metavar='DEVICE', type=str,
  591. help='Directs command to the device or emulator with '+
  592. 'the given serial number or qualifier. Overrides '+
  593. 'ANDROID_SERIAL environment variable. Use "adb devices" '+
  594. 'to list all connected devices with their respective '+
  595. 'serial number. '+
  596. 'Corresponds to the "-s" option of adb.')
  597. parser.add_argument('-H', '--host', metavar='HOST', type=str,
  598. help='Name of adb server host (default: localhost). '+
  599. 'Corresponds to the "-H" option of adb.')
  600. parser.add_argument('-P', '--port', metavar='PORT', type=str,
  601. help='Port of adb server (default: 5037). '+
  602. 'Corresponds to the "-P" option of adb.')
  603. parser.add_argument('-R', '--reverse', action='store_true',
  604. help='Reverse sync (pull, not push).')
  605. parser.add_argument('-2', '--two-way', action='store_true',
  606. help='Two-way sync (compare modification time; after '+
  607. 'the sync, both sides will have all files in the '+
  608. 'respective newest version. This relies on the clocks '+
  609. 'of your system and the device to match.')
  610. #parser.add_argument('-t', '--times', action='store_true',
  611. # help='Preserve modification times when copying.')
  612. parser.add_argument('-d', '--delete', action='store_true',
  613. help='Delete files from DST that are not present on '+
  614. 'SRC. Mutually exclusive with -2.')
  615. parser.add_argument('--exclude', metavar='PATTERN', action='append',
  616. help='Exclude files matching PATTERN')
  617. parser.add_argument('-f', '--force', action='store_true',
  618. help='Allow deleting files/directories when having to '+
  619. 'replace a file by a directory or vice versa. This is '+
  620. 'disabled by default to prevent large scale accidents.')
  621. parser.add_argument('-n', '--no-clobber', action='store_true',
  622. help='Do not ever overwrite any '+
  623. 'existing files. Mutually exclusive with -f.')
  624. parser.add_argument('--dry-run',action='store_true',
  625. help='Do not do anything - just show what would '+
  626. 'be done.')
  627. args = parser.parse_args()
  628. args_encoding = locale.getdefaultlocale()[1]
  629. localpatterns = [x.encode(args_encoding) for x in args.source]
  630. remotepath = args.destination.encode(args_encoding)
  631. adb = args.adb.encode(args_encoding).split(b' ')
  632. if args.device:
  633. adb += [b'-d']
  634. if args.emulator:
  635. adb += [b'-e']
  636. if args.serial != None:
  637. adb += [b'-s', args.serial.encode(args_encoding)]
  638. if args.host != None:
  639. adb += [b'-H', args.host.encode(args_encoding)]
  640. if args.port != None:
  641. adb += [b'-P', args.port.encode(args_encoding)]
  642. adb = AdbFileSystem(adb)
  643. # Expand wildcards.
  644. localpaths = []
  645. remotepaths = []
  646. if args.reverse:
  647. for pattern in localpatterns:
  648. for src in ExpandWildcards(adb, pattern):
  649. src, dst = FixPath(src, remotepath)
  650. localpaths.append(src)
  651. remotepaths.append(dst)
  652. else:
  653. for src in localpatterns:
  654. src, dst = FixPath(src, remotepath)
  655. localpaths.append(src)
  656. remotepaths.append(dst)
  657. preserve_times = False # args.times
  658. delete_missing = args.delete
  659. allow_replace = args.force
  660. allow_overwrite = not args.no_clobber
  661. dry_run = args.dry_run
  662. local_to_remote = True
  663. remote_to_local = False
  664. if args.two_way:
  665. local_to_remote = True
  666. remote_to_local = True
  667. if args.reverse:
  668. local_to_remote, remote_to_local = remote_to_local, local_to_remote
  669. localpaths, remotepaths = remotepaths, localpaths
  670. if allow_replace and not allow_overwrite:
  671. _print(b'--no-clobber and --force are mutually exclusive.')
  672. parser.print_help()
  673. return
  674. if delete_missing and local_to_remote and remote_to_local:
  675. _print(b'--delete and --two-way are mutually exclusive.')
  676. parser.print_help()
  677. return
  678. # Two-way sync is only allowed with disjoint remote and local path sets.
  679. if (remote_to_local and local_to_remote) or delete_missing:
  680. if ((remote_to_local and len(localpaths) != len(set(localpaths))) or
  681. (local_to_remote and len(remotepaths) != len(set(remotepaths)))):
  682. _print(b'--two-way and --delete are only supported for disjoint sets of '
  683. b'source and destination paths (in other words, all SRC must '
  684. b'differ in basename).')
  685. parser.print_help()
  686. return
  687. for i in range(len(localpaths)):
  688. _print(b'Sync: local %s, remote %s', localpaths[i], remotepaths[i])
  689. syncer = FileSyncer(adb, localpaths[i], remotepaths[i], args.exclude,
  690. local_to_remote, remote_to_local, preserve_times,
  691. delete_missing, allow_overwrite, allow_replace, dry_run)
  692. if not syncer.IsWorking():
  693. _print(b'Device not connected or not working.')
  694. return
  695. try:
  696. syncer.ScanAndDiff()
  697. syncer.PerformDeletions()
  698. syncer.PerformOverwrites()
  699. syncer.PerformCopies()
  700. finally:
  701. syncer.TimeReport()
  702. if __name__ == '__main__':
  703. main(*sys.argv)