7
7
8
8
from contextlib import contextmanager
9
9
import errno
10
+ from functools import partial
10
11
import io
11
12
import os
12
13
import shutil
13
14
15
+ from anyio import open_file , run_sync_in_worker_thread
14
16
from tornado .web import HTTPError
15
17
16
18
from jupyter_server .utils import (
@@ -32,6 +34,11 @@ def replace_file(src, dst):
32
34
"""
33
35
os .replace (src , dst )
34
36
37
+ async def async_replace_file (src , dst ):
38
+ """ replace dst with src asynchronously
39
+ """
40
+ await run_sync_in_worker_thread (os .replace , src , dst )
41
+
35
42
def copy2_safe (src , dst , log = None ):
36
43
"""copy src to dst
37
44
@@ -44,6 +51,18 @@ def copy2_safe(src, dst, log=None):
44
51
if log :
45
52
log .debug ("copystat on %s failed" , dst , exc_info = True )
46
53
54
+ async def async_copy2_safe (src , dst , log = None ):
55
+ """copy src to dst asynchronously
56
+
57
+ like shutil.copy2, but log errors in copystat instead of raising
58
+ """
59
+ await run_sync_in_worker_thread (shutil .copyfile , src , dst )
60
+ try :
61
+ await run_sync_in_worker_thread (shutil .copystat , src , dst )
62
+ except OSError :
63
+ if log :
64
+ log .debug ("copystat on %s failed" , dst , exc_info = True )
65
+
47
66
def path_to_intermediate (path ):
48
67
'''Name of the intermediate file used in atomic writes.
49
68
@@ -116,11 +135,10 @@ def atomic_writing(path, text=True, encoding='utf-8', log=None, **kwargs):
116
135
os .remove (tmp_path )
117
136
118
137
119
-
120
138
@contextmanager
121
139
def _simple_writing (path , text = True , encoding = 'utf-8' , log = None , ** kwargs ):
122
140
"""Context manager to write file without doing atomic writing
123
- ( for weird filesystem eg: nfs).
141
+ (for weird filesystem eg: nfs).
124
142
125
143
Parameters
126
144
----------
@@ -159,8 +177,6 @@ def _simple_writing(path, text=True, encoding='utf-8', log=None, **kwargs):
159
177
fileobj .close ()
160
178
161
179
162
-
163
-
164
180
class FileManagerMixin (Configurable ):
165
181
"""
166
182
Mixin for ContentsAPI classes that interact with the filesystem.
@@ -186,7 +202,7 @@ class FileManagerMixin(Configurable):
186
202
187
203
@contextmanager
188
204
def open (self , os_path , * args , ** kwargs ):
189
- """wrapper around io. open that turns permission errors into 403"""
205
+ """wrapper around open that turns permission errors into 403"""
190
206
with self .perm_to_403 (os_path ):
191
207
with io .open (os_path , * args , ** kwargs ) as f :
192
208
yield f
@@ -330,3 +346,94 @@ def _save_file(self, os_path, content, format):
330
346
331
347
with self .atomic_writing (os_path , text = False ) as f :
332
348
f .write (bcontent )
349
+
350
+ class AsyncFileManagerMixin (FileManagerMixin ):
351
+ """
352
+ Mixin for ContentsAPI classes that interact with the filesystem asynchronously.
353
+ """
354
+ async def _copy (self , src , dest ):
355
+ """copy src to dest
356
+
357
+ like shutil.copy2, but log errors in copystat
358
+ """
359
+ await async_copy2_safe (src , dest , log = self .log )
360
+
361
+ async def _read_notebook (self , os_path , as_version = 4 ):
362
+ """Read a notebook from an os path."""
363
+ with self .open (os_path , 'r' , encoding = 'utf-8' ) as f :
364
+ try :
365
+ return await run_sync_in_worker_thread (partial (nbformat .read , as_version = as_version ), f )
366
+ except Exception as e :
367
+ e_orig = e
368
+
369
+ # If use_atomic_writing is enabled, we'll guess that it was also
370
+ # enabled when this notebook was written and look for a valid
371
+ # atomic intermediate.
372
+ tmp_path = path_to_intermediate (os_path )
373
+
374
+ if not self .use_atomic_writing or not os .path .exists (tmp_path ):
375
+ raise HTTPError (
376
+ 400 ,
377
+ u"Unreadable Notebook: %s %r" % (os_path , e_orig ),
378
+ )
379
+
380
+ # Move the bad file aside, restore the intermediate, and try again.
381
+ invalid_file = path_to_invalid (os_path )
382
+ async_replace_file (os_path , invalid_file )
383
+ async_replace_file (tmp_path , os_path )
384
+ return await self ._read_notebook (os_path , as_version )
385
+
386
+ async def _save_notebook (self , os_path , nb ):
387
+ """Save a notebook to an os_path."""
388
+ with self .atomic_writing (os_path , encoding = 'utf-8' ) as f :
389
+ await run_sync_in_worker_thread (partial (nbformat .write , version = nbformat .NO_CONVERT ), nb , f )
390
+
391
+ async def _read_file (self , os_path , format ):
392
+ """Read a non-notebook file.
393
+
394
+ os_path: The path to be read.
395
+ format:
396
+ If 'text', the contents will be decoded as UTF-8.
397
+ If 'base64', the raw bytes contents will be encoded as base64.
398
+ If not specified, try to decode as UTF-8, and fall back to base64
399
+ """
400
+ if not os .path .isfile (os_path ):
401
+ raise HTTPError (400 , "Cannot read non-file %s" % os_path )
402
+
403
+ with self .open (os_path , 'rb' ) as f :
404
+ bcontent = await run_sync_in_worker_thread (f .read )
405
+
406
+ if format is None or format == 'text' :
407
+ # Try to interpret as unicode if format is unknown or if unicode
408
+ # was explicitly requested.
409
+ try :
410
+ return bcontent .decode ('utf8' ), 'text'
411
+ except UnicodeError as e :
412
+ if format == 'text' :
413
+ raise HTTPError (
414
+ 400 ,
415
+ "%s is not UTF-8 encoded" % os_path ,
416
+ reason = 'bad format' ,
417
+ ) from e
418
+ return encodebytes (bcontent ).decode ('ascii' ), 'base64'
419
+
420
+ async def _save_file (self , os_path , content , format ):
421
+ """Save content of a generic file."""
422
+ if format not in {'text' , 'base64' }:
423
+ raise HTTPError (
424
+ 400 ,
425
+ "Must specify format of file contents as 'text' or 'base64'" ,
426
+ )
427
+ try :
428
+ if format == 'text' :
429
+ bcontent = content .encode ('utf8' )
430
+ else :
431
+ b64_bytes = content .encode ('ascii' )
432
+ bcontent = decodebytes (b64_bytes )
433
+ except Exception as e :
434
+ raise HTTPError (
435
+ 400 , u'Encoding error saving %s: %s' % (os_path , e )
436
+ ) from e
437
+
438
+ with self .atomic_writing (os_path , text = False ) as f :
439
+ await run_sync_in_worker_thread (f .write , bcontent )
0 commit comments