ARI: Add the ability to download the media associated with a stored recording

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
changes/76/2876/2
Matt Jordan 9 years ago
parent d4b77dad1b
commit e773e3a9bb

@ -32,6 +32,10 @@ ARI
back to the resource. The "PlaybackFinished" event is raised when all media back to the resource. The "PlaybackFinished" event is raised when all media
URIs are done. 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
------------------ ------------------

@ -95,6 +95,8 @@ struct ast_ari_response {
/*! HTTP response code. /*! HTTP response code.
* See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html */ * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html */
int response_code; int response_code;
/*! File descriptor for whatever file we want to respond with */
int fd;
/*! Corresponding text for the response code */ /*! Corresponding text for the response code */
const char *response_text; /* Shouldn't http.c handle this? */ const char *response_text; /* Shouldn't http.c handle this? */
/*! Flag to indicate that no further response is needed */ /*! Flag to indicate that no further response is needed */

@ -48,6 +48,30 @@ struct stasis_app_stored_recording;
const char *stasis_app_stored_recording_get_file( 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. * \brief Convert stored recording info to JSON.
* *

@ -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, void ast_ari_recordings_copy_stored(struct ast_variable *headers,
struct ast_ari_recordings_copy_stored_args *args, struct ast_ari_recordings_copy_stored_args *args,
struct ast_ari_response *response) struct ast_ari_response *response)

@ -76,6 +76,20 @@ struct ast_ari_recordings_delete_stored_args {
* \param[out] response HTTP 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); 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() */ /*! Argument struct for ast_ari_recordings_copy_stored() */
struct ast_ari_recordings_copy_stored_args { struct ast_ari_recordings_copy_stored_args {
/*! The name of the recording to copy */ /*! The name of the recording to copy */

@ -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_ari_conf *, conf, NULL, ao2_cleanup);
RAII_VAR(struct ast_str *, response_body, ast_str_create(256), ast_free); 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_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); RAII_VAR(struct ast_variable *, post_vars, NULL, ast_variables_destroy);
if (!response_body) { if (!response_body) {
@ -1011,11 +1011,14 @@ request_failed:
response.response_text, ast_str_buffer(response.headers), ast_str_buffer(response_body)); response.response_text, ast_str_buffer(response.headers), ast_str_buffer(response_body));
ast_http_send(ser, method, response.response_code, ast_http_send(ser, method, response.response_code,
response.response_text, response.headers, response_body, 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 */ /* ast_http_send takes ownership, so we don't have to free them */
response_body = NULL; response_body = NULL;
ast_json_unref(response.message); 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, .callback = ast_ari_callback,
.description = "Asterisk RESTful API", .description = "Asterisk RESTful API",
.uri = "ari", .uri = "ari",
.has_subtree = 1, .has_subtree = 1,
.data = NULL, .data = NULL,
.key = __FILE__, .key = __FILE__,

@ -218,6 +218,65 @@ static void ast_ari_recordings_delete_stored_cb(
} }
#endif /* AST_DEVMODE */ #endif /* AST_DEVMODE */
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)) fin: __attribute__((unused))
return; return;
} }
@ -737,6 +796,15 @@ fin: __attribute__((unused))
return; return;
} }
/*! \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} */ /*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_stored_recordingName_copy = { static struct stasis_rest_handlers recordings_stored_recordingName_copy = {
.path_segment = "copy", .path_segment = "copy",
@ -754,8 +822,8 @@ static struct stasis_rest_handlers recordings_stored_recordingName = {
[AST_HTTP_GET] = ast_ari_recordings_get_stored_cb, [AST_HTTP_GET] = ast_ari_recordings_get_stored_cb,
[AST_HTTP_DELETE] = ast_ari_recordings_delete_stored_cb, [AST_HTTP_DELETE] = ast_ari_recordings_delete_stored_cb,
}, },
.num_children = 1, .num_children = 2,
.children = { &recordings_stored_recordingName_copy, } .children = { &recordings_stored_recordingName_file,&recordings_stored_recordingName_copy, }
}; };
/*! \brief REST handler for /api-docs/recordings.{format} */ /*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_stored = { static struct stasis_rest_handlers recordings_stored = {

@ -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.
* *

@ -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 headers HTTP headers
* \param args Swagger parameters * \param args Swagger parameters
* \param[out] response HTTP response * \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_req}}
{{#is_websocket}} {{#is_websocket}}

@ -91,7 +91,12 @@ static void ast_ari_{{c_name}}_{{c_nickname}}_cb(
#endif /* AST_DEVMODE */ #endif /* AST_DEVMODE */
{{> param_parsing}} {{> 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) #if defined(AST_DEVMODE)
code = response->response_code; code = response->response_code;
@ -114,8 +119,14 @@ static void ast_ari_{{c_name}}_{{c_nickname}}_cb(
ast_ari_validate_{{c_singular_name}}_fn()); ast_ari_validate_{{c_singular_name}}_fn());
{{/is_list}} {{/is_list}}
{{^is_list}} {{^is_list}}
{{^is_binary_response}}
is_valid = ast_ari_validate_{{c_name}}( is_valid = ast_ari_validate_{{c_name}}(
response->message); response->message);
{{/is_binary_response}}
{{#is_binary_response}}
/* No validation on a raw binary response */
is_valid = 1;
{{/is_binary_response}}
{{/is_list}} {{/is_list}}
{{/response_class}} {{/response_class}}
} else { } else {

@ -332,6 +332,7 @@ class SwaggerType(Stringify):
self.is_list = None self.is_list = None
self.singular_name = None self.singular_name = None
self.is_primitive = None self.is_primitive = None
self.is_binary = None
def load(self, type_name, processor, context): def load(self, type_name, processor, context):
# Some common errors # Some common errors
@ -346,6 +347,7 @@ class SwaggerType(Stringify):
else: else:
self.singular_name = self.name self.singular_name = self.name
self.is_primitive = self.singular_name in SWAGGER_PRIMITIVES self.is_primitive = self.singular_name in SWAGGER_PRIMITIVES
self.is_binary = (self.singular_name == 'binary')
processor.process_type(self, context) processor.process_type(self, context)
return self return self
@ -401,6 +403,7 @@ class Operation(Stringify):
self.has_header_parameters = self.header_parameters and True self.has_header_parameters = self.header_parameters and True
self.has_parameters = self.has_query_parameters or \ self.has_parameters = self.has_query_parameters or \
self.has_path_parameters or self.has_header_parameters 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 # Body param is different, since there's at most one
self.body_parameter = [ self.body_parameter = [

@ -69,6 +69,38 @@
} }
] ]
}, },
{
"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", "path": "/recordings/stored/{recordingName}/copy",
"description": "Copy an individual recording", "description": "Copy an individual recording",

Loading…
Cancel
Save