← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~julian-edwards/maas/serve-files into lp:maas

 

Julian Edwards has proposed merging lp:~julian-edwards/maas/serve-files into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~julian-edwards/maas/serve-files/+merge/92764
-- 
https://code.launchpad.net/~julian-edwards/maas/serve-files/+merge/92764
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~julian-edwards/maas/serve-files into lp:maas.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py	2012-02-13 10:40:45 +0000
+++ src/maasserver/api.py	2012-02-13 13:18:33 +0000
@@ -12,23 +12,36 @@
 __all__ = [
     "api_doc",
     "generate_api_doc",
+    "FilesHandler",
     "NodeHandler",
     "NodeMacsHandler",
     ]
 
+import httplib
 import types
 
-from django.core.exceptions import ValidationError
-from django.http import HttpResponseBadRequest
+from django.core.exceptions import (
+    ObjectDoesNotExist,
+    ValidationError,
+    )
+from django.http import (
+    HttpResponse,
+    HttpResponseBadRequest,
+    )
 from django.shortcuts import (
     get_object_or_404,
     render_to_response,
     )
 from django.template import RequestContext
 from docutils import core
+from maasserver.exceptions import (
+    MaasAPIBadRequest,
+    MaasAPINotFound,
+    )
 from maasserver.forms import NodeWithMACAddressesForm
 from maasserver.macaddress import validate_mac
 from maasserver.models import (
+    FileStorage,
     MACAddress,
     Node,
     )
@@ -315,6 +328,48 @@
 
 
 @api_operations
+class FilesHandler(BaseHandler):
+    """File management operations."""
+    allowed_methods = ('GET', 'POST',)
+
+    @api_exported('get', 'GET')
+    def get(self, request):
+        """Get a named file."""
+        filename = request.GET.get("filename", None)
+        #import pdb; pdb.set_trace()
+        if not filename:
+            raise MaasAPIBadRequest("Filename not supplied")
+        try:
+            db_file =  FileStorage.objects.get(filename=filename)
+        except ObjectDoesNotExist:
+            raise MaasAPINotFound("File not found")
+        return HttpResponse(db_file.data.read(), status=httplib.OK)
+
+    @api_exported('add', 'POST')
+    def add(self, request):
+        """Create a new Node."""
+        filename = request.data.get("filename", None)
+        if not filename:
+            raise MaasAPIBadRequest("Filename not supplied")
+        files = request.FILES
+        if not files:
+            raise MaasAPIBadRequest("File not supplied")
+        if len(files) != 1:
+            raise MaasAPIBadRequest("Exactly one file must be supplied")
+        uploaded_file = files['file']
+
+        # 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.
+        storage = FileStorage()
+        storage.save_file(filename, uploaded_file)
+        storage.save()
+
+    @classmethod
+    def resource_uri(cls, *args, **kwargs):
+        return ('files_handler', [])
+
+
 class AccountHandler(BaseHandler):
     """Manage the current logged-in user."""
     allowed_methods = ('POST',)

=== modified file 'src/maasserver/exceptions.py'
--- src/maasserver/exceptions.py	2012-02-13 11:53:47 +0000
+++ src/maasserver/exceptions.py	2012-02-13 13:18:33 +0000
@@ -11,7 +11,9 @@
 __metaclass__ = type
 __all__ = [
     "MaasException",
+    "MaasAPIBadRequest",
     "MaasAPIException",
+    "MaasAPINotFound",
     "PermissionDenied",
     ]
 
@@ -33,5 +35,13 @@
     api_error = httplib.INTERNAL_SERVER_ERROR
 
 
+class MaasAPIBadRequest(MaasAPIException):
+    api_error = httplib.BAD_REQUEST
+
+
+class MaasAPINotFound(MaasAPIException):
+    api_error = httplib.NOT_FOUND
+
+
 class PermissionDenied(MaasAPIException):
     api_error = httplib.UNAUTHORIZED

=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py	2012-02-13 11:53:47 +0000
+++ src/maasserver/tests/test_api.py	2012-02-13 13:18:33 +0000
@@ -13,8 +13,12 @@
 
 import httplib
 import json
+import os
+import shutil
+from StringIO import StringIO
 import time
 
+from django.conf import settings
 from django.test.client import Client
 from maasserver.models import (
     MACAddress,
@@ -516,3 +520,109 @@
                 self.node.system_id, 'invalid-mac'))
 
         self.assertEqual(httplib.BAD_REQUEST, response.status_code)
+
+
+class FileStorageAPITest(APITestCase):
+
+    def setUp(self):
+        super(FileStorageAPITest, self).setUp()
+        os.mkdir(settings.MEDIA_ROOT)
+        self.tmpdir = os.path.join(settings.MEDIA_ROOT, "testing")
+        os.mkdir(self.tmpdir)
+        self.addCleanup(shutil.rmtree, settings.MEDIA_ROOT)
+
+    def make_file(self, name="foo", contents="test file contents"):
+        """Make a temp file named `name` with contents `contents`.
+
+        :return: The full file path of the file that was created.
+        """
+        filepath = os.path.join(self.tmpdir, name)
+        with open(filepath, "w") as f:
+            f.write(contents)
+        return filepath
+
+    def _create_API_params(self, op=None, filename=None, fileObj=None):
+        params = {}
+        if op is not None:
+            params["op"] = op
+        if filename is not None:
+            params["filename"] = filename
+        if fileObj is not None:
+            params["file"] = fileObj
+        return params
+
+    def make_API_POST_request(self, op=None, filename=None, fileObj=None):
+        """Make an API POST request and return the response."""
+        params = self._create_API_params(op, filename, fileObj)
+        return self.client.post("/api/files/", params)
+
+    def make_API_GET_request(self, op=None, filename=None, fileObj=None):
+        """Make an API GET request and return the response."""
+        params = self._create_API_params(op, filename, fileObj)
+        return self.client.get("/api/files/", params)
+
+    def test_add_file_succeeds(self):
+        filepath = self.make_file()
+
+        with open(filepath) as f:
+            response = self.make_API_POST_request("add", "foo", f)
+
+        self.assertEqual(httplib.OK, response.status_code)
+
+    def test_add_file_fails_with_no_filename(self):
+        filepath = self.make_file()
+
+        with open(filepath) as f:
+            response = self.make_API_POST_request("add", fileObj=f)
+
+        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
+        self.assertIn('text/plain', response['Content-Type'])
+        self.assertEqual("Filename not supplied", response.content)
+
+    def test_add_file_fails_with_no_file_attached(self):
+        response = self.make_API_POST_request("add", "foo")
+
+        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
+        self.assertIn('text/plain', response['Content-Type'])
+        self.assertEqual("File not supplied", response.content)
+
+    def test_add_file_fails_with_too_many_files(self):
+        filepath = self.make_file(name="foo")
+        filepath2 = self.make_file(name="foo2")
+
+        with open(filepath) as f, open(filepath2) as f2:
+            response = self.client.post(
+                "/api/files/",
+                {
+                    "op": "add",
+                    "filename": "foo",
+                    "file": f,
+                    "file2": f2,
+                })
+
+        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
+        self.assertIn('text/plain', response['Content-Type'])
+        self.assertEqual("Exactly one file must be supplied", response.content)
+
+    def test_get_file_succeeds(self):
+        storage = factory.make_file_storage(
+            filename="foofilers", data="give me rope")
+        response = self.make_API_GET_request("get", "foofilers")
+
+        self.assertEqual(httplib.OK, response.status_code)
+        self.assertEqual("give me rope", response.content)
+
+    def test_get_file_fails_with_no_filename(self):
+        response = self.make_API_GET_request("get")
+
+        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
+        self.assertIn('text/plain', response['Content-Type'])
+        self.assertEqual("Filename not supplied", response.content)
+
+    def test_get_file_fails_with_missing_file(self):
+        #import pdb;pdb.set_trace()
+        response = self.make_API_GET_request("get", filename="missingfilename")
+
+        self.assertEqual(httplib.NOT_FOUND, response.status_code)
+        self.assertIn('text/plain', response['Content-Type'])
+        self.assertEqual("File not found", response.content)

=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py	2012-02-10 08:49:01 +0000
+++ src/maasserver/urls.py	2012-02-13 13:18:33 +0000
@@ -23,6 +23,7 @@
 from maasserver.api import (
     AccountHandler,
     api_doc,
+    FilesHandler,
     MaasAPIAuthentication,
     NodeHandler,
     NodeMacHandler,
@@ -66,11 +67,12 @@
 # API.
 auth = MaasAPIAuthentication(realm="MaaS API")
 
+account_handler = Resource(AccountHandler, authentication=auth)
+files_handler = Resource(FilesHandler, authentication=auth)
 node_handler = Resource(NodeHandler, authentication=auth)
 nodes_handler = Resource(NodesHandler, authentication=auth)
 node_mac_handler = Resource(NodeMacHandler, authentication=auth)
 node_macs_handler = Resource(NodeMacsHandler, authentication=auth)
-account_handler = Resource(AccountHandler, authentication=auth)
 
 # API URLs accessible to anonymous users.
 urlpatterns += patterns('',
@@ -90,5 +92,6 @@
         r'^api/nodes/(?P<system_id>[\w\-]+)/$', node_handler,
         name='node_handler'),
     url(r'^api/nodes/$', nodes_handler, name='nodes_handler'),
+    url(r'^api/files/$', files_handler, name='files_handler'),
     url(r'^api/account/$', account_handler, name='account_handler'),
 )