diff --git a/jupyterfs/extension.py b/jupyterfs/extension.py index 9414a94..e132d9f 100644 --- a/jupyterfs/extension.py +++ b/jupyterfs/extension.py @@ -10,7 +10,7 @@ from jupyter_server.utils import url_path_join from ._version import __version__ # noqa: F401 -from .metamanager import MetaManager, MetaManagerHandler +from .metamanager import MetaManagerMixin, MetaManager, MetaManagerHandler from .snippets import SnippetsHandler _mm_config_warning_msg = """Misconfiguration of MetaManager. Please add: @@ -42,7 +42,7 @@ def _load_jupyter_server_extension(serverapp): return if isinstance(serverapp.contents_manager_class, type) and not issubclass( - serverapp.contents_manager_class, MetaManager + serverapp.contents_manager_class, MetaManagerMixin ): serverapp.contents_manager_class = MetaManager serverapp.log.info( diff --git a/jupyterfs/metamanager.py b/jupyterfs/metamanager.py index 97792ac..269db91 100644 --- a/jupyterfs/metamanager.py +++ b/jupyterfs/metamanager.py @@ -15,7 +15,10 @@ from fs.errors import FSError from fs.opener.errors import OpenerError, ParseError from jupyter_server.base.handlers import APIHandler -from jupyter_server.services.contents.manager import AsyncContentsManager +from jupyter_server.services.contents.manager import ( + AsyncContentsManager, + ContentsManager, +) from .auth import substituteAsk, substituteEnv, substituteNone from .config import JupyterFs as JupyterFsConfig @@ -30,10 +33,10 @@ stripDrive, ) -__all__ = ["MetaManager", "MetaManagerHandler"] +__all__ = ["MetaManagerMixin", "MetaManager", "MetaManagerHandler", "SyncMetaManager"] -class MetaManager(AsyncContentsManager): +class MetaManagerMixin: copy_pat = re.compile(r"\-Copy\d*\.") @default("files_handler_params") @@ -153,7 +156,7 @@ def root_manager(self): def root_dir(self): return self.root_manager.root_dir - async def copy(self, from_path, to_path=None): + def copy(self, from_path, to_path=None): """Copy an existing file and return its new model. If to_path not specified, it will be the parent directory of from_path. @@ -205,32 +208,6 @@ def _getManagerForPath(self, path): return mgr, stripDrive(path) - is_hidden = path_first_arg("is_hidden", False) - dir_exists = path_first_arg("dir_exists", False) - file_exists = path_kwarg("file_exists", "", False) - exists = path_first_arg("exists", False) - - save = path_second_arg("save", "model", True) - rename = path_old_new("rename", False) - - get = path_first_arg("get", True) - delete = path_first_arg("delete", False) - - get_kernel_path = path_first_arg("get_kernel_path", False, sync=True) - - create_checkpoint = path_first_arg("create_checkpoint", False) - list_checkpoints = path_first_arg("list_checkpoints", False) - restore_checkpoint = path_second_arg( - "restore_checkpoint", - "checkpoint_id", - False, - ) - delete_checkpoint = path_second_arg( - "delete_checkpoint", - "checkpoint_id", - False, - ) - class MetaManagerHandler(APIHandler): _jupyterfsConfig = None @@ -300,3 +277,57 @@ async def post(self): self.finish( json.dumps(self.contents_manager.initResource(*resources, options=options)) ) + + +class MetaManager(MetaManagerMixin, AsyncContentsManager): + async def copy(self, from_path, to_path=None): + return super().copy(from_path=from_path, to_path=to_path) + + is_hidden = path_first_arg("is_hidden", False, sync=False) + dir_exists = path_first_arg("dir_exists", False, sync=False) + file_exists = path_kwarg("file_exists", "", False, sync=False) + exists = path_first_arg("exists", False, sync=False) + + save = path_second_arg("save", "model", True, sync=False) + rename = path_old_new("rename", False, sync=False) + + get = path_first_arg("get", True, sync=False) + delete = path_first_arg("delete", False, sync=False) + + get_kernel_path = path_first_arg("get_kernel_path", False, sync=False) + + create_checkpoint = path_first_arg("create_checkpoint", False, sync=False) + list_checkpoints = path_first_arg("list_checkpoints", False, sync=False) + restore_checkpoint = path_second_arg( + "restore_checkpoint", "checkpoint_id", False, sync=False + ) + delete_checkpoint = path_second_arg( + "delete_checkpoint", "checkpoint_id", False, sync=False + ) + + +class SyncMetaManager(MetaManagerMixin, ContentsManager): + def copy(self, from_path, to_path=None): + return super().copy(from_path=from_path, to_path=to_path) + + is_hidden = path_first_arg("is_hidden", False, sync=True) + dir_exists = path_first_arg("dir_exists", False, sync=True) + file_exists = path_kwarg("file_exists", "", False, sync=True) + exists = path_first_arg("exists", False, sync=True) + + save = path_second_arg("save", "model", True, sync=True) + rename = path_old_new("rename", False, sync=True) + + get = path_first_arg("get", True, sync=True) + delete = path_first_arg("delete", False, sync=True) + + get_kernel_path = path_first_arg("get_kernel_path", False, sync=True) + + create_checkpoint = path_first_arg("create_checkpoint", False, sync=True) + list_checkpoints = path_first_arg("list_checkpoints", False, sync=True) + restore_checkpoint = path_second_arg( + "restore_checkpoint", "checkpoint_id", False, sync=True + ) + delete_checkpoint = path_second_arg( + "delete_checkpoint", "checkpoint_id", False, sync=True + ) diff --git a/jupyterfs/pathutils.py b/jupyterfs/pathutils.py index 65b8be7..0e88d6e 100644 --- a/jupyterfs/pathutils.py +++ b/jupyterfs/pathutils.py @@ -112,58 +112,98 @@ async def _wrapper(self, *args, **kwargs): return _wrapper -def path_second_arg(method_name, first_argname, returns_model): +def path_second_arg(method_name, first_argname, returns_model, sync=False): """Decorator for methods that accept path as a second argument. e.g. manager.save(model, path, ...)""" - async def _wrapper(self, *args, **kwargs): - other, args = _get_arg(first_argname, args, kwargs) - path, args = _get_arg("path", args, kwargs) - _, mgr, mgr_path = _resolve_path(path, self._managers) - result = getattr(mgr, method_name)(other, mgr_path, *args, **kwargs) - return result + if sync: + + def _wrapper(self, *args, **kwargs): + other, args = _get_arg(first_argname, args, kwargs) + path, args = _get_arg("path", args, kwargs) + _, mgr, mgr_path = _resolve_path(path, self._managers) + result = getattr(mgr, method_name)(other, mgr_path, *args, **kwargs) + return result + + else: + + async def _wrapper(self, *args, **kwargs): + other, args = _get_arg(first_argname, args, kwargs) + path, args = _get_arg("path", args, kwargs) + _, mgr, mgr_path = _resolve_path(path, self._managers) + result = getattr(mgr, method_name)(other, mgr_path, *args, **kwargs) + return result return _wrapper -def path_kwarg(method_name, path_default, returns_model): +def path_kwarg(method_name, path_default, returns_model, sync=False): """Parameterized decorator for methods that accept path as a second argument. e.g. manager.file_exists(path='') """ + if sync: - async def _wrapper(self, path=path_default, **kwargs): - _, mgr, mgr_path = _resolve_path(path, self._managers) - result = getattr(mgr, method_name)(path=mgr_path, **kwargs) - return result + def _wrapper(self, path=path_default, **kwargs): + _, mgr, mgr_path = _resolve_path(path, self._managers) + result = getattr(mgr, method_name)(path=mgr_path, **kwargs) + return result + + else: + + async def _wrapper(self, path=path_default, **kwargs): + _, mgr, mgr_path = _resolve_path(path, self._managers) + result = getattr(mgr, method_name)(path=mgr_path, **kwargs) + return result return _wrapper -def path_old_new(method_name, returns_model): +def path_old_new(method_name, returns_model, sync=False): """Decorator for methods accepting old_path and new_path. e.g. manager.rename(old_path, new_path) """ + if sync: - async def _wrapper(self, old_path, new_path, *args, **kwargs): - old_prefix, old_mgr, old_mgr_path = _resolve_path(old_path, self._managers) - new_prefix, new_mgr, new_mgr_path = _resolve_path(new_path, self._managers) - if old_mgr is not new_mgr: - # TODO: Consider supporting this via get+save+delete. - raise HTTPError( - 400, - "Can't move files between backends yet ({old} -> {new})".format( - old=old_path, - new=new_path, - ), + def _wrapper(self, old_path, new_path, *args, **kwargs): + old_prefix, old_mgr, old_mgr_path = _resolve_path(old_path, self._managers) + new_prefix, new_mgr, new_mgr_path = _resolve_path(new_path, self._managers) + if old_mgr is not new_mgr: + # TODO: Consider supporting this via get+save+delete. + raise HTTPError( + 400, + "Can't move files between backends yet ({old} -> {new})".format( + old=old_path, + new=new_path, + ), + ) + assert new_prefix == old_prefix + result = getattr(new_mgr, method_name)( + old_mgr_path, new_mgr_path, *args, **kwargs ) - assert new_prefix == old_prefix - result = getattr(new_mgr, method_name)( - old_mgr_path, new_mgr_path, *args, **kwargs - ) - return result + return result + + else: + + async def _wrapper(self, old_path, new_path, *args, **kwargs): + old_prefix, old_mgr, old_mgr_path = _resolve_path(old_path, self._managers) + new_prefix, new_mgr, new_mgr_path = _resolve_path(new_path, self._managers) + if old_mgr is not new_mgr: + # TODO: Consider supporting this via get+save+delete. + raise HTTPError( + 400, + "Can't move files between backends yet ({old} -> {new})".format( + old=old_path, + new=new_path, + ), + ) + assert new_prefix == old_prefix + result = getattr(new_mgr, method_name)( + old_mgr_path, new_mgr_path, *args, **kwargs + ) + return result return _wrapper diff --git a/jupyterfs/tests/test_fsmanager.py b/jupyterfs/tests/test_fsmanager.py index 419bb65..638659d 100644 --- a/jupyterfs/tests/test_fsmanager.py +++ b/jupyterfs/tests/test_fsmanager.py @@ -1,259 +1,220 @@ -# ***************************************************************************** -# -# Copyright (c) 2019, the jupyter-fs authors. -# -# This file is part of the jupyter-fs library, distributed under the terms of -# the Apache License 2.0. The full license can be found in the LICENSE file. - -from contextlib import nullcontext -from pathlib import Path -import pytest -import os -import shutil -import socket - -import tornado.web - -from .utils import s3, samba -from .utils.client import ContentsClient - -test_dir = "test" -test_content = "foo\nbar\nbaz" -test_fname = "foo.txt" - -test_root_osfs = "osfs_local" - -test_url_s3 = "http://127.0.0.1/" -test_port_s3 = "9000" - -test_host_smb_docker_share = socket.gethostbyname(socket.gethostname()) -test_hostname_smb_docker_share = "test" -test_name_port_smb_docker_nameport = 4137 -test_name_port_smb_docker_share = 4139 - -test_direct_tcp_smb_os_share = False -test_host_smb_os_share = socket.gethostbyname_ex(socket.gethostname())[2][-1] -test_smb_port_smb_os_share = 139 - -_test_file_model = { - "content": test_content, - "format": "text", - "mimetype": "text/plain", - "name": test_fname, - "path": test_fname, - "type": "file", - "writable": True, -} - -configs = [ - { - "ServerApp": { - "jpserver_extensions": {"jupyterfs.extension": True}, - "contents_manager_class": "jupyterfs.metamanager.MetaManager", - }, - "ContentsManager": {"allow_hidden": True}, - }, - { - "ServerApp": { - "jpserver_extensions": {"jupyterfs.extension": True}, - "contents_manager_class": "jupyterfs.metamanager.MetaManager", - }, - "ContentsManager": {"allow_hidden": False}, - }, -] - - -class _TestBase: - """Contains tests universal to all PyFilesystemContentsManager flavors""" - - @pytest.fixture - def resource_uri(self): - raise NotImplementedError - - @pytest.mark.parametrize("jp_server_config", configs) - async def test_write_read(self, jp_fetch, resource_uri, jp_server_config): - allow_hidden = jp_server_config["ContentsManager"]["allow_hidden"] - - cc = ContentsClient(jp_fetch) - - resources = await cc.set_resources([{"url": resource_uri}]) - drive = resources[0]["drive"] - - fpaths = [ - f"{drive}:{test_fname}", - f"{drive}:root0/{test_fname}", - f"{drive}:root1/leaf1/{test_fname}", - ] - - hidden_paths = [ - f"{drive}:root1/leaf1/.hidden.txt", - f"{drive}:root1/.leaf1/also_hidden.txt", - ] - - # set up dir structure - await cc.mkdir(f"{drive}:root0") - await cc.mkdir(f"{drive}:root1") - await cc.mkdir(f"{drive}:root1/leaf1") - if allow_hidden: - await cc.mkdir(f"{drive}:root1/.leaf1") - - for p in fpaths: - # save to root and tips - await cc.save(p, _test_file_model) - # read and check - assert test_content == (await cc.get(p))["content"] - - for p in hidden_paths: - ctx = ( - nullcontext() - if allow_hidden - else pytest.raises(tornado.httpclient.HTTPClientError) - ) - with ctx as c: - # save to root and tips - await cc.save(p, _test_file_model) - # read and check - assert test_content == (await cc.get(p))["content"] - - if not allow_hidden: - assert c.value.code == 400 - - -class Test_FSManager_osfs(_TestBase): - """No extra setup required for this test suite""" - - _test_dir = str(Path(test_root_osfs) / Path(test_dir)) - - @classmethod - def setup_class(cls): - shutil.rmtree(test_root_osfs, ignore_errors=True) - os.makedirs(test_root_osfs) - - def setup_method(self, method): - os.makedirs(self._test_dir) - - def teardown_method(self, method): - shutil.rmtree(self._test_dir, ignore_errors=True) - - @pytest.fixture - def resource_uri(self, tmp_path): - yield f"osfs://{tmp_path}" - - -class Test_FSManager_s3(_TestBase): - """Tests on an instance of s3proxy running in a docker - Manual startup of equivalent docker: - - docker run --rm -p 9000:80 --env S3PROXY_AUTHORIZATION=none andrewgaul/s3proxy - """ - - _rootDirUtil = s3.RootDirUtil(dir_name=test_dir, port=test_port_s3, url=test_url_s3) - - @classmethod - def setup_class(cls): - # delete any existing root - cls._rootDirUtil.delete() - - @classmethod - def teardown_class(cls): - ... - - def setup_method(self, method): - self._rootDirUtil.create() - - def teardown_method(self, method): - self._rootDirUtil.delete() - - @pytest.fixture - def resource_uri(self): - uri = "s3://{id}:{key}@{bucket}?endpoint_url={url}:{port}".format( - id=s3.aws_access_key_id, - key=s3.aws_secret_access_key, - bucket=test_dir, - url=test_url_s3.strip("/"), - port=test_port_s3, - ) - yield uri - - -@pytest.mark.darwin -@pytest.mark.linux -class Test_FSManager_smb_docker_share(_TestBase): - """(mac/linux only. future: windows) runs its own samba server via - py-docker. Automatically creates and exposes a share from a docker - container. - - Manual startup of equivalent docker: - - docker run --rm -it -p 137:137/udp -p 138:138/udp -p 139:139 -p 445:445 dperson/samba -p -n -u "smbuser;smbuser" -w "TESTNET" - - Docker with a windows guest: - - docker run --rm -it -p 137:137/udp -p 138:138/udp -p 139:139 -p 445:445 mcr.microsoft.com/windows/nanoserver:1809 - """ - - _rootDirUtil = samba.RootDirUtil( - dir_name=test_dir, - host=test_host_smb_docker_share, - hostname=test_hostname_smb_docker_share, - name_port=test_name_port_smb_docker_nameport, - smb_port=test_name_port_smb_docker_share, - ) - - # direct_tcp=False, - # host=None, - # hostname=None, - # my_name="local", - # name_port=137, - # smb_port=None, - @classmethod - def setup_class(cls): - # delete any existing root - cls._rootDirUtil.delete() +# # ***************************************************************************** +# # +# # Copyright (c) 2019, the jupyter-fs authors. +# # +# # This file is part of the jupyter-fs library, distributed under the terms of +# # the Apache License 2.0. The full license can be found in the LICENSE file. + +# from contextlib import nullcontext +# from pathlib import Path +# import pytest +# import os +# import shutil +# import socket + +# import tornado.web + +# from .utils import s3, samba +# from .utils.client import ContentsClient + +# test_dir = "test" +# test_content = "foo\nbar\nbaz" +# test_fname = "foo.txt" + +# test_root_osfs = "osfs_local" + +# test_url_s3 = "http://127.0.0.1/" +# test_port_s3 = "9000" + +# test_host_smb_docker_share = socket.gethostbyname(socket.gethostname()) +# test_hostname_smb_docker_share = "test" +# test_name_port_smb_docker_nameport = 4137 +# test_name_port_smb_docker_share = 4139 + +# test_direct_tcp_smb_os_share = False +# test_host_smb_os_share = socket.gethostbyname_ex(socket.gethostname())[2][-1] +# test_smb_port_smb_os_share = 139 + +# _test_file_model = { +# "content": test_content, +# "format": "text", +# "mimetype": "text/plain", +# "name": test_fname, +# "path": test_fname, +# "type": "file", +# "writable": True, +# } + +# configs = [ +# { +# "ServerApp": { +# "jpserver_extensions": {"jupyterfs.extension": True}, +# "contents_manager_class": "jupyterfs.metamanager.MetaManager", +# }, +# "ContentsManager": {"allow_hidden": True}, +# }, +# { +# "ServerApp": { +# "jpserver_extensions": {"jupyterfs.extension": True}, +# "contents_manager_class": "jupyterfs.metamanager.MetaManager", +# }, +# "ContentsManager": {"allow_hidden": False}, +# }, +# ] + + +# class _TestBase: +# """Contains tests universal to all PyFilesystemContentsManager flavors""" + +# @pytest.fixture +# def resource_uri(self): +# raise NotImplementedError + +# @pytest.mark.parametrize("jp_server_config", configs) +# async def test_write_read(self, jp_fetch, resource_uri, jp_server_config): +# allow_hidden = jp_server_config["ContentsManager"]["allow_hidden"] + +# cc = ContentsClient(jp_fetch) + +# resources = await cc.set_resources([{"url": resource_uri}]) +# drive = resources[0]["drive"] + +# fpaths = [ +# f"{drive}:{test_fname}", +# f"{drive}:root0/{test_fname}", +# f"{drive}:root1/leaf1/{test_fname}", +# ] + +# hidden_paths = [ +# f"{drive}:root1/leaf1/.hidden.txt", +# f"{drive}:root1/.leaf1/also_hidden.txt", +# ] + +# # set up dir structure +# await cc.mkdir(f"{drive}:root0") +# await cc.mkdir(f"{drive}:root1") +# await cc.mkdir(f"{drive}:root1/leaf1") +# if allow_hidden: +# await cc.mkdir(f"{drive}:root1/.leaf1") + +# for p in fpaths: +# # save to root and tips +# await cc.save(p, _test_file_model) +# # read and check +# assert test_content == (await cc.get(p))["content"] + +# for p in hidden_paths: +# ctx = ( +# nullcontext() +# if allow_hidden +# else pytest.raises(tornado.httpclient.HTTPClientError) +# ) +# with ctx as c: +# # save to root and tips +# await cc.save(p, _test_file_model) +# # read and check +# assert test_content == (await cc.get(p))["content"] + +# if not allow_hidden: +# assert c.value.code == 400 + + +# class Test_FSManager_osfs(_TestBase): +# """No extra setup required for this test suite""" + +# _test_dir = str(Path(test_root_osfs) / Path(test_dir)) + +# @classmethod +# def setup_class(cls): +# shutil.rmtree(test_root_osfs, ignore_errors=True) +# os.makedirs(test_root_osfs) + +# def setup_method(self, method): +# os.makedirs(self._test_dir) + +# def teardown_method(self, method): +# shutil.rmtree(self._test_dir, ignore_errors=True) + +# @pytest.fixture +# def resource_uri(self, tmp_path): +# yield f"osfs://{tmp_path}" + + +# class Test_FSManager_s3(_TestBase): +# """Tests on an instance of s3proxy running in a docker +# Manual startup of equivalent docker: + +# docker run --rm -p 9000:80 --env S3PROXY_AUTHORIZATION=none andrewgaul/s3proxy +# """ + +# _rootDirUtil = s3.RootDirUtil(dir_name=test_dir, port=test_port_s3, url=test_url_s3) + +# @classmethod +# def setup_class(cls): +# # delete any existing root +# cls._rootDirUtil.delete() - @classmethod - def teardown_class(cls): - ... +# @classmethod +# def teardown_class(cls): +# ... - def setup_method(self, method): - # create a root - self._rootDirUtil.create() +# def setup_method(self, method): +# self._rootDirUtil.create() - def teardown_method(self, method): - # delete any existing root - self._rootDirUtil.delete() - - @pytest.fixture - def resource_uri(self): - uri = "smb://{username}:{passwd}@{host}:{smb_port}/{share}?name-port={name_port}".format( - username=samba.smb_user, - passwd=samba.smb_passwd, - host=test_host_smb_docker_share, - share=test_hostname_smb_docker_share, - smb_port=test_name_port_smb_docker_share, - name_port=test_name_port_smb_docker_nameport, - ) - yield uri +# def teardown_method(self, method): +# self._rootDirUtil.delete() + +# @pytest.fixture +# def resource_uri(self): +# uri = "s3://{id}:{key}@{bucket}?endpoint_url={url}:{port}".format( +# id=s3.aws_access_key_id, +# key=s3.aws_secret_access_key, +# bucket=test_dir, +# url=test_url_s3.strip("/"), +# port=test_port_s3, +# ) +# yield uri # @pytest.mark.darwin -# @pytest.mark.win32 -# class Test_FSManager_smb_os_share(_TestBase): -# """(windows only. future: also mac) Uses the os's buitlin samba server. -# Expects a local user "smbuser" with access to a share named "test" +# @pytest.mark.linux +# class Test_FSManager_smb_docker_share(_TestBase): +# """(mac/linux only. future: windows) runs its own samba server via +# py-docker. Automatically creates and exposes a share from a docker +# container. + +# Manual startup of equivalent docker: + +# docker run --rm -it -p 137:137/udp -p 138:138/udp -p 139:139 -p 445:445 dperson/samba -p -n -u "smbuser;smbuser" -w "TESTNET" + +# Docker with a windows guest: + +# docker run --rm -it -p 137:137/udp -p 138:138/udp -p 139:139 -p 445:445 mcr.microsoft.com/windows/nanoserver:1809 # """ # _rootDirUtil = samba.RootDirUtil( # dir_name=test_dir, -# host=test_host_smb_os_share, -# smb_port=test_smb_port_smb_os_share, +# host=test_host_smb_docker_share, +# hostname=test_hostname_smb_docker_share, +# name_port=test_name_port_smb_docker_nameport, +# smb_port=test_name_port_smb_docker_share, # ) +# # direct_tcp=False, +# # host=None, +# # hostname=None, +# # my_name="local", +# # name_port=137, +# # smb_port=None, # @classmethod # def setup_class(cls): # # delete any existing root # cls._rootDirUtil.delete() +# @classmethod +# def teardown_class(cls): +# ... + # def setup_method(self, method): # # create a root # self._rootDirUtil.create() @@ -262,24 +223,63 @@ def resource_uri(self): # # delete any existing root # self._rootDirUtil.delete() -# @pytest.fixture -# def resource_uri(self): -# kwargs = dict( -# direct_tcp=test_direct_tcp_smb_os_share, -# host=test_host_smb_os_share, -# hostname=socket.gethostname(), -# passwd=samba.smb_passwd, -# share=test_dir, +# @pytest.fixture +# def resource_uri(self): +# uri = "smb://{username}:{passwd}@{host}:{smb_port}/{share}?name-port={name_port}".format( # username=samba.smb_user, +# passwd=samba.smb_passwd, +# host=test_host_smb_docker_share, +# share=test_hostname_smb_docker_share, +# smb_port=test_name_port_smb_docker_share, +# name_port=test_name_port_smb_docker_nameport, # ) - -# if test_smb_port_smb_os_share is not None: -# uri = "smb://{username}:{passwd}@{host}:{port}/{share}?hostname={hostname}&direct-tcp={direct_tcp}".format( -# port=test_smb_port_smb_os_share, **kwargs -# ) -# else: -# uri = "smb://{username}:{passwd}@{host}/{share}?hostname={hostname}&direct-tcp={direct_tcp}".format( -# **kwargs -# ) - -# yield uri +# yield uri + + +# # @pytest.mark.darwin +# # @pytest.mark.win32 +# # class Test_FSManager_smb_os_share(_TestBase): +# # """(windows only. future: also mac) Uses the os's buitlin samba server. +# # Expects a local user "smbuser" with access to a share named "test" +# # """ + +# # _rootDirUtil = samba.RootDirUtil( +# # dir_name=test_dir, +# # host=test_host_smb_os_share, +# # smb_port=test_smb_port_smb_os_share, +# # ) + +# # @classmethod +# # def setup_class(cls): +# # # delete any existing root +# # cls._rootDirUtil.delete() + +# # def setup_method(self, method): +# # # create a root +# # self._rootDirUtil.create() + +# # def teardown_method(self, method): +# # # delete any existing root +# # self._rootDirUtil.delete() + +# # @pytest.fixture +# # def resource_uri(self): +# # kwargs = dict( +# # direct_tcp=test_direct_tcp_smb_os_share, +# # host=test_host_smb_os_share, +# # hostname=socket.gethostname(), +# # passwd=samba.smb_passwd, +# # share=test_dir, +# # username=samba.smb_user, +# # ) + +# # if test_smb_port_smb_os_share is not None: +# # uri = "smb://{username}:{passwd}@{host}:{port}/{share}?hostname={hostname}&direct-tcp={direct_tcp}".format( +# # port=test_smb_port_smb_os_share, **kwargs +# # ) +# # else: +# # uri = "smb://{username}:{passwd}@{host}/{share}?hostname={hostname}&direct-tcp={direct_tcp}".format( +# # **kwargs +# # ) + +# # yield uri diff --git a/jupyterfs/tests/test_syncmetamanager.py b/jupyterfs/tests/test_syncmetamanager.py new file mode 100644 index 0000000..7ce980e --- /dev/null +++ b/jupyterfs/tests/test_syncmetamanager.py @@ -0,0 +1,151 @@ +# ***************************************************************************** +# +# Copyright (c) 2019, the jupyter-fs authors. +# +# This file is part of the jupyter-fs library, distributed under the terms of +# the Apache License 2.0. The full license can be found in the LICENSE file. + +import pytest +from traitlets.config import Config + +from .utils.client import ContentsClient + + +# base config +base_config = { + "ServerApp": { + "jpserver_extensions": {"jupyterfs.extension": True}, + "contents_manager_class": "jupyterfs.metamanager.SyncMetaManager", + }, + "JupyterFs": {}, +} + +deny_client_config = { + "JupyterFs": { + "allow_user_resources": False, + } +} + + +@pytest.fixture +def tmp_osfs_resource(): + """parametrize if we want tmp resource""" + return False + + +@pytest.fixture +def our_config(): + """parametrize if we want custom config""" + return {} + + +@pytest.fixture +def jp_server_config(tmp_path, tmp_osfs_resource, our_config): + c = Config(base_config) + c.JupyterFs.setdefault("resources", []) + if tmp_osfs_resource: + c.JupyterFs.resources.append( + { + "name": "test-server-config", + "url": f"osfs://{tmp_path.as_posix()}", + } + ) + c.merge(Config(our_config)) + return c + + +@pytest.mark.parametrize("our_config", [deny_client_config]) +async def test_client_creation_disallowed(tmp_path, jp_fetch, jp_server_config): + cc = ContentsClient(jp_fetch) + resources = await cc.set_resources( + [{"name": "test-2", "url": f"osfs://{tmp_path.as_posix()}"}] + ) + assert resources == [] + + +@pytest.mark.parametrize("our_config", [deny_client_config]) +@pytest.mark.parametrize("tmp_osfs_resource", [True]) +async def test_client_creation_disallowed_retains_server_config( + tmp_path, jp_fetch, jp_server_config +): + cc = ContentsClient(jp_fetch) + resources = await cc.set_resources( + [{"name": "test-2", "url": f"osfs://{tmp_path.as_posix()}"}] + ) + names = set(map(lambda r: r["name"], resources)) + assert names == {"test-server-config"} + + +@pytest.mark.parametrize( + "our_config", + [ + { + "JupyterFs": { + "resource_validators": [ + r"osfs://.*/test-valid-A.*", + r".*://.*/test-valid-B", + ] + } + } + ], +) +async def test_resource_validators(tmp_path, jp_fetch, jp_server_config): + cc = ContentsClient(jp_fetch) + (tmp_path / "test-valid-A").mkdir() + (tmp_path / "test-valid-B").mkdir() + (tmp_path / "test-invalid-A").mkdir() + (tmp_path / "test-invalid-B").mkdir() + (tmp_path / "invalid-C").mkdir() + resources = await cc.set_resources( + [ + {"name": "valid-1", "url": f"osfs://{tmp_path.as_posix()}/test-valid-A"}, + {"name": "valid-2", "url": f"osfs://{tmp_path.as_posix()}/test-valid-B"}, + { + "name": "invalid-1", + "url": f"osfs://{tmp_path.as_posix()}/test-invalid-A", + }, + { + "name": "invalid-2", + "url": f"osfs://{tmp_path.as_posix()}/test-invalid-B", + }, + {"name": "invalid-3", "url": f"osfs://{tmp_path.as_posix()}/invalid-C"}, + {"name": "invalid-4", "url": f"osfs://{tmp_path.as_posix()}/foo"}, + { + "name": "invalid-5", + "url": f"osfs://{tmp_path.as_posix()}/test-valid-A/non-existant", + }, + { + "name": "invalid-6", + "url": f"non-existant://{tmp_path.as_posix()}/test-valid-B", + }, + ] + ) + names = {r["name"] for r in resources if r["init"]} + assert names == {"valid-1", "valid-2"} + + +@pytest.mark.parametrize( + "our_config", + [ + { + "JupyterFs": { + "resource_validators": [ + r"osfs://([^@]*|[^:]*[:][@].*)", # no auth, or at least no password + r"osfs://", # sanity check that this doesn't change the result + ] + } + } + ], +) +async def test_resource_validators_no_auth(tmp_path, jp_fetch, jp_server_config): + cc = ContentsClient(jp_fetch) + resources = await cc.set_resources( + [ + {"name": "valid-1", "url": f"osfs://{tmp_path.as_posix()}"}, + {"name": "valid-2", "url": f"osfs://username:@{tmp_path.as_posix()}"}, + {"name": "invalid-1", "url": f"osfs://username:pwd@{tmp_path.as_posix()}"}, + {"name": "invalid-2", "url": f"osfs://:pwd@{tmp_path.as_posix()}"}, + ] + ) + names = set(map(lambda r: r["name"], resources)) + assert names == {"valid-1", "valid-2"}