From dd33217762c1c4e5a3289213068bac1fa2efb83b Mon Sep 17 00:00:00 2001 From: Joshua Colp Date: Fri, 23 Aug 2013 21:49:47 +0000 Subject: Add the bucket API. 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 --- build_tools/menuselect-deps.in | 1 + configure | 143 ++++++ configure.ac | 3 + include/asterisk/autoconfig.h.in | 3 + include/asterisk/bucket.h | 397 ++++++++++++++++ include/asterisk/config_options.h | 10 + main/Makefile | 2 + main/asterisk.c | 6 + main/bucket.c | 963 ++++++++++++++++++++++++++++++++++++++ main/config_options.c | 5 + main/sorcery.c | 10 +- makeopts.in | 3 + tests/test_bucket.c | 883 ++++++++++++++++++++++++++++++++++ 13 files changed, 2426 insertions(+), 3 deletions(-) create mode 100644 include/asterisk/bucket.h create mode 100644 main/bucket.c create mode 100644 tests/test_bucket.c diff --git a/build_tools/menuselect-deps.in b/build_tools/menuselect-deps.in index bc7c26514..12f049f5c 100644 --- a/build_tools/menuselect-deps.in +++ b/build_tools/menuselect-deps.in @@ -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@ diff --git a/configure b/configure index 8ccfaa003..2d04b6d11 100755 --- 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 : diff --git a/configure.ac b/configure.ac index 9a814cba5..69228b517 100644 --- a/configure.ac +++ b/configure.ac @@ -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], diff --git a/include/asterisk/autoconfig.h.in b/include/asterisk/autoconfig.h.in index bbc035f92..1796a3f68 100644 --- a/include/asterisk/autoconfig.h.in +++ b/include/asterisk/autoconfig.h.in @@ -1022,6 +1022,9 @@ /* 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 index 000000000..a09ade5fd --- /dev/null +++ b/include/asterisk/bucket.h @@ -0,0 +1,397 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp + * + * 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 + * \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 */ diff --git a/include/asterisk/config_options.h b/include/asterisk/config_options.h index 8947521d3..55e40ad8a 100644 --- a/include/asterisk/config_options.h +++ b/include/asterisk/config_options.h @@ -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 diff --git a/main/Makefile b/main/Makefile index e3ed7c5d6..1d80f1198 100644 --- a/main/Makefile +++ b/main/Makefile @@ -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) diff --git a/main/asterisk.c b/main/asterisk.c index 959993147..abd4742c4 100644 --- a/main/asterisk.c +++ b/main/asterisk.c @@ -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 index 000000000..0efd4871c --- /dev/null +++ b/main/bucket.c @@ -0,0 +1,963 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp + * + * 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 + */ + +/*** MODULEINFO + uriparser + core + ***/ + +/*** DOCUMENTATION + + Bucket file API + + + + Scheme in use for bucket + + + Time at which the bucket was created + + + Time at which the bucket was last modified + + + + + Scheme in use for file + + + Time at which the file was created + + + Time at which the file was last modified + + + + +***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#ifdef HAVE_URIPARSER +#include +#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; +} diff --git a/main/config_options.c b/main/config_options.c index 37c743355..e789c1cfc 100644 --- a/main/config_options.c +++ b/main/config_options.c @@ -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 diff --git a/main/sorcery.c b/main/sorcery.c index b2bb87915..1bd55d453 100644 --- a/main/sorcery.c +++ b/main/sorcery.c @@ -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)); diff --git a/makeopts.in b/makeopts.in index fa9cce6c4..3b3d2d92f 100644 --- a/makeopts.in +++ b/makeopts.in @@ -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 index 000000000..68249d343 --- /dev/null +++ b/tests/test_bucket.c @@ -0,0 +1,883 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp + * + * 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 + * + */ + +/*** MODULEINFO + TEST_FRAMEWORK + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "") + +#include + +#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"); -- cgit v1.2.3