Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve contents #322

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions jupyverse_api/jupyverse_api/contents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,17 @@ async def rename_content(
user: User,
) -> Content:
...

@abstractmethod
async def is_dir(
self,
path: str,
) -> bool:
...

@abstractmethod
async def is_file(
self,
path: str,
) -> bool:
...
120 changes: 67 additions & 53 deletions plugins/contents/fps_contents/routes.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import base64
import json
import os
import pathlib
import shutil
from datetime import datetime
from http import HTTPStatus
from pathlib import Path
from typing import Dict, List, Optional, Union, cast

from anyio import open_file
from anyio import Path, open_file, to_thread
from fastapi import HTTPException, Response
from jupyverse_api.auth import User
from jupyverse_api.contents import Contents
Expand All @@ -32,12 +32,12 @@ async def create_checkpoint(
src_path = Path(path)
dst_path = Path(".ipynb_checkpoints") / f"{src_path.stem}-checkpoint{src_path.suffix}"
try:
dst_path.parent.mkdir(exist_ok=True)
shutil.copyfile(src_path, dst_path)
await dst_path.parent.mkdir(exist_ok=True)
await to_thread.run_sync(shutil.copyfile, src_path, dst_path)
except Exception:
# FIXME: return error code?
return []
mtime = get_file_modification_time(dst_path)
mtime = await get_file_modification_time(dst_path)
return Checkpoint(**{"id": "checkpoint", "last_modified": mtime})

async def create_content(
Expand All @@ -49,29 +49,32 @@ async def create_content(
create_content = CreateContent(**(await request.json()))
content_path = Path(create_content.path)
if create_content.type == "notebook":
available_path = get_available_path(content_path / "Untitled.ipynb")
available_path = await get_available_path(content_path / "Untitled.ipynb")
async with await open_file(available_path, "w") as f:
await f.write(
json.dumps({"cells": [], "metadata": {}, "nbformat": 4, "nbformat_minor": 5})
)
src_path = available_path
dst_path = Path(".ipynb_checkpoints") / f"{src_path.stem}-checkpoint{src_path.suffix}"
try:
dst_path.parent.mkdir(exist_ok=True)
shutil.copyfile(src_path, dst_path)
await dst_path.parent.mkdir(exist_ok=True)
await to_thread.run_sync(shutil.copyfile, src_path, dst_path)
except Exception:
# FIXME: return error code?
pass
elif create_content.type == "directory":
name = "Untitled Folder"
available_path = get_available_path(content_path / name, sep=" ")
available_path.mkdir(parents=True, exist_ok=True)
available_path = await get_available_path(content_path / name, sep=" ")
await available_path.mkdir(parents=True, exist_ok=True)
else:
assert create_content.ext is not None
available_path = get_available_path(content_path / ("untitled" + create_content.ext))
open(available_path, "w").close()
available_path = await get_available_path(
content_path / ("untitled" + create_content.ext)
)
async with await open_file(available_path, "w") as f:
pass

return await self.read_content(available_path, False)
return await self.read_content(pathlib.Path(available_path), False)

async def get_root_content(
self,
Expand All @@ -87,9 +90,9 @@ async def get_checkpoint(
):
src_path = Path(path)
dst_path = Path(".ipynb_checkpoints") / f"{src_path.stem}-checkpoint{src_path.suffix}"
if not dst_path.exists():
if not await dst_path.exists():
return []
mtime = get_file_modification_time(dst_path)
mtime = await get_file_modification_time(dst_path)
return [Checkpoint(**{"id": "checkpoint", "last_modified": mtime})]

async def get_content(
Expand Down Expand Up @@ -120,11 +123,11 @@ async def delete_content(
user: User,
):
p = Path(path)
if p.exists():
if p.is_dir():
shutil.rmtree(p)
if await p.exists():
if await p.is_dir():
await to_thread.run_sync(shutil.rmtree, p)
else:
p.unlink()
await p.unlink()
return Response(status_code=HTTPStatus.NO_CONTENT.value)

async def rename_content(
Expand All @@ -134,25 +137,24 @@ async def rename_content(
user: User,
):
rename_content = RenameContent(**(await request.json()))
Path(path).rename(rename_content.path)
await Path(path).rename(rename_content.path)
return await self.read_content(rename_content.path, False)

async def read_content(
self, path: Union[str, Path], get_content: bool, file_format: Optional[str] = None
self, path: Union[str, pathlib.Path], get_content: bool, file_format: Optional[str] = None
) -> Content:
if isinstance(path, str):
path = Path(path)
apath = Path(path)
content: Optional[Union[str, Dict, List[Dict]]] = None
if get_content:
if path.is_dir():
if await apath.is_dir():
content = [
(await self.read_content(subpath, get_content=False)).dict()
for subpath in path.iterdir()
(await self.read_content(pathlib.Path(subpath), get_content=False)).dict()
async for subpath in apath.iterdir()
if not subpath.name.startswith(".")
]
elif path.is_file() or path.is_symlink():
elif await apath.is_file() or await apath.is_symlink():
try:
async with await open_file(path, mode="rb") as f:
async with await open_file(apath, mode="rb") as f:
content_bytes = await f.read()
if file_format == "base64":
content = base64.b64encode(content_bytes).decode("ascii")
Expand All @@ -163,14 +165,14 @@ async def read_content(
except Exception:
raise HTTPException(status_code=404, detail="Item not found")
format: Optional[str] = None
if path.is_dir():
if await apath.is_dir():
size = None
type = "directory"
format = "json"
mimetype = None
elif path.is_file() or path.is_symlink():
size = get_file_size(path)
if path.suffix == ".ipynb":
elif await apath.is_file() or await apath.is_symlink():
size = await get_file_size(apath)
if apath.suffix == ".ipynb":
type = "notebook"
format = None
mimetype = None
Expand All @@ -188,7 +190,7 @@ async def read_content(
cell["metadata"].update({"trusted": False})
if file_format != "json":
content = json.dumps(nb)
elif path.suffix == ".json":
elif apath.suffix == ".json":
type = "json"
format = "text"
mimetype = "application/json"
Expand All @@ -201,15 +203,15 @@ async def read_content(

return Content(
**{
"name": path.name,
"path": path.as_posix(),
"last_modified": get_file_modification_time(path),
"created": get_file_creation_time(path),
"name": apath.name,
"path": apath.as_posix(),
"last_modified": await get_file_modification_time(apath),
"created": await get_file_creation_time(apath),
"content": content,
"format": format,
"mimetype": mimetype,
"size": size,
"writable": is_file_writable(path),
"writable": await is_file_writable(apath),
"type": type,
}
)
Expand Down Expand Up @@ -242,8 +244,20 @@ async def write_content(self, content: Union[SaveContent, Dict]) -> None:
def file_id_manager(self):
return FileIdManager()

async def is_dir(
self,
path: str,
) -> bool:
return await Path(path).is_dir()

async def is_file(
self,
path: str,
) -> bool:
return await Path(path).is_file()


def get_available_path(path: Path, sep: str = "") -> Path:
async def get_available_path(path: Path, sep: str = "") -> Path:
directory = path.parent
name = Path(path.name)
i = None
Expand All @@ -257,31 +271,31 @@ def get_available_path(path: Path, sep: str = "") -> Path:
if i_str:
i_str = sep + i_str
available_path = directory / (name.stem + i_str + name.suffix)
if not available_path.exists():
if not await available_path.exists():
return available_path


def get_file_modification_time(path: Path):
if path.exists():
return datetime.utcfromtimestamp(path.stat().st_mtime).isoformat() + "Z"
async def get_file_modification_time(path: Path):
if await path.exists():
return datetime.utcfromtimestamp((await path.stat()).st_mtime).isoformat() + "Z"


def get_file_creation_time(path: Path):
if path.exists():
return datetime.utcfromtimestamp(path.stat().st_ctime).isoformat() + "Z"
async def get_file_creation_time(path: Path):
if await path.exists():
return datetime.utcfromtimestamp((await path.stat()).st_ctime).isoformat() + "Z"


def get_file_size(path: Path) -> Optional[int]:
if path.exists():
return path.stat().st_size
async def get_file_size(path: Path) -> Optional[int]:
if await path.exists():
return (await path.stat()).st_size
raise HTTPException(status_code=404, detail="Item not found")


def is_file_writable(path: Path) -> bool:
if path.exists():
if path.is_dir():
async def is_file_writable(path: Path) -> bool:
if await path.exists():
if await path.is_dir():
# FIXME
return True
else:
return os.access(path, os.W_OK)
return await to_thread.run_sync(os.access, path, os.W_OK)
return False