You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

190 lines
6.9KB

  1. import argparse
  2. from configparser import ConfigParser
  3. from getpass import getpass
  4. import logging
  5. import os
  6. import pathlib
  7. import sys
  8. import appdirs
  9. from fuse import FUSE
  10. from requests.exceptions import ConnectionError, MissingSchema
  11. from api import SNAPIException, StandardNotesAPI
  12. from sn_fuse import StandardNotesFUSE
  13. OFFICIAL_SERVER_URL = 'https://sync.standardnotes.org'
  14. DEFAULT_SYNC_SEC = 30
  15. MINIMUM_SYNC_SEC = 5
  16. APP_NAME = 'standardnotes-fs'
  17. # path settings
  18. cfg_env = os.environ.get('SN_FS_CONFIG_PATH')
  19. CONFIG_PATH = cfg_env if cfg_env else appdirs.user_config_dir(APP_NAME)
  20. CONFIG_FILE = pathlib.PurePath(CONFIG_PATH, APP_NAME + '.conf')
  21. def parse_options():
  22. parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
  23. parser.add_argument('mountpoint', nargs='?', help='local mountpoint folder')
  24. parser.add_argument('--username',
  25. help='Standard Notes username to log in with')
  26. parser.add_argument('--password',
  27. help='Standard Notes password to log in with\n'
  28. 'NOTE: It is NOT recommended to use this option!\n'
  29. ' The password may be stored in history, so\n'
  30. ' use the password prompt instead.')
  31. parser.add_argument('-v', '--verbosity', action='count',
  32. help='output verbosity -v or -vv (implies --foreground)')
  33. parser.add_argument('--foreground', action='store_true',
  34. help='run standardnotes-fs in the foreground')
  35. parser.add_argument('--sync-sec', type=int, default=DEFAULT_SYNC_SEC,
  36. help='how many seconds between each sync. Default: '
  37. ''+str(DEFAULT_SYNC_SEC))
  38. parser.add_argument('--sync-url',
  39. help='URL of Standard File sync server. Defaults to:\n'
  40. ''+OFFICIAL_SERVER_URL)
  41. parser.add_argument('--no-config-file', action='store_true',
  42. help='don\'t load or create a config file')
  43. parser.add_argument('--config', default=CONFIG_FILE,
  44. help='specify a config file location. Defaults to:\n'
  45. ''+str(CONFIG_FILE))
  46. parser.add_argument('--logout', action='store_true',
  47. help='delete login credentials saved in config and quit')
  48. return parser.parse_args()
  49. def main():
  50. args = parse_options()
  51. config = ConfigParser()
  52. keys = {}
  53. login_success = False
  54. # configure logging
  55. if args.verbosity == 1:
  56. log_level = logging.INFO
  57. elif args.verbosity == 2:
  58. log_level = logging.DEBUG
  59. else:
  60. log_level = logging.ERROR
  61. logging.basicConfig(level=log_level,
  62. format='%(levelname)-8s: %(message)s')
  63. if args.verbosity: args.foreground = True
  64. # figure out config file
  65. config_file = pathlib.Path(args.config)
  66. # logout and quit if wanted
  67. if args.logout:
  68. try:
  69. config_file.unlink()
  70. print('Config file deleted and logged out.')
  71. except OSError:
  72. logging.info('Already logged out.')
  73. sys.exit(0)
  74. # make sure mountpoint is specified
  75. if not args.mountpoint:
  76. print('No mountpoint specified.')
  77. sys.exit(1)
  78. # keep sync_sec above the minimum sync time
  79. if args.sync_sec < MINIMUM_SYNC_SEC:
  80. sync_sec = MINIMUM_SYNC_SEC
  81. print('Sync interval must be at least', MINIMUM_SYNC_SEC,
  82. 'seconds. Using that instead.')
  83. else:
  84. sync_sec = args.sync_sec
  85. # load config file settings
  86. if not args.no_config_file:
  87. try:
  88. config_file.parent.mkdir(mode=0o0700, parents=True, exist_ok=True)
  89. log_msg = 'Using config directory "%s".'
  90. logging.info(log_msg % str(config_file.parent))
  91. except OSError:
  92. log_msg = 'Error creating config file directory "%s".'
  93. print(log_msg % str(config_file.parent))
  94. sys.exit(1)
  95. try:
  96. with config_file.open() as f:
  97. config.read_file(f)
  98. log_msg = 'Loaded config file "%s".'
  99. logging.info(log_msg % str(config_file))
  100. except OSError:
  101. log_msg = 'Unable to read config file "%s".'
  102. logging.info(log_msg % str(config_file))
  103. # figure out all login params
  104. if args.sync_url:
  105. sync_url = args.sync_url
  106. elif config.has_option('user', 'sync_url'):
  107. sync_url = config.get('user', 'sync_url')
  108. else:
  109. sync_url = OFFICIAL_SERVER_URL
  110. log_msg = 'Using sync URL "%s".'
  111. logging.info(log_msg % sync_url)
  112. if (config.has_option('user', 'username')
  113. and config.has_section('keys')
  114. and not args.username
  115. and not args.password):
  116. username = config.get('user', 'username')
  117. keys = dict(config.items('keys'))
  118. else:
  119. username = (args.username if args.username else
  120. input('Please enter your Standard Notes username: '))
  121. password = (args.password if args.password else
  122. getpass('Please enter your password (hidden): '))
  123. # log the user in
  124. try:
  125. sn_api = StandardNotesAPI(sync_url, username)
  126. if not keys:
  127. keys = sn_api.gen_keys(password)
  128. del password
  129. sn_api.sign_in(keys)
  130. log_msg = 'Successfully logged into account "%s".'
  131. logging.info(log_msg % username)
  132. login_success = True
  133. except SNAPIException as e:
  134. print(e)
  135. except ConnectionError:
  136. log_msg = 'Unable to connect to the sync server at "%s".'
  137. print(log_msg % sync_url)
  138. except MissingSchema:
  139. log_msg = 'Invalid sync server url "%s".'
  140. print(log_msg % sync_url)
  141. # write settings back if good, clear if not
  142. if not args.no_config_file:
  143. try:
  144. with config_file.open(mode='w+') as f:
  145. if login_success:
  146. config.read_dict(dict(user=dict(sync_url=sync_url,
  147. username=username),
  148. keys=keys))
  149. config.write(f)
  150. log_msg = 'Config written to file "%s".'
  151. else:
  152. log_msg = 'Clearing config file "%s".'
  153. logging.info(log_msg % config_file)
  154. config_file.chmod(0o600)
  155. except OSError:
  156. log_msg = 'Unable to write config file "%s".'
  157. logging.warning(log_msg % str(config_file))
  158. if login_success:
  159. logging.info('Starting FUSE filesystem.')
  160. try:
  161. fuse = FUSE(StandardNotesFUSE(sn_api, sync_sec),
  162. args.mountpoint,
  163. foreground=args.foreground,
  164. nothreads=True) # FUSE can't make threads, but we can
  165. except RuntimeError as e:
  166. print('Error mounting file system.')
  167. logging.info('Exiting.')
  168. if __name__ == '__main__':
  169. main()