def test_fallback(config_name): # create proxy w/o fallback with pytest.raises(CommunicationError): MaestralProxy(config_name) # create proxy w/ fallback with MaestralProxy(config_name, fallback=True) as m: assert m.config_name == config_name assert m._is_fallback assert isinstance(m._m, Maestral)
def test_start_stop(self): subprocess.run(["maestral", "start", "-c", self.config_name]) with MaestralProxy(self.config_name) as m: self.assertTrue(m.running) self.assertTrue(m.syncing) subprocess.run(["maestral", "stop", "-c", self.config_name]) with self.assertRaises(Pyro5.errors.CommunicationError): MaestralProxy(self.config_name)
def wait_for_idle(m: MaestralProxy, minimum: int = 2): while True: current_status = m.status time.sleep(minimum) if current_status in (IDLE, PAUSED, ERROR, ""): m.status_change_longpoll(timeout=minimum) if m.status == current_status: # status did not change, we are done return else: m.status_change_longpoll(timeout=minimum)
def analytics(yes: bool, no: bool, config_name: str) -> None: if yes or no: with MaestralProxy(config_name, fallback=True) as m: m.analytics = yes enabled_str = "Enabled" if yes else "Disabled" click.echo(f"{enabled_str} automatic error reports.") else: with MaestralProxy(config_name, fallback=True) as m: state = m.analytics enabled_str = "enabled" if state else "disabled" click.echo(f"Automatic error reports are {enabled_str}.")
def run(self): try: with MaestralProxy(self.config_name) as proxy: self.connection = proxy._m._pyroConnection func = proxy.__getattr__(self._target) res = func(*self._args, **self._kwargs) if hasattr(res, "__next__"): while True: try: next_res = next(res) self.emitter.sig_result.emit(next_res) except StopIteration: return else: self.emitter.sig_result.emit(res) except ConnectionClosedError: pass except Exception as exc: self.emitter.sig_result.emit(exc) finally: self.connection = None self.emitter.sig_done.emit()
def excluded_remove(dropbox_path: str, config_name: str): """Removes a file or folder from the excluded list and re-syncs.""" if not dropbox_path.startswith('/'): dropbox_path = '/' + dropbox_path if dropbox_path == '/': click.echo( click.style('The root directory is always included.', fg='red')) return if not pending_link_cli(config_name): from maestral.daemon import MaestralProxy try: with MaestralProxy(config_name) as m: if _check_for_fatal_errors(m): return try: m.include_item(dropbox_path) click.echo( f'Included \'{dropbox_path}\'. Now downloading...') except ConnectionError: raise click.ClickException('Could not connect to Dropbox.') except ValueError as e: raise click.ClickException(e.args[0]) except Pyro5.errors.CommunicationError: raise click.ClickException('Maestral daemon must be running ' 'to download folders.')
def test_notify_level(config_name): start_maestral_daemon_process(config_name, timeout=20) m = MaestralProxy(config_name) runner = CliRunner() result = runner.invoke(main, ["notify", "level", "-c", m.config_name]) level_name = level_number_to_name(m.notification_level) assert result.exit_code == 0 assert level_name in result.output level_name = "SYNCISSUE" level_number = level_name_to_number(level_name) result = runner.invoke( main, ["notify", "level", level_name, "-c", m.config_name]) assert result.exit_code == 0 assert level_name in result.output assert m.notification_level == level_number result = runner.invoke(main, ["notify", "level", "INVALID", "-c", m.config_name]) assert result.exit_code == 2 assert isinstance(result.exception, SystemExit)
def test_fallback(self): config_name = "daemon-lifecycle-test" # create proxy w/o fallback with self.assertRaises(CommunicationError): MaestralProxy(config_name) # create proxy w/ fallback with MaestralProxy(config_name, fallback=True) as m: self.assertEqual(m.config_name, config_name) self.assertTrue(m._is_fallback) self.assertIsInstance(m._m, Maestral) # clean up config remove_configuration(config_name)
def rebuild_index(config_name: str): """Rebuilds Maestral's index. May take several minutes.""" try: import textwrap from maestral.daemon import MaestralProxy with MaestralProxy(config_name) as m: width, height = click.get_terminal_size() msg = textwrap.fill( 'Rebuilding the index may take several minutes, depending on the size of ' 'your Dropbox. Any changes to local files will be synced once rebuilding ' 'has completed. If you stop the daemon during the process, rebuilding ' 'will start again on the next launch.', width=width) click.echo(msg + '\n') click.confirm('Do you want to continue?', abort=True) m.rebuild_index() except Pyro5.errors.CommunicationError: click.echo('Maestral daemon is not running.')
def history(config_name: str) -> None: from datetime import datetime with MaestralProxy(config_name, fallback=True) as m: history = m.get_history() paths = [] change_times = [] change_types = [] for event in history: dbx_path = cast(str, event["dbx_path"]) change_type = cast(str, event["change_type"]) change_time_or_sync_time = cast(float, event["change_time_or_sync_time"]) dt = datetime.fromtimestamp(change_time_or_sync_time) paths.append(dbx_path) change_times.append(dt.strftime("%d %b %Y %H:%M")) change_types.append(change_type) click.echo( format_table(columns=[paths, change_types, change_times], wrap=False) )
def revs(dropbox_path: str, config_name: str) -> None: from datetime import datetime if not dropbox_path.startswith("/"): dropbox_path = "/" + dropbox_path with MaestralProxy(config_name, fallback=True) as m: entries = m.list_revisions(dropbox_path) revs = [] last_modified = [] for e in entries: rev = cast(str, e["rev"]) cm = cast(str, e["client_modified"]) revs.append(rev) dt = datetime.strptime(cm, "%Y-%m-%dT%H:%M:%S%z").astimezone() last_modified.append(dt.strftime("%d %b %Y %H:%M")) click.echo(format_table(columns=[revs, last_modified]))
def rebuild_index(config_name: str) -> None: import textwrap with MaestralProxy(config_name, fallback=True) as m: width, height = click.get_terminal_size() msg = textwrap.fill( "Rebuilding the index may take several minutes, depending on the size of " "your Dropbox. Any changes to local files will be synced once rebuilding " "has completed. If you stop the daemon during the process, rebuilding will " "start again on the next launch.\nIf the daemon is not currently running, " "a rebuild will be schedules for the next startup.", width=width, ) click.echo(msg + "\n") click.confirm("Do you want to continue?", abort=True) m.rebuild_index() if isinstance(m, MaestralProxy): click.echo('Rebuilding now. Run "maestral status" to view progress.') else: click.echo("Daemon is not running. Rebuilding scheduled for next startup.")
def status(config_name: str) -> None: try: with MaestralProxy(config_name) as m: n_errors = len(m.sync_errors) color = "red" if n_errors > 0 else "green" n_errors_str = click.style(str(n_errors), fg=color) click.echo("") click.echo("Account: {}".format(m.get_state("account", "email"))) click.echo("Usage: {}".format(m.get_state("account", "usage"))) click.echo("Status: {}".format(m.status)) click.echo("Sync threads: {}".format("Running" if m.running else "Stopped")) click.echo("Sync errors: {}".format(n_errors_str)) click.echo("") check_for_fatal_errors(m) sync_err_list = m.sync_errors if len(sync_err_list) > 0: headers = ["PATH", "ERROR"] col0 = ["'{}'".format(err["dbx_path"]) for err in sync_err_list] col1 = ["{title}. {message}".format(**err) for err in sync_err_list] click.echo(format_table(columns=[col0, col1], headers=headers)) click.echo("") except Pyro5.errors.CommunicationError: click.echo("Maestral daemon is not running.")
def rebuild_in_thread(): if isinstance(m0, Pyro5.client.Proxy): # rebuild index from separate proxy with MaestralProxy(config_name) as m1: m1.rebuild_index() else: # rebuild index with main instance m0.rebuild_index()
def move_dir(new_path: str, config_name: str) -> None: new_path = new_path or select_dbx_path_dialog(config_name) with MaestralProxy(config_name, fallback=True) as m: m.move_dropbox_directory(new_path) click.echo(f"Dropbox folder moved to {new_path}.")
def pause(config_name: str) -> None: try: with MaestralProxy(config_name) as m: m.pause_sync() click.echo("Syncing paused.") except Pyro5.errors.CommunicationError: click.echo("Maestral daemon is not running.")
def resume(config_name: str) -> None: try: with MaestralProxy(config_name) as m: if not check_for_fatal_errors(m): m.resume_sync() click.echo("Syncing resumed.") except Pyro5.errors.CommunicationError: click.echo("Maestral daemon is not running.")
def pause(config_name: str): """Pauses syncing.""" from maestral.daemon import MaestralProxy try: with MaestralProxy(config_name) as m: m.pause_sync() click.echo('Syncing paused.') except Pyro5.errors.CommunicationError: click.echo('Maestral daemon is not running.')
def link(relink: bool, config_name: str) -> None: with MaestralProxy(config_name, fallback=True) as m: if m.pending_link or relink: link_dialog(m) else: click.echo( "Maestral is already linked. Use the option " "'-r' to relink to the same account." )
def notify_level(level_name: str, config_name: str) -> None: from maestral.utils.notify import MaestralDesktopNotifier as MDN with MaestralProxy(config_name, fallback=True) as m: if level_name: m.notification_level = MDN.level_name_to_number(level_name) click.echo(f"Notification level set to {level_name}.") else: level_name = MDN.level_number_to_name(m.notification_level) click.echo(f"Notification level: {level_name}.")
def log_level(level_name: str, config_name: str) -> None: import logging with MaestralProxy(config_name, fallback=True) as m: if level_name: m.log_level = cast(int, getattr(logging, level_name)) click.echo(f"Log level set to {level_name}.") else: level_name = logging.getLevelName(m.log_level) click.echo(f"Log level: {level_name}")
def resume(config_name: str): """Resumes syncing.""" from maestral.daemon import MaestralProxy try: with MaestralProxy(config_name) as m: if not _check_for_fatal_errors(m): m.resume_sync() click.echo('Syncing resumed.') except Pyro5.errors.CommunicationError: click.echo('Maestral daemon is not running.')
def test_remote_exceptions(config_name): # start daemon process start_maestral_daemon_process(config_name, timeout=20) # create proxy and call a remote method which raises an error with MaestralProxy(config_name) as m: with pytest.raises(NotLinkedError): m.get_account_info() # stop daemon stop_maestral_daemon_process(config_name)
def excluded_list(config_name: str) -> None: with MaestralProxy(config_name, fallback=True) as m: excluded_items = m.excluded_items excluded_items.sort() if len(excluded_items) == 0: click.echo("No excluded files or folders.") else: for item in excluded_items: click.echo(item)
def analytics(config_name: str, yes: bool, no: bool): """Enables or disables sharing error reports.""" # This is safe to call regardless if the GUI or daemon are running. from maestral.daemon import MaestralProxy if yes or no: try: with MaestralProxy(config_name) as m: m.analytics = yes except Pyro5.errors.CommunicationError: MaestralConfig(config_name).set('app', 'analytics', yes) enabled_str = 'Enabled' if yes else 'Disabled' click.echo(f'{enabled_str} automatic error reports.') else: try: with MaestralProxy(config_name) as m: state = m.analytics except Pyro5.errors.CommunicationError: state = MaestralConfig(config_name).get('app', 'analytics') enabled_str = 'enabled' if state else 'disabled' click.echo(f'Automatic error reports are {enabled_str}.')
def file_status(local_path: str, config_name: str) -> None: try: with MaestralProxy(config_name) as m: if check_for_fatal_errors(m): return stat = m.get_file_status(local_path) click.echo(stat) except Pyro5.errors.CommunicationError: click.echo("unwatched")
def move_dir(config_name: str, new_path: str): """Change the location of your Dropbox folder.""" if not pending_link_cli(config_name): from maestral.main import select_dbx_path_dialog from maestral.daemon import MaestralProxy new_path = new_path or select_dbx_path_dialog(config_name, allow_merge=False) with MaestralProxy(config_name, fallback=True) as m: m.move_dropbox_directory(new_path) click.echo(f'Dropbox folder moved to {new_path}.')
def notify_snooze(minutes: int, config_name: str) -> None: try: with MaestralProxy(config_name) as m: m.notification_snooze = minutes except Pyro5.errors.CommunicationError: click.echo("Maestral daemon is not running.") else: if minutes > 0: click.echo( f"Notifications snoozed for {minutes} min. " "Set snooze to 0 to reset." ) else: click.echo("Notifications enabled.")
def file_status(config_name: str, local_path: str): """Returns the current sync status of a given file or folder.""" from maestral.daemon import MaestralProxy try: with MaestralProxy(config_name) as m: if _check_for_fatal_errors(m): return stat = m.get_file_status(local_path) click.echo(stat) except Pyro5.errors.CommunicationError: click.echo('unwatched')
def excluded_add(dropbox_path: str, config_name: str) -> None: if not dropbox_path.startswith("/"): dropbox_path = "/" + dropbox_path if dropbox_path == "/": click.echo(click.style("Cannot exclude the root directory.", fg="red")) return with MaestralProxy(config_name, fallback=True) as m: if check_for_fatal_errors(m): return m.exclude_item(dropbox_path) click.echo(f"Excluded '{dropbox_path}'.")