Work with binary data and catch more exceptions

This commit is contained in:
Tanner Collin 2017-10-13 18:46:22 -06:00
parent be39849693
commit c7309c4136
4 changed files with 51 additions and 19 deletions

11
api.py
View File

@ -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):

View File

@ -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):

View File

@ -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()
self.item_manager.syncItems() try:
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)

View File

@ -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.')
fuse = FUSE(StandardNotesFUSE(sn_api, sync_sec), try:
args.mountpoint, fuse = FUSE(StandardNotesFUSE(sn_api, sync_sec),
foreground=args.foreground, args.mountpoint,
nothreads=True) # benefits don't outweigh the costs foreground=args.foreground,
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.')