summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/asterisk/stasis_app_recording.h64
-rw-r--r--res/Makefile5
-rw-r--r--res/ari/ari_model_validators.c45
-rw-r--r--res/ari/ari_model_validators.h6
-rw-r--r--res/ari/resource_recordings.c110
-rw-r--r--res/ari/resource_recordings.h11
-rw-r--r--res/res_ari_recordings.c52
-rw-r--r--res/res_stasis_playback.c15
-rw-r--r--res/res_stasis_recording.c5
-rw-r--r--res/stasis_recording/stored.c470
-rw-r--r--rest-api/api-docs/recordings.json60
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,