ARI: Add the ability to download the media associated with a stored recording
authorMatt Jordan <mjordan@digium.com>
Wed, 18 May 2016 11:19:58 +0000 (06:19 -0500)
committerMatt Jordan <mjordan@digium.com>
Fri, 20 May 2016 14:06:12 +0000 (09:06 -0500)
This patch adds a new feature to ARI that allows a client to download
the media associated with a stored recording. The new route is
/recordings/stored/{name}/file, and transmits the underlying binary file
using Asterisk's HTTP server's underlying file transfer facilities.

Because this REST route returns non-JSON, a few small enhancements had
to be made to the Python Swagger generation code, as well as the
mustache templates that generate the ARI bindings.

ASTERISK-26042 #close

Change-Id: I49ec5c4afdec30bb665d9c977ab423b5387e0181

12 files changed:
CHANGES
include/asterisk/ari.h
include/asterisk/stasis_app_recording.h
res/ari/resource_recordings.c
res/ari/resource_recordings.h
res/res_ari.c
res/res_ari_recordings.c
res/stasis_recording/stored.c
rest-api-templates/ari_resource.h.mustache
rest-api-templates/res_ari_resource.c.mustache
rest-api-templates/swagger_model.py
rest-api/api-docs/recordings.json

diff --git a/CHANGES b/CHANGES
index 628bde2..a547fbc 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -32,6 +32,10 @@ ARI
    back to the resource. The "PlaybackFinished" event is raised when all media
    URIs are done.
 
    back to the resource. The "PlaybackFinished" event is raised when all media
    URIs are done.
 
+ * Stored recordings now allow for the media associated with a stored recording
+   to be retrieved. The new route, GET /recordings/stored/{name}/file, will
+   transmit the raw media file to the requester as binary.
+
 
 Applications
 ------------------
 
 Applications
 ------------------
index c9f47a6..79b9516 100644 (file)
@@ -95,6 +95,8 @@ struct ast_ari_response {
        /*! HTTP response code.
         * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html */
        int response_code;
        /*! HTTP response code.
         * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html */
        int response_code;
+       /*! File descriptor for whatever file we want to respond with */
+       int fd;
        /*! Corresponding text for the response code */
        const char *response_text; /* Shouldn't http.c handle this? */
        /*! Flag to indicate that no further response is needed */
        /*! Corresponding text for the response code */
        const char *response_text; /* Shouldn't http.c handle this? */
        /*! Flag to indicate that no further response is needed */
index 543207a..bded306 100644 (file)
@@ -49,6 +49,30 @@ const char *stasis_app_stored_recording_get_file(
        struct stasis_app_stored_recording *recording);
 
 /*!
        struct stasis_app_stored_recording *recording);
 
 /*!
+ * \brief Returns the full filename, with extension, for this recording.
+ * \since 14.0.0
+ *
+ * \param recording Recording to query.
+ *
+ * \return Absolute path to the recording file, with the extension.
+ * \return \c NULL on error
+ */
+const char *stasis_app_stored_recording_get_filename(
+       struct stasis_app_stored_recording *recording);
+
+/*!
+ * \brief Returns the extension for this recording.
+ * \since 14.0.0
+ *
+ * \param recording Recording to query.
+ *
+ * \return The extension associated with this recording.
+ * \return \c NULL on error
+ */
+const char *stasis_app_stored_recording_get_extension(
+       struct stasis_app_stored_recording *recording);
+
+/*!
  * \brief Convert stored recording info to JSON.
  *
  * \param recording Recording to convert.
  * \brief Convert stored recording info to JSON.
  *
  * \param recording Recording to convert.
index a49c3b1..5661d60 100644 (file)
@@ -101,6 +101,50 @@ void ast_ari_recordings_get_stored(struct ast_variable *headers,
        ast_ari_response_ok(response, json);
 }
 
        ast_ari_response_ok(response, json);
 }
 
+void ast_ari_recordings_get_stored_file(struct ast_tcptls_session_instance *ser,
+       struct ast_variable *headers, struct ast_ari_recordings_get_stored_file_args *args,
+       struct ast_ari_response *response)
+{
+       RAII_VAR(struct stasis_app_stored_recording *, recording,
+               stasis_app_stored_recording_find_by_name(args->recording_name),
+               ao2_cleanup);
+       static const char *format_type_names[AST_MEDIA_TYPE_TEXT + 1] = {
+               [AST_MEDIA_TYPE_UNKNOWN] = "binary",
+               [AST_MEDIA_TYPE_AUDIO] = "audio",
+               [AST_MEDIA_TYPE_VIDEO] = "video",
+               [AST_MEDIA_TYPE_IMAGE] = "image",
+               [AST_MEDIA_TYPE_TEXT] = "text",
+       };
+       struct ast_format *format;
+
+       response->message = ast_json_null();
+
+       if (!recording) {
+               ast_ari_response_error(response, 404, "Not Found",
+                       "Recording not found");
+               return;
+       }
+
+       format = ast_get_format_for_file_ext(stasis_app_stored_recording_get_extension(recording));
+       if (!format) {
+               ast_ari_response_error(response, 500, "Internal Server Error",
+                       "Format specified by recording not available or loaded");
+               return;
+       }
+
+       response->fd = open(stasis_app_stored_recording_get_filename(recording), O_RDONLY);
+       if (response->fd < 0) {
+               ast_ari_response_error(response, 403, "Forbidden",
+                       "Recording could not be opened");
+               return;
+       }
+
+       ast_str_append(&response->headers, 0, "Content-Type: %s/%s\r\n",
+               format_type_names[ast_format_get_type(format)],
+               stasis_app_stored_recording_get_extension(recording));
+       ast_ari_response_ok(response, ast_json_null());
+}
+
 void ast_ari_recordings_copy_stored(struct ast_variable *headers,
        struct ast_ari_recordings_copy_stored_args *args,
        struct ast_ari_response *response)
 void ast_ari_recordings_copy_stored(struct ast_variable *headers,
        struct ast_ari_recordings_copy_stored_args *args,
        struct ast_ari_response *response)
index 196122f..1bc93c5 100644 (file)
@@ -76,6 +76,20 @@ struct ast_ari_recordings_delete_stored_args {
  * \param[out] response HTTP response
  */
 void ast_ari_recordings_delete_stored(struct ast_variable *headers, struct ast_ari_recordings_delete_stored_args *args, struct ast_ari_response *response);
  * \param[out] response HTTP response
  */
 void ast_ari_recordings_delete_stored(struct ast_variable *headers, struct ast_ari_recordings_delete_stored_args *args, struct ast_ari_response *response);
+/*! Argument struct for ast_ari_recordings_get_stored_file() */
+struct ast_ari_recordings_get_stored_file_args {
+       /*! The name of the recording */
+       const char *recording_name;
+};
+/*!
+ * \brief Get the file associated with the stored recording.
+ *
+ * \param ser TCP/TLS session instance
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void ast_ari_recordings_get_stored_file(struct ast_tcptls_session_instance *ser, struct ast_variable *headers, struct ast_ari_recordings_get_stored_file_args *args, struct ast_ari_response *response);
 /*! Argument struct for ast_ari_recordings_copy_stored() */
 struct ast_ari_recordings_copy_stored_args {
        /*! The name of the recording to copy */
 /*! Argument struct for ast_ari_recordings_copy_stored() */
 struct ast_ari_recordings_copy_stored_args {
        /*! The name of the recording to copy */
index 14dece8..0cc1ee7 100644 (file)
@@ -870,7 +870,7 @@ static int ast_ari_callback(struct ast_tcptls_session_instance *ser,
        RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
        RAII_VAR(struct ast_str *, response_body, ast_str_create(256), ast_free);
        RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
        RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
        RAII_VAR(struct ast_str *, response_body, ast_str_create(256), ast_free);
        RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
-       struct ast_ari_response response = {};
+       struct ast_ari_response response = { .fd = -1, 0 };
        RAII_VAR(struct ast_variable *, post_vars, NULL, ast_variables_destroy);
 
        if (!response_body) {
        RAII_VAR(struct ast_variable *, post_vars, NULL, ast_variables_destroy);
 
        if (!response_body) {
@@ -1011,11 +1011,14 @@ request_failed:
                response.response_text, ast_str_buffer(response.headers), ast_str_buffer(response_body));
        ast_http_send(ser, method, response.response_code,
                      response.response_text, response.headers, response_body,
                response.response_text, ast_str_buffer(response.headers), ast_str_buffer(response_body));
        ast_http_send(ser, method, response.response_code,
                      response.response_text, response.headers, response_body,
-                     0, 0);
+                     response.fd != -1 ? response.fd : 0, 0);
        /* ast_http_send takes ownership, so we don't have to free them */
        response_body = NULL;
 
        ast_json_unref(response.message);
        /* ast_http_send takes ownership, so we don't have to free them */
        response_body = NULL;
 
        ast_json_unref(response.message);
+       if (response.fd >= 0) {
+               close(response.fd);
+       }
        return 0;
 }
 
        return 0;
 }
 
@@ -1023,7 +1026,6 @@ static struct ast_http_uri http_uri = {
        .callback = ast_ari_callback,
        .description = "Asterisk RESTful API",
        .uri = "ari",
        .callback = ast_ari_callback,
        .description = "Asterisk RESTful API",
        .uri = "ari",
-
        .has_subtree = 1,
        .data = NULL,
        .key = __FILE__,
        .has_subtree = 1,
        .data = NULL,
        .key = __FILE__,
index df4a124..abc264d 100644 (file)
@@ -221,6 +221,65 @@ static void ast_ari_recordings_delete_stored_cb(
 fin: __attribute__((unused))
        return;
 }
 fin: __attribute__((unused))
        return;
 }
+/*!
+ * \brief Parameter parsing callback for /recordings/stored/{recordingName}/file.
+ * \param get_params GET parameters in the HTTP request.
+ * \param path_vars Path variables extracted from the request.
+ * \param headers HTTP headers.
+ * \param[out] response Response to the HTTP request.
+ */
+static void ast_ari_recordings_get_stored_file_cb(
+       struct ast_tcptls_session_instance *ser,
+       struct ast_variable *get_params, struct ast_variable *path_vars,
+       struct ast_variable *headers, struct ast_ari_response *response)
+{
+       struct ast_ari_recordings_get_stored_file_args args = {};
+       struct ast_variable *i;
+       RAII_VAR(struct ast_json *, body, NULL, ast_json_unref);
+#if defined(AST_DEVMODE)
+       int is_valid;
+       int code;
+#endif /* AST_DEVMODE */
+
+       for (i = path_vars; i; i = i->next) {
+               if (strcmp(i->name, "recordingName") == 0) {
+                       args.recording_name = (i->value);
+               } else
+               {}
+       }
+       ast_ari_recordings_get_stored_file(ser, headers, &args, response);
+#if defined(AST_DEVMODE)
+       code = response->response_code;
+
+       switch (code) {
+       case 0: /* Implementation is still a stub, or the code wasn't set */
+               is_valid = response->message == NULL;
+               break;
+       case 500: /* Internal Server Error */
+       case 501: /* Not Implemented */
+       case 404: /* Recording not found */
+               is_valid = 1;
+               break;
+       default:
+               if (200 <= code && code <= 299) {
+                       /* No validation on a raw binary response */
+                       is_valid = 1;
+               } else {
+                       ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}/file\n", code);
+                       is_valid = 0;
+               }
+       }
+
+       if (!is_valid) {
+               ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}/file\n");
+               ast_ari_response_error(response, 500,
+                       "Internal Server Error", "Response validation failed");
+       }
+#endif /* AST_DEVMODE */
+
+fin: __attribute__((unused))
+       return;
+}
 int ast_ari_recordings_copy_stored_parse_body(
        struct ast_json *body,
        struct ast_ari_recordings_copy_stored_args *args)
 int ast_ari_recordings_copy_stored_parse_body(
        struct ast_json *body,
        struct ast_ari_recordings_copy_stored_args *args)
@@ -738,6 +797,15 @@ fin: __attribute__((unused))
 }
 
 /*! \brief REST handler for /api-docs/recordings.{format} */
 }
 
 /*! \brief REST handler for /api-docs/recordings.{format} */
+static struct stasis_rest_handlers recordings_stored_recordingName_file = {
+       .path_segment = "file",
+       .callbacks = {
+               [AST_HTTP_GET] = ast_ari_recordings_get_stored_file_cb,
+       },
+       .num_children = 0,
+       .children = {  }
+};
+/*! \brief REST handler for /api-docs/recordings.{format} */
 static struct stasis_rest_handlers recordings_stored_recordingName_copy = {
        .path_segment = "copy",
        .callbacks = {
 static struct stasis_rest_handlers recordings_stored_recordingName_copy = {
        .path_segment = "copy",
        .callbacks = {
@@ -754,8 +822,8 @@ static struct stasis_rest_handlers recordings_stored_recordingName = {
                [AST_HTTP_GET] = ast_ari_recordings_get_stored_cb,
                [AST_HTTP_DELETE] = ast_ari_recordings_delete_stored_cb,
        },
                [AST_HTTP_GET] = ast_ari_recordings_get_stored_cb,
                [AST_HTTP_DELETE] = ast_ari_recordings_delete_stored_cb,
        },
-       .num_children = 1,
-       .children = { &recordings_stored_recordingName_copy, }
+       .num_children = 2,
+       .children = { &recordings_stored_recordingName_file,&recordings_stored_recordingName_copy, }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
 static struct stasis_rest_handlers recordings_stored = {
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
 static struct stasis_rest_handlers recordings_stored = {
index acabb65..50232c4 100644 (file)
@@ -62,6 +62,24 @@ const char *stasis_app_stored_recording_get_file(
        return recording->file;
 }
 
        return recording->file;
 }
 
+const char *stasis_app_stored_recording_get_filename(
+       struct stasis_app_stored_recording *recording)
+{
+       if (!recording) {
+               return NULL;
+       }
+       return recording->file_with_ext;
+}
+
+const char *stasis_app_stored_recording_get_extension(
+       struct stasis_app_stored_recording *recording)
+{
+       if (!recording) {
+               return NULL;
+       }
+       return recording->format;
+}
+
 /*!
  * \brief Split a path into directory and file, resolving canonical directory.
  *
 /*!
  * \brief Split a path into directory and file, resolving canonical directory.
  *
index 5e06af7..df075af 100644 (file)
@@ -82,11 +82,19 @@ int ast_ari_{{c_name}}_{{c_nickname}}_parse_body(
  * {{{notes}}}
 {{/notes}}
  *
  * {{{notes}}}
 {{/notes}}
  *
+{{#is_binary_response}}
+ * \param ser TCP/TLS session instance
+{{/is_binary_response}}
  * \param headers HTTP headers
  * \param args Swagger parameters
  * \param[out] response HTTP response
  */
  * \param headers HTTP headers
  * \param args Swagger parameters
  * \param[out] response HTTP response
  */
+{{^is_binary_response}}
 void ast_ari_{{c_name}}_{{c_nickname}}(struct ast_variable *headers, struct ast_ari_{{c_name}}_{{c_nickname}}_args *args, struct ast_ari_response *response);
 void ast_ari_{{c_name}}_{{c_nickname}}(struct ast_variable *headers, struct ast_ari_{{c_name}}_{{c_nickname}}_args *args, struct ast_ari_response *response);
+{{/is_binary_response}}
+{{#is_binary_response}}
+void ast_ari_{{c_name}}_{{c_nickname}}(struct ast_tcptls_session_instance *ser, struct ast_variable *headers, struct ast_ari_{{c_name}}_{{c_nickname}}_args *args, struct ast_ari_response *response);
+{{/is_binary_response}}
 {{/is_req}}
 {{#is_websocket}}
 
 {{/is_req}}
 {{#is_websocket}}
 
index 23f2a52..c4e6f3d 100644 (file)
@@ -91,7 +91,12 @@ static void ast_ari_{{c_name}}_{{c_nickname}}_cb(
 #endif /* AST_DEVMODE */
 
 {{> param_parsing}}
 #endif /* AST_DEVMODE */
 
 {{> param_parsing}}
+{{^is_binary_response}}
        ast_ari_{{c_name}}_{{c_nickname}}(headers, &args, response);
        ast_ari_{{c_name}}_{{c_nickname}}(headers, &args, response);
+{{/is_binary_response}}
+{{#is_binary_response}}
+       ast_ari_{{c_name}}_{{c_nickname}}(ser, headers, &args, response);
+{{/is_binary_response}}
 #if defined(AST_DEVMODE)
        code = response->response_code;
 
 #if defined(AST_DEVMODE)
        code = response->response_code;
 
@@ -114,8 +119,14 @@ static void ast_ari_{{c_name}}_{{c_nickname}}_cb(
                                ast_ari_validate_{{c_singular_name}}_fn());
 {{/is_list}}
 {{^is_list}}
                                ast_ari_validate_{{c_singular_name}}_fn());
 {{/is_list}}
 {{^is_list}}
+{{^is_binary_response}}
                        is_valid = ast_ari_validate_{{c_name}}(
                                response->message);
                        is_valid = ast_ari_validate_{{c_name}}(
                                response->message);
+{{/is_binary_response}}
+{{#is_binary_response}}
+                       /* No validation on a raw binary response */
+                       is_valid = 1;
+{{/is_binary_response}}
 {{/is_list}}
 {{/response_class}}
                } else {
 {{/is_list}}
 {{/response_class}}
                } else {
index f3b49e1..c76cb7f 100644 (file)
@@ -332,6 +332,7 @@ class SwaggerType(Stringify):
         self.is_list = None
         self.singular_name = None
         self.is_primitive = None
         self.is_list = None
         self.singular_name = None
         self.is_primitive = None
+        self.is_binary = None
 
     def load(self, type_name, processor, context):
         # Some common errors
 
     def load(self, type_name, processor, context):
         # Some common errors
@@ -346,6 +347,7 @@ class SwaggerType(Stringify):
         else:
             self.singular_name = self.name
         self.is_primitive = self.singular_name in SWAGGER_PRIMITIVES
         else:
             self.singular_name = self.name
         self.is_primitive = self.singular_name in SWAGGER_PRIMITIVES
+        self.is_binary = (self.singular_name == 'binary')
         processor.process_type(self, context)
         return self
 
         processor.process_type(self, context)
         return self
 
@@ -401,6 +403,7 @@ class Operation(Stringify):
         self.has_header_parameters = self.header_parameters and True
         self.has_parameters = self.has_query_parameters or \
             self.has_path_parameters or self.has_header_parameters
         self.has_header_parameters = self.header_parameters and True
         self.has_parameters = self.has_query_parameters or \
             self.has_path_parameters or self.has_header_parameters
+        self.is_binary_response = self.response_class.is_binary
 
         # Body param is different, since there's at most one
         self.body_parameter = [
 
         # Body param is different, since there's at most one
         self.body_parameter = [
index 51f0a21..d173ac9 100644 (file)
                        ]
                },
                {
                        ]
                },
                {
+                       "path": "/recordings/stored/{recordingName}/file",
+                       "description": "The actual file associated with the stored recording",
+                       "operations": [
+                               {
+                                       "httpMethod": "GET",
+                                       "summary": "Get the file associated with the stored recording.",
+                                       "nickname": "getStoredFile",
+                                       "responseClass": "binary",
+                                       "parameters": [
+                                               {
+                                                       "name": "recordingName",
+                                                       "description": "The name of the recording",
+                                                       "paramType": "path",
+                                                       "required": true,
+                                                       "allowMultiple": false,
+                                                       "dataType": "string"
+                                               }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": 403,
+                                                       "reason": "The recording file could not be opened"
+                                               },
+                                               {
+                                                       "code": 404,
+                                                       "reason": "Recording not found"
+                                               }
+                                       ]
+                               }
+                       ]
+               },
+               {
                        "path": "/recordings/stored/{recordingName}/copy",
                        "description": "Copy an individual recording",
                        "operations": [
                        "path": "/recordings/stored/{recordingName}/copy",
                        "description": "Copy an individual recording",
                        "operations": [