Tue, 29 Oct 2024 17:54:07 +0100
merge
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2019 Olaf Wintermann. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <libidav/utils.h> #include <cx/hash_map.h> #include <cx/utils.h> #include <cx/linked_list.h> #include <cx/printf.h> #include "scfg.h" #include "config.h" #define xstreq(a,b) xmlStrEqual(BAD_CAST a, BAD_CAST b) #define print_error(lineno, ...) \ do {\ fprintf(stderr, "Error (sync.xml line %u): ", lineno); \ fprintf(stderr, __VA_ARGS__); \ fprintf(stderr, "Abort.\n"); \ } while(0); #define print_warning(lineno, ...) \ do {\ fprintf(stderr, "Warning (sync.xml line %u): ", lineno); \ fprintf(stderr, __VA_ARGS__); \ } while(0); #ifdef _WIN32 #define ENV_HOME getenv("USERPROFILE") #else #define ENV_HOME getenv("HOME") #endif /* _WIN32 */ static CxMap *directories; CxIterator scfg_directory_iterator() { return cxMapIteratorValues(directories); } static int create_default_sync_config(char *file) { FILE *out = fopen(file, "w"); if(!out) { perror("Cannot create config file"); return -1; } fputs("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n", out); fputs("<configuration>\n", out); fputs("</configuration>\n", out); fclose(out); return 0; } static void add_regex_pattern(CxList *list, char *value, unsigned short xmlline) { regex_t regex; if (regcomp(®ex, value, REG_EXTENDED|REG_NOSUB)) { print_warning(xmlline, "Invalid regular expression (%s) ... skipped\n", value); } else { cxListAdd(list, ®ex); } } static int scfg_load_filter( xmlNode *node, CxList *include, CxList *exclude, CxList *tags) { node = node->children; while(node) { if(node->type == XML_ELEMENT_NODE) { char *value = util_xml_get_text(node); if(xstreq(node->name, "include")) { if(value) { add_regex_pattern(include, value, node->line); } } else if(xstreq(node->name, "exclude")) { if(value) { add_regex_pattern(exclude, value, node->line); } } else if(xstreq(node->name, "tags")) { if(value) { SyncTagFilter *tagfilter = parse_tagfilter_string( value, DAV_SYNC_TAGFILTER_SCOPE_RESOURCE); if(!tagfilter) { print_error( node->line, "malformed tag filter: %s\n", value); return 1; } else { // get scope xmlChar *scope = xmlGetNoNsProp(node, BAD_CAST "scope"); if(scope) { if(xstreq(scope, "resource")) { tagfilter->scope = DAV_SYNC_TAGFILTER_SCOPE_RESOURCE; } else if(xstreq(scope, "collection")) { tagfilter->scope = DAV_SYNC_TAGFILTER_SCOPE_COLLECTION; } else if(xstreq(scope, "all")) { tagfilter->scope = DAV_SYNC_TAGFILTER_SCOPE_RESOURCE | DAV_SYNC_TAGFILTER_SCOPE_COLLECTION; } else { tagfilter->scope = DAV_SYNC_TAGFILTER_SCOPE_RESOURCE; } } xmlFree(scope); cxListAdd(tags, tagfilter); } } } else { print_error(node->line, "unknown filter config element: %s\n", node->name); return 1; } if(!value) { print_error(node->line, "missing value for filter: %s\n", node->name); return 1; } } node = node->next; } return 0; } Filter* parse_filter(xmlNode *node) { CxList *include = cxLinkedListCreate(cxDefaultAllocator, NULL, sizeof(regex_t)); CxList *exclude = cxLinkedListCreate(cxDefaultAllocator, NULL, sizeof(regex_t)); CxList *tags = cxLinkedListCreate(cxDefaultAllocator, NULL, CX_STORE_POINTERS); cxDefineDestructor(include, regfree); cxDefineDestructor(exclude, regfree); // TODO: set tags destructor if(scfg_load_filter(node, include, exclude, tags)) { return NULL; } Filter *filter = malloc(sizeof(Filter)); filter->include = include; filter->exclude = exclude; filter->tags = tags; return filter; } void init_default_filter(Filter *filter) { if(cxListSize(filter->include) == 0) { regex_t matchall; regcomp(&matchall, ".*", REG_NOSUB); cxListAdd(filter->include, &matchall); } /* if(!filter->exclude) { regex_t *matchnothing = malloc(sizeof(regex_t)); regcomp(matchnothing, "///", REG_NOSUB); filter->exclude = ucx_list_append(NULL, matchnothing); } */ } static TagFormat str2tagformat(const char *str) { if(!strcmp(str, "text")) { return TAG_FORMAT_TEXT; } else if(!strcmp(str, "csv")) { return TAG_FORMAT_CSV; } else if(!strcmp(str, "xml")) { return TAG_FORMAT_XML; } else if(!strcmp(str, "macos")) { return TAG_FORMAT_MACOS; } return TAG_FORMAT_UNKNOWN; } #define CHECK_VALUE_RET_NULL(element, value) if(!(value)) \ {print_error(element->line, "missing value element: %s\n", element->name); return NULL;} static TagConfig* parse_tagconfig(xmlNode *node) { TagConfig conf; conf.store = TAG_STORE_XATTR; conf.local_format = TAG_FORMAT_TEXT; conf.server_format = TAG_FORMAT_XML; conf.xattr_name = NULL; conf.detect_changes = false; conf.conflict = TAG_NO_CONFLICT; xmlNode *c = node->children; // TODO: error handling while(c) { if(c->type == XML_ELEMENT_NODE) { char *value = util_xml_get_text(c); if(xstreq(c->name, "local-store")) { CHECK_VALUE_RET_NULL(c, value); if(xstreq(value, "xattr")) { conf.store = TAG_STORE_XATTR; } else { return NULL; } xmlChar *format = xmlGetNoNsProp(node, BAD_CAST "format"); if(format) { conf.local_format = str2tagformat((char*)format); xmlFree(format); } } else if(xstreq(c->name, "detect-changes")) { CHECK_VALUE_RET_NULL(c, value); conf.detect_changes = util_getboolean(value); } else if(xstreq(c->name, "xattr-name")) { if(!value) { return NULL; } conf.xattr_name = strdup(value); } else if(xstreq(c->name, "on-conflict")) { CHECK_VALUE_RET_NULL(c, value); if(xstreq(value, "no_conflict")) { conf.conflict = TAG_NO_CONFLICT; } else if(xstreq(value, "keep_local")) { conf.conflict = TAG_KEEP_LOCAL; } else if(xstreq(value, "keep_remote")) { conf.conflict = TAG_KEEP_REMOTE; } else if(xstreq(value, "merge")) { conf.conflict = TAG_MERGE; } else { fprintf(stderr, "on-conflict: unknown value: %s\n", value); return NULL; } } } c = c->next; } if(conf.store == TAG_STORE_XATTR && !conf.xattr_name) { switch(conf.local_format) { default: case TAG_FORMAT_TEXT: case TAG_FORMAT_CSV: case TAG_FORMAT_XML: conf.xattr_name = strdup(DEFAULT_TAG_XATTR); break; case TAG_FORMAT_MACOS: conf.xattr_name = strdup(MACOS_TAG_XATTR); break; } } TagConfig *tagconfig = malloc(sizeof(TagConfig)); *tagconfig = conf; return tagconfig; } static SplitConfig* parse_split(xmlNode *node) { Filter *filter = NULL; char *minsize = NULL; char *blocksize = NULL; xmlNode *c = node->children; while(c) { if(xstreq(c->name, "filter")) { filter = parse_filter(node); if(filter->tags) { fprintf(stderr, "splitconfig: tag filter not supported\n"); free_filter(*filter); free(filter); return NULL; } } else if(xstreq(c->name, "minsize")) { minsize = util_xml_get_text(c); } else if(xstreq(c->name, "blocksize")) { blocksize = util_xml_get_text(c); } c = c->next; } uint64_t sz = 0; if(!blocksize) { fprintf(stderr, "splitconfig: no blocksize specified\n"); return NULL; } size_t bsz_len = strlen(blocksize); if(bsz_len < 2) { fprintf(stderr, "splitconfig: blocksize too small\n"); return NULL; } if(!util_szstrtouint(blocksize, &sz)) { fprintf(stderr, "splitconfig: blocksize is not a number\n"); return NULL; } if(!filter && !minsize) { fprintf(stderr, "splitconfig: filter or minsize must be specified\n"); return NULL; } int64_t minsz = -1; if(minsize) { uint64_t m; if(!util_szstrtouint(minsize, &m)) { fprintf(stderr, "splitconfig: minsize is not a number\n"); return NULL; } minsz = (int64_t)m; } SplitConfig *sc = calloc(1, sizeof(SplitConfig)); if(filter) { init_default_filter(filter); } sc->minsize = minsz; sc->blocksize = (size_t)sz; return sc; } static CxList* parse_splitconfig(xmlNode *node, int *error) { CxList *splitconfig = cxLinkedListCreateSimple(CX_STORE_POINTERS); int err = 0; xmlNode *c = node->children; while(c) { if(c->type == XML_ELEMENT_NODE && xstreq(c->name, "split")) { SplitConfig *sc = parse_split(c); if(sc) { cxListAdd(splitconfig, sc); } else { err = 1; break; } } c = c->next; } if(error) { *error = err; } return splitconfig; } static Versioning* parse_versioning_config(xmlNode *node) { Versioning v; v.always = FALSE; v.type = VERSIONING_SIMPLE; v.collection = VERSIONING_DEFAULT_PATH; int err = 0; xmlChar *attr_type = xmlGetNoNsProp(node, BAD_CAST "type"); xmlChar *attr_always = xmlGetNoNsProp(node, BAD_CAST "always"); if(attr_type) { if(xstreq(attr_type, "simple")) { v.type = VERSIONING_SIMPLE; } else if(xstreq(attr_type, "deltav")) { v.type = VERSIONING_DELTAV; } else { print_error(node->line, "type attribute: unknown value: %s\n", attr_type); err = 1; } xmlFree(attr_type); } if(attr_always) { v.always = util_getboolean((const char*)attr_always); xmlFree(attr_always); } if(err) { return NULL; } xmlNode *c = node->children; while(c) { if(c->type == XML_ELEMENT_NODE) { char *value = util_xml_get_text(c); if(xstreq(c->name, "history")) { CHECK_VALUE_RET_NULL(c, value); v.collection = value; } } c = c->next; } v.collection = strdup(v.collection); Versioning *versioning = malloc(sizeof(Versioning)); *versioning = v; return versioning; } static int scfg_load_directory(xmlNode *node) { char *name = NULL; char *path = NULL; char *trash = NULL; char *collection = NULL; char *repository = NULL; char *database = NULL; char *logfile = NULL; TagConfig *tagconfig = NULL; Versioning *versioning = NULL; CxList *include = cxLinkedListCreateSimple(sizeof(regex_t)); CxList *exclude = cxLinkedListCreateSimple(sizeof(regex_t)); CxList *tagfilter = cxLinkedListCreateSimple(CX_STORE_POINTERS); CxList *splitconfig = NULL; int max_retry = 0; int allow_cmd = SYNC_CMD_PULL | SYNC_CMD_PUSH | SYNC_CMD_ARCHIVE | SYNC_CMD_RESTORE; bool backuppull = false; bool lockpull = false; bool lockpush = false; bool hashing = false; bool store_hash = false; bool pull_skip_hashing = false; //bool detect_copy = false; time_t lock_timeout = 0; uint32_t metadata = 0; uint32_t symlink = 0; PushStrategy pushstrat = PUSH_STRATEGY_METADATA; unsigned short parentlineno = node->line; node = node->children; while(node) { if(node->type == XML_ELEMENT_NODE) { char *value = util_xml_get_text(node); /* every key needs a value */ if(!value && !xstreq(node->name, "versioning")) { // TODO: only report if value is required print_error(node->line, "missing value for directory element: %s\n", node->name); return 1; } if(xstreq(node->name, "name")) { name = value; } else if(xstreq(node->name, "path")) { path = value; } else if(xstreq(node->name, "trash")) { trash = value; } else if(xstreq(node->name, "collection")) { collection = value; } else if(xstreq(node->name, "repository")) { repository = value; } else if(xstreq(node->name, "filter")) { if(scfg_load_filter(node, include, exclude, tagfilter)) { return 1; } } else if(xstreq(node->name, "database")) { database = value; } else if(xstreq(node->name, "logfile")) { logfile = value; } else if(xstreq(node->name, "tagconfig")) { tagconfig = parse_tagconfig(node); } else if(xstreq(node->name, "splitconfig")) { int err = 0; splitconfig = parse_splitconfig(node, &err); if(err) { return 1; } } else if(xstreq(node->name, "metadata")) { uint32_t md = 0; const char *delims = " ,\t\r\n"; char *metadatastr = strdup(value); char *m = strtok(metadatastr, delims); while(m) { if(!strcmp(m, "mtime")) { md |= FINFO_MTIME; } else if(!strcmp(m, "mode")) { md |= FINFO_MODE; } else if(!strcmp(m, "owner")) { md |= FINFO_OWNER; } else if(!strcmp(m, "xattr")) { md |= FINFO_XATTR; } else if(!strcmp(m, "all")) { md |= FINFO_MTIME | FINFO_MODE | FINFO_OWNER | FINFO_XATTR; } m = strtok(NULL, delims); } free(metadatastr); metadata = md; } else if(xstreq(node->name, "versioning")) { versioning = parse_versioning_config(node); } else if(xstreq(node->name, "max-retry")) { int64_t i; if(util_strtoint(value, &i) && i >= 0) { max_retry = (int)i; } else { print_warning(node->line, "unsigned integer value " "expected in <max-retry> element\n"); } } else if(xstreq(node->name, "allow-cmd")) { int cmds = 0; const char *delims = " ,\t\r\n"; char *cmdstr = strdup(value); char *cmd = strtok(cmdstr, delims); while(cmd) { if(!strcmp(cmd, "pull")) { cmds |= SYNC_CMD_PULL; } else if(!strcmp(cmd, "push")) { cmds |= SYNC_CMD_PUSH; } else if(!strcmp(cmd, "archive")) { cmds |= SYNC_CMD_ARCHIVE; } else if(!strcmp(cmd, "restore")) { cmds |= SYNC_CMD_RESTORE; } cmd = strtok(NULL, delims); } free(cmdstr); allow_cmd = cmds; } else if(xstreq(node->name, "backup-on-pull")) { backuppull = util_getboolean(value); } else if(xstreq(node->name, "lock-pull")) { lockpull = util_getboolean(value); } else if(xstreq(node->name, "lock-push")) { lockpush = util_getboolean(value); } else if(xstreq(node->name, "lock-timeout")) { int64_t t = 0; if(util_strtoint(value, &t)) { lock_timeout = (time_t)t; } else { print_warning(node->line, "integer value " "expected in <lock-timeout> element\n"); } } else if(xstreq(node->name, "hashing")) { hashing = util_getboolean(value); store_hash = hashing; } else if(xstreq(node->name, "push-strategy")) { if(value) { if(xstreq(value, "metadata")) { pushstrat = PUSH_STRATEGY_METADATA; } else if(xstreq(value, "hash")) { pushstrat = PUSH_STRATEGY_HASH; } } } else if(xstreq(node->name, "symlink-intern")) { if(!value) { print_error(node->line, "missing value"); } else if(xstreq(value, "sync")) { symlink |= SYNC_SYMLINK_SYNC; } else if(xstreq(value, "follow")) { // nothing, default } else if(xstreq(value, "ignore")) { symlink |= SYNC_SYMLINK_IGNORE_INTERN; } else { print_error(node->line, "unknown value: %s\n", value); } } else if(xstreq(node->name, "symlink-extern")) { if(!value) { print_error(node->line, "missing value"); } else if(xstreq(value, "follow")) { // nothing, default } else if(xstreq(value, "ignore")) { symlink |= SYNC_SYMLINK_IGNORE_EXTERN; } else { print_error(node->line, "unknown value: %s\n", value); } } else { print_error(node->line, "unknown directory config element: %s\n", node->name); return 1; } } node = node->next; } if(!name) { print_error(parentlineno, "missing name element for directory\n"); return 1; } if(!path) { print_error(parentlineno, "missing path element for directory %s\n", name); return 1; } if(!repository) { print_error(parentlineno, "missing repository element for directory %s\n", name); return 1; } if(!database) { print_error(parentlineno, "missing database element for directory %s\n", name); return 1; } SyncDirectory *dir = calloc(1, sizeof(SyncDirectory)); dir->name = strdup(name); dir->path = scfg_create_path(path); dir->collection = collection ? strdup(collection) : NULL; dir->repository = strdup(repository); dir->database = strdup(database); dir->logfile = logfile ? strdup(logfile) : NULL; dir->tagconfig = tagconfig; dir->versioning = versioning; dir->max_retry = max_retry; dir->allow_cmd = allow_cmd; dir->backuppull = backuppull; dir->lockpull = lockpull; dir->lockpush = lockpush; dir->hashing = hashing; dir->store_hash = store_hash; dir->pull_skip_hashing = pull_skip_hashing; dir->lock_timeout = lock_timeout; dir->metadata = metadata; dir->splitconfig = splitconfig; dir->symlink = symlink; dir->push_strategy = pushstrat; if((metadata & FINFO_MODE) == FINFO_MODE) { dir->db_settings = DB_STORE_MODE; } if((metadata & FINFO_OWNER) == FINFO_OWNER) { dir->db_settings |= DB_STORE_OWNER; } dir->filter.include = include; dir->filter.exclude = exclude; dir->filter.tags = tagfilter; init_default_filter(&dir->filter); if (trash && cx_strtrim(cx_str(trash)).length > 0) { if (trash[0] == '/' || trash[0] == '$') { dir->trash = scfg_create_path(trash); } else { char *t = util_concat_path(dir->path, trash); dir->trash = util_concat_path(t, "/"); free(t); } if(dir->trash[strlen(dir->trash)-1] != '/') { char *t = dir->trash; dir->trash = util_concat_path(t, "/"); free(t); } } else { dir->trash = NULL; } cxMapPut(directories, cx_hash_key_str(name), dir); return 0; } int load_sync_config() { directories = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 8); if(check_config_dir()) { fprintf(stderr, "Cannot create .dav directory\n"); return 1; } char *file = util_concat_path(ENV_HOME, ".dav/sync.xml"); struct stat s; if(stat(file, &s)) { switch(errno) { case ENOENT: { if(create_default_sync_config(file)) { return 1; } break; } default: { perror("Cannot load sync.xml"); } } free(file); return 0; } xmlDoc *doc = xmlReadFile(file, NULL, 0); if(!doc) { fprintf(stderr, "Cannot load sync.xml\n"); free(file); return -1; } int ret = 0; xmlNode *node = xmlDocGetRootElement(doc)->children; while(node && !ret) { if(node->type == XML_ELEMENT_NODE) { if(xstreq(node->name, "directory")) { ret = scfg_load_directory(node); } else { print_error(node->line, "unknown config element: %s\n", node->name); ret = 1; } } node = node->next; } xmlFreeDoc(doc); free(file); return ret; } SyncDirectory* scfg_get_dir(const char *name) { return cxMapGet(directories, cx_hash_key_str(name)); } int scfg_check_dir(SyncDirectory *dir) { struct stat s; if(stat(dir->path, &s)) { int err = errno; if(err == ENOENT) { fprintf(stderr, "directory %s does not exist.\n", dir->path); } else { fprintf(stderr, "Cannot stat directory %s.\n", dir->path); perror(NULL); } fprintf(stderr, "Abort.\n"); return -1; } if(dir->trash) { // create trash directory mode_t mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; if (util_mkdir(dir->trash, mode)) { if (errno != EEXIST) { fprintf(stderr, "Cannot create trash directory: %s\nAbort.\n", dir->trash); return -1; } } } return 0; } char* scfg_create_path(const char *cfg) { if(!cfg) { return NULL; } if(cfg[0] != '$') { return strdup(cfg); } cxstring s = cx_str(cfg); cxstring path = cx_strchr(cx_str(cfg), '/'); char *localpath = NULL; if(path.length > 0) { // path = $var/path/ cxstring var = cx_strsubsl(s, 1, path.ptr - s.ptr - 1); if(var.length > 0) { char *env = cx_strdup(var).ptr; char *envval = getenv(env); free(env); if(envval) { localpath = util_concat_path(envval, path.ptr); } else { fprintf( stderr, "Environment variable %.*s not set.\nAbort.\n", (int)var.length, var.ptr); exit(-1); } } else { localpath = cx_strdup(path).ptr; } } else { // path = $var char *envval = getenv(cfg + 1); if(envval) { localpath = strdup(envval); } else { fprintf( stderr, "Environment variable %s not set.\nAbort.\n", cfg); exit(-1); } } return localpath; } int add_directory(SyncDirectory *dir) { char *file = util_concat_path(ENV_HOME, ".dav/sync.xml"); xmlDoc *doc = xmlReadFile(file, NULL, 0); if(!doc) { free(file); fprintf(stderr, "Cannot load config.xml\n"); return 1; } xmlNode *root = xmlDocGetRootElement(doc); xmlNode *dirNode = xmlNewNode(NULL, BAD_CAST "directory"); xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t"); xmlNewTextChild(dirNode, NULL, BAD_CAST "name", BAD_CAST dir->name); xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t"); xmlNewTextChild(dirNode, NULL, BAD_CAST "path", BAD_CAST dir->path); xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t"); xmlNewTextChild(dirNode, NULL, BAD_CAST "repository", BAD_CAST dir->repository); xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t"); xmlNewTextChild(dirNode, NULL, BAD_CAST "collection", BAD_CAST dir->collection); xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t"); if(dir->trash) { xmlNewTextChild(dirNode, NULL, BAD_CAST "trash", BAD_CAST dir->trash); xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t"); } xmlNewTextChild(dirNode, NULL, BAD_CAST "database", BAD_CAST dir->database); xmlNodeAddContent(dirNode, BAD_CAST "\n\t"); xmlNodeAddContent(root, BAD_CAST "\n\t"); xmlAddChild(root, dirNode); xmlNodeAddContent(root, BAD_CAST "\n"); int ret = 0; if(xmlSaveFormatFileEnc(file, doc, "UTF-8", 1) == -1) { ret = 1; } xmlFreeDoc(doc); free(file); return ret; } char* generate_db_name(const char *basename) { char *dbname = NULL; int count = -1; while(!dbname) { cxmutstr name = count < 0 ? cx_asprintf("%s-db.xml", basename) : cx_asprintf("%s%d-db.xml", basename, count); count++; CxIterator i = cxMapIteratorValues(directories); bool unique = true; cx_foreach(SyncDirectory *, dir, i) { if(!cx_strcmp(cx_strcast(name), cx_str(dir->database))) { unique = false; break; } } if(unique) { dbname = name.ptr; break; } } return dbname; } void free_filter(Filter filter) { cxListDestroy(filter.include); cxListDestroy(filter.exclude); cxListDestroy(filter.tags); } void free_sync_config() { if(directories) { CxIterator i = cxMapIteratorValues(directories); cx_foreach(SyncDirectory *, dir, i) { free(dir->name); free(dir->path); free(dir->repository); free(dir->database); if(dir->collection) { free(dir->collection); } if(dir->trash) { free(dir->trash); } free_filter(dir->filter); free(dir); } cxMapDestroy(directories); } }