def __init__(self, mdbx, parent=None):
        super().__init__(parent=parent)
        self.setupUi(self)

        # noinspection PyTypeChecker
        self.setWindowFlags(
            Qt.WindowType.WindowStaysOnTopHint
            | Qt.WindowType.Sheet
            | Qt.WindowType.WindowTitleHint
            | Qt.WindowType.CustomizeWindowHint
        )

        self.mdbx = mdbx
        self.config_name = self.mdbx.config_name

        self.app_icon = QtGui.QIcon(APP_ICON_PATH)

        self.labelIcon.setPixmap(icon_to_pixmap(self.app_icon, 60))

        # set up Dropbox location combobox

        self.dropbox_location = self.mdbx.get_conf("sync", "path")

        if self.dropbox_location == "":
            folder_name = f"Dropbox ({self.config_name.capitalize()})"
            self.dropbox_location = osp.join(get_home_dir(), folder_name)

        self.comboBoxPath.addItem(native_folder_icon(), self.dropbox_location)
        self.comboBoxPath.insertSeparator(1)
        self.comboBoxPath.addItem(QtGui.QIcon(), "Choose...")
        self.comboBoxPath.currentIndexChanged.connect(self.on_combobox)

        self.dropbox_folder_dialog = QtWidgets.QFileDialog(self)
        self.dropbox_folder_dialog.setAcceptMode(
            QtWidgets.QFileDialog.AcceptMode.AcceptOpen
        )
        self.dropbox_folder_dialog.setFileMode(QtWidgets.QFileDialog.FileMode.Directory)
        self.dropbox_folder_dialog.setOption(
            QtWidgets.QFileDialog.Option.ShowDirsOnly, True
        )
        self.dropbox_folder_dialog.setLabelText(
            QtWidgets.QFileDialog.DialogLabel.Accept, "Select"
        )
        self.dropbox_folder_dialog.setDirectory(get_home_dir())
        self.dropbox_folder_dialog.fileSelected.connect(self.on_new_dbx_folder)
        self.dropbox_folder_dialog.rejected.connect(
            lambda: self.comboBoxPath.setCurrentIndex(0)
        )

        self.pushButtonSelect.setDefault(True)

        # connect buttons to callbacks
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
        self.pushButtonQuit.clicked.connect(self.on_quit_clicked)
        self.pushButtonSelect.clicked.connect(self.on_selected_clicked)
        self.pushButtonUnlink.clicked.connect(self.on_unlink_clicked)
示例#2
0
def test_macos_dirs():
    platform.system = lambda: "Darwin"

    assert (
        get_conf_path(create=False) == get_home_dir() + "/Library/Application Support"
    )
    assert get_cache_path(create=False) == get_conf_path(create=False)
    assert get_data_path(create=False) == get_conf_path(create=False)
    assert get_runtime_path(create=False) == get_conf_path(create=False)
    assert get_log_path(create=False) == get_home_dir() + "/Library/Logs"
    assert get_autostart_path(create=False) == get_home_dir() + "/Library/LaunchAgents"
示例#3
0
def test_macos_dirs():
    platform.system = lambda: 'Darwin'

    assert get_conf_path(
        create=False) == get_home_dir() + '/Library/Application Support'
    assert get_cache_path(create=False) == get_conf_path(create=False)
    assert get_data_path(create=False) == get_conf_path(create=False)
    assert get_runtime_path(create=False) == get_conf_path(create=False)
    assert get_old_runtime_path(create=False) == tempfile.gettempdir()
    assert get_log_path(create=False) == get_home_dir() + '/Library/Logs'
    assert get_autostart_path(
        create=False) == get_home_dir() + '/Library/LaunchAgents'
示例#4
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
示例#5
0
def test_unix_permissions(m):
    """
    Tests that a newly downloaded file is created with default permissions for our
    process and that any locally set permissions are preserved on remote file
    modifications.
    """

    dbx_path = "/sync_tests/file"
    local_path = m.to_local_path(dbx_path)

    m.client.upload(resources + "/file.txt", dbx_path)
    wait_for_idle(m)

    # create a local file and compare its permissions to the new download
    reference_file = osp.join(get_home_dir(), "reference")

    try:
        open(reference_file, "ab").close()
        assert os.stat(local_path).st_mode == os.stat(reference_file).st_mode
    finally:
        delete(reference_file)

    # make the local file executable
    os.chmod(local_path, 0o744)
    new_mode = os.stat(local_path).st_mode  # might not be 744...
    wait_for_idle(m)

    # perform some remote modifications
    m.client.upload(resources + "/file1.txt", dbx_path, mode=WriteMode.overwrite)
    wait_for_idle(m)

    # check that the local permissions have not changed
    assert os.stat(local_path).st_mode == new_mode
示例#6
0
def test_linux_dirs():
    platform.system = lambda: 'Linux'

    os.environ['XDG_CONFIG_HOME'] = '/xdg_config_home'
    os.environ['XDG_CACHE_HOME'] = '/xdg_cache_home'
    os.environ['XDG_DATA_DIR'] = '/xdg_data_dir'
    os.environ['XDG_RUNTIME_DIR'] = '/xdg_runtime_dir'

    assert get_conf_path(create=False) == '/xdg_config_home'
    assert get_cache_path(create=False) == '/xdg_cache_home'
    assert get_data_path(create=False) == '/xdg_data_dir'
    assert get_runtime_path(create=False) == '/xdg_runtime_dir'
    assert get_old_runtime_path(create=False) == '/xdg_runtime_dir'
    assert get_log_path(create=False) == '/xdg_cache_home'
    assert get_autostart_path(create=False) == '/xdg_config_home/autostart'

    del os.environ['XDG_CONFIG_HOME']
    del os.environ['XDG_CACHE_HOME']
    del os.environ['XDG_DATA_DIR']
    del os.environ['XDG_RUNTIME_DIR']

    assert get_conf_path(create=False) == get_home_dir() + '/.config'
    assert get_cache_path(create=False) == get_home_dir() + '/.cache'
    assert get_data_path(create=False) == get_home_dir() + '/.local/share'
    assert get_runtime_path(create=False) == get_home_dir() + '/.cache'
    assert get_old_runtime_path(create=False) == get_home_dir() + '/.cache'
    assert get_log_path(create=False) == get_home_dir() + '/.cache'
    assert get_autostart_path(
        create=False) == get_home_dir() + '/.config/autostart'
示例#7
0
 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
示例#8
0
def test_linux_dirs():
    platform.system = lambda: "Linux"

    # test that XDG environment variables for app dirs are respected

    os.environ["XDG_CONFIG_HOME"] = "/xdg_config_home"
    os.environ["XDG_CACHE_HOME"] = "/xdg_cache_home"
    os.environ["XDG_DATA_HOME"] = "/xdg_data_dir"
    os.environ["XDG_RUNTIME_DIR"] = "/xdg_runtime_dir"

    assert get_conf_path(create=False) == "/xdg_config_home"
    assert get_cache_path(create=False) == "/xdg_cache_home"
    assert get_data_path(create=False) == "/xdg_data_dir"
    assert get_runtime_path(create=False) == "/xdg_runtime_dir"
    assert get_log_path(create=False) == "/xdg_cache_home"
    assert get_autostart_path(create=False) == "/xdg_config_home/autostart"

    # test that we have reasonable fallbacks if XDG environment variables are not set

    del os.environ["XDG_CONFIG_HOME"]
    del os.environ["XDG_CACHE_HOME"]
    del os.environ["XDG_DATA_HOME"]
    del os.environ["XDG_RUNTIME_DIR"]

    assert get_conf_path(create=False) == get_home_dir() + "/.config"
    assert get_cache_path(create=False) == get_home_dir() + "/.cache"
    assert get_data_path(create=False) == get_home_dir() + "/.local/share"
    assert get_runtime_path(create=False) == get_home_dir() + "/.cache"
    assert get_log_path(create=False) == get_home_dir() + "/.cache"
    assert get_autostart_path(create=False) == get_home_dir() + "/.config/autostart"
示例#9
0
def test_macos_dirs(monkeypatch):
    # test appdirs on macOS

    monkeypatch.setattr(platform, "system", lambda: "Darwin")

    home = get_home_dir()

    assert get_conf_path(create=False) == home + "/Library/Application Support"
    assert get_cache_path(create=False) == get_conf_path(create=False)
    assert get_data_path(create=False) == get_conf_path(create=False)
    assert get_runtime_path(create=False) == get_conf_path(create=False)
    assert get_log_path(create=False) == home + "/Library/Logs"
    assert get_autostart_path(create=False) == home + "/Library/LaunchAgents"
示例#10
0
def select_dbx_path_dialog(config_name, allow_merge=False):
    """
    A CLI dialog to ask for a local Dropbox folder location.

    :param str config_name: The configuration to use for the default folder name.
    :param bool allow_merge: If ``True``, allows the selection of an existing folder
        without deleting it. Defaults to ``False``.
    :returns: Path given by user.
    :rtype: str
    """

    conf = MaestralConfig(config_name)

    default = osp.join(get_home_dir(), conf.get('main', 'default_dir_name'))

    while True:
        res = click.prompt('Please give Dropbox folder location',
                           default=default,
                           type=click.Path(writable=True))

        res = res.rstrip(osp.sep)

        dropbox_path = osp.expanduser(res or default)

        if osp.exists(dropbox_path):
            if allow_merge:
                choice = click.prompt(text=(
                    f'Directory "{dropbox_path}" already exists. Do you want to '
                    f'replace it or merge its content with your Dropbox?'),
                                      type=click.Choice(
                                          ['replace', 'merge', 'cancel']))
            else:
                replace = click.confirm(text=(
                    f'Directory "{dropbox_path}" already exists. Do you want to '
                    f'replace it? Its content will be lost!'), )
                choice = 'replace' if replace else 'cancel'

            if choice == 'replace':
                err = delete(dropbox_path)
                if err:
                    click.echo(
                        f'Could not write to location "{dropbox_path}". Please '
                        'make sure that you have sufficient permissions.')
                else:
                    return dropbox_path
            elif choice == 'merge':
                return dropbox_path

        else:
            return dropbox_path
示例#11
0
    def __init__(self, bundle_id: str, start_cmd: str) -> None:

        super().__init__()
        filename = bundle_id + ".plist"

        self.path = osp.join(get_home_dir(), "Library", "LaunchAgents")
        self.destination = osp.join(self.path, filename)

        self.plist_dict: Dict[str, Any] = dict(
            Label=None, ProcessType="Interactive", ProgramArguments=[], RunAtLoad=True
        )

        self.plist_dict["Label"] = str(bundle_id)
        self.plist_dict["ProgramArguments"] = shlex.split(start_cmd)
示例#12
0
def test_cased_path_candidates():

    # test that we can find a unique correctly cased path
    # starting from a candidate with scrambled casing

    path = "/usr/local/share".upper()
    candidates = cased_path_candidates(path)

    assert len(candidates) == 1
    assert "/usr/local/share" in candidates

    candidates = cased_path_candidates("/test", root="/usr/local/share")

    assert len(candidates) == 1
    assert "/usr/local/share/test" in candidates

    home = get_home_dir()

    # test that we can get multiple cased path
    # candidates on case-sensitive file systems

    if is_fs_case_sensitive(home):

        parent0 = osp.join(home, "test folder/subfolder")
        parent1 = osp.join(home, "Test Folder/subfolder")

        os.makedirs(parent0)
        os.makedirs(parent1)

        path = osp.join(parent0.lower(), "File.txt")

        try:
            candidates = cased_path_candidates(path)

            assert len(candidates) == 2
            assert osp.join(parent0, "File.txt") in candidates
            assert osp.join(parent1, "File.txt") in candidates

            candidates = cased_path_candidates(
                "/test folder/subfolder/File.txt", root=home
            )

            assert len(candidates) == 2
            assert osp.join(parent0, "File.txt") in candidates
            assert osp.join(parent1, "File.txt") in candidates

        finally:
            delete(parent0)
            delete(parent1)
示例#13
0
    def __init__(self, config_name, gui):
        super().__init__(config_name, gui)
        if self.gui:
            bundle_id = '{}.{}'.format(BUNDLE_ID, self.config_name)
        else:
            bundle_id = '{}-{}.{}'.format(BUNDLE_ID, 'daemon', self.config_name)
        filename = bundle_id + '.plist'

        with open(osp.join(_resources, 'com.samschott.maestral.plist'), 'r') as f:
            plist_template = f.read()

        self.destination = osp.join(get_home_dir(), 'Library', 'LaunchAgents', filename)
        self.contents = plist_template.format(
            bundle_id=bundle_id,
            start_cmd=self.start_cmd
        )
示例#14
0
def select_dbx_path_dialog(config_name, allow_merge=False):
    """
    A CLI dialog to ask for a local dropbox directory path.

    :param str config_name: The configuration to use for the default folder name.
    :param bool allow_merge: If ``True``, allows for selecting an existing path without
        deleting it. Defaults to ``False``.
    :returns: Path given by user.
    :rtype: str
    """

    conf = MaestralConfig(config_name)

    default = osp.join(get_home_dir(), conf.get('main', 'default_dir_name'))

    while True:
        res = click.prompt('Please give Dropbox folder location',
                           default=default,
                           type=click.Path(writable=True))

        res = res.rstrip(osp.sep)

        dropbox_path = osp.expanduser(res or default)
        old_path = osp.expanduser(conf.get('main', 'path'))

        try:
            same_path = osp.samefile(old_path, dropbox_path)
        except FileNotFoundError:
            same_path = False

        if osp.exists(dropbox_path) and not same_path:
            msg = (f'Directory "{dropbox_path}" already exist. Do you want to '
                   'overwrite it? Its content will be lost!')
            if click.confirm(msg, prompt_suffix=''):
                err = delete(dropbox_path)
                if err:
                    click.echo(
                        f'Could not write to location "{dropbox_path}". Please '
                        'make sure that you have sufficient permissions.')
                else:
                    return dropbox_path
            elif allow_merge:
                msg = 'Would you like to merge its content with your Dropbox?'
                if click.confirm(msg, prompt_suffix=''):
                    return dropbox_path
        else:
            return dropbox_path
示例#15
0
def test_xdg_fallback_dirs(monkeypatch):
    # test that we have reasonable fallbacks if XDG environment variables are not set

    monkeypatch.setattr(platform, "system", lambda: "Linux")

    monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
    monkeypatch.delenv("XDG_CACHE_HOME", raising=False)
    monkeypatch.delenv("XDG_DATA_HOME", raising=False)
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)

    home = get_home_dir()

    assert get_conf_path(create=False) == home + "/.config"
    assert get_cache_path(create=False) == home + "/.cache"
    assert get_data_path(create=False) == home + "/.local/share"
    assert get_runtime_path(create=False) == home + "/.cache"
    assert get_log_path(create=False) == home + "/.cache"
    assert get_autostart_path(create=False) == home + "/.config/autostart"
示例#16
0
    def setUp(self):

        syncing = Event()
        startup = Event()
        syncing.set()

        local_dir = osp.join(get_home_dir(), "dummy_dir")
        os.mkdir(local_dir)

        self.sync = SyncEngine(DropboxClient("test-config"),
                               FSEventHandler(syncing, startup))

        self.sync.dropbox_path = local_dir

        self.observer = Observer()
        self.observer.schedule(self.sync.fs_events,
                               self.sync.dropbox_path,
                               recursive=True)
        self.observer.start()
示例#17
0
def sync():

    local_dir = osp.join(get_home_dir(), "dummy_dir")
    os.mkdir(local_dir)

    sync = SyncEngine(DropboxClient("test-config"))
    sync.fs_events.enable()
    sync.dropbox_path = local_dir

    observer = Observer()
    observer.schedule(sync.fs_events, sync.dropbox_path, recursive=True)
    observer.start()

    yield sync

    observer.stop()
    observer.join()

    remove_configuration("test-config")
    delete(sync.dropbox_path)
示例#18
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()
示例#19
0
    def __init__(self, app: toga.App) -> None:
        # noinspection PyTypeChecker
        super().__init__(
            title="Maestral Setup",
            size=(self.WINDOW_WIDTH, self.WINDOW_HEIGHT),
            resizeable=False,
            minimizable=False,
            app=app,
        )

        # FIXME: remove private API access
        self._impl.native.titlebarAppearsTransparent = True
        self._impl.native.titleVisibility = 1
        self._impl.native.styleMask |= NSFullSizeContentViewWindowMask
        self._impl.native.movableByWindowBackground = True

        self.current_page = 0

        # ==== welcome page ============================================================
        # noinspection PyTypeChecker
        self.image0 = toga.ImageView(
            self.app.icon,
            style=Pack(width=128,
                       height=128,
                       alignment=CENTER,
                       padding=(40, 0, 40, 0)),
        )
        self.label0 = Label(
            text="Welcome to Maestral, an open source Dropbox client.",
            style=Pack(width=self.WINDOW_WIDTH,
                       padding_bottom=40,
                       text_align=CENTER),
        )
        self.btn_start = toga.Button("Link Dropbox Account",
                                     style=Pack(width=180))

        self.welcome_page = toga.Box(
            children=[
                self.image0, self.label0, self.btn_start,
                Spacer(COLUMN)
            ],
            style=self.page_style,
        )

        # ==== link page ===============================================================

        # noinspection PyTypeChecker
        self.image1 = toga.ImageView(self.app.icon,
                                     style=Pack(width=64,
                                                height=64,
                                                padding=(40, 0, 40, 0)))
        self.label1 = Label(
            text=(
                "To link Maestral to your Dropbox account, please retrieve an "
                "authorization token from Dropbox and enter it below."),
            linebreak_mode=WORD_WRAP,
            style=Pack(width=self.CONTENT_WIDTH * 0.9,
                       text_align=CENTER,
                       padding_bottom=10),
        )
        self.btn_auth_token = FollowLinkButton("Retrieve Token",
                                               style=Pack(width=125,
                                                          padding_bottom=35))
        self.text_field_auth_token = toga.TextInput(
            placeholder="Authorization Token",
            style=Pack(
                width=self.CONTENT_WIDTH * 0.9,
                text_align=CENTER,
                background_color=TRANSPARENT,
            ),
        )
        self.spinner_link = toga.ActivityIndicator(
            style=Pack(width=32, height=32))
        self.dialog_buttons_link_page = DialogButtons(labels=("Link",
                                                              "Cancel"),
                                                      style=self.btn_box_style)
        self.dialog_buttons_link_page["Link"].enabled = False

        self.link_page = toga.Box(
            children=[
                self.image1,
                self.label1,
                self.btn_auth_token,
                self.text_field_auth_token,
                Spacer(COLUMN),
                self.spinner_link,
                Spacer(COLUMN),
                self.dialog_buttons_link_page,
            ],
            style=self.page_style,
        )

        # ==== dbx location page =======================================================

        # noinspection PyTypeChecker
        self.image2 = toga.ImageView(self.app.icon,
                                     style=Pack(width=64,
                                                height=64,
                                                padding=(40, 0, 40, 0)))
        self.dbx_location_label = Label(
            text=
            ("Maestral has been successfully linked with your Dropbox account.\n\n"
             "Please select a local folder for your Dropbox. If the folder is not "
             "empty, you will be given the option to merge its content with your "
             "remote Dropbox. Merging will not transfer or duplicate any identical "
             "files.\n\n"
             "In the next step, you will be asked to choose which folders to sync."
             ),
            linebreak_mode=WORD_WRAP,
            style=Pack(
                width=self.CONTENT_WIDTH,
                height=90,
                padding_bottom=20,
                text_align=CENTER,
            ),
        )
        self.combobox_dbx_location = FileSelectionButton(
            initial=get_home_dir(),
            select_files=False,
            select_folders=True,
            show_full_path=True,
            style=Pack(width=self.CONTENT_WIDTH * 0.9, padding_bottom=20),
        )

        self.dialog_buttons_location_page = DialogButtons(
            labels=("Select", "Cancel & Unlink"), style=self.btn_box_style)

        self.dbx_location_page = toga.Box(
            children=[
                self.image2,
                self.dbx_location_label,
                self.combobox_dbx_location,
                Spacer(COLUMN),
                self.dialog_buttons_location_page,
            ],
            style=self.page_style,
        )

        # ==== selective sync page =====================================================

        self.label3 = Label(
            text=
            ("Please select which files and folders to sync below. The initial "
             "download may take some time, depending on the size of your Dropbox."
             ),
            linebreak_mode=WORD_WRAP,
            style=Pack(width=self.CONTENT_WIDTH, padding=(20, 0, 20, 0)),
        )
        self.dropbox_tree = toga.Tree(
            headings=["Name", "Included"],
            accessors=["name", "included"],
            data=[],
            style=Pack(width=self.CONTENT_WIDTH, padding_bottom=20, flex=1),
            multiple_select=True,
        )

        self.dialog_buttons_selective_sync_page = DialogButtons(
            labels=["Select", "Back"],
            style=self.btn_box_style,
        )

        self.selective_sync_page = toga.Box(
            children=[
                self.label3,
                self.dropbox_tree,
                self.dialog_buttons_selective_sync_page,
            ],
            style=self.page_style,
        )

        # ==== done page ===============================================================

        # noinspection PyTypeChecker
        self.image4 = toga.ImageView(
            self.app.icon,
            style=Pack(width=128,
                       height=128,
                       alignment=CENTER,
                       padding=(40, 0, 40, 0)),
        )
        self.label4 = Label(
            text=
            ("You have successfully set up Maestral. Please allow some time for the "
             "initial indexing and download of your Dropbox before Maestral will "
             "commence syncing."),
            linebreak_mode=WORD_WRAP,
            style=Pack(width=self.CONTENT_WIDTH,
                       text_align=CENTER,
                       padding_bottom=50),
        )
        self.close_button = toga.Button("Close",
                                        style=Pack(width=100),
                                        on_press=lambda s: self.close())

        self.done_page = toga.Box(
            children=[
                self.image4, self.label4, self.close_button,
                Spacer(COLUMN)
            ],
            style=self.page_style,
        )

        self.pages = (
            self.welcome_page,
            self.link_page,
            self.dbx_location_page,
            self.selective_sync_page,
            self.done_page,
        )
        self.content = toga.Box(children=[self.pages[0]])
示例#20
0
def select_dbx_path_dialog(
    config_name: str, default_dir_name: Optional[str] = None, allow_merge: bool = False
) -> str:
    """
    A CLI dialog to ask for a local Dropbox folder location.

    :param config_name: The configuration to use for the default folder name.
    :param default_dir_name: The default directory name. Defaults to
        "Dropbox ({config_name})" if not given.
    :param allow_merge: If ``True``, allows the selection of an existing folder without
        deleting it. Defaults to ``False``.
    :returns: Path given by user.
    """

    from maestral.utils.appdirs import get_home_dir
    from maestral.utils.path import delete

    default_dir_name = default_dir_name or f"Dropbox ({config_name.capitalize()})"
    default = osp.join(get_home_dir(), default_dir_name)

    while True:
        res = click.prompt(
            "Please give Dropbox folder location",
            default=default,
            type=click.Path(writable=True),
        )

        res = res.rstrip(osp.sep)

        dropbox_path = osp.expanduser(res or default)

        if osp.exists(dropbox_path):
            if allow_merge:
                choice = click.prompt(
                    text=(
                        f'Directory "{dropbox_path}" already exists.\nDo you want to '
                        f"replace it or merge its content with your Dropbox?"
                    ),
                    type=click.Choice(["replace", "merge", "cancel"]),
                )
            else:
                replace = click.confirm(
                    text=(
                        f'Directory "{dropbox_path}" already exists. Do you want to '
                        f"replace it? Its content will be lost!"
                    ),
                )
                choice = "replace" if replace else "cancel"

            if choice == "replace":
                err = delete(dropbox_path)
                if err:
                    click.echo(
                        f'Could not write to location "{dropbox_path}". Please '
                        "make sure that you have sufficient permissions."
                    )
                else:
                    return dropbox_path
            elif choice == "merge":
                return dropbox_path

        else:
            return dropbox_path
示例#21
0
    def __init__(self, **kwargs) -> None:
        super().__init__(
            title="Maestral Settings",
            resizeable=False,
            minimizable=False,
            release_on_close=False,
            **kwargs,
        )

        # ==== account info section ====================================================

        self.profile_pic_view = toga.ImageView(
            self.faceholder,
            style=Pack(
                width=SettingsGui.IMAGE_WIDTH,
                height=SettingsGui.IMAGE_WIDTH,
                background_color=TRANSPARENT,
            ),
        )
        apply_round_clipping(self.profile_pic_view)

        self.profile_pic_view_spacer = toga.Box(style=Pack(
            width=SettingsGui.COLUMN_WIDTH_LEFT - SettingsGui.IMAGE_WIDTH,
            direction=ROW,
            background_color=TRANSPARENT,
        ))

        self.label_name = Label(
            "Account Name (Company Name)",
            style=Pack(
                font_size=17,
                padding_bottom=SettingsGui.ELEMENT_PADDING - 4,
                width=SettingsGui.COLUMN_WIDTH_RIGHT,
            ),
        )
        self.label_email = Label(
            "[email protected], Business",
            style=Pack(
                padding_bottom=SettingsGui.SUBELEMENT_PADDING,
                width=SettingsGui.COLUMN_WIDTH_RIGHT,
                font_size=12,
            ),
        )
        self.label_usage = Label(
            "10.5 % from 1,005 TB used",
            style=Pack(
                padding_bottom=SettingsGui.ELEMENT_PADDING,
                width=SettingsGui.COLUMN_WIDTH_RIGHT,
                font_size=12,
            ),
        )
        self.btn_unlink = toga.Button(
            "Unlink this Dropbox...",
            style=Pack(width=SettingsGui.BUTTON_WIDTH))

        account_info_box = toga.Box(
            children=[
                self.profile_pic_view_spacer,
                self.profile_pic_view,
                toga.Box(
                    children=[
                        self.label_name,
                        self.label_email,
                        self.label_usage,
                        self.btn_unlink,
                    ],
                    style=Pack(direction=COLUMN,
                               padding_left=SettingsGui.COLUMN_PADDING),
                ),
            ],
            style=Pack(direction=ROW),
        )

        # ==== sync settings section ===================================================

        self._label_select_folders = Label(
            "Selective sync:",
            style=Pack(text_align=RIGHT, width=SettingsGui.COLUMN_WIDTH_LEFT),
        )
        self.btn_select_folders = toga.Button(
            label="Select files and folders...",
            style=Pack(padding_left=SettingsGui.COLUMN_PADDING,
                       width=SettingsGui.BUTTON_WIDTH),
        )

        self._label_dbx_location = Label(
            "Local Dropbox folder:",
            style=Pack(text_align=RIGHT, width=SettingsGui.COLUMN_WIDTH_LEFT),
        )
        self.combobox_dbx_location = FileSelectionButton(
            initial=get_home_dir(),
            select_files=False,
            select_folders=True,
            style=Pack(padding_left=SettingsGui.COLUMN_PADDING,
                       width=SettingsGui.BUTTON_WIDTH),
        )

        dropbox_settings_box = toga.Box(
            children=[
                toga.Box(
                    children=[
                        self._label_select_folders, self.btn_select_folders
                    ],
                    style=Pack(alignment=CENTER,
                               padding_bottom=SettingsGui.ELEMENT_PADDING),
                ),
                toga.Box(
                    children=[
                        self._label_dbx_location, self.combobox_dbx_location
                    ],
                    style=Pack(alignment=CENTER),
                ),
            ],
            style=Pack(direction=COLUMN),
        )

        # ==== system settings section =================================================

        self._label_update_interval = Label(
            "Check for updates:",
            style=Pack(text_align=RIGHT, width=SettingsGui.COLUMN_WIDTH_LEFT),
        )
        self.combobox_update_interval = toga.Selection(
            items=["Daily", "Weekly", "Monthly", "Never"],
            style=Pack(padding_left=SettingsGui.COLUMN_PADDING,
                       width=SettingsGui.BUTTON_WIDTH),
        )

        self._label_system_settings = Label(
            "System settings:",
            style=Pack(text_align=RIGHT, width=SettingsGui.COLUMN_WIDTH_LEFT),
        )
        self.checkbox_autostart = Switch(
            label="Start Maestral on login",
            style=Pack(
                padding_bottom=SettingsGui.SUBELEMENT_PADDING,
                width=SettingsGui.COLUMN_WIDTH_RIGHT,
            ),
        )
        self.checkbox_notifications = Switch(
            label="Enable notifications on file changes",
            style=Pack(
                padding_bottom=SettingsGui.SUBELEMENT_PADDING,
                width=SettingsGui.COLUMN_WIDTH_RIGHT,
            ),
        )

        children = [
            toga.Box(
                children=[
                    self._label_update_interval, self.combobox_update_interval
                ],
                style=Pack(alignment=CENTER,
                           padding_bottom=SettingsGui.ELEMENT_PADDING),
            ),
            toga.Box(children=[
                self._label_system_settings,
                toga.Box(
                    children=[
                        self.checkbox_autostart,
                        self.checkbox_notifications,
                    ],
                    style=Pack(
                        alignment=TOP,
                        direction=COLUMN,
                        padding_left=SettingsGui.COLUMN_PADDING,
                    ),
                ),
            ], ),
        ]

        if FROZEN:
            # add UI to install command line interface
            self._label_cli_tool = Label(
                "Command line tool:",
                style=Pack(text_align=RIGHT,
                           width=SettingsGui.COLUMN_WIDTH_LEFT),
            )

            self.label_cli_tool_info = Label(
                "Install the 'maestral' command line tool to /usr/local/bin.",
                style=Pack(
                    color=GRAY,
                    font_size=12,
                    width=SettingsGui.COLUMN_WIDTH_RIGHT,
                    padding_left=SettingsGui.COLUMN_PADDING,
                ),
            )

            self.btn_cli_tool = toga.Button(
                "Install",
                style=Pack(
                    width=SettingsGui.BUTTON_WIDTH / 2,
                    padding_bottom=SettingsGui.SUBELEMENT_PADDING,
                    padding_left=SettingsGui.COLUMN_PADDING,
                ),
            )

            children.append(
                toga.Box(
                    children=[
                        self._label_cli_tool,
                        self.btn_cli_tool,
                    ],
                    style=Pack(alignment=CENTER,
                               padding_top=SettingsGui.ELEMENT_PADDING),
                ))
            children.append(
                toga.Box(
                    children=[
                        Label(
                            " ",
                            style=Pack(text_align=RIGHT,
                                       width=SettingsGui.COLUMN_WIDTH_LEFT),
                        ),
                        self.label_cli_tool_info,
                    ],
                    style=Pack(alignment=CENTER,
                               padding_top=SettingsGui.SUBELEMENT_PADDING),
                ))

        maestral_settings_box = toga.Box(
            children=children,
            style=Pack(direction=COLUMN),
        )

        # ==== about section ===========================================================

        about_box = toga.Box(
            children=[
                Label(
                    "About Maestral:",
                    style=Pack(text_align=RIGHT,
                               width=SettingsGui.COLUMN_WIDTH_LEFT),
                ),
                toga.Box(
                    children=[
                        Label(
                            f"GUI v{__version__}, daemon v{__daemon_version__}",
                            style=Pack(
                                padding_bottom=SettingsGui.SUBELEMENT_PADDING,
                                width=SettingsGui.COLUMN_WIDTH_RIGHT,
                            ),
                        ),
                        LinkLabel(
                            text=__url__,
                            url=__url__,
                            style=Pack(
                                padding_bottom=SettingsGui.SUBELEMENT_PADDING,
                                width=SettingsGui.COLUMN_WIDTH_RIGHT,
                            ),
                        ),
                        Label(
                            f"© 2018 - {year}, {__author__}.",
                            style=Pack(color=GRAY,
                                       width=SettingsGui.COLUMN_WIDTH_RIGHT),
                        ),
                    ],
                    style=Pack(direction=COLUMN,
                               padding_left=SettingsGui.COLUMN_PADDING),
                ),
            ],
            style=Pack(direction=ROW),
        )

        main_box = toga.Box(
            children=[
                account_info_box,
                toga.Divider(style=Pack(padding=SettingsGui.SECTION_PADDING)),
                dropbox_settings_box,
                toga.Divider(style=Pack(padding=SettingsGui.SECTION_PADDING)),
                maestral_settings_box,
                toga.Divider(style=Pack(padding=SettingsGui.SECTION_PADDING)),
                about_box,
            ],
            style=Pack(
                direction=COLUMN,
                padding=30,
                width=SettingsGui.COLUMN_WIDTH_LEFT +
                SettingsGui.COLUMN_WIDTH_RIGHT,
            ),
        )

        self.content = main_box
示例#22
0
    # starting from a candidate with scrambled casing

    path = "/usr/local/share".upper()
    candidates = cased_path_candidates(path)

    assert len(candidates) == 1
    assert "/usr/local/share" in candidates

    candidates = cased_path_candidates("/test", root="/usr/local/share")

    assert len(candidates) == 1
    assert "/usr/local/share/test" in candidates


@pytest.mark.skipif(
    not is_fs_case_sensitive(get_home_dir()),
    reason="requires case-sensitive file system",
)
def test_multiple_cased_path_candidates(tmp_path):

    # test that we can get multiple cased path
    # candidates on case-sensitive file systems

    # create two folders that differ only in casing

    dir0 = tmp_path / "test folder/subfolder"
    dir1 = tmp_path / "Test Folder/subfolder"

    dir0.mkdir(parents=True, exist_ok=True)
    dir1.mkdir(parents=True, exist_ok=True)
示例#23
0
    def __init__(self, config_name='maestral', pending_link=True, parent=None):
        super().__init__(parent=parent)
        # load user interface layout from .ui file
        uic.loadUi(SETUP_DIALOG_PATH, self)
        self.setWindowFlags(Qt.WindowStaysOnTopHint)

        self._config_name = config_name
        self._conf = MaestralConfig(config_name)
        self._state = MaestralState(config_name)

        self.app_icon = QtGui.QIcon(APP_ICON_PATH)

        self.labelIcon_0.setPixmap(icon_to_pixmap(self.app_icon, 150))
        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, 120))

        self.mdbx = None
        self.dbx_model = None
        self.excluded_items = []

        # resize dialog buttons
        width = self.pushButtonAuthPageCancel.width()*1.1
        for b in (self.pushButtonAuthPageLink, self.pushButtonDropboxPathUnlink,
                  self.pushButtonDropboxPathSelect, self.pushButtonFolderSelectionBack,
                  self.pushButtonFolderSelectionSelect, self.pushButtonAuthPageCancel,
                  self.pushButtonDropboxPathCalcel, self.pushButtonClose):
            b.setMinimumWidth(width)
            b.setMaximumWidth(width)

        # set up combobox
        self.dropbox_location = osp.dirname(self._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(Qt.WA_DeleteOnClose)
        self.pushButtonLink.clicked.connect(self.on_link)
        self.pushButtonAuthPageCancel.clicked.connect(self.on_reject_requested)
        self.pushButtonAuthPageLink.clicked.connect(self.on_auth_clicked)
        self.pushButtonDropboxPathCalcel.clicked.connect(self.on_reject_requested)
        self.pushButtonDropboxPathSelect.clicked.connect(self.on_dropbox_location_selected)
        self.pushButtonDropboxPathUnlink.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.on_accept_requested)
        self.selectAllCheckBox.clicked.connect(self.on_select_all_clicked)

        default_dir_name = self._conf.get('main', 'default_dir_name')

        self.labelDropboxPath.setText(self.labelDropboxPath.text().format(default_dir_name))

        # check if we are already authenticated, skip authentication if yes
        if not pending_link:
            start_maestral_daemon_thread(self._config_name, run=False)
            self.mdbx = get_maestral_proxy(self._config_name)
            self.mdbx.get_account_info()
            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(self._conf.get('main', 'path'), default_dir_name))
            self.pushButtonDropboxPathCalcel.setText('Quit')
            self.stackedWidget.setCurrentIndex(2)
            self.stackedWidgetButtons.setCurrentIndex(2)
        else:
            self.stackedWidget.setCurrentIndex(0)
            self.stackedWidgetButtons.setCurrentIndex(0)
示例#24
0
    def __init__(self, mdbx, parent=None):
        super().__init__(parent=parent)
        self.setupUi(self)

        self.mdbx = mdbx
        self.config_name = self.mdbx.config_name
        self.dbx_model = None
        self.excluded_items = []

        self.app_icon = QtGui.QIcon(APP_ICON_PATH)

        self.labelIcon_0.setPixmap(icon_to_pixmap(self.app_icon, 150))
        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, 120))

        # prepare auth session
        self.auth_url = self.mdbx.get_auth_url()
        prompt = self.labelAuthLink.text().format(self.auth_url)
        self.labelAuthLink.setText(prompt)

        # set up Dropbox location combobox

        self.dropbox_location = self.mdbx.get_conf("sync", "path")

        if self.dropbox_location == "":
            folder_name = f"Dropbox ({self.config_name.capitalize()})"
            self.dropbox_location = osp.join(get_home_dir(), folder_name)

        self.comboBoxDropboxPath.addItem(native_folder_icon(), self.dropbox_location)
        self.comboBoxDropboxPath.insertSeparator(1)
        self.comboBoxDropboxPath.addItem(QtGui.QIcon(), "Choose...")
        self.comboBoxDropboxPath.currentIndexChanged.connect(self.on_combobox)

        # resize dialog buttons
        width = self.pushButtonAuthPageCancel.width() * 1.1
        width = round(width)
        for b in (
            self.pushButtonAuthPageLink,
            self.pushButtonDropboxPathUnlink,
            self.pushButtonDropboxPathSelect,
            self.pushButtonFolderSelectionBack,
            self.pushButtonFolderSelectionSelect,
            self.pushButtonAuthPageCancel,
            self.pushButtonDropboxPathCancel,
            self.pushButtonClose,
        ):
            b.setMinimumWidth(width)
            b.setMaximumWidth(width)

        self.dropbox_folder_dialog = QtWidgets.QFileDialog(self)
        self.dropbox_folder_dialog.setAcceptMode(
            QtWidgets.QFileDialog.AcceptMode.AcceptOpen
        )
        self.dropbox_folder_dialog.setFileMode(QtWidgets.QFileDialog.FileMode.Directory)
        self.dropbox_folder_dialog.setOption(
            QtWidgets.QFileDialog.Option.ShowDirsOnly, True
        )
        self.dropbox_folder_dialog.setLabelText(
            QtWidgets.QFileDialog.DialogLabel.Accept, "Select"
        )
        self.dropbox_folder_dialog.setDirectory(get_home_dir())
        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(Qt.WidgetAttribute.WA_DeleteOnClose)
        self.pushButtonLink.clicked.connect(self.on_link_clicked)
        self.pushButtonAuthPageCancel.clicked.connect(self.on_reject_requested)
        self.pushButtonAuthPageLink.clicked.connect(self.on_auth_clicked)
        self.pushButtonDropboxPathCancel.clicked.connect(self.on_reject_requested)
        self.pushButtonDropboxPathSelect.clicked.connect(
            self.on_dropbox_location_selected
        )
        self.pushButtonDropboxPathUnlink.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.on_accept_requested)
        self.selectAllCheckBox.clicked.connect(self.on_select_all_clicked)

        if not self.mdbx.pending_link:
            self.stackedWidget.setCurrentIndex(2)