You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by be...@apache.org on 2012/10/13 01:20:52 UTC

svn commit: r1397741 - in /incubator/mesos/trunk: src/files/ src/tests/ third_party/libprocess/include/process/ third_party/libprocess/include/stout/ third_party/libprocess/src/

Author: benh
Date: Fri Oct 12 23:20:51 2012
New Revision: 1397741

URL: http://svn.apache.org/viewvc?rev=1397741&view=rev
Log:
Added a /download.json endpoint to "files" abstraction.

From: Ben Mahler <be...@gmail.com>
Review: https://reviews.apache.org/r/7123

Modified:
    incubator/mesos/trunk/src/files/files.cpp
    incubator/mesos/trunk/src/tests/files_tests.cpp
    incubator/mesos/trunk/src/tests/utils.hpp
    incubator/mesos/trunk/third_party/libprocess/include/process/http.hpp
    incubator/mesos/trunk/third_party/libprocess/include/stout/os.hpp
    incubator/mesos/trunk/third_party/libprocess/src/decoder.hpp
    incubator/mesos/trunk/third_party/libprocess/src/process.cpp

Modified: incubator/mesos/trunk/src/files/files.cpp
URL: http://svn.apache.org/viewvc/incubator/mesos/trunk/src/files/files.cpp?rev=1397741&r1=1397740&r2=1397741&view=diff
==============================================================================
--- incubator/mesos/trunk/src/files/files.cpp (original)
+++ incubator/mesos/trunk/src/files/files.cpp Fri Oct 12 23:20:51 2012
@@ -7,6 +7,7 @@
 #include <process/dispatch.hpp>
 #include <process/future.hpp>
 #include <process/http.hpp>
+#include <process/mime.hpp>
 #include <process/process.hpp>
 
 #include <stout/hashmap.hpp>
@@ -58,6 +59,7 @@ private:
   // HTTP endpoints.
   Future<Response> browse(const Request& request);
   Future<Response> read(const Request& request);
+  Future<Response> download(const Request& request);
   Future<Response> debug(const Request& request);
 
   // Resolves the virtual path to an actual path.
@@ -80,6 +82,7 @@ void FilesProcess::initialize()
 {
   route("/browse.json", &FilesProcess::browse);
   route("/read.json", &FilesProcess::read);
+  route("/download.json", &FilesProcess::download);
   route("/debug.json", &FilesProcess::debug);
 }
 
@@ -131,7 +134,7 @@ Future<Response> FilesProcess::browse(co
   Result<string> resolvedPath = resolve(path.get());
 
   if (resolvedPath.isError()) {
-    return InternalServerError(resolvedPath.error());
+    return InternalServerError(resolvedPath.error() + ".\n");
   } else if (resolvedPath.isNone()) {
     return NotFound();
   }
@@ -173,7 +176,7 @@ Future<Response> FilesProcess::read(cons
   if (request.query.get("offset").isSome()) {
     Try<off_t> result = numify<off_t>(request.query.get("offset").get());
     if (result.isError()) {
-      return BadRequest("Failed to parse offset: " + result.error());
+      return BadRequest("Failed to parse offset: " + result.error() + ".\n");
     }
     offset = result.get();
   }
@@ -183,7 +186,7 @@ Future<Response> FilesProcess::read(cons
   if (request.query.get("length").isSome()) {
     Try<ssize_t> result = numify<ssize_t>(request.query.get("length").get());
     if (result.isError()) {
-      return BadRequest("Failed to parse length: " + result.error());
+      return BadRequest("Failed to parse length: " + result.error() + ".\n");
     }
     length = result.get();
   }
@@ -191,14 +194,14 @@ Future<Response> FilesProcess::read(cons
   Result<string> resolvedPath = resolve(path.get());
 
   if (resolvedPath.isError()) {
-    return BadRequest(resolvedPath.error());
+    return BadRequest(resolvedPath.error() + ".\n");
   } else if (!resolvedPath.isSome()) {
     return NotFound();
   }
 
   // Don't read directories.
   if (os::isdir(resolvedPath.get())) {
-    return BadRequest("Cannot read a directory.");
+    return BadRequest("Cannot read a directory.\n");
   }
 
   // TODO(benh): Cache file descriptors so we aren't constantly
@@ -209,7 +212,7 @@ Future<Response> FilesProcess::read(cons
     string error = strings::format("Failed to open file at '%s': %s",
         resolvedPath.get(), fd.error()).get();
     LOG(WARNING) << error;
-    return InternalServerError(error);
+    return InternalServerError(error + ".\n");
   }
 
   off_t size = lseek(fd.get(), 0, SEEK_END);
@@ -219,7 +222,7 @@ Future<Response> FilesProcess::read(cons
         resolvedPath.get(), strerror(errno)).get();
     LOG(WARNING) << error;
     close(fd.get());
-    return InternalServerError(error);
+    return InternalServerError(error + ".\n");
   }
 
   if (offset == -1) {
@@ -239,7 +242,7 @@ Future<Response> FilesProcess::read(cons
           resolvedPath.get(), strerror(errno)).get();
       LOG(WARNING) << error;
       close(fd.get());
-      return InternalServerError(error);
+      return InternalServerError(error + ".\n");
     }
 
     // Read length bytes (or to EOF).
@@ -258,7 +261,7 @@ Future<Response> FilesProcess::read(cons
       LOG(WARNING) << error;
       delete[] temp;
       close(fd.get());
-      return InternalServerError(error);
+      return InternalServerError(error + ".\n");
     } else {
       object.values["offset"] = offset;
       object.values["length"] = length;
@@ -276,6 +279,53 @@ Future<Response> FilesProcess::read(cons
 }
 
 
+Future<Response> FilesProcess::download(const Request& request)
+{
+  Option<string> path = request.query.get("path");
+
+  if (!path.isSome() || path.get().empty()) {
+    return BadRequest("Expecting 'path=value' in query.\n");
+  }
+
+  Result<string> resolvedPath = resolve(path.get());
+
+  if (resolvedPath.isError()) {
+    return BadRequest(resolvedPath.error() + ".\n");
+  } else if (!resolvedPath.isSome()) {
+    return NotFound();
+  }
+
+  // Don't download directories.
+  if (os::isdir(resolvedPath.get())) {
+    return BadRequest("Cannot download a directory.\n");
+  }
+
+  Try<string> basename = os::basename(resolvedPath.get());
+  if (basename.isError()) {
+    LOG(ERROR) << basename.error();
+    return InternalServerError(basename.error() + ".\n");
+  }
+
+  OK response;
+  response.type = response.PATH;
+  response.path = resolvedPath.get();
+  response.headers["Content-Type"] = "application/octet-stream";
+  response.headers["Content-Disposition"] =
+    strings::format("attachment; filename=%s", basename.get()).get();
+
+  // Attempt to detect the mime type.
+  size_t index = basename.get().find_last_of('.');
+  if (index != string::npos) {
+    string extension = basename.get().substr(index);
+    if (mime::types.count(extension) > 0) {
+      response.headers["Content-Type"] = mime::types[extension];
+    }
+  }
+
+  return response;
+}
+
+
 Future<Response> FilesProcess::debug(const Request& request)
 {
   JSON::Object object;
@@ -327,9 +377,9 @@ Result<string> FilesProcess::resolve(con
       // accessible.
       Try<string> result = os::realpath(path);
       if (result.isError()) {
-        return Result<string>::error("Cannot resolve path.");
+        return Result<string>::error("Cannot resolve path");
       } else if (!strings::startsWith(result.get(), paths[prefix])) {
-        return Result<string>::error("Resolved path is inaccessible.");
+        return Result<string>::error("Resolved path is inaccessible");
       }
 
       path = result.get();

Modified: incubator/mesos/trunk/src/tests/files_tests.cpp
URL: http://svn.apache.org/viewvc/incubator/mesos/trunk/src/tests/files_tests.cpp?rev=1397741&r1=1397740&r2=1397741&view=diff
==============================================================================
--- incubator/mesos/trunk/src/tests/files_tests.cpp (original)
+++ incubator/mesos/trunk/src/tests/files_tests.cpp Fri Oct 12 23:20:51 2012
@@ -16,6 +16,8 @@
  * limitations under the License.
  */
 
+#include <string>
+
 #include <gmock/gmock.h>
 
 #include <process/future.hpp>
@@ -40,6 +42,8 @@ using process::http::NotFound;
 using process::http::OK;
 using process::http::Response;
 
+using std::string;
+
 
 TEST_WITH_WORKDIR(FilesTest, AttachTest)
 {
@@ -86,14 +90,14 @@ TEST_WITH_WORKDIR(FilesTest, ReadTest)
 
   EXPECT_RESPONSE_STATUS_WILL_EQ(BadRequest().status, response);
   EXPECT_RESPONSE_BODY_WILL_EQ(
-      "Failed to parse offset: Failed to convert 'hello' to number",
+      "Failed to parse offset: Failed to convert 'hello' to number.\n",
       response);
 
   response = process::http::get(pid, "read.json?path=none&length=hello");
 
   EXPECT_RESPONSE_STATUS_WILL_EQ(BadRequest().status, response);
   EXPECT_RESPONSE_BODY_WILL_EQ(
-      "Failed to parse length: Failed to convert 'hello' to number",
+      "Failed to parse length: Failed to convert 'hello' to number.\n",
       response);
 
   // Now write a file.
@@ -247,3 +251,38 @@ TEST_WITH_WORKDIR(FilesTest, BrowseTest)
       NotFound().status,
       process::http::get(pid, "browse.json?path=missing"));
 }
+
+
+TEST_WITH_WORKDIR(FilesTest, DownloadTest)
+{
+  Files files;
+  const process::PID<>& pid = files.pid();
+
+  // This is a one-pixel black gif image.
+  char gifData[] = {
+      0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x01, 0x00, 0x01, 0x00, 0x91, 0x00,
+      0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x02,
+      0x02, 0x4c, 0x01, 0x00, 0x3b, 0x00
+  };
+  string data(gifData, sizeof(gifData));
+
+  ASSERT_TRUE(os::write("binary", "no file extension").isSome());
+  ASSERT_TRUE(os::write("black.gif", data).isSome());
+  EXPECT_FUTURE_WILL_SUCCEED(files.attach("binary", "binary"));
+  EXPECT_FUTURE_WILL_SUCCEED(files.attach("black.gif", "black.gif"));
+
+  Future<Response> response =
+    process::http::get(pid, "download.json?path=binary");
+  EXPECT_RESPONSE_STATUS_WILL_EQ(OK().status, response);
+  EXPECT_RESPONSE_HEADER_WILL_EQ(
+      "application/octet-stream",
+      "Content-Type",
+      response);
+  EXPECT_RESPONSE_BODY_WILL_EQ("no file extension", response);
+
+  response = process::http::get(pid, "download.json?path=black.gif");
+  EXPECT_RESPONSE_STATUS_WILL_EQ(OK().status, response);
+  EXPECT_RESPONSE_HEADER_WILL_EQ("image/gif", "Content-Type", response);
+  EXPECT_RESPONSE_BODY_WILL_EQ(data, response);
+}

Modified: incubator/mesos/trunk/src/tests/utils.hpp
URL: http://svn.apache.org/viewvc/incubator/mesos/trunk/src/tests/utils.hpp?rev=1397741&r1=1397740&r2=1397741&view=diff
==============================================================================
--- incubator/mesos/trunk/src/tests/utils.hpp (original)
+++ incubator/mesos/trunk/src/tests/utils.hpp Fri Oct 12 23:20:51 2012
@@ -188,6 +188,15 @@ inline void EXPECT_RESPONSE_BODY_WILL_EQ
 }
 
 
+#define EXPECT_RESPONSE_HEADER_WILL_EQ(expected, headerKey, future)            \
+  do {                                                                         \
+    typeof(future) _future = future;                                           \
+    ASSERT_FUTURE_WILL_SUCCEED(future);                                        \
+    EXPECT_TRUE(_future.get().headers.contains(headerKey));                    \
+    EXPECT_EQ(expected, _future.get().headers.find(headerKey)->second);        \
+  } while (false);
+
+
 /**
  * Definition of a mock Scheduler to be used in tests with gmock.
  */

Modified: incubator/mesos/trunk/third_party/libprocess/include/process/http.hpp
URL: http://svn.apache.org/viewvc/incubator/mesos/trunk/third_party/libprocess/include/process/http.hpp?rev=1397741&r1=1397740&r2=1397741&view=diff
==============================================================================
--- incubator/mesos/trunk/third_party/libprocess/include/process/http.hpp (original)
+++ incubator/mesos/trunk/third_party/libprocess/include/process/http.hpp Fri Oct 12 23:20:51 2012
@@ -103,6 +103,16 @@ struct OK : Response
 };
 
 
+struct TemporaryRedirect : Response
+{
+  TemporaryRedirect(const std::string& url) : Response("")
+  {
+    status = "307 Temporary Redirect";
+    headers["Location"] = url;
+  }
+};
+
+
 struct BadRequest : Response
 {
   BadRequest(const std::string& body = "") : Response(body)
@@ -139,16 +149,6 @@ struct ServiceUnavailable : Response
 };
 
 
-struct TemporaryRedirect : Response
-{
-  TemporaryRedirect(const std::string& url) : Response("")
-  {
-    status = "307 Temporary Redirect";
-    headers["Location"] = url;
-  }
-};
-
-
 namespace query {
 
 // Parses an HTTP query string into a map. For example:

Modified: incubator/mesos/trunk/third_party/libprocess/include/stout/os.hpp
URL: http://svn.apache.org/viewvc/incubator/mesos/trunk/third_party/libprocess/include/stout/os.hpp?rev=1397741&r1=1397740&r2=1397741&view=diff
==============================================================================
--- incubator/mesos/trunk/third_party/libprocess/include/stout/os.hpp (original)
+++ incubator/mesos/trunk/third_party/libprocess/include/stout/os.hpp Fri Oct 12 23:20:51 2012
@@ -247,6 +247,8 @@ inline Try<Nothing> write(const std::str
 
 // Read the contents of the file from its current offset
 // and return it as a string.
+// TODO(bmahler): Change this to a Try<std::string> since none()
+// is equivalent to an empty string.
 inline Result<std::string> read(int fd)
 {
   // Get the size of the file.

Modified: incubator/mesos/trunk/third_party/libprocess/src/decoder.hpp
URL: http://svn.apache.org/viewvc/incubator/mesos/trunk/third_party/libprocess/src/decoder.hpp?rev=1397741&r1=1397740&r2=1397741&view=diff
==============================================================================
--- incubator/mesos/trunk/third_party/libprocess/src/decoder.hpp (original)
+++ incubator/mesos/trunk/third_party/libprocess/src/decoder.hpp Fri Oct 12 23:20:51 2012
@@ -95,6 +95,12 @@ private:
   static int on_headers_complete(http_parser* p)
   {
     DataDecoder* decoder = (DataDecoder*) p->data;
+
+    // Add final header.
+    decoder->request->headers[decoder->field] = decoder->value;
+    decoder->field.clear();
+    decoder->value.clear();
+
     decoder->request->method = http_method_str((http_method) decoder->parser.method);
     decoder->request->keepAlive = http_should_keep_alive(&decoder->parser);
     return 0;
@@ -275,6 +281,13 @@ private:
 
   static int on_headers_complete(http_parser* p)
   {
+    ResponseDecoder* decoder = (ResponseDecoder*) p->data;
+
+    // Add final header.
+    decoder->response->headers[decoder->field] = decoder->value;
+    decoder->field.clear();
+    decoder->value.clear();
+
     return 0;
   }
 

Modified: incubator/mesos/trunk/third_party/libprocess/src/process.cpp
URL: http://svn.apache.org/viewvc/incubator/mesos/trunk/third_party/libprocess/src/process.cpp?rev=1397741&r1=1397740&r2=1397741&view=diff
==============================================================================
--- incubator/mesos/trunk/third_party/libprocess/src/process.cpp (original)
+++ incubator/mesos/trunk/third_party/libprocess/src/process.cpp Fri Oct 12 23:20:51 2012
@@ -3195,7 +3195,8 @@ Future<Response> decode(const string& bu
     for (size_t i = 0; i < responses.size(); ++i) {
       delete responses[i];
     }
-    return Future<Response>::failed("Failed to decode HTTP response");
+    return Future<Response>::failed(
+        "Failed to decode HTTP response:\n" + buffer + "\n");
   } else if (responses.size() > 1) {
     PLOG(ERROR) << "Received more than 1 HTTP Response";
   }