async def test_mountpoint_iterdir_with_many_files(n, base_path, base_mountpoint, alice_user_fs, event_bus): wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.mkdir(base_path, parents=True, exist_ok=True) names = [f"some_file_{i:03d}.txt" for i in range(n)] path_list = [FsPath(f"{base_path}/{name}") for name in names] for path in path_list: await workspace.touch(path) # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) test_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath(base_path)) # Work around trio issue #1308 (https://github.com/python-trio/trio/issues/1308) items = await trio.to_thread.run_sync( lambda: [path.name for path in Path(test_path).iterdir()]) assert items == names for path in path_list: item_path = mountpoint_manager.get_path_in_mountpoint(wid, path) assert await trio.Path(item_path).exists()
async def test_idempotent_mount(base_mountpoint, alice_user_fs, event_bus, manual_unmount): # Populate a bit the fs first... wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.touch("/bar.txt") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) bar_txt = get_path_in_mountpoint(mountpoint_manager, wid, "/bar.txt") assert await bar_txt.exists() with pytest.raises(MountpointAlreadyMounted): await mountpoint_manager.mount_workspace(wid) assert await bar_txt.exists() await mountpoint_manager.unmount_workspace(wid) assert not await bar_txt.exists() with pytest.raises(MountpointNotMounted): await mountpoint_manager.unmount_workspace(wid) assert not await bar_txt.exists() await mountpoint_manager.mount_workspace(wid) assert await bar_txt.exists()
async def test_get_path_in_mountpoint(base_mountpoint, alice_user_fs, event_bus): # Populate a bit the fs first... wid = await alice_user_fs.workspace_create("mounted_wksp") wid2 = await alice_user_fs.workspace_create("not_mounted_wksp") workspace1 = alice_user_fs.get_workspace(wid) workspace2 = alice_user_fs.get_workspace(wid2) await workspace1.touch("/bar.txt") await workspace2.touch("/foo.txt") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) bar_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath("/bar.txt")) assert isinstance(bar_path, PurePath) # Windows uses drives, not base_mountpoint if os.name != "nt": expected = base_mountpoint / "mounted_wksp" / "bar.txt" assert str(bar_path) == str(expected.absolute()) assert await trio.Path(bar_path).exists() with pytest.raises(MountpointNotMounted): mountpoint_manager.get_path_in_mountpoint(wid2, FsPath("/foo.txt"))
async def test_mount_and_explore_workspace(base_mountpoint, alice_user_fs, event_bus, manual_unmount): # Populate a bit the fs first... wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.mkdir("/foo") await workspace.touch("/bar.txt") await workspace.write_bytes("/bar.txt", b"Hello world !") # Now we can start fuse with event_bus.listen() as spy: async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) mountpoint_path = get_path_in_mountpoint(mountpoint_manager, wid, "/") expected = { "mountpoint": mountpoint_path, "workspace_id": wid, "timestamp": None } spy.assert_events_occured([ (ClientEvent.MOUNTPOINT_STARTING, expected), (ClientEvent.MOUNTPOINT_STARTED, expected), ]) # Finally explore the mountpoint def inspect_mountpoint(): wksp_children = set(os.listdir(mountpoint_path)) assert wksp_children == {"foo", "bar.txt"} bar_stat = os.stat(f"{mountpoint_path}/bar.txt") assert bar_stat.st_size == len(b"Hello world !") with open(f"{mountpoint_path}/bar.txt", "rb") as fd: bar_txt = fd.read() assert bar_txt == b"Hello world !" # Note given python fs api is blocking, we must run it inside a thread # to avoid blocking the trio loop and ending up in a deadlock await trio.to_thread.run_sync(inspect_mountpoint) if manual_unmount: await mountpoint_manager.unmount_workspace(wid) # Mountpoint should be stopped by now spy.assert_events_occured([(ClientEvent.MOUNTPOINT_STOPPED, expected)]) if not manual_unmount: # Mountpoint should be stopped by now spy.assert_events_occured([(ClientEvent.MOUNTPOINT_STOPPED, expected)])
async def test_mount_unknown_workspace(base_mountpoint, alice_user_fs, event_bus): async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: wid = uuid4() with pytest.raises(MountpointConfigurationError) as exc: await mountpoint_manager.mount_workspace(wid) assert exc.value.args == (f"Workspace `{wid}` doesn't exist", )
async def logged_client_factory(config: ClientConfig, device: LocalDevice, event_bus: Optional[EventBus] = None): event_bus = event_bus or EventBus() # Get the pattern filter if config.pattern_filter is None: pattern_filter = get_pattern_filter(config.pattern_filter_path) else: try: pattern_filter = re.compile(config.pattern_filter) except re.error: pattern_filter = get_pattern_filter(config.pattern_filter_path) backend_conn = BackendAuthenticatedConn( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, event_bus=event_bus, max_cooldown=config.backend_max_cooldown, max_pool=config.backend_max_connections, keepalive=config.backend_connection_keepalive, ) path = config.data_base_dir / device.slug remote_devices_manager = RemoteDevicesManager(backend_conn.cmds, device.root_verify_key) async with UserFS.run(device, path, backend_conn.cmds, remote_devices_manager, event_bus, pattern_filter) as user_fs: backend_conn.register_monitor( partial(monitor_messages, user_fs, event_bus)) backend_conn.register_monitor(partial(monitor_sync, user_fs, event_bus)) async with backend_conn.run(): async with mountpoint_manager_factory( user_fs, event_bus, config.mountpoint_base_dir, mount_all=config.mountpoint_enabled, mount_on_workspace_created=config.mountpoint_enabled, mount_on_workspace_shared=config.mountpoint_enabled, unmount_on_workspace_revoked=config.mountpoint_enabled, exclude_from_mount_all=config.disabled_workspaces, ) as mountpoint_manager: yield LoggedClient( config=config, device=device, event_bus=event_bus, mountpoint_manager=mountpoint_manager, user_fs=user_fs, remote_devices_manager=remote_devices_manager, backend_conn=backend_conn, )
async def test_inconsistent_folder_with_network(base_mountpoint, running_backend, alice_user_fs): async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint) as alice_mountpoint_manager: workspace = await create_inconsistent_workspace(alice_user_fs) mountpoint_path = await alice_mountpoint_manager.mount_workspace( workspace.workspace_id) await trio.to_thread.run_sync(_os_tests, mountpoint_path, errno.EACCES, WINDOWS_ERROR_PERMISSION_DENIED)
async def test_inconsistent_folder_no_network(base_mountpoint, running_backend, alice_user_fs): async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint) as alice_mountpoint_manager: workspace = await create_inconsistent_workspace(alice_user_fs) mountpoint_path = await alice_mountpoint_manager.mount_workspace( workspace.workspace_id) with running_backend.offline(): await trio.to_thread.run_sync(_os_tests, mountpoint_path, errno.EHOSTUNREACH, WINDOWS_ERROR_HOST_UNREACHABLE)
async def test_mountpoint_path_already_in_use(base_mountpoint, running_backend, alice_user_fs, alice2_user_fs): # Create a workspace and make it available in two devices wid = await alice_user_fs.workspace_create("w") await alice_user_fs.sync() await alice2_user_fs.sync() # Easily differenciate alice&alice2 await alice2_user_fs.get_workspace(wid).touch("/I_am_alice2.txt") await alice_user_fs.get_workspace(wid).touch("/I_am_alice.txt") naive_workspace_path = (base_mountpoint / "w").absolute() # Default workspace path already exists, souldn't be able to use it await trio.Path(base_mountpoint / "w").mkdir(parents=True) await trio.Path(base_mountpoint / "w" / "bar.txt").touch() async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint ) as alice_mountpoint_manager, mountpoint_manager_factory( alice2_user_fs, alice2_user_fs.event_bus, base_mountpoint) as alice2_mountpoint_manager: # Alice mount the workspace first alice_mountpoint_path = await alice_mountpoint_manager.mount_workspace( wid) assert str(alice_mountpoint_path) == f"{naive_workspace_path} (2)" # Alice2 should also be able to mount the workspace without name clashing alice2_mountpoint_path = await alice2_mountpoint_manager.mount_workspace( wid) assert str(alice2_mountpoint_path) == f"{naive_workspace_path} (3)" # Finally make sure each workspace is well mounted assert await trio.Path(alice_mountpoint_path / "I_am_alice.txt" ).exists() assert await trio.Path(alice2_mountpoint_path / "I_am_alice2.txt" ).exists()
async def test_base_mountpoint_not_created(base_mountpoint, alice_user_fs, event_bus): # Path should be created if it doesn' exist base_mountpoint = base_mountpoint / "dummy/dummy/dummy" wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.touch("/bar.txt") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) bar_txt = get_path_in_mountpoint(mountpoint_manager, wid, "/bar.txt") assert await bar_txt.exists()
async def _run_mountpoint( base_mountpoint, bootstrap_cb, *, task_status=trio.TASK_STATUS_IGNORED ): device = local_device_factory() async with user_fs_factory(device) as user_fs: async with mountpoint_manager_factory( user_fs, user_fs.event_bus, base_mountpoint, debug=False ) as mountpoint_manager: if bootstrap_cb: await bootstrap_cb(user_fs, mountpoint_manager) task_status.started((user_fs, mountpoint_manager)) await trio.sleep_forever()
async def test_runner_not_available(monkeypatch, alice_user_fs, event_bus): base_mountpoint = Path("/foo") def _import(name): if name == "winfspy": raise RuntimeError() else: raise ImportError() monkeypatch.setattr("guardata.client.mountpoint.manager.import_function", _import) with pytest.raises( (MountpointFuseNotAvailable, MountpointWinfspNotAvailable)): async with mountpoint_manager_factory(alice_user_fs, event_bus, base_mountpoint): pass
async def test_cancel_mount_workspace(base_mountpoint, alice_user_fs, event_bus, timeout): wid = await alice_user_fs.workspace_create("w") async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: with trio.move_on_after(timeout) as cancel_scope: await mountpoint_manager.mount_workspace(wid) if cancel_scope.cancelled_caught: with pytest.raises(MountpointNotMounted): mountpoint_manager.get_path_in_mountpoint(wid, FsPath("/")) else: path = trio.Path( mountpoint_manager.get_path_in_mountpoint(wid, FsPath("/"))) await path.exists() assert not await (path / "foo").exists()
async def test_mountpoint_access_unicode(base_mountpoint, alice_user_fs, event_bus): weird_name = "ÉŸ奇怪😀🔫🐍" wid = await alice_user_fs.workspace_create(weird_name) workspace = alice_user_fs.get_workspace(wid) await workspace.touch(f"/{weird_name}") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) root_path = mountpoint_manager.get_path_in_mountpoint(wid, FsPath("/")) # Work around trio issue #1308 (https://github.com/python-trio/trio/issues/1308) items = await trio.to_thread.run_sync( lambda: [path.name for path in Path(root_path).iterdir()]) assert items == [weird_name] item_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath(f"/{weird_name}")) assert await trio.Path(item_path).exists()
async def test_mountpoint_revoke_access( base_mountpoint, alice_user_fs, alice2_user_fs, bob_user_fs, event_bus, running_backend, revoking, ): # Parametrization new_role = None if revoking == "read" else WorkspaceRole.READER # Bob creates and share two files with Alice wid = await create_shared_workspace("w", bob_user_fs, alice_user_fs, alice2_user_fs) workspace = bob_user_fs.get_workspace(wid) await workspace.touch("/foo.txt") await workspace.touch("/bar.txt") await workspace.touch("/to_delete.txt") await workspace.sync() def get_root_path(mountpoint_manager): root_path = mountpoint_manager.get_path_in_mountpoint(wid, FsPath("/")) # A trio path is required here, otherwise we risk a messy deadlock! return trio.Path(root_path) async def assert_cannot_read(mountpoint_manager, root_is_cached=False): root_path = get_root_path(mountpoint_manager) foo_path = root_path / "foo.txt" bar_path = root_path / "bar.txt" # For some reason, root_path.stat() does not trigger a new getattr call # to fuse operations if there has been a prior recent call to stat. if not root_is_cached: with pytest.raises(PermissionError): await root_path.stat() with pytest.raises(PermissionError): await foo_path.exists() with pytest.raises(PermissionError): await foo_path.read_bytes() # with pytest.raises(PermissionError): # await bar_path.exists() with pytest.raises(PermissionError): await bar_path.read_bytes() async def assert_cannot_write(mountpoint_manager, new_role): expected_error, expected_errno = PermissionError, errno.EACCES # On linux, errno.EROFS is not translated to a PermissionError if new_role is WorkspaceRole.READER and os.name != "nt": expected_error, expected_errno = OSError, errno.EROFS root_path = get_root_path(mountpoint_manager) foo_path = root_path / "foo.txt" bar_path = root_path / "bar.txt" with pytest.raises(expected_error) as ctx: await (root_path / "new_file.txt").touch() assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await (root_path / "new_directory").mkdir() assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await foo_path.write_bytes(b"foo contents") assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await foo_path.unlink() assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await bar_path.write_bytes(b"bar contents") assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await bar_path.unlink() async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: # Mount Bob workspace on Alice's side await mountpoint_manager.mount_workspace(wid) root_path = get_root_path(mountpoint_manager) # Alice can read await (root_path / "bar.txt").read_bytes() # Alice can write await (root_path / "bar.txt").write_bytes(b"test") # Alice can delete await (root_path / "to_delete.txt").unlink() assert not await (root_path / "to_delete.txt").exists() # Bob revokes Alice's read or write rights from her workspace await bob_user_fs.workspace_share(wid, alice_user_fs.device.user_id, new_role) await workspace.sync() # Let Alice process the info await alice_user_fs.process_last_messages() await alice2_user_fs.process_last_messages() await trio.sleep(0.25) # Alice still has read access if new_role is WorkspaceRole.READER: await (root_path / "bar.txt").read_bytes() # Alice no longer has read access else: await assert_cannot_read(mountpoint_manager, root_is_cached=True) # Alice no longer has write access await assert_cannot_write(mountpoint_manager, new_role) # Try again with Alice first device async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: # Mount alice workspace on bob's side once again await mountpoint_manager.mount_workspace(wid) root_path = get_root_path(mountpoint_manager) # Alice still has read access if new_role is WorkspaceRole.READER: await (root_path / "bar.txt").read_bytes() # Alice no longer has read access else: await assert_cannot_read(mountpoint_manager, root_is_cached=True) # Alice no longer has write access await assert_cannot_write(mountpoint_manager, new_role) # Try again with Alice second device async with mountpoint_manager_factory( alice2_user_fs, event_bus, base_mountpoint) as mountpoint_manager: # Mount alice workspace on bob's side once again await mountpoint_manager.mount_workspace(wid) root_path = get_root_path(mountpoint_manager) # Alice still has read access if new_role is WorkspaceRole.READER: await (root_path / "bar.txt").read_bytes() # Alice no longer has read access else: await assert_cannot_read(mountpoint_manager, root_is_cached=True) # Alice no longer has write access await assert_cannot_write(mountpoint_manager, new_role)