BuildSystem: Remove unused variables.
[asterisk/asterisk.git] / main / media_cache.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2015, Matt Jordan
5  *
6  * Matt Jordan <mjordan@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 /*!
20  * \file
21  * \brief An in-memory media cache
22  *
23  * \author \verbatim Matt Jordan <mjordan@digium.com> \endverbatim
24  *
25  */
26
27 /*** MODULEINFO
28         <support_level>core</support_level>
29  ***/
30
31 #include "asterisk.h"
32
33 #include <sys/stat.h>
34 #include "asterisk/config.h"
35 #include "asterisk/bucket.h"
36 #include "asterisk/astdb.h"
37 #include "asterisk/cli.h"
38 #include "asterisk/media_cache.h"
39
40 /*! The name of the AstDB family holding items in the cache. */
41 #define AST_DB_FAMILY "MediaCache"
42
43 /*! Length of 'MediaCache' + 2 '/' characters */
44 #define AST_DB_FAMILY_LEN 12
45
46 /*! Number of buckets in the ao2 container holding our media items */
47 #define AO2_BUCKETS 61
48
49 /*! Our one and only container holding media items */
50 static struct ao2_container *media_cache;
51
52 int ast_media_cache_exists(const char *uri)
53 {
54         struct ast_bucket_file *bucket_file;
55
56         if (ast_strlen_zero(uri)) {
57                 return 0;
58         }
59
60         bucket_file = ao2_find(media_cache, uri, OBJ_SEARCH_KEY);
61         if (bucket_file) {
62                 ao2_ref(bucket_file, -1);
63                 return 1;
64         }
65
66         /* Check to see if any bucket implementation could return this item */
67         bucket_file = ast_bucket_file_retrieve(uri);
68         if (bucket_file) {
69                 ao2_ref(bucket_file, -1);
70                 return 1;
71         }
72
73         return 0;
74 }
75
76 /*!
77  * \internal
78  * \brief Sync \c bucket_file metadata to the AstDB
79  */
80 static int metadata_sync_to_astdb(void *obj, void *arg, int flags)
81 {
82         struct ast_bucket_metadata *metadata = obj;
83         const char *hash = arg;
84
85         ast_db_put(hash, metadata->name, metadata->value);
86
87         return 0;
88 }
89
90 /*!
91  * \internal
92  * \brief Sync a media cache item to the AstDB
93  * \param bucket_file The \c ast_bucket_file media cache item to sync
94  */
95 static void media_cache_item_sync_to_astdb(struct ast_bucket_file *bucket_file)
96 {
97         char hash[41]; /* 40 character SHA1 hash */
98
99         ast_sha1_hash(hash, ast_sorcery_object_get_id(bucket_file));
100         if (ast_db_put(AST_DB_FAMILY, ast_sorcery_object_get_id(bucket_file), hash)) {
101                 return;
102         }
103
104         ast_db_put(hash, "path", bucket_file->path);
105         ast_bucket_file_metadata_callback(bucket_file, metadata_sync_to_astdb, hash);
106 }
107
108 /*!
109  * \internal
110  * \brief Delete a media cache item from the AstDB
111  * \param bucket_file The \c ast_bucket_file media cache item to delete
112  */
113 static void media_cache_item_del_from_astdb(struct ast_bucket_file *bucket_file)
114 {
115         char *hash_value;
116
117         if (ast_db_get_allocated(AST_DB_FAMILY, ast_sorcery_object_get_id(bucket_file), &hash_value)) {
118                 return;
119         }
120
121         ast_db_deltree(hash_value, NULL);
122         ast_db_del(AST_DB_FAMILY, ast_sorcery_object_get_id(bucket_file));
123         ast_free(hash_value);
124 }
125
126 /*!
127  * \internal
128  * \brief Update the name of the file backing a \c bucket_file
129  * \param preferred_file_name The preferred name of the backing file
130  */
131 static void bucket_file_update_path(struct ast_bucket_file *bucket_file,
132         const char *preferred_file_name)
133 {
134         char *ext;
135
136         if (!ast_strlen_zero(preferred_file_name) && strcmp(bucket_file->path, preferred_file_name)) {
137                 /* Use the preferred file name if available */
138
139                 rename(bucket_file->path, preferred_file_name);
140                 ast_copy_string(bucket_file->path, preferred_file_name,
141                         sizeof(bucket_file->path));
142         } else if (!strchr(bucket_file->path, '.') && (ext = strrchr(ast_sorcery_object_get_id(bucket_file), '.'))) {
143                 /* If we don't have a file extension and were provided one in the URI, use it */
144                 char new_path[PATH_MAX];
145
146                 ast_bucket_file_metadata_set(bucket_file, "ext", ext);
147
148                 snprintf(new_path, sizeof(new_path), "%s%s", bucket_file->path, ext);
149                 rename(bucket_file->path, new_path);
150                 ast_copy_string(bucket_file->path, new_path, sizeof(bucket_file->path));
151         }
152 }
153
154 int ast_media_cache_retrieve(const char *uri, const char *preferred_file_name,
155         char *file_path, size_t len)
156 {
157         struct ast_bucket_file *bucket_file;
158         char *ext;
159         SCOPED_AO2LOCK(media_lock, media_cache);
160
161         if (ast_strlen_zero(uri)) {
162                 return -1;
163         }
164
165         /* First, retrieve from the ao2 cache here. If we find a bucket_file
166          * matching the requested URI, ask the appropriate backend if it is
167          * stale. If not; return it.
168          */
169         bucket_file = ao2_find(media_cache, uri, OBJ_SEARCH_KEY | OBJ_NOLOCK);
170         if (bucket_file) {
171                 if (!ast_bucket_file_is_stale(bucket_file)
172                         && ast_file_is_readable(bucket_file->path)) {
173                         ast_copy_string(file_path, bucket_file->path, len);
174                         if ((ext = strrchr(file_path, '.'))) {
175                                 *ext = '\0';
176                         }
177                         ao2_ref(bucket_file, -1);
178
179                         ast_debug(5, "Returning media at local file: %s\n", file_path);
180                         return 0;
181                 }
182
183                 /* Stale! Remove the item completely, as we're going to replace it next */
184                 ao2_unlink_flags(media_cache, bucket_file, OBJ_NOLOCK);
185                 ast_bucket_file_delete(bucket_file);
186                 ao2_ref(bucket_file, -1);
187         }
188
189         /* Either this is new or the resource is stale; do a full retrieve
190          * from the appropriate bucket_file backend
191          */
192         bucket_file = ast_bucket_file_retrieve(uri);
193         if (!bucket_file) {
194                 ast_debug(2, "Failed to obtain media at '%s'\n", uri);
195                 return -1;
196         }
197
198         /* We can manipulate the 'immutable' bucket_file here, as we haven't
199          * let anyone know of its existence yet
200          */
201         bucket_file_update_path(bucket_file, preferred_file_name);
202         media_cache_item_sync_to_astdb(bucket_file);
203         ast_copy_string(file_path, bucket_file->path, len);
204         if ((ext = strrchr(file_path, '.'))) {
205                 *ext = '\0';
206         }
207         ao2_link_flags(media_cache, bucket_file, OBJ_NOLOCK);
208         ao2_ref(bucket_file, -1);
209
210         ast_debug(5, "Returning media at local file: %s\n", file_path);
211
212         return 0;
213 }
214
215 int ast_media_cache_retrieve_metadata(const char *uri, const char *key,
216         char *value, size_t len)
217 {
218         struct ast_bucket_file *bucket_file;
219         struct ast_bucket_metadata *metadata;
220
221         if (ast_strlen_zero(uri) || ast_strlen_zero(key) || !value) {
222                 return -1;
223         }
224
225         bucket_file = ao2_find(media_cache, uri, OBJ_SEARCH_KEY);
226         if (!bucket_file) {
227                 return -1;
228         }
229
230         metadata = ao2_find(bucket_file->metadata, key, OBJ_SEARCH_KEY);
231         if (!metadata) {
232                 ao2_ref(bucket_file, -1);
233                 return -1;
234         }
235         ast_copy_string(value, metadata->value, len);
236
237         ao2_ref(metadata, -1);
238         ao2_ref(bucket_file, -1);
239         return 0;
240 }
241
242 int ast_media_cache_create_or_update(const char *uri, const char *file_path,
243         struct ast_variable *metadata)
244 {
245         struct ast_bucket_file *bucket_file;
246         struct ast_variable *it_metadata;
247         struct stat st;
248         char tmp[128];
249         char *ext;
250         char *file_path_ptr;
251         int created = 0;
252         SCOPED_AO2LOCK(media_lock, media_cache);
253
254         if (ast_strlen_zero(file_path) || ast_strlen_zero(uri)) {
255                 return -1;
256         }
257         file_path_ptr = ast_strdupa(file_path);
258
259         if (stat(file_path, &st)) {
260                 ast_log(LOG_WARNING, "Unable to obtain information for file %s for URI %s\n",
261                         file_path, uri);
262                 return -1;
263         }
264
265         bucket_file = ao2_find(media_cache, uri, OBJ_SEARCH_KEY | OBJ_NOLOCK);
266         if (bucket_file) {
267                 struct ast_bucket_file *clone;
268
269                 clone = ast_bucket_file_clone(bucket_file);
270                 if (!clone) {
271                         ao2_ref(bucket_file, -1);
272                         return -1;
273                 }
274
275                 /* Remove the old bucket_file. We'll replace it if we succeed below. */
276                 ao2_unlink_flags(media_cache, bucket_file, OBJ_NOLOCK);
277                 ao2_ref(bucket_file, -1);
278
279                 bucket_file = clone;
280         } else {
281                 bucket_file = ast_bucket_file_alloc(uri);
282                 if (!bucket_file) {
283                         ast_log(LOG_WARNING, "Failed to create file storage for %s and %s\n",
284                                 uri, file_path);
285                         return -1;
286                 }
287                 created = 1;
288         }
289
290         strcpy(bucket_file->path, file_path);
291         bucket_file->created.tv_sec = st.st_ctime;
292         bucket_file->modified.tv_sec = st.st_mtime;
293
294         snprintf(tmp, sizeof(tmp), "%ld", (long)st.st_atime);
295         ast_bucket_file_metadata_set(bucket_file, "accessed", tmp);
296
297         snprintf(tmp, sizeof(tmp), "%jd", (intmax_t)st.st_size);
298         ast_bucket_file_metadata_set(bucket_file, "size", tmp);
299
300         ext = strrchr(file_path_ptr, '.');
301         if (ext) {
302                 ast_bucket_file_metadata_set(bucket_file, "ext", ext + 1);
303         }
304
305         for (it_metadata = metadata; it_metadata; it_metadata = it_metadata->next) {
306                 ast_bucket_file_metadata_set(bucket_file, it_metadata->name, it_metadata->value);
307         }
308
309         if (created && ast_bucket_file_create(bucket_file)) {
310                 ast_log(LOG_WARNING, "Failed to create media for %s\n", uri);
311                 ao2_ref(bucket_file, -1);
312                 return -1;
313         }
314         media_cache_item_sync_to_astdb(bucket_file);
315
316         ao2_link_flags(media_cache, bucket_file, OBJ_NOLOCK);
317         ao2_ref(bucket_file, -1);
318         return 0;
319 }
320
321 int ast_media_cache_delete(const char *uri)
322 {
323         struct ast_bucket_file *bucket_file;
324         int res;
325
326         if (ast_strlen_zero(uri)) {
327                 return -1;
328         }
329
330         bucket_file = ao2_find(media_cache, uri, OBJ_SEARCH_KEY | OBJ_UNLINK);
331         if (!bucket_file) {
332                 return -1;
333         }
334
335         res = ast_bucket_file_delete(bucket_file);
336         media_cache_item_del_from_astdb(bucket_file);
337
338         ao2_ref(bucket_file, -1);
339
340         return res;
341 }
342
343 /*!
344  * \internal
345  * \brief Remove a media cache item from the AstDB
346  * \param uri The unique URI that represents the item in the cache
347  * \param hash The hash key for the item in the AstDB
348  */
349 static void media_cache_remove_from_astdb(const char *uri, const char *hash)
350 {
351         ast_db_del(AST_DB_FAMILY, uri + AST_DB_FAMILY_LEN);
352         ast_db_deltree(hash, NULL);
353 }
354
355 /*!
356  * \internal
357  * \brief Create an item in the media cache from entries in the AstDB
358  * \param uri The unique URI that represents the item in the cache
359  * \param hash The hash key for the item in the AstDB
360  * \retval 0 success
361  * \retval -1 failure
362  */
363 static int media_cache_item_populate_from_astdb(const char *uri, const char *hash)
364 {
365         struct ast_bucket_file *bucket_file;
366         struct ast_db_entry *db_tree;
367         struct ast_db_entry *db_entry;
368         struct stat st;
369
370         bucket_file = ast_bucket_file_alloc(uri);
371         if (!bucket_file) {
372                 return -1;
373         }
374
375         db_tree = ast_db_gettree(hash, NULL);
376         for (db_entry = db_tree; db_entry; db_entry = db_entry->next) {
377                 const char *key = strchr(db_entry->key + 1, '/');
378
379                 if (ast_strlen_zero(key)) {
380                         continue;
381                 }
382                 key++;
383
384                 if (!strcasecmp(key, "path")) {
385                         strcpy(bucket_file->path, db_entry->data);
386
387                         if (stat(bucket_file->path, &st)) {
388                                 ast_log(LOG_WARNING, "Unable to obtain information for file %s for URI %s\n",
389                                         bucket_file->path, uri);
390                                 ao2_ref(bucket_file, -1);
391                                 ast_db_freetree(db_tree);
392                                 return -1;
393                         }
394                 } else {
395                         ast_bucket_file_metadata_set(bucket_file, key, db_entry->data);
396                 }
397         }
398         ast_db_freetree(db_tree);
399
400         if (ast_strlen_zero(bucket_file->path)) {
401                 ao2_ref(bucket_file, -1);
402                 ast_log(LOG_WARNING, "Failed to restore media cache item for '%s' from AstDB: no 'path' specified\n",
403                         uri);
404                 return -1;
405         }
406
407         ao2_link(media_cache, bucket_file);
408         ao2_ref(bucket_file, -1);
409
410         return 0;
411 }
412
413 /*!
414  * \internal
415  * \brief Populate the media cache from entries in the AstDB
416  */
417 static void media_cache_populate_from_astdb(void)
418 {
419         struct ast_db_entry *db_entry;
420         struct ast_db_entry *db_tree;
421
422         db_tree = ast_db_gettree(AST_DB_FAMILY, NULL);
423         for (db_entry = db_tree; db_entry; db_entry = db_entry->next) {
424                 if (media_cache_item_populate_from_astdb(db_entry->key + AST_DB_FAMILY_LEN, db_entry->data)) {
425                         media_cache_remove_from_astdb(db_entry->key, db_entry->data);
426                 }
427         }
428         ast_db_freetree(db_tree);
429 }
430
431 /*!
432  * \internal
433  * \brief ao2 callback function for \ref media_cache_handle_show_all
434  */
435 static int media_cache_prnt_summary(void *obj, void *arg, int flags)
436 {
437 #define FORMAT_ROW "%-40s\n\t%-40s\n"
438         struct ast_bucket_file *bucket_file = obj;
439         struct ast_cli_args *a = arg;
440
441         ast_cli(a->fd, FORMAT_ROW, ast_sorcery_object_get_id(bucket_file), bucket_file->path);
442
443 #undef FORMAT_ROW
444         return CMP_MATCH;
445 }
446
447 static char *media_cache_handle_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
448 {
449         switch (cmd) {
450         case CLI_INIT:
451                 e->command = "media cache show all";
452                 e->usage =
453                         "Usage: media cache show all\n"
454                         "       Display a summary of all current items\n"
455                         "       in the media cache.\n";
456                 return NULL;
457         case CLI_GENERATE:
458                 return NULL;
459         }
460
461         if (a->argc != 4) {
462                 return CLI_SHOWUSAGE;
463         }
464
465         ast_cli(a->fd, "URI\n\tLocal File\n");
466         ast_cli(a->fd, "---------------\n");
467         ao2_callback(media_cache, OBJ_NODATA | OBJ_MULTIPLE, media_cache_prnt_summary, a);
468
469         return CLI_SUCCESS;
470 }
471
472 /*!
473  * \internal
474  * \brief CLI tab completion function for URIs
475  */
476 static char *cli_complete_uri(const char *word, int state)
477 {
478         struct ast_bucket_file *bucket_file;
479         struct ao2_iterator it_media_items;
480         int wordlen = strlen(word);
481         int which = 0;
482         char *result = NULL;
483
484         it_media_items = ao2_iterator_init(media_cache, 0);
485         while ((bucket_file = ao2_iterator_next(&it_media_items))) {
486                 if (!strncasecmp(word, ast_sorcery_object_get_id(bucket_file), wordlen)
487                         && ++which > state) {
488                         result = ast_strdup(ast_sorcery_object_get_id(bucket_file));
489                 }
490                 ao2_ref(bucket_file, -1);
491                 if (result) {
492                         break;
493                 }
494         }
495         ao2_iterator_destroy(&it_media_items);
496         return result;
497 }
498
499 static char *media_cache_handle_show_item(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
500 {
501 #define FORMAT_ROW "\t%20s: %-40.40s\n"
502         struct ast_bucket_file *bucket_file;
503         struct ao2_iterator it_metadata;
504         struct ast_bucket_metadata *metadata;
505
506         switch (cmd) {
507         case CLI_INIT:
508                 e->command = "media cache show";
509                 e->usage =
510                         "Usage: media cache show <uri>\n"
511                         "       Display all information about a particular\n"
512                         "       item in the media cache.\n";
513                 return NULL;
514         case CLI_GENERATE:
515                 return cli_complete_uri(a->word, a->n);
516         }
517
518         if (a->argc != 4) {
519                 return CLI_SHOWUSAGE;
520         }
521
522         bucket_file = ao2_find(media_cache, a->argv[3], OBJ_SEARCH_KEY);
523         if (!bucket_file) {
524                 ast_cli(a->fd, "Unable to find '%s' in the media cache\n", a->argv[3]);
525                 return CLI_SUCCESS;
526         }
527
528         ast_cli(a->fd, "URI: %s\n", ast_sorcery_object_get_id(bucket_file));
529         ast_cli(a->fd, "%s\n", "----------------------------------------");
530         ast_cli(a->fd, FORMAT_ROW, "Path", bucket_file->path);
531
532         it_metadata = ao2_iterator_init(bucket_file->metadata, 0);
533         while ((metadata = ao2_iterator_next(&it_metadata))) {
534                 ast_cli(a->fd, FORMAT_ROW, metadata->name, metadata->value);
535                 ao2_ref(metadata, -1);
536         }
537         ao2_iterator_destroy(&it_metadata);
538
539         ao2_ref(bucket_file, -1);
540 #undef FORMAT_ROW
541         return CLI_SUCCESS;
542 }
543
544 static char *media_cache_handle_delete_item(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
545 {
546         switch (cmd) {
547         case CLI_INIT:
548                 e->command = "media cache delete";
549                 e->usage =
550                         "Usage: media cache delete <uri>\n"
551                         "       Delete an item from the media cache.\n"
552                         "       Note that this will also remove any local\n"
553                         "       storage of the media associated with the URI,\n"
554                         "       and will inform the backend supporting the URI\n"
555                         "       scheme that it should remove the item.\n";
556                 return NULL;
557         case CLI_GENERATE:
558                 return cli_complete_uri(a->word, a->n);
559         }
560
561         if (a->argc != 4) {
562                 return CLI_SHOWUSAGE;
563         }
564
565         if (ast_media_cache_delete(a->argv[3])) {
566                 ast_cli(a->fd, "Unable to delete '%s'\n", a->argv[3]);
567         } else {
568                 ast_cli(a->fd, "Deleted '%s' from the media cache\n", a->argv[3]);
569         }
570
571         return CLI_SUCCESS;
572 }
573
574 static char *media_cache_handle_refresh_item(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
575 {
576         char file_path[PATH_MAX];
577
578         switch (cmd) {
579         case CLI_INIT:
580                 e->command = "media cache refresh";
581                 e->usage =
582                         "Usage: media cache refresh <uri>\n"
583                         "       Ask for a refresh of a particular URI.\n"
584                         "       If the item does not already exist in the\n"
585                         "       media cache, the item will be populated from\n"
586                         "       the backend supporting the URI scheme.\n";
587                 return NULL;
588         case CLI_GENERATE:
589                 return cli_complete_uri(a->word, a->n);
590         }
591
592         if (a->argc != 4) {
593                 return CLI_SHOWUSAGE;
594         }
595
596         if (ast_media_cache_retrieve(a->argv[3], NULL, file_path, sizeof(file_path))) {
597                 ast_cli(a->fd, "Unable to refresh '%s'\n", a->argv[3]);
598         } else {
599                 ast_cli(a->fd, "Refreshed '%s' to local storage '%s'\n", a->argv[3], file_path);
600         }
601
602         return CLI_SUCCESS;
603 }
604
605 static char *media_cache_handle_create_item(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
606 {
607         switch (cmd) {
608         case CLI_INIT:
609                 e->command = "media cache create";
610                 e->usage =
611                         "Usage: media cache create <uri> <file>\n"
612                         "       Create an item in the media cache by associating\n"
613                         "       a local media file with some URI.\n";
614                 return NULL;
615         case CLI_GENERATE:
616                 return NULL;
617         }
618
619         if (a->argc != 5) {
620                 return CLI_SHOWUSAGE;
621         }
622
623         if (ast_media_cache_create_or_update(a->argv[3], a->argv[4], NULL)) {
624                 ast_cli(a->fd, "Unable to create '%s' associated with local file '%s'\n",
625                         a->argv[3], a->argv[4]);
626         } else {
627                 ast_cli(a->fd, "Created '%s' for '%s' in the media cache\n",
628                         a->argv[3], a->argv[4]);
629         }
630
631         return CLI_SUCCESS;
632 }
633
634 static struct ast_cli_entry cli_media_cache[] = {
635         AST_CLI_DEFINE(media_cache_handle_show_all, "Show all items in the media cache"),
636         AST_CLI_DEFINE(media_cache_handle_show_item, "Show a single item in the media cache"),
637         AST_CLI_DEFINE(media_cache_handle_delete_item, "Remove an item from the media cache"),
638         AST_CLI_DEFINE(media_cache_handle_refresh_item, "Refresh an item in the media cache"),
639         AST_CLI_DEFINE(media_cache_handle_create_item, "Create an item in the media cache"),
640 };
641
642 /*!
643  * \internal
644  * \brief Shutdown the media cache
645  */
646 static void media_cache_shutdown(void)
647 {
648         ao2_ref(media_cache, -1);
649         media_cache = NULL;
650
651         ast_cli_unregister_multiple(cli_media_cache, ARRAY_LEN(cli_media_cache));
652 }
653
654 int ast_media_cache_init(void)
655 {
656         ast_register_atexit(media_cache_shutdown);
657
658         media_cache = ao2_container_alloc_options(AO2_ALLOC_OPT_LOCK_MUTEX, AO2_BUCKETS,
659                 ast_sorcery_object_id_hash, ast_sorcery_object_id_compare);
660         if (!media_cache) {
661                 return -1;
662         }
663
664         if (ast_cli_register_multiple(cli_media_cache, ARRAY_LEN(cli_media_cache))) {
665                 ao2_ref(media_cache, -1);
666                 return -1;
667         }
668
669         media_cache_populate_from_astdb();
670
671         return 0;
672 }