diff --git a/jupyter_server_ydoc/app.py b/jupyter_server_ydoc/app.py index ea43084e..f9551eef 100644 --- a/jupyter_server_ydoc/app.py +++ b/jupyter_server_ydoc/app.py @@ -9,7 +9,12 @@ from traitlets import Float, Int, Type from ypy_websocket.ystore import BaseYStore # type: ignore -from .handlers import SQLiteYStore, YDocRoomIdHandler, YDocWebSocketHandler +from .handlers import ( + DocSessionHandler, + SQLiteYStore, + YDocRoomIdHandler, + YDocWebSocketHandler, +) class YDocExtension(ExtensionApp): @@ -62,7 +67,11 @@ def initialize_settings(self): def initialize_handlers(self): self.handlers.extend( [ + # Deprecated - to remove for 1.0.0 (r"/api/yjs/roomid/(.*)", YDocRoomIdHandler), + # Deprecated - to remove for 1.0.0 (r"/api/yjs/(.*)", YDocWebSocketHandler), + (r"/api/collaboration/room/(.*)", YDocWebSocketHandler), + (r"/api/collaboration/session/(.*)", DocSessionHandler), ] ) diff --git a/jupyter_server_ydoc/handlers.py b/jupyter_server_ydoc/handlers.py index 517b983a..f0e2a297 100644 --- a/jupyter_server_ydoc/handlers.py +++ b/jupyter_server_ydoc/handlers.py @@ -3,6 +3,7 @@ import asyncio import json +import uuid from logging import Logger from pathlib import Path from typing import Any, Dict, Optional, Tuple @@ -24,6 +25,8 @@ YFILE = YDOCS["file"] +SERVER_SESSION = str(uuid.uuid4()) + class TempFileYStore(_TempFileYStore): prefix_dir = "jupyter_ystore_" @@ -195,6 +198,11 @@ async def open(self, path): self.saving_document = None asyncio.create_task(self.websocket_server.serve(self)) + # Close the connection if the document session expired + session_id = self.get_query_argument("sessionId", "") + if isinstance(self.room, DocumentRoom) and SERVER_SESSION != session_id: + self.close(1003, f"Document session {session_id} expired") + # cancel the deletion of the room if it was scheduled if isinstance(self.room, DocumentRoom) and self.room.cleaner is not None: self.room.cleaner.cancel() @@ -221,6 +229,7 @@ async def open(self, path): # if YStore updates and source file are out-of-sync, resync updates with source if self.room.document.source != model["content"]: read_from_source = True + if read_from_source: self.room.document.source = model["content"] if self.room.ystore: @@ -405,3 +414,39 @@ async def put(self, path): ws_url += str(idx) self.log.info("Request for Y document '%s' with room ID: %s", path, ws_url) return self.finish(ws_url) + + +class DocSessionHandler(APIHandler): + auth_resource = "contents" + + @web.authenticated + @authorized + async def put(self, path): + body = json.loads(self.request.body) + format = body["format"] + content_type = body["type"] + file_id_manager = self.settings["file_id_manager"] + + idx = file_id_manager.get_id(path) + if idx is not None: + # index already exists + self.log.info("Request for Y document '%s' with room ID: %s", path, idx) + data = json.dumps( + {"format": format, "type": content_type, "fileId": idx, "sessionId": SERVER_SESSION} + ) + self.set_status(200) + return self.finish(data) + + # try indexing + idx = file_id_manager.index(path) + if idx is None: + # file does not exists + raise web.HTTPError(404, f"File {path!r} does not exist") + + # index successfully created + self.log.info("Request for Y document '%s' with room ID: %s", path, idx) + data = json.dumps( + {"format": format, "type": content_type, "fileId": idx, "sessionId": SERVER_SESSION} + ) + self.set_status(201) + return self.finish(data)