Had to restat repo.
This commit is contained in:
commit
19d8d41a21
20 changed files with 2469 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
mobisync-log.txt
|
||||||
|
Sizakele
|
||||||
BIN
adb
Executable file
BIN
adb
Executable file
Binary file not shown.
774
adb-sync
Executable file
774
adb-sync
Executable file
|
|
@ -0,0 +1,774 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Sync files from/to an Android device."""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import locale
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def _sprintf(s, *args):
|
||||||
|
# To be able to use string formatting, we first have to covert to
|
||||||
|
# unicode strings; however, we must do so in a way that preserves all
|
||||||
|
# bytes, and convert back at the end. An encoding that maps all byte
|
||||||
|
# values to different Unicode codepoints is cp437.
|
||||||
|
return (s.decode('cp437') % tuple([
|
||||||
|
(x.decode('cp437') if type(x) == bytes else x) for x in args
|
||||||
|
])).encode('cp437')
|
||||||
|
|
||||||
|
|
||||||
|
def _print(s, *args):
|
||||||
|
"""Writes a binary string to stdout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
s: The binary format string to write.
|
||||||
|
args: The args for the format string.
|
||||||
|
"""
|
||||||
|
if hasattr(sys.stdout, 'buffer'):
|
||||||
|
# Python 3.
|
||||||
|
sys.stdout.buffer.write(_sprintf(s, *args) + b'\n')
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
else:
|
||||||
|
# Python 2.
|
||||||
|
sys.stdout.write(_sprintf(s, *args) + b'\n')
|
||||||
|
|
||||||
|
|
||||||
|
class AdbFileSystem(object):
|
||||||
|
"""Mimics os's file interface but uses the adb utility."""
|
||||||
|
|
||||||
|
def __init__(self, adb):
|
||||||
|
self.stat_cache = {}
|
||||||
|
self.adb = adb
|
||||||
|
|
||||||
|
# Regarding parsing stat results, we only care for the following fields:
|
||||||
|
# - st_size
|
||||||
|
# - st_mtime
|
||||||
|
# - st_mode (but only about S_ISDIR and S_ISREG properties)
|
||||||
|
# Therefore, we only capture parts of 'ls -l' output that we actually use.
|
||||||
|
# The other fields will be filled with dummy values.
|
||||||
|
LS_TO_STAT_RE = re.compile(br'''^
|
||||||
|
(?:
|
||||||
|
(?P<S_IFREG> -) |
|
||||||
|
(?P<S_IFBLK> b) |
|
||||||
|
(?P<S_IFCHR> c) |
|
||||||
|
(?P<S_IFDIR> d) |
|
||||||
|
(?P<S_IFLNK> l) |
|
||||||
|
(?P<S_IFIFO> p) |
|
||||||
|
(?P<S_IFSOCK> s))
|
||||||
|
[-r][-w][-xsS]
|
||||||
|
[-r][-w][-xsS]
|
||||||
|
[-r][-w][-xtT] # Mode string.
|
||||||
|
[ ]+
|
||||||
|
(?:
|
||||||
|
[0-9]+ # number of hard links
|
||||||
|
[ ]+
|
||||||
|
)?
|
||||||
|
[^ ]+ # User name/ID.
|
||||||
|
[ ]+
|
||||||
|
[^ ]+ # Group name/ID.
|
||||||
|
[ ]+
|
||||||
|
(?(S_IFBLK) [^ ]+[ ]+[^ ]+[ ]+) # Device numbers.
|
||||||
|
(?(S_IFCHR) [^ ]+[ ]+[^ ]+[ ]+) # Device numbers.
|
||||||
|
(?(S_IFDIR) [0-9]+ [ ]+)? # directory Size.
|
||||||
|
(?(S_IFREG)
|
||||||
|
(?P<st_size> [0-9]+) # Size.
|
||||||
|
[ ]+)
|
||||||
|
(?P<st_mtime>
|
||||||
|
[0-9]{4}-[0-9]{2}-[0-9]{2} # Date.
|
||||||
|
[ ]
|
||||||
|
[0-9]{2}:[0-9]{2}) # Time.
|
||||||
|
[ ]
|
||||||
|
# Don't capture filename for symlinks (ambiguous).
|
||||||
|
(?(S_IFLNK) .* | (?P<filename> .*))
|
||||||
|
$''', re.DOTALL | re.VERBOSE)
|
||||||
|
def LsToStat(self, line):
|
||||||
|
"""Convert a line from 'ls -l' output to a stat result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line: Output line of 'ls -l' on Android.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
os.stat_result for the line.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OSError: if the given string is not a 'ls -l' output line (but maybe an
|
||||||
|
error message instead).
|
||||||
|
"""
|
||||||
|
|
||||||
|
match = self.LS_TO_STAT_RE.match(line)
|
||||||
|
if match is None:
|
||||||
|
_print(b'Warning: could not parse %r.', line)
|
||||||
|
raise OSError('Unparseable ls -al result.')
|
||||||
|
groups = match.groupdict()
|
||||||
|
|
||||||
|
# Get the values we're interested in.
|
||||||
|
st_mode = ( # 0755
|
||||||
|
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||||
|
if groups['S_IFREG']: st_mode |= stat.S_IFREG
|
||||||
|
if groups['S_IFBLK']: st_mode |= stat.S_IFBLK
|
||||||
|
if groups['S_IFCHR']: st_mode |= stat.S_IFCHR
|
||||||
|
if groups['S_IFDIR']: st_mode |= stat.S_IFDIR
|
||||||
|
if groups['S_IFIFO']: st_mode |= stat.S_IFIFO
|
||||||
|
if groups['S_IFLNK']: st_mode |= stat.S_IFLNK
|
||||||
|
if groups['S_IFSOCK']: st_mode |= stat.S_IFSOCK
|
||||||
|
st_size = groups['st_size']
|
||||||
|
if st_size is not None:
|
||||||
|
st_size = int(st_size)
|
||||||
|
st_mtime = time.mktime(time.strptime(match.group('st_mtime').decode('utf-8'),
|
||||||
|
'%Y-%m-%d %H:%M'))
|
||||||
|
|
||||||
|
# Fill the rest with dummy values.
|
||||||
|
st_ino = 1
|
||||||
|
st_rdev = 0
|
||||||
|
st_nlink = 1
|
||||||
|
st_uid = -2 # Nobody.
|
||||||
|
st_gid = -2 # Nobody.
|
||||||
|
st_atime = st_ctime = st_mtime
|
||||||
|
|
||||||
|
stbuf = os.stat_result((st_mode, st_ino, st_rdev, st_nlink, st_uid, st_gid,
|
||||||
|
st_size, st_atime, st_mtime, st_ctime))
|
||||||
|
filename = groups['filename']
|
||||||
|
return stbuf, filename
|
||||||
|
|
||||||
|
def Stdout(self, *popen_args):
|
||||||
|
"""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.
|
||||||
|
# Usually, arguments in 'adb shell' use are put in double quotes by adb,
|
||||||
|
# but not in any way escaped.
|
||||||
|
arg = arg.replace(b'\\', b'\\\\')
|
||||||
|
arg = arg.replace(b'"', b'\\"')
|
||||||
|
arg = arg.replace(b'$', b'\\$')
|
||||||
|
arg = arg.replace(b'`', b'\\`')
|
||||||
|
arg = b'"' + arg + b'"'
|
||||||
|
return arg
|
||||||
|
|
||||||
|
def IsWorking(self):
|
||||||
|
"""Tests the adb connection."""
|
||||||
|
# This string should contain all possible evil, but no percent signs.
|
||||||
|
# Note this code uses 'date' and not 'echo', as date just calls strftime
|
||||||
|
# while echo does its own backslash escape handling additionally to the
|
||||||
|
# shell's. Too bad printf "%s\n" is not available.
|
||||||
|
test_strings = [
|
||||||
|
b'(',
|
||||||
|
b'(; #`ls`$PATH\'"(\\\\\\\\){};!\xc0\xaf\xff\xc2\xbf'
|
||||||
|
]
|
||||||
|
for test_string in test_strings:
|
||||||
|
good = False
|
||||||
|
with self.Stdout(self.adb + [b'shell', _sprintf(b'date +%s',
|
||||||
|
self.QuoteArgument(test_string))]) as stdout:
|
||||||
|
for line in stdout:
|
||||||
|
line = line.rstrip(b'\r\n')
|
||||||
|
if line == test_string:
|
||||||
|
good = True
|
||||||
|
if not good:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def listdir(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""List the contents of a directory, caching them for later lstat calls."""
|
||||||
|
with self.Stdout(self.adb + [b'shell', _sprintf(b'ls -al %s',
|
||||||
|
self.QuoteArgument(path + b'/'))]) as stdout:
|
||||||
|
for line in stdout:
|
||||||
|
if line.startswith(b'total '):
|
||||||
|
continue
|
||||||
|
line = line.rstrip(b'\r\n')
|
||||||
|
try:
|
||||||
|
statdata, filename = self.LsToStat(line)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if filename is None:
|
||||||
|
_print(b'Warning: could not parse %s', line)
|
||||||
|
else:
|
||||||
|
self.stat_cache[path + b'/' + filename] = statdata
|
||||||
|
yield filename
|
||||||
|
|
||||||
|
def lstat(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""Stat a file."""
|
||||||
|
if path in self.stat_cache:
|
||||||
|
return self.stat_cache[path]
|
||||||
|
with self.Stdout(self.adb + [b'shell', _sprintf(b'ls -ald %s',
|
||||||
|
self.QuoteArgument(path))]) as stdout:
|
||||||
|
for line in stdout:
|
||||||
|
if line.startswith(b'total '):
|
||||||
|
continue
|
||||||
|
line = line.rstrip(b'\r\n')
|
||||||
|
statdata, filename = self.LsToStat(line)
|
||||||
|
self.stat_cache[path] = statdata
|
||||||
|
return statdata
|
||||||
|
raise OSError('No such file or directory')
|
||||||
|
|
||||||
|
def unlink(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""Delete a file."""
|
||||||
|
if subprocess.call(self.adb + [b'shell', _sprintf(b'rm %s',
|
||||||
|
self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('unlink failed')
|
||||||
|
|
||||||
|
def rmdir(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""Delete a directory."""
|
||||||
|
if subprocess.call(self.adb + [b'shell', _sprintf(b'rmdir %s',
|
||||||
|
self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('rmdir failed')
|
||||||
|
|
||||||
|
def makedirs(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""Create a directory."""
|
||||||
|
if subprocess.call(self.adb + [b'shell', _sprintf(b'mkdir -p %s',
|
||||||
|
self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('mkdir failed')
|
||||||
|
|
||||||
|
def utime(self, path, times):
|
||||||
|
# TODO(rpolzer): Find out why this does not work (returns status 255).
|
||||||
|
"""Set the time of a file to a specified unix time."""
|
||||||
|
atime, mtime = times
|
||||||
|
timestr = time.strftime(b'%Y%m%d.%H%M%S', time.localtime(mtime))
|
||||||
|
if subprocess.call(self.adb + [b'shell', _sprintf(b'touch -mt %s %s',
|
||||||
|
timestr, self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('touch failed')
|
||||||
|
timestr = time.strftime(b'%Y%m%d.%H%M%S', time.localtime(atime))
|
||||||
|
if subprocess.call(self.adb + [b'shell',_sprintf( b'touch -at %s %s',
|
||||||
|
timestr, self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('touch failed')
|
||||||
|
|
||||||
|
def glob(self, path):
|
||||||
|
with self.Stdout(self.adb + [b'shell',
|
||||||
|
_sprintf(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):
|
||||||
|
"""Push a file from the local file system to the Android device."""
|
||||||
|
if subprocess.call(self.adb + [b'push', src, dst]) != 0:
|
||||||
|
raise OSError('push failed')
|
||||||
|
|
||||||
|
def Pull(self, src, dst):
|
||||||
|
"""Pull a file from the Android device to the local file system."""
|
||||||
|
if subprocess.call(self.adb + [b'pull', src, dst]) != 0:
|
||||||
|
raise OSError('pull failed')
|
||||||
|
|
||||||
|
|
||||||
|
def BuildFileList(fs, path, prefix=b''):
|
||||||
|
"""Builds a file list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fs: File system provider (can be os or AdbFileSystem()).
|
||||||
|
path: Initial path.
|
||||||
|
prefix: Path prefix for output file names.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
File names from path (prefixed by prefix).
|
||||||
|
Directories are yielded before their contents.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statresult = fs.lstat(path)
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
if stat.S_ISDIR(statresult.st_mode):
|
||||||
|
yield prefix, statresult
|
||||||
|
try:
|
||||||
|
files = list(fs.listdir(path))
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
files.sort()
|
||||||
|
for n in files:
|
||||||
|
if n == b'.' or n == b'..':
|
||||||
|
continue
|
||||||
|
for t in BuildFileList(fs, path + b'/' + n, prefix + b'/' + n):
|
||||||
|
yield t
|
||||||
|
elif stat.S_ISREG(statresult.st_mode) or stat.S_ISLNK(statresult.st_mode):
|
||||||
|
yield prefix, statresult
|
||||||
|
else:
|
||||||
|
_print(b'Note: unsupported file: %s', path)
|
||||||
|
|
||||||
|
|
||||||
|
def DiffLists(a, b):
|
||||||
|
"""Compares two lists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a: the first list.
|
||||||
|
b: the second list.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a_only: the items from list a.
|
||||||
|
both: the items from both list, with the remaining tuple items combined.
|
||||||
|
b_only: the items from list b.
|
||||||
|
"""
|
||||||
|
a_only = []
|
||||||
|
b_only = []
|
||||||
|
both = []
|
||||||
|
|
||||||
|
a_iter = iter(a)
|
||||||
|
b_iter = iter(b)
|
||||||
|
a_active = True
|
||||||
|
b_active = True
|
||||||
|
a_available = False
|
||||||
|
b_available = False
|
||||||
|
a_item = None
|
||||||
|
b_item = None
|
||||||
|
|
||||||
|
while a_active and b_active:
|
||||||
|
if not a_available:
|
||||||
|
try:
|
||||||
|
a_item = next(a_iter)
|
||||||
|
a_available = True
|
||||||
|
except StopIteration:
|
||||||
|
a_active = False
|
||||||
|
break
|
||||||
|
if not b_available:
|
||||||
|
try:
|
||||||
|
b_item = next(b_iter)
|
||||||
|
b_available = True
|
||||||
|
except StopIteration:
|
||||||
|
b_active = False
|
||||||
|
break
|
||||||
|
if a_item[0] == b_item[0]:
|
||||||
|
both.append(tuple([a_item[0]] + list(a_item[1:]) + list(b_item[1:])))
|
||||||
|
a_available = False
|
||||||
|
b_available = False
|
||||||
|
elif a_item[0] < b_item[0]:
|
||||||
|
a_only.append(a_item)
|
||||||
|
a_available = False
|
||||||
|
elif a_item[0] > b_item[0]:
|
||||||
|
b_only.append(b_item)
|
||||||
|
b_available = False
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if a_active:
|
||||||
|
if a_available:
|
||||||
|
a_only.append(a_item)
|
||||||
|
for item in a_iter:
|
||||||
|
a_only.append(item)
|
||||||
|
if b_active:
|
||||||
|
if b_available:
|
||||||
|
b_only.append(b_item)
|
||||||
|
for item in b_iter:
|
||||||
|
b_only.append(item)
|
||||||
|
|
||||||
|
return a_only, both, b_only
|
||||||
|
|
||||||
|
|
||||||
|
class FileSyncer(object):
|
||||||
|
"""File synchronizer."""
|
||||||
|
|
||||||
|
def __init__(self, adb, local_path, remote_path, local_to_remote,
|
||||||
|
remote_to_local, preserve_times, delete_missing, allow_overwrite,
|
||||||
|
allow_replace, dry_run):
|
||||||
|
self.local = local_path
|
||||||
|
self.remote = remote_path
|
||||||
|
self.adb = adb
|
||||||
|
self.local_to_remote = local_to_remote
|
||||||
|
self.remote_to_local = remote_to_local
|
||||||
|
self.preserve_times = preserve_times
|
||||||
|
self.delete_missing = delete_missing
|
||||||
|
self.allow_overwrite = allow_overwrite
|
||||||
|
self.allow_replace = allow_replace
|
||||||
|
self.dry_run = dry_run
|
||||||
|
self.local_only = None
|
||||||
|
self.both = None
|
||||||
|
self.remote_only = None
|
||||||
|
self.num_bytes = 0
|
||||||
|
self.start_time = time.time()
|
||||||
|
|
||||||
|
def IsWorking(self):
|
||||||
|
"""Tests the adb connection."""
|
||||||
|
return self.adb.IsWorking()
|
||||||
|
|
||||||
|
def ScanAndDiff(self):
|
||||||
|
"""Scans the local and remote locations and identifies differences."""
|
||||||
|
_print(b'Scanning and diffing...')
|
||||||
|
locallist = BuildFileList(os, self.local)
|
||||||
|
remotelist = BuildFileList(self.adb, self.remote)
|
||||||
|
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:
|
||||||
|
_print(b'No files seen. User error?')
|
||||||
|
self.src_to_dst = (self.local_to_remote, self.remote_to_local)
|
||||||
|
self.dst_to_src = (self.remote_to_local, self.local_to_remote)
|
||||||
|
self.src_only = (self.local_only, self.remote_only)
|
||||||
|
self.dst_only = (self.remote_only, self.local_only)
|
||||||
|
self.src = (self.local, self.remote)
|
||||||
|
self.dst = (self.remote, self.local)
|
||||||
|
self.dst_fs = (self.adb, os)
|
||||||
|
self.push = (b'Push', b'Pull')
|
||||||
|
self.copy = (self.adb.Push, self.adb.Pull)
|
||||||
|
|
||||||
|
def InterruptProtection(self, fs, name):
|
||||||
|
"""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:
|
||||||
|
_print(b'Interrupted-%s-Delete: %s',
|
||||||
|
b'Pull' if fs == os else b'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."""
|
||||||
|
if not self.delete_missing:
|
||||||
|
return
|
||||||
|
for i in [0, 1]:
|
||||||
|
if self.src_to_dst[i] and not self.dst_to_src[i]:
|
||||||
|
if not self.src_only[i] and not self.both:
|
||||||
|
_print(b'Cowardly refusing to delete everything.')
|
||||||
|
else:
|
||||||
|
for name, s in reversed(self.dst_only[i]):
|
||||||
|
dst_name = self.dst[i] + name
|
||||||
|
_print(b'%s-Delete: %s', self.push[i], dst_name)
|
||||||
|
if stat.S_ISDIR(s.st_mode):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].rmdir(dst_name)
|
||||||
|
else:
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].unlink(dst_name)
|
||||||
|
del self.dst_only[i][:]
|
||||||
|
|
||||||
|
def PerformOverwrites(self):
|
||||||
|
"""Delete files/directories that are in the way for overwriting."""
|
||||||
|
src_only_prepend = ([], [])
|
||||||
|
for name, localstat, remotestat in self.both:
|
||||||
|
if stat.S_ISDIR(localstat.st_mode) and stat.S_ISDIR(remotestat.st_mode):
|
||||||
|
# A dir is a dir is a dir.
|
||||||
|
continue
|
||||||
|
elif stat.S_ISDIR(localstat.st_mode) or stat.S_ISDIR(remotestat.st_mode):
|
||||||
|
# Dir vs file? Nothing to do here yet.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# File vs file? Compare sizes.
|
||||||
|
if localstat.st_size == remotestat.st_size:
|
||||||
|
continue
|
||||||
|
l2r = self.local_to_remote
|
||||||
|
r2l = self.remote_to_local
|
||||||
|
if l2r and r2l:
|
||||||
|
# Truncate times to full minutes, as Android's "ls" only outputs minute
|
||||||
|
# accuracy.
|
||||||
|
localminute = int(localstat.st_mtime / 60)
|
||||||
|
remoteminute = int(remotestat.st_mtime / 60)
|
||||||
|
if localminute > remoteminute:
|
||||||
|
r2l = False
|
||||||
|
elif localminute < remoteminute:
|
||||||
|
l2r = False
|
||||||
|
if l2r and r2l:
|
||||||
|
_print(b'Unresolvable: %s', name)
|
||||||
|
continue
|
||||||
|
if l2r:
|
||||||
|
i = 0 # Local to remote operation.
|
||||||
|
src_stat = localstat
|
||||||
|
dst_stat = remotestat
|
||||||
|
else:
|
||||||
|
i = 1 # Remote to local operation.
|
||||||
|
src_stat = remotestat
|
||||||
|
dst_stat = localstat
|
||||||
|
dst_name = self.dst[i] + name
|
||||||
|
_print(b'%s-Delete-Conflicting: %s', self.push[i], dst_name)
|
||||||
|
if stat.S_ISDIR(localstat.st_mode) or stat.S_ISDIR(remotestat.st_mode):
|
||||||
|
if not self.allow_replace:
|
||||||
|
_print(b'Would have to replace to do this. '
|
||||||
|
b'Use --force to allow this.')
|
||||||
|
continue
|
||||||
|
if not self.allow_overwrite:
|
||||||
|
_print(b'Would have to overwrite to do this, '
|
||||||
|
b'which --no-clobber forbids.')
|
||||||
|
continue
|
||||||
|
if stat.S_ISDIR(dst_stat.st_mode):
|
||||||
|
kill_files = [x for x in self.dst_only[i]
|
||||||
|
if x[0][:len(name) + 1] == name + b'/']
|
||||||
|
self.dst_only[i][:] = [x for x in self.dst_only[i]
|
||||||
|
if x[0][:len(name) + 1] != name + b'/']
|
||||||
|
for l, s in reversed(kill_files):
|
||||||
|
if stat.S_ISDIR(s.st_mode):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].rmdir(self.dst[i] + l)
|
||||||
|
else:
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].unlink(self.dst[i] + l)
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].rmdir(dst_name)
|
||||||
|
elif stat.S_ISDIR(src_stat.st_mode):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].unlink(dst_name)
|
||||||
|
else:
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].unlink(dst_name)
|
||||||
|
src_only_prepend[i].append((name, src_stat))
|
||||||
|
for i in [0, 1]:
|
||||||
|
self.src_only[i][:0] = src_only_prepend[i]
|
||||||
|
|
||||||
|
def PerformCopies(self):
|
||||||
|
"""Perform all copying necessary for the file sync operation."""
|
||||||
|
for i in [0, 1]:
|
||||||
|
if self.src_to_dst[i]:
|
||||||
|
for name, s in self.src_only[i]:
|
||||||
|
src_name = self.src[i] + name
|
||||||
|
dst_name = self.dst[i] + name
|
||||||
|
_print(b'%s: %s', self.push[i], dst_name)
|
||||||
|
if stat.S_ISDIR(s.st_mode):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].makedirs(dst_name)
|
||||||
|
else:
|
||||||
|
with self.InterruptProtection(self.dst_fs[i], dst_name):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.copy[i](src_name, dst_name)
|
||||||
|
self.num_bytes += s.st_size
|
||||||
|
if not self.dry_run:
|
||||||
|
if self.preserve_times:
|
||||||
|
_print(b'%s-Times: accessed %s, modified %s',
|
||||||
|
self.push[i],
|
||||||
|
time.asctime(time.localtime(s.st_atime)).encode('utf-8'),
|
||||||
|
time.asctime(time.localtime(s.st_mtime)).encode('utf-8'))
|
||||||
|
self.dst_fs[i].utime(dst_name, (s.st_atime, s.st_mtime))
|
||||||
|
|
||||||
|
def TimeReport(self):
|
||||||
|
"""Report time and amount of data transferred."""
|
||||||
|
if self.dry_run:
|
||||||
|
_print(b'Total: %d bytes', self.num_bytes)
|
||||||
|
else:
|
||||||
|
end_time = time.time()
|
||||||
|
dt = end_time - self.start_time
|
||||||
|
rate = self.num_bytes / 1024.0 / dt
|
||||||
|
_print(b'Total: %d KB/s (%d bytes in %.3fs)', rate, self.num_bytes, dt)
|
||||||
|
|
||||||
|
|
||||||
|
def ExpandWildcards(globber, path):
|
||||||
|
if path.find(b'?') == -1 and path.find(b'*') == -1 and path.find(b'[') == -1:
|
||||||
|
return [path]
|
||||||
|
return globber.glob(path)
|
||||||
|
|
||||||
|
|
||||||
|
def FixPath(src, dst):
|
||||||
|
# rsync-like path munging to make remote specifications shorter.
|
||||||
|
append = b''
|
||||||
|
pos = src.rfind(b'/')
|
||||||
|
if pos >= 0:
|
||||||
|
if src.endswith(b'/'):
|
||||||
|
# Final slash: copy to the destination "as is".
|
||||||
|
src = src[:-1]
|
||||||
|
else:
|
||||||
|
# No final slash: destination name == source name.
|
||||||
|
append = src[pos:]
|
||||||
|
else:
|
||||||
|
# No slash at all - use same name at destination.
|
||||||
|
append = b'/' + src
|
||||||
|
# Append the destination file name if any.
|
||||||
|
# BUT: do not append "." or ".." components!
|
||||||
|
if append != b'/.' and append != b'/..':
|
||||||
|
dst += append
|
||||||
|
return (src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def main(*args):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Synchronize a directory between an Android device and the '+
|
||||||
|
'local file system')
|
||||||
|
parser.add_argument('source', metavar='SRC', type=str, nargs='+',
|
||||||
|
help='The directory to read files/directories from. '+
|
||||||
|
'This must be a local path if -R is not specified, '+
|
||||||
|
'and an Android path if -R is specified. If SRC does '+
|
||||||
|
'not end with a final slash, its last path component '+
|
||||||
|
'is appended to DST (like rsync does).')
|
||||||
|
parser.add_argument('destination', metavar='DST', type=str,
|
||||||
|
help='The directory to write files/directories to. '+
|
||||||
|
'This must be an Android path if -R is not specified, '+
|
||||||
|
'and a local path if -R is specified.')
|
||||||
|
parser.add_argument('-e', '--adb', metavar='COMMAND', default='adb', type=str,
|
||||||
|
help='Use the given adb binary and arguments.')
|
||||||
|
parser.add_argument('--device', action='store_true',
|
||||||
|
help='Directs command to the only connected USB device; '+
|
||||||
|
'returns an error if more than one USB device is '+
|
||||||
|
'present. '+
|
||||||
|
'Corresponds to the "-d" option of adb.')
|
||||||
|
parser.add_argument('--emulator', action='store_true',
|
||||||
|
help='Directs command to the only running emulator; '+
|
||||||
|
'returns an error if more than one emulator is running. '+
|
||||||
|
'Corresponds to the "-e" option of adb.')
|
||||||
|
parser.add_argument('-s', '--serial', metavar='DEVICE', type=str,
|
||||||
|
help='Directs command to the device or emulator with '+
|
||||||
|
'the given serial number or qualifier. Overrides '+
|
||||||
|
'ANDROID_SERIAL environment variable. Use "adb devices" '+
|
||||||
|
'to list all connected devices with their respective '+
|
||||||
|
'serial number. '+
|
||||||
|
'Corresponds to the "-s" option of adb.')
|
||||||
|
parser.add_argument('-H', '--host', metavar='HOST', type=str,
|
||||||
|
help='Name of adb server host (default: localhost). '+
|
||||||
|
'Corresponds to the "-H" option of adb.')
|
||||||
|
parser.add_argument('-P', '--port', metavar='PORT', type=str,
|
||||||
|
help='Port of adb server (default: 5037). '+
|
||||||
|
'Corresponds to the "-P" option of adb.')
|
||||||
|
parser.add_argument('-R', '--reverse', action='store_true',
|
||||||
|
help='Reverse sync (pull, not push).')
|
||||||
|
parser.add_argument('-2', '--two-way', action='store_true',
|
||||||
|
help='Two-way sync (compare modification time; after '+
|
||||||
|
'the sync, both sides will have all files in the '+
|
||||||
|
'respective newest version. This relies on the clocks '+
|
||||||
|
'of your system and the device to match.')
|
||||||
|
#parser.add_argument('-t', '--times', action='store_true',
|
||||||
|
# help='Preserve modification times when copying.')
|
||||||
|
parser.add_argument('-d', '--delete', action='store_true',
|
||||||
|
help='Delete files from DST that are not present on '+
|
||||||
|
'SRC. Mutually exclusive with -2.')
|
||||||
|
parser.add_argument('-f', '--force', action='store_true',
|
||||||
|
help='Allow deleting files/directories when having to '+
|
||||||
|
'replace a file by a directory or vice versa. This is '+
|
||||||
|
'disabled by default to prevent large scale accidents.')
|
||||||
|
parser.add_argument('-n', '--no-clobber', action='store_true',
|
||||||
|
help='Do not ever overwrite any '+
|
||||||
|
'existing files. Mutually exclusive with -f.')
|
||||||
|
parser.add_argument('--dry-run',action='store_true',
|
||||||
|
help='Do not do anything - just show what would '+
|
||||||
|
'be done.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
args_encoding = locale.getdefaultlocale()[1]
|
||||||
|
|
||||||
|
localpatterns = [x.encode(args_encoding) for x in args.source]
|
||||||
|
remotepath = args.destination.encode(args_encoding)
|
||||||
|
adb = args.adb.encode(args_encoding).split(b' ')
|
||||||
|
if args.device:
|
||||||
|
adb += [b'-d']
|
||||||
|
if args.emulator:
|
||||||
|
adb += [b'-e']
|
||||||
|
if args.serial != None:
|
||||||
|
adb += [b'-s', args.serial.encode(args_encoding)]
|
||||||
|
if args.host != None:
|
||||||
|
adb += [b'-H', args.host.encode(args_encoding)]
|
||||||
|
if args.port != None:
|
||||||
|
adb += [b'-P', args.port.encode(args_encoding)]
|
||||||
|
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
|
||||||
|
delete_missing = args.delete
|
||||||
|
allow_replace = args.force
|
||||||
|
allow_overwrite = not args.no_clobber
|
||||||
|
dry_run = args.dry_run
|
||||||
|
local_to_remote = True
|
||||||
|
remote_to_local = False
|
||||||
|
if args.two_way:
|
||||||
|
local_to_remote = True
|
||||||
|
remote_to_local = True
|
||||||
|
if args.reverse:
|
||||||
|
local_to_remote, remote_to_local = remote_to_local, local_to_remote
|
||||||
|
localpaths, remotepaths = remotepaths, localpaths
|
||||||
|
if allow_replace and not allow_overwrite:
|
||||||
|
_print(b'--no-clobber and --force are mutually exclusive.')
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
if delete_missing and local_to_remote and remote_to_local:
|
||||||
|
_print(b'--delete and --two-way are mutually exclusive.')
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Two-way sync is only allowed with disjoint remote and local path sets.
|
||||||
|
if (remote_to_local and local_to_remote) or delete_missing:
|
||||||
|
if ((remote_to_local and len(localpaths) != len(set(localpaths))) or
|
||||||
|
(local_to_remote and len(remotepaths) != len(set(remotepaths)))):
|
||||||
|
_print(b'--two-way and --delete are only supported for disjoint sets of '
|
||||||
|
b'source and destination paths (in other words, all SRC must '
|
||||||
|
b'differ in basename).')
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(len(localpaths)):
|
||||||
|
_print(b'Sync: local %s, remote %s', 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)
|
||||||
|
if not syncer.IsWorking():
|
||||||
|
_print(b'Device not connected or not working.')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
syncer.ScanAndDiff()
|
||||||
|
syncer.PerformDeletions()
|
||||||
|
syncer.PerformOverwrites()
|
||||||
|
syncer.PerformCopies()
|
||||||
|
finally:
|
||||||
|
syncer.TimeReport()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(*sys.argv)
|
||||||
BIN
mobisync.app/Contents/Info.plist
Normal file
BIN
mobisync.app/Contents/Info.plist
Normal file
Binary file not shown.
BIN
mobisync.app/Contents/MacOS/mobisync
Executable file
BIN
mobisync.app/Contents/MacOS/mobisync
Executable file
Binary file not shown.
BIN
mobisync.app/Contents/Resources/AppIcon.icns
Normal file
BIN
mobisync.app/Contents/Resources/AppIcon.icns
Normal file
Binary file not shown.
BIN
mobisync.app/Contents/Resources/AppSettings.plist
Normal file
BIN
mobisync.app/Contents/Resources/AppSettings.plist
Normal file
Binary file not shown.
705
mobisync.app/Contents/Resources/MainMenu.nib/designable.nib
generated
Normal file
705
mobisync.app/Contents/Resources/MainMenu.nib/designable.nib
generated
Normal file
|
|
@ -0,0 +1,705 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
|
||||||
|
<plugIn identifier="com.apple.WebKitIBPlugin" version="14460.31"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="210"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
|
<customObject id="-3" userLabel="Application" customClass="NSObject">
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="446"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="207" userLabel="ScriptExecController" customClass="ScriptExecController">
|
||||||
|
<connections>
|
||||||
|
<outlet property="aboutMenuItem" destination="232" id="238"/>
|
||||||
|
<outlet property="dropletBox" destination="494" id="502"/>
|
||||||
|
<outlet property="dropletDropFilesLabel" destination="499" id="507"/>
|
||||||
|
<outlet property="dropletMessageTextField" destination="505" id="508"/>
|
||||||
|
<outlet property="dropletProgressIndicator" destination="496" id="504"/>
|
||||||
|
<outlet property="dropletShaderView" destination="519" id="w2q-dA-smc"/>
|
||||||
|
<outlet property="dropletWindow" destination="490" id="512"/>
|
||||||
|
<outlet property="hideMenuItem" destination="134" id="239"/>
|
||||||
|
<outlet property="openRecentMenuItem" destination="FvD-xH-BRz" id="V8D-h2-Uw1"/>
|
||||||
|
<outlet property="progressBarCancelButton" destination="206" id="299"/>
|
||||||
|
<outlet property="progressBarDetailsLabel" destination="290" id="300"/>
|
||||||
|
<outlet property="progressBarDetailsTriangle" destination="288" id="301"/>
|
||||||
|
<outlet property="progressBarIndicator" destination="204" id="298"/>
|
||||||
|
<outlet property="progressBarMessageTextField" destination="205" id="297"/>
|
||||||
|
<outlet property="progressBarTextView" destination="307" id="516"/>
|
||||||
|
<outlet property="progressBarWindow" destination="21" id="296"/>
|
||||||
|
<outlet property="quitMenuItem" destination="136" id="240"/>
|
||||||
|
<outlet property="textWindow" destination="216" id="dD5-aT-ujC"/>
|
||||||
|
<outlet property="textWindowCancelButton" destination="220" id="uuU-Xb-xDZ"/>
|
||||||
|
<outlet property="textWindowMessageTextField" destination="366" id="bT3-oV-9zc"/>
|
||||||
|
<outlet property="textWindowProgressIndicator" destination="230" id="AXd-F1-wQr"/>
|
||||||
|
<outlet property="textWindowTextView" destination="225" id="FZy-wJ-th1"/>
|
||||||
|
<outlet property="webView" destination="258" id="GEt-Hd-OFL"/>
|
||||||
|
<outlet property="webViewCancelButton" destination="251" id="i5n-LY-nJs"/>
|
||||||
|
<outlet property="webViewMessageTextField" destination="509" id="Unv-5h-E6P"/>
|
||||||
|
<outlet property="webViewProgressIndicator" destination="277" id="Hun-a2-4wC"/>
|
||||||
|
<outlet property="webViewWindow" destination="247" id="9lw-Pg-xRw"/>
|
||||||
|
<outlet property="windowMenu" destination="24" id="328"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="ProgressBarWindow" animationBehavior="default" id="21" userLabel="ProgressWindow">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<rect key="contentRect" x="472" y="537" width="438" height="83"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
|
||||||
|
<value key="minSize" type="size" width="438" height="83"/>
|
||||||
|
<value key="maxSize" type="size" width="600" height="83"/>
|
||||||
|
<view key="contentView" id="2">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="438" height="83"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<progressIndicator wantsLayer="YES" verticalHuggingPriority="750" maxValue="100" bezeled="NO" indeterminate="YES" style="bar" id="204">
|
||||||
|
<rect key="frame" x="18" y="27" width="310" height="20"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||||
|
</progressIndicator>
|
||||||
|
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" id="205">
|
||||||
|
<rect key="frame" x="18" y="55" width="403" height="17"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="clipping" sendsActionOnEndEditing="YES" baseWritingDirection="leftToRight" alignment="left" id="472">
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<button verticalHuggingPriority="750" id="206">
|
||||||
|
<rect key="frame" x="338" y="20" width="86" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="push" title="Quit" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="473">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
|
Gw
|
||||||
|
</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="cancel:" target="207" id="215"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<button toolTip="Show details" horizontalHuggingPriority="750" verticalHuggingPriority="750" id="288">
|
||||||
|
<rect key="frame" x="20" y="10" width="13" height="13"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="disclosureTriangle" bezelStyle="disclosure" imagePosition="above" alignment="left" controlSize="small" borderStyle="border" inset="2" id="474">
|
||||||
|
<behavior key="behavior" pushIn="YES" changeBackground="YES" changeGray="YES" lightByContents="YES"/>
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleDetails:" target="207" id="303"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" id="290">
|
||||||
|
<rect key="frame" x="33" y="7" width="41" height="17"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Details" id="475">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<scrollView autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" id="304">
|
||||||
|
<rect key="frame" x="20" y="-203" width="393" height="202"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||||
|
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="2mm-QF-kpX">
|
||||||
|
<rect key="frame" x="1" y="1" width="391" height="200"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<textView editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" findStyle="panel" allowsDocumentBackgroundColorChange="YES" linkDetection="YES" id="307">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="391" height="200"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<size key="minSize" width="391" height="200"/>
|
||||||
|
<size key="maxSize" width="463" height="10000000"/>
|
||||||
|
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="489"/>
|
||||||
|
</connections>
|
||||||
|
</textView>
|
||||||
|
</subviews>
|
||||||
|
</clipView>
|
||||||
|
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="481">
|
||||||
|
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</scroller>
|
||||||
|
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="0.39849624060150374" horizontal="NO" id="480">
|
||||||
|
<rect key="frame" x="323" y="1" width="15" height="200"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</scroller>
|
||||||
|
</scrollView>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="377"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="87" y="313"/>
|
||||||
|
</window>
|
||||||
|
<menu title="MainMenu" systemMenu="main" id="29" userLabel="MainMenu">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Application" id="56">
|
||||||
|
<menu key="submenu" title="Application" systemMenu="apple" id="57">
|
||||||
|
<items>
|
||||||
|
<menuItem title="About" id="232">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontStandardAboutPanel:" target="-2" id="237"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="233">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Services" id="235">
|
||||||
|
<menu key="submenu" title="Services" systemMenu="services" id="234"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="236">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Hide" keyEquivalent="h" id="134">
|
||||||
|
<connections>
|
||||||
|
<action selector="hide:" target="-2" id="152"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Hide Others" keyEquivalent="h" id="145">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="hideOtherApplications:" target="-2" id="146"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Show All" id="150">
|
||||||
|
<connections>
|
||||||
|
<action selector="unhideAllApplications:" target="-2" id="153"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="149">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Quit" keyEquivalent="q" id="136">
|
||||||
|
<connections>
|
||||||
|
<action selector="terminate:" target="-2" id="139"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="File" id="83">
|
||||||
|
<menu key="submenu" title="File" id="81">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Open…" keyEquivalent="o" id="72">
|
||||||
|
<connections>
|
||||||
|
<action selector="openFiles:" target="207" id="524"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Open Recent" id="FvD-xH-BRz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="QHk-NS-To3">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Clear Menu" id="CWw-N0-5lz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="79">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Close" keyEquivalent="w" id="73">
|
||||||
|
<connections>
|
||||||
|
<action selector="performClose:" target="-1" id="193"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Save to File…" keyEquivalent="s" id="75">
|
||||||
|
<connections>
|
||||||
|
<action selector="saveToFile:" target="207" id="523"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="74">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Page Setup…" keyEquivalent="P" id="77">
|
||||||
|
<connections>
|
||||||
|
<action selector="runPageLayout:" target="-1" id="87"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Print…" keyEquivalent="p" id="78">
|
||||||
|
<connections>
|
||||||
|
<action selector="print:" target="-1" id="86"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Edit" id="OYz-zT-LAM">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Edit" id="gbe-PM-Daj">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Undo" keyEquivalent="z" id="6BN-ih-O2m">
|
||||||
|
<connections>
|
||||||
|
<action selector="undo:" target="-1" id="oH0-3R-wus"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Redo" keyEquivalent="z" id="TgQ-5S-p2w">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="redo:" target="-1" id="bxh-7G-6rR"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="ktb-NS-TU3"/>
|
||||||
|
<menuItem title="Cut" keyEquivalent="x" id="yLq-sd-YR5">
|
||||||
|
<connections>
|
||||||
|
<action selector="cut:" target="-1" id="46i-iL-VLK"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Copy" keyEquivalent="c" id="s3v-O1-6pA">
|
||||||
|
<connections>
|
||||||
|
<action selector="copy:" target="-1" id="jxG-ij-fh7"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste" keyEquivalent="v" id="6Ld-ut-13f">
|
||||||
|
<connections>
|
||||||
|
<action selector="paste:" target="-1" id="PGw-sA-Gma"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste and Match Style" keyEquivalent="V" id="GyB-HZ-4kH">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="pasteAsPlainText:" target="-1" id="AlS-iZ-R4l"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Delete" id="YkA-TP-1J9">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="delete:" target="-1" id="cE3-5v-CP0"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select All" keyEquivalent="a" id="z4j-fk-SUb">
|
||||||
|
<connections>
|
||||||
|
<action selector="selectAll:" target="-1" id="eM9-h9-WVo"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="FXg-ZR-vZj"/>
|
||||||
|
<menuItem title="Find" id="amO-Yt-228">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Find" id="9XE-hK-Hbc">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Find…" tag="1" keyEquivalent="f" id="yA2-y8-dhZ">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="zqc-0l-nEk"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="ceF-be-hip">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="Fno-81-Q2N"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="6FX-CW-rwT">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="AeZ-8e-DSZ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="kIR-xV-7pl">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="ALM-eS-1XU"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="yTY-2E-Cp9">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="Gbj-g6-cYJ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Jump to Selection" keyEquivalent="j" id="80n-rn-d7I">
|
||||||
|
<connections>
|
||||||
|
<action selector="centerSelectionInVisibleArea:" target="-1" id="qfH-cs-Cdv"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Spelling and Grammar" id="uWG-xr-cNT">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Spelling" id="Xoa-7u-7zM">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="rZl-3I-YFV">
|
||||||
|
<connections>
|
||||||
|
<action selector="showGuessPanel:" target="-1" id="CSh-4b-cGc"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Document Now" keyEquivalent=";" id="tjk-pc-YjK">
|
||||||
|
<connections>
|
||||||
|
<action selector="checkSpelling:" target="-1" id="O3S-v5-8Ra"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="ohv-rn-e7b"/>
|
||||||
|
<menuItem title="Check Spelling While Typing" id="E3o-iC-0NO">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleContinuousSpellChecking:" target="-1" id="k4Q-aZ-O6y"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Grammar With Spelling" id="mUP-Jd-Tu1">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleGrammarChecking:" target="-1" id="Ypd-WC-IMS"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Correct Spelling Automatically" id="qq3-jk-TYN">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="Ob8-wh-bLR"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Substitutions" id="8Ti-fk-PNH">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Substitutions" id="NeW-07-aln">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Substitutions" id="DXm-9z-Sle">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="z43-IC-kpA"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="aVP-JA-dKq"/>
|
||||||
|
<menuItem title="Smart Copy/Paste" id="Pcq-B9-JQ5">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleSmartInsertDelete:" target="-1" id="iu4-Nn-DU4"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Quotes" id="iDT-WP-ISR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="pnI-ku-LV9"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Dashes" id="QFc-JW-5qE">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="pY9-lv-EM7"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Links" id="opP-BJ-vhR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticLinkDetection:" target="-1" id="lfD-XD-tfO"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Data Detectors" id="zWu-P7-fpg">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDataDetection:" target="-1" id="EAz-S6-L7B"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Text Replacement" id="2R8-fL-H32">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticTextReplacement:" target="-1" id="7Lb-Uz-uJ9"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Transformations" id="v1A-Fi-A5j">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Transformations" id="B0B-bK-IZ3">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Make Upper Case" id="1P0-qI-ZAM">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="uppercaseWord:" target="-1" id="cre-Qh-bSZ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Make Lower Case" id="sK6-Xl-9uz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="lowercaseWord:" target="-1" id="qid-xu-iAw"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Capitalize" id="zvd-g7-rwM">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="capitalizeWord:" target="-1" id="0tj-lN-nE0"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Speech" id="bsj-0j-07E">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Speech" id="r3p-Q2-qJZ">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Start Speaking" id="8Hl-RR-cUq">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="startSpeaking:" target="-1" id="Zbb-Tp-HBE"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Stop Speaking" id="SLa-Tk-XGJ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="stopSpeaking:" target="-1" id="Oyc-KO-3qC"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="View" id="KUk-6o-tke">
|
||||||
|
<menu key="submenu" title="View" id="ITS-DI-OJD">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Make Text Bigger" keyEquivalent="+" id="Qae-cV-V7J">
|
||||||
|
<connections>
|
||||||
|
<action selector="makeTextBigger:" target="207" id="cHK-EO-JtV"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Make Text Smaller" keyEquivalent="-" id="Gv4-oc-1eL">
|
||||||
|
<connections>
|
||||||
|
<action selector="makeTextSmaller:" target="207" id="JTY-6k-ZKY"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Window" id="19">
|
||||||
|
<menu key="submenu" title="Window" systemMenu="window" id="24">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Zoom" keyEquivalent="z" id="197">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performZoom:" target="-1" id="198"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Minimize" keyEquivalent="m" id="23">
|
||||||
|
<connections>
|
||||||
|
<action selector="performMiniaturize:" target="-1" id="37"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="322">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Bring All to Front" id="5">
|
||||||
|
<connections>
|
||||||
|
<action selector="arrangeInFront:" target="-1" id="39"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="525"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="-21" y="164"/>
|
||||||
|
</menu>
|
||||||
|
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="TextWindow" animationBehavior="default" id="216" userLabel="TextOutputWindow">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<rect key="contentRect" x="297" y="408" width="580" height="420"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
|
||||||
|
<value key="minSize" type="size" width="400" height="200"/>
|
||||||
|
<view key="contentView" id="217">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="580" height="420"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<button verticalHuggingPriority="750" id="220">
|
||||||
|
<rect key="frame" x="488" y="7" width="86" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||||
|
<buttonCell key="cell" type="push" title="Quit" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="476">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
|
Gw
|
||||||
|
</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="cancel:" target="207" id="223"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<scrollView autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" id="226">
|
||||||
|
<rect key="frame" x="9" y="45" width="561" height="367"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="Hk1-72-Owa">
|
||||||
|
<rect key="frame" x="1" y="1" width="559" height="365"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<textView editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" allowsDocumentBackgroundColorChange="YES" linkDetection="YES" id="225">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="559" height="365"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<size key="minSize" width="559" height="365"/>
|
||||||
|
<size key="maxSize" width="561" height="10000000"/>
|
||||||
|
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="488"/>
|
||||||
|
</connections>
|
||||||
|
</textView>
|
||||||
|
</subviews>
|
||||||
|
</clipView>
|
||||||
|
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="483">
|
||||||
|
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</scroller>
|
||||||
|
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="482">
|
||||||
|
<rect key="frame" x="-30" y="1" width="15" height="340"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</scroller>
|
||||||
|
</scrollView>
|
||||||
|
<progressIndicator horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" id="230">
|
||||||
|
<rect key="frame" x="15" y="18" width="16" height="16"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
</progressIndicator>
|
||||||
|
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" id="366">
|
||||||
|
<rect key="frame" x="43" y="17" width="429" height="17"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||||
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" id="477">
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="376"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="-478" y="370"/>
|
||||||
|
</window>
|
||||||
|
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="WebViewWindow" animationBehavior="default" id="247" userLabel="WebOutputWindow">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<rect key="contentRect" x="297" y="408" width="580" height="420"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
|
||||||
|
<value key="minSize" type="size" width="400" height="200"/>
|
||||||
|
<view key="contentView" id="248">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="580" height="420"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<button verticalHuggingPriority="750" id="251">
|
||||||
|
<rect key="frame" x="485" y="6" width="86" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||||
|
<buttonCell key="cell" type="push" title="Quit" bezelStyle="rounded" alignment="center" state="on" borderStyle="border" inset="2" id="478">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
|
Gw
|
||||||
|
</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="cancel:" target="207" id="267"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<box boxType="oldStyle" borderType="line" titlePosition="noTitle" id="257">
|
||||||
|
<rect key="frame" x="8" y="45" width="563" height="367"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<view key="contentView" id="X4z-hY-QR7">
|
||||||
|
<rect key="frame" x="1" y="1" width="561" height="365"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<webView maintainsBackForwardList="NO" id="258" customClass="STDragWebView">
|
||||||
|
<rect key="frame" x="-1" y="0.0" width="562" height="365"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<webPreferences key="preferences" defaultFontSize="12" defaultFixedFontSize="12">
|
||||||
|
<nil key="identifier"/>
|
||||||
|
</webPreferences>
|
||||||
|
<connections>
|
||||||
|
<outlet property="UIDelegate" destination="207" id="378"/>
|
||||||
|
<outlet property="downloadDelegate" destination="207" id="382"/>
|
||||||
|
<outlet property="dragDelegate" destination="207" id="kaI-rz-1Ps"/>
|
||||||
|
<outlet property="frameLoadDelegate" destination="207" id="381"/>
|
||||||
|
<outlet property="policyDelegate" destination="207" id="380"/>
|
||||||
|
<outlet property="resourceLoadDelegate" destination="207" id="379"/>
|
||||||
|
</connections>
|
||||||
|
</webView>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
</box>
|
||||||
|
<progressIndicator horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" id="277">
|
||||||
|
<rect key="frame" x="16" y="15" width="16" height="16"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
</progressIndicator>
|
||||||
|
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" id="509">
|
||||||
|
<rect key="frame" x="37" y="14" width="429" height="17"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||||
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" id="510">
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="375"/>
|
||||||
|
</connections>
|
||||||
|
</window>
|
||||||
|
<window title="Droplet" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" frameAutosaveName="DropletWindow" animationBehavior="default" id="490" userLabel="DropletWindow">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
||||||
|
<rect key="contentRect" x="157" y="273" width="294" height="280"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
|
||||||
|
<view key="contentView" id="491">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="294" height="280"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<textField hidden="YES" verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" id="519">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="294" height="280"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<textFieldCell key="cell" enabled="NO" allowsUndo="NO" sendsActionOnEndEditing="YES" drawsBackground="YES" id="520">
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="disabledControlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<box autoresizesSubviews="NO" borderType="line" titlePosition="noTitle" id="494">
|
||||||
|
<rect key="frame" x="17" y="16" width="260" height="246"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<view key="contentView" id="q0i-G4-K1a">
|
||||||
|
<rect key="frame" x="3" y="3" width="254" height="240"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" style="spinning" id="496">
|
||||||
|
<rect key="frame" x="112" y="124" width="32" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
</progressIndicator>
|
||||||
|
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" id="499">
|
||||||
|
<rect key="frame" x="92" y="108" width="71" height="34"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<textFieldCell key="cell" controlSize="mini" sendsActionOnEndEditing="YES" alignment="center" title="Drop files here" id="500">
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<color key="textColor" name="disabledControlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" id="505">
|
||||||
|
<rect key="frame" x="18" y="40" width="218" height="76"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" alignment="center" id="506">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<color key="textColor" name="disabledControlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
</box>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="207" id="RFt-L5-f73"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="608" y="262"/>
|
||||||
|
</window>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
BIN
mobisync.app/Contents/Resources/MainMenu.nib/keyedobjects.nib
generated
Normal file
BIN
mobisync.app/Contents/Resources/MainMenu.nib/keyedobjects.nib
generated
Normal file
Binary file not shown.
BIN
mobisync.app/Contents/Resources/adb
Executable file
BIN
mobisync.app/Contents/Resources/adb
Executable file
Binary file not shown.
774
mobisync.app/Contents/Resources/adb-sync
Executable file
774
mobisync.app/Contents/Resources/adb-sync
Executable file
|
|
@ -0,0 +1,774 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Sync files from/to an Android device."""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import locale
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def _sprintf(s, *args):
|
||||||
|
# To be able to use string formatting, we first have to covert to
|
||||||
|
# unicode strings; however, we must do so in a way that preserves all
|
||||||
|
# bytes, and convert back at the end. An encoding that maps all byte
|
||||||
|
# values to different Unicode codepoints is cp437.
|
||||||
|
return (s.decode('cp437') % tuple([
|
||||||
|
(x.decode('cp437') if type(x) == bytes else x) for x in args
|
||||||
|
])).encode('cp437')
|
||||||
|
|
||||||
|
|
||||||
|
def _print(s, *args):
|
||||||
|
"""Writes a binary string to stdout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
s: The binary format string to write.
|
||||||
|
args: The args for the format string.
|
||||||
|
"""
|
||||||
|
if hasattr(sys.stdout, 'buffer'):
|
||||||
|
# Python 3.
|
||||||
|
sys.stdout.buffer.write(_sprintf(s, *args) + b'\n')
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
else:
|
||||||
|
# Python 2.
|
||||||
|
sys.stdout.write(_sprintf(s, *args) + b'\n')
|
||||||
|
|
||||||
|
|
||||||
|
class AdbFileSystem(object):
|
||||||
|
"""Mimics os's file interface but uses the adb utility."""
|
||||||
|
|
||||||
|
def __init__(self, adb):
|
||||||
|
self.stat_cache = {}
|
||||||
|
self.adb = adb
|
||||||
|
|
||||||
|
# Regarding parsing stat results, we only care for the following fields:
|
||||||
|
# - st_size
|
||||||
|
# - st_mtime
|
||||||
|
# - st_mode (but only about S_ISDIR and S_ISREG properties)
|
||||||
|
# Therefore, we only capture parts of 'ls -l' output that we actually use.
|
||||||
|
# The other fields will be filled with dummy values.
|
||||||
|
LS_TO_STAT_RE = re.compile(br'''^
|
||||||
|
(?:
|
||||||
|
(?P<S_IFREG> -) |
|
||||||
|
(?P<S_IFBLK> b) |
|
||||||
|
(?P<S_IFCHR> c) |
|
||||||
|
(?P<S_IFDIR> d) |
|
||||||
|
(?P<S_IFLNK> l) |
|
||||||
|
(?P<S_IFIFO> p) |
|
||||||
|
(?P<S_IFSOCK> s))
|
||||||
|
[-r][-w][-xsS]
|
||||||
|
[-r][-w][-xsS]
|
||||||
|
[-r][-w][-xtT] # Mode string.
|
||||||
|
[ ]+
|
||||||
|
(?:
|
||||||
|
[0-9]+ # number of hard links
|
||||||
|
[ ]+
|
||||||
|
)?
|
||||||
|
[^ ]+ # User name/ID.
|
||||||
|
[ ]+
|
||||||
|
[^ ]+ # Group name/ID.
|
||||||
|
[ ]+
|
||||||
|
(?(S_IFBLK) [^ ]+[ ]+[^ ]+[ ]+) # Device numbers.
|
||||||
|
(?(S_IFCHR) [^ ]+[ ]+[^ ]+[ ]+) # Device numbers.
|
||||||
|
(?(S_IFDIR) [0-9]+ [ ]+)? # directory Size.
|
||||||
|
(?(S_IFREG)
|
||||||
|
(?P<st_size> [0-9]+) # Size.
|
||||||
|
[ ]+)
|
||||||
|
(?P<st_mtime>
|
||||||
|
[0-9]{4}-[0-9]{2}-[0-9]{2} # Date.
|
||||||
|
[ ]
|
||||||
|
[0-9]{2}:[0-9]{2}) # Time.
|
||||||
|
[ ]
|
||||||
|
# Don't capture filename for symlinks (ambiguous).
|
||||||
|
(?(S_IFLNK) .* | (?P<filename> .*))
|
||||||
|
$''', re.DOTALL | re.VERBOSE)
|
||||||
|
def LsToStat(self, line):
|
||||||
|
"""Convert a line from 'ls -l' output to a stat result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line: Output line of 'ls -l' on Android.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
os.stat_result for the line.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OSError: if the given string is not a 'ls -l' output line (but maybe an
|
||||||
|
error message instead).
|
||||||
|
"""
|
||||||
|
|
||||||
|
match = self.LS_TO_STAT_RE.match(line)
|
||||||
|
if match is None:
|
||||||
|
_print(b'Warning: could not parse %r.', line)
|
||||||
|
raise OSError('Unparseable ls -al result.')
|
||||||
|
groups = match.groupdict()
|
||||||
|
|
||||||
|
# Get the values we're interested in.
|
||||||
|
st_mode = ( # 0755
|
||||||
|
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||||
|
if groups['S_IFREG']: st_mode |= stat.S_IFREG
|
||||||
|
if groups['S_IFBLK']: st_mode |= stat.S_IFBLK
|
||||||
|
if groups['S_IFCHR']: st_mode |= stat.S_IFCHR
|
||||||
|
if groups['S_IFDIR']: st_mode |= stat.S_IFDIR
|
||||||
|
if groups['S_IFIFO']: st_mode |= stat.S_IFIFO
|
||||||
|
if groups['S_IFLNK']: st_mode |= stat.S_IFLNK
|
||||||
|
if groups['S_IFSOCK']: st_mode |= stat.S_IFSOCK
|
||||||
|
st_size = groups['st_size']
|
||||||
|
if st_size is not None:
|
||||||
|
st_size = int(st_size)
|
||||||
|
st_mtime = time.mktime(time.strptime(match.group('st_mtime').decode('utf-8'),
|
||||||
|
'%Y-%m-%d %H:%M'))
|
||||||
|
|
||||||
|
# Fill the rest with dummy values.
|
||||||
|
st_ino = 1
|
||||||
|
st_rdev = 0
|
||||||
|
st_nlink = 1
|
||||||
|
st_uid = -2 # Nobody.
|
||||||
|
st_gid = -2 # Nobody.
|
||||||
|
st_atime = st_ctime = st_mtime
|
||||||
|
|
||||||
|
stbuf = os.stat_result((st_mode, st_ino, st_rdev, st_nlink, st_uid, st_gid,
|
||||||
|
st_size, st_atime, st_mtime, st_ctime))
|
||||||
|
filename = groups['filename']
|
||||||
|
return stbuf, filename
|
||||||
|
|
||||||
|
def Stdout(self, *popen_args):
|
||||||
|
"""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.
|
||||||
|
# Usually, arguments in 'adb shell' use are put in double quotes by adb,
|
||||||
|
# but not in any way escaped.
|
||||||
|
arg = arg.replace(b'\\', b'\\\\')
|
||||||
|
arg = arg.replace(b'"', b'\\"')
|
||||||
|
arg = arg.replace(b'$', b'\\$')
|
||||||
|
arg = arg.replace(b'`', b'\\`')
|
||||||
|
arg = b'"' + arg + b'"'
|
||||||
|
return arg
|
||||||
|
|
||||||
|
def IsWorking(self):
|
||||||
|
"""Tests the adb connection."""
|
||||||
|
# This string should contain all possible evil, but no percent signs.
|
||||||
|
# Note this code uses 'date' and not 'echo', as date just calls strftime
|
||||||
|
# while echo does its own backslash escape handling additionally to the
|
||||||
|
# shell's. Too bad printf "%s\n" is not available.
|
||||||
|
test_strings = [
|
||||||
|
b'(',
|
||||||
|
b'(; #`ls`$PATH\'"(\\\\\\\\){};!\xc0\xaf\xff\xc2\xbf'
|
||||||
|
]
|
||||||
|
for test_string in test_strings:
|
||||||
|
good = False
|
||||||
|
with self.Stdout(self.adb + [b'shell', _sprintf(b'date +%s',
|
||||||
|
self.QuoteArgument(test_string))]) as stdout:
|
||||||
|
for line in stdout:
|
||||||
|
line = line.rstrip(b'\r\n')
|
||||||
|
if line == test_string:
|
||||||
|
good = True
|
||||||
|
if not good:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def listdir(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""List the contents of a directory, caching them for later lstat calls."""
|
||||||
|
with self.Stdout(self.adb + [b'shell', _sprintf(b'ls -al %s',
|
||||||
|
self.QuoteArgument(path + b'/'))]) as stdout:
|
||||||
|
for line in stdout:
|
||||||
|
if line.startswith(b'total '):
|
||||||
|
continue
|
||||||
|
line = line.rstrip(b'\r\n')
|
||||||
|
try:
|
||||||
|
statdata, filename = self.LsToStat(line)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if filename is None:
|
||||||
|
_print(b'Warning: could not parse %s', line)
|
||||||
|
else:
|
||||||
|
self.stat_cache[path + b'/' + filename] = statdata
|
||||||
|
yield filename
|
||||||
|
|
||||||
|
def lstat(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""Stat a file."""
|
||||||
|
if path in self.stat_cache:
|
||||||
|
return self.stat_cache[path]
|
||||||
|
with self.Stdout(self.adb + [b'shell', _sprintf(b'ls -ald %s',
|
||||||
|
self.QuoteArgument(path))]) as stdout:
|
||||||
|
for line in stdout:
|
||||||
|
if line.startswith(b'total '):
|
||||||
|
continue
|
||||||
|
line = line.rstrip(b'\r\n')
|
||||||
|
statdata, filename = self.LsToStat(line)
|
||||||
|
self.stat_cache[path] = statdata
|
||||||
|
return statdata
|
||||||
|
raise OSError('No such file or directory')
|
||||||
|
|
||||||
|
def unlink(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""Delete a file."""
|
||||||
|
if subprocess.call(self.adb + [b'shell', _sprintf(b'rm %s',
|
||||||
|
self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('unlink failed')
|
||||||
|
|
||||||
|
def rmdir(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""Delete a directory."""
|
||||||
|
if subprocess.call(self.adb + [b'shell', _sprintf(b'rmdir %s',
|
||||||
|
self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('rmdir failed')
|
||||||
|
|
||||||
|
def makedirs(self, path): # os's name, so pylint: disable=g-bad-name
|
||||||
|
"""Create a directory."""
|
||||||
|
if subprocess.call(self.adb + [b'shell', _sprintf(b'mkdir -p %s',
|
||||||
|
self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('mkdir failed')
|
||||||
|
|
||||||
|
def utime(self, path, times):
|
||||||
|
# TODO(rpolzer): Find out why this does not work (returns status 255).
|
||||||
|
"""Set the time of a file to a specified unix time."""
|
||||||
|
atime, mtime = times
|
||||||
|
timestr = time.strftime(b'%Y%m%d.%H%M%S', time.localtime(mtime))
|
||||||
|
if subprocess.call(self.adb + [b'shell', _sprintf(b'touch -mt %s %s',
|
||||||
|
timestr, self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('touch failed')
|
||||||
|
timestr = time.strftime(b'%Y%m%d.%H%M%S', time.localtime(atime))
|
||||||
|
if subprocess.call(self.adb + [b'shell',_sprintf( b'touch -at %s %s',
|
||||||
|
timestr, self.QuoteArgument(path))]) != 0:
|
||||||
|
raise OSError('touch failed')
|
||||||
|
|
||||||
|
def glob(self, path):
|
||||||
|
with self.Stdout(self.adb + [b'shell',
|
||||||
|
_sprintf(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):
|
||||||
|
"""Push a file from the local file system to the Android device."""
|
||||||
|
if subprocess.call(self.adb + [b'push', src, dst]) != 0:
|
||||||
|
raise OSError('push failed')
|
||||||
|
|
||||||
|
def Pull(self, src, dst):
|
||||||
|
"""Pull a file from the Android device to the local file system."""
|
||||||
|
if subprocess.call(self.adb + [b'pull', src, dst]) != 0:
|
||||||
|
raise OSError('pull failed')
|
||||||
|
|
||||||
|
|
||||||
|
def BuildFileList(fs, path, prefix=b''):
|
||||||
|
"""Builds a file list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fs: File system provider (can be os or AdbFileSystem()).
|
||||||
|
path: Initial path.
|
||||||
|
prefix: Path prefix for output file names.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
File names from path (prefixed by prefix).
|
||||||
|
Directories are yielded before their contents.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statresult = fs.lstat(path)
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
if stat.S_ISDIR(statresult.st_mode):
|
||||||
|
yield prefix, statresult
|
||||||
|
try:
|
||||||
|
files = list(fs.listdir(path))
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
files.sort()
|
||||||
|
for n in files:
|
||||||
|
if n == b'.' or n == b'..':
|
||||||
|
continue
|
||||||
|
for t in BuildFileList(fs, path + b'/' + n, prefix + b'/' + n):
|
||||||
|
yield t
|
||||||
|
elif stat.S_ISREG(statresult.st_mode) or stat.S_ISLNK(statresult.st_mode):
|
||||||
|
yield prefix, statresult
|
||||||
|
else:
|
||||||
|
_print(b'Note: unsupported file: %s', path)
|
||||||
|
|
||||||
|
|
||||||
|
def DiffLists(a, b):
|
||||||
|
"""Compares two lists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a: the first list.
|
||||||
|
b: the second list.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a_only: the items from list a.
|
||||||
|
both: the items from both list, with the remaining tuple items combined.
|
||||||
|
b_only: the items from list b.
|
||||||
|
"""
|
||||||
|
a_only = []
|
||||||
|
b_only = []
|
||||||
|
both = []
|
||||||
|
|
||||||
|
a_iter = iter(a)
|
||||||
|
b_iter = iter(b)
|
||||||
|
a_active = True
|
||||||
|
b_active = True
|
||||||
|
a_available = False
|
||||||
|
b_available = False
|
||||||
|
a_item = None
|
||||||
|
b_item = None
|
||||||
|
|
||||||
|
while a_active and b_active:
|
||||||
|
if not a_available:
|
||||||
|
try:
|
||||||
|
a_item = next(a_iter)
|
||||||
|
a_available = True
|
||||||
|
except StopIteration:
|
||||||
|
a_active = False
|
||||||
|
break
|
||||||
|
if not b_available:
|
||||||
|
try:
|
||||||
|
b_item = next(b_iter)
|
||||||
|
b_available = True
|
||||||
|
except StopIteration:
|
||||||
|
b_active = False
|
||||||
|
break
|
||||||
|
if a_item[0] == b_item[0]:
|
||||||
|
both.append(tuple([a_item[0]] + list(a_item[1:]) + list(b_item[1:])))
|
||||||
|
a_available = False
|
||||||
|
b_available = False
|
||||||
|
elif a_item[0] < b_item[0]:
|
||||||
|
a_only.append(a_item)
|
||||||
|
a_available = False
|
||||||
|
elif a_item[0] > b_item[0]:
|
||||||
|
b_only.append(b_item)
|
||||||
|
b_available = False
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if a_active:
|
||||||
|
if a_available:
|
||||||
|
a_only.append(a_item)
|
||||||
|
for item in a_iter:
|
||||||
|
a_only.append(item)
|
||||||
|
if b_active:
|
||||||
|
if b_available:
|
||||||
|
b_only.append(b_item)
|
||||||
|
for item in b_iter:
|
||||||
|
b_only.append(item)
|
||||||
|
|
||||||
|
return a_only, both, b_only
|
||||||
|
|
||||||
|
|
||||||
|
class FileSyncer(object):
|
||||||
|
"""File synchronizer."""
|
||||||
|
|
||||||
|
def __init__(self, adb, local_path, remote_path, local_to_remote,
|
||||||
|
remote_to_local, preserve_times, delete_missing, allow_overwrite,
|
||||||
|
allow_replace, dry_run):
|
||||||
|
self.local = local_path
|
||||||
|
self.remote = remote_path
|
||||||
|
self.adb = adb
|
||||||
|
self.local_to_remote = local_to_remote
|
||||||
|
self.remote_to_local = remote_to_local
|
||||||
|
self.preserve_times = preserve_times
|
||||||
|
self.delete_missing = delete_missing
|
||||||
|
self.allow_overwrite = allow_overwrite
|
||||||
|
self.allow_replace = allow_replace
|
||||||
|
self.dry_run = dry_run
|
||||||
|
self.local_only = None
|
||||||
|
self.both = None
|
||||||
|
self.remote_only = None
|
||||||
|
self.num_bytes = 0
|
||||||
|
self.start_time = time.time()
|
||||||
|
|
||||||
|
def IsWorking(self):
|
||||||
|
"""Tests the adb connection."""
|
||||||
|
return self.adb.IsWorking()
|
||||||
|
|
||||||
|
def ScanAndDiff(self):
|
||||||
|
"""Scans the local and remote locations and identifies differences."""
|
||||||
|
_print(b'Scanning and diffing...')
|
||||||
|
locallist = BuildFileList(os, self.local)
|
||||||
|
remotelist = BuildFileList(self.adb, self.remote)
|
||||||
|
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:
|
||||||
|
_print(b'No files seen. User error?')
|
||||||
|
self.src_to_dst = (self.local_to_remote, self.remote_to_local)
|
||||||
|
self.dst_to_src = (self.remote_to_local, self.local_to_remote)
|
||||||
|
self.src_only = (self.local_only, self.remote_only)
|
||||||
|
self.dst_only = (self.remote_only, self.local_only)
|
||||||
|
self.src = (self.local, self.remote)
|
||||||
|
self.dst = (self.remote, self.local)
|
||||||
|
self.dst_fs = (self.adb, os)
|
||||||
|
self.push = (b'Push', b'Pull')
|
||||||
|
self.copy = (self.adb.Push, self.adb.Pull)
|
||||||
|
|
||||||
|
def InterruptProtection(self, fs, name):
|
||||||
|
"""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:
|
||||||
|
_print(b'Interrupted-%s-Delete: %s',
|
||||||
|
b'Pull' if fs == os else b'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."""
|
||||||
|
if not self.delete_missing:
|
||||||
|
return
|
||||||
|
for i in [0, 1]:
|
||||||
|
if self.src_to_dst[i] and not self.dst_to_src[i]:
|
||||||
|
if not self.src_only[i] and not self.both:
|
||||||
|
_print(b'Cowardly refusing to delete everything.')
|
||||||
|
else:
|
||||||
|
for name, s in reversed(self.dst_only[i]):
|
||||||
|
dst_name = self.dst[i] + name
|
||||||
|
_print(b'%s-Delete: %s', self.push[i], dst_name)
|
||||||
|
if stat.S_ISDIR(s.st_mode):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].rmdir(dst_name)
|
||||||
|
else:
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].unlink(dst_name)
|
||||||
|
del self.dst_only[i][:]
|
||||||
|
|
||||||
|
def PerformOverwrites(self):
|
||||||
|
"""Delete files/directories that are in the way for overwriting."""
|
||||||
|
src_only_prepend = ([], [])
|
||||||
|
for name, localstat, remotestat in self.both:
|
||||||
|
if stat.S_ISDIR(localstat.st_mode) and stat.S_ISDIR(remotestat.st_mode):
|
||||||
|
# A dir is a dir is a dir.
|
||||||
|
continue
|
||||||
|
elif stat.S_ISDIR(localstat.st_mode) or stat.S_ISDIR(remotestat.st_mode):
|
||||||
|
# Dir vs file? Nothing to do here yet.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# File vs file? Compare sizes.
|
||||||
|
if localstat.st_size == remotestat.st_size:
|
||||||
|
continue
|
||||||
|
l2r = self.local_to_remote
|
||||||
|
r2l = self.remote_to_local
|
||||||
|
if l2r and r2l:
|
||||||
|
# Truncate times to full minutes, as Android's "ls" only outputs minute
|
||||||
|
# accuracy.
|
||||||
|
localminute = int(localstat.st_mtime / 60)
|
||||||
|
remoteminute = int(remotestat.st_mtime / 60)
|
||||||
|
if localminute > remoteminute:
|
||||||
|
r2l = False
|
||||||
|
elif localminute < remoteminute:
|
||||||
|
l2r = False
|
||||||
|
if l2r and r2l:
|
||||||
|
_print(b'Unresolvable: %s', name)
|
||||||
|
continue
|
||||||
|
if l2r:
|
||||||
|
i = 0 # Local to remote operation.
|
||||||
|
src_stat = localstat
|
||||||
|
dst_stat = remotestat
|
||||||
|
else:
|
||||||
|
i = 1 # Remote to local operation.
|
||||||
|
src_stat = remotestat
|
||||||
|
dst_stat = localstat
|
||||||
|
dst_name = self.dst[i] + name
|
||||||
|
_print(b'%s-Delete-Conflicting: %s', self.push[i], dst_name)
|
||||||
|
if stat.S_ISDIR(localstat.st_mode) or stat.S_ISDIR(remotestat.st_mode):
|
||||||
|
if not self.allow_replace:
|
||||||
|
_print(b'Would have to replace to do this. '
|
||||||
|
b'Use --force to allow this.')
|
||||||
|
continue
|
||||||
|
if not self.allow_overwrite:
|
||||||
|
_print(b'Would have to overwrite to do this, '
|
||||||
|
b'which --no-clobber forbids.')
|
||||||
|
continue
|
||||||
|
if stat.S_ISDIR(dst_stat.st_mode):
|
||||||
|
kill_files = [x for x in self.dst_only[i]
|
||||||
|
if x[0][:len(name) + 1] == name + b'/']
|
||||||
|
self.dst_only[i][:] = [x for x in self.dst_only[i]
|
||||||
|
if x[0][:len(name) + 1] != name + b'/']
|
||||||
|
for l, s in reversed(kill_files):
|
||||||
|
if stat.S_ISDIR(s.st_mode):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].rmdir(self.dst[i] + l)
|
||||||
|
else:
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].unlink(self.dst[i] + l)
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].rmdir(dst_name)
|
||||||
|
elif stat.S_ISDIR(src_stat.st_mode):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].unlink(dst_name)
|
||||||
|
else:
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].unlink(dst_name)
|
||||||
|
src_only_prepend[i].append((name, src_stat))
|
||||||
|
for i in [0, 1]:
|
||||||
|
self.src_only[i][:0] = src_only_prepend[i]
|
||||||
|
|
||||||
|
def PerformCopies(self):
|
||||||
|
"""Perform all copying necessary for the file sync operation."""
|
||||||
|
for i in [0, 1]:
|
||||||
|
if self.src_to_dst[i]:
|
||||||
|
for name, s in self.src_only[i]:
|
||||||
|
src_name = self.src[i] + name
|
||||||
|
dst_name = self.dst[i] + name
|
||||||
|
_print(b'%s: %s', self.push[i], dst_name)
|
||||||
|
if stat.S_ISDIR(s.st_mode):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.dst_fs[i].makedirs(dst_name)
|
||||||
|
else:
|
||||||
|
with self.InterruptProtection(self.dst_fs[i], dst_name):
|
||||||
|
if not self.dry_run:
|
||||||
|
self.copy[i](src_name, dst_name)
|
||||||
|
self.num_bytes += s.st_size
|
||||||
|
if not self.dry_run:
|
||||||
|
if self.preserve_times:
|
||||||
|
_print(b'%s-Times: accessed %s, modified %s',
|
||||||
|
self.push[i],
|
||||||
|
time.asctime(time.localtime(s.st_atime)).encode('utf-8'),
|
||||||
|
time.asctime(time.localtime(s.st_mtime)).encode('utf-8'))
|
||||||
|
self.dst_fs[i].utime(dst_name, (s.st_atime, s.st_mtime))
|
||||||
|
|
||||||
|
def TimeReport(self):
|
||||||
|
"""Report time and amount of data transferred."""
|
||||||
|
if self.dry_run:
|
||||||
|
_print(b'Total: %d bytes', self.num_bytes)
|
||||||
|
else:
|
||||||
|
end_time = time.time()
|
||||||
|
dt = end_time - self.start_time
|
||||||
|
rate = self.num_bytes / 1024.0 / dt
|
||||||
|
_print(b'Total: %d KB/s (%d bytes in %.3fs)', rate, self.num_bytes, dt)
|
||||||
|
|
||||||
|
|
||||||
|
def ExpandWildcards(globber, path):
|
||||||
|
if path.find(b'?') == -1 and path.find(b'*') == -1 and path.find(b'[') == -1:
|
||||||
|
return [path]
|
||||||
|
return globber.glob(path)
|
||||||
|
|
||||||
|
|
||||||
|
def FixPath(src, dst):
|
||||||
|
# rsync-like path munging to make remote specifications shorter.
|
||||||
|
append = b''
|
||||||
|
pos = src.rfind(b'/')
|
||||||
|
if pos >= 0:
|
||||||
|
if src.endswith(b'/'):
|
||||||
|
# Final slash: copy to the destination "as is".
|
||||||
|
src = src[:-1]
|
||||||
|
else:
|
||||||
|
# No final slash: destination name == source name.
|
||||||
|
append = src[pos:]
|
||||||
|
else:
|
||||||
|
# No slash at all - use same name at destination.
|
||||||
|
append = b'/' + src
|
||||||
|
# Append the destination file name if any.
|
||||||
|
# BUT: do not append "." or ".." components!
|
||||||
|
if append != b'/.' and append != b'/..':
|
||||||
|
dst += append
|
||||||
|
return (src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def main(*args):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Synchronize a directory between an Android device and the '+
|
||||||
|
'local file system')
|
||||||
|
parser.add_argument('source', metavar='SRC', type=str, nargs='+',
|
||||||
|
help='The directory to read files/directories from. '+
|
||||||
|
'This must be a local path if -R is not specified, '+
|
||||||
|
'and an Android path if -R is specified. If SRC does '+
|
||||||
|
'not end with a final slash, its last path component '+
|
||||||
|
'is appended to DST (like rsync does).')
|
||||||
|
parser.add_argument('destination', metavar='DST', type=str,
|
||||||
|
help='The directory to write files/directories to. '+
|
||||||
|
'This must be an Android path if -R is not specified, '+
|
||||||
|
'and a local path if -R is specified.')
|
||||||
|
parser.add_argument('-e', '--adb', metavar='COMMAND', default='adb', type=str,
|
||||||
|
help='Use the given adb binary and arguments.')
|
||||||
|
parser.add_argument('--device', action='store_true',
|
||||||
|
help='Directs command to the only connected USB device; '+
|
||||||
|
'returns an error if more than one USB device is '+
|
||||||
|
'present. '+
|
||||||
|
'Corresponds to the "-d" option of adb.')
|
||||||
|
parser.add_argument('--emulator', action='store_true',
|
||||||
|
help='Directs command to the only running emulator; '+
|
||||||
|
'returns an error if more than one emulator is running. '+
|
||||||
|
'Corresponds to the "-e" option of adb.')
|
||||||
|
parser.add_argument('-s', '--serial', metavar='DEVICE', type=str,
|
||||||
|
help='Directs command to the device or emulator with '+
|
||||||
|
'the given serial number or qualifier. Overrides '+
|
||||||
|
'ANDROID_SERIAL environment variable. Use "adb devices" '+
|
||||||
|
'to list all connected devices with their respective '+
|
||||||
|
'serial number. '+
|
||||||
|
'Corresponds to the "-s" option of adb.')
|
||||||
|
parser.add_argument('-H', '--host', metavar='HOST', type=str,
|
||||||
|
help='Name of adb server host (default: localhost). '+
|
||||||
|
'Corresponds to the "-H" option of adb.')
|
||||||
|
parser.add_argument('-P', '--port', metavar='PORT', type=str,
|
||||||
|
help='Port of adb server (default: 5037). '+
|
||||||
|
'Corresponds to the "-P" option of adb.')
|
||||||
|
parser.add_argument('-R', '--reverse', action='store_true',
|
||||||
|
help='Reverse sync (pull, not push).')
|
||||||
|
parser.add_argument('-2', '--two-way', action='store_true',
|
||||||
|
help='Two-way sync (compare modification time; after '+
|
||||||
|
'the sync, both sides will have all files in the '+
|
||||||
|
'respective newest version. This relies on the clocks '+
|
||||||
|
'of your system and the device to match.')
|
||||||
|
#parser.add_argument('-t', '--times', action='store_true',
|
||||||
|
# help='Preserve modification times when copying.')
|
||||||
|
parser.add_argument('-d', '--delete', action='store_true',
|
||||||
|
help='Delete files from DST that are not present on '+
|
||||||
|
'SRC. Mutually exclusive with -2.')
|
||||||
|
parser.add_argument('-f', '--force', action='store_true',
|
||||||
|
help='Allow deleting files/directories when having to '+
|
||||||
|
'replace a file by a directory or vice versa. This is '+
|
||||||
|
'disabled by default to prevent large scale accidents.')
|
||||||
|
parser.add_argument('-n', '--no-clobber', action='store_true',
|
||||||
|
help='Do not ever overwrite any '+
|
||||||
|
'existing files. Mutually exclusive with -f.')
|
||||||
|
parser.add_argument('--dry-run',action='store_true',
|
||||||
|
help='Do not do anything - just show what would '+
|
||||||
|
'be done.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
args_encoding = locale.getdefaultlocale()[1]
|
||||||
|
|
||||||
|
localpatterns = [x.encode(args_encoding) for x in args.source]
|
||||||
|
remotepath = args.destination.encode(args_encoding)
|
||||||
|
adb = args.adb.encode(args_encoding).split(b' ')
|
||||||
|
if args.device:
|
||||||
|
adb += [b'-d']
|
||||||
|
if args.emulator:
|
||||||
|
adb += [b'-e']
|
||||||
|
if args.serial != None:
|
||||||
|
adb += [b'-s', args.serial.encode(args_encoding)]
|
||||||
|
if args.host != None:
|
||||||
|
adb += [b'-H', args.host.encode(args_encoding)]
|
||||||
|
if args.port != None:
|
||||||
|
adb += [b'-P', args.port.encode(args_encoding)]
|
||||||
|
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
|
||||||
|
delete_missing = args.delete
|
||||||
|
allow_replace = args.force
|
||||||
|
allow_overwrite = not args.no_clobber
|
||||||
|
dry_run = args.dry_run
|
||||||
|
local_to_remote = True
|
||||||
|
remote_to_local = False
|
||||||
|
if args.two_way:
|
||||||
|
local_to_remote = True
|
||||||
|
remote_to_local = True
|
||||||
|
if args.reverse:
|
||||||
|
local_to_remote, remote_to_local = remote_to_local, local_to_remote
|
||||||
|
localpaths, remotepaths = remotepaths, localpaths
|
||||||
|
if allow_replace and not allow_overwrite:
|
||||||
|
_print(b'--no-clobber and --force are mutually exclusive.')
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
if delete_missing and local_to_remote and remote_to_local:
|
||||||
|
_print(b'--delete and --two-way are mutually exclusive.')
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Two-way sync is only allowed with disjoint remote and local path sets.
|
||||||
|
if (remote_to_local and local_to_remote) or delete_missing:
|
||||||
|
if ((remote_to_local and len(localpaths) != len(set(localpaths))) or
|
||||||
|
(local_to_remote and len(remotepaths) != len(set(remotepaths)))):
|
||||||
|
_print(b'--two-way and --delete are only supported for disjoint sets of '
|
||||||
|
b'source and destination paths (in other words, all SRC must '
|
||||||
|
b'differ in basename).')
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(len(localpaths)):
|
||||||
|
_print(b'Sync: local %s, remote %s', 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)
|
||||||
|
if not syncer.IsWorking():
|
||||||
|
_print(b'Device not connected or not working.')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
syncer.ScanAndDiff()
|
||||||
|
syncer.PerformDeletions()
|
||||||
|
syncer.PerformOverwrites()
|
||||||
|
syncer.PerformCopies()
|
||||||
|
finally:
|
||||||
|
syncer.TimeReport()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(*sys.argv)
|
||||||
112
mobisync.app/Contents/Resources/script
Executable file
112
mobisync.app/Contents/Resources/script
Executable file
|
|
@ -0,0 +1,112 @@
|
||||||
|
#!/bin/zsh
|
||||||
|
# This script syncs an Android with a Mac using adb and adb-sync.
|
||||||
|
# Version 9.5 (16 April 2019)
|
||||||
|
|
||||||
|
# The below are Platypus features for managing UI
|
||||||
|
echo "DETAILS:HIDE" # Hides the deatiled text in progress bar
|
||||||
|
echo "PROGRESS:0" # Show the progress bar at 0%
|
||||||
|
echo "Script starting" # Show this message above the progress bar
|
||||||
|
|
||||||
|
# Export paths for use if the script is turned into an app using Platypus.
|
||||||
|
export LC_ALL=en_US.UTF-8
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
|
||||||
|
# Set the path to the log file and destinations
|
||||||
|
export LOG=~/Projects/Programming/push/mobisync/mobisync-log.txt # A tilde will not work within quote marks.
|
||||||
|
export MEDIA=/sdcard/DCIM/Moment/
|
||||||
|
export DOCUMENTS=/sdcard/Documents/
|
||||||
|
export DOWNLOAD=/sdcard/Download/
|
||||||
|
export DOWNLOADS=~/Downloads/
|
||||||
|
export MAC_MUSIC=~/Music/
|
||||||
|
export ANDROID_MUSIC=/sdcard/Music/
|
||||||
|
export OCTOPUS=~/Documents/tentacles/
|
||||||
|
export TENTACLES=/sdcard/.tentacles/
|
||||||
|
export APPS=~/Backup/Apps/
|
||||||
|
export RECOVERY=~/Backup/Recovery/
|
||||||
|
export MOBILE=~/Backup/Mobile/
|
||||||
|
export SIGNAL=~/Backup/Signal/
|
||||||
|
export AUDIO=/sdcard/Record/
|
||||||
|
|
||||||
|
echo "NOTIFICATION:Syncing is starting..." # Send a notification (with logo)
|
||||||
|
echo "PROGRESS:10"
|
||||||
|
echo "Default paths set"
|
||||||
|
|
||||||
|
# Function: Reviews the last command for errors. Then prints update complete to log or shows error dialog. Takes section variable.
|
||||||
|
catcher () {
|
||||||
|
if [ "$?" = "0" ]; then
|
||||||
|
printf "$1 synced." >> $LOG # If no error, print sync complete to file.
|
||||||
|
echo "" >> $LOG # Add a line to file.
|
||||||
|
else # If error, show a dialog stating the section where the error occured.
|
||||||
|
echo "NOTIFICATION:'$1' sync failed."
|
||||||
|
printf "$1 failed to sync." >> $LOG # If error, print sync failed to file.
|
||||||
|
echo "" >> $LOG # Add a line to file.
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function: Creates a horizontal line in the text file.
|
||||||
|
line () {
|
||||||
|
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - >> $LOG # Prints line
|
||||||
|
}
|
||||||
|
|
||||||
|
# Writes the header for the log file: Program, Version number, Date and Line.
|
||||||
|
{ echo "mobisync 9.5"; echo "Log: " `date`; line; } > $LOG
|
||||||
|
|
||||||
|
# Syncing images / video on device to the Downloads folder and sync the wallpaper on computer.
|
||||||
|
{ adb-sync --reverse $MEDIA $DOWNLOADS; } >> $LOG
|
||||||
|
catcher Images
|
||||||
|
line
|
||||||
|
echo "PROGRESS:20"
|
||||||
|
echo "Images synced"
|
||||||
|
|
||||||
|
# Syncing audio on device to the Downloads folder on computer. Audio is isolated to extension-type downloads only.
|
||||||
|
{ adb-sync --reverse $AUDIO $DOWNLOADS; } >> $LOG
|
||||||
|
catcher Audio
|
||||||
|
line
|
||||||
|
echo "PROGRESS:30"
|
||||||
|
echo "Media synced"
|
||||||
|
|
||||||
|
# Syncing documents on device to the Downloads folder on computer.
|
||||||
|
{ adb-sync --reverse $DOCUMENTS $DOWNLOADS; adb-sync --reverse $DOWNLOAD $DOWNLOADS; } >> $LOG
|
||||||
|
catcher Documents
|
||||||
|
line
|
||||||
|
echo "PROGRESS:35"
|
||||||
|
|
||||||
|
# Syncing qSelf data on device to the Documents/qSelf folder on computer.
|
||||||
|
{ adb-sync --reverse $TENTACLES $OCTOPUS; } >> $LOG
|
||||||
|
catcher qSelf
|
||||||
|
line
|
||||||
|
echo "PROGRESS:40"
|
||||||
|
echo "Documents synced"
|
||||||
|
|
||||||
|
# Syncing music on computer to the music on phone.
|
||||||
|
adb-sync --delete $MAC_MUSIC $ANDROID_MUSIC >> $LOG
|
||||||
|
catcher Music
|
||||||
|
line
|
||||||
|
echo "PROGRESS:50"
|
||||||
|
echo "Music synced to phone"
|
||||||
|
|
||||||
|
# Syncing app, recovery and TWRP backups.
|
||||||
|
{ adb-sync --reverse --delete /sdcard/Backup/Apps/ $APPS; adb-sync --reverse /sdcard/.Ota/ $RECOVERY; adb-sync --reverse --delete /sdcard/TWRP/Backups/75fbabd4/ $MOBILE; adb-sync --reverse --delete /sdcard/Signal/Backups/ $SIGNAL; } >> $LOG
|
||||||
|
catcher Backups
|
||||||
|
line
|
||||||
|
echo "PROGRESS:70"
|
||||||
|
echo "Mobile backups synced"
|
||||||
|
|
||||||
|
# Syncing system backups and recovery to phone.
|
||||||
|
{ adb-sync --delete ~/Documents/Secure.sparsebundle /sdcard/Backup/System/; adb-sync --delete $RECOVERY /sdcard/Backup/Recovery/; } >> $LOG
|
||||||
|
catcher System
|
||||||
|
line
|
||||||
|
echo "PROGRESS:80"
|
||||||
|
echo "System backups synced"
|
||||||
|
|
||||||
|
# Deleting all synced media from phone.
|
||||||
|
{ adb shell rm -rf '/sdcard/Record/*.mp3'; adb shell rm -rf '/sdcard/Movies/*'; adb shell rm -rf '/sdcard/DJI/Camera/*'; adb shell rm -rf '/sdcard/DCIM/*'; adb shell rm -rf '/sdcard/DCIM/.thumbnails/*'; adb shell rm -rf '/sdcard/Documents/*'; adb shell rm -rf '/sdcard/Download/*' } >> $LOG
|
||||||
|
echo "PROGRESS:90"
|
||||||
|
echo "Cleanup completed"
|
||||||
|
|
||||||
|
# Notification that the sync is over.
|
||||||
|
printf "Syncing is complete. END" >> $LOG
|
||||||
|
echo "PROGRESS:100"
|
||||||
|
echo "NOTIFICATION:Syncing is complete"
|
||||||
|
|
||||||
|
echo "QUITAPP"
|
||||||
88
mobisync.sh
Executable file
88
mobisync.sh
Executable file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/bin/zsh
|
||||||
|
# This script syncs an Android with a Mac using adb and adb-sync.
|
||||||
|
# Version 11.2.1 (23 April 2020)
|
||||||
|
# Fixed System backup and Pictures folder download.
|
||||||
|
|
||||||
|
# Export paths for use if the script is turned into an app using Platypus.
|
||||||
|
export LC_ALL=en_US.UTF-8
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/$"
|
||||||
|
|
||||||
|
# Sets the local paths
|
||||||
|
export LOG=~/Projects/Programming/push/mobisync/mobisync-log.txt
|
||||||
|
export DOWNLOADS=~/Downloads/
|
||||||
|
export MOBILE=~/Mobile/
|
||||||
|
export SIGNAL=~/Mobile/Signal/
|
||||||
|
export TENTACLES=~/Documents/.tentacles/
|
||||||
|
export MUSIC=~/Music/
|
||||||
|
export SYSTEM=/Volumes/Archive\ 01/System/
|
||||||
|
export TRAVEL=~/Documents/Travel/
|
||||||
|
|
||||||
|
echo "NOTIFICATION: Sync is starting..." # Send a notification (with logo)
|
||||||
|
|
||||||
|
# Function: Reviews the last command for errors. Then prints update complete to log or shows error dialog. Takes section variable.
|
||||||
|
catcher () {
|
||||||
|
if [ "$?" = "0" ]; then
|
||||||
|
printf "$1 synced." >> $LOG # If no error, print sync complete to file.
|
||||||
|
echo "" >> $LOG # Add a line to file.
|
||||||
|
else # If error, show a dialog stating the section where the error occured.
|
||||||
|
echo "NOTIFICATION: '$1' sync failed."
|
||||||
|
printf "$1 failed to sync." >> $LOG # If error, print sync failed to file.
|
||||||
|
echo "" >> $LOG # Add a line to file.
|
||||||
|
ERROR=1 # Sets variable for error in script to 1.
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function: If there has been an error in the script open the log file.
|
||||||
|
verify () {
|
||||||
|
if [ $ERROR = 1 ]; then
|
||||||
|
open $LOG
|
||||||
|
echo "NOTIFICATION: Syncing completed with some errors."
|
||||||
|
else
|
||||||
|
echo "NOTIFICATION: Syncing is complete"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function: Creates a horizontal line in the text file.
|
||||||
|
line () {
|
||||||
|
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - >> $LOG # Prints line
|
||||||
|
}
|
||||||
|
|
||||||
|
# Writes the header for the log file: Program, Version number, Date and Line.
|
||||||
|
{ echo "mobisync 11.2"; echo "Log: " `date`; line; } > $LOG
|
||||||
|
|
||||||
|
# Syncing images and video on device to the Downloads folder and sync the wallpaper on computer.
|
||||||
|
{ adb shell find "/sdcard/DCIM/Camera/" -iname "*.mp4" | tr -d '\015' | while read line; do adb pull "$line" $DOWNLOADS; done; adb shell find "/sdcard/DCIM/Camera/" -iname "*.jpg" | tr -d '\015' | while read line; do adb pull "$line" $DOWNLOADS; done; adb shell find "/sdcard/DCIM/" -iname "*.jpg" | tr -d '\015' | while read line; do adb pull "$line" $DOWNLOADS; done; adb shell find "/sdcard/Pictures/" -iname "*.jpg" | tr -d '\015' | while read line; do adb pull "$line" $DOWNLOADS; done; } >> $LOG
|
||||||
|
catcher Images
|
||||||
|
line
|
||||||
|
|
||||||
|
# Syncing audio on device to the Downloads folder on computer and Music from the computer.
|
||||||
|
{ adb shell find "/sdcard/Recordings/" -iname "*.mp3" | tr -d '\015' | while read line; do adb pull "$line" $DOWNLOADS; done; adb-sync --delete $MUSIC "/sdcard/Music/" } >> $LOG
|
||||||
|
catcher Audio
|
||||||
|
line
|
||||||
|
|
||||||
|
# Syncing documents on device to the Downloads folder on computer.
|
||||||
|
{ adb-sync --reverse $DOWNLOADS "/sdcard/Documents/"; adb-sync --reverse $DOWNLOADS "/sdcard/Download/"; adb shell rm -rf '/sdcard/Documents/*'; adb-sync --delete $TRAVEL "/sdcard/Documents/"; } >> $LOG
|
||||||
|
catcher Documents
|
||||||
|
line
|
||||||
|
|
||||||
|
# Syncing qSelf data on device to the Documents/qSelf folder on computer.
|
||||||
|
{ adb-sync --reverse --delete "/sdcard/.tentacles/" $TENTACLES; } >> $LOG
|
||||||
|
catcher Tentacles
|
||||||
|
line
|
||||||
|
|
||||||
|
# Syncing app, recovery and TWRP backups.
|
||||||
|
{ adb-sync --reverse --delete "/sdcard/Mobile/" $MOBILE; adb-sync --reverse --delete "/sdcard/Signal/Backups/" $SIGNAL; adb-sync --delete $SYSTEM "/sdcard/System/"; } >> $LOG
|
||||||
|
catcher Backups
|
||||||
|
line
|
||||||
|
|
||||||
|
# Deleting all synced media from phone.
|
||||||
|
{ adb shell rm -rf '/sdcard/Recordings/*.mp3'; adb shell rm -rf '/sdcard/Movies/*'; adb shell rm -rf '/sdcard/DCIM/*'; adb shell rm -rf '/sdcard/Pictures/*'; adb shell rm -rf '/sdcard/Download/*'; adb shell rm -rf '/sdcard/Downloads/*' } >> $LOG
|
||||||
|
catcher Delete
|
||||||
|
line
|
||||||
|
|
||||||
|
# Notification that the sync is over.
|
||||||
|
printf "Syncing is complete. END" >> $LOG
|
||||||
|
verify $ERROR
|
||||||
|
|
||||||
|
echo "QUITAPP"
|
||||||
BIN
octopus-logo-2500x2500.afdesign
Normal file
BIN
octopus-logo-2500x2500.afdesign
Normal file
Binary file not shown.
BIN
octopus-logo-512x512.png
Normal file
BIN
octopus-logo-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
BIN
octopus-logo-purple-2500x2500.afphoto
Normal file
BIN
octopus-logo-purple-2500x2500.afphoto
Normal file
Binary file not shown.
BIN
octopus-logo-purple-2500x2500.png
Normal file
BIN
octopus-logo-purple-2500x2500.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
octopus-logo.icns
Normal file
BIN
octopus-logo.icns
Normal file
Binary file not shown.
14
readme.md
Normal file
14
readme.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# mobisync
|
||||||
|
|
||||||
|
This script syncs an Android with a Mac using adb and adb-sync. You will need to edit the script to meet your own needs.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**adb**
|
||||||
|
*brew install android-platform-tools*
|
||||||
|
|
||||||
|
**adb-sync**
|
||||||
|
*git clone https://github.com/google/adb-sync.git*
|
||||||
|
|
||||||
|
## Note
|
||||||
|
Some knowledge of adb and the Android file system is required. Runs on macOS.
|
||||||
Reference in a new issue