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