diff --git a/geeknote/gnsync.py b/geeknote/gnsync.py index 9b5342d..45d7e75 100644 --- a/geeknote/gnsync.py +++ b/geeknote/gnsync.py @@ -7,6 +7,7 @@ import logging import string import unicodedata, re +import pickle from geeknote import GeekNote from storage import Storage @@ -19,6 +20,9 @@ handler = logging.FileHandler(def_logpath) handler.setFormatter(formatter) +mtime_file_extention = '.mtime' +rej_file_extension = '.rej' + logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) logger.addHandler(handler) @@ -80,12 +84,14 @@ class GNSync: path = None mask = None twoway = None + merged = None notebook_guid = None all_set = False + mtime_file = None @log - def __init__(self, notebook_name, path, mask, format, twoway=False): + def __init__(self, notebook_name, path, mask, format, twoway=False, merged=False): # check auth if not Storage().getUserToken(): raise Exception("Auth error. There is not any oAuthToken.") @@ -98,6 +104,9 @@ def __init__(self, notebook_name, path, mask, format, twoway=False): raise Exception("Path to sync directories does not exist.") self.path = path + self.mtime_file = os.path.join(self.path, '.' + notebook_name + mtime_file_extention) + if not os.path.exists(self.mtime_file): + open(self.mtime_file, 'wb') # create the mtime file #set mask if not mask: @@ -117,6 +126,7 @@ def __init__(self, notebook_name, path, mask, format, twoway=False): self.extension = ".txt" self.twoway = twoway + self.merged = merged logger.info('Sync Start') @@ -138,30 +148,95 @@ def sync(self): files = self._get_files() notes = self._get_notes() + mtimes = self._get_mtimes() + if mtimes is None: + mtimes = {} + + #track files and notes changed in this sync. + updated = [] + for f in files: has_note = False for n in notes: if f['name'] == n.title: has_note = True - if f['mtime'] > n.updated: - self._update_note(f, n) - break + #get saved mtime for the note + mtime = mtimes.get(n.title, 0) + if mtime != 0: + if mtime['file_mtime'] == f['mtime'] and \ + mtime['note_mtime'] == n.updated: + #both have not changed since last sync + break + elif mtime['file_mtime'] != f['mtime'] and \ + mtime['note_mtime'] != n.updated: + #both changed, need merge + if self.merged: + self._update_note(f, n) + updated.append(f['name']) + else: + logger.warning('Skipped note (CONFLICT!!!): {0},\n' + 'merge the note manually and sync the ' + 'notebook again with --merged option'.format(f['name'])) + self._create_rej_file(n) + break + elif mtime['file_mtime'] != f['mtime']: + #only local note changed + self._update_note(f, n) + updated.append(f['name']) + else: + #only server note changed, handle it in twoway mode + pass + else: + if f['mtime'] > n.updated: + self._update_note(f, n) + updated.append(f['name']) + + break if not has_note: self._create_note(f) + updated.append(f['name']) if self.twoway: for n in notes: has_file = False for f in files: if f['name'] == n.title: - has_file = True - if f['mtime'] < n.updated: - self._update_file(f, n) + has_file = True + mtime = mtimes.get(n.title, 0) + if mtime != 0: + if mtime['file_mtime'] == f['mtime'] and \ + mtime['note_mtime'] == n.updated: + break + elif mtime['file_mtime'] != f['mtime'] and \ + mtime['note_mtime'] != n.updated: + break #handled already + elif mtime['note_mtime'] != n.updated: + #only server note changed + self._update_file(f, n) + updated.append(n.title) + else: + pass #handled already + else: + if f['mtime'] < n.updated: + self._update_file(f, n) + updated.append(n.title) + break if not has_file: self._create_file(n) + updated.append(n.title) + + # after sync, save the mtimes of both files and notes + files = self._get_files() + notes = self._get_notes() + for f in files: + for n in notes: + if f['name'] == n.title and n.title in updated: + mtimes[n.title] = {'file_mtime':f['mtime'], 'note_mtime':n.updated} + + self._save_mtimes(mtimes) logger.info('Sync Complete') @@ -230,6 +305,19 @@ def _create_file(self, note): open(path, "w").write(content) return True + @log + def _create_rej_file(self, note): + """ + Create reject file for note + """ + GeekNote().loadNoteContent(note) + content = Editor.ENMLtoText(note.content) + rej_file = "." + note.title + self.extension + rej_file_extension + path = os.path.join(self.path, rej_file) + open(path, "w").write(content) + logger.info('Created reject file {0} for note: {1}'.format(rej_file, note.title)) + return True + @log def _get_file_content(self, path): """ @@ -240,7 +328,7 @@ def _get_file_content(self, path): # strip unprintable characters content = remove_control_characters(content.decode('utf-8')).encode('utf-8') content = Editor.textToENML(content=content, raise_ex=True, format=self.format) - + if content is None: logger.warning("File {0}. Content must be " \ "an UTF-8 encode.".format(path)) @@ -306,6 +394,22 @@ def _get_notes(self): keywords = 'notebook:"{0}"'.format(tools.strip(self.notebook_name)) return GeekNote().findNotes(keywords, 10000).notes + @log + def _get_mtimes(self): + """ + Get modification times + """ + mtfile = open(self.mtime_file, 'rb') + mtimes = pickle.load(mtfile) + return mtimes + + @log + def _save_mtimes(self, mtimes): + """ + Save modification times + """ + mtfile = open(self.mtime_file, 'wb') + pickle.dump(mtimes, mtfile) def main(): try: @@ -316,6 +420,7 @@ def main(): parser.add_argument('--notebook', '-n', action='store', help='Notebook name for synchronize. Default is default notebook') parser.add_argument('--logpath', '-l', action='store', help='Path to log file. Default is GeekNoteSync in home dir') parser.add_argument('--two-way', '-t', action='store', help='Two-way sync') + parser.add_argument('--merged', '-M', action='store_true', help='Update merged notes to server. Specify it only when conflicts are reported and you have resolved all of them manually on your local files') args = parser.parse_args() @@ -325,10 +430,11 @@ def main(): notebook = args.notebook if args.notebook else None logpath = args.logpath if args.logpath else None twoway = True if args.two_way else False + merged = True if args.merged else False reset_logpath(logpath) - GNS = GNSync(notebook, path, mask, format, twoway) + GNS = GNSync(notebook, path, mask, format, twoway, merged) GNS.sync() except (KeyboardInterrupt, SystemExit, tools.ExitException):