ari: Add a copy operation for stored recordings
[asterisk/asterisk.git] / res / stasis_recording / stored.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2013, Digium, Inc.
5  *
6  * David M. Lee, II <dlee@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  *
21  * \brief Stored file operations for Stasis
22  *
23  * \author David M. Lee, II <dlee@digium.com>
24  */
25
26 #include "asterisk.h"
27
28 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
29
30 #include "asterisk/astobj2.h"
31 #include "asterisk/paths.h"
32 #include "asterisk/stasis_app_recording.h"
33
34 #include <dirent.h>
35 #include <sys/stat.h>
36 #include <sys/types.h>
37 #include <unistd.h>
38
39 struct stasis_app_stored_recording {
40         AST_DECLARE_STRING_FIELDS(
41                 AST_STRING_FIELD(name); /*!< Recording's name */
42                 AST_STRING_FIELD(file); /*!< Absolute filename, without extension; for use with streamfile */
43                 AST_STRING_FIELD(file_with_ext);        /*!< Absolute filename, with extension; for use with everything else */
44                 );
45
46         const char *format;     /*!< Format name (i.e. filename extension) */
47 };
48
49 static void stored_recording_dtor(void *obj)
50 {
51         struct stasis_app_stored_recording *recording = obj;
52
53         ast_string_field_free_memory(recording);
54 }
55
56 const char *stasis_app_stored_recording_get_file(
57         struct stasis_app_stored_recording *recording)
58 {
59         if (!recording) {
60                 return NULL;
61         }
62         return recording->file;
63 }
64
65 /*!
66  * \brief Split a path into directory and file, resolving canonical directory.
67  *
68  * The path is resolved relative to the recording directory. Both dir and file
69  * are allocated strings, which you must ast_free().
70  *
71  * \param path Path to split.
72  * \param[out] dir Output parameter for directory portion.
73  * \param[out] fail Output parameter for the file portion.
74  * \return 0 on success.
75  * \return Non-zero on error.
76  */
77 static int split_path(const char *path, char **dir, char **file)
78 {
79         RAII_VAR(char *, relative_dir, NULL, ast_free);
80         RAII_VAR(char *, absolute_dir, NULL, ast_free);
81         RAII_VAR(char *, real_dir, NULL, ast_std_free);
82         char *last_slash;
83         const char *file_portion;
84
85         relative_dir = ast_strdup(path);
86         if (!relative_dir) {
87                 return -1;
88         }
89
90         last_slash = strrchr(relative_dir, '/');
91         if (last_slash) {
92                 *last_slash = '\0';
93                 file_portion = last_slash + 1;
94                 ast_asprintf(&absolute_dir, "%s/%s",
95                         ast_config_AST_RECORDING_DIR, relative_dir);
96         } else {
97                 /* There is no directory portion */
98                 file_portion = path;
99                 *relative_dir = '\0';
100                 absolute_dir = ast_strdup(ast_config_AST_RECORDING_DIR);
101         }
102         if (!absolute_dir) {
103                 return -1;
104         }
105
106         real_dir = realpath(absolute_dir, NULL);
107         if (!real_dir) {
108                 return -1;
109         }
110
111 #if defined(__AST_DEBUG_MALLOC)
112         *dir = ast_strdup(real_dir); /* Dupe so we can ast_free() */
113 #else
114         /*
115          * ast_std_free() and ast_free() are the same thing at this time
116          * so we don't need to dupe.
117          */
118         *dir = real_dir;
119         real_dir = NULL;
120 #endif  /* defined(__AST_DEBUG_MALLOC) */
121         *file = ast_strdup(file_portion);
122         return 0;
123 }
124
125 static void safe_closedir(DIR *dirp)
126 {
127         if (!dirp) {
128                 return;
129         }
130         closedir(dirp);
131 }
132
133 /*!
134  * \brief Finds a recording in the given directory.
135  *
136  * This function searchs for a file with the given file name, with a registered
137  * format that matches its extension.
138  *
139  * \param dir_name Directory to search (absolute path).
140  * \param file File name, without extension.
141  * \return Absolute path of the recording file.
142  * \return \c NULL if recording is not found.
143  */
144 static char *find_recording(const char *dir_name, const char *file)
145 {
146         RAII_VAR(DIR *, dir, NULL, safe_closedir);
147         struct dirent entry;
148         struct dirent *result = NULL;
149         char *ext = NULL;
150         char *file_with_ext = NULL;
151
152         dir = opendir(dir_name);
153         if (!dir) {
154                 return NULL;
155         }
156
157         while (readdir_r(dir, &entry, &result) == 0 && result != NULL) {
158                 ext = strrchr(result->d_name, '.');
159
160                 if (!ext) {
161                         /* No file extension; not us */
162                         continue;
163                 }
164                 *ext++ = '\0';
165
166                 if (strcmp(file, result->d_name) == 0) {
167                         if (!ast_get_format_for_file_ext(ext)) {
168                                 ast_log(LOG_WARNING,
169                                         "Recording %s: unrecognized format %s\n",
170                                         result->d_name,
171                                         ext);
172                                 /* Keep looking */
173                                 continue;
174                         }
175                         /* We have a winner! */
176                         break;
177                 }
178         }
179
180         if (!result) {
181                 return NULL;
182         }
183
184         ast_asprintf(&file_with_ext, "%s/%s.%s", dir_name, file, ext);
185         return file_with_ext;
186 }
187
188 /*!
189  * \brief Allocate a recording object.
190  */
191 static struct stasis_app_stored_recording *recording_alloc(void)
192 {
193         RAII_VAR(struct stasis_app_stored_recording *, recording, NULL,
194                 ao2_cleanup);
195         int res;
196
197         recording = ao2_alloc(sizeof(*recording), stored_recording_dtor);
198         if (!recording) {
199                 return NULL;
200         }
201
202         res = ast_string_field_init(recording, 255);
203         if (res != 0) {
204                 return NULL;
205         }
206
207         ao2_ref(recording, +1);
208         return recording;
209 }
210
211 static int recording_sort(const void *obj_left, const void *obj_right, int flags)
212 {
213         const struct stasis_app_stored_recording *object_left = obj_left;
214         const struct stasis_app_stored_recording *object_right = obj_right;
215         const char *right_key = obj_right;
216         int cmp;
217
218         switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
219         case OBJ_POINTER:
220                 right_key = object_right->name;
221                 /* Fall through */
222         case OBJ_KEY:
223                 cmp = strcmp(object_left->name, right_key);
224                 break;
225         case OBJ_PARTIAL_KEY:
226                 /*
227                  * We could also use a partial key struct containing a length
228                  * so strlen() does not get called for every comparison instead.
229                  */
230                 cmp = strncmp(object_left->name, right_key, strlen(right_key));
231                 break;
232         default:
233                 /* Sort can only work on something with a full or partial key. */
234                 ast_assert(0);
235                 cmp = 0;
236                 break;
237         }
238         return cmp;
239 }
240
241 static int scan(struct ao2_container *recordings,
242         const char *base_dir, const char *subdir, struct dirent *entry);
243
244 static int scan_file(struct ao2_container *recordings,
245         const char *base_dir, const char *subdir, const char *filename,
246         const char *path)
247 {
248         RAII_VAR(struct stasis_app_stored_recording *, recording, NULL,
249                 ao2_cleanup);
250         const char *ext;
251         char *dot;
252
253         ext = strrchr(filename, '.');
254
255         if (!ext) {
256                 ast_verb(4, "  Ignore file without extension: %s\n",
257                         filename);
258                 /* No file extension; not us */
259                 return 0;
260         }
261         ++ext;
262
263         if (!ast_get_format_for_file_ext(ext)) {
264                 ast_verb(4, "  Not a media file: %s\n", filename);
265                 /* Not a media file */
266                 return 0;
267         }
268
269         recording = recording_alloc();
270         if (!recording) {
271                 return -1;
272         }
273
274         ast_string_field_set(recording, file_with_ext, path);
275
276         /* Build file and format from full path */
277         ast_string_field_set(recording, file, path);
278         dot = strrchr(recording->file, '.');
279         *dot = '\0';
280         recording->format = dot + 1;
281
282         /* Removed the recording dir from the file for the name. */
283         ast_string_field_set(recording, name,
284                 recording->file + strlen(ast_config_AST_RECORDING_DIR) + 1);
285
286         /* Add it to the recordings container */
287         ao2_link(recordings, recording);
288
289         return 0;
290 }
291
292 static int scan_dir(struct ao2_container *recordings,
293         const char *base_dir, const char *subdir, const char *dirname,
294         const char *path)
295 {
296         RAII_VAR(DIR *, dir, NULL, safe_closedir);
297         RAII_VAR(struct ast_str *, rel_dirname, NULL, ast_free);
298         struct dirent entry;
299         struct dirent *result = NULL;
300
301         if (strcmp(dirname, ".") == 0 ||
302                 strcmp(dirname, "..") == 0) {
303                 ast_verb(4, "  Ignoring self/parent dir\n");
304                 return 0;
305         }
306
307         /* Build relative dirname */
308         rel_dirname = ast_str_create(80);
309         if (!rel_dirname) {
310                 return -1;
311         }
312         if (!ast_strlen_zero(subdir)) {
313                 ast_str_append(&rel_dirname, 0, "%s/", subdir);
314         }
315         if (!ast_strlen_zero(dirname)) {
316                 ast_str_append(&rel_dirname, 0, "%s", dirname);
317         }
318
319         /* Read the directory */
320         dir = opendir(path);
321         if (!dir) {
322                 ast_log(LOG_WARNING, "Error reading dir '%s'\n", path);
323                 return -1;
324         }
325         while (readdir_r(dir, &entry, &result) == 0 && result != NULL) {
326                 scan(recordings, base_dir, ast_str_buffer(rel_dirname), result);
327         }
328
329         return 0;
330 }
331
332 static int scan(struct ao2_container *recordings,
333         const char *base_dir, const char *subdir, struct dirent *entry)
334 {
335         RAII_VAR(struct ast_str *, path, NULL, ast_free);
336
337         path = ast_str_create(255);
338         if (!path) {
339                 return -1;
340         }
341
342         /* Build file path */
343         ast_str_append(&path, 0, "%s", base_dir);
344         if (!ast_strlen_zero(subdir)) {
345                 ast_str_append(&path, 0, "/%s", subdir);
346         }
347         if (entry) {
348                 ast_str_append(&path, 0, "/%s", entry->d_name);
349         }
350         ast_verb(4, "Scanning '%s'\n", ast_str_buffer(path));
351
352         /* Handle this file */
353         switch (entry->d_type) {
354         case DT_REG:
355                 scan_file(recordings, base_dir, subdir, entry->d_name,
356                         ast_str_buffer(path));
357                 break;
358         case DT_DIR:
359                 scan_dir(recordings, base_dir, subdir, entry->d_name,
360                         ast_str_buffer(path));
361                 break;
362         default:
363                 ast_log(LOG_WARNING, "Skipping %s: not a regular file\n",
364                         ast_str_buffer(path));
365                 break;
366         }
367
368         return 0;
369 }
370
371 struct ao2_container *stasis_app_stored_recording_find_all(void)
372 {
373         RAII_VAR(struct ao2_container *, recordings, NULL, ao2_cleanup);
374         int res;
375
376         recordings = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
377                 AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, recording_sort, NULL);
378         if (!recordings) {
379                 return NULL;
380         }
381
382         res = scan_dir(recordings, ast_config_AST_RECORDING_DIR, "", "",
383                 ast_config_AST_RECORDING_DIR);
384         if (res != 0) {
385                 return NULL;
386         }
387
388         ao2_ref(recordings, +1);
389         return recordings;
390 }
391
392 struct stasis_app_stored_recording *stasis_app_stored_recording_find_by_name(
393         const char *name)
394 {
395         RAII_VAR(struct stasis_app_stored_recording *, recording, NULL,
396                 ao2_cleanup);
397         RAII_VAR(char *, dir, NULL, ast_free);
398         RAII_VAR(char *, file, NULL, ast_free);
399         RAII_VAR(char *, file_with_ext, NULL, ast_free);
400         int res;
401         struct stat file_stat;
402
403         errno = 0;
404
405         if (!name) {
406                 errno = EINVAL;
407                 return NULL;
408         }
409
410         recording = recording_alloc();
411         if (!recording) {
412                 return NULL;
413         }
414
415         res = split_path(name, &dir, &file);
416         if (res != 0) {
417                 return NULL;
418         }
419         ast_string_field_build(recording, file, "%s/%s", dir, file);
420
421         if (!ast_begins_with(dir, ast_config_AST_RECORDING_DIR)) {
422                 /* Attempt to escape the recording directory */
423                 ast_log(LOG_WARNING, "Attempt to access invalid recording %s\n",
424                         name);
425                 errno = EACCES;
426                 return NULL;
427         }
428
429         /* The actual name of the recording is file with the config dir
430          * prefix removed.
431          */
432         ast_string_field_set(recording, name,
433                 recording->file + strlen(ast_config_AST_RECORDING_DIR) + 1);
434
435         file_with_ext = find_recording(dir, file);
436         if (!file_with_ext) {
437                 return NULL;
438         }
439         ast_string_field_set(recording, file_with_ext, file_with_ext);
440         recording->format = strrchr(recording->file_with_ext, '.');
441         if (!recording->format) {
442                 return NULL;
443         }
444         ++(recording->format);
445
446         res = stat(file_with_ext, &file_stat);
447         if (res != 0) {
448                 return NULL;
449         }
450
451         if (!S_ISREG(file_stat.st_mode)) {
452                 /* Let's not play if it's not a regular file */
453                 errno = EACCES;
454                 return NULL;
455         }
456
457         ao2_ref(recording, +1);
458         return recording;
459 }
460
461 int stasis_app_stored_recording_copy(struct stasis_app_stored_recording *src_recording, const char *dst,
462         struct stasis_app_stored_recording **dst_recording)
463 {
464         RAII_VAR(char *, full_path, NULL, ast_free);
465         char *dst_file = ast_strdupa(dst);
466         char *format;
467         char *last_slash;
468         int res;
469
470         /* Drop the extension if specified, core will do this for us */
471         format = strrchr(dst_file, '.');
472         if (format) {
473                 format = '\0';
474         }
475
476         /* See if any intermediary directories need to be made */
477         last_slash = strrchr(dst_file, '/');
478         if (last_slash) {
479                 RAII_VAR(char *, tmp_path, NULL, ast_free);
480
481                 *last_slash = '\0';
482                 if (ast_asprintf(&tmp_path, "%s/%s", ast_config_AST_RECORDING_DIR, dst_file) < 0) {
483                         return -1;
484                 }
485                 if (ast_safe_mkdir(ast_config_AST_RECORDING_DIR,
486                                 tmp_path, 0777) != 0) {
487                         /* errno set by ast_mkdir */
488                         return -1;
489                 }
490                 *last_slash = '/';
491                 if (ast_asprintf(&full_path, "%s/%s", ast_config_AST_RECORDING_DIR, dst_file) < 0) {
492                         return -1;
493                 }
494         } else {
495                 /* There is no directory portion */
496                 if (ast_asprintf(&full_path, "%s/%s", ast_config_AST_RECORDING_DIR, dst_file) < 0) {
497                         return -1;
498                 }
499         }
500
501         ast_verb(4, "Copying recording %s to %s (format %s)\n", src_recording->file,
502                 full_path, src_recording->format);
503         res = ast_filecopy(src_recording->file, full_path, src_recording->format);
504         if (!res) {
505                 *dst_recording = stasis_app_stored_recording_find_by_name(dst_file);
506         }
507
508         return res;
509 }
510
511 int stasis_app_stored_recording_delete(
512         struct stasis_app_stored_recording *recording)
513 {
514         /* Path was validated when the recording object was created */
515         return unlink(recording->file_with_ext);
516 }
517
518 struct ast_json *stasis_app_stored_recording_to_json(
519         struct stasis_app_stored_recording *recording)
520 {
521         if (!recording) {
522                 return NULL;
523         }
524
525         return ast_json_pack("{ s: s, s: s }",
526                 "name", recording->name,
527                 "format", recording->format);
528 }