xybu/onedrived-dev

View on GitHub
onedrived/od_tasks/merge_dir.py

Summary

Maintainability
F
5 days
Test Coverage
File `merge_dir.py` has 467 lines of code (exceeds 250 allowed). Consider refactoring.
import itertools
import logging
import os
import shutil
 
import onedrivesdk.error
from onedrivesdk import Item, Folder, ChildrenCollectionRequest
from send2trash import send2trash
 
from . import base
from . import delete_item, download_file, upload_file
from .. import mkdir, fix_owner_and_timestamp
from ..od_api_helper import get_item_modified_datetime, item_request_call
from ..od_dateutils import datetime_to_timestamp, diff_timestamps
from ..od_hashutils import hash_match, sha1_value
from ..od_repo import ItemRecordType
 
 
Cyclomatic complexity is too high in function rename_with_suffix. (7)
Function `rename_with_suffix` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.
def rename_with_suffix(parent_abspath, name, host_name):
suffix = ' (' + host_name + ')'
parent_abspath = parent_abspath + '/'
 
# Calculate the file name without suffix.
ent_name, ent_ext = os.path.splitext(name)
if ent_name.endswith(suffix):
ent_name = ent_name[:-len(suffix)]
 
new_name = ent_name + suffix + ent_ext
if os.path.exists(parent_abspath + new_name):
count = 1
if ' ' in ent_name:
ent_name, count_str = ent_name.rsplit(' ', maxsplit=1)
if count_str.isdigit() and count_str[0] != '0':
count = int(count_str) + 1
Identical blocks of code found in 2 locations. Consider refactoring.
new_name = ent_name + ' ' + str(count) + suffix + ent_ext
while os.path.exists(parent_abspath + new_name):
count += 1
Identical blocks of code found in 2 locations. Consider refactoring.
new_name = ent_name + ' ' + str(count) + suffix + ent_ext
shutil.move(parent_abspath + name, parent_abspath + new_name)
return new_name
 
 
def get_os_stat(path):
try:
return os.stat(path)
except FileNotFoundError:
return None
 
 
Cyclomatic complexity is too high in class MergeDirectoryTask. (8)
class MergeDirectoryTask(base.TaskBase):
 
Function `__init__` has 7 arguments (exceeds 4 allowed). Consider refactoring.
def __init__(self, repo, task_pool, rel_path, item_request, deep_merge=True,
assume_remote_unchanged=False, parent_remote_unchanged=False):
"""
:param onedrived.od_repo.OneDriveLocalRepository repo:
:param onedrived.od_task.TaskPool task_pool:
:param str rel_path: Path of the target item relative to repository root. Assume not ending with '/'.
:param onedrivesdk.request.item_request_builder.ItemRequestBuilder item_request:
:param True | False deep_merge: If False, only sync files under the specified directory.
:param True | False assume_remote_unchanged: If True, assume there is no change in remote repository.
Can be set True if ctag and etag of the folder Item match its database record.
:param True | False parent_remote_unchanged: If parent remote dir item wasn't changed.
"""
super().__init__(repo, task_pool)
self.rel_path = rel_path
self.item_request = item_request
self.local_abspath = repo.local_root + rel_path
self.deep_merge = deep_merge
self.assume_remote_unchanged = assume_remote_unchanged
self.parent_remote_unchanged = parent_remote_unchanged
 
def __repr__(self):
return type(self).__name__ + '(%s, deep=%s, remote_unchanged=%s, parent_remote_unchanged=%s)' % (
self.local_abspath, self.deep_merge, self.assume_remote_unchanged, self.parent_remote_unchanged)
 
Cyclomatic complexity is too high in method list_local_names. (8)
Function `list_local_names` has a Cognitive Complexity of 12 (exceeds 5 allowed). Consider refactoring.
def list_local_names(self):
"""
List all names under the task local directory.
Try resolving naming conflict (same name case-INsensitive) as it goes.
:return [str]: A list of entry names.
"""
TODO found
# TODO: This logic can be improved if remote info is provided.
ents_orig = os.listdir(self.local_abspath)
ents_lower = [s.lower() for s in ents_orig]
ents_lower_uniq = set(ents_lower)
if len(ents_orig) == len(ents_lower_uniq):
return set(ents_orig)
ents_ret = set()
ents_ret_lower = set()
for ent, ent_lower in zip(ents_orig, ents_lower):
ent_abspath = self.local_abspath + '/' + ent
if ent_lower in ents_ret_lower:
ent_name, ent_ext = os.path.splitext(ent)
count = 1
new_ent = ent_name + ' ' + str(count) + ent_ext
new_ent_lower = new_ent.lower()
while new_ent_lower in ents_ret_lower or new_ent_lower in ents_lower_uniq:
count += 1
new_ent = ent_name + ' ' + str(count) + ent_ext
new_ent_lower = new_ent.lower()
try:
shutil.move(ent_abspath, self.local_abspath + '/' + new_ent)
ents_ret.add(new_ent)
ents_ret_lower.add(new_ent_lower)
except (IOError, OSError) as e:
logging.error('Error occurred when solving name conflict of "%s": %s.', ent_abspath, e)
continue
else:
ents_ret.add(ent)
ents_ret_lower.add(ent_lower)
return ents_ret
 
Cyclomatic complexity is too high in method handle. (12)
Function `handle` has a Cognitive Complexity of 18 (exceeds 5 allowed). Consider refactoring.
def handle(self):
if not os.path.isdir(self.local_abspath):
logging.error('Error: Local path "%s" is not a directory.' % self.local_abspath)
return
 
self.repo.context.watcher.rm_watch(self.repo, self.local_abspath)
 
try:
all_local_items = self.list_local_names()
except (IOError, OSError) as e:
logging.error('Error merging dir "%s": %s.', self.local_abspath, e)
return
 
all_records = self.repo.get_immediate_children_of_dir(self.rel_path)
 
if not self.assume_remote_unchanged or not self.parent_remote_unchanged:
try:
remote_item_page = item_request_call(self.repo, self.item_request.children.get)
all_remote_items = remote_item_page
 
while True:
HACK found
# HACK: ChildrenCollectionPage is not guaranteed to have
# the _next_page_link attribute and
# ChildrenCollectionPage.get_next_page_request doesn't
# implement the check correctly
if not hasattr(remote_item_page, '_next_page_link'):
break
 
logging.debug('Paging for more items: %s', self.rel_path)
remote_item_page = item_request_call(
self.repo,
ChildrenCollectionRequest.get_next_page_request(
remote_item_page,
self.repo.authenticator.client).get)
all_remote_items = itertools.chain(
all_remote_items, remote_item_page)
except onedrivesdk.error.OneDriveError as e:
logging.error('Encountered API Error: %s. Skip directory "%s".', e, self.rel_path)
return
 
for remote_item in all_remote_items:
remote_is_folder = remote_item.folder is not None
all_local_items.discard(remote_item.name) # Remove remote item from untouched list.
if not self.repo.path_filter.should_ignore(self.rel_path + '/' + remote_item.name, remote_is_folder):
self._handle_remote_item(remote_item, all_local_items, all_records)
else:
logging.debug('Ignored remote path "%s/%s".', self.rel_path, remote_item.name)
 
for n in all_local_items:
self._handle_local_item(n, all_records)
 
for rec_name, rec in all_records.items():
logging.info('Record for item %s (%s/%s) is dead. Delete it it.', rec.item_id, rec.parent_path, rec_name)
self.repo.delete_item(rec_name, rec.parent_path, is_folder=rec.type == ItemRecordType.FOLDER)
 
self.repo.context.watcher.add_watch(self.repo, self.local_abspath)
 
def _rename_local_and_download_remote(self, remote_item, all_local_items):
all_local_items.add(rename_with_suffix(self.local_abspath, remote_item.name, self.repo.context.host_name))
self.task_pool.add_task(
download_file.DownloadFileTask(self.repo, self.task_pool, remote_item, self.rel_path))
 
Function `_handle_remote_file_with_record` has a Cognitive Complexity of 42 (exceeds 5 allowed). Consider refactoring.
Cyclomatic complexity is too high in method _handle_remote_file_with_record. (27)
Function `_handle_remote_file_with_record` has 5 arguments (exceeds 4 allowed). Consider refactoring.
def _handle_remote_file_with_record(self, remote_item, item_record, item_stat, item_local_abspath, all_local_items):
"""
:param onedrivesdk.model.item.Item remote_item:
:param onedrived.od_repo.ItemRecord item_record:
:param posix.stat_result | None item_stat:
:param str item_local_abspath:
:param [str] all_local_items:
"""
# In this case we have all three pieces of information -- remote item metadata, database record, and local inode
# stats. The best case is that all of them agree, and the worst case is that they all disagree.
 
if os.path.isdir(item_local_abspath):
# Remote item is a file yet the local item is a folder.
if item_record and item_record.type == ItemRecordType.FOLDER:
TODO found
# TODO: Use the logic in handle_local_folder to solve this.
send2trash(item_local_abspath)
self.repo.delete_item(remote_item.name, self.rel_path, True)
else:
# When db record does not exist or says the path is a file, then it does not agree with local inode
# and the information is useless. We delete it and sync both remote and local items.
if item_record:
self.repo.delete_item(remote_item.name, self.rel_path, False)
return self._handle_remote_file_without_record(remote_item, None, item_local_abspath, all_local_items)
 
remote_mtime, _ = get_item_modified_datetime(remote_item)
local_mtime_ts = item_stat.st_mtime if item_stat else None
remote_mtime_ts = datetime_to_timestamp(remote_mtime)
record_mtime_ts = datetime_to_timestamp(item_record.modified_time)
try:
remote_sha1_hash = remote_item.file.hashes.sha1_hash
except AttributeError:
remote_sha1_hash = None
 
Line break after binary operator
if (remote_item.id == item_record.item_id and remote_item.c_tag == item_record.c_tag or
Line break after binary operator
remote_item.size == item_record.size and
diff_timestamps(remote_mtime_ts, record_mtime_ts) == 0):
# The remote item metadata matches the database record. So this item has been synced before.
if item_stat is None:
# The local file was synced but now is gone. Delete remote one as well.
logging.debug('Local file "%s" is gone but remote item matches db record. Delete remote item.',
item_local_abspath)
Similar blocks of code found in 2 locations. Consider refactoring.
self.task_pool.add_task(delete_item.DeleteRemoteItemTask(
self.repo, self.task_pool, self.rel_path, remote_item.name, remote_item.id, False))
Line break after binary operator
elif (item_stat.st_size == item_record.size_local and
Line break after binary operator
(diff_timestamps(local_mtime_ts, record_mtime_ts) == 0 or
remote_sha1_hash and remote_sha1_hash == sha1_value(item_local_abspath))):
# If the local file matches the database record (i.e., same mtime timestamp or same content),
# simply return. This is the best case.
if diff_timestamps(local_mtime_ts, remote_mtime_ts) != 0:
logging.info('File "%s" seems to have same content but different timestamp (%f, %f). Fix it.',
item_local_abspath, local_mtime_ts, remote_mtime_ts)
fix_owner_and_timestamp(item_local_abspath, self.repo.context.user_uid, remote_mtime_ts)
self.repo.update_item(remote_item, self.rel_path, item_stat.st_size)
else:
# Content of local file has changed. Because we assume the remote item was synced before, we overwrite
# the remote item with local one.
# API Issue: size field may not match file size.
# Refer to https://github.com/OneDrive/onedrive-sdk-python/issues/88
# Workaround -- storing both remote and local sizes.
logging.debug('File "%s" was changed locally and the remote version is known old. Upload it.',
item_local_abspath)
self.task_pool.add_task(upload_file.UploadFileTask(
self.repo, self.task_pool, self.item_request, self.rel_path, remote_item.name))
else:
# The remote file metadata and database record disagree.
Similar blocks of code found in 2 locations. Consider refactoring.
if item_stat is None:
# If the remote file is the one on record, then the remote one is newer than the deleted local file
# so it should be downloaded. If they are not the same, then the remote one should definitely
# be kept. So the remote file needs to be kept and downloaded anyway.
logging.debug('Local file "%s" is gone but remote item disagrees with db record. Download it.',
item_local_abspath)
self.task_pool.add_task(
download_file.DownloadFileTask(self.repo, self.task_pool, remote_item, self.rel_path))
Similar blocks of code found in 2 locations. Consider refactoring.
elif item_stat.st_size == item_record.size_local and \
Line break after binary operator
(diff_timestamps(local_mtime_ts, record_mtime_ts) == 0 or
item_record.sha1_hash and item_record.sha1_hash == sha1_value(item_local_abspath)):
# Local file agrees with database record. This means that the remote file is strictly newer.
# The local file can be safely overwritten.
logging.debug('Local file "%s" agrees with db record but remote item is different. Overwrite local.',
item_local_abspath)
self.task_pool.add_task(
download_file.DownloadFileTask(self.repo, self.task_pool, remote_item, self.rel_path))
else:
# So both the local file and remote file have been changed after the record was created.
equal_ts = diff_timestamps(local_mtime_ts, remote_mtime_ts) == 0
if (item_stat.st_size == remote_item.size and (
(equal_ts or remote_sha1_hash and remote_sha1_hash == sha1_value(item_local_abspath)))):
# Fortunately the two files seem to be the same.
# Here the logic is written as if there is no size mismatch issue.
logging.debug(
'Local file "%s" seems to have same content with remote but record disagrees. Fix db record.',
item_local_abspath)
if not equal_ts:
fix_owner_and_timestamp(item_local_abspath, self.repo.context.user_uid, remote_mtime_ts)
self.repo.update_item(remote_item, self.rel_path, item_stat.st_size)
else:
# Worst case we keep both files.
logging.debug('Local file "%s" differs from db record and remote item. Keep both versions.',
item_local_abspath)
self._rename_local_and_download_remote(remote_item, all_local_items)
 
Cyclomatic complexity is too high in method _handle_remote_file_without_record. (7)
Function `_handle_remote_file_without_record` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.
def _handle_remote_file_without_record(self, remote_item, item_stat, item_local_abspath, all_local_items):
"""
Handle the case in which a remote item is not found in the database. The local item may or may not exist.
:param onedrivesdk.model.item.Item remote_item:
:param posix.stat_result | None item_stat:
:param str item_local_abspath:
:param [str] all_local_items:
"""
if item_stat is None:
# The file does not exist locally, and there is no record in database. The safest approach is probably
# download the file and update record.
self.task_pool.add_task(
download_file.DownloadFileTask(self.repo, self.task_pool, remote_item, self.rel_path))
elif os.path.isdir(item_local_abspath):
# Remote path is file yet local path is a dir.
logging.info('Path "%s" is a folder yet the remote item is a file. Keep both.', item_local_abspath)
self._rename_local_and_download_remote(remote_item, all_local_items)
else:
# We first compare timestamp and size -- if both properties match then we think the items are identical
# and just update the database record. Otherwise if sizes are equal, we calculate hash of local item to
# determine if they are the same. If so we update timestamp of local item and update database record.
# If the remote item has different hash, then we rename the local one and download the remote one so that no
# information is lost.
remote_mtime, remote_mtime_w = get_item_modified_datetime(remote_item)
remote_mtime_ts = datetime_to_timestamp(remote_mtime)
equal_ts = diff_timestamps(remote_mtime_ts, item_stat.st_mtime) == 0
equal_attr = remote_item.size == item_stat.st_size and equal_ts
# Because of the size mismatch issue, we can't use size not being equal as a shortcut for hash not being
# equal. When the bug is fixed we can do it.
if equal_attr or hash_match(item_local_abspath, remote_item):
if not equal_ts:
logging.info('Local file "%s" has same content but wrong timestamp. '
'Remote: mtime=%s, w=%s, ts=%s, size=%d. '
'Local: ts=%s, size=%d. Fix it.',
item_local_abspath,
remote_mtime, remote_mtime_w, remote_mtime_ts, remote_item.size,
item_stat.st_mtime, item_stat.st_size)
fix_owner_and_timestamp(item_local_abspath, self.repo.context.user_uid, remote_mtime_ts)
self.repo.update_item(remote_item, self.rel_path, item_stat.st_size)
else:
self._rename_local_and_download_remote(remote_item, all_local_items)
 
@staticmethod
def _remote_dir_matches_record(remote_item, record):
return record and record.type == ItemRecordType.FOLDER and record.size == remote_item.size and \
record.c_tag == remote_item.c_tag and record.e_tag == remote_item.e_tag
 
Cyclomatic complexity is too high in method _handle_remote_folder. (7)
Function `_handle_remote_folder` has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.
def _handle_remote_folder(self, remote_item, item_local_abspath, record, all_local_items):
if not self.deep_merge:
return
try:
remote_dir_matches_record = self._remote_dir_matches_record(remote_item, record)
if os.path.isfile(item_local_abspath):
# Remote item is a directory but local item is a file.
if remote_dir_matches_record:
# The remote item is very LIKELY to be outdated.
logging.warning('Local path "%s" is a file but its remote counterpart is a folder which seems to '
'be synced before. Will delete the remote folder. To restore it, go to '
'OneDrive.com and move the folder out of Recycle Bin.', item_local_abspath)
delete_item.DeleteRemoteItemTask(
self.repo, self.task_pool, self.rel_path, remote_item.name, remote_item.id, True).handle()
self.task_pool.add_task(upload_file.UploadFileTask(
self.repo, self.task_pool, self.item_request, self.rel_path, remote_item.name))
return
# If the remote metadata doesn't agree with record, keep both by renaming the local file.
all_local_items.add(
rename_with_suffix(self.local_abspath, remote_item.name, self.repo.context.host_name))
 
if not os.path.exists(item_local_abspath):
if remote_dir_matches_record:
logging.debug('Local dir "%s" is gone but db record matches remote metadata. Delete remote dir.',
item_local_abspath)
Similar blocks of code found in 2 locations. Consider refactoring.
self.task_pool.add_task(delete_item.DeleteRemoteItemTask(
self.repo, self.task_pool, self.rel_path, remote_item.name, remote_item.id, True))
return
# Local directory does not exist. Create it.
logging.debug('Create missing directory "%s".', item_local_abspath)
mkdir(item_local_abspath, uid=self.repo.context.user_uid, exist_ok=True)
 
# The database is temporarily corrupted until the whole dir is merged. But unfortunately we returned early.
self.repo.update_item(remote_item, self.rel_path, 0)
self.task_pool.add_task(MergeDirectoryTask(
repo=self.repo, task_pool=self.task_pool, rel_path=self.rel_path + '/' + remote_item.name,
item_request=self.repo.authenticator.client.item(drive=self.repo.drive.id, id=remote_item.id),
assume_remote_unchanged=remote_dir_matches_record,
parent_remote_unchanged=self.assume_remote_unchanged))
except OSError as e:
logging.error('Error occurred when merging directory "%s": %s', item_local_abspath, e)
 
Cyclomatic complexity is too high in method _handle_remote_item. (7)
Function `_handle_remote_item` has a Cognitive Complexity of 11 (exceeds 5 allowed). Consider refactoring.
def _handle_remote_item(self, remote_item, all_local_items, all_records):
"""
:param onedrivesdk.model.item.Item remote_item:
:param [str] all_local_items:
:param dict(str, onedrived.od_repo.ItemRecord) all_records:
"""
# So we have three pieces of information -- the remote item metadata, the record in database, and the inode
# on local file system. For the case of handling a remote item, the last two may be missing.
item_local_abspath = self.local_abspath + '/' + remote_item.name
record = all_records.pop(remote_item.name, None)
 
try:
stat = get_os_stat(item_local_abspath)
except OSError as e:
logging.error('Error occurred when accessing path "%s": %s.', item_local_abspath, e)
return
 
if remote_item.folder is not None:
return self._handle_remote_folder(remote_item, item_local_abspath, record, all_local_items)
 
if remote_item.file is None:
if stat:
logging.info('Remote item "%s/%s" is neither a file nor a directory yet local counterpart exists. '
'Rename local item.', self.rel_path, remote_item.name)
try:
new_name = rename_with_suffix(self.local_abspath, remote_item.name, self.repo.context.host_name)
all_local_items.add(new_name)
except OSError as e:
logging.error('Error renaming "%s/%s": %s. Skip this item due to unsolvable type conflict.',
self.rel_path, remote_item.name, e)
else:
logging.info('Remote item "%s/%s" is neither a file nor a directory. Skip it.',
self.rel_path, remote_item.name)
return
 
if record is None:
self._handle_remote_file_without_record(remote_item, stat, item_local_abspath, all_local_items)
else:
self._handle_remote_file_with_record(remote_item, record, stat, item_local_abspath, all_local_items)
 
Cyclomatic complexity is too high in method _handle_local_folder. (9)
Function `_handle_local_folder` has a Cognitive Complexity of 12 (exceeds 5 allowed). Consider refactoring.
def _handle_local_folder(self, item_name, item_record, item_local_abspath):
"""
:param str item_name:
:param onedrived.od_repo.ItemRecord | None item_record:
:param str item_local_abspath:
"""
if not self.deep_merge:
return
 
Similar blocks of code found in 2 locations. Consider refactoring.
if self.repo.path_filter.should_ignore(self.rel_path + '/' + item_name, True):
logging.debug('Ignored local directory "%s/%s".', self.rel_path, item_name)
return
 
if item_record is not None and item_record.type == ItemRecordType.FOLDER:
if self.assume_remote_unchanged:
rel_path = self.rel_path + '/' + item_name
self.task_pool.add_task(MergeDirectoryTask(
repo=self.repo, task_pool=self.task_pool, rel_path=rel_path,
item_request=self.repo.authenticator.client.item(drive=self.repo.drive.id, path=rel_path),
assume_remote_unchanged=True, parent_remote_unchanged=self.assume_remote_unchanged))
else:
send2trash(item_local_abspath)
self.repo.delete_item(item_name, self.rel_path, True)
return
# try:
# # If there is any file accessed after the time when the record was created, do not delete the dir.
# # Instead, upload it back.
# # As a note, the API will return HTTP 404 Not Found after the item was deleted. So we cannot know from
# # API when the item was deleted. Otherwise this deletion time should be the timestamp to use.
TODO found
# # TODO: A second best timestamp is the latest timestamp of any children item under this dir.
# visited_files = subprocess.check_output(
# ['find', item_local_abspath, '-type', 'f',
# '(', '-newermt', item_record.record_time_str, '-o',
# '-newerat', item_record.record_time_str, ')', '-print'], universal_newlines=True)
# if visited_files == '':
# logging.info('Local item "%s" was deleted remotely and not used since %s. Delete it locally.',
# item_local_abspath, item_record.record_time_str)
# send2trash(item_local_abspath)
# self.repo.delete_item(item_name, self.rel_path, True)
# return
# logging.info('Local directory "%s" was deleted remotely but locally used. Upload it back.')
# except subprocess.CalledProcessError as e:
# logging.error('Error enumerating files in "%s" accessed after "%s": %s.',
# item_local_abspath, item_record.record_time_str, e)
# except OSError as e:
# logging.error('Error checking local folder "%s": %s.', item_local_abspath, e)
Similar blocks of code found in 2 locations. Consider refactoring.
elif item_record is not None:
if self.assume_remote_unchanged:
logging.info('Remote item for local dir "%s" is a file that has been deleted locally. '
'Delete the remote item and upload the file.', item_local_abspath)
if not delete_item.DeleteRemoteItemTask(
repo=self.repo, task_pool=self.task_pool, parent_relpath=self.rel_path,
item_name=item_name, item_id=item_record.item_id, is_folder=False).handle():
logging.error('Failed to delete outdated remote directory "%s/%s" of Drive %s.',
self.rel_path, item_name, self.repo.drive.id)
# Keep the record so that the branch can be revisited next time.
return
 
# Either we decide to upload the item above, or the folder does not exist remotely and we have no reference
# whether it existed remotely or not in the past. Better upload it back.
logging.info('Local directory "%s" seems new. Upload it.', item_local_abspath)
self.task_pool.add_task(CreateFolderTask(
self.repo, self.task_pool, item_name, self.rel_path, True, True))
 
Function `_handle_local_file` has a Cognitive Complexity of 26 (exceeds 5 allowed). Consider refactoring.
Cyclomatic complexity is too high in method _handle_local_file. (16)
def _handle_local_file(self, item_name, item_record, item_stat, item_local_abspath):
"""
:param str item_name:
:param onedrived.od_repo.ItemRecord | None item_record:
:param posix.stat_result | None item_stat:
:param str item_local_abspath:
"""
Similar blocks of code found in 2 locations. Consider refactoring.
if self.repo.path_filter.should_ignore(self.rel_path + '/' + item_name, False):
logging.debug('Ignored local file "%s/%s".', self.rel_path, item_name)
return
 
if item_stat is None:
logging.info('Local-only file "%s" existed when scanning but is now gone. Skip it.', item_local_abspath)
if item_record is not None:
self.repo.delete_item(item_record.item_name, item_record.parent_path, False)
if self.assume_remote_unchanged:
self.task_pool.add_task(delete_item.DeleteRemoteItemTask(
repo=self.repo, task_pool=self.task_pool, parent_relpath=self.rel_path,
item_name=item_name, item_id=item_record.item_id, is_folder=False))
return
 
if item_record is not None and item_record.type == ItemRecordType.FILE:
record_ts = datetime_to_timestamp(item_record.modified_time)
equal_ts = diff_timestamps(item_stat.st_mtime, record_ts) == 0
if item_stat.st_size == item_record.size_local and \
(equal_ts or item_record.sha1_hash and item_record.sha1_hash == sha1_value(item_local_abspath)):
# Local file matches record.
if self.assume_remote_unchanged:
if not equal_ts:
fix_owner_and_timestamp(item_local_abspath, self.repo.context.user_uid, record_ts)
else:
logging.debug('Local file "%s" used to exist remotely but not found. Delete it.',
item_local_abspath)
send2trash(item_local_abspath)
self.repo.delete_item(item_record.item_name, item_record.parent_path, False)
return
logging.debug('Local file "%s" is different from when it was last synced. Upload it.', item_local_abspath)
elif item_record is not None:
# Record is a dir but local entry is a file.
Similar blocks of code found in 2 locations. Consider refactoring.
if self.assume_remote_unchanged:
logging.info('Remote item for local file "%s" is a directory that has been deleted locally. '
'Delete the remote item and upload the file.', item_local_abspath)
if not delete_item.DeleteRemoteItemTask(
repo=self.repo, task_pool=self.task_pool, parent_relpath=self.rel_path,
item_name=item_name, item_id=item_record.item_id, is_folder=True).handle():
logging.error('Failed to delete outdated remote directory "%s/%s" of Drive %s.',
self.rel_path, item_name, self.repo.drive.id)
# Keep the record so that the branch can be revisited next time.
return
logging.debug('Local file "%s" is new to OneDrive. Upload it.', item_local_abspath)
 
self.task_pool.add_task(upload_file.UploadFileTask(
self.repo, self.task_pool, self.item_request, self.rel_path, item_name))
 
Function `_handle_local_item` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.
def _handle_local_item(self, item_name, all_records):
"""
:param str item_name:
:param dict(str, onedrived.od_repo.ItemRecord) all_records:
:return:
"""
item_local_abspath = self.local_abspath + '/' + item_name
record = all_records.pop(item_name, None)
try:
if os.path.isfile(item_local_abspath):
# stat can be None because the function can be called long after dir is listed.
stat = get_os_stat(item_local_abspath)
self._handle_local_file(item_name, record, stat, item_local_abspath)
elif os.path.isdir(item_local_abspath):
self._handle_local_folder(item_name, record, item_local_abspath)
else:
logging.warning('Unsupported type of local item "%s". Skip it and remove record.', item_local_abspath)
if record is not None:
self.repo.delete_item(record.item_name, record.parent_path, record.type == ItemRecordType.FOLDER)
except OSError as e:
logging.error('Error occurred when accessing path "%s": %s.', item_local_abspath, e)
 
 
class CreateFolderTask(base.TaskBase):
 
Function `__init__` has 6 arguments (exceeds 4 allowed). Consider refactoring.
def __init__(self, repo, task_pool, item_name, parent_relpath, upload_if_success=True, abort_if_local_gone=True):
"""
:param onedrived.od_repo.OneDriveLocalRepository repo:
:param onedrived.od_task.TaskPool task_pool:
:param str item_name:
:param str parent_relpath:
:param True | False upload_if_success:
:param True | False abort_if_local_gone:
"""
super().__init__(repo, task_pool)
self.item_name = item_name
self.parent_relpath = parent_relpath
self.local_abspath = repo.local_root + parent_relpath + '/' + item_name
self.upload_if_success = upload_if_success
self.abort_if_local_gone = abort_if_local_gone
 
Similar blocks of code found in 2 locations. Consider refactoring.
def __repr__(self):
return type(self).__name__ + '(%s, upload=%s)' % (self.local_abspath, self.upload_if_success)
 
@staticmethod
def _get_folder_pseudo_item(item_name):
item = Item()
item.name = item_name
item.folder = Folder()
return item
 
def _get_item_request(self):
if self.parent_relpath == '':
return self.repo.authenticator.client.item(drive=self.repo.drive.id, id='root')
else:
return self.repo.authenticator.client.item(drive=self.repo.drive.id, path=self.parent_relpath)
 
def handle(self):
logging.info('Creating remote item for local dir "%s".', self.local_abspath)
try:
if self.abort_if_local_gone and not os.path.isdir(self.local_abspath):
logging.warning('Local dir "%s" is gone. Skip creating remote item for it.', self.local_abspath)
return
item = self._get_folder_pseudo_item(self.item_name)
item_request = self._get_item_request()
item = item_request_call(self.repo, item_request.children.add, item)
self.repo.update_item(item, self.parent_relpath, 0)
logging.info('Created remote item for local dir "%s".', self.local_abspath)
if self.upload_if_success:
logging.info('Adding task to merge "%s" after remote item was created.', self.local_abspath)
self.task_pool.add_task(MergeDirectoryTask(
self.repo, self.task_pool, self.parent_relpath + '/' + self.item_name,
self.repo.authenticator.client.item(drive=self.repo.drive.id, id=item.id)))
return True
except (onedrivesdk.error.OneDriveError, OSError) as e:
logging.error('Error when creating remote dir of "%s": %s.', self.local_abspath, e)
return False