diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 425ce64..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/0.3.1.tar.gz -/0.3.2.tar.gz diff --git a/acd_cli-0.3.2-to-af929608f6279fca56e6c9ac6c48bd76d4ff1dcb.patch b/acd_cli-0.3.2-to-af929608f6279fca56e6c9ac6c48bd76d4ff1dcb.patch deleted file mode 100644 index 8e3583c..0000000 --- a/acd_cli-0.3.2-to-af929608f6279fca56e6c9ac6c48bd76d4ff1dcb.patch +++ /dev/null @@ -1,1347 +0,0 @@ -diff --git a/.travis.yml b/.travis.yml -index b8a50f9..493c3cf 100644 ---- a/.travis.yml -+++ b/.travis.yml -@@ -3,6 +3,8 @@ python: - - "3.2" - - "3.3" - - "3.4" -+ - "3.5" -+ - "3.6" - - addons: - apt: -diff --git a/README.rst b/README.rst -index 4b7b099..d7417d3 100644 ---- a/README.rst -+++ b/README.rst -@@ -98,10 +98,16 @@ Have a look at the `contributing guidelines `_. - Recent Changes - -------------- - -+0.3.3 (upcoming) -+~~~~~~~~~~~~~~~~ -+ -+* stat -+* Appspot OAuth proxy switch -+ - 0.3.2 - ~~~~~ - * added ``--remove-source-files`` argument to upload action --* added ``--times``` argument to download action for preservation of modification times -+* added ``--times`` argument to download action for preservation of modification times - * added streamed overwrite action - * fixed upload of directories containing broken symlinks - * disabled FUSE autosync by default -diff --git a/acd_cli.py b/acd_cli.py -index 52f4b4b..322963b 100755 ---- a/acd_cli.py -+++ b/acd_cli.py -@@ -69,11 +69,12 @@ for path in paths: - - def_conf = ConfigParser() - def_conf['download'] = dict(keep_corrupt=False, keep_incomplete=True) -+def_conf['upload'] = dict(timeout_wait=10) - conf = None - - # consts - --MIN_AUTOSYNC_INTERVAL = 60 -+MIN_SYNC_INTERVAL = 300 - MAX_LOG_SIZE = 10 * 2 ** 20 - MAX_LOG_FILES = 5 - -@@ -130,6 +131,14 @@ class CacheConsts(object): - def sync_node_list(full=False, to_file=None, from_file=None) -> 'Union[int, None]': - global cache - cp_ = cache.KeyValueStorage.get(CacheConsts.CHECKPOINT_KEY) if not full else None -+ lst = cache.KeyValueStorage.get(CacheConsts.LAST_SYNC_KEY) -+ lst = float(lst) if lst else 0 -+ -+ wt = min(lst + MIN_SYNC_INTERVAL - time.time(), MIN_SYNC_INTERVAL) -+ if lst and wt > 0: -+ print('Last sync was very recent or has invalid date. Waiting %im %is.' -+ % (wt / 60, wt % 60)) -+ time.sleep(wt) - - print('Getting changes', end='', flush=True) - -@@ -197,14 +206,57 @@ def old_sync() -> 'Union[int, None]': - files = acd_client.get_file_list() - files.extend(acd_client.get_trashed_files()) - except RequestError as e: -+ logger.error(e) - logger.critical('Sync failed.') -- print(e) - return ERROR_RETVAL - - cache.insert_nodes(files + folders, partial=False) - cache.KeyValueStorage['sync_date'] = time.time() - - -+def partial_sync(path: str, recursive: bool) -> 'Union[int|None]': -+ path = '/' + '/'.join(list(filter(bool, path.split('/')))) -+ n = cache.resolve(path, trash=False) -+ fid = None -+ -+ if n: -+ fid = n.id -+ n = acd_client.get_metadata(fid) -+ cache.insert_node(n) -+ else: -+ exc = None -+ try: -+ folder_chain = acd_client.resolve_folder_path(path) -+ except RequestError as e: -+ exc = e -+ -+ if not folder_chain or exc: -+ if exc: -+ logger.error(e) -+ logger.critical('Could not resolve path "%s".' % path) -+ return INVALID_ARG_RETVAL -+ -+ cache.insert_nodes(folder_chain) -+ fid = folder_chain[-1]['id'] -+ -+ try: -+ children = acd_client.list_children(fid) -+ if recursive: -+ recursive_insert(children) -+ else: -+ cache.insert_nodes(children) -+ except RequestError as e: -+ logger.error("Sync failed: %s" % e) -+ return ERROR_RETVAL -+ -+ -+def recursive_insert(nodes: 'List[dict]'): -+ cache.insert_nodes(nodes) -+ for n in nodes: -+ if n['kind'] == 'FOLDER': -+ recursive_insert(acd_client.list_children(n['id'])) -+ -+ - def autosync(interval: int, stop: Event = None): - """Periodically syncs the node cache each *interval* seconds. - -@@ -213,7 +265,7 @@ def autosync(interval: int, stop: Event = None): - if not interval: - return - -- interval = max(MIN_AUTOSYNC_INTERVAL, interval) -+ interval = max(MIN_SYNC_INTERVAL, interval) - while True: - if stop.is_set(): - break -@@ -233,6 +285,7 @@ RetryRetVal = namedtuple('RetryRetVal', ['ret_val', 'retry']) - STD_RETRY_RETVALS = [UL_DL_FAILED] - DL_RETRY_RETVALS = [UL_DL_FAILED, HASH_MISMATCH] - -+ - def retry_on(ret_vals: 'List[int]'): - """Retry decorator that sets the wrapped function's progress handler argument according to its - return value and wraps the return value in RetryRetVal object. -@@ -308,7 +361,7 @@ def upload_complete(node: dict, path: str, hash_: str, size_: int, rsf: bool) -> - - - def upload_timeout(parent_id: str, path: str, hash_: str, size_: int, rsf: bool) -> int: -- minutes = 10 -+ minutes = conf.getint('upload', 'timeout_wait') - while minutes > 0: - time.sleep(60) - minutes -= 1 -@@ -322,7 +375,7 @@ def upload_timeout(parent_id: str, path: str, hash_: str, size_: int, rsf: bool) - - - def overwrite_timeout(initial_node: dict, path: str, hash_: str, size_: int, rsf: bool) -> int: -- minutes = 10 -+ minutes = conf.getint('upload', 'timeout_wait') - while minutes > 0: - time.sleep(60) - minutes -= 1 -@@ -426,19 +479,23 @@ def traverse_ul_dir(dirs: list, directory: str, parent_id: str, overwr: bool, fo - curr_node = cache.get_node(r['id']) - except RequestError as e: - if e.status_code == 409: -- logger.error('Folder "%s" already exists. Please sync.' % short_nm) -+ logger.error('Folder "%s" already exists in %s [%s]. Error message: %s.' -+ 'You may need to sync.' -+ % (short_nm, parent.simple_name, parent_id, e)) - else: -- logger.error('Error creating remote folder "%s": %s.' % (short_nm, e)) -+ logger.error('Error creating remote folder "%s" in %s [%s]. Error message: %s.' -+ % (short_nm, parent.simple_name, parent_id, e)) - return ERR_CR_FOLDER - elif curr_node.is_file: -- logger.error('Cannot create remote folder "%s", ' -- 'because a file of the same name already exists.' % short_nm) -+ logger.error('Cannot create remote folder "%s" in %s [%s], ' -+ 'because a file of the same name already exists.' -+ % (short_nm, parent.simple_name, parent_id)) - return ERR_CR_FOLDER - - try: - entries = sorted(os.listdir(directory)) - except OSError as e: -- logger.error('Skipping directory %s because of an error.' % directory) -+ logger.error('Skipping directory %s because of an error: %s' % (directory, e)) - logger.info(e) - return ERROR_RETVAL - -@@ -460,7 +517,8 @@ def upload_file(path: str, parent_id: str, overwr: bool, force: bool, dedup: boo - nodes = cache.find_by_md5(hashing.hash_file(path)) - nodes = [n for n in cache.path_format(nodes)] - if len(nodes) > 0: -- logger.info('Skipping upload of duplicate file "%s". Location of duplicates: %s' % (short_nm, nodes)) -+ logger.info('Skipping upload of duplicate file "%s". Location of duplicates: %s' -+ % (short_nm, nodes)) - pg_handler.done() - if rsf: - return remove_file(path) -@@ -470,13 +528,17 @@ def upload_file(path: str, parent_id: str, overwr: bool, force: bool, dedup: boo - file_id = None - if conflicting_node: - if conflicting_node.name != short_nm: -- logger.error('File name "%s" collides with remote node "%s".' -- % (short_nm, conflicting_node.name)) -+ logger.error('Name collision in %s [%s]: ' -+ 'File name "%s" collides with existing node "%s".' -+ % (cache.get_node(parent_id).simple_name, parent_id, -+ short_nm, conflicting_node.simple_name)) - return NAME_COLLISION - - if conflicting_node.is_folder: -- logger.error('Name collision with existing folder ' -- 'in the same location: "%s".' % short_nm) -+ logger.error('Name collision in %s [%s]: ' -+ 'File name "%s" collides with existing folder "%s".' -+ % (cache.get_node(parent_id).simple_name, parent_id, -+ short_nm, conflicting_node.simple_name)) - return NAME_COLLISION - - file_id = conflicting_node.id -@@ -492,19 +554,22 @@ def upload_file(path: str, parent_id: str, overwr: bool, force: bool, dedup: boo - except RequestError as e: - if e.status_code == 409: # might happen if cache is outdated - if not dedup: -- logger.error('Uploading "%s" failed. Name collision with non-cached file. ' -- 'If you want to overwrite, please sync and try again.' % short_nm) -+ logger.error('Uploading "%s" failed. Name collision with non-cached file ' -+ 'in folder "%s" [%s]. If you want to overwrite, ' -+ 'please sync and try again. Error message: %s.' -+ % (short_nm, cache.get_node(parent_id).simple_name, parent_id, e)) - else: -- logger.error( -- 'Uploading "%s" failed. ' -- 'Name or hash collision with non-cached file.' % short_nm) -+ logger.error('Uploading "%s" failed. Name or hash collision' -+ 'in folder "%s" [%s] with non-cached file. Error message: %s.' -+ % (short_nm, cache.get_node(parent_id).simple_name, parent_id, e)) - logger.info(e) - # colliding node ID is returned in error message -> could be used to continue - return CACHE_ASYNC - elif e.status_code == 504 or e.status_code == 408: # proxy timeout / request timeout - return upload_timeout(parent_id, path, hasher.get_result(), local_size, rsf) - else: -- logger.error('Uploading "%s" failed. %s.' % (short_nm, str(e))) -+ logger.error('Uploading "%s" to "%s" [%s] failed. Error message: %s.' -+ % (short_nm, cache.get_node(parent_id).simple_name, parent_id, e)) - return UL_DL_FAILED - else: - return upload_complete(r, path, hasher.get_result(), local_size, rsf) -@@ -521,17 +586,16 @@ def upload_file(path: str, parent_id: str, overwr: bool, force: bool, dedup: boo - if not overwr and not force: - logger.info('Skipping upload of existing file "%s".' % short_nm) - pg_handler.done() -- -+ - if not rsf: - return 0 - - if not compare_sizes(os.path.getsize(path), conflicting_node.size, short_nm): - return remove_file(path) -- -+ - logger.info('Keeping "%s" because of remote size mismatch.' % path) - return 0 - -- - # ctime is checked because files can be overwritten by files with older mtime - if rmod < lmod or (rmod < lcre and conflicting_node.size != os.path.getsize(path)) \ - or force: -@@ -621,7 +685,7 @@ def create_dl_jobs(node_id: str, local_path: str, preserve_mtime: bool, rsf: boo - - flp = os.path.join(local_path, loc_name) - if os.path.isfile(flp): -- logger.info('Skipping download of existing file "%s"' % loc_name) -+ logger.info('Skipping download of existing file "%s".' % loc_name) - if os.path.getsize(flp) != node.size: - logger.info('Skipped file "%s" has different size than local file.' % loc_name) - return SIZE_MISMATCH -@@ -718,7 +782,11 @@ def no_autores_trash_action(func): - # actual actions - - def sync_action(args: argparse.Namespace): -- return sync_node_list(args.full, args.to_file, args.from_file) -+ ret = sync_node_list(args.full, args.to_file, args.from_file) -+ if cache.get_root_node() or args.to_file: -+ return ret -+ logger.error("Root node not found. Sync may have been incomplete.") -+ return ret if ret else 0 | ERROR_RETVAL - - - def old_sync_action(args: argparse.Namespace): -@@ -729,6 +797,14 @@ def old_sync_action(args: argparse.Namespace): - return r - - -+def partial_sync_action(args: argparse.Namespace): -+ print('Syncing...') -+ r = partial_sync(args.path, args.recursive) -+ if not r: -+ print('Done.') -+ return r -+ -+ - @nocache_action - @offline_action - def delete_everything_action(args: argparse.Namespace): -@@ -746,9 +822,10 @@ def delete_everything_action(args: argparse.Namespace): - print('Deleting directory failed.') - - -+@nocache_action - @offline_action - def clear_action(args: argparse.Namespace): -- if not cache.drop_all(): -+ if not db.NodeCache.remove_db_file(CACHE_PATH, SETTINGS_PATH): - return ERROR_RETVAL - - -@@ -976,7 +1053,7 @@ def restore_action(args: argparse.Namespace) -> int: - def resolve_action(args: argparse.Namespace) -> int: - node = cache.resolve(args.path) - if node: -- print(node) -+ print(node.id) - else: - return INVALID_ARG_RETVAL - -@@ -1085,7 +1162,8 @@ def mount_action(args: argparse.Namespace): - nothreads=args.single_threaded, - nonempty=args.nonempty, modules=args.modules, - umask=args.umask,gid=args.gid,uid=args.uid, -- allow_root=args.allow_root, allow_other=args.allow_other) -+ allow_root=args.allow_root, allow_other=args.allow_other, -+ volname=args.volname) - - - @offline_action -@@ -1120,6 +1198,9 @@ def resolve_remote_path_args(args: argparse.Namespace, attrs: list, incl_trash: - setattr(args, id_attr, v.id) - setattr(args, id_attr + '_path', val) - elif is_valid_id(val): -+ if not cache.get_node(val): -+ logger.critical('Cannot find node with ID "%s".' % val) -+ sys.exit(INVALID_ARG_RETVAL) - setattr(args, id_attr + '_path', cache.first_path(val)) - else: - logger.critical('Invalid ID format: "%s".' % val) -@@ -1295,7 +1376,7 @@ def get_parser() -> tuple: - vers_sp.set_defaults(func=print_version_action) - - sync_sp = subparsers.add_parser('sync', aliases=['s'], -- help='[+] refresh node list cache; fetches complete node list ' -+ help='[+] refresh node cache; fetches complete node list ' - 'if the cache is empty or incremental changes ' - 'if the cache is non-empty') - sync_sp.add_argument('--full', '-f', action='store_true', -@@ -1309,6 +1390,12 @@ def get_parser() -> tuple: - old_sync_sp = subparsers.add_parser('old-sync', add_help=False) - old_sync_sp.set_defaults(func=old_sync_action) - -+ psync_sp = subparsers.add_parser('psync', help='[+] only refresh the node cache for the ' -+ 'specified folder [does not include trash]') -+ psync_sp.add_argument('--recursive', '-r', action='store_true') -+ psync_sp.add_argument('path') -+ psync_sp.set_defaults(func=partial_sync_action) -+ - clear_sp = subparsers.add_parser('clear-cache', aliases=['cc'], - help='delete node cache file [offline operation]\n\n') - clear_sp.set_defaults(func=clear_action) -@@ -1507,6 +1594,7 @@ def get_parser() -> tuple: - fuse_sp.add_argument('--nlinks', '-n', action='store_true', help='calculate nlinks') - fuse_sp.add_argument('--interval', '-i', type=int, default=0, - help='sync every x seconds [turned off by default]') -+ fuse_sp.add_argument('--volname', '-vn', help='override volume name') - fuse_sp.add_argument('path') - fuse_sp.set_defaults(func=mount_action) - -@@ -1556,11 +1644,13 @@ def main(): - logger.info(msg) - - logger.info('Settings path is "%s".' % SETTINGS_PATH) -- -- conf = get_conf(SETTINGS_PATH, _SETTINGS_FILENAME, def_conf) -+ logger.info('Cache path is "%s".' % CACHE_PATH) - - global acd_client - global cache -+ global conf -+ -+ conf = get_conf(SETTINGS_PATH, _SETTINGS_FILENAME, def_conf) - - if args.func not in offline_actions: - try: -@@ -1576,7 +1666,7 @@ def main(): - raise - sys.exit(INIT_FAILED_RETVAL) - -- if args.func not in [sync_action, old_sync_action, clear_action]: -+ if args.func not in [sync_action, old_sync_action, partial_sync_action, clear_action]: - if not check_cache(): - sys.exit(INIT_FAILED_RETVAL) - pass -@@ -1600,7 +1690,7 @@ def main(): - - ret = args.func(args) - if not ret: -- sys.exit() -+ sys.exit(ret) - - trunc_ret = ret % 256 - if trunc_ret != ret: -diff --git a/acdcli/acd_fuse.py b/acdcli/acd_fuse.py -index 68e026c..2d30158 100644 ---- a/acdcli/acd_fuse.py -+++ b/acdcli/acd_fuse.py -@@ -45,6 +45,7 @@ except: - _SETTINGS_FILENAME = 'fuse.ini' - - _def_conf = configparser.ConfigParser() -+_def_conf['fs'] = dict(block_size=512) - _def_conf['read'] = dict(open_chunk_limit=10, timeout=5) - _def_conf['write'] = dict(buffer_size = 32, timeout=30) - -@@ -381,11 +382,12 @@ class ACDFuse(LoggingMixIn, Operations): - self.acd_client = kwargs['acd_client'] - autosync = kwargs['autosync'] - conf = kwargs['conf'] -+ self.conf = conf - -- self.rp = ReadProxy(self.acd_client, -+ self.rp = ReadProxy(self.acd_client, - conf.getint('read', 'open_chunk_limit'), conf.getint('read', 'timeout')) - """collection of files opened for reading""" -- self.wp = WriteProxy(self.acd_client, self.cache, -+ self.wp = WriteProxy(self.acd_client, self.cache, - conf.getint('write', 'buffer_size'), conf.getint('write', 'timeout')) - """collection of files opened for writing""" - try: -@@ -453,6 +455,8 @@ class ACDFuse(LoggingMixIn, Operations): - return dict(st_mode=stat.S_IFREG | 0o0666, - st_nlink=self.cache.num_parents(node.id) if self.nlinks else 1, - st_size=node.size, -+ st_blksize=self.conf.getint('fs', 'block_size'), -+ st_blocks=(node.size+511)//512, - **times) - - def read(self, path, length, offset, fh) -> bytes: -@@ -476,7 +480,7 @@ class ACDFuse(LoggingMixIn, Operations): - def statfs(self, path) -> dict: - """Gets some filesystem statistics as specified in :manpage:`stat(2)`.""" - -- bs = 512 * 1024 # no effect? -+ bs = self.conf.getint('fs', 'block_size') - return dict(f_bsize=bs, - f_frsize=bs, - f_blocks=self.total // bs, # total no of blocks -@@ -706,8 +710,11 @@ def mount(path: str, args: dict, **kwargs) -> 'Union[int, None]': - return 1 - - opts = dict(auto_cache=True, sync_read=True) -- if sys.platform == 'linux': -- opts['big_writes']=True -+ if sys.platform.startswith('linux'): -+ opts['big_writes'] = True -+ -+ if sys.platform != 'darwin' or kwargs['volname'] is None: -+ del kwargs['volname'] - - kwargs.update(opts) - -diff --git a/acdcli/api/__init__.py b/acdcli/api/__init__.py -index b3ceea1..5e2c76b 100644 ---- a/acdcli/api/__init__.py -+++ b/acdcli/api/__init__.py -@@ -54,7 +54,7 @@ A folder's JSON looks similar, but it lacks the ``contentProperties`` dictionary - - """ - --__version__ = '0.9.0' -+__version__ = '0.9.3' - - # monkey patch the user agent - try: -diff --git a/acdcli/api/client.py b/acdcli/api/client.py -index 9a2a27f..64fd00f 100644 ---- a/acdcli/api/client.py -+++ b/acdcli/api/client.py -@@ -32,7 +32,6 @@ _def_conf['proxies'] = dict() - class ACDClient(AccountMixin, ContentMixin, MetadataMixin, TrashMixin): - """Provides a client to the Amazon Cloud Drive RESTful interface.""" - -- - def __init__(self, cache_path='', settings_path=''): - """Initializes OAuth and endpoints.""" - -diff --git a/acdcli/api/content.py b/acdcli/api/content.py -index ae5d9fd..901ab43 100644 ---- a/acdcli/api/content.py -+++ b/acdcli/api/content.py -@@ -56,6 +56,7 @@ def _stream_is_empty(stream) -> bool: - 'not contain at least one byte.') - return False - -+ - class ContentMixin(object): - """Implements content portion of the ACD API.""" - -@@ -125,11 +126,8 @@ class ContentMixin(object): - mime_type = _get_mimetype(basename) - f = _tee_open(file_name, callbacks=read_callbacks) - -- # basename is ignored - m = MultipartEncoder(fields=OrderedDict([('metadata', json.dumps(metadata)), -- ( -- 'content', -- (quote_plus(basename), f, mime_type))])) -+ ('content', ('filename', f, mime_type))])) - - ok_codes = [http.CREATED] - r = self.BOReq.post(self.content_url + 'nodes', params=params, data=m, -@@ -305,6 +303,12 @@ class ContentMixin(object): - - dl_chunk_sz = self._conf.getint('transfer', 'dl_chunk_size') - -+ seekable = True -+ try: -+ file.tell() -+ except OSError: -+ seekable = False -+ - retries = 0 - while chunk_start < length: - chunk_end = chunk_start + dl_chunk_sz - 1 -@@ -341,7 +345,10 @@ class ContentMixin(object): - curr_ln += len(chunk) - finally: - r.close() -- chunk_start = file.tell() -+ if seekable: -+ chunk_start = file.tell() -+ else: -+ chunk_start = chunk_start + curr_ln - - retries = 0 - -diff --git a/acdcli/api/metadata.py b/acdcli/api/metadata.py -index fdfc34e..494d6fc 100644 ---- a/acdcli/api/metadata.py -+++ b/acdcli/api/metadata.py -@@ -159,10 +159,8 @@ class MetadataMixin(object): - raise RequestError(r.status_code, r.text) - return r.json() - -- def get_root_id(self) -> str: -- """Gets the ID of the root node -- -- :returns: the topmost folder id""" -+ def get_root_node(self) -> dict: -+ """Gets the root node metadata""" - - params = {'filters': 'isRoot:true'} - r = self.BOReq.get(self.metadata_url + 'nodes', params=params) -@@ -172,13 +170,26 @@ class MetadataMixin(object): - - data = r.json() - -- if 'id' in data['data'][0]: -- return data['data'][0]['id'] -+ return data['data'][0] -+ -+ def get_root_id(self) -> str: -+ """Gets the ID of the root node -+ -+ :returns: the topmost folder id""" -+ -+ r = self.get_root_node() -+ if 'id' in r['data'][0]: -+ return r['data'][0]['id'] - - def list_children(self, node_id: str) -> list: - l = self.BOReq.paginated_get(self.metadata_url + 'nodes/' + node_id + '/children') - return l - -+ def list_child_folders(self, node_id: str) -> list: -+ l = self.BOReq.paginated_get(self.metadata_url + 'nodes/' + node_id + '/children', -+ params={'filters': 'kind:FOLDER'}) -+ return l -+ - def add_child(self, parent_id: str, child_id: str) -> dict: - """Adds node with ID *child_id* to folder with ID *parent_id*. - -@@ -274,3 +285,26 @@ class MetadataMixin(object): - % (self.metadata_url, node_id, owner_id, key), acc_codes=ok_codes) - if r.status_code not in ok_codes: - raise RequestError(r.status_code, r.text) -+ -+ def resolve_folder_path(self, path: str) -> 'List[dict]': -+ """Resolves a non-trash folder path to a list of folder entries.""" -+ segments = list(filter(bool, path.split('/'))) -+ folder_chain = [] -+ -+ root = self.get_root_node() -+ folder_chain.append(root) -+ -+ if not segments: -+ return folder_chain -+ -+ for i, segment in enumerate(segments): -+ dir_entries = self.list_child_folders(folder_chain[-1]['id']) -+ -+ for ent in dir_entries: -+ if ent['status'] == 'AVAILABLE' and ent['name'] == segment: -+ folder_chain.append(ent) -+ break -+ if len(folder_chain) != i + 2: -+ return [] -+ -+ return folder_chain -diff --git a/acdcli/api/oauth.py b/acdcli/api/oauth.py -index 049f4fd..46bf9d9 100644 ---- a/acdcli/api/oauth.py -+++ b/acdcli/api/oauth.py -@@ -29,7 +29,7 @@ def create_handler(path: str): - - - class OAuthHandler(AuthBase): -- OAUTH_DATA_FILE = 'oauth_data' -+ OAUTH_DATA_FILE = 'oauth.json' - - class KEYS(object): - EXP_IN = 'expires_in' -@@ -160,7 +160,7 @@ class OAuthHandler(AuthBase): - - - class AppspotOAuthHandler(OAuthHandler): -- APPSPOT_URL = 'https://tensile-runway-92512.appspot.com/' -+ APPSPOT_URL = 'https://acd-api-oa.appspot.com/' - - def __init__(self, path): - super().__init__(path) -diff --git a/acdcli/cache/cursors.py b/acdcli/cache/cursors.py -old mode 100644 -new mode 100755 -index 7d5d16e..f9db7f2 ---- a/acdcli/cache/cursors.py -+++ b/acdcli/cache/cursors.py -@@ -21,5 +21,8 @@ class mod_cursor(object): - return self.cursor - - def __exit__(self, exc_type, exc_val, exc_tb): -- self.conn.commit() -+ if exc_type is None: -+ self.conn.commit() -+ else: -+ self.conn.rollback() - self.cursor.close() -diff --git a/acdcli/cache/db.py b/acdcli/cache/db.py -index 994a925..efdac5e 100644 ---- a/acdcli/cache/db.py -+++ b/acdcli/cache/db.py -@@ -3,6 +3,7 @@ import logging - import os - import re - import sqlite3 -+import sys - from threading import local - - from acdcli.utils.conf import get_conf -@@ -22,7 +23,7 @@ _SETTINGS_FILENAME = 'cache.ini' - - _def_conf = configparser.ConfigParser() - _def_conf['sqlite'] = dict(filename='nodes.db', busy_timeout=30000, journal_mode='wal') --_def_conf['blacklist'] = dict(folders= []) -+_def_conf['blacklist'] = dict(folders=[]) - - - -@@ -57,7 +58,10 @@ class NodeCache(SchemaMixin, QueryMixin, SyncMixin, FormatterMixin): - self.tl = local() - - self.integrity_check(check) -- self.init() -+ try: -+ self.init() -+ except sqlite3.DatabaseError as e: -+ raise IntegrityError(e) - - self._conn.create_function('REGEXP', _regex_match.__code__.co_argcount, _regex_match) - -@@ -75,7 +79,8 @@ class NodeCache(SchemaMixin, QueryMixin, SyncMixin, FormatterMixin): - self.root_id = first_id - - self._execute_pragma('busy_timeout', self._conf['sqlite']['busy_timeout']) -- self._execute_pragma('journal_mode', self._conf['sqlite']['journal_mode']) -+ if sys.version_info[:3] != (3, 6, 0): -+ self._execute_pragma('journal_mode', self._conf['sqlite']['journal_mode']) - - @property - def _conn(self) -> sqlite3.Connection: -@@ -91,22 +96,25 @@ class NodeCache(SchemaMixin, QueryMixin, SyncMixin, FormatterMixin): - logger.debug('Set %s to %s. Result: %s.' % (key, value, r[0])) - return r[0] - -- def remove_db_file(self) -> bool: -+ @classmethod -+ def remove_db_file(cls, cache_path='', settings_path='') -> bool: - """Removes database file.""" -- self._conn.close() - - import os - import random - import string - import tempfile - -+ conf = get_conf(settings_path, _SETTINGS_FILENAME, _def_conf) -+ db_path = os.path.join(cache_path, conf['sqlite']['filename']) -+ - tmp_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(16)) - tmp_name = os.path.join(tempfile.gettempdir(), tmp_name) - - try: -- os.rename(self.db_path, tmp_name) -+ os.rename(db_path, tmp_name) - except OSError: -- logger.critical('Error renaming/removing database file "%s".' % self.db_path) -+ logger.critical('Error renaming/removing database file "%s".' % db_path) - return False - else: - try: -diff --git a/acdcli/cache/query.py b/acdcli/cache/query.py -index 44ea869..469b953 100644 ---- a/acdcli/cache/query.py -+++ b/acdcli/cache/query.py -@@ -13,6 +13,12 @@ def datetime_from_string(dt: str) -> datetime: - return dt - - -+CONFLICTING_NODE_SQL = """SELECT n.*, f.* FROM nodes n -+ JOIN parentage p ON n.id = p.child -+ LEFT OUTER JOIN files f ON n.id = f.id -+ WHERE p.parent = (?) AND LOWER(name) = (?) AND status = 'AVAILABLE' -+ ORDER BY n.name""" -+ - CHILDREN_SQL = """SELECT n.*, f.* FROM nodes n - JOIN parentage p ON n.id = p.child - LEFT OUTER JOIN files f ON n.id = f.id -@@ -146,10 +152,11 @@ class QueryMixin(object): - - def get_conflicting_node(self, name: str, parent_id: str): - """Finds conflicting node in folder specified by *parent_id*, if one exists.""" -- folders, files = self.list_children(parent_id) -- for n in folders + files: -- if n.is_available and n.name.lower() == name.lower(): -- return n -+ with cursor(self._conn) as c: -+ c.execute(CONFLICTING_NODE_SQL, [parent_id, name.lower()]) -+ r = c.fetchone() -+ if r: -+ return Node(r) - - def resolve(self, path: str, trash=False) -> 'Union[Node|None]': - segments = list(filter(bool, path.split('/'))) -@@ -176,9 +183,9 @@ class QueryMixin(object): - if not trash: - return - if r2: -- logger.debug('None-unique trash name "%s" in %s.' %(segment, parent)) -+ logger.debug('None-unique trash name "%s" in %s.' % (segment, parent)) - return -- if i + 1 == segments.__len__(): -+ if i + 1 == len(segments): - return r - if r.is_folder: - parent = r.id -diff --git a/acdcli/cache/schema.py b/acdcli/cache/schema.py -old mode 100644 -new mode 100755 -index d5e138b..92a5512 ---- a/acdcli/cache/schema.py -+++ b/acdcli/cache/schema.py -@@ -52,14 +52,25 @@ _CREATION_SCRIPT = """ - FOREIGN KEY(child) REFERENCES nodes (id) - ); - -+ CREATE INDEX ix_parentage_child ON parentage(child); - CREATE INDEX ix_nodes_names ON nodes(name); -- PRAGMA user_version = 2; -+ PRAGMA user_version = 3; - """ - - _GEN_DROP_TABLES_SQL = \ - 'SELECT "DROP TABLE " || name || ";" FROM sqlite_master WHERE type == "table"' - -+_migrations = [] -+"""list of all schema migrations""" - -+ -+def _migration(func): -+ """scheme migration annotation; must be used in correct order""" -+ _migrations.append(func) -+ return func -+ -+ -+@_migration - def _0_to_1(conn): - conn.executescript( - 'ALTER TABLE nodes ADD updated DATETIME;' -@@ -69,6 +80,7 @@ def _0_to_1(conn): - conn.commit() - - -+@_migration - def _1_to_2(conn): - conn.executescript( - 'DROP TABLE IF EXISTS folders;' -@@ -79,12 +91,18 @@ def _1_to_2(conn): - conn.commit() - - --_migrations = [_0_to_1, _1_to_2] --"""list of all migrations from index -> index+1""" -+@_migration -+def _2_to_3(conn): -+ conn.executescript( -+ 'CREATE INDEX IF NOT EXISTS ix_parentage_child ON parentage(child);' -+ 'REINDEX;' -+ 'PRAGMA user_version = 3;' -+ ) -+ conn.commit() - - - class SchemaMixin(object): -- _DB_SCHEMA_VER = 2 -+ _DB_SCHEMA_VER = 3 - - def init(self): - try: -diff --git a/acdcli/utils/conf.py b/acdcli/utils/conf.py -index 9ebf249..b11034a 100644 ---- a/acdcli/utils/conf.py -+++ b/acdcli/utils/conf.py -@@ -14,7 +14,7 @@ def get_conf(path, filename, default_conf: configparser.ConfigParser) \ - try: - with open(conffn) as cf: - conf.read_file(cf) -- except OSError: -+ except (OSError, IOError): - pass - - logger.debug('configuration resulting from merging default and %s: %s' % (filename, -diff --git a/docs/FAQ.rst b/docs/FAQ.rst -index 7bccc88..4ad97c4 100644 ---- a/docs/FAQ.rst -+++ b/docs/FAQ.rst -@@ -34,6 +34,9 @@ If the sync times out, consider increasing the idle timeout (refer to the - You may also want to try the deprecated (and undocumented) synchronization method ``acd_cli old-sync`` - if you happen to have only up to a few thousand files and folders in total. - -+If you do not need to synchronize your full Drive hierarchy, consider running a partial sync -+(``psync``). -+ - How Do I Pass a Node ID Starting with ``-`` (dash/minus/hyphen)? - ---------------------------------------------------------------- - -diff --git a/docs/FUSE.rst b/docs/FUSE.rst -index c79dde9..606c88e 100644 ---- a/docs/FUSE.rst -+++ b/docs/FUSE.rst -@@ -86,6 +86,7 @@ may be used, e.g. ``--modules="iconv,to_code=CHARSET"``. - --single-threaded, -st disallow multi-threaded FUSE operations - --uid UID override the user ID (defaults to the user's uid) - --umask UMASK override the standard permission bits -+--volname VN, -vn VN set the volume name to VN (Mac OS) - - Automatic Remount - ~~~~~~~~~~~~~~~~~ -@@ -106,7 +107,7 @@ For this to work, an executable shell script /usr/bin/acdmount must be created - - #!/bin/bash - -- acd_cli mount -nl $1 -+ acd_cli -nl mount $1 - - Library Path - ~~~~~~~~~~~~ -diff --git a/docs/authorization.rst b/docs/authorization.rst -index f032761..b2a97f9 100644 ---- a/docs/authorization.rst -+++ b/docs/authorization.rst -@@ -2,30 +2,43 @@ Authorization - ------------- - - Before you can use the program, you will have to complete the OAuth procedure with Amazon. --It is necessary to have a Web browser installed in order to do so. --There is a fast and simple way and a secure way. -+The initially obtained OAuth credentials can subsequently be refreshed automatically when -+necessary, which is at most once an hour. -+ -+It is necessary to have a (preferrably graphical) Web browser installed to complete the procedure. -+You may use another computer for this than the one acd\_cli will run on eventually. -+ -+If you are a new user, your only option is to use the Appspot authentication method -+which relays your OAuth tokens through a small Google Compute Engine app. -+If you have a security profile which was whitelisted for Amazon Drive access (prior to fall 2016), -+please skip to the Security Profile section. - - Simple (Appspot) - ++++++++++++++++ - -+This authorization method was created to remove the initial barrier for most casual users. It will -+forward your authentication data through an external computing platform service (Google App -+Engine) and may be less secure than using your own security profile. Use it at your own risk. -+ -+You may view the source code of the Appspot app that is used to handle the server part -+of the OAuth procedure at https://acd-api-oa.appspot.com/src. -+ - You will not have to prepare anything to initiate this authorization method, just - run, for example, ``acd_cli init``. - - A browser (tab) will open and you will be asked to log into your Amazon account --or grant access for 'acd\_cli\_oa'. -+or grant access for 'acd-api'. - Signing in or clicking on 'Continue' will download a JSON file named ``oauth_data``, which must be - placed in the cache directory displayed on screen (e.g. ``/home//.cache/acd_cli``). - --You may view the source code of the Appspot app that is used to handle the server part --of the OAuth procedure at https://tensile-runway-92512.appspot.com/src. -- - Advanced Users (Security Profile) - +++++++++++++++++++++++++++++++++ - --You must create a security profile and have it whitelisted. Have a look at Amazon's -+You must have a security profile and have it whitelisted, as described in Amazon's - `ACD getting started guide - `_. --Select all permissions for your security profile and add a redirect URL to ``http://localhost``. -+The security profile must be whitelisted for read and write aceess and have a redirect -+URL set for ``http://localhost``. - - Put your own security profile data in a file called ``client_data`` in the cache directory - and have it adhere to the following form. -@@ -49,3 +62,19 @@ Changing Authorization Methods - - If you want to change between authorization methods, go to your cache path (it is stated in the - output of ``acd_cli -v init``) and delete the file ``oauth_data`` and, if it exists, ``client_data``. -+ -+Copying Credentials -++++++++++++++++++++ -+ -+The same OAuth credentials may be used on multiple user accounts and multiple machines without a -+problem. To copy them, first look up acd\_cli's source and destination cache path like -+mentioned in the section above. Find the file/s ``oauth_data`` and possibly ``client_data`` in the -+source path and just copy it/them to the destination path. -+ -+Accessing multiple Amazon accounts -+++++++++++++++++++++++++++++++++++ -+ -+It is possible to use the cache path environment variable to set up an additional cache that is -+linked to a different Amazon account by OAuth credentials. Please see the -+:doc:`setup section ` on environment variables. -+ -diff --git a/docs/configuration.rst b/docs/configuration.rst -index 78c5a40..4d8658b 100644 ---- a/docs/configuration.rst -+++ b/docs/configuration.rst -@@ -5,6 +5,22 @@ Some module constants may be set in INI-style configuration files. If you want t - the defaults as described below, create a plain text file for the module using the section heading - as the file name in the settings directory. - -+acd\_cli.ini -+------------ -+ -+:: -+ -+ [download] -+ ;do not delete corrupt files -+ keep_corrupt = False -+ -+ ;do not delete partially downloaded files -+ keep_incomplete = True -+ -+ [upload] -+ ;waiting time for timed-out uploads/overwrittes to appear remotely [minutes] -+ timeout_wait = 10 -+ - acd\_client.ini - --------------- - -@@ -63,6 +79,9 @@ fuse.ini - -------- - - :: -+ [fs] -+ ;block size used for size info -+ block_size = 512 - - [read] - ;maximal number of simultaneously opened chunks per file -diff --git a/docs/contributors.rst b/docs/contributors.rst -index ce1944a..89cd829 100644 ---- a/docs/contributors.rst -+++ b/docs/contributors.rst -@@ -23,6 +23,8 @@ Thanks to - - - `memoz `_ for amending proxy documentation - -+- `gerph `_ for making file searches faster, particularly on large repositories -+ - Also thanks to - - - `fibersnet `_ for pointing out a possible deadlock in ACDFuse. -diff --git a/docs/find.rst b/docs/find.rst -index 73eccb5..02a3b5e 100644 ---- a/docs/find.rst -+++ b/docs/find.rst -@@ -7,7 +7,7 @@ find - ---- - - The find action will perform a case-insensitive search for files and folders that include the --name or name segment given as argument, so e.g. ``acdcli find foo`` will find "foo" , "Foobar", etc. -+name or name segment given as argument, so e.g. ``acdcli find foo`` will find "foo", "Foobar", etc. - - find-md5 - -------- -diff --git a/docs/index.rst b/docs/index.rst -index 43ce9d6..5f29c0e 100644 ---- a/docs/index.rst -+++ b/docs/index.rst -@@ -14,6 +14,7 @@ Contents: - authorization - usage - configuration -+ sync - transfer - find - FUSE -diff --git a/docs/setup.rst b/docs/setup.rst -index 4020c74..41ee97d 100644 ---- a/docs/setup.rst -+++ b/docs/setup.rst -@@ -8,8 +8,8 @@ Check which Python 3 version is installed on your system, e.g. by running - - If it is Python 3.2.3, 3.3.0 or 3.3.1, you need to upgrade to a higher minor version. - --You may now proceed to install using PIP, your Arch package manager or build Debian/RedHat --packages. -+You may now proceed to install using PIP, your package manager if you are using -+Arch Linux/Devuan/Fedora or build Debian/RedHat packages using fpm. - - Installation with PIP - --------------------- -@@ -33,14 +33,15 @@ The recommended and most up-to-date way is to directly install the master branch - - pip3 install --upgrade git+https://github.com/yadayada/acd_cli.git - --The easiest way is to directly install from PyPI. -+Or use the usual installation method by specifying the PyPI package name. This may not work -+flawlessly on Windows systems. - :: - - pip3 install --upgrade --pre acdcli - - --PIP Errors --~~~~~~~~~~ -+PIP Errors on Debian -+~~~~~~~~~~~~~~~~~~~~ - - A version incompatibility may arise with PIP when upgrading the requests package. - PIP will throw the following error: -@@ -57,19 +58,31 @@ Run these commands to fix it: - This will remove the distribution's pip3 package and replace it with a version that is compatible - with the newer requests package. - --Installation on Arch/Debian/RedHat -+Installation on Arch/Devuan/Fedora - ---------------------------------- - - Arch Linux - ~~~~~~~~~~ - - There are two packages for Arch Linux in the AUR, --`acd_cli-git `_, which is linked to the -+`acd_cli-git `_, which is linked to the - master branch of the GitHub repository, and - `acd_cli `_, which is linked to the PyPI release. - -+Devuan -+~~~~~~ -+ -+The Devuan package is called "python3-acdcli" and may be installed as usual -+(by running `apt-get install python3-acdcli` as superuser). -+ -+Fedora -+~~~~~~ -+ -+An official `rpm package `_ exists -+that may be installed. -+ - Building deb/rpm packages --~~~~~~~~~~~~~~~~~~~~~~~~~ -+------------------------- - - You will need to have `fpm `_ installed to build packages. - -@@ -78,7 +91,6 @@ There is a Makefile in the assets directory that includes commands to build Debi - requests-toolbelt package. - fpm may also be able to build packages for other distributions or operating systems. - -- - Environment Variables - --------------------- - -diff --git a/docs/sync.rst b/docs/sync.rst -new file mode 100644 -index 0000000..dd24317 ---- /dev/null -+++ b/docs/sync.rst -@@ -0,0 +1,40 @@ -+Syncing -+======= -+ -+**acd\_cli** keeps a local cache of node metadata to reduce latency. Syncing simply -+means updating the local cache with current data from Amazon Drive. -+[An Amazon Drive `node` may be file or folder.] -+ -+Regular syncing -+--------------- -+ -+Regular syncing ``acd_cli sync`` should be the preferred method to update the metadata for -+your whole Drive account. When invoked for the first time, it will get a complete list of -+the file and folder metadata. For later uses, it will utilize the saved checkpoint from the -+last sync to only fetch the metadata that has changed since then. -+ -+The ``--full`` (``-f``) flag forces the cache to be cleared before syncing, resulting in -+a non-incremental, full sync. -+ -+Sync changesets may also be written to or inserted from a file. -+ -+Incomplete sync -++++++++++++++++ -+ -+For large syncsets, for instance when doing a full sync, you may get the error message -+"Root node not found. Sync may have been incomplete." Please try to resume the sync process -+later, omitting the ``--full`` flag if you had specified it prior. -+ -+Partial syncing -+--------------- -+ -+Partial syncing may be a quick-and-dirty way to synchronize the metadata of a single directory -+with a smallish number of files and folders. E.g. ``acd_cli psync /`` will non-recursively fetch -+the metadata for the root folder. -+ -+The ``--recursive`` (``-r``) flag will also descend into the specified folder's subfolders. -+It is not advisible to use this flag for folders with many subfolders -+ -+The partial sync action will need to fetch node metadata in batches of 200. T -+Please be aware that when using regular and partial syncing alternatingly, your metadata -+may be in an inconsistent state. -diff --git a/docs/transfer.rst b/docs/transfer.rst -index 6a5d88d..b1e7c49 100644 ---- a/docs/transfer.rst -+++ b/docs/transfer.rst -@@ -80,8 +80,8 @@ Abort/Resume - Incomplete file downloads will be resumed automatically. Aborted file uploads are not resumable - at the moment. - -- Folder or directory hierarchies that were created for a transfer do not need to be recreated when -- resuming a transfer. -+ Folder or directory hierarchies that were created for a transfer do not need to be recreated -+ when resuming a transfer. - - Retry - -@@ -101,12 +101,17 @@ Remove Source Files - - #. if the upload succeeds - #. if deduplication is enabled and at least one duplicate is found -- #. if a file of the same name is present in the remote upload path but the file is not to bei -+ #. if a file of the same name is present in the remote upload path but the file is not to be - overwritten (deletion then only occurs if the file sizes match) - - Deduplication - -- Server-side deduplication prevents completely uploaded files from being saved as a node if another -- file with the same MD5 checksum already exists. -+ Server-side deduplication prevents completely uploaded files from being saved as a node if -+ another file with the same MD5 checksum already exists. - acd\_cli can prevent uploading duplicates by checking local files' sizes and MD5s. - Empty files are never regarded duplicates. -+ -+Progress indicator -+ -+ To suppress the progress indicator from being displayed on standard output, use the ``--quiet`` -+ flag. -diff --git a/docs/usage.rst b/docs/usage.rst -index 841a04c..2e3dc0a 100644 ---- a/docs/usage.rst -+++ b/docs/usage.rst -@@ -3,13 +3,16 @@ Usage - - acd_cli may be invoked as ``acd_cli`` or ``acdcli``. - --Most actions need the node cache to be initialized and up-to-date, so please run a sync. --A sync will fetch the changes since the last sync or the full node list if the cache is empty. -+Most actions need the node cache to be initialized and up-to-date, so please run a sync. An ordinary -+sync will fetch the changes since the last sync or the full node list if the cache is empty. -+Partially syncing will only fetch the active contents of one folder, optionally recursively. - - The following actions are built in --:: - -- sync (s) refresh node list cache; necessary for many actions -+.. code-block:: none -+ -+ sync (s) refresh node cache; prerequisite for many actions -+ psync only refresh the contents of the specified folder - clear-cache (cc) clear node cache [offline operation] - - tree (t) print directory tree [offline operation] -@@ -49,8 +52,9 @@ arguments of an action and their order can be printed by calling ``acd_cli [acti - Most node arguments may be specified as a 22 character ID or a UNIX-style path. - Trashed nodes' paths might not be able to be resolved correctly; use their ID instead. - --There are more detailed instructions for :doc:`file transfer actions `, --:doc:`find actions ` and :doc:`FUSE documentation `. -+There are more detailed instructions for :doc:`sycing `, -+:doc:`file transfer actions `, -+:doc:`find actions ` and the :doc:`FUSE module `. - - Logs will automatically be saved into the cache directory. - -@@ -102,4 +106,4 @@ error deleting source file 4096 - If multiple errors occur, their respective flag values will be compounded into the exit - status value by a binary OR operation. Because exit status values may not be larger than 255, - flags 256 and above cannot be returned via exit status. --A warning message will be displayed at the end of execution if those errors occured. -+A warning message will be displayed at the end of execution if those errors occurred. -diff --git a/setup.py b/setup.py -index 97d2a0f..6f66faf 100644 ---- a/setup.py -+++ b/setup.py -@@ -1,5 +1,6 @@ - import os - import re -+import sys - from setuptools import setup, find_packages - from distutils.version import StrictVersion - import acdcli -@@ -15,8 +16,10 @@ repl = ('`([^`]*?) <(docs/)?(.*?)\.rst>`_', - version = acdcli.__version__ - StrictVersion(version) - -+requests_py32 = ',<2.11.0' if sys.version_info[0:2] == (3, 2) else '' -+ - dependencies = ['appdirs', 'colorama', 'fusepy', 'python_dateutil', -- 'requests>=2.1.0,!=2.9.0', 'requests_toolbelt!=0.5.0'] -+ 'requests>=2.1.0,!=2.9.0,!=2.12.0%s' % requests_py32, 'requests_toolbelt!=0.5.0'] - doc_dependencies = ['sphinx_paramlinks'] - test_dependencies = ['httpretty<0.8.11', 'mock'] - -@@ -53,6 +56,7 @@ setup( - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', -+ 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3 :: Only', - 'Development Status :: 4 - Beta', - 'Topic :: System :: Archiving :: Backup', -diff --git a/tests/dummy_files/oauth_data b/tests/dummy_files/oauth.json -similarity index 100% -rename from tests/dummy_files/oauth_data -rename to tests/dummy_files/oauth.json -diff --git a/tests/test_actions.py b/tests/test_actions.py -index e8ea767..96f3ac2 100644 ---- a/tests/test_actions.py -+++ b/tests/test_actions.py -@@ -36,7 +36,7 @@ class ActionTestCase(unittest.TestCase): - self.cache = db.NodeCache(cache_path) - - def tearDown(self): -- self.cache.remove_db_file() -+ db.NodeCache.remove_db_file(cache_path) - - # tests - -diff --git a/tests/test_api.py b/tests/test_api.py -index c788897..477c37b 100644 ---- a/tests/test_api.py -+++ b/tests/test_api.py -@@ -92,7 +92,8 @@ class APITestCase(unittest.TestCase): - '"nodes": [ {"kind": "FILE", "status": "TRASH"} ], ' - '"statusCode": 200}\n' - '{"end": true}') -- changesets = [c for c in self.acd.get_changes()] -+ tmp = self.acd.get_changes() -+ changesets = [c for c in self.acd._iter_changes_lines(tmp)] - self.assertEqual(len(changesets), 1) - changeset = changesets[0] - self.assertEqual(len(changeset.nodes), 1) -@@ -105,7 +106,8 @@ class APITestCase(unittest.TestCase): - httpretty.register_uri(httpretty.POST, self.acd.metadata_url + 'changes', - body='{"checkpoint": "foo", "reset": true, "nodes": [], ' - '"statusCode": 200}\n') -- changesets = [c for c in self.acd.get_changes()] -+ tmp = self.acd.get_changes() -+ changesets = [c for c in self.acd._iter_changes_lines(tmp)] - self.assertEqual(len(changesets), 1) - changeset = changesets[0] - self.assertEqual(len(changeset.nodes), 0) -@@ -118,7 +120,8 @@ class APITestCase(unittest.TestCase): - httpretty.register_uri(httpretty.POST, self.acd.metadata_url + 'changes', - body='{"checkpoint": }') - with self.assertRaises(RequestError): -- [cs for cs in self.acd.get_changes()] -+ tmp = self.acd.get_changes() -+ [cs for cs in self.acd._iter_changes_lines(tmp)] - - # - # oauth -@@ -140,7 +143,8 @@ class APITestCase(unittest.TestCase): - os.path.isfile = MagicMock() - with patch('builtins.open', mock_file, create=True): - with patch('os.fsync', MagicMock): -- h = oauth.AppspotOAuthHandler('') -+ with patch('os.rename', MagicMock): -+ h = oauth.AppspotOAuthHandler('') - - mock_file.assert_any_call(oauth.OAuthHandler.OAUTH_DATA_FILE) - self.assertIn(oauth.OAuthHandler.KEYS.EXP_TIME, h.oauth_data) -diff --git a/tests/test_cache.py b/tests/test_cache.py -index bba08dc..16178db 100644 ---- a/tests/test_cache.py -+++ b/tests/test_cache.py -@@ -12,8 +12,7 @@ class CacheTestCase(unittest.TestCase): - self.cache = db.NodeCache(self.path) - - def tearDown(self): -- self.cache.drop_all() -- self.cache.remove_db_file() -+ db.NodeCache.remove_db_file(self.path) - - def testEmpty(self): - self.assertEqual(self.cache.get_node_count(), 0) diff --git a/acd_cli.spec b/acd_cli.spec deleted file mode 100644 index 2b6cc6b..0000000 --- a/acd_cli.spec +++ /dev/null @@ -1,92 +0,0 @@ -%global commit cd4a9eea52f1740aa8de10d8c75ab2f6c17de52b -%global shortcommit %(c=%{commit}; echo ${c:0:7}) - - -Name: acd_cli -Version: 0.3.2 -Release: 6.20170530.git%{shortcommit}%{?dist} -Summary: A command line interface and FUSE filesystem for Amazon Cloud Drive - -License: GPLv2+ -URL: https://github.com/yadayada/acd_cli -Source0: https://github.com/yadayada/acd_cli/archive/%{version}.tar.gz - -Patch0: acd_cli-0.3.2-to-af929608f6279fca56e6c9ac6c48bd76d4ff1dcb.patch - -BuildArch: noarch -BuildRequires: python3-devel - -# in order from https://acd-cli.readthedocs.org/en/latest/setup.html#python-packages -Requires: python3-appdirs -Requires: python3-colorama -Requires: python3-dateutil -Requires: python3-requests > 2.1.0 -Requires: python3-requests-toolbelt -Requires: python3-sqlalchemy -Recommends: python3-fuse - -%description -acd_cli provides a command line interface to Amazon Cloud Drive and allows -mounting your cloud drive using FUSE for read and write access. It is -currently in beta stage. - -%prep -%setup -q - -%patch0 -p1 -b git - -%build -%{__python3} setup.py build - - -%install -rm -rf $RPM_BUILD_ROOT -%{__python3} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT - -# default installs acd_cli, acdcli, and acd_cli.py -- docs only refer -# to the first two. -rm $RPM_BUILD_ROOT/%_bindir/{acd_cli,acdcli} -mv $RPM_BUILD_ROOT/%_bindir/acd_cli.py $RPM_BUILD_ROOT/%_bindir/acd_cli -ln -s acd_cli $RPM_BUILD_ROOT/%_bindir/acdcli - -%files -%doc README.rst CONTRIBUTING.rst -%license LICENSE -%{python3_sitelib}/acdcli/ -%{python3_sitelib}/acdcli-*egg-info/ -%_bindir/acdcli -%_bindir/acd_cli - -%changelog -* Wed Jun 14 2017 Matthew Miller - 6.20170530.gitcd4a9ee -- Peter Robinson reminds me that numbers can't go backwards :) - -* Mon Jun 12 2017 Matthew Miller - 0.3.2-gitcd4a9ee -- because it was entirely broken. - -* Fri Feb 10 2017 Fedora Release Engineering - 0.3.2-5 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild - -* Mon Dec 19 2016 Miro HronĨok - 0.3.2-4 -- Rebuild for Python 3.6 - -* Tue Nov 22 2016 Juan Orti Alcaine - 0.3.2-3 -- Add python3-fuse dependency - -* Sun Oct 2 2016 Matthew Miller - 03.2-2 -- package docs say "dateutils' but means "dateutil" -- change that and requests-toolbelt to hard dep, because package will not actually - function without - -* Sat Sep 3 2016 Juan Orti Alcaine - 0.3.2-1 -- Version 0.3.2 - -* Tue Jul 19 2016 Fedora Release Engineering - 0.3.1-3 -- https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages - -* Wed Feb 03 2016 Fedora Release Engineering - 0.3.1-2 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild - -* Tue Dec 1 2015 Matthew Miller - 0.3.1-1 -- initial RPM - diff --git a/dead.package b/dead.package new file mode 100644 index 0000000..d9f8855 --- /dev/null +++ b/dead.package @@ -0,0 +1 @@ +Amazon has effectively banned open source clients. This package is dead. diff --git a/sources b/sources deleted file mode 100644 index 8079ef9..0000000 --- a/sources +++ /dev/null @@ -1 +0,0 @@ -9e37108191f178a270583d3536cffc0f 0.3.2.tar.gz