Add the bucket API.
authorJoshua Colp <jcolp@digium.com>
Fri, 23 Aug 2013 21:49:47 +0000 (21:49 +0000)
committerJoshua Colp <jcolp@digium.com>
Fri, 23 Aug 2013 21:49:47 +0000 (21:49 +0000)
Bucket is a URI based API for the creation, retrieval, updating, and deletion
of "buckets" and files contained within them.

Review: https://reviewboard.asterisk.org/r/2715/

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@397600 65c4cc65-6c06-0410-ace0-fbb531ad65f3

13 files changed:
build_tools/menuselect-deps.in
configure
configure.ac
include/asterisk/autoconfig.h.in
include/asterisk/bucket.h [new file with mode: 0644]
include/asterisk/config_options.h
main/Makefile
main/asterisk.c
main/bucket.c [new file with mode: 0644]
main/config_options.c
main/sorcery.c
makeopts.in
tests/test_bucket.c [new file with mode: 0644]

index bc7c265..12f049f 100644 (file)
@@ -26,6 +26,7 @@ ISDNNET=@PBX_ISDNNET@
 IXJUSER=@PBX_IXJUSER@
 JACK=@PBX_JACK@
 JANSSON=@PBX_JANSSON@
+URIPARSER=@PBX_URIPARSER@
 KQUEUE=@PBX_KQUEUE@
 LDAP=@PBX_LDAP@
 LIBEDIT=@PBX_LIBEDIT@
index 8ccfaa0..2d04b6d 100755 (executable)
--- a/configure
+++ b/configure
@@ -991,6 +991,10 @@ PBX_KQUEUE
 KQUEUE_DIR
 KQUEUE_INCLUDE
 KQUEUE_LIB
+PBX_URIPARSER
+URIPARSER_DIR
+URIPARSER_INCLUDE
+URIPARSER_LIB
 PBX_JANSSON
 JANSSON_DIR
 JANSSON_INCLUDE
@@ -1272,6 +1276,7 @@ with_iodbc
 with_isdnnet
 with_jack
 with_jansson
+with_uriparser
 with_kqueue
 with_ldap
 with_libcurl
@@ -2007,6 +2012,7 @@ Optional Packages:
   --with-isdnnet=PATH     use ISDN4Linux files in PATH
   --with-jack=PATH        use Jack Audio Connection Kit files in PATH
   --with-jansson=PATH     use Jansson JSON library files in PATH
+  --with-uriparser=PATH   use uriparser library files in PATH
   --with-kqueue=PATH      use kqueue support files in PATH
   --with-ldap=PATH        use OpenLDAP files in PATH
   --with-libcurl=DIR      look for the curl library in DIR
@@ -9016,6 +9022,38 @@ fi
 
 
 
+    URIPARSER_DESCRIP="uriparser library"
+    URIPARSER_OPTION="uriparser"
+    PBX_URIPARSER=0
+
+# Check whether --with-uriparser was given.
+if test "${with_uriparser+set}" = set; then :
+  withval=$with_uriparser;
+       case ${withval} in
+       n|no)
+       USE_URIPARSER=no
+       # -1 is a magic value used by menuselect to know that the package
+       # was disabled, other than 'not found'
+       PBX_URIPARSER=-1
+       ;;
+       y|ye|yes)
+       ac_mandatory_list="${ac_mandatory_list} URIPARSER"
+       ;;
+       *)
+       URIPARSER_DIR="${withval}"
+       ac_mandatory_list="${ac_mandatory_list} URIPARSER"
+       ;;
+       esac
+
+fi
+
+
+
+
+
+
+
+
     KQUEUE_DESCRIP="kqueue support"
     KQUEUE_OPTION="kqueue"
     PBX_KQUEUE=0
@@ -12548,6 +12586,111 @@ if test "x$JANSSON_LIB" == "x"; then
   as_fn_error $? "*** JSON support not found (this typically means the libjansson development package is missing)" "$LINENO" 5
 fi
 
+
+if test "x${PBX_URIPARSER}" != "x1" -a "${USE_URIPARSER}" != "no"; then
+   pbxlibdir=""
+   # if --with-URIPARSER=DIR has been specified, use it.
+   if test "x${URIPARSER_DIR}" != "x"; then
+      if test -d ${URIPARSER_DIR}/lib; then
+         pbxlibdir="-L${URIPARSER_DIR}/lib"
+      else
+         pbxlibdir="-L${URIPARSER_DIR}"
+      fi
+   fi
+   pbxfuncname="uriParseUriA"
+   if test "x${pbxfuncname}" = "x" ; then   # empty lib, assume only headers
+      AST_URIPARSER_FOUND=yes
+   else
+      ast_ext_lib_check_save_CFLAGS="${CFLAGS}"
+      CFLAGS="${CFLAGS} "
+      as_ac_Lib=`$as_echo "ac_cv_lib_uriparser_${pbxfuncname}" | $as_tr_sh`
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ${pbxfuncname} in -luriparser" >&5
+$as_echo_n "checking for ${pbxfuncname} in -luriparser... " >&6; }
+if eval \${$as_ac_Lib+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-luriparser ${pbxlibdir}  $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char ${pbxfuncname} ();
+int
+main ()
+{
+return ${pbxfuncname} ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+  eval "$as_ac_Lib=yes"
+else
+  eval "$as_ac_Lib=no"
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+eval ac_res=\$$as_ac_Lib
+              { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
+$as_echo "$ac_res" >&6; }
+if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
+  AST_URIPARSER_FOUND=yes
+else
+  AST_URIPARSER_FOUND=no
+fi
+
+      CFLAGS="${ast_ext_lib_check_save_CFLAGS}"
+   fi
+
+   # now check for the header.
+   if test "${AST_URIPARSER_FOUND}" = "yes"; then
+      URIPARSER_LIB="${pbxlibdir} -luriparser "
+      # if --with-URIPARSER=DIR has been specified, use it.
+      if test "x${URIPARSER_DIR}" != "x"; then
+         URIPARSER_INCLUDE="-I${URIPARSER_DIR}/include"
+      fi
+      URIPARSER_INCLUDE="${URIPARSER_INCLUDE} "
+      if test "xuriparser/Uri.h" = "x" ; then  # no header, assume found
+         URIPARSER_HEADER_FOUND="1"
+      else                             # check for the header
+         ast_ext_lib_check_saved_CPPFLAGS="${CPPFLAGS}"
+         CPPFLAGS="${CPPFLAGS} ${URIPARSER_INCLUDE}"
+         ac_fn_c_check_header_mongrel "$LINENO" "uriparser/Uri.h" "ac_cv_header_uriparser_Uri_h" "$ac_includes_default"
+if test "x$ac_cv_header_uriparser_Uri_h" = xyes; then :
+  URIPARSER_HEADER_FOUND=1
+else
+  URIPARSER_HEADER_FOUND=0
+fi
+
+
+         CPPFLAGS="${ast_ext_lib_check_saved_CPPFLAGS}"
+      fi
+      if test "x${URIPARSER_HEADER_FOUND}" = "x0" ; then
+         URIPARSER_LIB=""
+         URIPARSER_INCLUDE=""
+      else
+         if test "x${pbxfuncname}" = "x" ; then                # only checking headers -> no library
+            URIPARSER_LIB=""
+         fi
+         PBX_URIPARSER=1
+         cat >>confdefs.h <<_ACEOF
+#define HAVE_URIPARSER 1
+_ACEOF
+
+      fi
+   fi
+fi
+
+
+
 # Another mandatory item (unless it's explicitly disabled)
 # Check whether --enable-xmldoc was given.
 if test "${enable_xmldoc+set}" = set; then :
index 9a814cb..69228b5 100644 (file)
@@ -408,6 +408,7 @@ AST_EXT_LIB_SETUP([IODBC], [iODBC], [iodbc])
 AST_EXT_LIB_SETUP([ISDNNET], [ISDN4Linux], [isdnnet])
 AST_EXT_LIB_SETUP([JACK], [Jack Audio Connection Kit], [jack])
 AST_EXT_LIB_SETUP([JANSSON], [Jansson JSON library], [jansson])
+AST_EXT_LIB_SETUP([URIPARSER], [uriparser library], [uriparser])
 AST_EXT_LIB_SETUP([KQUEUE], [kqueue support], [kqueue])
 AST_EXT_LIB_SETUP([LDAP], [OpenLDAP], [ldap])
 AST_LIBCURL_CHECK_CONFIG([], [7.10.1])
@@ -544,6 +545,8 @@ if test "x$JANSSON_LIB" == "x"; then
   AC_MSG_ERROR([*** JSON support not found (this typically means the libjansson development package is missing)])
 fi
 
+AST_EXT_LIB_CHECK([URIPARSER], [uriparser], [uriParseUriA], [uriparser/Uri.h])
+
 # Another mandatory item (unless it's explicitly disabled)
 AC_ARG_ENABLE([xmldoc],
        [AS_HELP_STRING([--disable-xmldoc],
index bbc035f..1796a3f 100644 (file)
 /* Define to 1 if you have the `unsetenv' function. */
 #undef HAVE_UNSETENV
 
+/* Define to 1 if you have the uriparser library library. */
+#undef HAVE_URIPARSER
+
 /* Define to 1 if you have the `utime' function. */
 #undef HAVE_UTIME
 
diff --git a/include/asterisk/bucket.h b/include/asterisk/bucket.h
new file mode 100644 (file)
index 0000000..a09ade5
--- /dev/null
@@ -0,0 +1,397 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@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 Bucket File API
+ * \author Joshua Colp <jcolp@digium.com>
+ * \ref AstBucket
+ */
+
+/*!
+ * \page AstBucket Bucket File API
+ *
+ * Bucket is an API which provides directory and file access in a generic fashion. It is
+ * implemented as a thin wrapper over the sorcery data access layer API and is written in
+ * a pluggable fashion to allow different backend storage mechanisms.
+ *
+ */
+
+#ifndef _ASTERISK_BUCKET_H
+#define _ASTERISK_BUCKET_H
+
+#if defined(__cplusplus) || defined(c_plusplus)
+extern "C" {
+#endif
+
+#include "asterisk/sorcery.h"
+
+/*! \brief Opaque structure for internal details about a scheme */
+struct ast_bucket_scheme;
+
+/*! \brief Bucket metadata structure, AO2 key value pair */
+struct ast_bucket_metadata {
+       /*! \brief Name of the attribute */
+       const char *name;
+       /*! \brief Value of the attribute */
+       const char *value;
+       /*! \brief Storage for the above name and value */
+       char data[0];
+};
+
+/*! \brief Bucket structure, contains other buckets and files */
+struct ast_bucket {
+       /*! \brief Sorcery object information */
+       SORCERY_OBJECT(details);
+       /*! \brief Scheme implementation in use */
+       struct ast_bucket_scheme *scheme_impl;
+       /*! \brief Stringfields */
+       AST_DECLARE_STRING_FIELDS(
+               /*! \brief Name of scheme in use */
+               AST_STRING_FIELD(scheme);
+       );
+       /*! \brief When this bucket was created */
+       struct timeval created;
+       /*! \brief When this bucket was last modified */
+       struct timeval modified;
+       /*! \brief Container of string URIs of buckets within this bucket */
+       struct ao2_container *buckets;
+       /*! \brief Container of string URIs of files within this bucket */
+       struct ao2_container *files;
+};
+
+/*! \brief Bucket file structure, contains reference to file and information about it */
+struct ast_bucket_file {
+       /*! \brief Sorcery object information */
+       SORCERY_OBJECT(details);
+       /*! \brief Scheme implementation in use */
+       struct ast_bucket_scheme *scheme_impl;
+       /*! \brief Stringfields */
+       AST_DECLARE_STRING_FIELDS(
+               /*! \brief Name of scheme in use */
+               AST_STRING_FIELD(scheme);
+       );
+       /*! \brief When this file was created */
+       struct timeval created;
+       /*! \brief When this file was last modified */
+       struct timeval modified;
+       /*! \brief Container of metadata attributes about file */
+       struct ao2_container *metadata;
+       /*! \brief Local path to this file */
+       char path[PATH_MAX];
+};
+
+/*!
+ * \brief A callback function invoked when creating a file snapshot
+ *
+ * \param file Pointer to the file snapshot
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+typedef int (*bucket_file_create_cb)(struct ast_bucket_file *file);
+
+/*!
+ * \brief A callback function invoked when destroying a file snapshot
+ *
+ * \param file Pointer to the file snapshot
+ */
+typedef void (*bucket_file_destroy_cb)(struct ast_bucket_file *file);
+
+/*!
+ * \brief Initialize bucket support
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_bucket_init(void);
+
+/*!
+ * \brief Register support for a specific scheme
+ *
+ * \param name Name of the scheme, used to find based on scheme in URIs
+ * \param bucket Sorcery wizard used for buckets
+ * \param file Sorcery wizard used for files
+ * \param create_cb Required file snapshot creation callback
+ * \param destroy_cb Optional file snapshot destruction callback
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ *
+ * \note Once a scheme has been registered it can not be unregistered
+ */
+#define ast_bucket_scheme_register(name, bucket, file, create_cb, destroy_cb) __ast_bucket_scheme_register(name, bucket, file, create_cb, destroy_cb, ast_module_info ? ast_module_info->self : NULL)
+
+/*!
+ * \brief Register support for a specific scheme
+ *
+ * \param name Name of the scheme, used to find based on scheme in URIs
+ * \param bucket Sorcery wizard used for buckets
+ * \param file Sorcery wizard used for files
+ * \param create_cb Required file snapshot creation callback
+ * \param destroy_cb Optional file snapshot destruction callback
+ * \param module The module which implements this scheme
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ *
+ * \note Once a scheme has been registered it can not be unregistered
+ */
+int __ast_bucket_scheme_register(const char *name, struct ast_sorcery_wizard *bucket,
+       struct ast_sorcery_wizard *file, bucket_file_create_cb create_cb,
+       bucket_file_destroy_cb destroy_cb, struct ast_module *module);
+
+/*!
+ * \brief Set a metadata attribute on a file to a specific value
+ *
+ * \param file The bucket file
+ * \param name Name of the attribute
+ * \param value Value of the attribute
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ *
+ * \note This function will overwrite an existing attribute of the same name, unless an error
+ * occurs. If an error occurs the existing attribute is left alone.
+ */
+int ast_bucket_file_metadata_set(struct ast_bucket_file *file, const char *name, const char *value);
+
+/*!
+ * \brief Unset a specific metadata attribute on a file
+ *
+ * \param file The bucket file
+ * \param name Name of the attribute
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_bucket_file_metadata_unset(struct ast_bucket_file *file, const char *name);
+
+/*!
+ * \brief Retrieve a metadata attribute from a file
+ *
+ * \param file The bucket file
+ * \param name Name of the attribute
+ *
+ * \retval non-NULL if found
+ * \retval NULL if not found
+ *
+ * \note The object is returned with reference count increased
+ */
+struct ast_bucket_metadata *ast_bucket_file_metadata_get(struct ast_bucket_file *file, const char *name);
+
+/*!
+ * \brief Allocate a new bucket
+ *
+ * \param uri Complete URI for the bucket
+ *
+ * \param non-NULL success
+ * \param NULL failure
+ *
+ * \note This only creates a local bucket object, to persist in backend storage you must call
+ * ast_bucket_create
+ */
+struct ast_bucket *ast_bucket_alloc(const char *uri);
+
+/*!
+ * \brief Create a new bucket in backend storage
+ *
+ * \param bucket The bucket
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_bucket_create(struct ast_bucket *bucket);
+
+/*!
+ * \brief Delete a bucket from backend storage
+ *
+ * \param bucket The bucket
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_bucket_delete(struct ast_bucket *bucket);
+
+/*!
+ * \brief Retrieve information about a bucket
+ *
+ * \param uri Complete URI of the bucket
+ *
+ * \retval non-NULL if found
+ * \retval NULL if not found
+ *
+ * \note The object is returned with reference count increased
+ */
+struct ast_bucket *ast_bucket_retrieve(const char *uri);
+
+/*!
+ * \brief Add an observer for bucket creation and deletion operations
+ *
+ * \param callbacks Implementation of the sorcery observer interface
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ *
+ * \note You must be ready to accept observer invocations before this function is called
+ */
+int ast_bucket_observer_add(const struct ast_sorcery_observer *callbacks);
+
+/*!
+ * \brief Remove an observer from bucket creation and deletion
+ *
+ * \param callbacks Implementation of the sorcery observer interface
+ */
+void ast_bucket_observer_remove(struct ast_sorcery_observer *callbacks);
+
+/*!
+ * \brief Get a JSON representation of a bucket
+ *
+ * \param bucket The specific bucket
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ *
+ * \note The returned ast_json object must be unreferenced using ast_json_unref
+ */
+struct ast_json *ast_bucket_json(const struct ast_bucket *bucket);
+
+/*!
+ * \brief Allocate a new bucket file
+ *
+ * \param uri Complete URI for the bucket file
+ *
+ * \param non-NULL success
+ * \param NULL failure
+ *
+ * \note This only creates a local bucket file object, to persist in backend storage you must call
+ * ast_bucket_file_create
+ */
+struct ast_bucket_file *ast_bucket_file_alloc(const char *uri);
+
+/*!
+ * \brief Create a new bucket file in backend storage
+ *
+ * \param file The bucket file
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_bucket_file_create(struct ast_bucket_file *file);
+
+/*!
+ * \brief Copy a bucket file to a new URI
+ *
+ * \param file The source bucket file
+ * \param uri The new URI
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ *
+ * \note This operation stages things locally, you must call ast_bucket_file_create on the file
+ * that is returned to commit the copy to backend storage
+ *
+ */
+struct ast_bucket_file *ast_bucket_file_copy(struct ast_bucket_file *file, const char *uri);
+
+/*!
+ * \brief Update an existing bucket file in backend storage
+ *
+ * \param file The bucket file
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ *
+ * \note This operation will update both the actual content of the file and the metadata associated with it
+ */
+int ast_bucket_file_update(struct ast_bucket_file *file);
+
+/*!
+ * \brief Delete a bucket file from backend storage
+ *
+ * \param file The bucket file
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_bucket_file_delete(struct ast_bucket_file *file);
+
+/*!
+ * \brief Retrieve a bucket file
+ *
+ * \param uri Complete URI of the bucket file
+ *
+ * \retval non-NULL if found
+ * \retval NULL if not found
+ *
+ * \note The object is returned with reference count increased
+ */
+struct ast_bucket_file *ast_bucket_file_retrieve(const char *uri);
+
+/*!
+ * \brief Add an observer for bucket file creation and deletion operations
+ *
+ * \param callbacks Implementation of the sorcery observer interface
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ *
+ * \note You must be ready to accept observer invocations before this function is called
+ */
+int ast_bucket_file_observer_add(const struct ast_sorcery_observer *callbacks);
+
+/*!
+ * \brief Remove an observer from bucket file creation and deletion
+ *
+ * \param callbacks Implementation of the sorcery observer interface
+ */
+void ast_bucket_file_observer_remove(struct ast_sorcery_observer *callbacks);
+
+/*!
+ * \brief Get a JSON representation of a bucket file
+ *
+ * \param file The specific bucket file
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ *
+ * \note The returned ast_json object must be unreferenced using ast_json_unref
+ */
+struct ast_json *ast_bucket_file_json(const struct ast_bucket_file *file);
+
+/*!
+ * \brief Common file snapshot creation callback for creating a temporary file
+ *
+ * \param file Pointer to the file snapshot
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_bucket_file_temporary_create(struct ast_bucket_file *file);
+
+/*!
+ * \brief Common file snapshot destruction callback for deleting a temporary file
+ *
+ * \param file Pointer to the file snapshot
+ */
+void ast_bucket_file_temporary_destroy(struct ast_bucket_file *file);
+
+#if defined(__cplusplus) || defined(c_plusplus)
+}
+#endif
+
+#endif /* _ASTERISK_BUCKET_H */
index 8947521..55e40ad 100644 (file)
@@ -609,6 +609,16 @@ int aco_option_register_deprecated(struct aco_info *info, const char *name, stru
  */
 unsigned int aco_option_get_flags(const struct aco_option *option);
 
+/*!
+ * \brief Get the offset position for an argument within a config option
+ *
+ * \param option Pointer to the aco_option struct
+ * \param arg Argument number
+ *
+ * \retval position of the argument
+ */
+intptr_t aco_option_get_argument(const struct aco_option *option, unsigned int position);
+
 /*! \note  Everything below this point is to handle converting varargs
  * containing field names, to varargs containing a count of args, followed
  * by the offset of each of the field names in the struct type that is
index e3ed7c5..1d80f11 100644 (file)
@@ -37,6 +37,7 @@ AST_LIBS+=$(LIBXSLT_LIB)
 AST_LIBS+=$(SQLITE3_LIB)
 AST_LIBS+=$(ASTSSL_LIBS)
 AST_LIBS+=$(JANSSON_LIB)
+AST_LIBS+=$(URIPARSER_LIB)
 AST_LIBS+=$(UUID_LIB)
 AST_LIBS+=$(CRYPT_LIB)
 
@@ -155,6 +156,7 @@ db.o: _ASTCFLAGS+=$(SQLITE3_INCLUDE)
 asterisk.o: _ASTCFLAGS+=$(LIBEDIT_INCLUDE)
 cli.o: _ASTCFLAGS+=$(LIBEDIT_INCLUDE)
 json.o: _ASTCFLAGS+=$(JANSSON_INCLUDE)
+bucket.o: _ASTCFLAGS+=$(URIPARSER_INCLUDE)
 crypt.o: _ASTCFLAGS+=$(CRYPT_INCLUDE)
 uuid.o: _ASTCFLAGS+=$(UUID_INCLUDE)
 
index 9599931..abd4742 100644 (file)
@@ -241,6 +241,7 @@ int daemon(int, int);  /* defined in libresolv of all places */
 #include "asterisk/aoc.h"
 #include "asterisk/uuid.h"
 #include "asterisk/sorcery.h"
+#include "asterisk/bucket.h"
 #include "asterisk/stasis.h"
 #include "asterisk/json.h"
 #include "asterisk/stasis_endpoints.h"
@@ -4187,6 +4188,11 @@ int main(int argc, char *argv[])
 
        aco_init();
 
+       if (ast_bucket_init()) {
+               printf("%s", term_quit());
+               exit(1);
+       }
+
        if (stasis_init()) {
                printf("Stasis initialization failed.\n%s", term_quit());
                exit(1);
diff --git a/main/bucket.c b/main/bucket.c
new file mode 100644 (file)
index 0000000..0efd487
--- /dev/null
@@ -0,0 +1,963 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@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 Bucket File API
+ *
+ * \author Joshua Colp <jcolp@digium.com>
+ */
+
+/*** MODULEINFO
+       <use type="external">uriparser</use>
+       <support_level>core</support_level>
+ ***/
+
+/*** DOCUMENTATION
+        <configInfo name="core" language="en_US">
+                <synopsis>Bucket file API</synopsis>
+                <configFile name="bucket">
+                        <configObject name="bucket">
+                                <configOption name="scheme">
+                                        <synopsis>Scheme in use for bucket</synopsis>
+                                </configOption>
+                                <configOption name="created">
+                                        <synopsis>Time at which the bucket was created</synopsis>
+                                </configOption>
+                                <configOption name="modified">
+                                        <synopsis>Time at which the bucket was last modified</synopsis>
+                                </configOption>
+                        </configObject>
+                        <configObject name="file">
+                                <configOption name="scheme">
+                                        <synopsis>Scheme in use for file</synopsis>
+                                </configOption>
+                                <configOption name="created">
+                                        <synopsis>Time at which the file was created</synopsis>
+                                </configOption>
+                                <configOption name="modified">
+                                        <synopsis>Time at which the file was last modified</synopsis>
+                                </configOption>
+                        </configObject>
+                </configFile>
+        </configInfo>
+***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#ifdef HAVE_URIPARSER
+#include <uriparser/Uri.h>
+#endif
+
+#include "asterisk/logger.h"
+#include "asterisk/sorcery.h"
+#include "asterisk/bucket.h"
+#include "asterisk/config_options.h"
+#include "asterisk/astobj2.h"
+#include "asterisk/strings.h"
+#include "asterisk/json.h"
+#include "asterisk/file.h"
+#include "asterisk/module.h"
+
+/*! \brief Number of buckets for the container of schemes */
+#define SCHEME_BUCKETS 53
+
+/*! \brief Number of buckets for the container of metadata in a file */
+#define METADATA_BUCKETS 53
+
+/*! \brief Sorcery instance for all bucket operations */
+static struct ast_sorcery *bucket_sorcery;
+
+/*! \brief Container of registered schemes */
+static struct ao2_container *schemes;
+
+/*! \brief Structure for available schemes */
+struct ast_bucket_scheme {
+       /*! \brief Wizard for buckets */
+       struct ast_sorcery_wizard *bucket;
+       /*! \brief Wizard for files */
+       struct ast_sorcery_wizard *file;
+       /*! \brief Pointer to the file snapshot creation callback */
+       bucket_file_create_cb create;
+       /*! \brief Pointer to the file snapshot destruction callback */
+       bucket_file_destroy_cb destroy;
+       /*! \brief Name of the scheme */
+       char name[0];
+};
+
+/*! \brief Callback function for creating a bucket */
+static int bucket_wizard_create(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       struct ast_bucket *bucket = object;
+
+       return bucket->scheme_impl->bucket->create(sorcery, data, object);
+}
+
+/*! \brief Callback function for retrieving a bucket */
+static void *bucket_wizard_retrieve(const struct ast_sorcery *sorcery, void *data, const char *type,
+       const char *id)
+{
+#ifdef HAVE_URIPARSER
+       UriParserStateA state;
+       UriUriA uri;
+#else
+       char *tmp = ast_strdupa(id);
+#endif
+       SCOPED_AO2RDLOCK(lock, schemes);
+       size_t len;
+       char *uri_scheme;
+       RAII_VAR(struct ast_bucket_scheme *, scheme, NULL, ao2_cleanup);
+
+#ifdef HAVE_URIPARSER
+       state.uri = &uri;
+       if (uriParseUriA(&state, id) != URI_SUCCESS ||
+               !uri.scheme.first || !uri.scheme.afterLast) {
+               uriFreeUriMembersA(&uri);
+               return NULL;
+       }
+
+       len = (uri.scheme.afterLast - uri.scheme.first) + 1;
+       uri_scheme = ast_alloca(len);
+       ast_copy_string(uri_scheme, uri.scheme.first, len);
+
+       uriFreeUriMembersA(&uri);
+#else
+       uri_scheme = tmp;
+       if (!(tmp = strchr(':'))) {
+               return NULL;
+       }
+       *tmp = '\0';
+#endif
+
+       scheme = ao2_find(schemes, uri_scheme, OBJ_KEY | OBJ_NOLOCK);
+
+       if (!scheme) {
+               return NULL;
+       }
+
+       return scheme->bucket->retrieve_id(sorcery, data, type, id);
+}
+
+/*! \brief Callback function for deleting a bucket */
+static int bucket_wizard_delete(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       struct ast_bucket *bucket = object;
+
+       return bucket->scheme_impl->bucket->delete(sorcery, data, object);
+}
+
+/*! \brief Intermediary bucket wizard */
+static struct ast_sorcery_wizard bucket_wizard = {
+       .name = "bucket",
+       .create = bucket_wizard_create,
+       .retrieve_id = bucket_wizard_retrieve,
+       .delete = bucket_wizard_delete,
+};
+
+/*! \brief Callback function for creating a bucket file */
+static int bucket_file_wizard_create(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       struct ast_bucket_file *file = object;
+
+       return file->scheme_impl->file->create(sorcery, data, object);
+}
+
+/*! \brief Callback function for retrieving a bucket file */
+static void *bucket_file_wizard_retrieve(const struct ast_sorcery *sorcery, void *data, const char *type,
+       const char *id)
+{
+#ifdef HAVE_URIPARSER
+       UriParserStateA state;
+       UriUriA uri;
+#else
+       char *tmp = ast_strdupa(id);
+#endif
+       size_t len;
+       char *uri_scheme;
+       SCOPED_AO2RDLOCK(lock, schemes);
+       RAII_VAR(struct ast_bucket_scheme *, scheme, NULL, ao2_cleanup);
+
+#ifdef HAVE_URIPARSER
+       state.uri = &uri;
+       if (uriParseUriA(&state, id) != URI_SUCCESS ||
+               !uri.scheme.first || !uri.scheme.afterLast) {
+               uriFreeUriMembersA(&uri);
+               return NULL;
+       }
+
+       len = (uri.scheme.afterLast - uri.scheme.first) + 1;
+       uri_scheme = ast_alloca(len);
+       ast_copy_string(uri_scheme, uri.scheme.first, len);
+
+       uriFreeUriMembersA(&uri);
+#else
+       uri_scheme = tmp;
+       if (!(tmp = strchr(':'))) {
+               return NULL;
+       }
+       *tmp = '\0';
+#endif
+
+       scheme = ao2_find(schemes, uri_scheme, OBJ_KEY | OBJ_NOLOCK);
+
+       if (!scheme) {
+               return NULL;
+       }
+
+       return scheme->file->retrieve_id(sorcery, data, type, id);
+}
+
+/*! \brief Callback function for updating a bucket file */
+static int bucket_file_wizard_update(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       struct ast_bucket_file *file = object;
+
+       return file->scheme_impl->file->update(sorcery, data, object);
+}
+
+/*! \brief Callback function for deleting a bucket file */
+static int bucket_file_wizard_delete(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       struct ast_bucket_file *file = object;
+
+       return file->scheme_impl->file->delete(sorcery, data, object);
+}
+
+/*! \brief Intermediary file wizard */
+static struct ast_sorcery_wizard bucket_file_wizard = {
+       .name = "bucket_file",
+       .create = bucket_file_wizard_create,
+       .retrieve_id = bucket_file_wizard_retrieve,
+       .update = bucket_file_wizard_update,
+       .delete = bucket_file_wizard_delete,
+};
+
+int __ast_bucket_scheme_register(const char *name, struct ast_sorcery_wizard *bucket,
+       struct ast_sorcery_wizard *file, bucket_file_create_cb create_cb,
+       bucket_file_destroy_cb destroy_cb, struct ast_module *module)
+{
+       SCOPED_AO2WRLOCK(lock, schemes);
+       struct ast_bucket_scheme *scheme;
+
+       if (ast_strlen_zero(name) || !bucket || !file ||
+           !bucket->create || !bucket->delete || !bucket->retrieve_id ||
+           !create_cb) {
+               return -1;
+       }
+
+       scheme = ao2_find(schemes, name, OBJ_KEY | OBJ_NOLOCK);
+       if (scheme) {
+               return -1;
+       }
+
+       scheme = ao2_alloc(sizeof(*scheme) + strlen(name) + 1, NULL);
+       if (!scheme) {
+               return -1;
+       }
+
+       strcpy(scheme->name, name);
+       scheme->bucket = bucket;
+       scheme->file = file;
+       scheme->create = create_cb;
+       scheme->destroy = destroy_cb;
+
+       ao2_link_flags(schemes, scheme, OBJ_NOLOCK);
+
+       ast_verb(2, "Registered bucket scheme '%s'\n", name);
+
+       ast_module_ref(module);
+
+       return 0;
+}
+
+/*! \brief Allocator for metadata attributes */
+static struct ast_bucket_metadata *bucket_metadata_alloc(const char *name, const char *value)
+{
+       int name_len = strlen(name) + 1, value_len = strlen(value) + 1;
+       struct ast_bucket_metadata *metadata = ao2_alloc(sizeof(*metadata) + name_len + value_len, NULL);
+       char *dst;
+
+       if (!metadata) {
+               return NULL;
+       }
+
+       dst = metadata->data;
+       metadata->name = strcpy(dst, name);
+       dst += name_len;
+       metadata->value = strcpy(dst, value);
+
+       return metadata;
+}
+
+int ast_bucket_file_metadata_set(struct ast_bucket_file *file, const char *name, const char *value)
+{
+       RAII_VAR(struct ast_bucket_metadata *, metadata, bucket_metadata_alloc(name, value), ao2_cleanup);
+
+       if (!metadata) {
+               return -1;
+       }
+
+       ao2_find(file->metadata, name, OBJ_NODATA | OBJ_UNLINK | OBJ_KEY);
+       ao2_link(file->metadata, metadata);
+
+       return 0;
+}
+
+int ast_bucket_file_metadata_unset(struct ast_bucket_file *file, const char *name)
+{
+       RAII_VAR(struct ast_bucket_metadata *, metadata, ao2_find(file->metadata, name, OBJ_UNLINK | OBJ_KEY), ao2_cleanup);
+
+       if (!metadata) {
+               return -1;
+       }
+
+       return 0;
+}
+
+struct ast_bucket_metadata *ast_bucket_file_metadata_get(struct ast_bucket_file *file, const char *name)
+{
+       return ao2_find(file->metadata, name, OBJ_KEY);
+}
+
+/*! \brief Destructor for buckets */
+static void bucket_destroy(void *obj)
+{
+       struct ast_bucket *bucket = obj;
+
+       ao2_cleanup(bucket->scheme_impl);
+       ast_string_field_free_memory(bucket);
+       ao2_cleanup(bucket->buckets);
+       ao2_cleanup(bucket->files);
+}
+
+/*! \brief Sorting function for red black tree string container */
+static int bucket_rbtree_str_sort_cmp(const void *obj_left, const void *obj_right, int flags)
+{
+       const char *str_left = obj_left;
+       const char *str_right = obj_right;
+       int cmp = 0;
+
+       switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
+       default:
+       case OBJ_POINTER:
+       case OBJ_KEY:
+               cmp = strcmp(str_left, str_right);
+               break;
+       case OBJ_PARTIAL_KEY:
+               cmp = strncmp(str_left, str_right, strlen(str_right));
+               break;
+       }
+       return cmp;
+}
+
+/*! \brief Allocator for buckets */
+static void *bucket_alloc(const char *name)
+{
+       RAII_VAR(struct ast_bucket *, bucket, NULL, ao2_cleanup);
+
+       bucket = ast_sorcery_generic_alloc(sizeof(*bucket), bucket_destroy);
+       if (!bucket) {
+               return NULL;
+       }
+
+       if (ast_string_field_init(bucket, 128)) {
+               return NULL;
+       }
+
+       bucket->buckets = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
+               AO2_CONTAINER_ALLOC_OPT_DUPS_REJECT, bucket_rbtree_str_sort_cmp, NULL);
+       if (!bucket->buckets) {
+               return NULL;
+       }
+
+       bucket->files = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
+               AO2_CONTAINER_ALLOC_OPT_DUPS_REJECT, bucket_rbtree_str_sort_cmp, NULL);
+       if (!bucket->files) {
+               return NULL;
+       }
+
+       ao2_ref(bucket, +1);
+       return bucket;
+}
+
+struct ast_bucket *ast_bucket_alloc(const char *uri)
+{
+#ifdef HAVE_URIPARSER
+       UriParserStateA state;
+       UriUriA full_uri;
+#else
+       char *tmp = ast_strdupa(uri);
+#endif
+       size_t len;
+       char *uri_scheme;
+       RAII_VAR(struct ast_bucket_scheme *, scheme, NULL, ao2_cleanup);
+       struct ast_bucket *bucket;
+
+       if (ast_strlen_zero(uri)) {
+               return NULL;
+       }
+
+#ifdef HAVE_URIPARSER
+       state.uri = &full_uri;
+       if (uriParseUriA(&state, uri) != URI_SUCCESS ||
+               !full_uri.scheme.first || !full_uri.scheme.afterLast ||
+               !full_uri.pathTail) {
+               uriFreeUriMembersA(&full_uri);
+               return NULL;
+       }
+
+       len = (full_uri.scheme.afterLast - full_uri.scheme.first) + 1;
+       uri_scheme = ast_alloca(len);
+       ast_copy_string(uri_scheme, full_uri.scheme.first, len);
+
+       uriFreeUriMembersA(&full_uri);
+#else
+       uri_scheme = tmp;
+       if (!(tmp = strchr(':'))) {
+               return NULL;
+       }
+       *tmp = '\0';
+#endif
+
+       scheme = ao2_find(schemes, uri_scheme, OBJ_KEY);
+       if (!scheme) {
+               return NULL;
+       }
+
+       bucket = ast_sorcery_alloc(bucket_sorcery, "bucket", uri);
+       if (!bucket) {
+               return NULL;
+       }
+
+       ao2_ref(scheme, +1);
+       bucket->scheme_impl = scheme;
+
+       ast_string_field_set(bucket, scheme, uri_scheme);
+
+       return bucket;
+}
+
+int ast_bucket_create(struct ast_bucket *bucket)
+{
+       return ast_sorcery_create(bucket_sorcery, bucket);
+}
+
+struct ast_bucket *ast_bucket_retrieve(const char *uri)
+{
+       if (ast_strlen_zero(uri)) {
+               return NULL;
+       }
+
+       return ast_sorcery_retrieve_by_id(bucket_sorcery, "bucket", uri);
+}
+
+int ast_bucket_observer_add(const struct ast_sorcery_observer *callbacks)
+{
+       return ast_sorcery_observer_add(bucket_sorcery, "bucket", callbacks);
+}
+
+void ast_bucket_observer_remove(struct ast_sorcery_observer *callbacks)
+{
+       ast_sorcery_observer_remove(bucket_sorcery, "bucket", callbacks);
+}
+
+int ast_bucket_delete(struct ast_bucket *bucket)
+{
+       return ast_sorcery_delete(bucket_sorcery, bucket);
+}
+
+struct ast_json *ast_bucket_json(const struct ast_bucket *bucket)
+{
+       RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+       struct ast_json *id, *files, *buckets;
+       struct ao2_iterator i;
+       char *uri;
+       int res = 0;
+
+       json = ast_sorcery_objectset_json_create(bucket_sorcery, bucket);
+       if (!json) {
+               return NULL;
+       }
+
+       id = ast_json_string_create(ast_sorcery_object_get_id(bucket));
+       if (!id) {
+               return NULL;
+       }
+
+       if (ast_json_object_set(json, "id", id)) {
+               return NULL;
+       }
+
+       buckets = ast_json_array_create();
+       if (!buckets) {
+               return NULL;
+       }
+
+       if (ast_json_object_set(json, "buckets", buckets)) {
+               return NULL;
+       }
+
+       i = ao2_iterator_init(bucket->buckets, 0);
+       for (; (uri = ao2_iterator_next(&i)); ao2_ref(uri, -1)) {
+               struct ast_json *bucket_uri = ast_json_string_create(uri);
+
+               if (!bucket_uri || ast_json_array_append(buckets, bucket_uri)) {
+                       res = -1;
+                       break;
+               }
+       }
+       ao2_iterator_destroy(&i);
+
+       if (res) {
+               return NULL;
+       }
+
+       files = ast_json_array_create();
+       if (!files) {
+               return NULL;
+       }
+
+       if (ast_json_object_set(json, "files", files)) {
+               return NULL;
+       }
+
+       i = ao2_iterator_init(bucket->files, 0);
+       for (; (uri = ao2_iterator_next(&i)); ao2_ref(uri, -1)) {
+               struct ast_json *file_uri = ast_json_string_create(uri);
+
+               if (!file_uri || ast_json_array_append(files, file_uri)) {
+                       res = -1;
+                       break;
+               }
+       }
+       ao2_iterator_destroy(&i);
+
+       if (res) {
+               return NULL;
+       }
+
+       ast_json_ref(json);
+       return json;
+}
+
+/*! \brief Hashing function for file metadata */
+static int bucket_file_metadata_hash(const void *obj, const int flags)
+{
+       const struct ast_bucket_metadata *object;
+       const char *key;
+
+       switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
+       case OBJ_KEY:
+               key = obj;
+               return ast_str_hash(key);
+       case OBJ_POINTER:
+               object = obj;
+               return ast_str_hash(object->name);
+       default:
+               /* Hash can only work on something with a full key */
+               ast_assert(0);
+               return 0;
+       }
+}
+
+/*! \brief Comparison function for file metadata */
+static int bucket_file_metadata_cmp(void *obj, void *arg, int flags)
+{
+       struct ast_bucket_metadata *metadata1 = obj, *metadata2 = arg;
+       const char *name = arg;
+
+       return !strcmp(metadata1->name, flags & OBJ_KEY ? name : metadata2->name) ? CMP_MATCH | CMP_STOP : 0;
+}
+
+/*! \brief Destructor for bucket files */
+static void bucket_file_destroy(void *obj)
+{
+       struct ast_bucket_file *file = obj;
+
+       if (file->scheme_impl->destroy) {
+               file->scheme_impl->destroy(file);
+       }
+
+       ao2_cleanup(file->scheme_impl);
+       ao2_cleanup(file->metadata);
+}
+
+/*! \brief Allocator for bucket files */
+static void *bucket_file_alloc(const char *name)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+
+       file = ast_sorcery_generic_alloc(sizeof(*file), bucket_file_destroy);
+       if (!file) {
+               return NULL;
+       }
+
+       if (ast_string_field_init(file, 128)) {
+               return NULL;
+       }
+
+       file->metadata = ao2_container_alloc_options(AO2_ALLOC_OPT_LOCK_NOLOCK, METADATA_BUCKETS,
+               bucket_file_metadata_hash, bucket_file_metadata_cmp);
+       if (!file->metadata) {
+               return NULL;
+       }
+
+       ao2_ref(file, +1);
+       return file;
+}
+
+struct ast_bucket_file *ast_bucket_file_alloc(const char *uri)
+{
+#ifdef HAVE_URIPARSER
+       UriParserStateA state;
+       UriUriA full_uri;
+#else
+       char *tmp = ast_strdupa(uri);
+#endif
+       size_t len;
+       char *uri_scheme;
+       RAII_VAR(struct ast_bucket_scheme *, scheme, NULL, ao2_cleanup);
+       struct ast_bucket_file *file;
+
+       if (ast_strlen_zero(uri)) {
+               return NULL;
+       }
+
+#ifdef HAVE_URIPARSER
+       state.uri = &full_uri;
+       if (uriParseUriA(&state, uri) != URI_SUCCESS ||
+               !full_uri.scheme.first || !full_uri.scheme.afterLast ||
+               !full_uri.pathTail) {
+               uriFreeUriMembersA(&full_uri);
+               return NULL;
+       }
+
+       len = (full_uri.scheme.afterLast - full_uri.scheme.first) + 1;
+       uri_scheme = ast_alloca(len);
+       ast_copy_string(uri_scheme, full_uri.scheme.first, len);
+
+       uriFreeUriMembersA(&full_uri);
+#else
+       uri_scheme = tmp;
+       if (!(tmp = strchr(':'))) {
+               return NULL;
+       }
+       *tmp = '\0';
+#endif
+
+       scheme = ao2_find(schemes, uri_scheme, OBJ_KEY);
+       if (!scheme) {
+               return NULL;
+       }
+
+       file = ast_sorcery_alloc(bucket_sorcery, "file", uri);
+       if (!file) {
+               return NULL;
+       }
+
+       ao2_ref(scheme, +1);
+       file->scheme_impl = scheme;
+
+       ast_string_field_set(file, scheme, uri_scheme);
+
+       if (scheme->create(file)) {
+               ao2_ref(file, -1);
+               return NULL;
+       }
+
+       return file;
+}
+
+int ast_bucket_file_create(struct ast_bucket_file *file)
+{
+       return ast_sorcery_create(bucket_sorcery, file);
+}
+
+/*! \brief Copy a file, shamelessly taken from file.c */
+static int bucket_copy(const char *infile, const char *outfile)
+{
+       int ifd, ofd, len;
+       char buf[4096]; /* XXX make it lerger. */
+
+       if ((ifd = open(infile, O_RDONLY)) < 0) {
+               ast_log(LOG_WARNING, "Unable to open %s in read-only mode, error: %s\n", infile, strerror(errno));
+               return -1;
+       }
+       if ((ofd = open(outfile, O_WRONLY | O_TRUNC | O_CREAT, AST_FILE_MODE)) < 0) {
+               ast_log(LOG_WARNING, "Unable to open %s in write-only mode, error: %s\n", outfile, strerror(errno));
+               close(ifd);
+               return -1;
+       }
+       while ( (len = read(ifd, buf, sizeof(buf)) ) ) {
+               int res;
+               if (len < 0) {
+                       ast_log(LOG_WARNING, "Read failed on %s: %s\n", infile, strerror(errno));
+                       break;
+               }
+               /* XXX handle partial writes */
+               res = write(ofd, buf, len);
+               if (res != len) {
+                       ast_log(LOG_WARNING, "Write failed on %s (%d of %d): %s\n", outfile, res, len, strerror(errno));
+                       len = -1; /* error marker */
+                       break;
+               }
+       }
+       close(ifd);
+       close(ofd);
+       if (len < 0) {
+               unlink(outfile);
+               return -1; /* error */
+       }
+       return 0;       /* success */
+}
+
+struct ast_bucket_file *ast_bucket_file_copy(struct ast_bucket_file *file, const char *uri)
+{
+       RAII_VAR(struct ast_bucket_file *, copy, ast_bucket_file_alloc(uri), ao2_cleanup);
+
+       if (!copy) {
+               return NULL;
+       }
+
+       ao2_cleanup(copy->metadata);
+       copy->metadata = ao2_container_clone(file->metadata, 0);
+       if (!copy->metadata ||
+               bucket_copy(file->path, copy->path)) {
+               return NULL;
+       }
+
+       ao2_ref(copy, +1);
+       return copy;
+}
+
+struct ast_bucket_file *ast_bucket_file_retrieve(const char *uri)
+{
+       if (ast_strlen_zero(uri)) {
+               return NULL;
+       }
+
+       return ast_sorcery_retrieve_by_id(bucket_sorcery, "file", uri);
+}
+
+int ast_bucket_file_observer_add(const struct ast_sorcery_observer *callbacks)
+{
+       return ast_sorcery_observer_add(bucket_sorcery, "file", callbacks);
+}
+
+void ast_bucket_file_observer_remove(struct ast_sorcery_observer *callbacks)
+{
+       ast_sorcery_observer_remove(bucket_sorcery, "file", callbacks);
+}
+
+int ast_bucket_file_update(struct ast_bucket_file *file)
+{
+       return ast_sorcery_update(bucket_sorcery, file);
+}
+
+int ast_bucket_file_delete(struct ast_bucket_file *file)
+{
+       return ast_sorcery_delete(bucket_sorcery, file);
+}
+
+struct ast_json *ast_bucket_file_json(const struct ast_bucket_file *file)
+{
+       RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+       struct ast_json *id, *metadata;
+       struct ao2_iterator i;
+       struct ast_bucket_metadata *attribute;
+       int res = 0;
+
+       json = ast_sorcery_objectset_json_create(bucket_sorcery, file);
+       if (!json) {
+               return NULL;
+       }
+
+       id = ast_json_string_create(ast_sorcery_object_get_id(file));
+       if (!id) {
+               return NULL;
+       }
+
+       if (ast_json_object_set(json, "id", id)) {
+               return NULL;
+       }
+
+       metadata = ast_json_object_create();
+       if (!metadata) {
+               return NULL;
+       }
+
+       if (ast_json_object_set(json, "metadata", metadata)) {
+               return NULL;
+       }
+
+       i = ao2_iterator_init(file->metadata, 0);
+       for (; (attribute = ao2_iterator_next(&i)); ao2_ref(attribute, -1)) {
+               struct ast_json *value = ast_json_string_create(attribute->value);
+
+               if (!value || ast_json_object_set(metadata, attribute->name, value)) {
+                       res = -1;
+                       break;
+               }
+       }
+       ao2_iterator_destroy(&i);
+
+       if (res) {
+               return NULL;
+       }
+
+       ast_json_ref(json);
+       return json;
+}
+
+int ast_bucket_file_temporary_create(struct ast_bucket_file *file)
+{
+       int fd;
+
+       ast_copy_string(file->path, "/tmp/bucket-XXXXXX", sizeof(file->path));
+
+       fd = mkstemp(file->path);
+       if (fd < 0) {
+               return -1;
+       }
+
+       close(fd);
+       return 0;
+}
+
+void ast_bucket_file_temporary_destroy(struct ast_bucket_file *file)
+{
+       if (!ast_strlen_zero(file->path)) {
+               unlink(file->path);
+       }
+}
+
+/*! \brief Hashing function for scheme container */
+static int bucket_scheme_hash(const void *obj, const int flags)
+{
+       const struct ast_bucket_scheme *object;
+       const char *key;
+
+       switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
+       case OBJ_KEY:
+               key = obj;
+               return ast_str_hash(key);
+       case OBJ_POINTER:
+               object = obj;
+               return ast_str_hash(object->name);
+       default:
+               /* Hash can only work on something with a full key */
+               ast_assert(0);
+               return 0;
+       }
+}
+
+/*! \brief Comparison function for scheme container */
+static int bucket_scheme_cmp(void *obj, void *arg, int flags)
+{
+       struct ast_bucket_scheme *scheme1 = obj, *scheme2 = arg;
+       const char *name = arg;
+
+       return !strcmp(scheme1->name, flags & OBJ_KEY ? name : scheme2->name) ? CMP_MATCH | CMP_STOP : 0;
+}
+
+/*! \brief Cleanup function for graceful shutdowns */
+static void bucket_cleanup(void)
+{
+       if (bucket_sorcery) {
+               ast_sorcery_unref(bucket_sorcery);
+       }
+
+       ast_sorcery_wizard_unregister(&bucket_wizard);
+       ast_sorcery_wizard_unregister(&bucket_file_wizard);
+
+       ao2_cleanup(schemes);
+}
+
+/*! \brief Custom handler for translating from a string timeval to actual structure */
+static int timeval_str2struct(const struct aco_option *opt, struct ast_variable *var, void *obj)
+{
+       struct timeval *field = (struct timeval *)(obj + aco_option_get_argument(opt, 0));
+       return ast_get_timeval(var->value, field, ast_tv(0, 0), NULL);
+}
+
+/*! \brief Custom handler for translating from an actual structure timeval to string */
+static int timeval_struct2str(const void *obj, const intptr_t *args, char **buf)
+{
+       struct timeval *field = (struct timeval *)(obj + args[0]);
+       return (ast_asprintf(buf, "%lu.%06lu", field->tv_sec, field->tv_usec) < 0) ? -1 : 0;
+}
+
+/*! \brief Initialize bucket support */
+int ast_bucket_init(void)
+{
+       ast_register_cleanup(&bucket_cleanup);
+
+       schemes = ao2_container_alloc_options(AO2_ALLOC_OPT_LOCK_RWLOCK, SCHEME_BUCKETS, bucket_scheme_hash,
+               bucket_scheme_cmp);
+       if (!schemes) {
+               ast_log(LOG_ERROR, "Failed to create container for Bucket schemes\n");
+               return -1;
+       }
+
+       if (__ast_sorcery_wizard_register(&bucket_wizard, NULL)) {
+               ast_log(LOG_ERROR, "Failed to register sorcery wizard for 'bucket' intermediary\n");
+               return -1;
+       }
+
+       if (__ast_sorcery_wizard_register(&bucket_file_wizard, NULL)) {
+               ast_log(LOG_ERROR, "Failed to register sorcery wizard for 'file' intermediary\n");
+               return -1;
+       }
+
+       if (!(bucket_sorcery = ast_sorcery_open())) {
+               ast_log(LOG_ERROR, "Failed to create sorcery instance for Bucket support\n");
+               return -1;
+       }
+
+       if (ast_sorcery_apply_default(bucket_sorcery, "bucket", "bucket", NULL)) {
+               ast_log(LOG_ERROR, "Failed to apply intermediary for 'bucket' object type in Bucket sorcery\n");
+               return -1;
+       }
+
+       if (ast_sorcery_object_register(bucket_sorcery, "bucket", bucket_alloc, NULL, NULL)) {
+               ast_log(LOG_ERROR, "Failed to register 'bucket' object type in Bucket sorcery\n");
+               return -1;
+       }
+
+       ast_sorcery_object_field_register(bucket_sorcery, "bucket", "scheme", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_bucket, scheme));
+       ast_sorcery_object_field_register_custom(bucket_sorcery, "bucket", "created", "", timeval_str2struct, timeval_struct2str, 0, FLDSET(struct ast_bucket, created));
+       ast_sorcery_object_field_register_custom(bucket_sorcery, "bucket", "modified", "", timeval_str2struct, timeval_struct2str, 0, FLDSET(struct ast_bucket, modified));
+
+       if (ast_sorcery_apply_default(bucket_sorcery, "file", "bucket_file", NULL)) {
+               ast_log(LOG_ERROR, "Failed to apply intermediary for 'file' object type in Bucket sorcery\n");
+               return -1;
+       }
+
+       if (ast_sorcery_object_register(bucket_sorcery, "file", bucket_file_alloc, NULL, NULL)) {
+               ast_log(LOG_ERROR, "Failed to register 'file' object type in Bucket sorcery\n");
+               return -1;
+       }
+
+       ast_sorcery_object_field_register(bucket_sorcery, "file", "scheme", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_bucket_file, scheme));
+       ast_sorcery_object_field_register_custom(bucket_sorcery, "file", "created", "", timeval_str2struct, timeval_struct2str, 0, FLDSET(struct ast_bucket_file, created));
+       ast_sorcery_object_field_register_custom(bucket_sorcery, "file", "modified", "", timeval_str2struct, timeval_struct2str, 0, FLDSET(struct ast_bucket_file, modified));
+
+       return 0;
+}
index 37c7433..e789c1c 100644 (file)
@@ -229,6 +229,11 @@ unsigned int aco_option_get_flags(const struct aco_option *option)
        return option->flags;
 }
 
+intptr_t aco_option_get_argument(const struct aco_option *option, unsigned int position)
+{
+       return option->args[position];
+}
+
 #ifdef AST_XML_DOCS
 /*! \internal
  * \brief Find a particular ast_xml_doc_item from it's parent config_info, types, and name
index b2bb879..1bd55d4 100644 (file)
@@ -61,7 +61,7 @@ static struct ast_threadpool *threadpool;
 /*! \brief Structure for internal sorcery object information */
 struct ast_sorcery_object {
        /*! \brief Unique identifier of this object */
-       char id[AST_UUID_STR_LEN];
+       char *id;
 
        /*! \brief Type of object */
        char type[MAX_OBJECT_TYPE];
@@ -1041,6 +1041,7 @@ static void sorcery_object_destructor(void *object)
        }
 
        ast_variables_destroy(details->object->extended);
+       ast_free(details->object->id);
 }
 
 void *ast_sorcery_generic_alloc(size_t size, ao2_destructor_fn destructor)
@@ -1069,9 +1070,12 @@ void *ast_sorcery_alloc(const struct ast_sorcery *sorcery, const char *type, con
        }
 
        if (ast_strlen_zero(id)) {
-               ast_uuid_generate_str(details->object->id, sizeof(details->object->id));
+               char uuid[AST_UUID_STR_LEN];
+
+               ast_uuid_generate_str(uuid, sizeof(uuid));
+               details->object->id = ast_strdup(uuid);
        } else {
-               ast_copy_string(details->object->id, id, sizeof(details->object->id));
+               details->object->id = ast_strdup(id);
        }
 
        ast_copy_string(details->object->type, type, sizeof(details->object->type));
index fa9cce6..3b3d2d9 100644 (file)
@@ -173,6 +173,9 @@ JACK_LIB=@JACK_LIB@
 JANSSON_INCLUDE=@JANSSON_INCLUDE@
 JANSSON_LIB=@JANSSON_LIB@
 
+URIPARSER_INCLUDE=@URIPARSER_INCLUDE@
+URIPARSER_LIB=@URIPARSER_LIB@
+
 LDAP_INCLUDE=@LDAP_INCLUDE@
 LDAP_LIB=@LDAP_LIB@
 
diff --git a/tests/test_bucket.c b/tests/test_bucket.c
new file mode 100644 (file)
index 0000000..68249d3
--- /dev/null
@@ -0,0 +1,883 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@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 Bucket Unit Tests
+ *
+ * \author Joshua Colp <jcolp@digium.com>
+ *
+ */
+
+/*** MODULEINFO
+       <depend>TEST_FRAMEWORK</depend>
+       <support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "")
+
+#include <sys/stat.h>
+
+#include "asterisk/test.h"
+#include "asterisk/module.h"
+#include "asterisk/bucket.h"
+#include "asterisk/logger.h"
+#include "asterisk/json.h"
+#include "asterisk/file.h"
+
+/*! \brief Test state structure for scheme wizards */
+struct bucket_test_state {
+       /*! \brief Whether the object has been created or not */
+       unsigned int created:1;
+       /*! \brief Whether the object has been updated or not */
+       unsigned int updated:1;
+       /*! \brief Whether the object has been deleted or not */
+       unsigned int deleted:1;
+};
+
+/*! \brief Global scope structure for testing bucket wizards */
+static struct bucket_test_state bucket_test_wizard_state;
+
+static void bucket_test_wizard_clear(void)
+{
+       bucket_test_wizard_state.created = 0;
+       bucket_test_wizard_state.updated = 0;
+       bucket_test_wizard_state.deleted = 0;
+}
+
+static int bucket_test_wizard_create(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       if (bucket_test_wizard_state.created) {
+               return -1;
+       }
+
+       bucket_test_wizard_state.created = 1;
+
+       return 0;
+}
+
+static int bucket_test_wizard_update(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       if (bucket_test_wizard_state.updated) {
+               return -1;
+       }
+
+       bucket_test_wizard_state.updated = 1;
+
+       return 0;
+}
+
+static void *bucket_test_wizard_retrieve_id(const struct ast_sorcery *sorcery, void *data, const char *type,
+       const char *id)
+{
+       if (!strcmp(type, "bucket")) {
+               return ast_bucket_alloc(id);
+       } else if (!strcmp(type, "file")) {
+               return ast_bucket_file_alloc(id);
+       } else {
+               return NULL;
+       }
+}
+
+static int bucket_test_wizard_delete(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       if (bucket_test_wizard_state.deleted) {
+               return -1;
+       }
+
+       bucket_test_wizard_state.deleted = 1;
+
+       return 0;
+}
+
+static struct ast_sorcery_wizard bucket_test_wizard = {
+       .name = "test",
+       .create = bucket_test_wizard_create,
+       .retrieve_id = bucket_test_wizard_retrieve_id,
+       .delete = bucket_test_wizard_delete,
+};
+
+static struct ast_sorcery_wizard bucket_file_test_wizard = {
+       .name = "test",
+       .create = bucket_test_wizard_create,
+       .update = bucket_test_wizard_update,
+       .retrieve_id = bucket_test_wizard_retrieve_id,
+       .delete = bucket_test_wizard_delete,
+};
+
+AST_TEST_DEFINE(bucket_scheme_register)
+{
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_scheme_register_unregister";
+               info->category = "/main/bucket/";
+               info->summary = "bucket scheme registration/unregistration unit test";
+               info->description =
+                       "Test registration and unregistration of bucket scheme";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!ast_bucket_scheme_register("", NULL, NULL, NULL, NULL)) {
+               ast_test_status_update(test, "Successfully registered a Bucket scheme without name or wizards\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!ast_bucket_scheme_register("test", &bucket_test_wizard, &bucket_file_test_wizard, NULL, NULL)) {
+               ast_test_status_update(test, "Successfully registered a Bucket scheme twice\n");
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_alloc)
+{
+       RAII_VAR(struct ast_bucket *, bucket, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_alloc";
+               info->category = "/main/bucket/";
+               info->summary = "bucket allocation unit test";
+               info->description =
+                       "Test allocation of buckets";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if ((bucket = ast_bucket_alloc(""))) {
+               ast_test_status_update(test, "Allocated a bucket with no URI provided\n");
+               return AST_TEST_FAIL;
+       }
+
+       if ((bucket = ast_bucket_alloc("test://"))) {
+               ast_test_status_update(test, "Allocated a bucket with no name\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!(bucket = ast_bucket_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate bucket\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (strcmp(ast_sorcery_object_get_id(bucket), "test:///tmp/bob")) {
+               ast_test_status_update(test, "URI within allocated bucket is '%s' and should be test:///tmp/bob\n",
+                       ast_sorcery_object_get_id(bucket));
+               return AST_TEST_FAIL;
+       }
+
+       if (strcmp(bucket->scheme, "test")) {
+               ast_test_status_update(test, "Scheme within allocated bucket is '%s' and should be test\n",
+                       bucket->scheme);
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_create)
+{
+       RAII_VAR(struct ast_bucket *, bucket, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_create";
+               info->category = "/main/bucket/";
+               info->summary = "bucket creation unit test";
+               info->description =
+                       "Test creation of buckets";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(bucket = ast_bucket_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate bucket\n");
+               return AST_TEST_FAIL;
+       }
+
+       bucket_test_wizard_clear();
+
+       if (ast_bucket_create(bucket)) {
+               ast_test_status_update(test, "Failed to create bucket with URI '%s'\n",
+                       ast_sorcery_object_get_id(bucket));
+               return AST_TEST_FAIL;
+       }
+
+       if (!bucket_test_wizard_state.created) {
+               ast_test_status_update(test, "Bucket creation returned success but scheme implementation never actually created it\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!ast_bucket_create(bucket)) {
+               ast_test_status_update(test, "Successfully created bucket with URI '%s' twice\n",
+                       ast_sorcery_object_get_id(bucket));
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_delete)
+{
+       RAII_VAR(struct ast_bucket *, bucket, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_delete";
+               info->category = "/main/bucket/";
+               info->summary = "bucket deletion unit test";
+               info->description =
+                       "Test deletion of buckets";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(bucket = ast_bucket_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate bucket\n");
+               return AST_TEST_FAIL;
+       }
+
+       bucket_test_wizard_clear();
+
+       if (ast_bucket_delete(bucket)) {
+               ast_test_status_update(test, "Failed to delete bucket with URI '%s'\n",
+                       ast_sorcery_object_get_id(bucket));
+               return AST_TEST_FAIL;
+       }
+
+       if (!bucket_test_wizard_state.deleted) {
+               ast_test_status_update(test, "Bucket deletion returned success but scheme implementation never actually deleted it\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!ast_bucket_delete(bucket)) {
+               ast_test_status_update(test, "Successfully deleted bucket with URI '%s' twice\n",
+                       ast_sorcery_object_get_id(bucket));
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_json)
+{
+       RAII_VAR(struct ast_bucket *, bucket, NULL, ao2_cleanup);
+       RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref);
+       RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_json";
+               info->category = "/main/bucket/";
+               info->summary = "bucket json unit test";
+               info->description =
+                       "Test creation of JSON for a bucket";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(bucket = ast_bucket_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate bucket\n");
+               return AST_TEST_FAIL;
+       }
+
+       ast_str_container_add(bucket->buckets, "test:///tmp/bob/joe");
+       ast_str_container_add(bucket->files, "test:///tmp/bob/recording.wav");
+
+       expected = ast_json_pack("{s: s, s: s, s: [s], s: s, s: [s], s: s}",
+               "modified", "0.000000", "created", "0.000000",
+               "buckets", "test:///tmp/bob/joe",
+               "scheme", "test",
+               "files", "test:///tmp/bob/recording.wav",
+               "id", "test:///tmp/bob");
+       if (!expected) {
+               ast_test_status_update(test, "Could not produce JSON for expected bucket value\n");
+               return AST_TEST_FAIL;
+       }
+
+       json = ast_bucket_json(bucket);
+       if (!json) {
+               ast_test_status_update(test, "Could not produce JSON for a valid bucket\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!ast_json_equal(json, expected)) {
+               ast_test_status_update(test, "Bucket JSON does not match expected output\n");
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_retrieve)
+{
+       RAII_VAR(struct ast_bucket *, bucket, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_retrieve";
+               info->category = "/main/bucket/";
+               info->summary = "bucket retrieval unit test";
+               info->description =
+                       "Test retrieval of buckets";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(bucket = ast_bucket_retrieve("test://tmp/bob"))) {
+               ast_test_status_update(test, "Failed to retrieve known valid bucket\n");
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_alloc)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_alloc";
+               info->category = "/main/bucket/";
+               info->summary = "bucket file allocation unit test";
+               info->description =
+                       "Test allocation of bucket files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if ((file = ast_bucket_file_alloc(""))) {
+               ast_test_status_update(test, "Allocated a file with no URI provided\n");
+               return AST_TEST_FAIL;
+       }
+
+       if ((file = ast_bucket_file_alloc("test://"))) {
+               ast_test_status_update(test, "Allocated a file with no name\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (ast_strlen_zero(file->path)) {
+               ast_test_status_update(test, "Expected temporary path in allocated file");
+               return AST_TEST_FAIL;
+       }
+
+       if (strcmp(ast_sorcery_object_get_id(file), "test:///tmp/bob")) {
+               ast_test_status_update(test, "URI within allocated file is '%s' and should be test:///tmp/bob\n",
+                       ast_sorcery_object_get_id(file));
+               return AST_TEST_FAIL;
+       }
+
+       if (strcmp(file->scheme, "test")) {
+               ast_test_status_update(test, "Scheme within allocated file is '%s' and should be test\n",
+                       file->scheme);
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_create)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_create";
+               info->category = "/main/bucket/";
+               info->summary = "file creation unit test";
+               info->description =
+                       "Test creation of files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate file\n");
+               return AST_TEST_FAIL;
+       }
+
+       bucket_test_wizard_clear();
+
+       if (ast_bucket_file_create(file)) {
+               ast_test_status_update(test, "Failed to create file with URI '%s'\n",
+                       ast_sorcery_object_get_id(file));
+               return AST_TEST_FAIL;
+       }
+
+       if (!bucket_test_wizard_state.created) {
+               ast_test_status_update(test, "Bucket file creation returned success but scheme implementation never actually created it\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!ast_bucket_file_create(file)) {
+               ast_test_status_update(test, "Successfully created file with URI '%s' twice\n",
+                       ast_sorcery_object_get_id(file));
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_copy)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+       RAII_VAR(struct ast_bucket_file *, copy, NULL, ao2_cleanup);
+       FILE *temporary;
+       struct stat old, new;
+       RAII_VAR(struct ast_bucket_metadata *, metadata, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_copy";
+               info->category = "/main/bucket/";
+               info->summary = "bucket file copying unit test";
+               info->description =
+                       "Test copying of bucket files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate file\n");
+               return AST_TEST_FAIL;
+       }
+
+       ast_bucket_file_metadata_set(file, "bob", "joe");
+
+       if (!(temporary = fopen(file->path, "w"))) {
+               ast_test_status_update(test, "Failed to open temporary file '%s'\n", file->path);
+               return AST_TEST_FAIL;
+       }
+
+       fprintf(temporary, "bob");
+       fclose(temporary);
+
+       if (!(copy = ast_bucket_file_copy(file, "test:///tmp/bob2"))) {
+               ast_test_status_update(test, "Failed to copy file '%s' to test:///tmp/bob2\n",
+                       ast_sorcery_object_get_id(file));
+               return AST_TEST_FAIL;
+       }
+
+       if (stat(file->path, &old)) {
+               ast_test_status_update(test, "Failed to retrieve information on old file '%s'\n", file->path);
+               return AST_TEST_FAIL;
+       }
+
+       if (stat(copy->path, &new)) {
+               ast_test_status_update(test, "Failed to retrieve information on copy file '%s'\n", copy->path);
+               return AST_TEST_FAIL;
+       }
+
+       if (old.st_size != new.st_size) {
+               ast_test_status_update(test, "Copying of underlying temporary file failed\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (ao2_container_count(file->metadata) != ao2_container_count(copy->metadata)) {
+               ast_test_status_update(test, "Number of metadata entries does not match original\n");
+               return AST_TEST_FAIL;
+       }
+
+       metadata = ast_bucket_file_metadata_get(copy, "bob");
+       if (!metadata) {
+               ast_test_status_update(test, "Copy of file does not have expected metadata\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (strcmp(metadata->value, "joe")) {
+               ast_test_status_update(test, "Copy of file contains metadata for 'bob' but value is not what it should be\n");
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_retrieve)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_retrieve";
+               info->category = "/main/bucket/";
+               info->summary = "file retrieval unit test";
+               info->description =
+                       "Test retrieval of files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_retrieve("test://tmp/bob"))) {
+               ast_test_status_update(test, "Failed to retrieve known valid file\n");
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_update)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_update";
+               info->category = "/main/bucket/";
+               info->summary = "file updating unit test";
+               info->description =
+                       "Test updating of files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate file\n");
+               return AST_TEST_FAIL;
+       }
+
+       bucket_test_wizard_clear();
+
+       if (ast_bucket_file_update(file)) {
+               ast_test_status_update(test, "Failed to update file with URI '%s'\n",
+                       ast_sorcery_object_get_id(file));
+               return AST_TEST_FAIL;
+       }
+
+       if (!bucket_test_wizard_state.updated) {
+               ast_test_status_update(test, "Successfully returned file was updated, but it was not\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!ast_bucket_file_update(file)) {
+               ast_test_status_update(test, "Successfully updated file with URI '%s' twice\n",
+                       ast_sorcery_object_get_id(file));
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_delete)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_delete";
+               info->category = "/main/bucket/";
+               info->summary = "file deletion unit test";
+               info->description =
+                       "Test deletion of files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate file\n");
+               return AST_TEST_FAIL;
+       }
+
+       bucket_test_wizard_clear();
+
+       if (ast_bucket_file_delete(file)) {
+               ast_test_status_update(test, "Failed to delete file with URI '%s'\n",
+                       ast_sorcery_object_get_id(file));
+               return AST_TEST_FAIL;
+       }
+
+       if (!bucket_test_wizard_state.deleted) {
+               ast_test_status_update(test, "Bucket file deletion returned success but scheme implementation never actually deleted it\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!ast_bucket_file_delete(file)) {
+               ast_test_status_update(test, "Successfully deleted file with URI '%s' twice\n",
+                       ast_sorcery_object_get_id(file));
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_metadata_set)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+       RAII_VAR(struct ast_bucket_metadata *, metadata, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_metadata_set";
+               info->category = "/main/bucket/";
+               info->summary = "file metadata setting unit test";
+               info->description =
+                       "Test setting of metadata on files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (ao2_container_count(file->metadata) != 0) {
+               ast_test_status_update(test, "Newly allocated file has metadata count of '%d' when should be 0\n",
+                       ao2_container_count(file->metadata));
+               return AST_TEST_FAIL;
+       }
+
+       if (ast_bucket_file_metadata_set(file, "bob", "joe")) {
+               ast_test_status_update(test, "Failed to set metadata 'bob' to 'joe' on newly allocated file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!(metadata = ao2_find(file->metadata, "bob", OBJ_KEY))) {
+               ast_test_status_update(test, "Failed to find set metadata 'bob' on newly allocated file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (strcmp(metadata->value, "joe")) {
+               ast_test_status_update(test, "Metadata has value '%s' when should be 'joe'\n",
+                       metadata->value);
+               return AST_TEST_FAIL;
+       }
+
+       ao2_cleanup(metadata);
+       metadata = NULL;
+
+       if (ast_bucket_file_metadata_set(file, "bob", "fred")) {
+               ast_test_status_update(test, "Failed to overwrite metadata 'bob' with new value 'fred'\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!(metadata = ao2_find(file->metadata, "bob", OBJ_KEY))) {
+               ast_test_status_update(test, "Failed to find overwritten metadata 'bob' on newly allocated file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (strcmp(metadata->value, "fred")) {
+               ast_test_status_update(test, "Metadata has value '%s' when should be 'fred'\n",
+                       metadata->value);
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_metadata_unset)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+       RAII_VAR(struct ast_bucket_metadata *, metadata, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_metadata_unset";
+               info->category = "/main/bucket/";
+               info->summary = "file metadata unsetting unit test";
+               info->description =
+                       "Test unsetting of metadata on files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (ast_bucket_file_metadata_set(file, "bob", "joe")) {
+               ast_test_status_update(test, "Failed to set metadata 'bob' to 'joe' on newly allocated file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (ast_bucket_file_metadata_unset(file, "bob")) {
+               ast_test_status_update(test, "Failed to unset metadata 'bob' on newly allocated file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if ((metadata = ao2_find(file->metadata, "bob", OBJ_KEY))) {
+               ast_test_status_update(test, "Metadata 'bob' was unset, but can still be found\n");
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_metadata_get)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+       RAII_VAR(struct ast_bucket_metadata *, metadata, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_metadata_get";
+               info->category = "/main/bucket/";
+               info->summary = "file metadata getting unit test";
+               info->description =
+                       "Test getting of metadata on files";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (ast_bucket_file_metadata_set(file, "bob", "joe")) {
+               ast_test_status_update(test, "Failed to set metadata 'bob' to 'joe' on newly allocated file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!(metadata = ast_bucket_file_metadata_get(file, "bob"))) {
+               ast_test_status_update(test, "Failed to retrieve metadata 'bob' that was just set\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (strcmp(metadata->value, "joe")) {
+               ast_test_status_update(test, "Retrieved metadata value is '%s' while it should be 'joe'\n",
+                       metadata->value);
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(bucket_file_json)
+{
+       RAII_VAR(struct ast_bucket_file *, file, NULL, ao2_cleanup);
+       RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref);
+       RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "bucket_file_json";
+               info->category = "/main/bucket/";
+               info->summary = "file json unit test";
+               info->description =
+                       "Test creation of JSON for a file";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(file = ast_bucket_file_alloc("test:///tmp/bob"))) {
+               ast_test_status_update(test, "Failed to allocate bucket\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (ast_bucket_file_metadata_set(file, "bob", "joe")) {
+               ast_test_status_update(test, "Failed to set metadata 'bob' to 'joe' on newly allocated file\n");
+               return AST_TEST_FAIL;
+       }
+
+       expected = ast_json_pack("{s: s, s: s, s: s, s: s, s: {s :s}}",
+               "modified", "0.000000", "created", "0.000000", "scheme", "test",
+               "id", "test:///tmp/bob", "metadata", "bob", "joe");
+       if (!expected) {
+               ast_test_status_update(test, "Could not produce JSON for expected bucket file value\n");
+               return AST_TEST_FAIL;
+       }
+
+       json = ast_bucket_file_json(file);
+       if (!json) {
+               ast_test_status_update(test, "Could not produce JSON for a valid file\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!ast_json_equal(json, expected)) {
+               ast_test_status_update(test, "Bucket file JSON does not match expected output\n");
+               return AST_TEST_FAIL;
+       }
+
+       return AST_TEST_PASS;
+}
+
+static int unload_module(void)
+{
+       AST_TEST_UNREGISTER(bucket_scheme_register);
+       AST_TEST_UNREGISTER(bucket_alloc);
+       AST_TEST_UNREGISTER(bucket_create);
+       AST_TEST_UNREGISTER(bucket_delete);
+       AST_TEST_UNREGISTER(bucket_retrieve);
+       AST_TEST_UNREGISTER(bucket_json);
+       AST_TEST_UNREGISTER(bucket_file_alloc);
+       AST_TEST_UNREGISTER(bucket_file_create);
+       AST_TEST_UNREGISTER(bucket_file_copy);
+       AST_TEST_UNREGISTER(bucket_file_retrieve);
+       AST_TEST_UNREGISTER(bucket_file_update);
+       AST_TEST_UNREGISTER(bucket_file_delete);
+       AST_TEST_UNREGISTER(bucket_file_metadata_set);
+       AST_TEST_UNREGISTER(bucket_file_metadata_unset);
+       AST_TEST_UNREGISTER(bucket_file_metadata_get);
+       AST_TEST_UNREGISTER(bucket_file_json);
+       return 0;
+}
+
+static int load_module(void)
+{
+       if (ast_bucket_scheme_register("test", &bucket_test_wizard, &bucket_file_test_wizard,
+               ast_bucket_file_temporary_create, ast_bucket_file_temporary_destroy)) {
+               ast_log(LOG_ERROR, "Failed to register Bucket test wizard scheme implementation\n");
+               return AST_MODULE_LOAD_FAILURE;
+       }
+
+       AST_TEST_REGISTER(bucket_scheme_register);
+       AST_TEST_REGISTER(bucket_alloc);
+       AST_TEST_REGISTER(bucket_create);
+       AST_TEST_REGISTER(bucket_delete);
+       AST_TEST_REGISTER(bucket_retrieve);
+       AST_TEST_REGISTER(bucket_json);
+       AST_TEST_REGISTER(bucket_file_alloc);
+       AST_TEST_REGISTER(bucket_file_create);
+       AST_TEST_REGISTER(bucket_file_copy);
+       AST_TEST_REGISTER(bucket_file_retrieve);
+       AST_TEST_REGISTER(bucket_file_update);
+       AST_TEST_REGISTER(bucket_file_delete);
+       AST_TEST_REGISTER(bucket_file_metadata_set);
+       AST_TEST_REGISTER(bucket_file_metadata_unset);
+       AST_TEST_REGISTER(bucket_file_metadata_get);
+       AST_TEST_REGISTER(bucket_file_json);
+       return AST_MODULE_LOAD_SUCCESS;
+}
+
+AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Bucket test module");