diff --git a/itemmanager.py b/itemmanager.py index 68d2d79..0277729 100644 --- a/itemmanager.py +++ b/itemmanager.py @@ -56,20 +56,18 @@ class ItemManager: notes[title] = dict(text=text, created=item['created_at'], - modified=item['updated_at'], + modified=item.get('updated_at', item['created_at']), uuid=item['uuid']) return notes def touchNote(self, uuid): item = self.items[uuid] item['dirty'] = True - self.syncItems() def writeNote(self, uuid, text): item = self.items[uuid] item['content']['text'] = text.strip() item['dirty'] = True - self.syncItems() def createNote(self, name, time): uuid = str(uuid1()) @@ -82,19 +80,16 @@ class ItemManager: updated_at=time, enc_item_key='', content=content) - self.syncItems() def renameNote(self, uuid, new_note_name): item = self.items[uuid] item['content']['title'] = new_note_name item['dirty'] = True - self.syncItems() def deleteNote(self, uuid): item = self.items[uuid] item['deleted'] = True item['dirty'] = True - self.syncItems() def __init__(self, sn_api): self.sn_api = sn_api diff --git a/sn_fuse.py b/sn_fuse.py index 883f0e9..0cb96d1 100644 --- a/sn_fuse.py +++ b/sn_fuse.py @@ -1,20 +1,20 @@ -from __future__ import print_function, absolute_import, division - import errno import iso8601 import logging import os +import time from stat import S_IFDIR, S_IFREG from sys import argv, exit from datetime import datetime from pathlib import PurePath +from threading import Thread, Event from fuse import FUSE, FuseOSError, Operations, LoggingMixIn from itemmanager import ItemManager class StandardNotesFUSE(LoggingMixIn, Operations): - def __init__(self, sn_api, path='.'): + def __init__(self, sn_api, sync_sec, path='.'): self.item_manager = ItemManager(sn_api) self.notes = self.item_manager.getNotes() @@ -30,15 +30,38 @@ class StandardNotesFUSE(LoggingMixIn, Operations): st_mtime=now, st_atime=now, st_nlink=1, st_uid=self.uid, st_gid=self.gid) + self.sync_sec = sync_sec + self.run_sync = Event() + self.stop_sync = Event() + self.sync_thread = Thread(target=self._syncThread) + self.sync_thread.start() + + def destroy(self, path): + self._syncNow() + logging.info('Stopping sync thread.') + self.stop_sync.set() + self.sync_thread.join() + return 0 + + def _syncThread(self): + while not self.stop_sync.is_set(): + self.run_sync.clear() + manually_synced = self.run_sync.wait(timeout=self.sync_sec) + if not manually_synced: logging.info('Auto-syncing items...') + time.sleep(0.1) # fixes race condition of quick create() then write() + self.item_manager.syncItems() + + def _syncNow(self): + self.run_sync.set() + def _pathToNote(self, path): pp = PurePath(path) note_name = pp.parts[1] + self.notes = self.item_manager.getNotes() note = self.notes[note_name] return note, note['uuid'] def getattr(self, path, fh=None): - self.notes = self.item_manager.getNotes() - if path == '/': return self.dir_stat @@ -67,6 +90,8 @@ class StandardNotesFUSE(LoggingMixIn, Operations): note, uuid = self._pathToNote(path) text = note['text'][:length] self.item_manager.writeNote(uuid, text) + self._syncNow() + return 0 def write(self, path, data, offset, fh): note, uuid = self._pathToNote(path) @@ -78,6 +103,7 @@ class StandardNotesFUSE(LoggingMixIn, Operations): raise FuseOSError(errno.EIO) self.item_manager.writeNote(uuid, text) + self._syncNow() return len(data) def create(self, path, mode): @@ -92,11 +118,13 @@ class StandardNotesFUSE(LoggingMixIn, Operations): now = datetime.utcnow().isoformat()[:-3] + 'Z' # hack self.item_manager.createNote(note_name, now) + self._syncNow() return 0 def unlink(self, path): note, uuid = self._pathToNote(path) self.item_manager.deleteNote(uuid) + self._syncNow() return 0 def mkdir(self, path, mode): @@ -106,6 +134,7 @@ class StandardNotesFUSE(LoggingMixIn, Operations): def utimens(self, path, times=None): note, uuid = self._pathToNote(path) self.item_manager.touchNote(uuid) + self._syncNow() return 0 def rename(self, old, new): @@ -113,6 +142,7 @@ class StandardNotesFUSE(LoggingMixIn, Operations): new_path_parts = new.split('/') new_note_name = new_path_parts[1] self.item_manager.renameNote(uuid, new_note_name) + self._syncNow() return 0 def chmod(self, path, mode): @@ -123,9 +153,6 @@ class StandardNotesFUSE(LoggingMixIn, Operations): logging.error('chown is disabled.') raise FuseOSError(errno.EPERM) - def destroy(self, path): - return 0 - def readlink(self, path): return 0 diff --git a/standardnotes_fs.py b/standardnotes_fs.py index 90a5de6..28323ea 100644 --- a/standardnotes_fs.py +++ b/standardnotes_fs.py @@ -13,6 +13,8 @@ from sn_fuse import StandardNotesFUSE from fuse import FUSE OFFICIAL_SERVER_URL = 'https://sync.standardnotes.org' +DEFAULT_SYNC_SEC = 30 +MINIMUM_SYNC_SEC = 5 APP_NAME = 'standardnotes-fs' # path settings @@ -34,12 +36,14 @@ def parse_options(): help='output verbosity -v or -vv (implies --foreground)') parser.add_argument('--foreground', action='store_true', help='run standardnotes-fs in the foreground') + parser.add_argument('--sync-sec', type=int, default=DEFAULT_SYNC_SEC, + help='how many seconds between each sync. Default: 10') parser.add_argument('--sync-url', help='URL of Standard File sync server. Defaults to:\n' ''+OFFICIAL_SERVER_URL) parser.add_argument('--no-config-file', action='store_true', help='don\'t load or create a config file') - parser.add_argument('--config', + parser.add_argument('--config', default=CONFIG_FILE, help='specify a config file location. Defaults to:\n' ''+str(CONFIG_FILE)) parser.add_argument('--logout', action='store_true', @@ -63,8 +67,7 @@ def main(): if args.verbosity: args.foreground = True # figure out config file - config_file = args.config if args.config else CONFIG_FILE - config_file = pathlib.Path(config_file) + config_file = pathlib.Path(args.config) # logout and quit if wanted if args.logout: @@ -80,6 +83,14 @@ def main(): print('No mountpoint specified.') sys.exit(1) + # keep sync_sec above the minimum sync time + if args.sync_sec < MINIMUM_SYNC_SEC: + sync_sec = MINIMUM_SYNC_SEC + print('Sync interval must be at least', MINIMUM_SYNC_SEC, + 'seconds. Using that instead.') + else: + sync_sec = args.sync_sec + # load config file settings if not args.no_config_file: try: @@ -157,7 +168,7 @@ def main(): if login_success: logging.info('Starting FUSE filesystem.') - fuse = FUSE(StandardNotesFUSE(sn_api), + fuse = FUSE(StandardNotesFUSE(sn_api, sync_sec), args.mountpoint, foreground=args.foreground, nothreads=True) # benefits don't outweigh the costs