diff options
-rw-r--r-- | include/asterisk/stasis_app_recording.h | 64 | ||||
-rw-r--r-- | res/Makefile | 5 | ||||
-rw-r--r-- | res/ari/ari_model_validators.c | 45 | ||||
-rw-r--r-- | res/ari/ari_model_validators.h | 6 | ||||
-rw-r--r-- | res/ari/resource_recordings.c | 110 | ||||
-rw-r--r-- | res/ari/resource_recordings.h | 11 | ||||
-rw-r--r-- | res/res_ari_recordings.c | 52 | ||||
-rw-r--r-- | res/res_stasis_playback.c | 15 | ||||
-rw-r--r-- | res/res_stasis_recording.c | 5 | ||||
-rw-r--r-- | res/stasis_recording/stored.c | 470 | ||||
-rw-r--r-- | rest-api/api-docs/recordings.json | 60 |
11 files changed, 702 insertions, 141 deletions
diff --git a/include/asterisk/stasis_app_recording.h b/include/asterisk/stasis_app_recording.h index e8b4558ab..aa9047054 100644 --- a/include/asterisk/stasis_app_recording.h +++ b/include/asterisk/stasis_app_recording.h @@ -31,6 +31,68 @@ #include "asterisk/app.h" #include "asterisk/stasis_app.h" +/*! @{ */ + +/*! \brief Structure representing a recording stored on disk */ +struct stasis_app_stored_recording; + +/*! + * \brief Returns the filename for this recording, for use with streamfile. + * + * The returned string will be valid until the \a recording object is freed. + * + * \param recording Recording to query. + * \return Absolute path to the recording file, without the extension. + * \return \c NULL on error. + */ +const char *stasis_app_stored_recording_get_file( + struct stasis_app_stored_recording *recording); + +/*! + * \brief Convert stored recording info to JSON. + * + * \param recording Recording to convert. + * \return JSON representation. + * \return \c NULL on error. + */ +struct ast_json *stasis_app_stored_recording_to_json( + struct stasis_app_stored_recording *recording); + +/*! + * \brief Find all stored recordings on disk. + * + * \return Container of \ref stasis_app_stored_recording objects. + * \return \c NULL on error. + */ +struct ao2_container *stasis_app_stored_recording_find_all(void); + +/*! + * \brief Creates a stored recording object, with the given name. + * + * \param name Name of the recording. + * \return New recording object. + * \return \c NULL if recording is not found. \c errno is set to indicate why + * - \c ENOMEM - out of memeory + * - \c EACCES - file permissions (or recording is outside the config dir) + * - Any of the error codes for stat(), opendir(), readdir() + */ +struct stasis_app_stored_recording *stasis_app_stored_recording_find_by_name( + const char *name); + +/*! + * \brief Delete a recording from disk. + * + * \param recording Recording to delete. + * \return 0 on success. + * \return Non-zero on error. + */ +int stasis_app_stored_recording_delete( + struct stasis_app_stored_recording *recording); + +/*! @} */ + +/*! @{ */ + /*! Opaque struct for handling the recording of media to a file. */ struct stasis_app_recording; @@ -216,4 +278,6 @@ enum stasis_app_recording_oper_results stasis_app_recording_operation( */ struct stasis_message_type *stasis_app_recording_snapshot_type(void); +/*! @} */ + #endif /* _ASTERISK_STASIS_APP_RECORDING_H */ diff --git a/res/Makefile b/res/Makefile index c9c8dd857..403863426 100644 --- a/res/Makefile +++ b/res/Makefile @@ -75,7 +75,7 @@ ael/pval.o: ael/pval.c clean:: rm -f snmp/*.[oi] ael/*.[oi] ais/*.[oi] ari/*.[oi] rm -f res_pjsip/*.[oi] stasis/*.[oi] - rm -f parking/*.o parking/*.i + rm -f parking/*.o parking/*.i stasis_recording/*.[oi] $(if $(filter res_parking,$(EMBEDDED_MODS)),modules.link,res_parking.so): $(subst .c,.o,$(wildcard parking/*.c)) $(subst .c,.o,$(wildcard parking/*.c)): _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_parking) @@ -86,5 +86,8 @@ ari/cli.o ari/config.o ari/ari_websockets.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,re res_ari_model.so: ari/ari_model_validators.o ari/ari_model_validators.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ari_model) +res_stasis_recording.so: stasis_recording/stored.o +stasis_recording/stored.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_stasis_recording) + # Dependencies for res_ari_*.so are generated, so they're in this file include ari.make diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c index 6932cf5ac..0905642c8 100644 --- a/res/ari/ari_model_validators.c +++ b/res/ari/ari_model_validators.c @@ -1061,46 +1061,27 @@ int ast_ari_validate_stored_recording(struct ast_json *json) { int res = 1; struct ast_json_iter *iter; - int has_formats = 0; - int has_id = 0; + int has_format = 0; + int has_name = 0; for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) { - if (strcmp("duration_seconds", ast_json_object_iter_key(iter)) == 0) { - int prop_is_valid; - prop_is_valid = ast_ari_validate_int( - ast_json_object_iter_value(iter)); - if (!prop_is_valid) { - ast_log(LOG_ERROR, "ARI StoredRecording field duration_seconds failed validation\n"); - res = 0; - } - } else - if (strcmp("formats", ast_json_object_iter_key(iter)) == 0) { - int prop_is_valid; - has_formats = 1; - prop_is_valid = ast_ari_validate_list( - ast_json_object_iter_value(iter), - ast_ari_validate_string); - if (!prop_is_valid) { - ast_log(LOG_ERROR, "ARI StoredRecording field formats failed validation\n"); - res = 0; - } - } else - if (strcmp("id", ast_json_object_iter_key(iter)) == 0) { + if (strcmp("format", ast_json_object_iter_key(iter)) == 0) { int prop_is_valid; - has_id = 1; + has_format = 1; prop_is_valid = ast_ari_validate_string( ast_json_object_iter_value(iter)); if (!prop_is_valid) { - ast_log(LOG_ERROR, "ARI StoredRecording field id failed validation\n"); + ast_log(LOG_ERROR, "ARI StoredRecording field format failed validation\n"); res = 0; } } else - if (strcmp("time", ast_json_object_iter_key(iter)) == 0) { + if (strcmp("name", ast_json_object_iter_key(iter)) == 0) { int prop_is_valid; - prop_is_valid = ast_ari_validate_date( + has_name = 1; + prop_is_valid = ast_ari_validate_string( ast_json_object_iter_value(iter)); if (!prop_is_valid) { - ast_log(LOG_ERROR, "ARI StoredRecording field time failed validation\n"); + ast_log(LOG_ERROR, "ARI StoredRecording field name failed validation\n"); res = 0; } } else @@ -1112,13 +1093,13 @@ int ast_ari_validate_stored_recording(struct ast_json *json) } } - if (!has_formats) { - ast_log(LOG_ERROR, "ARI StoredRecording missing required field formats\n"); + if (!has_format) { + ast_log(LOG_ERROR, "ARI StoredRecording missing required field format\n"); res = 0; } - if (!has_id) { - ast_log(LOG_ERROR, "ARI StoredRecording missing required field id\n"); + if (!has_name) { + ast_log(LOG_ERROR, "ARI StoredRecording missing required field name\n"); res = 0; } diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h index e8ef8e210..a8a856f15 100644 --- a/res/ari/ari_model_validators.h +++ b/res/ari/ari_model_validators.h @@ -937,10 +937,8 @@ ari_validator ast_ari_validate_stasis_start_fn(void); * - name: string (required) * - state: string (required) * StoredRecording - * - duration_seconds: int - * - formats: List[string] (required) - * - id: string (required) - * - time: Date + * - format: string (required) + * - name: string (required) * FormatLangPair * - format: string (required) * - language: string (required) diff --git a/res/ari/resource_recordings.c b/res/ari/resource_recordings.c index f87ff0a0a..d6803469f 100644 --- a/res/ari/resource_recordings.c +++ b/res/ari/resource_recordings.c @@ -30,21 +30,111 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/stasis_app_recording.h" #include "resource_recordings.h" -void ast_ari_get_stored_recordings(struct ast_variable *headers, struct ast_get_stored_recordings_args *args, struct ast_ari_response *response) -{ - ast_log(LOG_ERROR, "TODO: ast_ari_get_stored_recordings\n"); -} -void ast_ari_get_stored_recording(struct ast_variable *headers, struct ast_get_stored_recording_args *args, struct ast_ari_response *response) +void ast_ari_get_stored_recordings(struct ast_variable *headers, + struct ast_get_stored_recordings_args *args, + struct ast_ari_response *response) { - ast_log(LOG_ERROR, "TODO: ast_ari_get_stored_recording\n"); + RAII_VAR(struct ao2_container *, recordings, NULL, ao2_cleanup); + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + struct ao2_iterator i; + void *obj; + + recordings = stasis_app_stored_recording_find_all(); + + if (!recordings) { + ast_ari_response_alloc_failed(response); + return; + } + + json = ast_json_array_create(); + if (!json) { + ast_ari_response_alloc_failed(response); + return; + } + + i = ao2_iterator_init(recordings, 0); + while ((obj = ao2_iterator_next(&i))) { + RAII_VAR(struct stasis_app_stored_recording *, recording, obj, + ao2_cleanup); + + int r = ast_json_array_append( + json, stasis_app_stored_recording_to_json(recording)); + if (r != 0) { + ast_ari_response_alloc_failed(response); + ao2_iterator_destroy(&i); + return; + } + } + ao2_iterator_destroy(&i); + + ast_ari_response_ok(response, ast_json_ref(json)); } -void ast_ari_delete_stored_recording(struct ast_variable *headers, struct ast_delete_stored_recording_args *args, struct ast_ari_response *response) + +void ast_ari_get_stored_recording(struct ast_variable *headers, + struct ast_get_stored_recording_args *args, + struct ast_ari_response *response) { - ast_log(LOG_ERROR, "TODO: ast_ari_delete_stored_recording\n"); + RAII_VAR(struct stasis_app_stored_recording *, recording, NULL, + ao2_cleanup); + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + + recording = stasis_app_stored_recording_find_by_name( + args->recording_name); + if (recording == NULL) { + ast_ari_response_error(response, 404, "Not Found", + "Recording not found"); + return; + } + + json = stasis_app_stored_recording_to_json(recording); + if (json == NULL) { + ast_ari_response_error(response, 500, + "Internal Server Error", "Error building response"); + return; + } + + ast_ari_response_ok(response, ast_json_ref(json)); } -void ast_ari_get_live_recordings(struct ast_variable *headers, struct ast_get_live_recordings_args *args, struct ast_ari_response *response) + +void ast_ari_delete_stored_recording(struct ast_variable *headers, + struct ast_delete_stored_recording_args *args, + struct ast_ari_response *response) { - ast_log(LOG_ERROR, "TODO: ast_ari_get_live_recordings\n"); + RAII_VAR(struct stasis_app_stored_recording *, recording, NULL, + ao2_cleanup); + int res; + + recording = stasis_app_stored_recording_find_by_name( + args->recording_name); + if (recording == NULL) { + ast_ari_response_error(response, 404, "Not Found", + "Recording not found"); + return; + } + + res = stasis_app_stored_recording_delete(recording); + + if (res != 0) { + switch (errno) { + case EACCES: + case EPERM: + ast_ari_response_error(response, 500, + "Internal Server Error", + "Delete failed"); + break; + default: + ast_log(LOG_WARNING, + "Unexpected error deleting recording %s: %s\n", + args->recording_name, strerror(errno)); + ast_ari_response_error(response, 500, + "Internal Server Error", + "Delete failed"); + break; + } + return; + } + + ast_ari_response_no_content(response); } void ast_ari_get_live_recording(struct ast_variable *headers, diff --git a/res/ari/resource_recordings.h b/res/ari/resource_recordings.h index 2529766e7..682b45c56 100644 --- a/res/ari/resource_recordings.h +++ b/res/ari/resource_recordings.h @@ -76,17 +76,6 @@ struct ast_delete_stored_recording_args { * \param[out] response HTTP response */ void ast_ari_delete_stored_recording(struct ast_variable *headers, struct ast_delete_stored_recording_args *args, struct ast_ari_response *response); -/*! \brief Argument struct for ast_ari_get_live_recordings() */ -struct ast_get_live_recordings_args { -}; -/*! - * \brief List libe recordings. - * - * \param headers HTTP headers - * \param args Swagger parameters - * \param[out] response HTTP response - */ -void ast_ari_get_live_recordings(struct ast_variable *headers, struct ast_get_live_recordings_args *args, struct ast_ari_response *response); /*! \brief Argument struct for ast_ari_get_live_recording() */ struct ast_get_live_recording_args { /*! \brief The name of the recording */ diff --git a/res/res_ari_recordings.c b/res/res_ari_recordings.c index 77fc830cf..3f7458324 100644 --- a/res/res_ari_recordings.c +++ b/res/res_ari_recordings.c @@ -134,6 +134,7 @@ static void ast_ari_get_stored_recording_cb( break; case 500: /* Internal Server Error */ case 501: /* Not Implemented */ + case 404: /* Recording not found */ is_valid = 1; break; default: @@ -190,6 +191,7 @@ static void ast_ari_delete_stored_recording_cb( break; case 500: /* Internal Server Error */ case 501: /* Not Implemented */ + case 404: /* Recording not found */ is_valid = 1; break; default: @@ -213,55 +215,6 @@ fin: __attribute__((unused)) return; } /*! - * \brief Parameter parsing callback for /recordings/live. - * \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_get_live_recordings_cb( - struct ast_variable *get_params, struct ast_variable *path_vars, - struct ast_variable *headers, struct ast_ari_response *response) -{ - struct ast_get_live_recordings_args args = {}; -#if defined(AST_DEVMODE) - int is_valid; - int code; -#endif /* AST_DEVMODE */ - - ast_ari_get_live_recordings(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 */ - is_valid = 1; - break; - default: - if (200 <= code && code <= 299) { - is_valid = ast_ari_validate_list(response->message, - ast_ari_validate_live_recording_fn()); - } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live\n", code); - is_valid = 0; - } - } - - if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/live\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. * \param path_vars Path variables extracted from the request. @@ -745,7 +698,6 @@ static struct stasis_rest_handlers recordings_live_recordingName = { static struct stasis_rest_handlers recordings_live = { .path_segment = "live", .callbacks = { - [AST_HTTP_GET] = ast_ari_get_live_recordings_cb, }, .num_children = 1, .children = { &recordings_live_recordingName, } diff --git a/res/res_stasis_playback.c b/res/res_stasis_playback.c index 83730491c..b55e39fb2 100644 --- a/res/res_stasis_playback.c +++ b/res/res_stasis_playback.c @@ -25,6 +25,7 @@ /*** MODULEINFO <depend type="module">res_stasis</depend> + <depend type="module">res_stasis_recording</depend> <support_level>core</support_level> ***/ @@ -42,6 +43,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/paths.h" #include "asterisk/stasis_app_impl.h" #include "asterisk/stasis_app_playback.h" +#include "asterisk/stasis_app_recording.h" #include "asterisk/stasis_channels.h" #include "asterisk/stringfields.h" #include "asterisk/uuid.h" @@ -279,13 +281,14 @@ static void play_on_channel(struct stasis_app_playback *playback, file = ast_strdup(playback->media + strlen(SOUND_URI_SCHEME)); } else if (ast_begins_with(playback->media, RECORDING_URI_SCHEME)) { /* Play recording */ + RAII_VAR(struct stasis_app_stored_recording *, recording, NULL, + ao2_cleanup); const char *relname = playback->media + strlen(RECORDING_URI_SCHEME); - if (relname[0] == '/') { - file = ast_strdup(relname); - } else { - ast_asprintf(&file, "%s/%s", - ast_config_AST_RECORDING_DIR, relname); + recording = stasis_app_stored_recording_find_by_name(relname); + if (recording) { + file = ast_strdup(stasis_app_stored_recording_get_file( + recording)); } } else { /* Play URL */ @@ -627,4 +630,4 @@ static int unload_module(void) AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, "Stasis application playback support", .load = load_module, .unload = unload_module, - .nonoptreq = "res_stasis"); + .nonoptreq = "res_stasis,res_stasis_recording"); diff --git a/res/res_stasis_recording.c b/res/res_stasis_recording.c index f62716826..49044c443 100644 --- a/res/res_stasis_recording.c +++ b/res/res_stasis_recording.c @@ -564,7 +564,8 @@ static int unload_module(void) return 0; } -AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, "Stasis application recording support", +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Stasis application recording support", .load = load_module, .unload = unload_module, - .nonoptreq = "res_stasis"); + .nonoptreq = "res_stasis", + .load_pri = AST_MODPRI_APP_DEPEND); diff --git a/res/stasis_recording/stored.c b/res/stasis_recording/stored.c new file mode 100644 index 000000000..f7ecaa179 --- /dev/null +++ b/res/stasis_recording/stored.c @@ -0,0 +1,470 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II <dlee@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Stored file operations for Stasis + * + * \author David M. Lee, II <dlee@digium.com> + */ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/astobj2.h" +#include "asterisk/paths.h" +#include "asterisk/stasis_app_recording.h" + +#include <dirent.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +struct stasis_app_stored_recording { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(name); /*!< Recording's name */ + AST_STRING_FIELD(file); /*!< Absolute filename, without extension; for use with streamfile */ + AST_STRING_FIELD(file_with_ext); /*!< Absolute filename, with extension; for use with everything else */ + ); + + const char *format; /*!< Format name (i.e. filename extension) */ +}; + +static void stored_recording_dtor(void *obj) +{ + struct stasis_app_stored_recording *recording = obj; + + ast_string_field_free_memory(recording); +} + +const char *stasis_app_stored_recording_get_file( + struct stasis_app_stored_recording *recording) +{ + if (!recording) { + return NULL; + } + return recording->file; +} + +/*! + * \brief Split a path into directory and file, resolving canonical directory. + * + * The path is resolved relative to the recording directory. Both dir and file + * are allocated strings, which you must ast_free(). + * + * \param path Path to split. + * \param[out] dir Output parameter for directory portion. + * \param[out] fail Output parameter for the file portion. + * \return 0 on success. + * \return Non-zero on error. + */ +static int split_path(const char *path, char **dir, char **file) +{ + RAII_VAR(char *, relative_dir, NULL, ast_free); + RAII_VAR(char *, absolute_dir, NULL, ast_free); + RAII_VAR(char *, real_dir, NULL, free); + char *last_slash; + const char *file_portion; + + relative_dir = ast_strdup(path); + if (!relative_dir) { + return -1; + } + + last_slash = strrchr(relative_dir, '/'); + if (last_slash) { + *last_slash = '\0'; + file_portion = last_slash + 1; + ast_asprintf(&absolute_dir, "%s/%s", + ast_config_AST_RECORDING_DIR, relative_dir); + } else { + /* There is no directory portion */ + file_portion = path; + *relative_dir = '\0'; + absolute_dir = ast_strdup(ast_config_AST_RECORDING_DIR); + } + if (!absolute_dir) { + return -1; + } + + real_dir = realpath(absolute_dir, NULL); + if (!real_dir) { + return -1; + } + + *dir = ast_strdup(real_dir); /* Dupe so we can ast_free() */ + *file = ast_strdup(file_portion); + return 0; +} + +static void safe_closedir(DIR *dirp) +{ + if (!dirp) { + return; + } + closedir(dirp); +} + +/*! + * \brief Finds a recording in the given directory. + * + * This function searchs for a file with the given file name, with a registered + * format that matches its extension. + * + * \param dir_name Directory to search (absolute path). + * \param file File name, without extension. + * \return Absolute path of the recording file. + * \return \c NULL if recording is not found. + */ +static char *find_recording(const char *dir_name, const char *file) +{ + RAII_VAR(DIR *, dir, NULL, safe_closedir); + struct dirent entry; + struct dirent *result = NULL; + char *ext = NULL; + char *file_with_ext = NULL; + + dir = opendir(dir_name); + if (!dir) { + return NULL; + } + + while (readdir_r(dir, &entry, &result) == 0 && result != NULL) { + ext = strrchr(result->d_name, '.'); + + if (!ext) { + /* No file extension; not us */ + continue; + } + *ext++ = '\0'; + + if (strcmp(file, result->d_name) == 0) { + if (!ast_get_format_for_file_ext(ext)) { + ast_log(LOG_WARNING, + "Recording %s: unrecognized format %s\n", + result->d_name, + ext); + /* Keep looking */ + continue; + } + /* We have a winner! */ + break; + } + } + + if (!result) { + return NULL; + } + + ast_asprintf(&file_with_ext, "%s/%s.%s", dir_name, file, ext); + return file_with_ext; +} + +/*! + * \brief Allocate a recording object. + */ +static struct stasis_app_stored_recording *recording_alloc(void) +{ + RAII_VAR(struct stasis_app_stored_recording *, recording, NULL, + ao2_cleanup); + int res; + + recording = ao2_alloc(sizeof(*recording), stored_recording_dtor); + if (!recording) { + return NULL; + } + + res = ast_string_field_init(recording, 255); + if (res != 0) { + return NULL; + } + + ao2_ref(recording, +1); + return recording; +} + +static int recording_sort(const void *obj_left, const void *obj_right, int flags) +{ + const struct stasis_app_stored_recording *object_left = obj_left; + const struct stasis_app_stored_recording *object_right = obj_right; + const char *right_key = obj_right; + int cmp; + + switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) { + case OBJ_POINTER: + right_key = object_right->name; + /* Fall through */ + case OBJ_KEY: + cmp = strcmp(object_left->name, right_key); + break; + case OBJ_PARTIAL_KEY: + /* + * We could also use a partial key struct containing a length + * so strlen() does not get called for every comparison instead. + */ + cmp = strncmp(object_left->name, right_key, strlen(right_key)); + break; + default: + /* Sort can only work on something with a full or partial key. */ + ast_assert(0); + cmp = 0; + break; + } + return cmp; +} + +static int scan(struct ao2_container *recordings, + const char *base_dir, const char *subdir, struct dirent *entry); + +static int scan_file(struct ao2_container *recordings, + const char *base_dir, const char *subdir, const char *filename, + const char *path) +{ + RAII_VAR(struct stasis_app_stored_recording *, recording, NULL, + ao2_cleanup); + RAII_VAR(struct ast_str *, name, NULL, ast_free); + const char *ext; + char *dot; + + ext = strrchr(filename, '.'); + + if (!ext) { + ast_verb(4, " Ignore file without extension: %s\n", + filename); + /* No file extension; not us */ + return 0; + } + ++ext; + + if (!ast_get_format_for_file_ext(ext)) { + ast_verb(4, " Not a media file: %s\n", filename); + /* Not a media file */ + return 0; + } + + recording = recording_alloc(); + if (!recording) { + return -1; + } + + ast_string_field_set(recording, file_with_ext, path); + + /* Build file and format from full path */ + ast_string_field_set(recording, file, path); + dot = strrchr(recording->file, '.'); + *dot = '\0'; + recording->format = dot + 1; + + /* Removed the recording dir from the file for the name. */ + ast_string_field_set(recording, name, + recording->file + strlen(ast_config_AST_RECORDING_DIR) + 1); + + /* Add it to the recordings container */ + ao2_link(recordings, recording); + + return 0; +} + +static int scan_dir(struct ao2_container *recordings, + const char *base_dir, const char *subdir, const char *dirname, + const char *path) +{ + RAII_VAR(DIR *, dir, NULL, safe_closedir); + RAII_VAR(struct ast_str *, rel_dirname, NULL, ast_free); + struct dirent entry; + struct dirent *result = NULL; + + if (strcmp(dirname, ".") == 0 || + strcmp(dirname, "..") == 0) { + ast_verb(4, " Ignoring self/parent dir\n"); + return 0; + } + + /* Build relative dirname */ + rel_dirname = ast_str_create(80); + if (!rel_dirname) { + return -1; + } + if (!ast_strlen_zero(subdir)) { + ast_str_append(&rel_dirname, 0, "%s/", subdir); + } + if (!ast_strlen_zero(dirname)) { + ast_str_append(&rel_dirname, 0, "%s", dirname); + } + + /* Read the directory */ + dir = opendir(path); + if (!dir) { + ast_log(LOG_WARNING, "Error reading dir '%s'\n", path); + return -1; + } + while (readdir_r(dir, &entry, &result) == 0 && result != NULL) { + scan(recordings, base_dir, ast_str_buffer(rel_dirname), result); + } + + return 0; +} + +static int scan(struct ao2_container *recordings, + const char *base_dir, const char *subdir, struct dirent *entry) +{ + RAII_VAR(struct ast_str *, path, NULL, ast_free); + + path = ast_str_create(255); + if (!path) { + return -1; + } + + /* Build file path */ + ast_str_append(&path, 0, "%s", base_dir); + if (!ast_strlen_zero(subdir)) { + ast_str_append(&path, 0, "/%s", subdir); + } + if (entry) { + ast_str_append(&path, 0, "/%s", entry->d_name); + } + ast_verb(4, "Scanning '%s'\n", ast_str_buffer(path)); + + /* Handle this file */ + switch (entry->d_type) { + case DT_REG: + scan_file(recordings, base_dir, subdir, entry->d_name, + ast_str_buffer(path)); + break; + case DT_DIR: + scan_dir(recordings, base_dir, subdir, entry->d_name, + ast_str_buffer(path)); + break; + default: + ast_log(LOG_WARNING, "Skipping %s: not a regular file\n", + ast_str_buffer(path)); + break; + } + + return 0; +} + +struct ao2_container *stasis_app_stored_recording_find_all(void) +{ + RAII_VAR(struct ao2_container *, recordings, NULL, ao2_cleanup); + int res; + + recordings = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, + AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, recording_sort, NULL); + if (!recordings) { + return NULL; + } + + res = scan_dir(recordings, ast_config_AST_RECORDING_DIR, "", "", + ast_config_AST_RECORDING_DIR); + if (res != 0) { + return NULL; + } + + ao2_ref(recordings, +1); + return recordings; +} + +struct stasis_app_stored_recording *stasis_app_stored_recording_find_by_name( + const char *name) +{ + RAII_VAR(struct stasis_app_stored_recording *, recording, NULL, + ao2_cleanup); + RAII_VAR(char *, dir, NULL, ast_free); + RAII_VAR(char *, file, NULL, ast_free); + RAII_VAR(char *, file_with_ext, NULL, ast_free); + int res; + struct stat file_stat; + + errno = 0; + + if (!name) { + errno = EINVAL; + return NULL; + } + + recording = recording_alloc(); + if (!recording) { + return NULL; + } + + res = split_path(name, &dir, &file); + if (res != 0) { + return NULL; + } + ast_string_field_build(recording, file, "%s/%s", dir, file); + + if (!ast_begins_with(dir, ast_config_AST_RECORDING_DIR)) { + /* Attempt to escape the recording directory */ + ast_log(LOG_WARNING, "Attempt to access invalid recording %s\n", + name); + errno = EACCES; + return NULL; + } + + /* The actual name of the recording is file with the config dir + * prefix removed. + */ + ast_string_field_set(recording, name, + recording->file + strlen(ast_config_AST_RECORDING_DIR) + 1); + + file_with_ext = find_recording(dir, file); + if (!file_with_ext) { + return NULL; + } + ast_string_field_set(recording, file_with_ext, file_with_ext); + recording->format = strrchr(recording->file_with_ext, '.'); + if (!recording->format) { + return NULL; + } + ++(recording->format); + + res = stat(file_with_ext, &file_stat); + if (res != 0) { + return NULL; + } + + if (!S_ISREG(file_stat.st_mode)) { + /* Let's not play if it's not a regular file */ + errno = EACCES; + return NULL; + } + + ao2_ref(recording, +1); + return recording; +} + +int stasis_app_stored_recording_delete( + struct stasis_app_stored_recording *recording) +{ + /* Path was validated when the recording object was created */ + return unlink(recording->file_with_ext); +} + +struct ast_json *stasis_app_stored_recording_to_json( + struct stasis_app_stored_recording *recording) +{ + if (!recording) { + return NULL; + } + + return ast_json_pack("{ s: s, s: s }", + "name", recording->name, + "format", recording->format); +} diff --git a/rest-api/api-docs/recordings.json b/rest-api/api-docs/recordings.json index b4dd6d090..5767ce479 100644 --- a/rest-api/api-docs/recordings.json +++ b/rest-api/api-docs/recordings.json @@ -37,6 +37,12 @@ "allowMultiple": false, "dataType": "string" } + ], + "errorResponses": [ + { + "code": 404, + "reason": "Recording not found" + } ] }, { @@ -53,23 +59,17 @@ "allowMultiple": false, "dataType": "string" } + ], + "errorResponses": [ + { + "code": 404, + "reason": "Recording not found" + } ] } ] }, { - "path": "/recordings/live", - "description": "Recordings that are in progress", - "operations": [ - { - "httpMethod": "GET", - "summary": "List libe recordings.", - "nickname": "getLiveRecordings", - "responseClass": "List[LiveRecording]" - } - ] - }, - { "path": "/recordings/live/{recordingName}", "description": "A recording that is in progress", "operations": [ @@ -278,22 +278,13 @@ "id": "StoredRecording", "description": "A past recording that may be played back.", "properties": { - "id": { + "name": { "required": true, "type": "string" }, - "formats": { + "format": { "required": true, - "type": "List[string]" - }, - "duration_seconds": { - "required": false, - "type": "int" - }, - "time": { - "description": "Time recording was started", - "required": false, - "type": "Date" + "type": "string" } } }, @@ -303,7 +294,26 @@ "properties": { "name": { "required": true, - "type": "string" + "type": "string", + "description": "Base name for the recording" + }, + "format": { + "required": true, + "type": "string", + "description": "Recording format (wav, gsm, etc.)" + }, + "state": { + "required": false, + "type": "string", + "allowableValues": { + "valueType": "LIST", + "values": [ + "queued", + "playing", + "paused", + "done" + ] + } }, "state": { "required": true, |