Skip to content

Commit b5a6306

Browse files
vidartfblink1073
andauthored
Optimize hidden checks (#1226)
Co-authored-by: Steven Silvester <[email protected]>
1 parent e60b048 commit b5a6306

File tree

8 files changed

+171
-33
lines changed

8 files changed

+171
-33
lines changed

jupyter_server/base/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,7 @@ def validate_absolute_path(self, root, absolute_path):
893893
abs_path = super().validate_absolute_path(root, absolute_path)
894894
abs_root = os.path.abspath(root)
895895
assert abs_path is not None
896-
if is_hidden(abs_path, abs_root) and not self.contents_manager.allow_hidden:
896+
if not self.contents_manager.allow_hidden and is_hidden(abs_path, abs_root):
897897
self.log.info(
898898
"Refusing to serve hidden file, via 404 Error, use flag 'ContentsManager.allow_hidden' to enable"
899899
)

jupyter_server/files/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def get(self, path, include_body=True):
4848
self.check_xsrf_cookie()
4949
cm = self.contents_manager
5050

51-
if await ensure_async(cm.is_hidden(path)) and not cm.allow_hidden:
51+
if not cm.allow_hidden and await ensure_async(cm.is_hidden(path)):
5252
self.log.info("Refusing to serve hidden file, via 404 Error")
5353
raise web.HTTPError(404)
5454

jupyter_server/services/contents/filemanager.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def _base_model(self, path):
226226

227227
four_o_four = "file or directory does not exist: %r" % path
228228

229-
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
229+
if not self.allow_hidden and is_hidden(os_path, self.root_dir):
230230
self.log.info("Refusing to serve hidden file or directory %r, via 404 Error", os_path)
231231
raise web.HTTPError(404, four_o_four)
232232

@@ -278,7 +278,7 @@ def _dir_model(self, path, content=True):
278278

279279
if not os.path.isdir(os_path):
280280
raise web.HTTPError(404, four_o_four)
281-
elif is_hidden(os_path, self.root_dir) and not self.allow_hidden:
281+
elif not self.allow_hidden and is_hidden(os_path, self.root_dir):
282282
self.log.info("Refusing to serve hidden directory %r, via 404 Error", os_path)
283283
raise web.HTTPError(404, four_o_four)
284284

@@ -414,7 +414,7 @@ def get(self, path, content=True, type=None, format=None):
414414
if not self.exists(path):
415415
raise web.HTTPError(404, four_o_four)
416416

417-
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
417+
if not self.allow_hidden and is_hidden(os_path, self.root_dir):
418418
self.log.info("Refusing to serve hidden file or directory %r, via 404 Error", os_path)
419419
raise web.HTTPError(404, four_o_four)
420420

@@ -437,7 +437,7 @@ def get(self, path, content=True, type=None, format=None):
437437

438438
def _save_directory(self, os_path, model, path=""):
439439
"""create a directory"""
440-
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
440+
if not self.allow_hidden and is_hidden(os_path, self.root_dir):
441441
raise web.HTTPError(400, "Cannot create directory %r" % os_path)
442442
if not os.path.exists(os_path):
443443
with self.perm_to_403():
@@ -460,7 +460,7 @@ def save(self, model, path=""):
460460

461461
os_path = self._get_os_path(path)
462462

463-
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
463+
if not self.allow_hidden and is_hidden(os_path, self.root_dir):
464464
raise web.HTTPError(400, f"Cannot create file or directory {os_path!r}")
465465

466466
self.log.debug("Saving %s", os_path)
@@ -506,7 +506,7 @@ def delete_file(self, path):
506506
os_path = self._get_os_path(path)
507507
rm = os.unlink
508508

509-
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
509+
if not self.allow_hidden and is_hidden(os_path, self.root_dir):
510510
raise web.HTTPError(400, f"Cannot delete file or directory {os_path!r}")
511511

512512
four_o_four = "file or directory does not exist: %r" % path
@@ -576,9 +576,9 @@ def rename_file(self, old_path, new_path):
576576
new_os_path = self._get_os_path(new_path)
577577
old_os_path = self._get_os_path(old_path)
578578

579-
if (
579+
if not self.allow_hidden and (
580580
is_hidden(old_os_path, self.root_dir) or is_hidden(new_os_path, self.root_dir)
581-
) and not self.allow_hidden:
581+
):
582582
raise web.HTTPError(400, f"Cannot rename file or directory {old_os_path!r}")
583583

584584
# Should we proceed with the move?
@@ -741,7 +741,7 @@ async def _dir_model(self, path, content=True):
741741

742742
if not os.path.isdir(os_path):
743743
raise web.HTTPError(404, four_o_four)
744-
elif is_hidden(os_path, self.root_dir) and not self.allow_hidden:
744+
elif not self.allow_hidden and is_hidden(os_path, self.root_dir):
745745
self.log.info("Refusing to serve hidden directory %r, via 404 Error", os_path)
746746
raise web.HTTPError(404, four_o_four)
747747

@@ -896,7 +896,7 @@ async def get(self, path, content=True, type=None, format=None):
896896

897897
async def _save_directory(self, os_path, model, path=""):
898898
"""create a directory"""
899-
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
899+
if not self.allow_hidden and is_hidden(os_path, self.root_dir):
900900
raise web.HTTPError(400, "Cannot create hidden directory %r" % os_path)
901901
if not os.path.exists(os_path):
902902
with self.perm_to_403():
@@ -961,7 +961,7 @@ async def delete_file(self, path):
961961
os_path = self._get_os_path(path)
962962
rm = os.unlink
963963

964-
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
964+
if not self.allow_hidden and is_hidden(os_path, self.root_dir):
965965
raise web.HTTPError(400, f"Cannot delete file or directory {os_path!r}")
966966

967967
if not os.path.exists(os_path):
@@ -1035,9 +1035,9 @@ async def rename_file(self, old_path, new_path):
10351035
new_os_path = self._get_os_path(new_path)
10361036
old_os_path = self._get_os_path(old_path)
10371037

1038-
if (
1038+
if not self.allow_hidden and (
10391039
is_hidden(old_os_path, self.root_dir) or is_hidden(new_os_path, self.root_dir)
1040-
) and not self.allow_hidden:
1040+
):
10411041
raise web.HTTPError(400, f"Cannot rename file or directory {old_os_path!r}")
10421042

10431043
# Should we proceed with the move?

jupyter_server/services/contents/handlers.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ async def get(self, path=""):
115115
raise web.HTTPError(400, "Content %r is invalid" % content_str)
116116
content = int(content_str or "")
117117

118-
if await ensure_async(cm.is_hidden(path)) and not cm.allow_hidden:
118+
if not cm.allow_hidden and await ensure_async(cm.is_hidden(path)):
119119
raise web.HTTPError(404, f"file or directory {path!r} does not exist")
120120

121121
model = await ensure_async(
@@ -141,10 +141,10 @@ async def patch(self, path=""):
141141
old_path = model.get("path")
142142
if (
143143
old_path
144+
and not cm.allow_hidden
144145
and (
145146
await ensure_async(cm.is_hidden(path)) or await ensure_async(cm.is_hidden(old_path))
146147
)
147-
and not cm.allow_hidden
148148
):
149149
raise web.HTTPError(400, f"Cannot rename file or directory {path!r}")
150150

@@ -252,10 +252,10 @@ async def put(self, path=""):
252252
if model:
253253
if model.get("copy_from"):
254254
raise web.HTTPError(400, "Cannot copy with PUT, only POST")
255-
if (
255+
if not cm.allow_hidden and (
256256
(model.get("path") and await ensure_async(cm.is_hidden(model.get("path"))))
257257
or await ensure_async(cm.is_hidden(path))
258-
) and not cm.allow_hidden:
258+
):
259259
raise web.HTTPError(400, f"Cannot create file or directory {path!r}")
260260

261261
exists = await ensure_async(self.contents_manager.file_exists(path))
@@ -275,7 +275,7 @@ async def delete(self, path=""):
275275
"""delete a file in the given path"""
276276
cm = self.contents_manager
277277

278-
if await ensure_async(cm.is_hidden(path)) and not cm.allow_hidden:
278+
if not cm.allow_hidden and await ensure_async(cm.is_hidden(path)):
279279
raise web.HTTPError(400, f"Cannot delete file or directory {path!r}")
280280

281281
self.log.warning("delete %s", path)

tests/base/test_handlers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ async def test_authenticated_file_handler(jp_serverapp, tmpdir):
8282

8383
handler = AuthenticatedFileHandler(app.web_app, request, path=str(tmpdir))
8484
for key in list(handler.settings):
85-
del handler.settings[key]
85+
if key != "contents_manager":
86+
del handler.settings[key]
8687
handler.check_xsrf_cookie = MagicMock() # type:ignore
8788
handler._jupyter_current_user = "foo" # type:ignore
8889
with warnings.catch_warnings():

tests/services/contents/test_api.py

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import warnings
55
from base64 import decodebytes, encodebytes
66
from unicodedata import normalize
7+
from unittest.mock import patch
78

89
import pytest
910
import tornado
@@ -185,7 +186,6 @@ async def test_get_text_file_contents(jp_fetch, contents, path, name):
185186
assert expected_http_error(e, 400)
186187

187188

188-
@pytest.mark.skipif(sys.platform == "win32", reason="Disabled retrieving hidden files on Windows")
189189
async def test_get_404_hidden(jp_fetch, contents, contents_dir):
190190
# Create text files
191191
hidden_dir = contents_dir / ".hidden"
@@ -398,7 +398,6 @@ async def test_upload_txt(jp_fetch, contents, contents_dir, _check_created):
398398
assert model["content"] == body
399399

400400

401-
@pytest.mark.skipif(sys.platform == "win32", reason="Disabled uploading hidden files on Windows")
402401
async def test_upload_txt_hidden(jp_fetch, contents, contents_dir):
403402
with pytest.raises(tornado.httpclient.HTTPClientError) as e:
404403
body = "ünicode téxt"
@@ -551,7 +550,6 @@ async def test_copy_put_400(jp_fetch, contents, contents_dir, _check_created):
551550
assert expected_http_error(e, 400)
552551

553552

554-
@pytest.mark.skipif(sys.platform == "win32", reason="Disabled copying hidden files on Windows")
555553
async def test_copy_put_400_hidden(
556554
jp_fetch,
557555
contents,
@@ -598,7 +596,6 @@ async def test_copy_put_400_hidden(
598596
assert expected_http_error(e, 400)
599597

600598

601-
@pytest.mark.skipif(sys.platform == "win32", reason="Disabled copying hidden files on Windows")
602599
async def test_copy_400_hidden(
603600
jp_fetch,
604601
contents,
@@ -686,7 +683,7 @@ async def test_delete_dirs(jp_fetch, contents, folders):
686683
assert model["content"] == []
687684

688685

689-
@pytest.mark.skipif(sys.platform == "win32", reason="Disabled deleting non-empty dirs on Windows")
686+
@pytest.mark.xfail(sys.platform == "win32", reason="Deleting non-empty dirs on Windows")
690687
async def test_delete_non_empty_dir(jp_fetch, contents):
691688
# Delete a folder
692689
await jp_fetch("api", "contents", "å b", method="DELETE")
@@ -696,14 +693,12 @@ async def test_delete_non_empty_dir(jp_fetch, contents):
696693
assert expected_http_error(e, 404)
697694

698695

699-
@pytest.mark.skipif(sys.platform == "win32", reason="Disabled deleting hidden dirs on Windows")
700696
async def test_delete_hidden_dir(jp_fetch, contents):
701697
with pytest.raises(tornado.httpclient.HTTPClientError) as e:
702698
await jp_fetch("api", "contents", ".hidden", method="DELETE")
703699
assert expected_http_error(e, 400)
704700

705701

706-
@pytest.mark.skipif(sys.platform == "win32", reason="Disabled deleting hidden dirs on Windows")
707702
async def test_delete_hidden_file(jp_fetch, contents):
708703
# Test deleting file in a hidden directory
709704
with pytest.raises(tornado.httpclient.HTTPClientError) as e:
@@ -747,7 +742,6 @@ async def test_rename(jp_fetch, jp_base_url, contents, contents_dir):
747742
assert "a.ipynb" not in nbnames
748743

749744

750-
@pytest.mark.skipif(sys.platform == "win32", reason="Disabled copying hidden files on Windows")
751745
async def test_rename_400_hidden(jp_fetch, jp_base_url, contents, contents_dir):
752746
with pytest.raises(tornado.httpclient.HTTPClientError) as e:
753747
old_path = ".hidden/old.txt"
@@ -1018,3 +1012,73 @@ async def test_trust(jp_fetch, contents):
10181012
allow_nonstandard_methods=True,
10191013
)
10201014
assert r.code == 201
1015+
1016+
1017+
@patch(
1018+
"jupyter_core.paths.is_hidden",
1019+
side_effect=AssertionError("Should not call is_hidden if not important"),
1020+
)
1021+
@patch(
1022+
"jupyter_server.services.contents.filemanager.is_hidden",
1023+
side_effect=AssertionError("Should not call is_hidden if not important"),
1024+
)
1025+
async def test_regression_is_hidden(m1, m2, jp_fetch, jp_serverapp, contents, _check_created):
1026+
# check that no is_hidden check runs if configured to allow hidden files
1027+
contents_dir = contents["contents_dir"]
1028+
1029+
hidden_dir = contents_dir / ".hidden"
1030+
hidden_dir.mkdir(parents=True, exist_ok=True)
1031+
txt = "visible text file in hidden dir"
1032+
txtname = hidden_dir.joinpath("visible.txt")
1033+
txtname.write_text(txt, encoding="utf-8")
1034+
1035+
# Our role here is to check that the side-effect never triggers
1036+
jp_serverapp.contents_manager.allow_hidden = True
1037+
r = await jp_fetch(
1038+
"api",
1039+
"contents",
1040+
".hidden",
1041+
)
1042+
assert r.code == 200
1043+
1044+
r = await jp_fetch(
1045+
"api",
1046+
"contents",
1047+
".hidden",
1048+
method="POST",
1049+
body=json.dumps(
1050+
{
1051+
"copy_from": ".hidden/visible.txt",
1052+
}
1053+
),
1054+
)
1055+
_check_created(r, str(contents_dir), ".hidden", "visible-Copy1.txt", type="file")
1056+
1057+
r = await jp_fetch(
1058+
"api",
1059+
"contents",
1060+
".hidden",
1061+
"visible-Copy1.txt",
1062+
method="DELETE",
1063+
)
1064+
assert r.code == 204
1065+
1066+
model = {
1067+
"content": "foo",
1068+
"format": "text",
1069+
"type": "file",
1070+
}
1071+
r = await jp_fetch(
1072+
"api", "contents", ".hidden", "new.txt", method="PUT", body=json.dumps(model)
1073+
)
1074+
_check_created(r, str(contents_dir), ".hidden", "new.txt", type="file")
1075+
1076+
# sanity check that is actually triggers when flag set to false
1077+
jp_serverapp.contents_manager.allow_hidden = False
1078+
with pytest.raises(tornado.httpclient.HTTPClientError) as e:
1079+
await jp_fetch(
1080+
"api",
1081+
"contents",
1082+
".hidden",
1083+
)
1084+
assert expected_http_error(e, 500)

tests/services/contents/test_manager.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,6 @@ async def test_403(jp_file_contents_manager_class, tmp_path):
296296
assert e.status_code == 403
297297

298298

299-
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Can't test hidden files on Windows")
300299
async def test_400(jp_file_contents_manager_class, tmp_path): # noqa
301300
# Test Delete behavior
302301
# Test delete of file in hidden directory
@@ -406,7 +405,6 @@ async def test_400(jp_file_contents_manager_class, tmp_path): # noqa
406405
assert excinfo.value.status_code == 400
407406

408407

409-
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Can't test hidden files on Windows")
410408
async def test_404(jp_file_contents_manager_class, tmp_path):
411409
# Test visible file in hidden folder
412410
with pytest.raises(HTTPError) as excinfo:
@@ -1010,3 +1008,43 @@ async def test_validate_notebook_model(jp_contents_manager):
10101008
cm.validate_notebook_model(model)
10111009
assert mock_validate_nb.call_count == 1
10121010
mock_validate_nb.reset_mock()
1011+
1012+
1013+
@patch(
1014+
"jupyter_core.paths.is_hidden",
1015+
side_effect=AssertionError("Should not call is_hidden if not important"),
1016+
)
1017+
@patch(
1018+
"jupyter_server.services.contents.filemanager.is_hidden",
1019+
side_effect=AssertionError("Should not call is_hidden if not important"),
1020+
)
1021+
async def test_regression_is_hidden(m1, m2, jp_contents_manager):
1022+
cm = jp_contents_manager
1023+
cm.allow_hidden = True
1024+
# Our role here is to check that the side-effect never triggers
1025+
dirname = "foo/.hidden_dir"
1026+
await make_populated_dir(cm, dirname)
1027+
await ensure_async(cm.get(dirname))
1028+
await check_populated_dir_files(cm, dirname)
1029+
await ensure_async(cm.get(path="/".join([dirname, "nb.ipynb"])))
1030+
await ensure_async(cm.get(path="/".join([dirname, "file.txt"])))
1031+
await ensure_async(cm.new(path="/".join([dirname, "nb2.ipynb"])))
1032+
await ensure_async(cm.new(path="/".join([dirname, "file2.txt"])))
1033+
await ensure_async(cm.new(path="/".join([dirname, "subdir"]), model={"type": "directory"}))
1034+
await ensure_async(
1035+
cm.copy(
1036+
from_path="/".join([dirname, "file.txt"]), to_path="/".join([dirname, "file-copy.txt"])
1037+
)
1038+
)
1039+
await ensure_async(
1040+
cm.rename_file(
1041+
old_path="/".join([dirname, "file-copy.txt"]),
1042+
new_path="/".join([dirname, "file-renamed.txt"]),
1043+
)
1044+
)
1045+
await ensure_async(cm.delete_file(path="/".join([dirname, "file-renamed.txt"])))
1046+
1047+
# sanity check that is actually triggers when flag set to false
1048+
cm.allow_hidden = False
1049+
with pytest.raises(AssertionError):
1050+
await ensure_async(cm.get(dirname))

0 commit comments

Comments
 (0)