diff options
-rw-r--r-- | CHANGES | 6 | ||||
-rw-r--r-- | include/asterisk/stasis_app_recording.h | 13 | ||||
-rw-r--r-- | res/ari/resource_recordings.c | 73 | ||||
-rw-r--r-- | res/ari/resource_recordings.h | 26 | ||||
-rw-r--r-- | res/res_ari_recordings.c | 111 | ||||
-rw-r--r-- | res/stasis_recording/stored.c | 50 | ||||
-rw-r--r-- | rest-api/api-docs/recordings.json | 40 |
7 files changed, 317 insertions, 2 deletions
@@ -186,6 +186,12 @@ res_config_pgsql --- Functionality changes from Asterisk 12.4.0 to Asterisk 12.5.0 ------------ ------------------------------------------------------------------------------ +ARI +------------------ + * Stored recordings now support a new operation, copy. This will take an + existing stored recording and copy it to a new location in the recordings + directory. + res_pjsip ------------------ * The endpoint configuration object now supports 'accountcode'. Any channel diff --git a/include/asterisk/stasis_app_recording.h b/include/asterisk/stasis_app_recording.h index f67c204ef..543207a47 100644 --- a/include/asterisk/stasis_app_recording.h +++ b/include/asterisk/stasis_app_recording.h @@ -80,6 +80,19 @@ struct stasis_app_stored_recording *stasis_app_stored_recording_find_by_name( const char *name); /*! + * \brief Copy a recording. + * + * \param src_recording The recording to copy + * \param dst The destination of the recording to make + * \param dst_recording If successful, the stored recording created as a result of the copy + * + * \retval 0 on success + * \retval Non-zero on error + */ +int stasis_app_stored_recording_copy(struct stasis_app_stored_recording *src_recording, const char *dst, + struct stasis_app_stored_recording **dst_recording); + +/*! * \brief Delete a recording from disk. * * \param recording Recording to delete. diff --git a/res/ari/resource_recordings.c b/res/ari/resource_recordings.c index bc07d5599..bd273aa62 100644 --- a/res/ari/resource_recordings.c +++ b/res/ari/resource_recordings.c @@ -96,6 +96,79 @@ void ast_ari_recordings_get_stored(struct ast_variable *headers, ast_ari_response_ok(response, json); } +void ast_ari_recordings_copy_stored(struct ast_variable *headers, + struct ast_ari_recordings_copy_stored_args *args, + struct ast_ari_response *response) +{ + RAII_VAR(struct stasis_app_stored_recording *, src_recording, NULL, + ao2_cleanup); + RAII_VAR(struct stasis_app_stored_recording *, dst_recording, NULL, + ao2_cleanup); + struct ast_json *json; + int res; + + src_recording = stasis_app_stored_recording_find_by_name( + args->recording_name); + if (src_recording == NULL) { + ast_ari_response_error(response, 404, "Not Found", + "Recording not found"); + return; + } + + dst_recording = stasis_app_stored_recording_find_by_name( + args->destination_recording_name); + if (dst_recording) { + ast_ari_response_error(response, 409, "Conflict", + "A recording with the same name already exists on the system"); + return; + } + + /* See if we got our name rejected */ + switch (errno) { + case EINVAL: + ast_ari_response_error(response, 400, "Bad request", + "Invalid destination recording name"); + return; + case EACCES: + ast_ari_response_error(response, 403, "Forbidden", + "Destination file path is forbidden"); + return; + default: + break; + } + + res = stasis_app_stored_recording_copy(src_recording, + args->destination_recording_name, &dst_recording); + if (res) { + switch (errno) { + case EACCES: + case EPERM: + ast_ari_response_error(response, 500, + "Internal Server Error", + "Copy failed"); + break; + default: + ast_log(LOG_WARNING, + "Unexpected error copying recording %s to %s: %s\n", + args->recording_name, args->destination_recording_name, strerror(errno)); + ast_ari_response_error(response, 500, + "Internal Server Error", + "Copy failed"); + break; + } + return; + } + + json = stasis_app_stored_recording_to_json(dst_recording); + if (json == NULL) { + ast_ari_response_error(response, 500, + "Internal Server Error", "Error building response"); + return; + } + + ast_ari_response_ok(response, json); +} + void ast_ari_recordings_delete_stored(struct ast_variable *headers, struct ast_ari_recordings_delete_stored_args *args, struct ast_ari_response *response) diff --git a/res/ari/resource_recordings.h b/res/ari/resource_recordings.h index 039cb168a..196122f44 100644 --- a/res/ari/resource_recordings.h +++ b/res/ari/resource_recordings.h @@ -76,6 +76,32 @@ 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); +/*! Argument struct for ast_ari_recordings_copy_stored() */ +struct ast_ari_recordings_copy_stored_args { + /*! The name of the recording to copy */ + const char *recording_name; + /*! The destination name of the recording */ + const char *destination_recording_name; +}; +/*! + * \brief Body parsing function for /recordings/stored/{recordingName}/copy. + * \param body The JSON body from which to parse parameters. + * \param[out] args The args structure to parse into. + * \retval zero on success + * \retval non-zero on failure + */ +int ast_ari_recordings_copy_stored_parse_body( + struct ast_json *body, + struct ast_ari_recordings_copy_stored_args *args); + +/*! + * \brief Copy a stored recording. + * + * \param headers HTTP headers + * \param args Swagger parameters + * \param[out] response HTTP response + */ +void ast_ari_recordings_copy_stored(struct ast_variable *headers, struct ast_ari_recordings_copy_stored_args *args, struct ast_ari_response *response); /*! Argument struct for ast_ari_recordings_get_live() */ struct ast_ari_recordings_get_live_args { /*! The name of the recording */ diff --git a/res/res_ari_recordings.c b/res/res_ari_recordings.c index 56fb9ecca..3f4ef2951 100644 --- a/res/res_ari_recordings.c +++ b/res/res_ari_recordings.c @@ -220,6 +220,104 @@ static void ast_ari_recordings_delete_stored_cb( fin: __attribute__((unused)) return; } +int ast_ari_recordings_copy_stored_parse_body( + struct ast_json *body, + struct ast_ari_recordings_copy_stored_args *args) +{ + struct ast_json *field; + /* Parse query parameters out of it */ + field = ast_json_object_get(body, "destinationRecordingName"); + if (field) { + args->destination_recording_name = ast_json_string_get(field); + } + return 0; +} + +/*! + * \brief Parameter parsing callback for /recordings/stored/{recordingName}/copy. + * \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_copy_stored_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_copy_stored_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 = get_params; i; i = i->next) { + if (strcmp(i->name, "destinationRecordingName") == 0) { + args.destination_recording_name = (i->value); + } else + {} + } + for (i = path_vars; i; i = i->next) { + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); + } else + {} + } + /* Look for a JSON request entity */ + body = ast_http_get_json(ser, headers); + if (!body) { + switch (errno) { + case EFBIG: + ast_ari_response_error(response, 413, "Request Entity Too Large", "Request body too large"); + goto fin; + case ENOMEM: + ast_ari_response_error(response, 500, "Internal Server Error", "Error processing request"); + goto fin; + case EIO: + ast_ari_response_error(response, 400, "Bad Request", "Error parsing request body"); + goto fin; + } + } + if (ast_ari_recordings_copy_stored_parse_body(body, &args)) { + ast_ari_response_alloc_failed(response); + goto fin; + } + ast_ari_recordings_copy_stored(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 */ + case 409: /* A recording with the same name already exists on the system */ + is_valid = 1; + break; + default: + if (200 <= code && code <= 299) { + is_valid = ast_ari_validate_stored_recording( + response->message); + } else { + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}/copy\n", code); + is_valid = 0; + } + } + + if (!is_valid) { + ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}/copy\n"); + ast_ari_response_error(response, 500, + "Internal Server Error", "Response validation failed"); + } +#endif /* AST_DEVMODE */ + +fin: __attribute__((unused)) + return; +} /*! * \brief Parameter parsing callback for /recordings/live/{recordingName}. * \param get_params GET parameters in the HTTP request. @@ -639,6 +737,15 @@ fin: __attribute__((unused)) } /*! \brief REST handler for /api-docs/recordings.{format} */ +static struct stasis_rest_handlers recordings_stored_recordingName_copy = { + .path_segment = "copy", + .callbacks = { + [AST_HTTP_POST] = ast_ari_recordings_copy_stored_cb, + }, + .num_children = 0, + .children = { } +}; +/*! \brief REST handler for /api-docs/recordings.{format} */ static struct stasis_rest_handlers recordings_stored_recordingName = { .path_segment = "recordingName", .is_wildcard = 1, @@ -646,8 +753,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, }, - .num_children = 0, - .children = { } + .num_children = 1, + .children = { &recordings_stored_recordingName_copy, } }; /*! \brief REST handler for /api-docs/recordings.{format} */ static struct stasis_rest_handlers recordings_stored = { diff --git a/res/stasis_recording/stored.c b/res/stasis_recording/stored.c index 03af06987..6cc60fd28 100644 --- a/res/stasis_recording/stored.c +++ b/res/stasis_recording/stored.c @@ -458,6 +458,56 @@ struct stasis_app_stored_recording *stasis_app_stored_recording_find_by_name( return recording; } +int stasis_app_stored_recording_copy(struct stasis_app_stored_recording *src_recording, const char *dst, + struct stasis_app_stored_recording **dst_recording) +{ + RAII_VAR(char *, full_path, NULL, ast_free); + char *dst_file = ast_strdupa(dst); + char *format; + char *last_slash; + int res; + + /* Drop the extension if specified, core will do this for us */ + format = strrchr(dst_file, '.'); + if (format) { + format = '\0'; + } + + /* See if any intermediary directories need to be made */ + last_slash = strrchr(dst_file, '/'); + if (last_slash) { + RAII_VAR(char *, tmp_path, NULL, ast_free); + + *last_slash = '\0'; + if (ast_asprintf(&tmp_path, "%s/%s", ast_config_AST_RECORDING_DIR, dst_file) < 0) { + return -1; + } + if (ast_safe_mkdir(ast_config_AST_RECORDING_DIR, + tmp_path, 0777) != 0) { + /* errno set by ast_mkdir */ + return -1; + } + *last_slash = '/'; + if (ast_asprintf(&full_path, "%s/%s", ast_config_AST_RECORDING_DIR, dst_file) < 0) { + return -1; + } + } else { + /* There is no directory portion */ + if (ast_asprintf(&full_path, "%s/%s", ast_config_AST_RECORDING_DIR, dst_file) < 0) { + return -1; + } + } + + ast_verb(4, "Copying recording %s to %s (format %s)\n", src_recording->file, + full_path, src_recording->format); + res = ast_filecopy(src_recording->file, full_path, src_recording->format); + if (!res) { + *dst_recording = stasis_app_stored_recording_find_by_name(dst_file); + } + + return res; +} + int stasis_app_stored_recording_delete( struct stasis_app_stored_recording *recording) { diff --git a/rest-api/api-docs/recordings.json b/rest-api/api-docs/recordings.json index 1f825a2b0..54daa33cf 100644 --- a/rest-api/api-docs/recordings.json +++ b/rest-api/api-docs/recordings.json @@ -70,6 +70,46 @@ ] }, { + "path": "/recordings/stored/{recordingName}/copy", + "description": "Copy an individual recording", + "operations": [ + { + "httpMethod": "POST", + "summary": "Copy a stored recording.", + "nickname": "copyStored", + "responseClass": "StoredRecording", + "parameters": [ + { + "name": "recordingName", + "description": "The name of the recording to copy", + "paramType": "path", + "required": true, + "allowMultiple": false, + "dataType": "string" + }, + { + "name": "destinationRecordingName", + "description": "The destination name of the recording", + "paramType": "query", + "required": true, + "allowMultiple": false, + "dataType": "string" + } + ], + "errorResponses": [ + { + "code": 404, + "reason": "Recording not found" + }, + { + "code": 409, + "reason": "A recording with the same name already exists on the system" + } + ] + } + ] + }, + { "path": "/recordings/live/{recordingName}", "description": "A recording that is in progress", "operations": [ |