Esempio n. 1
0
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
Esempio n. 2
0
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)
Esempio n. 3
0
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)
Esempio n. 4
0
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
Esempio n. 5
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()
Esempio n. 6
0
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.")
Esempio n. 7
0
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