Вы не можете выбрать более 25 тем
Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
189 строки
6.9 KiB
189 строки
6.9 KiB
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()
|
|
|