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)
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"
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'
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 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
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'
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 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"
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"
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
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)
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)
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 )
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
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"
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()
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)
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 __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]])
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
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
# 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)
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)
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)