Sync notes automatically and on changes
This commit is contained in:
		@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										43
									
								
								sn_fuse.py
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								sn_fuse.py
									
									
									
									
									
								
							@@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user