Merge lp:~rvb/maas/file-delete into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 1435
Proposed branch: lp:~rvb/maas/file-delete
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 173 lines (+92/-0)
3 files modified
src/maasserver/api.py (+39/-0)
src/maasserver/tests/test_api.py (+50/-0)
src/maasserver/urls_api.py (+3/-0)
To merge this branch: bzr merge lp:~rvb/maas/file-delete
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+148381@code.launchpad.net

Commit message

Add support for listing files (FileStorage objects).

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good, with only one question.

[1]

> +    url(r'files/(?P<filename>[^/]+)/$', file_handler, name='file_handler'),

It seems fishy to refer to a filename with a URL that looks like a
directory.

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

> Looks good, with only one question.
>
>
> [1]
>
> > +    url(r'files/(?P<filename>[^/]+)/$', file_handler, name='file_handler'),
>
> It seems fishy to refer to a filename with a URL that looks like a
> directory.

I don't understand the question… I suppose you're referring to the fact that the filename is used as an element of the path… but what do you suggest instead?

Revision history for this message
Gavin Panella (allenap) wrote :

> I don't understand the question… I suppose you're referring to the fact that the filename is used as an element of the path… but what do you suggest instead?

It's that there's a forward slash after the filename.

Revision history for this message
Raphaël Badin (rvb) wrote :

> > I don't understand the question… I suppose you're referring to the fact that
> the filename is used as an element of the path… but what do you suggest
> instead?
>
> It's that there's a forward slash after the filename.

Oh. That.

The fact that we have a final slash is an option in Django we choose to turn on to make urls unique (instead of having http://server.com/aa and http://server.com/aa/, we only have http://server.com/aa/ and http://server.com/aa redirects to http://server.com/aa/).

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Question: what about the multiple users bug? ie. every user can see all the
files. This is bad :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api.py'
2--- src/maasserver/api.py 2013-01-30 15:32:09 +0000
3+++ src/maasserver/api.py 2013-02-14 08:15:27 +0000
4@@ -63,6 +63,7 @@
5 "CommissioningScriptHandler",
6 "CommissioningScriptsHandler",
7 "CommissioningResultsHandler",
8+ "FileHandler",
9 "FilesHandler",
10 "get_oauth_token",
11 "MaasHandler",
12@@ -108,6 +109,7 @@
13 render_to_response,
14 )
15 from django.template import RequestContext
16+from django.utils.http import urlquote_plus
17 from docutils import core
18 from formencode import validators
19 from maasserver.api_support import (
20@@ -808,6 +810,26 @@
21 return ('files_handler', [])
22
23
24+DISPLAYED_FILES_FIELDS = ('filename', )
25+
26+
27+class FileHandler(OperationsHandler):
28+ """Manage a FileStorage object.
29+
30+ The file is identified by its filename.
31+ """
32+ model = FileStorage
33+ fields = DISPLAYED_FILES_FIELDS
34+ create = read = update = delete = None
35+
36+ @classmethod
37+ def resource_uri(cls, stored_file=None):
38+ filename = "filename"
39+ if stored_file is not None:
40+ filename = urlquote_plus(stored_file.filename)
41+ return ('file_handler', (filename, ))
42+
43+
44 class FilesHandler(OperationsHandler):
45 """File management operations."""
46 create = read = update = delete = None
47@@ -840,6 +862,23 @@
48 FileStorage.objects.save_file(filename, uploaded_file)
49 return HttpResponse('', status=httplib.CREATED)
50
51+ @operation(idempotent=True)
52+ def list(self, request):
53+ """List the files from the file storage.
54+
55+ The returned files are ordered by file name and the content is
56+ excluded.
57+
58+ :param prefix: Optional prefix used to filter out the returned files.
59+ :type prefix: string
60+ """
61+ prefix = request.GET.get("prefix", None)
62+ files = FileStorage.objects.all()
63+ if prefix is not None:
64+ files = files.filter(filename__startswith=prefix)
65+ files = files.order_by('filename')
66+ return files
67+
68 @classmethod
69 def resource_uri(cls, *args, **kwargs):
70 return ('files_handler', [])
71
72=== modified file 'src/maasserver/tests/test_api.py'
73--- src/maasserver/tests/test_api.py 2013-01-30 10:24:06 +0000
74+++ src/maasserver/tests/test_api.py 2013-02-14 08:15:27 +0000
75@@ -45,6 +45,7 @@
76 )
77 from django.http import QueryDict
78 from django.test.client import RequestFactory
79+from django.utils.http import urlquote_plus
80 from fixtures import (
81 EnvironmentVariableFixture,
82 Fixture,
83@@ -2626,6 +2627,13 @@
84 self.assertEqual(httplib.OK, response.status_code)
85 self.assertEqual(b"give me rope", response.content)
86
87+ def test_anon_cannot_list_files(self):
88+ factory.make_file_storage(
89+ filename="filename", content=b"test content")
90+ response = self.make_API_GET_request("list")
91+ # The 'list' operation is not available to anon users.
92+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
93+
94
95 class FileStorageAPITest(FileStorageAPITestMixin, APITestCase):
96
97@@ -2702,6 +2710,48 @@
98 self.assertIn('text/plain', response['Content-Type'])
99 self.assertEqual("File not found", response.content)
100
101+ def test_list_files_returns_ordered_list(self):
102+ filenames = ["myfiles/a", "myfiles/z", "myfiles/b"]
103+ for filename in filenames:
104+ factory.make_file_storage(
105+ filename=filename, content=b"test content")
106+ response = self.make_API_GET_request("list")
107+ self.assertEqual(httplib.OK, response.status_code)
108+ parsed_results = json.loads(response.content)
109+ filenames = [result['filename'] for result in parsed_results]
110+ self.assertEqual(sorted(filenames), filenames)
111+
112+ def test_list_files_lists_files_with_prefix(self):
113+ filenames_with_prefix = ["prefix-file1", "prefix-file2"]
114+ filenames = filenames_with_prefix + ["otherfile", "otherfile2"]
115+ for filename in filenames:
116+ factory.make_file_storage(
117+ filename=filename, content=b"test content")
118+ response = self.client.get(
119+ self.get_uri('files/'), {"op": "list", "prefix": "prefix-"})
120+ self.assertEqual(httplib.OK, response.status_code)
121+ parsed_results = json.loads(response.content)
122+ filenames = [result['filename'] for result in parsed_results]
123+ self.assertItemsEqual(filenames_with_prefix, filenames)
124+
125+ def test_list_files_does_not_include_file_content(self):
126+ factory.make_file_storage(
127+ filename="filename", content=b"test content")
128+ response = self.make_API_GET_request("list")
129+ parsed_results = json.loads(response.content)
130+ self.assertNotIn('content', parsed_results[0].keys())
131+
132+ def test_files_resource_uri_are_url_escaped(self):
133+ filename = "&*!c/%//filename/"
134+ factory.make_file_storage(
135+ filename=filename, content=b"test content")
136+ response = self.make_API_GET_request("list")
137+ parsed_results = json.loads(response.content)
138+ resource_uri = parsed_results[0]['resource_uri']
139+ resource_uri_elements = resource_uri.split('/')
140+ # The url-escaped name of the file is part of the resource uri.
141+ self.assertIn(urlquote_plus(filename), resource_uri_elements)
142+
143
144 class TestTagAPI(APITestCase):
145 """Tests for /api/1.0/tags/<tagname>/."""
146
147=== modified file 'src/maasserver/urls_api.py'
148--- src/maasserver/urls_api.py 2012-12-20 14:42:17 +0000
149+++ src/maasserver/urls_api.py 2013-02-14 08:15:27 +0000
150@@ -24,6 +24,7 @@
151 CommissioningScriptHandler,
152 CommissioningScriptsHandler,
153 describe,
154+ FileHandler,
155 FilesHandler,
156 MaasHandler,
157 NodeGroupHandler,
158@@ -47,6 +48,7 @@
159
160 account_handler = RestrictedResource(AccountHandler, authentication=api_auth)
161 files_handler = RestrictedResource(FilesHandler, authentication=api_auth)
162+file_handler = RestrictedResource(FileHandler, authentication=api_auth)
163 node_handler = RestrictedResource(NodeHandler, authentication=api_auth)
164 nodes_handler = RestrictedResource(NodesHandler, authentication=api_auth)
165 node_mac_handler = RestrictedResource(NodeMacHandler, authentication=api_auth)
166@@ -105,6 +107,7 @@
167 url(r'nodegroups/(?P<uuid>[^/]+)/interfaces/(?P<interface>[^/]+)/$',
168 nodegroupinterface_handler, name='nodegroupinterface_handler'),
169 url(r'files/$', files_handler, name='files_handler'),
170+ url(r'files/(?P<filename>[^/]+)/$', file_handler, name='file_handler'),
171 url(r'account/$', account_handler, name='account_handler'),
172 url(r'boot-images/$', boot_images_handler, name='boot_images_handler'),
173 url(r'tags/(?P<name>[\w\-]+)/$', tag_handler, name='tag_handler'),