← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/bug-1123986-api-fix into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/bug-1123986-api-fix into lp:maas with lp:~rvb/maas/bug-1123986-db-filestorage-key as a prerequisite.

Commit message:
Adapt the file storage API to the 'owner' field on FileStorage objects.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/bug-1123986-api-fix/+merge/149059
-- 
https://code.launchpad.net/~rvb/maas/bug-1123986-api-fix/+merge/149059
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/bug-1123986-api-fix into lp:maas.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py	2013-02-14 14:25:39 +0000
+++ src/maasserver/api.py	2013-02-18 14:30:33 +0000
@@ -777,18 +777,16 @@
         return ('node_mac_handler', [node_system_id, mac_address])
 
 
-def get_file(handler, request):
+def get_file_by_name(handler, request):
     """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.GET.get("filename", None)
-    if not filename:
-        raise MAASAPIBadRequest("Filename not supplied")
+    filename = get_mandatory_param(request.GET, 'filename')
     try:
-        db_file = FileStorage.objects.get(filename=filename)
+        db_file = FileStorage.objects.filter(filename=filename).latest('id')
     except FileStorage.DoesNotExist:
         raise MAASAPINotFound("File not found")
     return HttpResponse(db_file.content, status=httplib.OK)
@@ -808,7 +806,7 @@
     """
     create = read = update = delete = None
 
-    get = operation(idempotent=True, exported_as='get')(get_file)
+    get = operation(idempotent=True, exported_as='get')(get_file_by_name)
 
     @classmethod
     def resource_uri(cls, *args, **kwargs):
@@ -826,7 +824,7 @@
 class FileHandler(OperationsHandler):
     """Manage a FileStorage object.
 
-    The file is identified by its filename.
+    The file is identified by its filename and owner.
     """
     model = FileStorage
     fields = DISPLAYED_FILES_FIELDS
@@ -838,11 +836,18 @@
         The 'content' of the file is base64-encoded."""
         # 'content' is a BinaryField, its 'value' is a base64-encoded version
         # of its value.
-        stored_files = FileStorage.objects.filter(filename=filename).values()
+        stored_files = FileStorage.objects.filter(
+            filename=filename, owner=request.user).values()
         # filename is a 'unique' field, we can have either 0 or 1 objects in
         # 'stored_files'.
         if len(stored_files) == 0:
-            raise Http404
+            # In order to support upgrading from installations where the notion
+            # of a file owner was not yet implemented, return non-owned files
+            # with this filename at a last resort option.
+            stored_files = FileStorage.objects.filter(
+                filename=filename, owner=None).values()
+            if len(stored_files) == 0:
+                raise Http404
         stored_file = stored_files[0]
         # Emit the json for this object manually because, no matter what the
         # piston documentation says, once an type is associated with a list
@@ -858,7 +863,8 @@
     @operation(idempotent=False)
     def delete(self, request, filename):
         """Delete a FileStorage object."""
-        stored_file = get_object_or_404(FileStorage, filename=filename)
+        stored_file = get_object_or_404(
+            FileStorage, filename=filename, owner=request.user)
         stored_file.delete()
         return rc.DELETED
 
@@ -875,7 +881,7 @@
     create = read = update = delete = None
     anonymous = AnonFilesHandler
 
-    get = operation(idempotent=True, exported_as='get')(get_file)
+    get = operation(idempotent=True, exported_as='get')(get_file_by_name)
 
     @operation(idempotent=False)
     def add(self, request):
@@ -899,7 +905,7 @@
         # As per the comment in FileStorage, this ought to deal in
         # chunks instead of reading the file into memory, but large
         # files are not expected.
-        FileStorage.objects.save_file(filename, uploaded_file)
+        FileStorage.objects.save_file(filename, uploaded_file, request.user)
         return HttpResponse('', status=httplib.CREATED)
 
     @operation(idempotent=True)
@@ -913,7 +919,7 @@
         :type prefix: string
         """
         prefix = request.GET.get("prefix", None)
-        files = FileStorage.objects.all()
+        files = FileStorage.objects.filter(owner=request.user)
         if prefix is not None:
             files = files.filter(filename__startswith=prefix)
         files = files.order_by('filename')

=== modified file 'src/maasserver/models/filestorage.py'
--- src/maasserver/models/filestorage.py	2013-02-18 14:30:33 +0000
+++ src/maasserver/models/filestorage.py	2013-02-18 14:30:33 +0000
@@ -47,17 +47,17 @@
     old file will continue without iterruption.
     """
 
-    def save_file(self, filename, file_object):
+    def save_file(self, filename, file_object, owner):
         """Save the file to the database.
 
-        If a file of that name already existed, it will be replaced by the
-        new contents.
+        If a file of that name/owner already existed, it will be replaced by
+        the new contents.
         """
         # This probably ought to read in chunks but large files are
         # not expected.
         content = Bin(file_object.read())
         storage, created = self.get_or_create(
-            filename=filename, defaults={'content': content})
+            filename=filename, owner=owner, defaults={'content': content})
         if not created:
             storage.content = content
             storage.save()

=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py	2012-12-19 12:31:11 +0000
+++ src/maasserver/testing/factory.py	2013-02-18 14:30:33 +0000
@@ -330,9 +330,9 @@
         admin.save()
         return admin
 
-    def make_file_storage(self, filename=None, content=None):
+    def make_file_storage(self, filename=None, content=None, owner=None):
         fake_file = self.make_file_upload(filename, content)
-        return FileStorage.objects.save_file(fake_file.name, fake_file)
+        return FileStorage.objects.save_file(fake_file.name, fake_file, owner)
 
     def make_oauth_header(self, **kwargs):
         """Fake an OAuth authorization header.

=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py	2013-02-14 12:42:30 +0000
+++ src/maasserver/tests/test_api.py	2013-02-18 14:30:33 +0000
@@ -2624,34 +2624,38 @@
 class AnonymousFileStorageAPITest(FileStorageAPITestMixin, AnonAPITestCase):
 
     def test_get_works_anonymously(self):
-        factory.make_file_storage(
-            filename="foofilers", content=b"give me rope")
-        response = self.make_API_GET_request("get", "foofilers")
-
-        self.assertEqual(httplib.OK, response.status_code)
-        self.assertEqual(b"give me rope", response.content)
+        storage = factory.make_file_storage()
+        response = self.make_API_GET_request("get", storage.filename)
+
+        self.assertEqual(httplib.OK, response.status_code)
+        self.assertEqual(storage.content, response.content)
+
+    def test_get_fetches_the_most_recent_file(self):
+        filename = factory.make_name('file')
+        factory.make_file_storage(filename=filename, owner=factory.make_user())
+        storage = factory.make_file_storage(
+            filename=filename, owner=factory.make_user())
+        response = self.make_API_GET_request("get", filename)
+
+        self.assertEqual(httplib.OK, response.status_code)
+        self.assertEqual(storage.content, response.content)
 
     def test_anon_cannot_list_files(self):
-        factory.make_file_storage(
-             filename="filename", content=b"test content")
+        factory.make_file_storage()
         response = self.make_API_GET_request("list")
         # The 'list' operation is not available to anon users.
         self.assertEqual(httplib.BAD_REQUEST, response.status_code)
 
     def test_anon_cannot_get_file(self):
-        filename = factory.make_name("file")
-        factory.make_file_storage(
-            filename=filename, content=b"test file content")
+        storage = factory.make_file_storage()
         response = self.client.get(
-            reverse('file_handler', args=[filename]))
+            reverse('file_handler', args=[storage.filename]))
         self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
 
     def test_anon_cannot_delete_file(self):
-        filename = factory.make_name('file')
-        factory.make_file_storage(
-            filename=filename, content=b"test content")
+        storage = factory.make_file_storage()
         response = self.client.delete(
-            reverse('file_handler', args=[filename]))
+            reverse('file_handler', args=[storage.filename]))
         self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
 
 
@@ -2721,7 +2725,7 @@
 
         self.assertEqual(httplib.BAD_REQUEST, response.status_code)
         self.assertIn('text/plain', response['Content-Type'])
-        self.assertEqual("Filename not supplied", response.content)
+        self.assertEqual("No provided filename!", response.content)
 
     def test_get_file_fails_with_missing_file(self):
         response = self.make_API_GET_request("get", filename="missingfilename")
@@ -2734,19 +2738,28 @@
         filenames = ["myfiles/a", "myfiles/z", "myfiles/b"]
         for filename in filenames:
             factory.make_file_storage(
-                filename=filename, content=b"test content")
+                filename=filename, content=b"test content",
+                owner=self.logged_in_user)
         response = self.make_API_GET_request("list")
         self.assertEqual(httplib.OK, response.status_code)
         parsed_results = json.loads(response.content)
         filenames = [result['filename'] for result in parsed_results]
         self.assertEqual(sorted(filenames), filenames)
 
+    def test_list_files_filters_by_owner(self):
+        factory.make_file_storage(owner=factory.make_user())
+        response = self.make_API_GET_request("list")
+        self.assertEqual(httplib.OK, response.status_code)
+        parsed_results = json.loads(response.content)
+        self.assertEqual([], parsed_results)
+
     def test_list_files_lists_files_with_prefix(self):
         filenames_with_prefix = ["prefix-file1", "prefix-file2"]
         filenames = filenames_with_prefix + ["otherfile", "otherfile2"]
         for filename in filenames:
             factory.make_file_storage(
-                filename=filename, content=b"test content")
+                filename=filename, content=b"test content",
+                owner=self.logged_in_user)
         response = self.client.get(
             self.get_uri('files/'), {"op": "list", "prefix": "prefix-"})
         self.assertEqual(httplib.OK, response.status_code)
@@ -2756,7 +2769,8 @@
 
     def test_list_files_does_not_include_file_content(self):
         factory.make_file_storage(
-             filename="filename", content=b"test content")
+             filename="filename", content=b"test content",
+             owner=self.logged_in_user)
         response = self.make_API_GET_request("list")
         parsed_results = json.loads(response.content)
         self.assertNotIn('content', parsed_results[0].keys())
@@ -2764,7 +2778,8 @@
     def test_files_resource_uri_are_url_escaped(self):
         filename = "&*!c/%//filename/"
         factory.make_file_storage(
-             filename=filename, content=b"test content")
+             filename=filename, content=b"test content",
+             owner=self.logged_in_user)
         response = self.make_API_GET_request("list")
         parsed_results = json.loads(response.content)
         resource_uri = parsed_results[0]['resource_uri']
@@ -2775,21 +2790,67 @@
     def test_get_file_returns_file_object_with_content_base64_encoded(self):
         filename = factory.make_name("file")
         content = sample_binary_data
-        factory.make_file_storage(filename=filename, content=content)
-        response = self.client.get(
-            reverse('file_handler', args=[filename]))
-        parsed_result = json.loads(response.content)
-        self.assertEqual(
-            (filename, content),
-            (
-                parsed_result['filename'],
-                b64decode(parsed_result['content'])
-            ))
+        factory.make_file_storage(
+            filename=filename, content=content, owner=self.logged_in_user)
+        response = self.client.get(
+            reverse('file_handler', args=[filename]))
+        parsed_result = json.loads(response.content)
+        self.assertEqual(
+            (filename, content),
+            (
+                parsed_result['filename'],
+                b64decode(parsed_result['content'])
+            ))
+
+    def test_get_file_returns_owned_file(self):
+        # If both an owned file and a non-owned file are present (with the
+        # same name), the owned file is returned.
+        filename = factory.make_name("file")
+        factory.make_file_storage(filename=filename, owner=None)
+        content = sample_binary_data
+        factory.make_file_storage(
+            filename=filename, content=content, owner=None)
+        response = self.client.get(
+            reverse('file_handler', args=[filename]))
+        parsed_result = json.loads(response.content)
+        self.assertEqual(
+            (filename, content),
+            (
+                parsed_result['filename'],
+                b64decode(parsed_result['content'])
+            ))
+
+    def test_get_file_returns_file_object_with_anon_owner(self):
+        # To get compatible with the MAAS provider present in Juju as of
+        # revision 616 (lp:juju), reading a file returns the non-owned files
+        # if no owned file can be found.
+        filename = factory.make_name("file")
+        content = sample_binary_data
+        factory.make_file_storage(
+            filename=filename, content=content, owner=None)
+        response = self.client.get(
+            reverse('file_handler', args=[filename]))
+        parsed_result = json.loads(response.content)
+        self.assertEqual(
+            (filename, content),
+            (
+                parsed_result['filename'],
+                b64decode(parsed_result['content'])
+            ))
+
+    def test_delete_filters_by_owner(self):
+        storage = factory.make_file_storage(owner=factory.make_user())
+        response = self.client.delete(
+            reverse('file_handler', args=[storage.filename]))
+        self.assertEqual(httplib.NOT_FOUND, response.status_code)
+        files = FileStorage.objects.filter(filename=storage.filename)
+        self.assertEqual([storage], list(files))
 
     def test_delete_file_deletes_file(self):
         filename = factory.make_name('file')
         factory.make_file_storage(
-            filename=filename, content=b"test content")
+            filename=filename, content=b"test content",
+            owner=self.logged_in_user)
         response = self.client.delete(
             reverse('file_handler', args=[filename]))
         self.assertEqual(httplib.NO_CONTENT, response.status_code)

=== modified file 'src/maasserver/tests/test_filestorage.py'
--- src/maasserver/tests/test_filestorage.py	2013-02-18 14:30:33 +0000
+++ src/maasserver/tests/test_filestorage.py	2013-02-18 14:30:33 +0000
@@ -40,10 +40,12 @@
     def test_save_file_creates_storage(self):
         filename = factory.getRandomString()
         content = self.make_data()
-        storage = FileStorage.objects.save_file(filename, BytesIO(content))
+        user = factory.make_user()
+        storage = FileStorage.objects.save_file(
+            filename, BytesIO(content), user)
         self.assertEqual(
-            (filename, content),
-            (storage.filename, storage.content))
+            (filename, content, user),
+            (storage.filename, storage.content, storage.owner))
 
     def test_storage_can_be_retrieved(self):
         filename = factory.getRandomString()


Follow ups