Skip to content

Commit 9be92d8

Browse files
Mariko WakabayashiZsailer
authored andcommitted
Create AsyncLargeFileManager
1 parent 3bc0feb commit 9be92d8

File tree

2 files changed

+92
-15
lines changed

2 files changed

+92
-15
lines changed

jupyter_server/services/contents/largefilemanager.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from jupyter_server.services.contents.filemanager import FileContentsManager
1+
from anyio import run_sync_in_worker_thread
22
from contextlib import contextmanager
33
from tornado import web
44
import nbformat
55
import base64
66
import os, io
77

8+
from jupyter_server.services.contents.filemanager import AsyncFileContentsManager, FileContentsManager
9+
810

911
class LargeFileManager(FileContentsManager):
1012
"""Handle large file upload."""
@@ -71,3 +73,69 @@ def _save_large_file(self, os_path, content, format):
7173
with io.open(os_path, 'ab') as f:
7274
f.write(bcontent)
7375

76+
77+
class AsyncLargeFileManager(AsyncFileContentsManager):
78+
"""Handle large file upload asynchronously"""
79+
80+
async def save(self, model, path=''):
81+
"""Save the file model and return the model with no content."""
82+
chunk = model.get('chunk', None)
83+
if chunk is not None:
84+
path = path.strip('/')
85+
86+
if 'type' not in model:
87+
raise web.HTTPError(400, u'No file type provided')
88+
if model['type'] != 'file':
89+
raise web.HTTPError(400, u'File type "{}" is not supported for large file transfer'.format(model['type']))
90+
if 'content' not in model and model['type'] != 'directory':
91+
raise web.HTTPError(400, u'No file content provided')
92+
93+
os_path = self._get_os_path(path)
94+
95+
try:
96+
if chunk == 1:
97+
self.log.debug("Saving %s", os_path)
98+
self.run_pre_save_hook(model=model, path=path)
99+
await super(AsyncLargeFileManager, self)._save_file(os_path, model['content'], model.get('format'))
100+
else:
101+
await self._save_large_file(os_path, model['content'], model.get('format'))
102+
except web.HTTPError:
103+
raise
104+
except Exception as e:
105+
self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
106+
raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' %
107+
(path, e)) from e
108+
109+
model = await self.get(path, content=False)
110+
111+
# Last chunk
112+
if chunk == -1:
113+
self.run_post_save_hook(model=model, os_path=os_path)
114+
return model
115+
else:
116+
return await super(AsyncLargeFileManager, self).save(model, path)
117+
118+
async def _save_large_file(self, os_path, content, format):
119+
"""Save content of a generic file."""
120+
if format not in {'text', 'base64'}:
121+
raise web.HTTPError(
122+
400,
123+
"Must specify format of file contents as 'text' or 'base64'",
124+
)
125+
try:
126+
if format == 'text':
127+
bcontent = content.encode('utf8')
128+
else:
129+
b64_bytes = content.encode('ascii')
130+
bcontent = base64.b64decode(b64_bytes)
131+
except Exception as e:
132+
raise web.HTTPError(
133+
400, u'Encoding error saving %s: %s' % (os_path, e)
134+
) from e
135+
136+
with self.perm_to_403(os_path):
137+
if os.path.islink(os_path):
138+
os_path = os.path.join(os.path.dirname(os_path), os.readlink(os_path))
139+
with io.open(os_path, 'ab') as f:
140+
await run_sync_in_worker_thread(f.write, bcontent)
141+

tests/services/contents/test_largefilemanager.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
import pytest
22
import tornado
33

4+
from jupyter_server.services.contents.largefilemanager import AsyncLargeFileManager, LargeFileManager
5+
from jupyter_server.utils import ensure_async
46
from ...utils import expected_http_error
57

68

7-
def test_save(jp_large_contents_manager):
9+
@pytest.fixture(params=[LargeFileManager, AsyncLargeFileManager])
10+
def jp_large_contents_manager(request, tmp_path):
11+
"""Returns a LargeFileManager instance."""
12+
file_manager = request.param
13+
return file_manager(root_dir=str(tmp_path))
14+
15+
16+
async def test_save(jp_large_contents_manager):
817
cm = jp_large_contents_manager
9-
model = cm.new_untitled(type='notebook')
18+
model = await ensure_async(cm.new_untitled(type='notebook'))
1019
name = model['name']
1120
path = model['path']
1221

1322
# Get the model with 'content'
14-
full_model = cm.get(path)
23+
full_model = await ensure_async(cm.get(path))
1524
# Save the notebook
16-
model = cm.save(full_model, path)
25+
model = await ensure_async(cm.save(full_model, path))
1726
assert isinstance(model, dict)
1827
assert 'name' in model
1928
assert 'path' in model
@@ -43,26 +52,26 @@ def test_save(jp_large_contents_manager):
4352
)
4453
]
4554
)
46-
def test_bad_save(jp_large_contents_manager, model, err_message):
55+
async def test_bad_save(jp_large_contents_manager, model, err_message):
4756
with pytest.raises(tornado.web.HTTPError) as e:
48-
jp_large_contents_manager.save(model, model['path'])
57+
await ensure_async(jp_large_contents_manager.save(model, model['path']))
4958
assert expected_http_error(e, 400, expected_message=err_message)
5059

5160

52-
def test_saving_different_chunks(jp_large_contents_manager):
61+
async def test_saving_different_chunks(jp_large_contents_manager):
5362
cm = jp_large_contents_manager
5463
model = {'name': 'test', 'path': 'test', 'type': 'file',
5564
'content': u'test==', 'format': 'text'}
5665
name = model['name']
5766
path = model['path']
58-
cm.save(model, path)
67+
await ensure_async(cm.save(model, path))
5968

6069
for chunk in (1, 2, -1):
6170
for fm in ('text', 'base64'):
62-
full_model = cm.get(path)
71+
full_model = await ensure_async(cm.get(path))
6372
full_model['chunk'] = chunk
6473
full_model['format'] = fm
65-
model_res = cm.save(full_model, path)
74+
model_res = await ensure_async(cm.save(full_model, path))
6675
assert isinstance(model_res, dict)
6776
assert 'name' in model_res
6877
assert 'path' in model_res
@@ -71,16 +80,16 @@ def test_saving_different_chunks(jp_large_contents_manager):
7180
assert model_res['path'] == path
7281

7382

74-
def test_save_in_subdirectory(jp_large_contents_manager, tmp_path):
83+
async def test_save_in_subdirectory(jp_large_contents_manager, tmp_path):
7584
cm = jp_large_contents_manager
7685
sub_dir = tmp_path / 'foo'
7786
sub_dir.mkdir()
78-
model = cm.new_untitled(path='/foo/', type='notebook')
87+
model = await ensure_async(cm.new_untitled(path='/foo/', type='notebook'))
7988
path = model['path']
80-
model = cm.get(path)
89+
model = await ensure_async(cm.get(path))
8190

8291
# Change the name in the model for rename
83-
model = cm.save(model, path)
92+
model = await ensure_async(cm.save(model, path))
8493
assert isinstance(model, dict)
8594
assert 'name' in model
8695
assert 'path' in model

0 commit comments

Comments
 (0)