Show More
@@ -1,147 +1,203 b'' | |||||
1 | """A base class contents manager. |
|
1 | """A base class contents manager. | |
2 |
|
2 | |||
3 | Authors: |
|
3 | Authors: | |
4 |
|
4 | |||
5 | * Zach Sailer |
|
5 | * Zach Sailer | |
6 | """ |
|
6 | """ | |
7 |
|
7 | |||
8 | #----------------------------------------------------------------------------- |
|
8 | #----------------------------------------------------------------------------- | |
9 | # Copyright (C) 2013 The IPython Development Team |
|
9 | # Copyright (C) 2013 The IPython Development Team | |
10 | # |
|
10 | # | |
11 | # Distributed under the terms of the BSD License. The full license is in |
|
11 | # Distributed under the terms of the BSD License. The full license is in | |
12 | # the file COPYING, distributed as part of this software. |
|
12 | # the file COPYING, distributed as part of this software. | |
13 | #----------------------------------------------------------------------------- |
|
13 | #----------------------------------------------------------------------------- | |
14 |
|
14 | |||
15 | #----------------------------------------------------------------------------- |
|
15 | #----------------------------------------------------------------------------- | |
16 | # Imports |
|
16 | # Imports | |
17 | #----------------------------------------------------------------------------- |
|
17 | #----------------------------------------------------------------------------- | |
18 |
|
18 | |||
19 | import datetime |
|
19 | import datetime | |
20 | import io |
|
20 | import io | |
21 | import os |
|
21 | import os | |
22 | import glob |
|
22 | import glob | |
23 | import shutil |
|
23 | import shutil | |
24 |
import |
|
24 | import errno | |
25 | import base64 |
|
|||
26 |
|
25 | |||
27 | from tornado import web |
|
26 | from tornado import web | |
28 |
|
27 | |||
29 | from IPython.config.configurable import LoggingConfigurable |
|
28 | from IPython.config.configurable import LoggingConfigurable | |
30 | from IPython.nbformat import current |
|
29 | from IPython.utils.traitlets import Unicode, TraitError | |
31 | from IPython.utils.traitlets import List, Dict, Unicode, TraitError |
|
|||
32 | from IPython.utils import tz |
|
30 | from IPython.utils import tz | |
33 |
|
31 | |||
34 | #----------------------------------------------------------------------------- |
|
32 | #----------------------------------------------------------------------------- | |
35 | # Classes |
|
33 | # Classes | |
36 | #----------------------------------------------------------------------------- |
|
34 | #----------------------------------------------------------------------------- | |
37 |
|
35 | |||
38 | class ContentManager(LoggingConfigurable): |
|
36 | class ContentManager(LoggingConfigurable): | |
39 |
|
37 | |||
40 | content_dir = Unicode(os.getcwdu(), config=True, help=""" |
|
38 | content_dir = Unicode(os.getcwdu(), config=True, help=""" | |
41 | The directory to use for contents. |
|
39 | The directory to use for contents. | |
42 | """) |
|
40 | """) | |
43 |
|
41 | |||
44 | def get_os_path(self, fname=None, path='/'): |
|
42 | def get_os_path(self, fname=None, path='/'): | |
45 | """Given a notebook name and a server URL path, return its file system |
|
43 | """Given a notebook name and a server URL path, return its file system | |
46 | path. |
|
44 | path. | |
47 |
|
45 | |||
48 | Parameters |
|
46 | Parameters | |
49 | ---------- |
|
47 | ---------- | |
50 | fname : string |
|
48 | fname : string | |
51 | The name of a notebook file with the .ipynb extension |
|
49 | The name of a notebook file with the .ipynb extension | |
52 | path : string |
|
50 | path : string | |
53 | The relative URL path (with '/' as separator) to the named |
|
51 | The relative URL path (with '/' as separator) to the named | |
54 | notebook. |
|
52 | notebook. | |
55 |
|
53 | |||
56 | Returns |
|
54 | Returns | |
57 | ------- |
|
55 | ------- | |
58 | path : string |
|
56 | path : string | |
59 | A file system path that combines notebook_dir (location where |
|
57 | A file system path that combines notebook_dir (location where | |
60 | server started), the relative path, and the filename with the |
|
58 | server started), the relative path, and the filename with the | |
61 | current operating system's url. |
|
59 | current operating system's url. | |
62 | """ |
|
60 | """ | |
63 | parts = path.split('/') |
|
61 | parts = path.split('/') | |
64 | parts = [p for p in parts if p != ''] # remove duplicate splits |
|
62 | parts = [p for p in parts if p != ''] # remove duplicate splits | |
65 | if fname is not None: |
|
63 | if fname is not None: | |
66 | parts += [fname] |
|
64 | parts += [fname] | |
67 | path = os.path.join(self.content_dir, *parts) |
|
65 | path = os.path.join(self.content_dir, *parts) | |
68 | return path |
|
66 | return path | |
69 |
|
67 | |||
70 | def get_content_names(self, content_path='/'): |
|
68 | def get_content_names(self, content_path='/'): | |
71 | """Returns list of names in the server's root + relative |
|
69 | """Returns list of names in the server's root + relative | |
72 | location given by 'content_path'.""" |
|
70 | location given by 'content_path'.""" | |
73 | names = os.listdir(self.get_os_path(None, content_path)) |
|
71 | names = os.listdir(self.get_os_path(None, content_path)) | |
74 | return names |
|
72 | return names | |
75 |
|
73 | |||
76 | def list_contents(self, content_path='/'): |
|
74 | def list_contents(self, content_path='/'): | |
77 | """Returns a list of dictionaries including info for all |
|
75 | """Returns a list of dictionaries including info for all | |
78 | contents in the location given by 'content_path'. |
|
76 | contents in the location given by 'content_path'. | |
79 |
|
77 | |||
80 | Parameters |
|
78 | Parameters | |
81 | ---------- |
|
79 | ---------- | |
82 | content_path: str |
|
80 | content_path: str | |
83 | the relative path/location of the desired files. |
|
81 | the relative path/location of the desired files. | |
84 |
|
82 | |||
85 | Returns |
|
83 | Returns | |
86 | ------- |
|
84 | ------- | |
87 | contents: list of dicts |
|
85 | contents: list of dicts | |
88 | a the contents of each dict includes information for each item |
|
86 | a the contents of each dict includes information for each item | |
89 | in the named location given by 'content_path'. |
|
87 | in the named location given by 'content_path'. | |
90 | """ |
|
88 | """ | |
91 | names = self.get_content_names(content_path) |
|
89 | names = self.get_content_names(content_path) | |
92 | contents = list() |
|
90 | contents = list() | |
93 | dirs = list() |
|
91 | dirs = list() | |
94 | notebooks = list() |
|
92 | notebooks = list() | |
95 | for name in names: |
|
93 | for name in names: | |
96 | if os.path.isdir(name) == True: |
|
94 | if os.path.isdir(name) == True: | |
97 | dirs.append(os.path.split(name)[1]) |
|
95 | dirs.append(os.path.split(name)[1]) | |
98 | elif os.path.splitext(name)[1] == '.ipynb': |
|
96 | elif os.path.splitext(name)[1] == '.ipynb': | |
99 | notebooks.append(os.path.split(name)[1]) |
|
97 | notebooks.append(os.path.split(name)[1]) | |
100 | else: |
|
98 | else: | |
101 | contents.append(os.path.split(name)[1]) |
|
99 | contents.append(os.path.split(name)[1]) | |
102 | return dirs, notebooks, contents |
|
100 | return dirs, notebooks, contents | |
103 |
|
101 | |||
104 | def list_contents(self, content_path): |
|
102 | def list_contents(self, content_path): | |
105 | """List all contents in the named path.""" |
|
103 | """List all contents in the named path.""" | |
106 | dir_names, notebook_names, content_names = self.get_content_names(content_path) |
|
104 | dir_names, notebook_names, content_names = self.get_content_names(content_path) | |
107 | content_mapping = [] |
|
105 | content_mapping = [] | |
108 | for name in dir_names: |
|
106 | for name in dir_names: | |
109 | model = self.content_model(name, content_path, type='dir') |
|
107 | model = self.content_model(name, content_path, type='dir') | |
110 | content_mapping.append(model) |
|
108 | content_mapping.append(model) | |
111 | for name in content_names: |
|
109 | for name in content_names: | |
112 | model = self.content_model(name, content_path, type='file') |
|
110 | model = self.content_model(name, content_path, type='file') | |
113 | content_mapping.append(model) |
|
111 | content_mapping.append(model) | |
114 | for name in notebook_names: |
|
112 | for name in notebook_names: | |
115 | model = self.content_model(name, content_path, type='notebook') |
|
113 | model = self.content_model(name, content_path, type='notebook') | |
116 | content_mapping.append(model) |
|
114 | content_mapping.append(model) | |
117 | return content_mapping |
|
115 | return content_mapping | |
118 |
|
116 | |||
119 | def get_path_by_name(self, name, content_path): |
|
117 | def get_path_by_name(self, name, content_path): | |
120 | """Return a full path to content""" |
|
118 | """Return a full path to content""" | |
121 | path = os.path.join(self.content_dir, content_path, name) |
|
119 | path = os.path.join(self.content_dir, content_path, name) | |
122 | return path |
|
120 | return path | |
123 |
|
121 | |||
124 | def content_info(self, name, content_path): |
|
122 | def content_info(self, name, content_path): | |
125 | """Read the content of a named file""" |
|
123 | """Read the content of a named file""" | |
126 | file_type = os.path.splitext(os.path.basename(name))[1] |
|
124 | file_type = os.path.splitext(os.path.basename(name))[1] | |
127 | full_path = self.get_path_by_name(name, content_path) |
|
125 | full_path = self.get_path_by_name(name, content_path) | |
128 | info = os.stat(full_path) |
|
126 | info = os.stat(full_path) | |
129 | size = info.st_size |
|
127 | size = info.st_size | |
130 | last_modified = tz.utcfromtimestamp(info.st_mtime) |
|
128 | last_modified = tz.utcfromtimestamp(info.st_mtime) | |
131 | return last_modified, file_type, size |
|
129 | return last_modified, file_type, size | |
132 |
|
130 | |||
133 | def content_model(self, name, content_path, type=None): |
|
131 | def content_model(self, name, content_path, type=None): | |
134 | """Create a dict standard model for any file (other than notebooks)""" |
|
132 | """Create a dict standard model for any file (other than notebooks)""" | |
135 | last_modified, file_type, size = self.content_info(name, content_path) |
|
133 | last_modified, file_type, size = self.content_info(name, content_path) | |
136 | model = {"name": name, |
|
134 | model = {"name": name, | |
137 | "path": content_path, |
|
135 | "path": content_path, | |
138 | "type": type, |
|
136 | "type": type, | |
139 | "MIME-type": "", |
|
137 | "MIME-type": "", | |
140 | "last_modified (UTC)": last_modified.ctime(), |
|
138 | "last_modified (UTC)": last_modified.ctime(), | |
141 | "size": size} |
|
139 | "size": size} | |
142 | return model |
|
140 | return model | |
143 |
|
141 | |||
144 |
def |
|
142 | def create_folder(self, name, path): | |
145 | """Delete a file""" |
|
143 | """ | |
146 | os.unlink(os.path.join(self.content_dir, content_path)) |
|
144 | Parameters | |
|
145 | ---------- | |||
|
146 | name : str | |||
|
147 | The name you want give to the folder thats created. | |||
|
148 | If this is None, it will assign an incremented name | |||
|
149 | 'new_folder'. | |||
|
150 | path : str | |||
|
151 | The relative location to put the created folder. | |||
|
152 | ||||
|
153 | Returns | |||
|
154 | ------- | |||
|
155 | The name of the created folder. | |||
|
156 | """ | |||
|
157 | if name is None: | |||
|
158 | name = self.increment_filename("new_folder", path) | |||
|
159 | new_path = self.get_os_path(name, path) | |||
|
160 | # Raise an error if the file exists | |||
|
161 | try: | |||
|
162 | os.makedirs(new_path) | |||
|
163 | except OSError as e: | |||
|
164 | if e.errno == errno.EEXIST: | |||
|
165 | raise web.HTTPError(409, u'Directory already exists.') | |||
|
166 | elif e.errno == errno.EACCES: | |||
|
167 | raise web.HTTPError(403, u'Create dir: permission denied.') | |||
|
168 | else: | |||
|
169 | raise web.HTTPError(400, str(e)) | |||
|
170 | return name | |||
147 |
|
171 | |||
|
172 | def increment_filename(self, basename, content_path='/'): | |||
|
173 | """Return a non-used filename of the form basename<int>. | |||
|
174 | ||||
|
175 | This searches through the filenames (basename0, basename1, ...) | |||
|
176 | until is find one that is not already being used. It is used to | |||
|
177 | create Untitled and Copy names that are unique. | |||
|
178 | """ | |||
|
179 | i = 0 | |||
|
180 | while True: | |||
|
181 | name = u'%s%i' % (basename,i) | |||
|
182 | path = self.get_os_path(name, content_path) | |||
|
183 | if not os.path.isdir(path): | |||
|
184 | break | |||
|
185 | else: | |||
|
186 | i = i+1 | |||
|
187 | return name | |||
|
188 | ||||
|
189 | def delete_content(self, name=None, content_path='/'): | |||
|
190 | """Delete a file or folder in the named location. | |||
|
191 | Raises an error if the named file/folder doesn't exist | |||
|
192 | """ | |||
|
193 | path = self.get_os_path(name, content_path) | |||
|
194 | if path != self.content_dir: | |||
|
195 | try: | |||
|
196 | shutil.rmtree(path) | |||
|
197 | except OSError as e: | |||
|
198 | if e.errno == errno.ENOENT: | |||
|
199 | raise web.HTTPError(404, u'Directory or file does not exist.') | |||
|
200 | else: | |||
|
201 | raise web.HTTPError(400, str(e)) | |||
|
202 | else: | |||
|
203 | raise web.HTTPError(403, "Cannot delete root directory where notebook server lives.") |
@@ -1,111 +1,115 b'' | |||||
1 | """Tests for the content manager.""" |
|
1 | """Tests for the content manager.""" | |
2 |
|
2 | |||
3 | import os |
|
3 | import os | |
4 | from unittest import TestCase |
|
4 | from unittest import TestCase | |
5 | from tempfile import NamedTemporaryFile |
|
5 | from tempfile import NamedTemporaryFile | |
6 |
|
6 | |||
7 | from IPython.utils.tempdir import TemporaryDirectory |
|
7 | from IPython.utils.tempdir import TemporaryDirectory | |
8 | from IPython.utils.traitlets import TraitError |
|
8 | from IPython.utils.traitlets import TraitError | |
9 |
|
9 | |||
10 | from ..contentmanager import ContentManager |
|
10 | from ..contentmanager import ContentManager | |
11 |
|
11 | |||
12 |
|
12 | |||
13 | class TestContentManager(TestCase): |
|
13 | class TestContentManager(TestCase): | |
14 |
|
14 | |||
15 | def test_new_folder(self): |
|
15 | def test_new_folder(self): | |
16 | with TemporaryDirectory() as td: |
|
16 | with TemporaryDirectory() as td: | |
17 | # Test that a new directory/folder is created |
|
17 | # Test that a new directory/folder is created | |
18 | cm = ContentManager(content_dir=td) |
|
18 | cm = ContentManager(content_dir=td) | |
19 | name = cm.new_folder(None, '/') |
|
19 | name = cm.new_folder(None, '/') | |
20 | path = cm.get_os_path(name, '/') |
|
20 | path = cm.get_os_path(name, '/') | |
21 | self.assertTrue(os.path.isdir(path)) |
|
21 | self.assertTrue(os.path.isdir(path)) | |
22 |
|
22 | |||
23 | # Test that a new directory is created with |
|
23 | # Test that a new directory is created with | |
24 | # the name given. |
|
24 | # the name given. | |
25 | name = cm.new_folder('foo') |
|
25 | name = cm.new_folder('foo') | |
26 | path = cm.get_os_path(name) |
|
26 | path = cm.get_os_path(name) | |
27 | self.assertTrue(os.path.isdir(path)) |
|
27 | self.assertTrue(os.path.isdir(path)) | |
28 |
|
28 | |||
29 | # Test that a new directory/folder is created |
|
29 | # Test that a new directory/folder is created | |
30 | # in the '/foo' subdirectory |
|
30 | # in the '/foo' subdirectory | |
31 | name1 = cm.new_folder(None, '/foo/') |
|
31 | name1 = cm.new_folder(None, '/foo/') | |
32 | path1 = cm.get_os_path(name1, '/foo/') |
|
32 | path1 = cm.get_os_path(name1, '/foo/') | |
33 | self.assertTrue(os.path.isdir(path1)) |
|
33 | self.assertTrue(os.path.isdir(path1)) | |
34 |
|
34 | |||
35 | # make another file and make sure it incremented |
|
35 | # make another file and make sure it incremented | |
36 | # the name and does not write over another file. |
|
36 | # the name and does not write over another file. | |
37 | name2 = cm.new_folder(None, '/foo/') |
|
37 | name2 = cm.new_folder(None, '/foo/') | |
38 | path2 = cm.get_os_path(name, '/foo/') |
|
38 | path2 = cm.get_os_path(name, '/foo/') | |
39 | self.assertEqual(name2, 'new_folder1') |
|
39 | self.assertEqual(name2, 'new_folder1') | |
40 |
|
40 | |||
41 | # Test that an HTTP Error is raised when the user |
|
41 | # Test that an HTTP Error is raised when the user | |
42 | # tries to create a new folder with a name that |
|
42 | # tries to create a new folder with a name that | |
43 | # already exists |
|
43 | # already exists | |
44 | bad_name = 'new_folder1' |
|
44 | bad_name = 'new_folder1' | |
45 | self.assertRaises(HTTPError, cm.new_folder, name=bad_name, path='/foo/') |
|
45 | self.assertRaises(HTTPError, cm.new_folder, name=bad_name, path='/foo/') | |
46 |
|
46 | |||
47 | def test_delete_folder(self): |
|
47 | def test_delete_folder(self): | |
48 | with TemporaryDirectory() as td: |
|
48 | with TemporaryDirectory() as td: | |
49 | # Create a folder |
|
49 | # Create a folder | |
50 | cm = ContentManager(content_dir=td) |
|
50 | cm = ContentManager(content_dir=td) | |
51 | name = cm.new_folder('test_folder', '/') |
|
51 | name = cm.new_folder('test_folder', '/') | |
52 | path = cm.get_os_path(name, '/') |
|
52 | path = cm.get_os_path(name, '/') | |
53 |
|
53 | |||
54 | # Raise an exception when trying to delete a |
|
54 | # Raise an exception when trying to delete a | |
55 | # folder that does not exist. |
|
55 | # folder that does not exist. | |
56 |
self.assertRaises( |
|
56 | self.assertRaises(HTTPError, cm.delete_content, name='non_existing_folder', content_path='/') | |
57 |
|
57 | |||
58 | # Create a subfolder in the folder created above. |
|
58 | # Create a subfolder in the folder created above. | |
59 | # *Recall 'name' = 'test_folder' (the new path for |
|
59 | # *Recall 'name' = 'test_folder' (the new path for | |
60 | # subfolder) |
|
60 | # subfolder) | |
61 | name01 = cm.new_folder(None, name) |
|
61 | name01 = cm.new_folder(None, name) | |
62 | path01 = cm.get_os_path(name01, name) |
|
62 | path01 = cm.get_os_path(name01, name) | |
63 | # Try to delete a subfolder that does not exist. |
|
63 | # Try to delete a subfolder that does not exist. | |
64 |
self.assertRaises( |
|
64 | self.assertRaises(HTTPError, cm.delete_content, name='non_existing_folder', content_path='/') | |
65 | # Delete the created subfolder |
|
65 | # Delete the created subfolder | |
66 | cm.delete_content(name01, name) |
|
66 | cm.delete_content(name01, name) | |
67 | self.assertFalse(os.path.isdir(path01)) |
|
67 | self.assertFalse(os.path.isdir(path01)) | |
68 |
|
68 | |||
69 | # Delete the created folder |
|
69 | # Delete the created folder | |
70 | cm.delete_content(name, '/') |
|
70 | cm.delete_content(name, '/') | |
71 | self.assertFalse(os.path.isdir(path)) |
|
71 | self.assertFalse(os.path.isdir(path)) | |
72 |
|
72 | |||
|
73 | self.assertRaises(HTTPError, cm.delete_content, name=None, content_path='/') | |||
|
74 | self.assertRaises(HTTPError, cm.delete_content, name='/', content_path='/') | |||
|
75 | ||||
|
76 | ||||
73 | def test_get_content_names(self): |
|
77 | def test_get_content_names(self): | |
74 | with TemporaryDirectory() as td: |
|
78 | with TemporaryDirectory() as td: | |
75 | # Create a few folders and subfolders |
|
79 | # Create a few folders and subfolders | |
76 | cm = ContentManager(content_dir=td) |
|
80 | cm = ContentManager(content_dir=td) | |
77 | name1 = cm.new_folder('fold1', '/') |
|
81 | name1 = cm.new_folder('fold1', '/') | |
78 | name2 = cm.new_folder('fold2', '/') |
|
82 | name2 = cm.new_folder('fold2', '/') | |
79 | name3 = cm.new_folder('fold3', '/') |
|
83 | name3 = cm.new_folder('fold3', '/') | |
80 | name01 = cm.new_folder('fold01', 'fold1') |
|
84 | name01 = cm.new_folder('fold01', 'fold1') | |
81 | name02 = cm.new_folder('fold02', 'fold1') |
|
85 | name02 = cm.new_folder('fold02', 'fold1') | |
82 | name03 = cm.new_folder('fold03', 'fold1') |
|
86 | name03 = cm.new_folder('fold03', 'fold1') | |
83 |
|
87 | |||
84 | # List the names in the root folder |
|
88 | # List the names in the root folder | |
85 | names = cm.get_content_names('/') |
|
89 | names = cm.get_content_names('/') | |
86 | expected = ['fold1', 'fold2', 'fold3'] |
|
90 | expected = ['fold1', 'fold2', 'fold3'] | |
87 | self.assertEqual(set(names), set(expected)) |
|
91 | self.assertEqual(set(names), set(expected)) | |
88 |
|
92 | |||
89 | # List the names in the subfolder 'fold1'. |
|
93 | # List the names in the subfolder 'fold1'. | |
90 | names = cm.get_content_names('fold1') |
|
94 | names = cm.get_content_names('fold1') | |
91 | expected = ['fold01', 'fold02', 'fold03'] |
|
95 | expected = ['fold01', 'fold02', 'fold03'] | |
92 | self.assertEqual(set(names), set(expected)) |
|
96 | self.assertEqual(set(names), set(expected)) | |
93 |
|
97 | |||
94 | def test_content_model(self): |
|
98 | def test_content_model(self): | |
95 | with TemporaryDirectory() as td: |
|
99 | with TemporaryDirectory() as td: | |
96 | # Create a few folders and subfolders |
|
100 | # Create a few folders and subfolders | |
97 | cm = ContentManager(content_dir=td) |
|
101 | cm = ContentManager(content_dir=td) | |
98 | name1 = cm.new_folder('fold1', '/') |
|
102 | name1 = cm.new_folder('fold1', '/') | |
99 | name2 = cm.new_folder('fold2', '/') |
|
103 | name2 = cm.new_folder('fold2', '/') | |
100 | name01 = cm.new_folder('fold01', 'fold1') |
|
104 | name01 = cm.new_folder('fold01', 'fold1') | |
101 | name02 = cm.new_folder('fold02', 'fold1') |
|
105 | name02 = cm.new_folder('fold02', 'fold1') | |
102 |
|
106 | |||
103 | # Check to see if the correct model and list of |
|
107 | # Check to see if the correct model and list of | |
104 | # model dicts are returned for root directory |
|
108 | # model dicts are returned for root directory | |
105 | # and subdirectory. |
|
109 | # and subdirectory. | |
106 | contents = cm.list_contents('/') |
|
110 | contents = cm.list_contents('/') | |
107 | contents1 = cm.list_contents('fold1') |
|
111 | contents1 = cm.list_contents('fold1') | |
108 | self.assertEqual(type(contents), type(list())) |
|
112 | self.assertEqual(type(contents), type(list())) | |
109 | self.assertEqual(type(contents[0]), type(dict())) |
|
113 | self.assertEqual(type(contents[0]), type(dict())) | |
110 | self.assertEqual(contents[0]['path'], '/') |
|
114 | self.assertEqual(contents[0]['path'], '/') | |
111 | self.assertEqual(contents1[0]['path'], 'fold1') |
|
115 | self.assertEqual(contents1[0]['path'], 'fold1') |
General Comments 0
You need to be logged in to leave comments.
Login now