From 5d2232b2ef4e04d71fbbbf3a2e30e1c498be1edf Mon Sep 17 00:00:00 2001 From: Vishnutheep B Date: Tue, 20 Aug 2024 21:56:12 +0530 Subject: [PATCH] Support optional hash output in save method --- .../services/contents/filemanager.py | 24 ++++++++++++---- jupyter_server/services/contents/handlers.py | 15 ++++++++-- .../services/contents/largefilemanager.py | 28 +++++++++++++------ jupyter_server/services/contents/manager.py | 14 ++++++++-- 4 files changed, 62 insertions(+), 19 deletions(-) diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index 96029d96d6..6f33329dd7 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -475,8 +475,14 @@ def _save_directory(self, os_path, model, path=""): else: self.log.debug("Directory %r already exists", os_path) - def save(self, model, path=""): - """Save the file model and return the model with no content.""" + def save(self, model, path="", require_hash=False): + """Save the file model and return the model with no content. + + Parameters + ---------- + require_hash: bool, optional + Whether to include the hash of the file contents. + """ path = path.strip("/") self.run_pre_save_hooks(model=model, path=path) @@ -519,7 +525,7 @@ def save(self, model, path=""): self.validate_notebook_model(model, validation_error=validation_error) validation_message = model.get("message", None) - model = self.get(path, content=False) + model = self.get(path, content=False, require_hash=require_hash) if validation_message: model["message"] = validation_message @@ -941,8 +947,14 @@ async def _save_directory(self, os_path, model, path=""): else: self.log.debug("Directory %r already exists", os_path) - async def save(self, model, path=""): - """Save the file model and return the model with no content.""" + async def save(self, model, path="", require_hash=False): + """Save the file model and return the model with no content. + + Parameters + ---------- + require_hash: bool, optional + Whether to include the hash of the file contents. + """ path = path.strip("/") self.run_pre_save_hooks(model=model, path=path) @@ -982,7 +994,7 @@ async def save(self, model, path=""): self.validate_notebook_model(model, validation_error=validation_error) validation_message = model.get("message", None) - model = await self.get(path, content=False) + model = await self.get(path, content=False, require_hash=require_hash) if validation_message: model["message"] = validation_message diff --git a/jupyter_server/services/contents/handlers.py b/jupyter_server/services/contents/handlers.py index 13e987809b..da53553e0c 100644 --- a/jupyter_server/services/contents/handlers.py +++ b/jupyter_server/services/contents/handlers.py @@ -238,12 +238,14 @@ async def _new_untitled(self, path, type="", ext=""): validate_model(model) self._finish_model(model) - async def _save(self, model, path): + async def _save(self, model, path, require_hash): """Save an existing file.""" chunk = model.get("chunk", None) if not chunk or chunk == -1: # Avoid tedious log information self.log.info("Saving file at %s", path) - model = await ensure_async(self.contents_manager.save(model, path)) + model = await ensure_async( + self.contents_manager.save(model, path, require_hash=require_hash) + ) validate_model(model) self._finish_model(model) @@ -304,6 +306,13 @@ async def put(self, path=""): model = self.get_json_body() cm = self.contents_manager + hash_str = self.get_query_argument("hash", default="0") + if hash_str not in {"0", "1"}: + raise web.HTTPError( + 400, f"Hash argument {hash_str!r} is invalid. It must be '0' or '1'." + ) + require_hash = int(hash_str) + if model: if model.get("copy_from"): raise web.HTTPError(400, "Cannot copy with PUT, only POST") @@ -318,7 +327,7 @@ async def put(self, path=""): # fall back to file if unknown type model["type"] = "file" if exists: - await self._save(model, path) + await self._save(model, path, require_hash=require_hash) else: await self._upload(model, path) else: diff --git a/jupyter_server/services/contents/largefilemanager.py b/jupyter_server/services/contents/largefilemanager.py index 78f0d55629..ddb7554d7c 100644 --- a/jupyter_server/services/contents/largefilemanager.py +++ b/jupyter_server/services/contents/largefilemanager.py @@ -13,8 +13,14 @@ class LargeFileManager(FileContentsManager): """Handle large file upload.""" - def save(self, model, path=""): - """Save the file model and return the model with no content.""" + def save(self, model, path="", require_hash=False): + """Save the file model and return the model with no content. + + Parameters + ---------- + require_hash: bool, optional + Whether to include the hash of the file contents. + """ chunk = model.get("chunk", None) if chunk is not None: path = path.strip("/") @@ -49,7 +55,7 @@ def save(self, model, path=""): self.log.error("Error while saving file: %s %s", path, e, exc_info=True) raise web.HTTPError(500, f"Unexpected error while saving file: {path} {e}") from e - model = self.get(path, content=False) + model = self.get(path, content=False, require_hash=require_hash) # Last chunk if chunk == -1: @@ -57,7 +63,7 @@ def save(self, model, path=""): self.emit(data={"action": "save", "path": path}) return model else: - return super().save(model, path) + return super().save(model, path, require_hash=require_hash) def _save_large_file(self, os_path, content, format): """Save content of a generic file.""" @@ -85,8 +91,14 @@ def _save_large_file(self, os_path, content, format): class AsyncLargeFileManager(AsyncFileContentsManager): """Handle large file upload asynchronously""" - async def save(self, model, path=""): - """Save the file model and return the model with no content.""" + async def save(self, model, path="", require_hash=False): + """Save the file model and return the model with no content. + + Parameters + ---------- + require_hash: bool, optional + Whether to include the hash of the file contents. + """ chunk = model.get("chunk", None) if chunk is not None: path = path.strip("/") @@ -121,7 +133,7 @@ async def save(self, model, path=""): self.log.error("Error while saving file: %s %s", path, e, exc_info=True) raise web.HTTPError(500, f"Unexpected error while saving file: {path} {e}") from e - model = await self.get(path, content=False) + model = await self.get(path, content=False, require_hash=require_hash) # Last chunk if chunk == -1: @@ -130,7 +142,7 @@ async def save(self, model, path=""): self.emit(data={"action": "save", "path": path}) return model else: - return await super().save(model, path) + return await super().save(model, path, require_hash=require_hash) async def _save_large_file(self, os_path, content, format): """Save content of a generic file.""" diff --git a/jupyter_server/services/contents/manager.py b/jupyter_server/services/contents/manager.py index 71e998992a..29f25f4e66 100644 --- a/jupyter_server/services/contents/manager.py +++ b/jupyter_server/services/contents/manager.py @@ -460,10 +460,15 @@ def get(self, path, content=True, type=None, format=None, require_hash=False): """ raise NotImplementedError - def save(self, model, path): + def save(self, model, path, require_hash=False): """ Save a file or directory model to path. + Parameters + ---------- + require_hash : bool + Whether the file hash must be returned or not. + Should return the saved model with no content. Save implementations should call self.run_pre_save_hook(model=model, path=path) prior to writing any data. @@ -870,10 +875,15 @@ async def get(self, path, content=True, type=None, format=None, require_hash=Fal """ raise NotImplementedError - async def save(self, model, path): + async def save(self, model, path, require_hash=False): """ Save a file or directory model to path. + Parameters + ---------- + require_hash : bool + Whether the file hash must be returned or not. + Should return the saved model with no content. Save implementations should call self.run_pre_save_hook(model=model, path=path) prior to writing any data.