11import base64
22import json
33import os
4+ import pathlib
45import shutil
56from datetime import datetime
67from http import HTTPStatus
7- from pathlib import Path
88from typing import Dict , List , Optional , Union , cast
99
10- from anyio import open_file
10+ from anyio import Path , open_file , to_thread
1111from fastapi import HTTPException , Response
1212from jupyverse_api .auth import User
1313from jupyverse_api .contents import Contents
@@ -32,12 +32,12 @@ async def create_checkpoint(
3232 src_path = Path (path )
3333 dst_path = Path (".ipynb_checkpoints" ) / f"{ src_path .stem } -checkpoint{ src_path .suffix } "
3434 try :
35- dst_path .parent .mkdir (exist_ok = True )
36- shutil .copyfile ( src_path , dst_path )
35+ await dst_path .parent .mkdir (exist_ok = True )
36+ await to_thread . run_sync ( shutil .copyfile , src_path , dst_path )
3737 except Exception :
3838 # FIXME: return error code?
3939 return []
40- mtime = get_file_modification_time (dst_path )
40+ mtime = await get_file_modification_time (dst_path )
4141 return Checkpoint (** {"id" : "checkpoint" , "last_modified" : mtime })
4242
4343 async def create_content (
@@ -49,29 +49,32 @@ async def create_content(
4949 create_content = CreateContent (** (await request .json ()))
5050 content_path = Path (create_content .path )
5151 if create_content .type == "notebook" :
52- available_path = get_available_path (content_path / "Untitled.ipynb" )
52+ available_path = await get_available_path (content_path / "Untitled.ipynb" )
5353 async with await open_file (available_path , "w" ) as f :
5454 await f .write (
5555 json .dumps ({"cells" : [], "metadata" : {}, "nbformat" : 4 , "nbformat_minor" : 5 })
5656 )
5757 src_path = available_path
5858 dst_path = Path (".ipynb_checkpoints" ) / f"{ src_path .stem } -checkpoint{ src_path .suffix } "
5959 try :
60- dst_path .parent .mkdir (exist_ok = True )
61- shutil .copyfile ( src_path , dst_path )
60+ await dst_path .parent .mkdir (exist_ok = True )
61+ await to_thread . run_sync ( shutil .copyfile , src_path , dst_path )
6262 except Exception :
6363 # FIXME: return error code?
6464 pass
6565 elif create_content .type == "directory" :
6666 name = "Untitled Folder"
67- available_path = get_available_path (content_path / name , sep = " " )
68- available_path .mkdir (parents = True , exist_ok = True )
67+ available_path = await get_available_path (content_path / name , sep = " " )
68+ await available_path .mkdir (parents = True , exist_ok = True )
6969 else :
7070 assert create_content .ext is not None
71- available_path = get_available_path (content_path / ("untitled" + create_content .ext ))
72- open (available_path , "w" ).close ()
71+ available_path = await get_available_path (
72+ content_path / ("untitled" + create_content .ext )
73+ )
74+ async with await open_file (available_path , "w" ) as f :
75+ pass
7376
74- return await self .read_content (available_path , False )
77+ return await self .read_content (pathlib . Path ( available_path ) , False )
7578
7679 async def get_root_content (
7780 self ,
@@ -87,9 +90,9 @@ async def get_checkpoint(
8790 ):
8891 src_path = Path (path )
8992 dst_path = Path (".ipynb_checkpoints" ) / f"{ src_path .stem } -checkpoint{ src_path .suffix } "
90- if not dst_path .exists ():
93+ if not await dst_path .exists ():
9194 return []
92- mtime = get_file_modification_time (dst_path )
95+ mtime = await get_file_modification_time (dst_path )
9396 return [Checkpoint (** {"id" : "checkpoint" , "last_modified" : mtime })]
9497
9598 async def get_content (
@@ -120,11 +123,11 @@ async def delete_content(
120123 user : User ,
121124 ):
122125 p = Path (path )
123- if p .exists ():
124- if p .is_dir ():
125- shutil .rmtree ( p )
126+ if await p .exists ():
127+ if await p .is_dir ():
128+ await to_thread . run_sync ( shutil .rmtree , p )
126129 else :
127- p .unlink ()
130+ await p .unlink ()
128131 return Response (status_code = HTTPStatus .NO_CONTENT .value )
129132
130133 async def rename_content (
@@ -134,25 +137,24 @@ async def rename_content(
134137 user : User ,
135138 ):
136139 rename_content = RenameContent (** (await request .json ()))
137- Path (path ).rename (rename_content .path )
140+ await Path (path ).rename (rename_content .path )
138141 return await self .read_content (rename_content .path , False )
139142
140143 async def read_content (
141- self , path : Union [str , Path ], get_content : bool , file_format : Optional [str ] = None
144+ self , path : Union [str , pathlib . Path ], get_content : bool , file_format : Optional [str ] = None
142145 ) -> Content :
143- if isinstance (path , str ):
144- path = Path (path )
146+ apath = Path (path )
145147 content : Optional [Union [str , Dict , List [Dict ]]] = None
146148 if get_content :
147- if path .is_dir ():
149+ if await apath .is_dir ():
148150 content = [
149151 (await self .read_content (subpath , get_content = False )).dict ()
150- for subpath in path .iterdir ()
152+ async for subpath in apath .iterdir ()
151153 if not subpath .name .startswith ("." )
152154 ]
153- elif path .is_file () or path .is_symlink ():
155+ elif await apath .is_file () or await apath .is_symlink ():
154156 try :
155- async with await open_file (path , mode = "rb" ) as f :
157+ async with await open_file (apath , mode = "rb" ) as f :
156158 content_bytes = await f .read ()
157159 if file_format == "base64" :
158160 content = base64 .b64encode (content_bytes ).decode ("ascii" )
@@ -163,14 +165,14 @@ async def read_content(
163165 except Exception :
164166 raise HTTPException (status_code = 404 , detail = "Item not found" )
165167 format : Optional [str ] = None
166- if path .is_dir ():
168+ if await apath .is_dir ():
167169 size = None
168170 type = "directory"
169171 format = "json"
170172 mimetype = None
171- elif path .is_file () or path .is_symlink ():
172- size = get_file_size (path )
173- if path .suffix == ".ipynb" :
173+ elif await apath .is_file () or await apath .is_symlink ():
174+ size = await get_file_size (apath )
175+ if apath .suffix == ".ipynb" :
174176 type = "notebook"
175177 format = None
176178 mimetype = None
@@ -188,7 +190,7 @@ async def read_content(
188190 cell ["metadata" ].update ({"trusted" : False })
189191 if file_format != "json" :
190192 content = json .dumps (nb )
191- elif path .suffix == ".json" :
193+ elif apath .suffix == ".json" :
192194 type = "json"
193195 format = "text"
194196 mimetype = "application/json"
@@ -201,15 +203,15 @@ async def read_content(
201203
202204 return Content (
203205 ** {
204- "name" : path .name ,
205- "path" : path .as_posix (),
206- "last_modified" : get_file_modification_time (path ),
207- "created" : get_file_creation_time (path ),
206+ "name" : apath .name ,
207+ "path" : apath .as_posix (),
208+ "last_modified" : await get_file_modification_time (apath ),
209+ "created" : await get_file_creation_time (apath ),
208210 "content" : content ,
209211 "format" : format ,
210212 "mimetype" : mimetype ,
211213 "size" : size ,
212- "writable" : is_file_writable (path ),
214+ "writable" : await is_file_writable (apath ),
213215 "type" : type ,
214216 }
215217 )
@@ -242,8 +244,20 @@ async def write_content(self, content: Union[SaveContent, Dict]) -> None:
242244 def file_id_manager (self ):
243245 return FileIdManager ()
244246
247+ async def is_dir (
248+ self ,
249+ path : str ,
250+ ) -> bool :
251+ return await Path (path ).is_dir ()
252+
253+ async def is_file (
254+ self ,
255+ path : str ,
256+ ) -> bool :
257+ return await Path (path ).is_file ()
258+
245259
246- def get_available_path (path : Path , sep : str = "" ) -> Path :
260+ async def get_available_path (path : Path , sep : str = "" ) -> Path :
247261 directory = path .parent
248262 name = Path (path .name )
249263 i = None
@@ -257,31 +271,31 @@ def get_available_path(path: Path, sep: str = "") -> Path:
257271 if i_str :
258272 i_str = sep + i_str
259273 available_path = directory / (name .stem + i_str + name .suffix )
260- if not available_path .exists ():
274+ if not await available_path .exists ():
261275 return available_path
262276
263277
264- def get_file_modification_time (path : Path ):
265- if path .exists ():
266- return datetime .utcfromtimestamp (path .stat ().st_mtime ).isoformat () + "Z"
278+ async def get_file_modification_time (path : Path ):
279+ if await path .exists ():
280+ return datetime .utcfromtimestamp (( await path .stat () ).st_mtime ).isoformat () + "Z"
267281
268282
269- def get_file_creation_time (path : Path ):
270- if path .exists ():
271- return datetime .utcfromtimestamp (path .stat ().st_ctime ).isoformat () + "Z"
283+ async def get_file_creation_time (path : Path ):
284+ if await path .exists ():
285+ return datetime .utcfromtimestamp (( await path .stat () ).st_ctime ).isoformat () + "Z"
272286
273287
274- def get_file_size (path : Path ) -> Optional [int ]:
275- if path .exists ():
276- return path .stat ().st_size
288+ async def get_file_size (path : Path ) -> Optional [int ]:
289+ if await path .exists ():
290+ return ( await path .stat () ).st_size
277291 raise HTTPException (status_code = 404 , detail = "Item not found" )
278292
279293
280- def is_file_writable (path : Path ) -> bool :
281- if path .exists ():
282- if path .is_dir ():
294+ async def is_file_writable (path : Path ) -> bool :
295+ if await path .exists ():
296+ if await path .is_dir ():
283297 # FIXME
284298 return True
285299 else :
286- return os .access ( path , os .W_OK )
300+ return await to_thread . run_sync ( os .access , path , os .W_OK )
287301 return False
0 commit comments