Sync notes automatically and on changes

This commit is contained in:
Tanner Collin 2017-10-11 21:00:04 -06:00
parent 2fe82ddf6f
commit efb5f3b8c5
3 changed files with 51 additions and 18 deletions

View File

@ -56,20 +56,18 @@ class ItemManager:
notes[title] = dict(text=text, notes[title] = dict(text=text,
created=item['created_at'], created=item['created_at'],
modified=item['updated_at'], modified=item.get('updated_at', item['created_at']),
uuid=item['uuid']) uuid=item['uuid'])
return notes return notes
def touchNote(self, uuid): def touchNote(self, uuid):
item = self.items[uuid] item = self.items[uuid]
item['dirty'] = True item['dirty'] = True
self.syncItems()
def writeNote(self, uuid, text): def writeNote(self, uuid, text):
item = self.items[uuid] item = self.items[uuid]
item['content']['text'] = text.strip() item['content']['text'] = text.strip()
item['dirty'] = True item['dirty'] = True
self.syncItems()
def createNote(self, name, time): def createNote(self, name, time):
uuid = str(uuid1()) uuid = str(uuid1())
@ -82,19 +80,16 @@ class ItemManager:
updated_at=time, updated_at=time,
enc_item_key='', enc_item_key='',
content=content) content=content)
self.syncItems()
def renameNote(self, uuid, new_note_name): def renameNote(self, uuid, new_note_name):
item = self.items[uuid] item = self.items[uuid]
item['content']['title'] = new_note_name item['content']['title'] = new_note_name
item['dirty'] = True item['dirty'] = True
self.syncItems()
def deleteNote(self, uuid): def deleteNote(self, uuid):
item = self.items[uuid] item = self.items[uuid]
item['deleted'] = True item['deleted'] = True
item['dirty'] = True item['dirty'] = True
self.syncItems()
def __init__(self, sn_api): def __init__(self, sn_api):
self.sn_api = sn_api self.sn_api = sn_api

View File

@ -1,20 +1,20 @@
from __future__ import print_function, absolute_import, division
import errno import errno
import iso8601 import iso8601
import logging import logging
import os import os
import time
from stat import S_IFDIR, S_IFREG from stat import S_IFDIR, S_IFREG
from sys import argv, exit from sys import argv, exit
from datetime import datetime from datetime import datetime
from pathlib import PurePath from pathlib import PurePath
from threading import Thread, Event
from fuse import FUSE, FuseOSError, Operations, LoggingMixIn from fuse import FUSE, FuseOSError, Operations, LoggingMixIn
from itemmanager import ItemManager from itemmanager import ItemManager
class StandardNotesFUSE(LoggingMixIn, Operations): 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.item_manager = ItemManager(sn_api)
self.notes = self.item_manager.getNotes() self.notes = self.item_manager.getNotes()
@ -30,15 +30,38 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
st_mtime=now, st_atime=now, st_nlink=1, st_mtime=now, st_atime=now, st_nlink=1,
st_uid=self.uid, st_gid=self.gid) 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): def _pathToNote(self, path):
pp = PurePath(path) pp = PurePath(path)
note_name = pp.parts[1] note_name = pp.parts[1]
self.notes = self.item_manager.getNotes()
note = self.notes[note_name] note = self.notes[note_name]
return note, note['uuid'] return note, note['uuid']
def getattr(self, path, fh=None): def getattr(self, path, fh=None):
self.notes = self.item_manager.getNotes()
if path == '/': if path == '/':
return self.dir_stat return self.dir_stat
@ -67,6 +90,8 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
note, uuid = self._pathToNote(path) note, uuid = self._pathToNote(path)
text = note['text'][:length] text = note['text'][:length]
self.item_manager.writeNote(uuid, text) self.item_manager.writeNote(uuid, text)
self._syncNow()
return 0
def write(self, path, data, offset, fh): def write(self, path, data, offset, fh):
note, uuid = self._pathToNote(path) note, uuid = self._pathToNote(path)
@ -78,6 +103,7 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
raise FuseOSError(errno.EIO) raise FuseOSError(errno.EIO)
self.item_manager.writeNote(uuid, text) self.item_manager.writeNote(uuid, text)
self._syncNow()
return len(data) return len(data)
def create(self, path, mode): def create(self, path, mode):
@ -92,11 +118,13 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
now = datetime.utcnow().isoformat()[:-3] + 'Z' # hack now = datetime.utcnow().isoformat()[:-3] + 'Z' # hack
self.item_manager.createNote(note_name, now) self.item_manager.createNote(note_name, now)
self._syncNow()
return 0 return 0
def unlink(self, path): def unlink(self, path):
note, uuid = self._pathToNote(path) note, uuid = self._pathToNote(path)
self.item_manager.deleteNote(uuid) self.item_manager.deleteNote(uuid)
self._syncNow()
return 0 return 0
def mkdir(self, path, mode): def mkdir(self, path, mode):
@ -106,6 +134,7 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
def utimens(self, path, times=None): def utimens(self, path, times=None):
note, uuid = self._pathToNote(path) note, uuid = self._pathToNote(path)
self.item_manager.touchNote(uuid) self.item_manager.touchNote(uuid)
self._syncNow()
return 0 return 0
def rename(self, old, new): def rename(self, old, new):
@ -113,6 +142,7 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
new_path_parts = new.split('/') new_path_parts = new.split('/')
new_note_name = new_path_parts[1] new_note_name = new_path_parts[1]
self.item_manager.renameNote(uuid, new_note_name) self.item_manager.renameNote(uuid, new_note_name)
self._syncNow()
return 0 return 0
def chmod(self, path, mode): def chmod(self, path, mode):
@ -123,9 +153,6 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
logging.error('chown is disabled.') logging.error('chown is disabled.')
raise FuseOSError(errno.EPERM) raise FuseOSError(errno.EPERM)
def destroy(self, path):
return 0
def readlink(self, path): def readlink(self, path):
return 0 return 0

View File

@ -13,6 +13,8 @@ from sn_fuse import StandardNotesFUSE
from fuse import FUSE from fuse import FUSE
OFFICIAL_SERVER_URL = 'https://sync.standardnotes.org' OFFICIAL_SERVER_URL = 'https://sync.standardnotes.org'
DEFAULT_SYNC_SEC = 30
MINIMUM_SYNC_SEC = 5
APP_NAME = 'standardnotes-fs' APP_NAME = 'standardnotes-fs'
# path settings # path settings
@ -34,12 +36,14 @@ def parse_options():
help='output verbosity -v or -vv (implies --foreground)') help='output verbosity -v or -vv (implies --foreground)')
parser.add_argument('--foreground', action='store_true', parser.add_argument('--foreground', action='store_true',
help='run standardnotes-fs in the foreground') 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', parser.add_argument('--sync-url',
help='URL of Standard File sync server. Defaults to:\n' help='URL of Standard File sync server. Defaults to:\n'
''+OFFICIAL_SERVER_URL) ''+OFFICIAL_SERVER_URL)
parser.add_argument('--no-config-file', action='store_true', parser.add_argument('--no-config-file', action='store_true',
help='don\'t load or create a config file') 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' help='specify a config file location. Defaults to:\n'
''+str(CONFIG_FILE)) ''+str(CONFIG_FILE))
parser.add_argument('--logout', action='store_true', parser.add_argument('--logout', action='store_true',
@ -63,8 +67,7 @@ def main():
if args.verbosity: args.foreground = True if args.verbosity: args.foreground = True
# figure out config file # figure out config file
config_file = args.config if args.config else CONFIG_FILE config_file = pathlib.Path(args.config)
config_file = pathlib.Path(config_file)
# logout and quit if wanted # logout and quit if wanted
if args.logout: if args.logout:
@ -80,6 +83,14 @@ def main():
print('No mountpoint specified.') print('No mountpoint specified.')
sys.exit(1) 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 # load config file settings
if not args.no_config_file: if not args.no_config_file:
try: try:
@ -157,7 +168,7 @@ def main():
if login_success: if login_success:
logging.info('Starting FUSE filesystem.') logging.info('Starting FUSE filesystem.')
fuse = FUSE(StandardNotesFUSE(sn_api), fuse = FUSE(StandardNotesFUSE(sn_api, sync_sec),
args.mountpoint, args.mountpoint,
foreground=args.foreground, foreground=args.foreground,
nothreads=True) # benefits don't outweigh the costs nothreads=True) # benefits don't outweigh the costs