summaryrefslogtreecommitdiff
path: root/res/stasis_recording/stored.c
diff options
context:
space:
mode:
Diffstat (limited to 'res/stasis_recording/stored.c')
-rw-r--r--res/stasis_recording/stored.c470
1 files changed, 470 insertions, 0 deletions
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);
+}