Skip to content

Commit

Permalink
NXDRIVE-46: Add move detection for Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
loopingz committed Jul 30, 2014
1 parent 14900cc commit 69da6eb
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 65 deletions.
151 changes: 88 additions & 63 deletions nuxeo-drive-client/nxdrive/synchronizer.py
Expand Up @@ -21,6 +21,7 @@
from nxdrive.model import LastKnownState
from nxdrive.logging_config import get_logger
from nxdrive.utils import safe_long_path
import sys

WindowsError = None
try:
Expand Down Expand Up @@ -1832,71 +1833,86 @@ def handle_local_changes(self, server_binding):
sleep(self.test_delay)
local_client = self.get_local_client(local_folder)
sorted_evts = []
deleted_files = []
# Use the thread_safe pop() to extract events
while (len(self.local_changes)):
evt = self.local_changes.pop()
sorted_evts.append(evt)
sorted(sorted_evts, key=lambda evt: evt.time)
log.info('Sorted events: %r', sorted_evts)
sorted_evts = sorted(sorted_evts, key=lambda evt: evt.time)
log.debug('Sorted events: %r', sorted_evts)
for evt in sorted_evts:
src_path = normalize_event_filename(evt.src_path)
rel_path = local_client.get_path(src_path)
if len(rel_path) == 0:
rel_path = '/'
file_name = os.path.basename(src_path)
if (local_client.is_ignored(file_name)
and evt.event_type != 'moved'):
continue
doc_pair = session.query(LastKnownState).filter_by(
local_folder=local_folder, local_path=rel_path).first()
if (evt.event_type == 'created'
and doc_pair is None):
# If doc_pair is not None mean
# the creation has been catched by scan
doc_pair = LastKnownState(local_folder,
local_info=local_client.get_info(rel_path))
doc_pair.local_state = 'created'
session.add(doc_pair)
elif doc_pair is not None:
if (evt.event_type == 'moved'):
remote_client = self.get_remote_fs_client(server_binding)
self.handle_move(local_client, remote_client,
doc_pair, src_path,
normalize_event_filename(evt.dest_path))
session.commit()
continue
if evt.event_type == 'deleted':
doc_pair.update_state('deleted', doc_pair.remote_state)
continue
if evt.event_type == 'modified' and doc_pair.folderish:
continue
doc_pair.update_local(local_client.get_info(rel_path))
else:
# Event is the reflection of remote deletion
if evt.event_type == 'deleted':
continue
# As you receive an event for every children move also
if evt.event_type == 'moved':
# Try to see if it is a move from update
# No previous pair as it was hidden file
# Existing pair (may want to check the pair state)
dst_rel_path = local_client.get_path(
normalize_event_filename(evt.dest_path))
dst_pair = session.query(LastKnownState).filter_by(
local_folder=local_folder,
local_path=dst_rel_path).first()
# No pair so it must be a filed moved to this folder
if dst_pair is None:
# It can be consider as a creation
doc_pair = LastKnownState(local_folder,
local_info=local_client.get_info(dst_rel_path))
session.add(doc_pair)
else:
# Must come from a modification
dst_pair.update_local(
local_client.get_info(dst_rel_path))
try:
src_path = normalize_event_filename(evt.src_path)
rel_path = local_client.get_path(src_path)
if len(rel_path) == 0:
rel_path = '/'
file_name = os.path.basename(src_path)
if (local_client.is_ignored(file_name)
and evt.event_type != 'moved'):
continue
log.info('Unhandle case: %r %s %s', evt, rel_path, file_name)
doc_pair = session.query(LastKnownState).filter_by(
local_folder=local_folder, local_path=rel_path).first()
if (evt.event_type == 'created'
and doc_pair is None):
# If doc_pair is not None mean
# the creation has been catched by scan
# As Windows send a delete / create event for reparent
local_info = local_client.get_info(rel_path)
digest = local_info.get_digest()
for deleted in deleted_files:
if deleted.local_digest == digest:
# Move detected
log.info('Detected a file movement %r', deleted)
deleted.update_state('moved',deleted.remote_state)
deleted.update_local(local_client.get_info(rel_path))
continue
doc_pair = LastKnownState(local_folder,
local_info=local_info)
doc_pair.local_state = 'created'
session.add(doc_pair)
elif doc_pair is not None:
if (evt.event_type == 'moved'):
remote_client = self.get_remote_fs_client(server_binding)
self.handle_move(local_client, remote_client,
doc_pair, src_path,
normalize_event_filename(evt.dest_path))
session.commit()
continue
if evt.event_type == 'deleted':
doc_pair.update_state('deleted', doc_pair.remote_state)
deleted_files.append(doc_pair)
continue
if evt.event_type == 'modified' and doc_pair.folderish:
continue
doc_pair.update_local(local_client.get_info(rel_path))
else:
# Event is the reflection of remote deletion
if evt.event_type == 'deleted':
continue
# As you receive an event for every children move also
if evt.event_type == 'moved':
# Try to see if it is a move from update
# No previous pair as it was hidden file
# Existing pair (may want to check the pair state)
dst_rel_path = local_client.get_path(
normalize_event_filename(evt.dest_path))
dst_pair = session.query(LastKnownState).filter_by(
local_folder=local_folder,
local_path=dst_rel_path).first()
# No pair so it must be a filed moved to this folder
if dst_pair is None:
# It can be consider as a creation
doc_pair = LastKnownState(local_folder,
local_info=local_client.get_info(dst_rel_path))
session.add(doc_pair)
else:
# Must come from a modification
dst_pair.update_local(
local_client.get_info(dst_rel_path))
continue
log.info('Unhandle case: %r %s %s', evt, rel_path, file_name)
except:
log.info(sys.exc_info()[0])
session.commit()

def handle_rename(self, local_client, remote_client, doc_pair, dest_path):
Expand All @@ -1907,7 +1923,13 @@ def handle_rename(self, local_client, remote_client, doc_pair, dest_path):
rel_path = local_client.get_path(dest_path)
if len(rel_path) == 0:
rel_path = '/'
doc_pair.update_local(local_client.get_info(rel_path))
try:
doc_pair.update_local(local_client.get_info(rel_path))
except NotFound:
# In case of Windows with several move in a row
# Still need to update path
doc_pair.local_name = new_name
doc_pair.local_path = rel_path
doc_pair.update_state(state, 'synchronized')

def handle_move(self, local_client, remote_client, doc_pair, src_path,
Expand Down Expand Up @@ -1959,7 +1981,6 @@ def setup_local_watchdog(self, server_binding):

def normalize_event_filename(filename):
import unicodedata
import sys
if sys.platform == 'win32':
return unicodedata.normalize('NFKC', unicode(filename))
else:
Expand All @@ -1970,6 +1991,7 @@ class DriveFSEventHandler(FileSystemEventHandler):
def __init__(self, queue):
super(DriveFSEventHandler, self).__init__()
self.queue = queue
self.counter = 0

def on_any_event(self, event):
if event.event_type == 'moved':
Expand All @@ -1995,6 +2017,9 @@ def on_any_event(self, event):
return
except ValueError:
pass
event.time = time()
# Use counter instead of time so to be sure to respect the order
# As 2 events can have the same ms
self.counter += 1
event.time = self.counter
self.queue.append(event)
log.debug(event)
log.debug('%d %r', self.counter, event)
21 changes: 19 additions & 2 deletions nuxeo-drive-client/nxdrive/tests/common.py
Expand Up @@ -158,6 +158,17 @@ def setUp(self):
self.remote_file_system_client_2 = remote_file_system_client_2

def tearDown(self):
# Force to clean all observers
for observer in self.controller_1.synchronizer.observers:
observer.stop()
observer.join()
del observer
self.controller_1.synchronizer.observers = []
for observer in self.controller_2.synchronizer.observers:
observer.stop()
observer.join()
del observer
self.controller_2.synchronizer.observers = []
# Note that unbinding a server revokes the related token if needed,
# see Controller.unbind_server()
self.controller_1.unbind_all()
Expand All @@ -171,11 +182,17 @@ def tearDown(self):

if os.path.exists(self.local_test_folder_1):
self.controller_1.dispose()
shutil.rmtree(safe_long_path(self.local_test_folder_1))
try:
shutil.rmtree(safe_long_path(self.local_test_folder_1))
except:
pass

if os.path.exists(self.local_test_folder_2):
self.controller_2.dispose()
shutil.rmtree(safe_long_path(self.local_test_folder_2))
try:
shutil.rmtree(safe_long_path(self.local_test_folder_2))
except:
pass

def get_all_states(self, session=None):
"""Utility to quickly introspect the current known states"""
Expand Down

0 comments on commit 69da6eb

Please sign in to comment.