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