Skip to content

Commit 3dafbe4

Browse files
Handle binary (base64-encoded) files (#315)
1 parent 79353a0 commit 3dafbe4

File tree

4 files changed

+52
-36
lines changed

4 files changed

+52
-36
lines changed

jupyverse_api/jupyverse_api/contents/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def file_id_manager(self) -> FileIdManager:
113113

114114
@abstractmethod
115115
async def read_content(
116-
self, path: Union[str, Path], get_content: bool, as_json: bool = False
116+
self, path: Union[str, Path], get_content: bool, file_format: Optional[str] = None
117117
) -> Content:
118118
...
119119

plugins/contents/fps_contents/routes.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
import json
23
import os
34
import shutil
@@ -137,7 +138,7 @@ async def rename_content(
137138
return await self.read_content(rename_content.path, False)
138139

139140
async def read_content(
140-
self, path: Union[str, Path], get_content: bool, as_json: bool = False
141+
self, path: Union[str, Path], get_content: bool, file_format: Optional[str] = None
141142
) -> Content:
142143
if isinstance(path, str):
143144
path = Path(path)
@@ -151,10 +152,14 @@ async def read_content(
151152
]
152153
elif path.is_file() or path.is_symlink():
153154
try:
154-
async with await open_file(path) as f:
155-
content = await f.read()
156-
if as_json:
157-
content = json.loads(content)
155+
async with await open_file(path, mode="rb") as f:
156+
content_bytes = await f.read()
157+
if file_format == "base64":
158+
content = base64.b64encode(content_bytes).decode("ascii")
159+
elif file_format == "json":
160+
content = json.loads(content_bytes)
161+
else:
162+
content = content_bytes.decode()
158163
except Exception:
159164
raise HTTPException(status_code=404, detail="Item not found")
160165
format: Optional[str] = None
@@ -171,7 +176,7 @@ async def read_content(
171176
mimetype = None
172177
if content is not None:
173178
nb: dict
174-
if as_json:
179+
if file_format == "json":
175180
content = cast(Dict, content)
176181
nb = content
177182
else:
@@ -181,7 +186,7 @@ async def read_content(
181186
if "metadata" not in cell:
182187
cell["metadata"] = {}
183188
cell["metadata"].update({"trusted": False})
184-
if not as_json:
189+
if file_format != "json":
185190
content = json.dumps(nb)
186191
elif path.suffix == ".json":
187192
type = "json"
@@ -212,24 +217,33 @@ async def read_content(
212217
async def write_content(self, content: Union[SaveContent, Dict]) -> None:
213218
if not isinstance(content, SaveContent):
214219
content = SaveContent(**content)
215-
async with await open_file(content.path, "w") as f:
216-
if content.format == "json":
217-
dict_content = cast(Dict, content.content)
218-
if content.type == "notebook":
219-
# see https://github.com/jupyterlab/jupyterlab/issues/11005
220-
if "metadata" in dict_content and "orig_nbformat" in dict_content["metadata"]:
221-
del dict_content["metadata"]["orig_nbformat"]
222-
await f.write(json.dumps(dict_content, indent=2))
223-
else:
220+
if content.format == "base64":
221+
async with await open_file(content.path, "wb") as f:
224222
content.content = cast(str, content.content)
225-
await f.write(content.content)
223+
content_bytes = content.content.encode("ascii")
224+
await f.write(content_bytes)
225+
else:
226+
async with await open_file(content.path, "wt") as f:
227+
if content.format == "json":
228+
dict_content = cast(Dict, content.content)
229+
if content.type == "notebook":
230+
# see https://github.com/jupyterlab/jupyterlab/issues/11005
231+
if (
232+
"metadata" in dict_content
233+
and "orig_nbformat" in dict_content["metadata"]
234+
):
235+
del dict_content["metadata"]["orig_nbformat"]
236+
await f.write(json.dumps(dict_content, indent=2))
237+
else:
238+
content.content = cast(str, content.content)
239+
await f.write(content.content)
226240

227241
@property
228242
def file_id_manager(self):
229243
return FileIdManager()
230244

231245

232-
def get_available_path(path: Path, sep: str = ""):
246+
def get_available_path(path: Path, sep: str = "") -> Path:
233247
directory = path.parent
234248
name = Path(path.name)
235249
i = None

plugins/webdav/fps_webdav/py.typed

Whitespace-only changes.

plugins/yjs/fps_yjs/routes.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,8 @@ async def serve(self, websocket: YpyWebsocket, permissions) -> None:
175175
logger.info(f"Opening collaboration room: {websocket.path} ({file_path})")
176176
document = YDOCS.get(file_type, YFILE)(room.ydoc)
177177
self.documents[websocket.path] = document
178-
is_notebook = file_type == "notebook"
179178
async with self.lock:
180-
model = await self.contents.read_content(file_path, True, as_json=is_notebook)
179+
model = await self.contents.read_content(file_path, True, file_format)
181180
assert model.last_modified is not None
182181
self.last_modified[file_id] = to_datetime(model.last_modified)
183182
if not room.ready:
@@ -201,11 +200,13 @@ async def serve(self, websocket: YpyWebsocket, permissions) -> None:
201200
document.dirty = False
202201
room.ready = True
203202
# save the document to file when changed
204-
document.observe(partial(self.on_document_change, file_id, file_type, document))
203+
document.observe(
204+
partial(self.on_document_change, file_id, file_type, file_format, document)
205+
)
205206
# update the document when file changes
206207
if file_id not in self.watchers:
207208
self.watchers[file_id] = asyncio.create_task(
208-
self.watch_file(file_type, file_id, document)
209+
self.watch_file(file_format, file_id, document)
209210
)
210211

211212
await self.websocket_server.serve(websocket)
@@ -245,7 +246,7 @@ async def get_file_path(self, file_id: str, document) -> str | None:
245246
document.path = file_path
246247
return file_path
247248

248-
async def watch_file(self, file_type: str, file_id: str, document: YBaseDoc) -> None:
249+
async def watch_file(self, file_format: str, file_id: str, document: YBaseDoc) -> None:
249250
file_path = await self.get_file_path(file_id, document)
250251
assert file_path is not None
251252
logger.debug(f"Watching file: {file_path}")
@@ -260,26 +261,25 @@ async def watch_file(self, file_type: str, file_id: str, document: YBaseDoc) ->
260261
self.contents.file_id_manager.unwatch(file_path, watcher)
261262
file_path = new_file_path
262263
# break
263-
await self.maybe_load_file(file_type, file_path, file_id)
264+
await self.maybe_load_file(file_format, file_path, file_id)
264265

265-
async def maybe_load_file(self, file_type: str, file_path: str, file_id: str) -> None:
266+
async def maybe_load_file(self, file_format: str, file_path: str, file_id: str) -> None:
266267
async with self.lock:
267268
model = await self.contents.read_content(file_path, False)
268269
# do nothing if the file was saved by us
269270
assert model.last_modified is not None
270271
if self.last_modified[file_id] < to_datetime(model.last_modified):
271272
# the file was not saved by us, update the shared document(s)
272-
is_notebook = file_type == "notebook"
273273
async with self.lock:
274-
model = await self.contents.read_content(file_path, True, as_json=is_notebook)
274+
model = await self.contents.read_content(file_path, True, file_format)
275275
assert model.last_modified is not None
276276
documents = [v for k, v in self.documents.items() if k.split(":", 2)[2] == file_id]
277277
for document in documents:
278278
document.source = model.content
279279
self.last_modified[file_id] = to_datetime(model.last_modified)
280280

281281
def on_document_change(
282-
self, file_id: str, file_type: str, document: YBaseDoc, target, event
282+
self, file_id: str, file_type: str, file_format: str, document: YBaseDoc, target, event
283283
) -> None:
284284
if target == "state" and "dirty" in event.keys:
285285
dirty = event.keys["dirty"]["newValue"]
@@ -289,14 +289,18 @@ def on_document_change(
289289
# unobserve and observe again because the structure of the document may have changed
290290
# e.g. a new cell added to a notebook
291291
document.unobserve()
292-
document.observe(partial(self.on_document_change, file_id, file_type, document))
292+
document.observe(
293+
partial(self.on_document_change, file_id, file_type, file_format, document)
294+
)
293295
if file_id in self.savers:
294296
self.savers[file_id].cancel()
295297
self.savers[file_id] = asyncio.create_task(
296-
self.maybe_save_document(file_id, file_type, document)
298+
self.maybe_save_document(file_id, file_type, file_format, document)
297299
)
298300

299-
async def maybe_save_document(self, file_id: str, file_type: str, document: YBaseDoc) -> None:
301+
async def maybe_save_document(
302+
self, file_id: str, file_type: str, file_format: str, document: YBaseDoc
303+
) -> None:
300304
# save after 1 second of inactivity to prevent too frequent saving
301305
await asyncio.sleep(1) # FIXME: pass in config
302306
# if the room cannot be found, don't save
@@ -305,9 +309,8 @@ async def maybe_save_document(self, file_id: str, file_type: str, document: YBas
305309
except BaseException:
306310
return
307311
assert file_path is not None
308-
is_notebook = file_type == "notebook"
309312
async with self.lock:
310-
model = await self.contents.read_content(file_path, True, as_json=is_notebook)
313+
model = await self.contents.read_content(file_path, True, file_format)
311314
assert model.last_modified is not None
312315
if self.last_modified[file_id] < to_datetime(model.last_modified):
313316
# file changed on disk, let's revert
@@ -318,10 +321,9 @@ async def maybe_save_document(self, file_id: str, file_type: str, document: YBas
318321
# don't save if not needed
319322
# this also prevents the dirty flag from bouncing between windows of
320323
# the same document opened as different types (e.g. notebook/text editor)
321-
format = "json" if file_type == "notebook" else "text"
322324
content = {
323325
"content": document.source,
324-
"format": format,
326+
"format": file_format,
325327
"path": file_path,
326328
"type": file_type,
327329
}

0 commit comments

Comments
 (0)