1
1
import base64
2
2
import json
3
3
import os
4
+ import pathlib
4
5
import shutil
5
6
from datetime import datetime
6
7
from http import HTTPStatus
7
- from pathlib import Path
8
8
from typing import Dict , List , Optional , Union , cast
9
9
10
- from anyio import open_file
10
+ from anyio import Path , open_file , to_thread
11
11
from fastapi import HTTPException , Response
12
12
from jupyverse_api .auth import User
13
13
from jupyverse_api .contents import Contents
@@ -32,12 +32,12 @@ async def create_checkpoint(
32
32
src_path = Path (path )
33
33
dst_path = Path (".ipynb_checkpoints" ) / f"{ src_path .stem } -checkpoint{ src_path .suffix } "
34
34
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 )
37
37
except Exception :
38
38
# FIXME: return error code?
39
39
return []
40
- mtime = get_file_modification_time (dst_path )
40
+ mtime = await get_file_modification_time (dst_path )
41
41
return Checkpoint (** {"id" : "checkpoint" , "last_modified" : mtime })
42
42
43
43
async def create_content (
@@ -49,29 +49,32 @@ async def create_content(
49
49
create_content = CreateContent (** (await request .json ()))
50
50
content_path = Path (create_content .path )
51
51
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" )
53
53
async with await open_file (available_path , "w" ) as f :
54
54
await f .write (
55
55
json .dumps ({"cells" : [], "metadata" : {}, "nbformat" : 4 , "nbformat_minor" : 5 })
56
56
)
57
57
src_path = available_path
58
58
dst_path = Path (".ipynb_checkpoints" ) / f"{ src_path .stem } -checkpoint{ src_path .suffix } "
59
59
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 )
62
62
except Exception :
63
63
# FIXME: return error code?
64
64
pass
65
65
elif create_content .type == "directory" :
66
66
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 )
69
69
else :
70
70
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
73
76
74
- return await self .read_content (available_path , False )
77
+ return await self .read_content (pathlib . Path ( available_path ) , False )
75
78
76
79
async def get_root_content (
77
80
self ,
@@ -87,9 +90,9 @@ async def get_checkpoint(
87
90
):
88
91
src_path = Path (path )
89
92
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 ():
91
94
return []
92
- mtime = get_file_modification_time (dst_path )
95
+ mtime = await get_file_modification_time (dst_path )
93
96
return [Checkpoint (** {"id" : "checkpoint" , "last_modified" : mtime })]
94
97
95
98
async def get_content (
@@ -120,11 +123,11 @@ async def delete_content(
120
123
user : User ,
121
124
):
122
125
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 )
126
129
else :
127
- p .unlink ()
130
+ await p .unlink ()
128
131
return Response (status_code = HTTPStatus .NO_CONTENT .value )
129
132
130
133
async def rename_content (
@@ -134,25 +137,24 @@ async def rename_content(
134
137
user : User ,
135
138
):
136
139
rename_content = RenameContent (** (await request .json ()))
137
- Path (path ).rename (rename_content .path )
140
+ await Path (path ).rename (rename_content .path )
138
141
return await self .read_content (rename_content .path , False )
139
142
140
143
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
142
145
) -> Content :
143
- if isinstance (path , str ):
144
- path = Path (path )
146
+ apath = Path (path )
145
147
content : Optional [Union [str , Dict , List [Dict ]]] = None
146
148
if get_content :
147
- if path .is_dir ():
149
+ if await apath .is_dir ():
148
150
content = [
149
151
(await self .read_content (subpath , get_content = False )).dict ()
150
- for subpath in path .iterdir ()
152
+ async for subpath in apath .iterdir ()
151
153
if not subpath .name .startswith ("." )
152
154
]
153
- elif path .is_file () or path .is_symlink ():
155
+ elif await apath .is_file () or await apath .is_symlink ():
154
156
try :
155
- async with await open_file (path , mode = "rb" ) as f :
157
+ async with await open_file (apath , mode = "rb" ) as f :
156
158
content_bytes = await f .read ()
157
159
if file_format == "base64" :
158
160
content = base64 .b64encode (content_bytes ).decode ("ascii" )
@@ -163,14 +165,14 @@ async def read_content(
163
165
except Exception :
164
166
raise HTTPException (status_code = 404 , detail = "Item not found" )
165
167
format : Optional [str ] = None
166
- if path .is_dir ():
168
+ if await apath .is_dir ():
167
169
size = None
168
170
type = "directory"
169
171
format = "json"
170
172
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" :
174
176
type = "notebook"
175
177
format = None
176
178
mimetype = None
@@ -188,7 +190,7 @@ async def read_content(
188
190
cell ["metadata" ].update ({"trusted" : False })
189
191
if file_format != "json" :
190
192
content = json .dumps (nb )
191
- elif path .suffix == ".json" :
193
+ elif apath .suffix == ".json" :
192
194
type = "json"
193
195
format = "text"
194
196
mimetype = "application/json"
@@ -201,15 +203,15 @@ async def read_content(
201
203
202
204
return Content (
203
205
** {
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 ),
208
210
"content" : content ,
209
211
"format" : format ,
210
212
"mimetype" : mimetype ,
211
213
"size" : size ,
212
- "writable" : is_file_writable (path ),
214
+ "writable" : await is_file_writable (apath ),
213
215
"type" : type ,
214
216
}
215
217
)
@@ -242,8 +244,20 @@ async def write_content(self, content: Union[SaveContent, Dict]) -> None:
242
244
def file_id_manager (self ):
243
245
return FileIdManager ()
244
246
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
+
245
259
246
- def get_available_path (path : Path , sep : str = "" ) -> Path :
260
+ async def get_available_path (path : Path , sep : str = "" ) -> Path :
247
261
directory = path .parent
248
262
name = Path (path .name )
249
263
i = None
@@ -257,31 +271,31 @@ def get_available_path(path: Path, sep: str = "") -> Path:
257
271
if i_str :
258
272
i_str = sep + i_str
259
273
available_path = directory / (name .stem + i_str + name .suffix )
260
- if not available_path .exists ():
274
+ if not await available_path .exists ():
261
275
return available_path
262
276
263
277
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"
267
281
268
282
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"
272
286
273
287
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
277
291
raise HTTPException (status_code = 404 , detail = "Item not found" )
278
292
279
293
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 ():
283
297
# FIXME
284
298
return True
285
299
else :
286
- return os .access ( path , os .W_OK )
300
+ return await to_thread . run_sync ( os .access , path , os .W_OK )
287
301
return False
0 commit comments