import argparse from configparser import ConfigParser from getpass import getpass import logging import os import pathlib import sys import appdirs from fuse import FUSE from requests.exceptions import ConnectionError, MissingSchema from api import SNAPIException, StandardNotesAPI from sn_fuse import StandardNotesFUSE OFFICIAL_SERVER_URL = 'https://sync.standardnotes.org' DEFAULT_SYNC_SEC = 30 MINIMUM_SYNC_SEC = 5 APP_NAME = 'standardnotes-fs' # path settings cfg_env = os.environ.get('SN_FS_CONFIG_PATH') CONFIG_PATH = cfg_env if cfg_env else appdirs.user_config_dir(APP_NAME) CONFIG_FILE = pathlib.PurePath(CONFIG_PATH, APP_NAME + '.conf') def parse_options(): parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('mountpoint', nargs='?', help='local mountpoint folder') parser.add_argument('--username', help='Standard Notes username to log in with') parser.add_argument('--password', help='Standard Notes password to log in with\n' 'NOTE: It is NOT recommended to use this option!\n' ' The password may be stored in history, so\n' ' use the password prompt instead.') parser.add_argument('-v', '--verbosity', action='count', 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: ' ''+str(DEFAULT_SYNC_SEC)) 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', default=CONFIG_FILE, help='specify a config file location. Defaults to:\n' ''+str(CONFIG_FILE)) parser.add_argument('--logout', action='store_true', help='delete login credentials saved in config and quit') return parser.parse_args() def main(): args = parse_options() config = ConfigParser() keys = {} login_success = False # configure logging if args.verbosity == 1: log_level = logging.INFO elif args.verbosity == 2: log_level = logging.DEBUG else: log_level = logging.ERROR logging.basicConfig(level=log_level, format='%(levelname)-8s: %(message)s') if args.verbosity: args.foreground = True # figure out config file config_file = pathlib.Path(args.config) # logout and quit if wanted if args.logout: try: config_file.unlink() print('Config file deleted and logged out.') except OSError: logging.info('Already logged out.') sys.exit(0) # make sure mountpoint is specified if not args.mountpoint: 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: config_file.parent.mkdir(mode=0o0700, parents=True, exist_ok=True) log_msg = 'Using config directory "%s".' logging.info(log_msg % str(config_file.parent)) except OSError: log_msg = 'Error creating config file directory "%s".' print(log_msg % str(config_file.parent)) sys.exit(1) try: with config_file.open() as f: config.read_file(f) log_msg = 'Loaded config file "%s".' logging.info(log_msg % str(config_file)) except OSError: log_msg = 'Unable to read config file "%s".' logging.info(log_msg % str(config_file)) # figure out all login params if args.sync_url: sync_url = args.sync_url elif config.has_option('user', 'sync_url'): sync_url = config.get('user', 'sync_url') else: sync_url = OFFICIAL_SERVER_URL log_msg = 'Using sync URL "%s".' logging.info(log_msg % sync_url) if (config.has_option('user', 'username') and config.has_section('keys') and not args.username and not args.password): username = config.get('user', 'username') keys = dict(config.items('keys')) else: username = (args.username if args.username else input('Please enter your Standard Notes username: ')) password = (args.password if args.password else getpass('Please enter your password (hidden): ')) # log the user in try: sn_api = StandardNotesAPI(sync_url, username) if not keys: keys = sn_api.gen_keys(password) del password sn_api.sign_in(keys) log_msg = 'Successfully logged into account "%s".' logging.info(log_msg % username) login_success = True except SNAPIException as e: print(e) except ConnectionError: 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 if not args.no_config_file: try: with config_file.open(mode='w+') as f: if login_success: config.read_dict(dict(user=dict(sync_url=sync_url, username=username), keys=keys)) config.write(f) log_msg = 'Config written to file "%s".' else: log_msg = 'Clearing config file "%s".' logging.info(log_msg % config_file) config_file.chmod(0o600) except OSError: log_msg = 'Unable to write config file "%s".' logging.warning(log_msg % str(config_file)) if login_success: logging.info('Starting FUSE filesystem.') try: fuse = FUSE(StandardNotesFUSE(sn_api, sync_sec), args.mountpoint, 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.') if __name__ == '__main__': main()