mirror of
https://github.com/samschott/maestral.git
synced 2024-10-06 10:28:07 +03:00
moved auth from client to main
This commit is contained in:
parent
f572a8af64
commit
662f8193e4
@ -24,7 +24,6 @@ import dropbox
|
||||
|
||||
# maestral modules
|
||||
from maestral import __version__
|
||||
from maestral.oauth import OAuth2Session
|
||||
from maestral.config import MaestralState
|
||||
from maestral.errors import dropbox_to_maestral_error, os_to_maestral_error
|
||||
from maestral.errors import CursorResetError
|
||||
@ -134,22 +133,20 @@ class MaestralApiClient:
|
||||
:class:`ConnectionError`.
|
||||
|
||||
:param str config_name: Name of config file and state file to use.
|
||||
:param str access_token: Dropbox access token for user.
|
||||
:param int timeout: Timeout for individual requests in sec. Defaults to 60 sec.
|
||||
"""
|
||||
|
||||
SDK_VERSION = '2.0'
|
||||
_timeout = 60
|
||||
|
||||
def __init__(self, config_name='maestral', timeout=_timeout):
|
||||
def __init__(self, config_name, access_token, timeout=_timeout):
|
||||
|
||||
self.config_name = config_name
|
||||
|
||||
self._state = MaestralState(config_name)
|
||||
|
||||
# get Dropbox session
|
||||
self.auth = OAuth2Session(config_name)
|
||||
if not self.auth.load_token():
|
||||
self.auth.link()
|
||||
self._timeout = timeout
|
||||
self._last_longpoll = None
|
||||
self._backoff = 0
|
||||
@ -157,7 +154,7 @@ class MaestralApiClient:
|
||||
|
||||
# initialize API client
|
||||
self.dbx = dropbox.Dropbox(
|
||||
self.auth.access_token,
|
||||
access_token,
|
||||
session=SESSION,
|
||||
user_agent=USER_AGENT,
|
||||
timeout=self._timeout
|
||||
@ -218,9 +215,8 @@ class MaestralApiClient:
|
||||
@to_maestral_error()
|
||||
def unlink(self):
|
||||
"""
|
||||
Unlinks the Dropbox account and deletes local sync information.
|
||||
Unlinks the Dropbox account.
|
||||
"""
|
||||
self.auth.delete_creds()
|
||||
self.dbx.auth_token_revoke() # should only raise auth errors
|
||||
|
||||
@to_maestral_error(dbx_path_arg=1)
|
||||
|
@ -117,6 +117,11 @@ class FileSizeError(SyncError):
|
||||
|
||||
# ==== fatal errors, require user action for syncing to continue =========================
|
||||
|
||||
class NotLinkedError(MaestralApiError):
|
||||
"""Raised when authentication fails because the user's token has expired."""
|
||||
pass
|
||||
|
||||
|
||||
class InotifyError(MaestralApiError):
|
||||
"""Raised when the local Dropbox folder is too large to monitor with inotify."""
|
||||
pass
|
||||
|
259
maestral/main.py
259
maestral/main.py
@ -37,9 +37,10 @@ import sdnotify
|
||||
|
||||
# maestral modules
|
||||
from maestral import __version__
|
||||
from maestral.oauth import OAuth2Session
|
||||
from maestral.client import MaestralApiClient, to_maestral_error
|
||||
from maestral.sync import MaestralMonitor
|
||||
from maestral.errors import MaestralApiError
|
||||
from maestral.errors import MaestralApiError, NotLinkedError
|
||||
from maestral.config import MaestralConfig, MaestralState
|
||||
from maestral.utils.path import is_child, to_cased_path, delete
|
||||
from maestral.utils.notify import MaestralDesktopNotifier
|
||||
@ -145,10 +146,9 @@ class SdNotificationHandler(logging.Handler):
|
||||
# decorators
|
||||
|
||||
def with_sync_paused(func):
|
||||
"""
|
||||
Decorator which pauses syncing before a method call, resumes afterwards. This should
|
||||
only be used to decorate Maestral methods.
|
||||
"""
|
||||
"""Decorator which pauses syncing before a method call, resumes afterwards. This
|
||||
should only be used to decorate Maestral methods."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# pause syncing
|
||||
@ -164,6 +164,21 @@ def with_sync_paused(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
def require_linked(func):
|
||||
"""Decorator which raises a NotLinkedError if Maestral is not linked to a Dropbox
|
||||
account."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# pause syncing
|
||||
if self.pending_link:
|
||||
raise NotLinkedError('No linked Dropbox account',
|
||||
'Run "link" to link a new account.')
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# ========================================================================================
|
||||
# Main API
|
||||
# ========================================================================================
|
||||
@ -171,6 +186,8 @@ def with_sync_paused(func):
|
||||
class Maestral:
|
||||
"""The public API.
|
||||
|
||||
Some methods, such
|
||||
|
||||
All methods and properties return objects or raise exceptions which can safely be
|
||||
serialized, i.e., pure Python types. The only exception are instances of
|
||||
:class:`errors.MaestralApiError` which need to be registered explicitly with the
|
||||
@ -179,7 +196,8 @@ class Maestral:
|
||||
:param str config_name: Name of maestral configuration to run. This will create a new
|
||||
configuration file if none exists.
|
||||
:param bool run: If ``True``, Maestral will start syncing immediately. Defaults to
|
||||
``True``.
|
||||
``True``. If no Dropbox account is linked or no local folder has been set up, this
|
||||
will also run a command line setup dialog first.
|
||||
:param bool log_to_stdout: If ``True``, Maestral will print log messages to stdout.
|
||||
When started as a systemd services, this can result in duplicate log messages.
|
||||
Defaults to ``False``.
|
||||
@ -193,12 +211,39 @@ class Maestral:
|
||||
self._config_name = config_name
|
||||
self._conf = MaestralConfig(self._config_name)
|
||||
self._state = MaestralState(self._config_name)
|
||||
self._auth = OAuth2Session(self.config_name)
|
||||
|
||||
self._setup_logging()
|
||||
|
||||
self.client = MaestralApiClient(self._config_name)
|
||||
self.monitor = MaestralMonitor(self.client)
|
||||
self.sync = self.monitor.sync
|
||||
if self.pending_link:
|
||||
self.client = None
|
||||
self.monitor = None
|
||||
self.sync = None
|
||||
else:
|
||||
self.client = MaestralApiClient(self.config_name, self._auth.access_token)
|
||||
self.monitor = MaestralMonitor(self.client)
|
||||
self.sync = self.monitor.sync
|
||||
|
||||
if run:
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Runs setup if necessary, starts syncing, and starts systemd notifications if
|
||||
run as a systemd notify service. Any setup will be run as a CLI dialog and will
|
||||
block without user input. To avoid blocking with a CLI dialog, call :meth:`link`
|
||||
and :meth:`create_dropbox_directory` with the appropriate arguments first.
|
||||
"""
|
||||
|
||||
if self.pending_link:
|
||||
self.link()
|
||||
|
||||
if self.pending_dropbox_folder:
|
||||
self.monitor.reset_sync_state()
|
||||
self.create_dropbox_directory()
|
||||
|
||||
# start syncing
|
||||
self.start_sync()
|
||||
|
||||
# periodically check for updates and refresh account info
|
||||
self.update_thread = Thread(
|
||||
@ -208,22 +253,6 @@ class Maestral:
|
||||
)
|
||||
self.update_thread.start()
|
||||
|
||||
if run:
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Runs setup if necessary, starts syncing, and starts systemd notifications if run
|
||||
as a systemd notify service.
|
||||
"""
|
||||
|
||||
if self.pending_dropbox_folder:
|
||||
self.monitor.reset_sync_state()
|
||||
self.create_dropbox_directory()
|
||||
|
||||
# start syncing
|
||||
self.start_sync()
|
||||
|
||||
if NOTIFY_SOCKET: # notify systemd that we have started
|
||||
logger.debug('Running as systemd notify service')
|
||||
logger.debug('NOTIFY_SOCKET = %s', NOTIFY_SOCKET)
|
||||
@ -241,6 +270,95 @@ class Maestral:
|
||||
)
|
||||
self.watchdog_thread.start()
|
||||
|
||||
def get_auth_url(self):
|
||||
"""
|
||||
Returns a URL to authorize access to a Dropbox account. To link a Dropbox
|
||||
account, retrieve an auth token from the URL and link Maestral by calling
|
||||
:meth:`link` with the privided token.
|
||||
|
||||
:returns: URL to retrieve an OAuth token.
|
||||
:rtype: str
|
||||
"""
|
||||
return self._auth.get_auth_url()
|
||||
|
||||
def link(self, token=None, relink=False):
|
||||
"""
|
||||
Links Maestral with a Dropbox account. This uses the given auth token or performs
|
||||
the full auth flow in a CLI dialog.
|
||||
|
||||
:param Optional[str] token: OAuth token for Dropbox access.
|
||||
:param bool relink: If ``True``, allows relinking an already linked account, for
|
||||
instance if the auth token expired.
|
||||
:returns: OAuth2Session.Success, OAuth2Session.InvalidToken or OAuth2Session.ConnectionFailed
|
||||
:rtype: int
|
||||
"""
|
||||
if not (self.pending_link or relink):
|
||||
raise RuntimeError('Already linked')
|
||||
|
||||
if token:
|
||||
res = self._auth.verify_auth_token(token)
|
||||
|
||||
else:
|
||||
authorize_url = self.get_auth_url()
|
||||
click.echo('1. Go to: ' + authorize_url)
|
||||
click.echo('2. Click "Allow" (you may have to log in first).')
|
||||
click.echo('3. Copy the authorization token.')
|
||||
|
||||
res = self._auth.InvalidToken
|
||||
while res != self._auth.Success:
|
||||
auth_code = click.prompt('Enter the authorization token here', type=str)
|
||||
auth_code = auth_code.strip()
|
||||
res = self._auth.verify_auth_token(auth_code)
|
||||
|
||||
if res == self._auth.InvalidToken:
|
||||
click.secho('Invalid token. Please try again.', fg='red')
|
||||
elif res == self._auth.ConnectionFailed:
|
||||
click.secho('Could not connect to Dropbox. Please try again.',
|
||||
fg='red')
|
||||
|
||||
if res == self._auth.Success:
|
||||
self._auth.save_creds()
|
||||
|
||||
self.client = MaestralApiClient(self.config_name, self._auth.access_token)
|
||||
self.monitor = MaestralMonitor(self.client)
|
||||
self.sync = self.monitor.sync
|
||||
|
||||
self.get_account_info()
|
||||
|
||||
return res
|
||||
|
||||
@require_linked
|
||||
def unlink(self):
|
||||
"""
|
||||
Unlinks the configured Dropbox account but leaves all downloaded files in place.
|
||||
All syncing metadata will be removed as well. Connection and API errors will be
|
||||
handled silently but the Dropbox access key will always be removed from the
|
||||
user's PC.
|
||||
"""
|
||||
|
||||
self.stop_sync()
|
||||
|
||||
# revoke and delete token
|
||||
try:
|
||||
self.client.unlink()
|
||||
except (ConnectionError, MaestralApiError):
|
||||
pass
|
||||
|
||||
self._auth.delete_creds()
|
||||
|
||||
# clean up config + state
|
||||
try:
|
||||
os.remove(self.sync.rev_file_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.sync.clear_rev_index()
|
||||
delete(self.sync.rev_file_path)
|
||||
self._conf.cleanup()
|
||||
self._state.cleanup()
|
||||
|
||||
logger.info('Unlinked Dropbox account.')
|
||||
|
||||
def _setup_logging(self):
|
||||
"""
|
||||
Sets up logging to log files, status and error properties, desktop notifications,
|
||||
@ -345,13 +463,15 @@ class Maestral:
|
||||
# ==== getters / setters for config with side effects ================================
|
||||
|
||||
@property
|
||||
@require_linked
|
||||
def dropbox_path(self):
|
||||
"""Returns the path to the local Dropbox directory (read only). Use
|
||||
:meth:`create_dropbox_directory` or :meth:`move_dropbox_directory` to set or
|
||||
change the Dropbox directory location instead. """
|
||||
change the Dropbox directory location instead."""
|
||||
return self.sync.dropbox_path
|
||||
|
||||
@property
|
||||
@require_linked
|
||||
def excluded_items(self):
|
||||
"""
|
||||
Returns a list of excluded folders (read only). Use :meth:`exclude_item`,
|
||||
@ -427,6 +547,11 @@ class Maestral:
|
||||
|
||||
# ==== state information ============================================================
|
||||
|
||||
@property
|
||||
def pending_link(self):
|
||||
"""Bool indicating if Maestral is linked to a Dropbox account."""
|
||||
return self._auth.load_token() is None
|
||||
|
||||
@property
|
||||
def pending_dropbox_folder(self):
|
||||
"""Bool indicating if a local Dropbox directory has been created."""
|
||||
@ -444,23 +569,39 @@ class Maestral:
|
||||
Bool indicating if Maestral is syncing. It will be ``True`` if syncing is not
|
||||
paused by the user *and* Maestral is connected to the internet.
|
||||
"""
|
||||
return self.monitor.syncing.is_set() or self.monitor.startup.is_set()
|
||||
|
||||
if self.pending_link:
|
||||
return False
|
||||
else:
|
||||
return not self.monitor.syncing.is_set() or self.monitor.startup.is_set()
|
||||
|
||||
@property
|
||||
def paused(self):
|
||||
"""Bool indicating if syncing is paused by the user. This is set by
|
||||
calling :meth:`pause`."""
|
||||
return self.monitor.paused_by_user.is_set() and not self.sync.lock.locked()
|
||||
|
||||
if self.pending_link:
|
||||
return False
|
||||
else:
|
||||
return self.monitor.paused_by_user.is_set() and not self.sync.lock.locked()
|
||||
|
||||
@property
|
||||
def stopped(self):
|
||||
"""Bool indicating if syncing is stopped, for instance because of an exception."""
|
||||
return not self.monitor.running.is_set() and not self.sync.lock.locked()
|
||||
|
||||
if self.pending_link:
|
||||
return True
|
||||
else:
|
||||
return not self.monitor.running.is_set() and not self.sync.lock.locked()
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Bool indicating if Dropbox servers can be reached."""
|
||||
return self.monitor.connected.is_set()
|
||||
|
||||
if self.pending_link:
|
||||
return False
|
||||
else:
|
||||
return self.monitor.connected.is_set()
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
@ -471,8 +612,10 @@ class Maestral:
|
||||
return self._log_handler_info_cache.getLastMessage()
|
||||
|
||||
@property
|
||||
@require_linked
|
||||
def sync_errors(self):
|
||||
"""Returns list of current sync errors as dicts."""
|
||||
|
||||
sync_errors = list(self.sync.sync_errors.queue)
|
||||
sync_errors_dicts = [error_to_dict(e) for e in sync_errors]
|
||||
return sync_errors_dicts
|
||||
@ -538,6 +681,7 @@ class Maestral:
|
||||
else:
|
||||
return FileStatus.Unwatched.value
|
||||
|
||||
@require_linked
|
||||
def get_activity(self):
|
||||
"""
|
||||
Gets current upload / download activity.
|
||||
@ -546,6 +690,7 @@ class Maestral:
|
||||
uploaded or downloaded. Paths are given relative to the Dropbox folder.
|
||||
:rtype: dict(list, list)
|
||||
"""
|
||||
|
||||
PathItem = namedtuple('PathItem', 'path status')
|
||||
uploading = []
|
||||
downloading = []
|
||||
@ -568,6 +713,7 @@ class Maestral:
|
||||
|
||||
return dict(uploading=uploading, downloading=downloading)
|
||||
|
||||
@require_linked
|
||||
def get_account_info(self):
|
||||
"""
|
||||
Gets account information from Dropbox and returns it as a dictionary. The entries
|
||||
@ -577,9 +723,11 @@ class Maestral:
|
||||
:rtype: dict[str, bool]
|
||||
:raises: :class:`errors.MaestralApiError`, :class:`ConnectionError`
|
||||
"""
|
||||
|
||||
res = self.client.get_account_info()
|
||||
return dropbox_stone_to_dict(res)
|
||||
|
||||
@require_linked
|
||||
def get_space_usage(self):
|
||||
"""
|
||||
Gets the space usage stored by Dropbox and returns it as a dictionary. The
|
||||
@ -589,11 +737,13 @@ class Maestral:
|
||||
:rtype: dict[str, bool]
|
||||
:raises: :class:`errors.MaestralApiError`, :class:`ConnectionError`
|
||||
"""
|
||||
|
||||
res = self.client.get_space_usage()
|
||||
return dropbox_stone_to_dict(res)
|
||||
|
||||
# ==== control methods for front ends ================================================
|
||||
|
||||
@require_linked
|
||||
@to_maestral_error() # to handle errors when downloading and saving profile pic
|
||||
def get_profile_pic(self):
|
||||
"""
|
||||
@ -615,6 +765,7 @@ class Maestral:
|
||||
else:
|
||||
self._delete_old_profile_pics()
|
||||
|
||||
@require_linked
|
||||
def list_folder(self, dbx_path, **kwargs):
|
||||
"""
|
||||
List all items inside the folder given by ``dbx_path``. Keyword arguments are
|
||||
@ -626,6 +777,7 @@ class Maestral:
|
||||
:rtype: list[dict]
|
||||
:raises: :class:`errors.MaestralApiError`, :class:`ConnectionError`
|
||||
"""
|
||||
|
||||
res = self.client.list_folder(dbx_path, **kwargs)
|
||||
|
||||
entries = [dropbox_stone_to_dict(e) for e in res.entries]
|
||||
@ -640,6 +792,7 @@ class Maestral:
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@require_linked
|
||||
def rebuild_index(self):
|
||||
"""
|
||||
Rebuilds the rev file by comparing remote with local files and updating rev
|
||||
@ -654,32 +807,41 @@ class Maestral:
|
||||
|
||||
self.monitor.rebuild_index()
|
||||
|
||||
@require_linked
|
||||
def start_sync(self):
|
||||
"""
|
||||
Creates syncing threads and starts syncing. This will be called by :meth:`run`
|
||||
and typically does not need to be called manually.
|
||||
"""
|
||||
|
||||
self.monitor.start()
|
||||
|
||||
@require_linked
|
||||
def resume_sync(self):
|
||||
"""
|
||||
Resumes the syncing threads if paused.
|
||||
"""
|
||||
|
||||
self.monitor.resume()
|
||||
|
||||
@require_linked
|
||||
def pause_sync(self):
|
||||
"""
|
||||
Pauses the syncing threads if running.
|
||||
"""
|
||||
|
||||
self.monitor.pause()
|
||||
|
||||
@require_linked
|
||||
def stop_sync(self):
|
||||
"""
|
||||
Stops the syncing threads if running. Call :meth:`start_sync` to restart all
|
||||
threads.
|
||||
"""
|
||||
|
||||
self.monitor.stop()
|
||||
|
||||
@require_linked
|
||||
def reset_sync_state(self):
|
||||
"""
|
||||
Resets the sync index and state. Only call this to clean up leftover state
|
||||
@ -689,31 +851,7 @@ class Maestral:
|
||||
|
||||
self.monitor.reset_sync_state()
|
||||
|
||||
def unlink(self):
|
||||
"""
|
||||
Unlinks the configured Dropbox account but leaves all downloaded files in place.
|
||||
All syncing metadata will be removed as well. Connection and API errors will be
|
||||
handled silently but the Dropbox access key will always be removed from the
|
||||
user's PC.
|
||||
"""
|
||||
self.stop_sync()
|
||||
try:
|
||||
self.client.unlink()
|
||||
except (ConnectionError, MaestralApiError):
|
||||
pass
|
||||
|
||||
try:
|
||||
os.remove(self.sync.rev_file_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.sync.clear_rev_index()
|
||||
delete(self.sync.rev_file_path)
|
||||
self._conf.cleanup()
|
||||
self._state.cleanup()
|
||||
|
||||
logger.info('Unlinked Dropbox account.')
|
||||
|
||||
@require_linked
|
||||
def exclude_item(self, dbx_path):
|
||||
"""
|
||||
Excludes file or folder from sync and deletes it locally. It is safe to call this
|
||||
@ -765,6 +903,7 @@ class Maestral:
|
||||
event_types=(EVENT_TYPE_DELETED,)):
|
||||
delete(local_path)
|
||||
|
||||
@require_linked
|
||||
def include_item(self, dbx_path):
|
||||
"""
|
||||
Includes file or folder in sync and downloads in the background. It is safe to
|
||||
@ -815,6 +954,7 @@ class Maestral:
|
||||
for folder in new_included_items:
|
||||
self.sync.queued_newly_included_downloads.put(folder)
|
||||
|
||||
@require_linked
|
||||
def set_excluded_items(self, items=None):
|
||||
"""
|
||||
Sets the list of excluded files or folders. If not given, gets all top level
|
||||
@ -862,6 +1002,7 @@ class Maestral:
|
||||
|
||||
logger.info(IDLE)
|
||||
|
||||
@require_linked
|
||||
def excluded_status(self, dbx_path):
|
||||
"""
|
||||
Returns 'excluded', 'partially excluded' or 'included'. This function will not
|
||||
@ -883,6 +1024,7 @@ class Maestral:
|
||||
else:
|
||||
return 'included'
|
||||
|
||||
@require_linked
|
||||
@with_sync_paused
|
||||
def move_dropbox_directory(self, new_path=None):
|
||||
"""
|
||||
@ -896,8 +1038,7 @@ class Maestral:
|
||||
|
||||
# get old and new paths
|
||||
old_path = self.sync.dropbox_path
|
||||
new_path = new_path or select_dbx_path_dialog(self._config_name,
|
||||
allow_merge=False)
|
||||
new_path = new_path or select_dbx_path_dialog(self._config_name)
|
||||
|
||||
try:
|
||||
if osp.samefile(old_path, new_path):
|
||||
@ -917,6 +1058,7 @@ class Maestral:
|
||||
# update config file and client
|
||||
self.sync.dropbox_path = new_path
|
||||
|
||||
@require_linked
|
||||
@with_sync_paused
|
||||
def create_dropbox_directory(self, path=None):
|
||||
"""
|
||||
@ -926,6 +1068,7 @@ class Maestral:
|
||||
user will be prompted to input the path.
|
||||
:raises: :class:`OSError` if creation fails
|
||||
"""
|
||||
|
||||
path = path or select_dbx_path_dialog(self._config_name, allow_merge=True)
|
||||
|
||||
# create new folder
|
||||
@ -936,6 +1079,7 @@ class Maestral:
|
||||
|
||||
# ==== utility methods for front ends ================================================
|
||||
|
||||
@require_linked
|
||||
def to_local_path(self, dbx_path):
|
||||
"""
|
||||
Converts a path relative to the Dropbox folder to a correctly cased local file
|
||||
@ -945,6 +1089,7 @@ class Maestral:
|
||||
:returns: Corresponding path on local hard drive.
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
return self.sync.to_local_path(dbx_path)
|
||||
|
||||
@staticmethod
|
||||
|
@ -33,8 +33,7 @@ class OAuth2Session:
|
||||
To authenticate with Dropbox, run :meth:`get_auth_url`` first and direct the user to
|
||||
visit that URL and retrieve an auth token. Verify the provided auth token with
|
||||
:meth:`verify_auth_token` and save it in the system keyring together with the
|
||||
corresponding Dropbox ID by calling :meth:`save_creds`. The convenience method
|
||||
:meth:`link` runs through the above auth flow in a command line user dialog.
|
||||
corresponding Dropbox ID by calling :meth:`save_creds`.
|
||||
|
||||
This will currently use PKCE if available and fall back to the implicit grant flow
|
||||
implemented in :mod:`utils.oauth_implicit` otherwise.
|
||||
@ -129,29 +128,6 @@ class OAuth2Session:
|
||||
logger.error('Could not access the user keyring to save your authentication '
|
||||
'token. Please make sure that the keyring is unlocked.')
|
||||
|
||||
def link(self):
|
||||
"""
|
||||
Command line flow to get an auth key from Dropbox and save it in the system
|
||||
keyring.
|
||||
"""
|
||||
authorize_url = self.get_auth_url()
|
||||
click.echo('1. Go to: ' + authorize_url)
|
||||
click.echo('2. Click "Allow" (you might have to log in first).')
|
||||
click.echo('3. Copy the authorization token.')
|
||||
|
||||
res = self.InvalidToken
|
||||
while res != self.Success:
|
||||
auth_code = click.prompt('Enter the authorization token here', type=str)
|
||||
auth_code = auth_code.strip()
|
||||
res = self.verify_auth_token(auth_code)
|
||||
|
||||
if res == self.InvalidToken:
|
||||
click.secho('Invalid token. Please try again.', fg='red')
|
||||
elif res == self.ConnectionFailed:
|
||||
click.secho('Could not connect to Dropbox. Please try again.', fg='red')
|
||||
|
||||
self.save_creds()
|
||||
|
||||
def delete_creds(self):
|
||||
"""Deletes auth key from system keyring."""
|
||||
self._conf.set('account', 'account_id', "")
|
||||
|
Loading…
Reference in New Issue
Block a user