Work with binary data and catch more exceptions
This commit is contained in:
parent
be39849693
commit
c7309c4136
11
api.py
11
api.py
|
@ -2,6 +2,9 @@ import json, requests, time
|
||||||
|
|
||||||
from crypt import EncryptionHelper
|
from crypt import EncryptionHelper
|
||||||
|
|
||||||
|
class SNAPIException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class RESTAPI:
|
class RESTAPI:
|
||||||
def __init__(self, base_url):
|
def __init__(self, base_url):
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
|
@ -27,11 +30,19 @@ class StandardNotesAPI:
|
||||||
|
|
||||||
def genKeys(self, password):
|
def genKeys(self, password):
|
||||||
pw_info = self.getAuthParamsForEmail()
|
pw_info = self.getAuthParamsForEmail()
|
||||||
|
|
||||||
|
if 'error' in pw_info:
|
||||||
|
raise SNAPIException(pw_info['error']['message'])
|
||||||
|
|
||||||
return self.encryption_helper.pure_generatePasswordAndKey(password, pw_info['pw_salt'], pw_info['pw_cost'])
|
return self.encryption_helper.pure_generatePasswordAndKey(password, pw_info['pw_salt'], pw_info['pw_cost'])
|
||||||
|
|
||||||
def signIn(self, keys):
|
def signIn(self, keys):
|
||||||
self.keys = keys
|
self.keys = keys
|
||||||
res = self.api.post('/auth/sign_in', dict(email=self.username, password=self.keys['pw']))
|
res = self.api.post('/auth/sign_in', dict(email=self.username, password=self.keys['pw']))
|
||||||
|
|
||||||
|
if 'error' in res:
|
||||||
|
raise SNAPIException(res['error']['message'])
|
||||||
|
|
||||||
self.api.addHeader(dict(Authorization='Bearer ' + res['token']))
|
self.api.addHeader(dict(Authorization='Bearer ' + res['token']))
|
||||||
|
|
||||||
def sync(self, dirty_items):
|
def sync(self, dirty_items):
|
||||||
|
|
|
@ -44,9 +44,16 @@ class ItemManager:
|
||||||
for uuid, item in sorted_items:
|
for uuid, item in sorted_items:
|
||||||
if item['content_type'] == 'Note':
|
if item['content_type'] == 'Note':
|
||||||
note = item['content']
|
note = item['content']
|
||||||
text = note['text'] + '\n'
|
text = note['text']
|
||||||
count = 0 # used to remove title duplicates
|
|
||||||
|
|
||||||
|
# Add a new line so it outputs pretty
|
||||||
|
if not text.endswith('\n'):
|
||||||
|
text += '\n';
|
||||||
|
|
||||||
|
text = text.encode() # convert to binary data
|
||||||
|
|
||||||
|
# remove title duplicates by adding a number to the end
|
||||||
|
count = 0
|
||||||
while True:
|
while True:
|
||||||
title = note['title'] + ('' if not count else ' ' + str(count + 1))
|
title = note['title'] + ('' if not count else ' ' + str(count + 1))
|
||||||
if title in notes:
|
if title in notes:
|
||||||
|
@ -66,7 +73,7 @@ class ItemManager:
|
||||||
|
|
||||||
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.decode() # convert back to string
|
||||||
item['dirty'] = True
|
item['dirty'] = True
|
||||||
|
|
||||||
def createNote(self, name, time):
|
def createNote(self, name, time):
|
||||||
|
|
10
sn_fuse.py
10
sn_fuse.py
|
@ -12,6 +12,7 @@ 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
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
class StandardNotesFUSE(LoggingMixIn, Operations):
|
class StandardNotesFUSE(LoggingMixIn, Operations):
|
||||||
def __init__(self, sn_api, sync_sec, path='.'):
|
def __init__(self, sn_api, sync_sec, path='.'):
|
||||||
|
@ -51,7 +52,10 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
|
||||||
manually_synced = self.run_sync.wait(timeout=self.sync_sec)
|
manually_synced = self.run_sync.wait(timeout=self.sync_sec)
|
||||||
if not manually_synced: logging.info('Auto-syncing items...')
|
if not manually_synced: logging.info('Auto-syncing items...')
|
||||||
time.sleep(0.1) # fixes race condition of quick create() then write()
|
time.sleep(0.1) # fixes race condition of quick create() then write()
|
||||||
|
try:
|
||||||
self.item_manager.syncItems()
|
self.item_manager.syncItems()
|
||||||
|
except ConnectionError:
|
||||||
|
logging.error('Unable to connect to sync server. Retrying...')
|
||||||
|
|
||||||
def _syncNow(self):
|
def _syncNow(self):
|
||||||
self.run_sync.set()
|
self.run_sync.set()
|
||||||
|
@ -86,7 +90,7 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
|
||||||
|
|
||||||
def read(self, path, size, offset, fh):
|
def read(self, path, size, offset, fh):
|
||||||
note, uuid = self._pathToNote(path)
|
note, uuid = self._pathToNote(path)
|
||||||
return note['text'][offset : offset + size].encode()
|
return note['text'][offset : offset + size]
|
||||||
|
|
||||||
def truncate(self, path, length, fh=None):
|
def truncate(self, path, length, fh=None):
|
||||||
note, uuid = self._pathToNote(path)
|
note, uuid = self._pathToNote(path)
|
||||||
|
@ -97,14 +101,14 @@ class StandardNotesFUSE(LoggingMixIn, Operations):
|
||||||
|
|
||||||
def write(self, path, data, offset, fh):
|
def write(self, path, data, offset, fh):
|
||||||
note, uuid = self._pathToNote(path)
|
note, uuid = self._pathToNote(path)
|
||||||
|
text = note['text'][:offset] + data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
text = note['text'][:offset] + data.decode()
|
self.item_manager.writeNote(uuid, text)
|
||||||
except UnicodeError:
|
except UnicodeError:
|
||||||
logging.error('Unable to parse non-unicode data.')
|
logging.error('Unable to parse non-unicode data.')
|
||||||
raise FuseOSError(errno.EIO)
|
raise FuseOSError(errno.EIO)
|
||||||
|
|
||||||
self.item_manager.writeNote(uuid, text)
|
|
||||||
self._syncNow()
|
self._syncNow()
|
||||||
return len(data)
|
return len(data)
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,10 @@ import sys
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
|
|
||||||
from api import StandardNotesAPI
|
from api import StandardNotesAPI, SNAPIException
|
||||||
from sn_fuse import StandardNotesFUSE
|
from sn_fuse import StandardNotesFUSE
|
||||||
from fuse import FUSE
|
from fuse import FUSE
|
||||||
|
from requests.exceptions import ConnectionError, MissingSchema
|
||||||
|
|
||||||
OFFICIAL_SERVER_URL = 'https://sync.standardnotes.org'
|
OFFICIAL_SERVER_URL = 'https://sync.standardnotes.org'
|
||||||
DEFAULT_SYNC_SEC = 30
|
DEFAULT_SYNC_SEC = 30
|
||||||
|
@ -54,6 +55,7 @@ def main():
|
||||||
args = parse_options()
|
args = parse_options()
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
keys = {}
|
keys = {}
|
||||||
|
login_success = False
|
||||||
|
|
||||||
# configure logging
|
# configure logging
|
||||||
if args.verbosity == 1:
|
if args.verbosity == 1:
|
||||||
|
@ -61,7 +63,7 @@ def main():
|
||||||
elif args.verbosity == 2:
|
elif args.verbosity == 2:
|
||||||
log_level = logging.DEBUG
|
log_level = logging.DEBUG
|
||||||
else:
|
else:
|
||||||
log_level = logging.CRITICAL
|
log_level = logging.ERROR
|
||||||
logging.basicConfig(level=log_level,
|
logging.basicConfig(level=log_level,
|
||||||
format='%(levelname)-8s: %(message)s')
|
format='%(levelname)-8s: %(message)s')
|
||||||
if args.verbosity: args.foreground = True
|
if args.verbosity: args.foreground = True
|
||||||
|
@ -143,18 +145,23 @@ def main():
|
||||||
log_msg = 'Successfully logged into account "%s".'
|
log_msg = 'Successfully logged into account "%s".'
|
||||||
logging.info(log_msg % username)
|
logging.info(log_msg % username)
|
||||||
login_success = True
|
login_success = True
|
||||||
except:
|
except SNAPIException as e:
|
||||||
log_msg = 'Failed to log into account "%s".'
|
print(e)
|
||||||
print(log_msg % username)
|
except ConnectionError:
|
||||||
login_success = False
|
log_msg = 'Unable to connect to the sync server at "%s".'
|
||||||
|
print(log_msg % sync_url)
|
||||||
|
except MissingSchema:
|
||||||
|
log_msg = 'Invalid sync server url "%s".'
|
||||||
|
print(log_msg % sync_url)
|
||||||
|
|
||||||
# write settings back if good, clear if not
|
# write settings back if good, clear if not
|
||||||
if not args.no_config_file:
|
if not args.no_config_file:
|
||||||
config.read_dict(dict(user=dict(sync_url=sync_url, username=username),
|
|
||||||
keys=keys))
|
|
||||||
try:
|
try:
|
||||||
with config_file.open(mode='w+') as f:
|
with config_file.open(mode='w+') as f:
|
||||||
if login_success:
|
if login_success:
|
||||||
|
config.read_dict(dict(user=dict(sync_url=sync_url,
|
||||||
|
username=username),
|
||||||
|
keys=keys))
|
||||||
config.write(f)
|
config.write(f)
|
||||||
log_msg = 'Config written to file "%s".'
|
log_msg = 'Config written to file "%s".'
|
||||||
logging.info(log_msg % str(config_file))
|
logging.info(log_msg % str(config_file))
|
||||||
|
@ -168,10 +175,13 @@ def main():
|
||||||
|
|
||||||
if login_success:
|
if login_success:
|
||||||
logging.info('Starting FUSE filesystem.')
|
logging.info('Starting FUSE filesystem.')
|
||||||
|
try:
|
||||||
fuse = FUSE(StandardNotesFUSE(sn_api, sync_sec),
|
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) # FUSE can't make threads, but we can
|
||||||
|
except RuntimeError as e:
|
||||||
|
print('Error mounting file system.')
|
||||||
|
|
||||||
logging.info('Exiting.')
|
logging.info('Exiting.')
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user