Merge lp:~rvb/maas/bug-1123986-api-fix into lp:~maas-committers/maas/trunk
- bug-1123986-api-fix
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Raphaël Badin | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 1441 | ||||
Proposed branch: | lp:~rvb/maas/bug-1123986-api-fix | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Prerequisite: | lp:~rvb/maas/bug-1123986-db-filestorage-key | ||||
Diff against target: |
462 lines (+200/-61) 5 files modified
src/maasserver/api.py (+37/-19) src/maasserver/models/filestorage.py (+4/-4) src/maasserver/testing/factory.py (+2/-2) src/maasserver/tests/test_api.py (+152/-33) src/maasserver/tests/test_filestorage.py (+5/-3) |
||||
To merge this branch: | bzr merge lp:~rvb/maas/bug-1123986-api-fix | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+149059@code.launchpad.net |
Commit message
Adapt the file storage API to the 'owner' field on FileStorage objects.
Description of the change
This branch adapts the file storage API to the new 'owner' field.
Each user is now virtually isolated in his own namespace when uploading/reading file from the file storage API.
Since we have to cope with existing installations there are a few caveats:
- when a user gets a file from /files/<filename>/, if no owned-file can be found, the API will try to return a non-owned file with the same name
- the anonymous API will always return the *last* uploaded file with the given name.
Raphaël Badin (rvb) wrote : | # |
> Looks good, but I have a couple of questions.
>
>
> [1]
>
> -def get_file(handler, request):
> +def get_file_
> """Get a named file from the file storage.
>
> :param filename: The exact name of the file you want to get.
> :type filename: string
> :return: The file is returned in the response content.
> """
> - filename = request.
> - if not filename:
> - raise MAASAPIBadReque
> + filename = get_mandatory_
> try:
> - db_file = FileStorage.
> + db_file = FileStorage.
>
> Shouldn't this restrict itself to files without an owner?
No, imagine you upgrade your juju client in an existing env with services deployed: next time the juju client will upload a file (the file will be storage with an owner), the node might want to read it with the URL it has stored.
> [2]
>
> def delete(self, request, filename):
> """Delete a FileStorage object."""
> - stored_file = get_object_
> + stored_file = get_object_or_404(
> + FileStorage, filename=filename, owner=request.user)
> stored_
>
> Should this fall back to deleting files without owner when an
> owned file is not found?
The python provider does not use this. So the only provider which will use this is the Go provider, which will always set an owner.
Raphaël Badin (rvb) wrote : | # |
> [2]
>
> def delete(self, request, filename):
> """Delete a FileStorage object."""
> - stored_file = get_object_
> + stored_file = get_object_or_404(
> + FileStorage, filename=filename, owner=request.user)
> stored_
>
> Should this fall back to deleting files without owner when an
> owned file is not found?
The response I made above is still valid but I think we've got a choice to make here:
a) either we focus on supporting the old provider: in this case, as I said, the version of delete is all right because the provider does not use it. But this means we also can remove the compatibility layer I added in the handler of "GET /api/1.
b) or we try to have a general behavior that is coherent when upgrading (independently from what the provider uses). In this case, as you pointed out, we need to change delete() to delete files without an owner.
I'm tempted to go for a) because:
- the filestorage stuff is really just here to be used by Juju
- adding a compatibility layer adds yet another layer of complexity to the code
Gavin Panella (allenap) wrote : | # |
> > Shouldn't this restrict itself to files without an owner?
>
> No, imagine you upgrade your juju client in an existing env with services
> deployed: next time the juju client will upload a file (the file will be
> storage with an owner), the node might want to read it with the URL it has
> stored.
But might this code get the file that belongs to another user? I'm
thinking it should look for a file either owned by the user *or*
unowned. I suspect I'm not seeing something in the implementation,
which could indicate that it could be structured better, or that I'm
stupid :)
> > Should this fall back to deleting files without owner when an
> > owned file is not found?
>
> The python provider does not use this. So the only provider which will use
> this is the Go provider, which will always set an owner.
The API ought to have a way to delete unowned files, even if neither
Juju provider needs it.
Raphaël Badin (rvb) wrote : | # |
> But might this code get the file that belongs to another user?
Yes, it will definitely do that if you're still using an old version of juju … and that's exactly the situation we have now already (basically the goal is to: make the situation not worse if you use an old juju, fix the situation if you use a new juju… I'm afraid that's the best we can do)
Raphaël Badin (rvb) wrote : | # |
> The API ought to have a way to delete unowned files, even if neither
> Juju provider needs it.
Just like we have a consensus on this, done.
Raphaël Badin (rvb) wrote : | # |
This is now all fixed. I get rid of the whole 'values' non-sense in the follow-up branch, don't worry.
Gavin Panella (allenap) : | # |
Preview Diff
1 | === modified file 'src/maasserver/api.py' |
2 | --- src/maasserver/api.py 2013-02-14 14:25:39 +0000 |
3 | +++ src/maasserver/api.py 2013-02-22 09:28:21 +0000 |
4 | @@ -777,18 +777,16 @@ |
5 | return ('node_mac_handler', [node_system_id, mac_address]) |
6 | |
7 | |
8 | -def get_file(handler, request): |
9 | +def get_file_by_name(handler, request): |
10 | """Get a named file from the file storage. |
11 | |
12 | :param filename: The exact name of the file you want to get. |
13 | :type filename: string |
14 | :return: The file is returned in the response content. |
15 | """ |
16 | - filename = request.GET.get("filename", None) |
17 | - if not filename: |
18 | - raise MAASAPIBadRequest("Filename not supplied") |
19 | + filename = get_mandatory_param(request.GET, 'filename') |
20 | try: |
21 | - db_file = FileStorage.objects.get(filename=filename) |
22 | + db_file = FileStorage.objects.filter(filename=filename).latest('id') |
23 | except FileStorage.DoesNotExist: |
24 | raise MAASAPINotFound("File not found") |
25 | return HttpResponse(db_file.content, status=httplib.OK) |
26 | @@ -808,7 +806,7 @@ |
27 | """ |
28 | create = read = update = delete = None |
29 | |
30 | - get = operation(idempotent=True, exported_as='get')(get_file) |
31 | + get = operation(idempotent=True, exported_as='get')(get_file_by_name) |
32 | |
33 | @classmethod |
34 | def resource_uri(cls, *args, **kwargs): |
35 | @@ -823,10 +821,35 @@ |
36 | DISPLAYED_FILES_FIELDS_OBJECT = DISPLAYED_FILES_FIELDS + ('content', ) |
37 | |
38 | |
39 | +def get_owned_file_or_404(filename, user, values=False): |
40 | + """Return the file named 'filename' owned by 'user'. |
41 | + |
42 | + If there is no such file, try getting the corresponding unowned file |
43 | + to be compatible with old versions of MAAS where all the files where |
44 | + unowned. |
45 | + """ |
46 | + stored_files = FileStorage.objects.filter( |
47 | + filename=filename, owner=user) |
48 | + # filename and owner are 'unique together', we can have either 0 or 1 |
49 | + # objects in 'stored_files'. |
50 | + if len(stored_files) == 0: |
51 | + # In order to support upgrading from installations where the notion |
52 | + # of a file owner was not yet implemented, return non-owned files |
53 | + # with this filename at a last resort option. |
54 | + stored_files = FileStorage.objects.filter( |
55 | + filename=filename, owner=None) |
56 | + if len(stored_files) == 0: |
57 | + raise Http404 |
58 | + if values: |
59 | + return stored_files.values()[0] |
60 | + else: |
61 | + return stored_files[0] |
62 | + |
63 | + |
64 | class FileHandler(OperationsHandler): |
65 | """Manage a FileStorage object. |
66 | |
67 | - The file is identified by its filename. |
68 | + The file is identified by its filename and owner. |
69 | """ |
70 | model = FileStorage |
71 | fields = DISPLAYED_FILES_FIELDS |
72 | @@ -836,14 +859,8 @@ |
73 | """GET a FileStorage object as a json object. |
74 | |
75 | The 'content' of the file is base64-encoded.""" |
76 | - # 'content' is a BinaryField, its 'value' is a base64-encoded version |
77 | - # of its value. |
78 | - stored_files = FileStorage.objects.filter(filename=filename).values() |
79 | - # filename is a 'unique' field, we can have either 0 or 1 objects in |
80 | - # 'stored_files'. |
81 | - if len(stored_files) == 0: |
82 | - raise Http404 |
83 | - stored_file = stored_files[0] |
84 | + stored_file = get_owned_file_or_404( |
85 | + filename=filename, user=request.user, values=True) |
86 | # Emit the json for this object manually because, no matter what the |
87 | # piston documentation says, once an type is associated with a list |
88 | # of fields by piston's typemapper mechanism, there is no way to |
89 | @@ -858,7 +875,8 @@ |
90 | @operation(idempotent=False) |
91 | def delete(self, request, filename): |
92 | """Delete a FileStorage object.""" |
93 | - stored_file = get_object_or_404(FileStorage, filename=filename) |
94 | + stored_file = get_owned_file_or_404( |
95 | + filename=filename, user=request.user) |
96 | stored_file.delete() |
97 | return rc.DELETED |
98 | |
99 | @@ -875,7 +893,7 @@ |
100 | create = read = update = delete = None |
101 | anonymous = AnonFilesHandler |
102 | |
103 | - get = operation(idempotent=True, exported_as='get')(get_file) |
104 | + get = operation(idempotent=True, exported_as='get')(get_file_by_name) |
105 | |
106 | @operation(idempotent=False) |
107 | def add(self, request): |
108 | @@ -899,7 +917,7 @@ |
109 | # As per the comment in FileStorage, this ought to deal in |
110 | # chunks instead of reading the file into memory, but large |
111 | # files are not expected. |
112 | - FileStorage.objects.save_file(filename, uploaded_file) |
113 | + FileStorage.objects.save_file(filename, uploaded_file, request.user) |
114 | return HttpResponse('', status=httplib.CREATED) |
115 | |
116 | @operation(idempotent=True) |
117 | @@ -913,7 +931,7 @@ |
118 | :type prefix: string |
119 | """ |
120 | prefix = request.GET.get("prefix", None) |
121 | - files = FileStorage.objects.all() |
122 | + files = FileStorage.objects.filter(owner=request.user) |
123 | if prefix is not None: |
124 | files = files.filter(filename__startswith=prefix) |
125 | files = files.order_by('filename') |
126 | |
127 | === modified file 'src/maasserver/models/filestorage.py' |
128 | --- src/maasserver/models/filestorage.py 2013-02-22 09:28:21 +0000 |
129 | +++ src/maasserver/models/filestorage.py 2013-02-22 09:28:21 +0000 |
130 | @@ -47,17 +47,17 @@ |
131 | old file will continue without iterruption. |
132 | """ |
133 | |
134 | - def save_file(self, filename, file_object): |
135 | + def save_file(self, filename, file_object, owner): |
136 | """Save the file to the database. |
137 | |
138 | - If a file of that name already existed, it will be replaced by the |
139 | - new contents. |
140 | + If a file of that name/owner already existed, it will be replaced by |
141 | + the new contents. |
142 | """ |
143 | # This probably ought to read in chunks but large files are |
144 | # not expected. |
145 | content = Bin(file_object.read()) |
146 | storage, created = self.get_or_create( |
147 | - filename=filename, defaults={'content': content}) |
148 | + filename=filename, owner=owner, defaults={'content': content}) |
149 | if not created: |
150 | storage.content = content |
151 | storage.save() |
152 | |
153 | === modified file 'src/maasserver/testing/factory.py' |
154 | --- src/maasserver/testing/factory.py 2012-12-19 12:31:11 +0000 |
155 | +++ src/maasserver/testing/factory.py 2013-02-22 09:28:21 +0000 |
156 | @@ -330,9 +330,9 @@ |
157 | admin.save() |
158 | return admin |
159 | |
160 | - def make_file_storage(self, filename=None, content=None): |
161 | + def make_file_storage(self, filename=None, content=None, owner=None): |
162 | fake_file = self.make_file_upload(filename, content) |
163 | - return FileStorage.objects.save_file(fake_file.name, fake_file) |
164 | + return FileStorage.objects.save_file(fake_file.name, fake_file, owner) |
165 | |
166 | def make_oauth_header(self, **kwargs): |
167 | """Fake an OAuth authorization header. |
168 | |
169 | === modified file 'src/maasserver/tests/test_api.py' |
170 | --- src/maasserver/tests/test_api.py 2013-02-14 12:42:30 +0000 |
171 | +++ src/maasserver/tests/test_api.py 2013-02-22 09:28:21 +0000 |
172 | @@ -46,7 +46,10 @@ |
173 | reverse, |
174 | get_script_prefix, |
175 | ) |
176 | -from django.http import QueryDict |
177 | +from django.http import ( |
178 | + Http404, |
179 | + QueryDict, |
180 | + ) |
181 | from django.test.client import RequestFactory |
182 | from django.utils.http import urlquote_plus |
183 | from fixtures import ( |
184 | @@ -62,6 +65,7 @@ |
185 | DISPLAYED_NODEGROUP_FIELDS, |
186 | extract_constraints, |
187 | find_nodegroup_for_pxeconfig_request, |
188 | + get_owned_file_or_404, |
189 | store_node_power_parameters, |
190 | ) |
191 | from maasserver.enum import ( |
192 | @@ -2621,37 +2625,82 @@ |
193 | return self.client.get(self.get_uri('files/'), params) |
194 | |
195 | |
196 | +class CompatibilityUtilitiesTest(TestCase): |
197 | + |
198 | + def test_get_owned_file_or_404_returns_owned_file(self): |
199 | + user = factory.make_user() |
200 | + filename = factory.make_name('filename') |
201 | + factory.make_file_storage(filename=filename, owner=None) |
202 | + filestorage = factory.make_file_storage(filename=filename, owner=user) |
203 | + |
204 | + self.assertEqual(filestorage, get_owned_file_or_404(filename, user)) |
205 | + |
206 | + def test_get_owned_file_or_404_fallsback_to_nonowned_file(self): |
207 | + user = factory.make_user() |
208 | + filename = factory.make_name('filename') |
209 | + factory.make_file_storage( |
210 | + filename=factory.make_name('filename'), owner=user) |
211 | + filestorage = factory.make_file_storage(filename=filename, owner=None) |
212 | + |
213 | + self.assertEqual(filestorage, get_owned_file_or_404(filename, user)) |
214 | + |
215 | + def test_get_owned_file_or_404_raises_404_if_no_file(self): |
216 | + user = factory.make_user() |
217 | + filename = factory.make_name('filename') |
218 | + factory.make_file_storage( |
219 | + filename=factory.make_name('filename'), owner=user) |
220 | + factory.make_file_storage( |
221 | + filename=filename, owner=factory.make_user()) |
222 | + |
223 | + self.assertRaises(Http404, get_owned_file_or_404, filename, user) |
224 | + |
225 | + def test_get_owned_file_or_404_returns_values(self): |
226 | + user = factory.make_user() |
227 | + filename = factory.make_name('filename') |
228 | + factory.make_file_storage(filename=filename, owner=None) |
229 | + factory.make_file_storage(filename=filename, owner=user) |
230 | + |
231 | + values = FileStorage.objects.filter( |
232 | + filename=filename, owner=user).values()[0] |
233 | + self.assertEqual( |
234 | + values, get_owned_file_or_404(filename, user, values=True)) |
235 | + |
236 | + |
237 | class AnonymousFileStorageAPITest(FileStorageAPITestMixin, AnonAPITestCase): |
238 | |
239 | def test_get_works_anonymously(self): |
240 | - factory.make_file_storage( |
241 | - filename="foofilers", content=b"give me rope") |
242 | - response = self.make_API_GET_request("get", "foofilers") |
243 | - |
244 | - self.assertEqual(httplib.OK, response.status_code) |
245 | - self.assertEqual(b"give me rope", response.content) |
246 | + storage = factory.make_file_storage() |
247 | + response = self.make_API_GET_request("get", storage.filename) |
248 | + |
249 | + self.assertEqual(httplib.OK, response.status_code) |
250 | + self.assertEqual(storage.content, response.content) |
251 | + |
252 | + def test_get_fetches_the_most_recent_file(self): |
253 | + filename = factory.make_name('file') |
254 | + factory.make_file_storage(filename=filename, owner=factory.make_user()) |
255 | + storage = factory.make_file_storage( |
256 | + filename=filename, owner=factory.make_user()) |
257 | + response = self.make_API_GET_request("get", filename) |
258 | + |
259 | + self.assertEqual(httplib.OK, response.status_code) |
260 | + self.assertEqual(storage.content, response.content) |
261 | |
262 | def test_anon_cannot_list_files(self): |
263 | - factory.make_file_storage( |
264 | - filename="filename", content=b"test content") |
265 | + factory.make_file_storage() |
266 | response = self.make_API_GET_request("list") |
267 | # The 'list' operation is not available to anon users. |
268 | self.assertEqual(httplib.BAD_REQUEST, response.status_code) |
269 | |
270 | def test_anon_cannot_get_file(self): |
271 | - filename = factory.make_name("file") |
272 | - factory.make_file_storage( |
273 | - filename=filename, content=b"test file content") |
274 | + storage = factory.make_file_storage() |
275 | response = self.client.get( |
276 | - reverse('file_handler', args=[filename])) |
277 | + reverse('file_handler', args=[storage.filename])) |
278 | self.assertEqual(httplib.UNAUTHORIZED, response.status_code) |
279 | |
280 | def test_anon_cannot_delete_file(self): |
281 | - filename = factory.make_name('file') |
282 | - factory.make_file_storage( |
283 | - filename=filename, content=b"test content") |
284 | + storage = factory.make_file_storage() |
285 | response = self.client.delete( |
286 | - reverse('file_handler', args=[filename])) |
287 | + reverse('file_handler', args=[storage.filename])) |
288 | self.assertEqual(httplib.UNAUTHORIZED, response.status_code) |
289 | |
290 | |
291 | @@ -2721,7 +2770,7 @@ |
292 | |
293 | self.assertEqual(httplib.BAD_REQUEST, response.status_code) |
294 | self.assertIn('text/plain', response['Content-Type']) |
295 | - self.assertEqual("Filename not supplied", response.content) |
296 | + self.assertEqual("No provided filename!", response.content) |
297 | |
298 | def test_get_file_fails_with_missing_file(self): |
299 | response = self.make_API_GET_request("get", filename="missingfilename") |
300 | @@ -2734,19 +2783,28 @@ |
301 | filenames = ["myfiles/a", "myfiles/z", "myfiles/b"] |
302 | for filename in filenames: |
303 | factory.make_file_storage( |
304 | - filename=filename, content=b"test content") |
305 | + filename=filename, content=b"test content", |
306 | + owner=self.logged_in_user) |
307 | response = self.make_API_GET_request("list") |
308 | self.assertEqual(httplib.OK, response.status_code) |
309 | parsed_results = json.loads(response.content) |
310 | filenames = [result['filename'] for result in parsed_results] |
311 | self.assertEqual(sorted(filenames), filenames) |
312 | |
313 | + def test_list_files_filters_by_owner(self): |
314 | + factory.make_file_storage(owner=factory.make_user()) |
315 | + response = self.make_API_GET_request("list") |
316 | + self.assertEqual(httplib.OK, response.status_code) |
317 | + parsed_results = json.loads(response.content) |
318 | + self.assertEqual([], parsed_results) |
319 | + |
320 | def test_list_files_lists_files_with_prefix(self): |
321 | filenames_with_prefix = ["prefix-file1", "prefix-file2"] |
322 | filenames = filenames_with_prefix + ["otherfile", "otherfile2"] |
323 | for filename in filenames: |
324 | factory.make_file_storage( |
325 | - filename=filename, content=b"test content") |
326 | + filename=filename, content=b"test content", |
327 | + owner=self.logged_in_user) |
328 | response = self.client.get( |
329 | self.get_uri('files/'), {"op": "list", "prefix": "prefix-"}) |
330 | self.assertEqual(httplib.OK, response.status_code) |
331 | @@ -2756,7 +2814,8 @@ |
332 | |
333 | def test_list_files_does_not_include_file_content(self): |
334 | factory.make_file_storage( |
335 | - filename="filename", content=b"test content") |
336 | + filename="filename", content=b"test content", |
337 | + owner=self.logged_in_user) |
338 | response = self.make_API_GET_request("list") |
339 | parsed_results = json.loads(response.content) |
340 | self.assertNotIn('content', parsed_results[0].keys()) |
341 | @@ -2764,7 +2823,8 @@ |
342 | def test_files_resource_uri_are_url_escaped(self): |
343 | filename = "&*!c/%//filename/" |
344 | factory.make_file_storage( |
345 | - filename=filename, content=b"test content") |
346 | + filename=filename, content=b"test content", |
347 | + owner=self.logged_in_user) |
348 | response = self.make_API_GET_request("list") |
349 | parsed_results = json.loads(response.content) |
350 | resource_uri = parsed_results[0]['resource_uri'] |
351 | @@ -2775,21 +2835,80 @@ |
352 | def test_get_file_returns_file_object_with_content_base64_encoded(self): |
353 | filename = factory.make_name("file") |
354 | content = sample_binary_data |
355 | - factory.make_file_storage(filename=filename, content=content) |
356 | - response = self.client.get( |
357 | - reverse('file_handler', args=[filename])) |
358 | - parsed_result = json.loads(response.content) |
359 | - self.assertEqual( |
360 | - (filename, content), |
361 | - ( |
362 | - parsed_result['filename'], |
363 | - b64decode(parsed_result['content']) |
364 | - )) |
365 | + factory.make_file_storage( |
366 | + filename=filename, content=content, owner=self.logged_in_user) |
367 | + response = self.client.get( |
368 | + reverse('file_handler', args=[filename])) |
369 | + parsed_result = json.loads(response.content) |
370 | + self.assertEqual( |
371 | + (filename, content), |
372 | + ( |
373 | + parsed_result['filename'], |
374 | + b64decode(parsed_result['content']) |
375 | + )) |
376 | + |
377 | + def test_get_file_returns_owned_file(self): |
378 | + # If both an owned file and a non-owned file are present (with the |
379 | + # same name), the owned file is returned. |
380 | + filename = factory.make_name("file") |
381 | + factory.make_file_storage(filename=filename, owner=None) |
382 | + content = sample_binary_data |
383 | + factory.make_file_storage( |
384 | + filename=filename, content=content, owner=self.logged_in_user) |
385 | + response = self.client.get( |
386 | + reverse('file_handler', args=[filename])) |
387 | + parsed_result = json.loads(response.content) |
388 | + self.assertEqual( |
389 | + (filename, content), |
390 | + ( |
391 | + parsed_result['filename'], |
392 | + b64decode(parsed_result['content']) |
393 | + )) |
394 | + |
395 | + def test_get_file_returns_file_object_with_anon_owner(self): |
396 | + # To be compatible with the MAAS provider present in Juju as of |
397 | + # revision 616 (lp:juju), reading a file returns the non-owned files |
398 | + # if no owned file can be found. |
399 | + filename = factory.make_name("file") |
400 | + content = sample_binary_data |
401 | + factory.make_file_storage( |
402 | + filename=filename, content=content, owner=None) |
403 | + response = self.client.get( |
404 | + reverse('file_handler', args=[filename])) |
405 | + parsed_result = json.loads(response.content) |
406 | + self.assertEqual( |
407 | + (filename, content), |
408 | + ( |
409 | + parsed_result['filename'], |
410 | + b64decode(parsed_result['content']) |
411 | + )) |
412 | + |
413 | + def test_delete_filters_by_owner(self): |
414 | + storage = factory.make_file_storage(owner=factory.make_user()) |
415 | + response = self.client.delete( |
416 | + reverse('file_handler', args=[storage.filename])) |
417 | + self.assertEqual(httplib.NOT_FOUND, response.status_code) |
418 | + files = FileStorage.objects.filter(filename=storage.filename) |
419 | + self.assertEqual([storage], list(files)) |
420 | |
421 | def test_delete_file_deletes_file(self): |
422 | filename = factory.make_name('file') |
423 | factory.make_file_storage( |
424 | - filename=filename, content=b"test content") |
425 | + filename=filename, content=b"test content", |
426 | + owner=self.logged_in_user) |
427 | + response = self.client.delete( |
428 | + reverse('file_handler', args=[filename])) |
429 | + self.assertEqual(httplib.NO_CONTENT, response.status_code) |
430 | + files = FileStorage.objects.filter(filename=filename) |
431 | + self.assertEqual([], list(files)) |
432 | + |
433 | + def test_delete_file_deletes_file_with_anon_owner(self): |
434 | + # To be compatible with the MAAS provider present in Juju as of |
435 | + # revision 616 (lp:juju), deleting a file deletes the non-owned file |
436 | + # if no owned file can be found. |
437 | + filename = factory.make_name('file') |
438 | + factory.make_file_storage( |
439 | + filename=filename, content=b"test content", owner=None) |
440 | response = self.client.delete( |
441 | reverse('file_handler', args=[filename])) |
442 | self.assertEqual(httplib.NO_CONTENT, response.status_code) |
443 | |
444 | === modified file 'src/maasserver/tests/test_filestorage.py' |
445 | --- src/maasserver/tests/test_filestorage.py 2013-02-22 09:28:21 +0000 |
446 | +++ src/maasserver/tests/test_filestorage.py 2013-02-22 09:28:21 +0000 |
447 | @@ -40,10 +40,12 @@ |
448 | def test_save_file_creates_storage(self): |
449 | filename = factory.getRandomString() |
450 | content = self.make_data() |
451 | - storage = FileStorage.objects.save_file(filename, BytesIO(content)) |
452 | + user = factory.make_user() |
453 | + storage = FileStorage.objects.save_file( |
454 | + filename, BytesIO(content), user) |
455 | self.assertEqual( |
456 | - (filename, content), |
457 | - (storage.filename, storage.content)) |
458 | + (filename, content, user), |
459 | + (storage.filename, storage.content, storage.owner)) |
460 | |
461 | def test_storage_can_be_retrieved(self): |
462 | filename = factory.getRandomString() |
Looks good, but I have a couple of questions.
[1]
-def get_file(handler, request): by_name( handler, request):
+def get_file_
"""Get a named file from the file storage.
:param filename: The exact name of the file you want to get. GET.get( "filename" , None) st("Filename not supplied") param(request. GET, 'filename') objects. get(filename= filename) objects. filter( filename= filename) .latest( 'id')
:type filename: string
:return: The file is returned in the response content.
"""
- filename = request.
- if not filename:
- raise MAASAPIBadReque
+ filename = get_mandatory_
try:
- db_file = FileStorage.
+ db_file = FileStorage.
Shouldn't this restrict itself to files without an owner?
[2]
def delete(self, request, filename): or_404( FileStorage, filename=filename)
stored_ file.delete( )
"""Delete a FileStorage object."""
- stored_file = get_object_
+ stored_file = get_object_or_404(
+ FileStorage, filename=filename, owner=request.user)
Should this fall back to deleting files without owner when an
owned file is not found?