def setup_test_config( config_name: str = "test-config", access_token: Optional[str] = env_token ) -> Maestral: """ Sets up a new maestral configuration and links it to a Dropbox account with the given token. Creates a new local Dropbox folder for the config. The token must be an "access token" which can be used to directly make Dropbox API calls and not a "refresh token". Both short lived and long lived access token will work but short lived tokens must not expire before the tests are complete. :param config_name: Config name to use or create. :param access_token: The access token to use to link the config to an account. :returns: A linked Maestral instance. """ m = Maestral(config_name) m.log_level = logging.DEBUG # link with given token m.client._init_sdk_with_token(access_token=access_token) # get corresponding Dropbox ID and store in keyring for other processes res = m.client.get_account_info() m.client.auth._account_id = res.account_id m.client.auth._access_token = access_token m.client.auth._token_access_type = "legacy" m.client.auth.save_creds() home = get_home_dir() local_dropbox_dir = generate_cc_name( os.path.join(home, "Dropbox"), suffix="test runner" ) m.create_dropbox_directory(local_dropbox_dir) return m
def cleanup_test_config(m: Maestral, test_folder_dbx: Optional[str] = None) -> None: """ Shuts down syncing for the given Maestral instance, removes all local files and folders related to that instance, including the local Dropbox folder, and removes any '.mignore' files. :param m: Maestral instance. :param test_folder_dbx: Optional test folder to clean up. """ # stop syncing and clean up remote folder m.stop_sync() if test_folder_dbx: try: m.client.remove(test_folder_dbx) except NotFoundError: pass try: m.client.remove("/.mignore") except NotFoundError: pass # remove creds from system keyring m.client.auth.delete_creds() # remove local files and folders delete(m.dropbox_path) remove_configuration("test-config")
def on_verify_token_finished(self, res): if res == OAuth2Session.Success: self.auth_session.save_creds() # switch to next page self.stackedWidget.slideInIdx(2) self.pussButtonDropboxPathSelect.setFocus() self.lineEditAuthCode.clear( ) # clear since we might come back on unlink # start Maestral after linking to Dropbox account self.mdbx = Maestral(run=False) self.mdbx.client.get_account_info() elif res == OAuth2Session.InvalidToken: msg = "Please make sure that you entered the correct authentication token." msg_box = UserDialog("Authentication failed.", msg, parent=self) msg_box.open() elif res == OAuth2Session.ConnectionFailed: msg = "Please make sure that you are connected to the internet and try again." msg_box = UserDialog("Connection failed.", msg, parent=self) msg_box.open() self.progressIndicator.stopAnimation() self.pushButtonAuthPageLink.setEnabled(True) self.lineEditAuthCode.setEnabled(True)
def __init__(self, config_name: str = "maestral", fallback: bool = False) -> None: self._config_name = config_name self._is_fallback = False if is_running(config_name): sock_name = sockpath_for_config(config_name) # print remote tracebacks locally sys.excepthook = Pyro5.errors.excepthook self._m = Proxy(URI.format(config_name, "./u:" + sock_name)) try: self._m._pyroBind() except CommunicationError: self._m._pyroRelease() raise else: # If daemon is not running, fall back to new Maestral instance # or raise a CommunicationError if fallback not allowed. if fallback: from maestral.main import Maestral self._m = Maestral(config_name) else: raise CommunicationError("Could not get proxy") self._is_fallback = not isinstance(self._m, Proxy)
def unlink(config_name: str) -> None: if click.confirm("Are you sure you want unlink your account?"): from maestral.main import Maestral stop_daemon_with_cli_feedback(config_name) m = Maestral(config_name) m.unlink() click.echo("Unlinked Maestral.")
def unlink(config_name: str): """Unlinks your Dropbox account.""" if not pending_link_cli(config_name): from maestral.main import Maestral stop_daemon_with_cli_feedback(config_name) m = Maestral(config_name, run=False) m.unlink() click.echo('Unlinked Maestral.')
def start(config_name: str, foreground: bool): """Starts the Maestral as a daemon.""" from maestral.daemon import get_maestral_pid from maestral.utils.backend import pending_dropbox_folder # do nothing if already running if get_maestral_pid(config_name): click.echo('Maestral daemon is already running.') return from maestral.main import Maestral # run setup if not yet done if pending_link_cli(config_name) or pending_dropbox_folder(config_name): m = Maestral(config_name, run=False) m.reset_sync_state() m.create_dropbox_directory() m.set_excluded_items() del m # start daemon if foreground: from maestral.daemon import run_maestral_daemon run_maestral_daemon(config_name, run=True, log_to_stdout=True) else: start_daemon_subprocess_with_cli_feedback(config_name)
def get_maestral_proxy(config_name='maestral', fallback=False): """ Returns a Pyro proxy of the a running Maestral instance. If ``fallback`` is ``True``, a new instance of Maestral will be returned when the daemon cannot be reached. :param str config_name: The name of the Maestral configuration to use. :param bool fallback: If ``True``, a new instance of Maestral will be returned when the daemon cannot be reached. Defaults to ``False``. :returns: Pyro proxy of Maestral or a new instance. :raises: ``Pyro5.errors.CommunicationError`` if the daemon cannot be reached and ``fallback`` is ``False``. """ pid = get_maestral_pid(config_name) if pid: sock_name = sockpath_for_config(config_name) sys.excepthook = Pyro5.errors.excepthook maestral_daemon = Proxy(URI.format(config_name, './u:' + sock_name)) try: maestral_daemon._pyroBind() return maestral_daemon except Pyro5.errors.CommunicationError: maestral_daemon._pyroRelease() if fallback: from maestral.main import Maestral m = Maestral(config_name, run=False) m.log_handler_stream.setLevel(logging.CRITICAL) return m else: raise Pyro5.errors.CommunicationError
def is_maestral_linked(config_name): """ This does not create a Maestral instance and is therefore safe to call from anywhere at any time. """ os.environ["MAESTRAL_CONFIG"] = config_name from maestral.main import Maestral if Maestral.pending_link(): click.echo("No Dropbox account linked.") return False else: return True
def set_dir(config_name: str, new_path: str, running): """Change the location of your Dropbox folder.""" if running == "gui": click.echo("Maestral GUI is already running. Please use the GUI.") return if is_maestral_linked(config_name): from maestral.main import Maestral with MaestralProxy(config_name, fallback=True) as m: if not new_path: new_path = Maestral._ask_for_path() m.move_dropbox_directory(new_path) click.echo("Dropbox folder moved to {}.".format(new_path))
def start(config_name: str, foreground: bool, verbose: bool): """Starts the Maestral as a daemon.""" from maestral.daemon import get_maestral_pid from maestral.utils.backend import pending_dropbox_folder # do nothing if already running if get_maestral_pid(config_name): click.echo('Maestral daemon is already running.') return # run setup if not yet done if pending_link_cli(config_name) or pending_dropbox_folder(config_name): from maestral.main import Maestral m = Maestral(config_name, run=False) m.reset_sync_state() m.create_dropbox_directory() exclude_folders_q = click.confirm( 'Would you like to exclude any folders from syncing?', default=False, ) if exclude_folders_q: click.echo( 'Please choose which top-level folders to exclude. You can exclude\n' 'individual files or subfolders later with "maestral excluded add".' ) m.set_excluded_items() del m # start daemon if foreground: from maestral.daemon import run_maestral_daemon run_maestral_daemon(config_name, run=True, log_to_stdout=verbose) else: start_daemon_subprocess_with_cli_feedback(config_name, log_to_stdout=verbose)
def link(config_name: str, running): """Links Maestral with your Dropbox account.""" if not is_maestral_linked(config_name): if running == "gui": click.echo( "Maestral GUI is already running. Please link through the GUI." ) return if running == "daemon": # stop daemon stop_maestral_daemon(config_name) from maestral.main import Maestral Maestral(run=False) if running == "daemon": # start daemon start_daemon_subprocess(config_name) else: click.echo("Maestral is already linked.")
def start_daemon_subprocess(config_name): """Starts the Maestral daemon as a subprocess (by calling `start_maestral_daemon`). This command will create a new daemon on each run. Take care not to sync the same directory with multiple instances of Meastral! You can use `get_maestral_process_info` to check if either a Meastral gui or daemon is already running for the given `config_name`. :param str config_name: The name of maestral configuration to use. :returns: Popen object instance. """ import subprocess from maestral.main import Maestral if Maestral.pending_link() or Maestral.pending_dropbox_folder(): # run onboarding m = Maestral(run=False) m.create_dropbox_directory() m.select_excluded_folders() click.echo("Starting Maestral...", nl=False) proc = subprocess.Popen("maestral sync -c {}".format(config_name), shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # check if the subprocess is still running after 1 sec try: proc.wait(timeout=1) click.echo("\rStarting Maestral... " + FAILED) except subprocess.TimeoutExpired: click.echo("\rStarting Maestral... " + OK) return proc
def get_maestral_daemon_proxy(config_name="maestral", fallback=False): """ Returns a proxy of the running Maestral daemon. If fallback == True, a new instance of Maestral will be returned when the daemon cannot be reached. This can be dangerous if the GUI is running at the same time. """ pid, location, p_type = get_maestral_process_info(config_name) if p_type == "daemon": maestral_daemon = Pyro4.Proxy(URI.format(config_name, location)) try: maestral_daemon._pyroBind() return maestral_daemon except Pyro4.errors.CommunicationError: maestral_daemon._pyroRelease() if fallback: from maestral.main import Maestral m = Maestral(run=False) return m else: raise Pyro4.errors.CommunicationError
class SetupDialog(QtWidgets.QDialog): """A dialog to link and set up a new Drobox account.""" auth_session = "" auth_url = "" def __init__(self, pending_link=True, parent=None): super(self.__class__, self).__init__(parent=parent) # load user interface layout from .ui file uic.loadUi(SETUP_DIALOG_PATH, self) self.app_icon = QtGui.QIcon(APP_ICON_PATH) self.labelIcon_0.setPixmap(icon_to_pixmap(self.app_icon, 170)) self.labelIcon_1.setPixmap(icon_to_pixmap(self.app_icon, 70)) self.labelIcon_2.setPixmap(icon_to_pixmap(self.app_icon, 70)) self.labelIcon_3.setPixmap(icon_to_pixmap(self.app_icon, 100)) self.mdbx = None self.folder_items = [] # resize dialog buttons width = self.pushButtonAuthPageCancel.width() * 1.1 for b in (self.pushButtonAuthPageLink, self.pussButtonDropboxPathUnlink, self.pussButtonDropboxPathSelect, self.pushButtonFolderSelectionBack, self.pushButtonFolderSelectionSelect, self.pushButtonAuthPageCancel, self.pussButtonDropboxPathCalcel, self.pushButtonClose): b.setMinimumWidth(width) b.setMaximumWidth(width) # set up combobox self.dropbox_location = osp.dirname(CONF.get( "main", "path")) or get_home_dir() relative_path = self.rel_path(self.dropbox_location) folder_icon = get_native_item_icon(self.dropbox_location) self.comboBoxDropboxPath.addItem(folder_icon, relative_path) self.comboBoxDropboxPath.insertSeparator(1) self.comboBoxDropboxPath.addItem(QtGui.QIcon(), "Other...") self.comboBoxDropboxPath.currentIndexChanged.connect(self.on_combobox) self.dropbox_folder_dialog = QtWidgets.QFileDialog(self) self.dropbox_folder_dialog.setAcceptMode( QtWidgets.QFileDialog.AcceptOpen) self.dropbox_folder_dialog.setFileMode(QtWidgets.QFileDialog.Directory) self.dropbox_folder_dialog.setOption( QtWidgets.QFileDialog.ShowDirsOnly, True) self.dropbox_folder_dialog.fileSelected.connect(self.on_new_dbx_folder) self.dropbox_folder_dialog.rejected.connect( lambda: self.comboBoxDropboxPath.setCurrentIndex(0)) # connect buttons to callbacks self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.pushButtonLink.clicked.connect(self.on_link) self.pushButtonAuthPageCancel.clicked.connect(self.abort) self.pushButtonAuthPageLink.clicked.connect(self.on_auth_clicked) self.pussButtonDropboxPathCalcel.clicked.connect(self.abort) self.pussButtonDropboxPathSelect.clicked.connect( self.on_dropbox_location_selected) self.pussButtonDropboxPathUnlink.clicked.connect( self.unlink_and_go_to_start) self.pushButtonFolderSelectionBack.clicked.connect( self.stackedWidget.slideInPrev) self.pushButtonFolderSelectionSelect.clicked.connect( self.on_folders_selected) self.pushButtonClose.clicked.connect(self.accept) self.listWidgetFolders.itemChanged.connect( self.update_select_all_checkbox) self.selectAllCheckBox.clicked.connect(self.on_select_all_clicked) self.labelDropboxPath.setText(self.labelDropboxPath.text().format( CONF.get("main", "default_dir_name"))) # check if we are already authenticated, skip authentication if yes if not pending_link: self.labelDropboxPath.setText(""" <html><head/><body> <p align="left"> Your Dropbox folder has been moved or deleted from its original location. Maestral will not work properly until you move it back. It used to be located at: </p><p align="left">{0}</p> <p align="left"> To move it back, click "Quit" below, move the Dropbox folder back to its original location, and launch Maestral again. </p> <p align="left"> To re-download your Dropbox, please select a location for your Dropbox folder below. Maestral will create a new folder named "{1}" in the selected location.</p> <p align="left"> To unlink your Dropbox account from Maestral, click "Unlink" below.</p> </body></html> """.format(CONF.get("main", "path"), CONF.get("main", "default_dir_name"))) self.pussButtonDropboxPathCalcel.setText("Quit") self.stackedWidget.setCurrentIndex(2) self.stackedWidgetButtons.setCurrentIndex(2) self.mdbx = Maestral(run=False) self.mdbx.client.get_account_info() else: self.stackedWidget.setCurrentIndex(0) self.stackedWidgetButtons.setCurrentIndex(0) # ============================================================================= # Main callbacks # ============================================================================= def closeEvent(self, event): if self.stackedWidget.currentIndex == 4: self.accept() else: self.abort() def abort(self): self.mdbx = None self.reject() def unlink_and_go_to_start(self, b): self.mdbx.unlink() self.stackedWidget.slideInIdx(0) def on_link(self): self.auth_session = OAuth2Session() self.auth_url = self.auth_session.get_auth_url() prompt = self.labelAuthLink.text().format(self.auth_url) self.labelAuthLink.setText(prompt) self.stackedWidget.fadeInIdx(1) self.pushButtonAuthPageLink.setFocus() def on_auth_clicked(self): if self.lineEditAuthCode.text() == "": msg = "Please enter an authentication token." msg_box = UserDialog("Authentication failed.", msg, parent=self) msg_box.open() else: self.progressIndicator.startAnimation() self.pushButtonAuthPageLink.setEnabled(False) self.lineEditAuthCode.setEnabled(False) self.verify_token_async() def verify_token_async(self): token = self.lineEditAuthCode.text() self.auth_task = MaestralBackgroundTask( parent=self, target=self.auth_session.verify_auth_token, args=(token, )) self.auth_task.sig_done.connect(self.on_verify_token_finished) def on_verify_token_finished(self, res): if res == OAuth2Session.Success: self.auth_session.save_creds() # switch to next page self.stackedWidget.slideInIdx(2) self.pussButtonDropboxPathSelect.setFocus() self.lineEditAuthCode.clear( ) # clear since we might come back on unlink # start Maestral after linking to Dropbox account self.mdbx = Maestral(run=False) self.mdbx.client.get_account_info() elif res == OAuth2Session.InvalidToken: msg = "Please make sure that you entered the correct authentication token." msg_box = UserDialog("Authentication failed.", msg, parent=self) msg_box.open() elif res == OAuth2Session.ConnectionFailed: msg = "Please make sure that you are connected to the internet and try again." msg_box = UserDialog("Connection failed.", msg, parent=self) msg_box.open() self.progressIndicator.stopAnimation() self.pushButtonAuthPageLink.setEnabled(True) self.lineEditAuthCode.setEnabled(True) def on_dropbox_location_selected(self): # reset sync status, we are starting fresh! self.mdbx.sync.last_cursor = "" self.mdbx.sync.last_sync = None self.mdbx.sync.dropbox_path = "" # apply dropbox path dropbox_path = osp.join(self.dropbox_location, CONF.get("main", "default_dir_name")) if osp.isdir(dropbox_path): msg = ('The folder "%s" already exists. Would ' 'you like to keep using it?' % self.dropbox_location) msg_box = UserDialog("Folder already exists", msg, parent=self) msg_box.setAcceptButtonName("Keep") msg_box.addSecondAcceptButton("Replace", icon="edit-clear") msg_box.addCancelButton() res = msg_box.exec_() if res == 1: pass elif res == 2: shutil.rmtree(dropbox_path, ignore_errors=True) else: return elif osp.isfile(dropbox_path): msg = ( 'There already is a file named "{0}" at this location. Would ' 'you like to replace it?'.format( CONF.get("main", "default_dir_name"))) msg_box = UserDialog("File conflict", msg, parent=self) msg_box.setAcceptButtonName("Replace") msg_box.addCancelButton() res = msg_box.exec_() if res == 0: return else: try: os.unlink(dropbox_path) except OSError: pass self.mdbx.create_dropbox_directory(path=dropbox_path, overwrite=False) # switch to next page self.stackedWidget.slideInIdx(3) self.pushButtonFolderSelectionSelect.setFocus() # populate folder list if self.folder_items == []: self.populate_folders_list() def on_folders_selected(self): # switch to next page self.stackedWidget.slideInIdx(4) # exclude folders excluded_folders = [] included_folders = [] for item in self.folder_items: if not item.isIncluded(): excluded_folders.append("/" + item.name.lower()) elif item.isIncluded(): included_folders.append("/" + item.name.lower()) CONF.set("main", "excluded_folders", excluded_folders) self.mdbx.get_remote_dropbox_async("", callback=self.mdbx.start_sync) # ============================================================================= # Helper functions # ============================================================================= def on_combobox(self, idx): if idx == 2: self.dropbox_folder_dialog.open() def on_new_dbx_folder(self, new_location): self.comboBoxDropboxPath.setCurrentIndex(0) if not new_location == '': self.comboBoxDropboxPath.setItemText(0, self.rel_path(new_location)) self.comboBoxDropboxPath.setItemIcon( 0, get_native_item_icon(new_location)) self.dropbox_location = new_location def populate_folders_list(self): self.listWidgetFolders.addItem("Loading your folders...") # add new entries root_folders = self.mdbx.client.list_folder("", recursive=False) self.listWidgetFolders.clear() if root_folders is False: self.listWidgetFolders.addItem( "Unable to connect. Please try again later.") self.pushButtonFolderSelectionSelect.setEnabled(False) else: self.pushButtonFolderSelectionSelect.setEnabled(True) for entry in root_folders.entries: if isinstance(entry, files.FolderMetadata): inc = not self.mdbx.sync.is_excluded_by_user( entry.path_lower) item = FolderItem(entry.name, inc) self.folder_items.append(item) for item in self.folder_items: self.listWidgetFolders.addItem(item) self.update_select_all_checkbox() def update_select_all_checkbox(self): is_included_list = (i.isIncluded() for i in self.folder_items) self.selectAllCheckBox.setChecked(all(is_included_list)) def on_select_all_clicked(self, checked): for item in self.folder_items: item.setIncluded(checked) @staticmethod def rel_path(path): """ Returns the path relative to the users directory, or the absolute path if not in a user directory. """ usr = osp.abspath(osp.join(get_home_dir(), osp.pardir)) if osp.commonprefix([path, usr]) == usr: return osp.relpath(path, usr) else: return path def changeEvent(self, QEvent): if QEvent.type() == QtCore.QEvent.PaletteChange: self.update_dark_mode() def update_dark_mode(self): # update folder icons: the system may provide different icons in dark mode for item in self.folder_items: item.setIcon(get_native_folder_icon()) # static method to create the dialog and return Maestral instance on success @staticmethod def configureMaestral(pending_link=True, parent=None): fsd = SetupDialog(pending_link, parent) fsd.exec_() return fsd.mdbx
def m(): m = Maestral("test-config") m.log_level = logging.DEBUG m._conf.save() yield m remove_configuration(m.config_name)
def __init__(self, pending_link=True, parent=None): super(self.__class__, self).__init__(parent=parent) # load user interface layout from .ui file uic.loadUi(SETUP_DIALOG_PATH, self) self.app_icon = QtGui.QIcon(APP_ICON_PATH) self.labelIcon_0.setPixmap(icon_to_pixmap(self.app_icon, 170)) self.labelIcon_1.setPixmap(icon_to_pixmap(self.app_icon, 70)) self.labelIcon_2.setPixmap(icon_to_pixmap(self.app_icon, 70)) self.labelIcon_3.setPixmap(icon_to_pixmap(self.app_icon, 100)) self.mdbx = None self.folder_items = [] # resize dialog buttons width = self.pushButtonAuthPageCancel.width() * 1.1 for b in (self.pushButtonAuthPageLink, self.pussButtonDropboxPathUnlink, self.pussButtonDropboxPathSelect, self.pushButtonFolderSelectionBack, self.pushButtonFolderSelectionSelect, self.pushButtonAuthPageCancel, self.pussButtonDropboxPathCalcel, self.pushButtonClose): b.setMinimumWidth(width) b.setMaximumWidth(width) # set up combobox self.dropbox_location = osp.dirname(CONF.get( "main", "path")) or get_home_dir() relative_path = self.rel_path(self.dropbox_location) folder_icon = get_native_item_icon(self.dropbox_location) self.comboBoxDropboxPath.addItem(folder_icon, relative_path) self.comboBoxDropboxPath.insertSeparator(1) self.comboBoxDropboxPath.addItem(QtGui.QIcon(), "Other...") self.comboBoxDropboxPath.currentIndexChanged.connect(self.on_combobox) self.dropbox_folder_dialog = QtWidgets.QFileDialog(self) self.dropbox_folder_dialog.setAcceptMode( QtWidgets.QFileDialog.AcceptOpen) self.dropbox_folder_dialog.setFileMode(QtWidgets.QFileDialog.Directory) self.dropbox_folder_dialog.setOption( QtWidgets.QFileDialog.ShowDirsOnly, True) self.dropbox_folder_dialog.fileSelected.connect(self.on_new_dbx_folder) self.dropbox_folder_dialog.rejected.connect( lambda: self.comboBoxDropboxPath.setCurrentIndex(0)) # connect buttons to callbacks self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.pushButtonLink.clicked.connect(self.on_link) self.pushButtonAuthPageCancel.clicked.connect(self.abort) self.pushButtonAuthPageLink.clicked.connect(self.on_auth_clicked) self.pussButtonDropboxPathCalcel.clicked.connect(self.abort) self.pussButtonDropboxPathSelect.clicked.connect( self.on_dropbox_location_selected) self.pussButtonDropboxPathUnlink.clicked.connect( self.unlink_and_go_to_start) self.pushButtonFolderSelectionBack.clicked.connect( self.stackedWidget.slideInPrev) self.pushButtonFolderSelectionSelect.clicked.connect( self.on_folders_selected) self.pushButtonClose.clicked.connect(self.accept) self.listWidgetFolders.itemChanged.connect( self.update_select_all_checkbox) self.selectAllCheckBox.clicked.connect(self.on_select_all_clicked) self.labelDropboxPath.setText(self.labelDropboxPath.text().format( CONF.get("main", "default_dir_name"))) # check if we are already authenticated, skip authentication if yes if not pending_link: self.labelDropboxPath.setText(""" <html><head/><body> <p align="left"> Your Dropbox folder has been moved or deleted from its original location. Maestral will not work properly until you move it back. It used to be located at: </p><p align="left">{0}</p> <p align="left"> To move it back, click "Quit" below, move the Dropbox folder back to its original location, and launch Maestral again. </p> <p align="left"> To re-download your Dropbox, please select a location for your Dropbox folder below. Maestral will create a new folder named "{1}" in the selected location.</p> <p align="left"> To unlink your Dropbox account from Maestral, click "Unlink" below.</p> </body></html> """.format(CONF.get("main", "path"), CONF.get("main", "default_dir_name"))) self.pussButtonDropboxPathCalcel.setText("Quit") self.stackedWidget.setCurrentIndex(2) self.stackedWidgetButtons.setCurrentIndex(2) self.mdbx = Maestral(run=False) self.mdbx.client.get_account_info() else: self.stackedWidget.setCurrentIndex(0) self.stackedWidgetButtons.setCurrentIndex(0)
def m(): config_name = "test-config" m = Maestral(config_name) m.log_level = logging.DEBUG # link with given token access_token = os.environ.get("DROPBOX_ACCESS_TOKEN") refresh_token = os.environ.get("DROPBOX_REFRESH_TOKEN") if access_token: m.client._init_sdk_with_token(access_token=access_token) m.client.auth._access_token = access_token m.client.auth._token_access_type = "legacy" elif refresh_token: m.client._init_sdk_with_token(refresh_token=refresh_token) m.client.auth._refresh_token = refresh_token m.client.auth._token_access_type = "offline" else: raise RuntimeError( "Either access token or refresh token must be given as environment " "variable DROPBOX_ACCESS_TOKEN or DROPBOX_REFRESH_TOKEN." ) # get corresponding Dropbox ID and store in keyring for other processes res = m.client.get_account_info() m.client.auth._account_id = res.account_id m.client.auth.loaded = True m.client.auth.save_creds() # set local Dropbox directory home = get_home_dir() local_dropbox_dir = generate_cc_name(home + "/Dropbox", suffix="test runner") m.create_dropbox_directory(local_dropbox_dir) # acquire test lock and perform initial sync lock = DropboxTestLock(m) if not lock.acquire(timeout=60 * 60): raise TimeoutError("Could not acquire test lock") # create / clean our temporary test folder m.test_folder_dbx = "/sync_tests" m.test_folder_local = m.to_local_path(m.test_folder_dbx) try: m.client.remove(m.test_folder_dbx) except NotFoundError: pass m.client.make_dir(m.test_folder_dbx) # start syncing m.start_sync() wait_for_idle(m) # return synced and running instance yield m # stop syncing and clean up remote folder m.stop_sync() try: m.client.remove(m.test_folder_dbx) except NotFoundError: pass try: m.client.remove("/.mignore") except NotFoundError: pass # remove all shared links res = m.client.list_shared_links() for link in res.links: m.revoke_shared_link(link.url) # remove creds from system keyring m.client.auth.delete_creds() # remove local files and folders delete(m.dropbox_path) remove_configuration(m.config_name) # release lock lock.release()
def start(foreground: bool, verbose: bool, config_name: str) -> None: # ---- run setup if necessary ------------------------------------------------------ # We run the setup in the current process. This avoids starting a subprocess despite # running with the --foreground flag, prevents leaving a zombie process if the setup # fails with an exception and does not confuse systemd. from maestral.main import Maestral m = Maestral(config_name, log_to_stdout=verbose) if m.pending_link: # this may raise KeyringAccessError link_dialog(m) if m.pending_dropbox_folder: path = select_dbx_path_dialog(config_name, allow_merge=True) while True: try: m.create_dropbox_directory(path) break except OSError: click.echo( "Could not create folder. Please make sure that you have " "permissions to write to the selected location or choose a " "different location." ) exclude_folders_q = click.confirm( "Would you like to exclude any folders from syncing?", ) if exclude_folders_q: click.echo( "Please choose which top-level folders to exclude. You can exclude\n" 'individual files or subfolders later with "maestral excluded add".\n' ) click.echo("Loading...", nl=False) # get all top-level Dropbox folders entries = m.list_folder("/", recursive=False) excluded_items: List[str] = [] click.echo("\rLoading... Done") # paginate through top-level folders, ask to exclude for e in entries: if e["type"] == "FolderMetadata": yes = click.confirm( 'Exclude "{path_display}" from sync?'.format(**e) ) if yes: path_lower = cast(str, e["path_lower"]) excluded_items.append(path_lower) m.set_excluded_items(excluded_items) # free resources del m if foreground: # stop daemon process after setup and restart in our current process stop_maestral_daemon_process(config_name) start_maestral_daemon(config_name, log_to_stdout=verbose, start_sync=True) else: # start daemon process click.echo("Starting Maestral...", nl=False) res = start_maestral_daemon_process( config_name, log_to_stdout=verbose, start_sync=True ) if res == Start.Ok: click.echo("\rStarting Maestral... " + OK) elif res == Start.AlreadyRunning: click.echo("\rStarting Maestral... Already running.") else: click.echo("\rStarting Maestral... " + FAILED) click.echo("Please check logs for more information.")
class MaestralProxy: """ A Proxy to the Maestral daemon. All methods and properties of Maestral's public API are accessible and calls / access will be forwarded to the corresponding Maestral instance. This class can be used as a context manager to close the connection to the daemon on exit. :Example: Use MaestralProxy as a context manager: >>> with MaestralProxy() as m: ... print(m.status) Use MaestralProxy directly: >>> m = MaestralProxy() >>> print(m.status) >>> m._disconnect() :param config_name: The name of the Maestral configuration to use. :param fallback: If ``True``, a new instance of Maestral will created in the current process when the daemon is not running. :raises: :class:`Pyro5.errors.CommunicationError` if the daemon is running but cannot be reached or if the daemon is not running and ``fallback`` is ``False``. """ _m: Union["Maestral", Proxy] def __init__(self, config_name: str = "maestral", fallback: bool = False) -> None: self._config_name = config_name self._is_fallback = False if is_running(config_name): sock_name = sockpath_for_config(config_name) # print remote tracebacks locally sys.excepthook = Pyro5.errors.excepthook self._m = Proxy(URI.format(config_name, "./u:" + sock_name)) try: self._m._pyroBind() except CommunicationError: self._m._pyroRelease() raise else: # If daemon is not running, fall back to new Maestral instance # or raise a CommunicationError if fallback not allowed. if fallback: from maestral.main import Maestral self._m = Maestral(config_name) else: raise CommunicationError("Could not get proxy") self._is_fallback = not isinstance(self._m, Proxy) def _disconnect(self) -> None: if isinstance(self._m, Proxy): self._m._pyroRelease() def __enter__(self) -> "MaestralProxy": return self def __exit__(self, exc_type: Type[Exception], exc_value: Exception, tb: TracebackType) -> None: self._disconnect() del self._m def __getattr__(self, item: str) -> Any: if item.startswith("_"): super().__getattribute__(item) elif isinstance(self._m, Proxy): return self._m.__getattr__(item) else: return self._m.__getattribute__(item) def __setattr__(self, key, value) -> None: if key.startswith("_"): super().__setattr__(key, value) else: self._m.__setattr__(key, value) def __dir__(self) -> Iterable[str]: own_result = dir(self.__class__) + list(self.__dict__.keys()) proxy_result = list(k for k in self._m.__dir__() if not k.startswith("_")) return sorted(set(own_result) | set(proxy_result)) def __repr__(self) -> str: return (f"<{self.__class__.__name__}(config={self._config_name!r}, " f"is_fallback={self._is_fallback})>")
def m(): m = Maestral("test-config") m._conf.save() yield m remove_configuration(m.config_name)