dav/sync.c

Thu, 28 Nov 2024 17:20:19 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Thu, 28 Nov 2024 17:20:19 +0100
changeset 849
c949c6ab5346
parent 833
8aa2dc02d9b7
permissions
-rw-r--r--

fix crash in printxmldoc, fixes #514

/*
 * 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 <signal.h>
#include <time.h>
#include <libxml/xmlerror.h>
#include <sys/types.h>
#include <cx/map.h>
#include <cx/utils.h>
#include <cx/list.h>
#include <cx/hash_map.h>
#include <cx/printf.h>

#ifndef _WIN32
// unix includes
#include <unistd.h>
#include <utime.h>
#include <pthread.h>
#else
//windows includes

#endif


#include <math.h>

#include <libidav/webdav.h>
#include <libidav/utils.h>
#include <libidav/crypto.h>

#include <libidav/session.h>

#include "sync.h"

#include "config.h"
#include "sopt.h"
#include "error.h"
#include "assistant.h"
#include "libxattr.h"
#include "tags.h"
#include "connect.h"

#include "system.h"


#ifdef _WIN32
#define strcasecmp _stricmp

#ifndef S_ISDIR
#define S_ISDIR(mode) ((mode) & _S_IFMT) == _S_IFDIR
#define S_ISREG(mode) ((mode) & _S_IFMT) == _S_IFREG
#endif

#endif

static DavContext *ctx;

static int sync_shutdown = 0;

static FILE *synclog;

static int orig_argc;
static char **orig_argv;

static void xmlerrorfnc(void * c, const char * msg, ... ) {
    va_list ap;
    va_start(ap, msg);
    vfprintf(stderr, msg, ap);
    va_end(ap);
}

static DavPropName defprops[] = {
    { "DAV:", "getetag" },
    { DAV_NS, "status" },
    { DAV_NS, "content-hash" },
    { DAV_NS, "split" },
    { DAV_PROPS_NS, "finfo" },
    { DAV_PROPS_NS, "tags" },
    { DAV_PROPS_NS, "xattributes" },
    { DAV_PROPS_NS, "link" }
};
static size_t numdefprops = 8 ;

void log_printf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    cxmutstr str = cx_vasprintf(fmt, ap);
    
    printf("%s", str.ptr);
    if(synclog) {
        fprintf(synclog, "%s", str.ptr);
    }
    free(str.ptr);
    
    va_end(ap);
}

void log_error(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    cxmutstr str = cx_vasprintf(fmt, ap);
    
    fprintf(stderr, "%s", str.ptr);
    if(synclog) {
        fprintf(synclog, "%s", str.ptr);
    }
    free(str.ptr);
    
    va_end(ap);
}

void log_resource_error(DavSession *sn, const char *path) {
    print_resource_error(sn, path);
    if(synclog) {
        print_resource_error_to_file(synclog, sn, path);
    }
}


int logfile_open(SyncDirectory *dir) {
    int ret = 0;
    if(dir && dir->logfile) {
        char *lf_path = dir->logfile;
        char *lf_path_fr = NULL;
        if(dir->logfile[0] != '/') {
            lf_path = config_file_path(dir->logfile);
            lf_path_fr = lf_path;
        }
        
        synclog = fopen(lf_path, "a");
        if(!synclog) {
            fprintf(stderr, "Cannot open logfile %s: %s\n", lf_path, strerror(errno));
            ret = 1;
        } else {
            time_t t = time(NULL);
            char *now = ctime(&t);
            size_t len = strlen(now);
            if(now[len-1] == '\n') {
                len--;
            }
            
            fprintf(synclog, "[%.*s] ", (int)len, now);
            for(int i=0;i<orig_argc;i++) {
                fprintf(synclog, "%s ", orig_argv[i]);
            }
            fprintf(synclog, "\n");
        }
        if(lf_path_fr) {
            free(lf_path_fr);
        }
    }
    return ret;
}

/*
 * strcmp version that works with NULL pointers
 */
static int nullstrcmp(const char *s1, const char *s2) {
    if(!s1 && s2) {
        return -1;
    }
    if(s1 && !s2) {
        return 1;
    }
    if(!s1 && !s2) {
        return 0;
    }
    return strcmp(s1, s2);
}

static char* nullstrdup(const char *s) {
    return s ? strdup(s) : NULL;
}

static void nullfree(void *p) {
    if(p) {
        free(p);
    }
}



static CxIterator mapIteratorValues(CxMap *map) {
    return cxMapIteratorValues(map ? map : cxEmptyMap);
}

static CxIterator listIterator(CxList *list) {
    return cxListIterator(list ? list : cxEmptyList);
}

typedef void*(*clonefunc)(void *elm, void *userdata);

static CxMap* mapClone(const CxAllocator *a, CxMap *map, clonefunc clone, void *userdata) {
    CxMap *newmap = cxHashMapCreate(
            a,
            cxMapIsStoringPointers(map) ? CX_STORE_POINTERS : map->collection.elem_size,
            cxMapSize(map) + 4
    );
    
    CxIterator i = cxMapIterator(map);
    if(clone) {
        cx_foreach(CxMapEntry*, entry, i) {
            void *newdata = clone(entry->value, userdata);
            cxMapPut(newmap, *entry->key, newdata);
        }
    } else {
        cx_foreach(CxMapEntry*, entry, i) {
            cxMapPut(newmap, *entry->key, entry->value);
        }
    }
    
    return newmap;
}

int dav_sync_main(int argc, char **argv);

#ifdef _WIN32
static char* wchar2utf8(const wchar_t *wstr, size_t wlen) {
    size_t maxlen = wlen * 4;
    char *ret = malloc(maxlen + 1);
    int ret_len = WideCharToMultiByte(
        CP_UTF8,
        0,
        wstr,
        wlen,
        ret,
        maxlen,
        NULL,
        NULL);
    ret[ret_len] = 0;
    return ret;
}

int wmain(int argc, wchar_t **argv) {
    char **argv_utf8 = calloc(argc, sizeof(char*));
    for(int i=0;i<argc;i++) {
        argv_utf8[i] = wchar2utf8(argv[i], wcslen(argv[i]));
    }
    
    int ret = dav_sync_main(argc, argv_utf8);
    
    for(int i=0;i<argc;i++) {
        free(argv_utf8[i]);
    }
    free(argv_utf8);
    
    return ret;
}
#else
int main(int argc, char **argv) {
    return dav_sync_main(argc, argv);
}
#endif

int dav_sync_main(int argc, char **argv) {
    orig_argc = argc;
    orig_argv = argv;
    
    if(argc < 2) {
        fprintf(stderr, "Missing command\n");
        print_usage(argv[0]);
        return -1;
    }
    
    char *cmd = argv[1];
    CmdArgs *args = cmd_parse_args(argc - 2, argv + 2);
    if(!args) {
        print_usage(argv[0]);
        return -1;
    }
    int ret = EXIT_FAILURE;
    
    if(!strcasecmp(cmd, "version") || !strcasecmp(cmd, "-version")
            || !strcasecmp(cmd, "--version")) {
        fprintf(stderr, "dav-sync %s\n", DAV_VERSION);
        cmd_args_free(args);
        return -1;
    }
    
    xmlGenericErrorFunc fnc = xmlerrorfnc;
    initGenericErrorDefaultFunc(&fnc);
    sys_init();
    ctx = dav_context_new();
    int cfgret = load_config(ctx) || load_sync_config();
    
    // ignore sigpipe to make sure the program doesn't exit
    // if stdout will be closed (for example by using dav-sync ... | head)
#ifndef _WIN32
    struct sigaction act;
    memset(&act, 0, sizeof(struct sigaction));
    act.sa_handler = SIG_IGN;
    sigaction(SIGPIPE, &act, NULL);

    // prepare signal handler thread
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    pthread_t tid;
#else
    int tid;
    int mutex;
#endif 
    
    if(!strcmp(cmd, "check") || !strcmp(cmd, "check-config")) {
        if(!cfgret) {
            fprintf(stdout, "Configuration OK.\n");
            ret = EXIT_SUCCESS;
        } else {
            /* no output, the warnings are written by load_config */
            ret = EXIT_FAILURE;
        }
    } else if(!cfgret) {
        if(!strcmp(cmd, "pull")) {
            tid = start_sighandler(&mutex);
            ret = cmd_pull(args, FALSE);
            stop_sighandler(&mutex, tid);
        } else if(!strcmp(cmd, "push")) {
            tid = start_sighandler(&mutex);
            ret = cmd_push(args, FALSE, FALSE);
            stop_sighandler(&mutex, tid);
        } else if(!strcmp(cmd, "outgoing")) {
            ret = cmd_push(args, TRUE, FALSE);
        } else if(!strcmp(cmd, "archive")) {
            tid = start_sighandler(&mutex);
            ret = cmd_push(args, FALSE, TRUE);
            stop_sighandler(&mutex, tid);
        } else if(!strcmp(cmd, "restore")) {
            tid = start_sighandler(&mutex);
            ret = cmd_restore(args);
            stop_sighandler(&mutex, tid);
        } else if(!strcmp(cmd, "list-conflicts")) {
            ret = cmd_list_conflicts(args);
        } else if(!strcmp(cmd, "resolve-conflicts")) {
            ret = cmd_resolve_conflicts(args);
        } else if(!strcmp(cmd, "delete-conflicts")) {
            ret = cmd_delete_conflicts(args);
        } else if(!strcmp(cmd, "list-versions")) {
            ret = cmd_list_versions(args);
        } else if(!strcmp(cmd, "trash-info")) {
            ret = cmd_trash_info(args);
        } else if(!strcmp(cmd, "empty-trash")) {
            ret = cmd_empty_trash(args);
        } else if(!strcmp(cmd, "add-tag")) {
            ret = cmd_add_tag(args);
        } else if(!strcmp(cmd, "remove-tag")) {
            ret = cmd_remove_tag(args);
        } else if(!strcmp(cmd, "set-tags")) {
            ret = cmd_set_tags(args);
        } else if(!strcmp(cmd, "list-tags")) {
            ret = cmd_list_tags(args);
        } else if(!strcmp(cmd, "add-dir")
                || !strcmp(cmd, "add-directory")) {
            ret = cmd_add_directory(args);
        } else if(!strcmp(cmd, "list-dirs")
                || !strcmp(cmd, "list-directories")) {
            ret = cmd_list_dirs();
        } else if(!strcmp(cmd, "check-repos")
                || !strcmp(cmd, "check-repositories")) {
            ret = cmd_check_repositories(args);
        } else {
            print_usage(argv[0]);
        }
    }
    
    // cleanup
    cmd_args_free(args);
    dav_context_destroy(ctx);
    
    free_config();
    free_sync_config();
    
    curl_global_cleanup();
    xmlCleanupParser();
    
    sys_uninit();
    
    return ret;
}

void print_usage(char *cmd) {
    fprintf(stderr, "Usage: %s command [options] arguments...\n\n", cmd);
    
    fprintf(stderr, "Commands:\n");
    fprintf(stderr, "        pull [-cldr] [-t <tags>] <directory>\n");
    fprintf(stderr, "        push [-cldrSRM] [-t <tags>] <directory>\n");
    fprintf(stderr, "        archive [-cldSRM] [-t <tags>] <directory>\n");
    fprintf(stderr,
        "        restore [-ldRM] [-V <version>] [-s <directory>] [file...]\n");
    fprintf(stderr, "        list-conflicts <directory>\n");
    fprintf(stderr, "        resolve-conflicts <directory>\n");
    fprintf(stderr, "        delete-conflicts <directory>\n");
    fprintf(stderr, "        trash-info <directory>\n");
    fprintf(stderr, "        empty-trash <directory>\n");
    fprintf(stderr, "        list-versions [-s <syncdir>] <file>\n");
    fprintf(stderr, "        add-tag [-s <syncdir>] <file> <tag>\n");
    fprintf(stderr, "        remove-tag [-s <syncdir>] <file> <tag>\n");
    fprintf(stderr, "        set-tags [-s <syncdir>] <file> [tags]\n");
    fprintf(stderr, "        list-tags [-s <syncdir>] <file>\n\n");
    
    fprintf(stderr, "Options:\n");
    fprintf(stderr, "        -c            Disable conflict detection\n");
    fprintf(stderr, "        -l            Lock the repository before access\n");
    fprintf(stderr, "        -d            Don't lock the repository\n");
    fprintf(stderr, "        -t <tags>     "
                    "Only sync files which have the specified tags\n");
    fprintf(stderr, "        -r            "
                    "Remove resources not matching the tag filter\n");
    fprintf(stderr, "        -V <vers>     Restore specific version\n");
    fprintf(stderr, "        -S            Save previous file version\n");
    fprintf(stderr, "        -R            Restore removed files\n");
    fprintf(stderr, "        -M            Restore modified files\n");
    fprintf(stderr, "        -s <syncdir>  Name of the syncdir the file is in\n");
    fprintf(stderr, "        -v            Verbose output (all commands)\n\n");
    
    fprintf(stderr, "Config commands:\n");
    fprintf(stderr, "        add-directory\n");
    fprintf(stderr, "        list-directories\n");
    fprintf(stderr, "        check-config\n");
    fprintf(stderr, "        check-repositories\n\n");
}

static void handlesig(int sig) {
    if(sync_shutdown) {
        exit(-1);
    }
    fprintf(stderr, "abort\n");
    sync_shutdown = 1;
}

#ifndef _WIN32
static void* sighandler(void *data) {
    signal(SIGTERM, handlesig);
    signal(SIGINT, handlesig);
    
    pthread_mutex_t *mutex = data;
    pthread_mutex_lock(mutex); // block thread
    return NULL;
}

pthread_t start_sighandler(pthread_mutex_t *mutex) {
    pthread_t tid;
    if(pthread_create(&tid, NULL, sighandler, mutex)) {
        perror("pthread_create");
        exit(-1);
    }
    return tid;
}

void stop_sighandler(pthread_mutex_t *mutex, pthread_t tid) {
    pthread_mutex_unlock(mutex);
    void *data;
    pthread_join(tid, &data);
}
#else

int start_sighandler(int* mutex) {
    return 0;
}
int stop_sighandler(int* mutex, int tid) {
    return 0;
}

#endif

static char* create_local_path(SyncDirectory *dir, const char *path) {
    char *local_path = util_concat_path(dir->path, path);
    size_t local_path_len = strlen(local_path);
    if(local_path[local_path_len-1] == '/') {
        local_path[local_path_len-1] = '\0';
    }
    return local_path;
}

static int res_matches_filter(Filter *filter, char *res_path) {
    // include/exclude filter
    CxIterator i = cxListIterator(filter->include);
    cx_foreach(regex_t*, pattern, i) {
        if (regexec(pattern, res_path, 0, NULL, 0) == 0) {
            CxIterator e = cxListIterator(filter->exclude);
            cx_foreach(regex_t*, expat, e) {
                if (regexec(expat, res_path, 0, NULL, 0) == 0) {
                    return 1;
                }
            }
            return 0;
        }
    }
    return 1;
}

static int res_matches_dir_filter(SyncDirectory *dir, char *res_path) {
    // trash filter
    if (dir->trash) {
        cxmutstr rpath = cx_mutstr(util_concat_path(dir->path, res_path));
        if (util_path_isrelated(dir->trash, rpath.ptr)) {
            free(rpath.ptr);
            return 1;
        }
        free(rpath.ptr);
    }
    
    // versioning filter
    if (dir->versioning) {
        if(util_path_isrelated(dir->versioning->collection, res_path)) {
            return 1;
        }
    }
    
    return res_matches_filter(&dir->filter, res_path);
}

static int res_matches_tags(DavResource *res, SyncTagFilter *tagfilter) {
    if(!tagfilter || tagfilter->mode == DAV_SYNC_TAGFILTER_OFF) {
        return 1;
    }
    // NOTE: currently not implementable
    //int scope = res->iscollection ?
    //        DAV_SYNC_TAGFILTER_SCOPE_COLLECTION
    //      : DAV_SYNC_TAGFILTER_SCOPE_RESOURCE;
    //if((tagfilter->scope & scope) != scope) {
    //    return 1;
    //}
    if(res->iscollection) {
        return 1;
    }
    
    DavXmlNode *tagsprop = dav_get_property_ns(res, DAV_PROPS_NS, "tags");
    CxList *res_tags = parse_dav_xml_taglist(tagsprop);
    
    int ret = matches_tagfilter(res_tags, tagfilter);
    
    cxListDestroy(res_tags);
    
    return ret;
}

static int localres_matches_tags(
        SyncDirectory *dir,
        LocalResource *res,
        SyncTagFilter *tagfilter)
{
    if(!tagfilter || tagfilter->mode == DAV_SYNC_TAGFILTER_OFF) {
        return 1;
    }
    //int scope = res->isdirectory ?
    //        DAV_SYNC_TAGFILTER_SCOPE_COLLECTION
    //      : DAV_SYNC_TAGFILTER_SCOPE_RESOURCE;
    //if((tagfilter->scope & scope) != scope) {
    //    return 1;
    //}
    if(res->isdirectory) {
        return 1;
    }

    DavBool changed = 0;
    CxList *res_tags = sync_get_file_tags(dir, res, &changed, NULL);
    if(!res_tags) {
        res_tags = cxEmptyList;
    }
    
    int ret = matches_tagfilter(res_tags, tagfilter);
    cxListDestroy(res_tags);
    return ret;
}

static int localres_cmp_path(LocalResource *a, LocalResource *b, void *n) {
    return strcmp(a->path, b->path);
}

static int localres_cmp_path_desc(LocalResource *a, LocalResource *b, void *n) {
    return -strcmp(a->path, b->path);
}

static DavSession* create_session(CmdArgs *a, DavContext *davctx, DavCfgRepository *repo, char *collection) {
    int flags = dav_repository_get_flags(repo);
    DavBool find_collection = TRUE;
    if((flags & DAV_SESSION_DECRYPT_NAME) != DAV_SESSION_DECRYPT_NAME) {
        char *url = util_concat_path(repo->url.value.ptr, collection);
        dav_repository_set_url(get_config(), repo, cx_str(url));
        free(url);
        collection = NULL;
        find_collection = FALSE;
    }
    if(!collection || (collection[0] == '/' && strlen(collection) == 1)) {
        // collection is NULL or "/"
        // we don't need to find any collection because the repo url is
        // the base url
        find_collection = FALSE;
    }
    
    DavSession *sn = connect_to_repo(davctx, repo, collection, request_auth, a);
    
    sn->flags = flags;
    sn->key = dav_context_get_key(davctx, repo->default_key.value.ptr);
    curl_easy_setopt(sn->handle, CURLOPT_HTTPAUTH, repo->authmethods);
    curl_easy_setopt(sn->handle, CURLOPT_SSLVERSION, repo->ssl_version);
    if(repo->cert.value.ptr) {
        curl_easy_setopt(sn->handle, CURLOPT_CAPATH, repo->cert.value.ptr);
    }
    if(!repo->verification.value) {
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYPEER, 0);
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYHOST, 0);
    }
    
    if(find_collection) {
        DavResource *col = dav_resource_new(sn, collection);
        dav_exists(col); // exec this to get the href
        // we actually don't care what the result is
        // if it doesn't exists, an error will occur later
        // and we can't handle it here
        char *newurl = util_concat_path(repo->url.value.ptr, util_resource_name(col->href));
        dav_session_set_baseurl(sn, newurl);
        free(newurl);
    }
      
    return sn;
}

static void print_allowed_cmds(SyncDirectory *dir) {
    fprintf(stderr, "Allowed commands: ");
    char *sep = "";
    if((dir->allow_cmd & SYNC_CMD_PULL) == SYNC_CMD_PULL) {
        fprintf(stderr, "pull");
        sep = ", ";
    }
    if((dir->allow_cmd & SYNC_CMD_PUSH) == SYNC_CMD_PUSH) {
        fprintf(stderr, "%spush", sep);
        sep = ", ";
    }
    if((dir->allow_cmd & SYNC_CMD_ARCHIVE) == SYNC_CMD_ARCHIVE) {
        fprintf(stderr, "%sarchive", sep);
    }
    fprintf(stderr, "\n");
}

static void localres_keep(SyncDatabase *db, const char *path) {
    LocalResource *local = cxMapRemoveAndGet(db->resources, cx_hash_key_str(path));
    if(local) {
        local->keep = TRUE;
    }
}

static int xattr_filter(const char *name, SyncDirectory *dir) {
    // exclude tag xattr
    if(
        dir->tagconfig &&
        dir->tagconfig->store == TAG_STORE_XATTR &&
        !strcmp(dir->tagconfig->xattr_name, name))
    {
        return 0;
    }
    return 1;
}

void res2map(DavResource *root, CxMap *map) {
    CxList *stack = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    cxListInsert(stack, 0, root->children);
    while(cxListSize(stack) > 0) {
        DavResource *res = cxListAt(stack, 0);
        cxListRemove(stack, 0);
        
        while(res) {
            cxMapPut(map, cx_hash_key_str(res->path), res);
            
            if(res->children) {
                cxListInsert(stack, 0, res->children);
            }
            res = res->next;
        }
    }
    cxListDestroy(stack);
}

static CxHashKey resource_path_key(DavResource *res) {
    CxHashKey key = { NULL, 0, 0 };
    if(res && res->path) {
        cxstring res_path = cx_str(res->path);
        if(res_path.length > 0 && res_path.ptr[res_path.length-1] == '/') {
            res_path.length--;
        }
        key = cx_hash_key(res_path.ptr, res_path.length);
    }
    return key;
}

int cmd_pull(CmdArgs *a, DavBool incoming) {
    if(a->argc != 1) {
        fprintf(stderr, "Too %s arguments\n", a->argc < 1 ? "few" : "many");
        return -1;
    }
    // if there are syntax errors in the command line, fail asap.
    SyncTagFilter* tagfilter = parse_tagfilter_string(
            cmd_getoption(a, "tags"), DAV_SYNC_TAGFILTER_SCOPE_RESOURCE);
    if (!tagfilter) {
        fprintf(stderr, "Malformed tag filter\n");
        return -1;
    }
    // TODO: tons of memory leaks...
    //       call free_tagfilter() before each return
    
    SyncDirectory *dir = scfg_get_dir(a->argv[0]);
    if(!dir) {
        fprintf(stderr, "Unknown sync dir: %s\n", a->argv[0]);
        return -1;
    }
    if(scfg_check_dir(dir)) {
        return -1;
    }
    if(logfile_open(dir)) {
        return -1;
    }
    
    if((dir->allow_cmd & SYNC_CMD_PULL) != SYNC_CMD_PULL) {
        fprintf(stderr, "Command 'pull' is not allowed for this sync dir\n");
        print_allowed_cmds(dir);
        return -1;
    }
    
    DavCfgRepository *repo = dav_config_get_repository(get_config(), cx_str(dir->repository));
    if(!repo) {
        fprintf(stderr, "Unknown repository %s\n", dir->repository);
        return -1;
    }
    
    SyncDatabase *db = load_db(dir->database);
    if(!db) {
        fprintf(stderr, "Cannot load database file: %s\n", dir->database);
        return -1;
    }
    remove_deleted_conflicts(dir, db);
    
    CxMap *hashes = NULL;
    if(SYNC_HASHING(dir)) {
        hashes = create_hash_index(db);
    }
    
    DavSession *sn = create_session(a, ctx, repo, dir->collection);
    cxMempoolRegister(sn->mp, db, (cx_destructor_func)destroy_db);
    if (cmd_getoption(a, "verbose")) {
        curl_easy_setopt(sn->handle, CURLOPT_VERBOSE, 1L);
        curl_easy_setopt(sn->handle, CURLOPT_STDERR, stderr);
        sn->logfunc = dav_verbose_log;
    }
    
    // lock repository
    char *locktokenfile = NULL;
    DavBool locked = FALSE;
    DavResource *root = dav_resource_new(sn, "/");
    root->iscollection = TRUE;
    if((dir->lockpush || cmd_getoption(a, "lock")) && !cmd_getoption(a, "nolock")) {
        if(dav_lock_t(root, dir->lock_timeout)) {
            log_resource_error(sn, "/");
            dav_session_destroy(sn);
            log_error("Abort\n");
            return -1;
        }
        DavLock *lock = dav_get_lock(sn, "/");
        if(lock) {
            log_printf("Lock-Token: %s\n", lock->token);
        }
        locked = TRUE;
        locktokenfile = create_locktoken_file(dir->name, lock->token);
    }
    
    int ret = 0;
    DavResource *ls = dav_query(sn, "select D:getetag,idav:split,idav:status,`idav:content-hash`,idavprops:tags,idavprops:finfo,idavprops:xattributes,idavprops:link from / with depth = infinity");
    if(!ls) {
        log_resource_error(sn, "/");
        if(locked) {
            if(dav_unlock(root)) {
                log_resource_error(sn, "/");
            } else {
                locked = FALSE;
            }
        }
        
        log_error("Abort\n");
        
        dav_session_destroy(sn);
        // TODO: free
        return -1;
    }
    if(!ls->iscollection) {
        fprintf(stderr, "%s is not a collection.\nAbort.\n", ls->path);
        if(locked) {
            if(dav_unlock(root)) {
                log_resource_error(sn, "/");
            } else {
                locked = FALSE;
            }
        }
        // TODO: free
        dav_session_destroy(sn);
        
        if(!locked && locktokenfile) {
            remove(locktokenfile);
        }
        
        return -1;
    }
    
    DavBool remove_file = cmd_getoption(a, "remove") ? 1 : 0;
    
    int sync_success = 0;
    int sync_delete = 0;
    int sync_error = 0;
    int sync_conflict = 0;
    
    CxList *res_modified = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    CxList *res_new = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    CxList *res_moved = cxLinkedListCreateSimple(CX_STORE_POINTERS); // type: MovedFile*
    CxList *res_link = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    CxList *res_conflict = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    CxList *res_mkdir = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    CxList *res_metadata = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    CxList *res_broken = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    CxMap  *lres_removed = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 16); // type: LocalResource*
    
    //UcxMap *svrres = ucx_map_new(db->resources->count);
    CxMap *dbres = mapClone(cxDefaultAllocator, db->resources, NULL, NULL);
    
    CxList *stack = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    cxListInsert(stack, 0, ls->children);
    while(cxListSize(stack) > 0) {
        DavResource *res = cxListAt(stack, 0);
        cxListRemove(stack, 0);
         
        while(res) {        
            DavBool res_filtered = FALSE;
            if (res_matches_dir_filter(dir, res->path)) {
                res_filtered = TRUE;
            } else {
                CxIterator iter = cxListIterator(dir->filter.tags);
                cx_foreach(SyncTagFilter *, tf, iter) {
                    if(!res_matches_tags(res, tf)) {
                        res_filtered = TRUE;
                        break;
                    }
                }
            }
            if(res_filtered) {
                // don't delete files filtered by config
                localres_keep(db, res->path);
                res = res->next;
                continue;
            }
            
            if (!res_matches_tags(res, tagfilter)) {
                if(!remove_file) {
                    localres_keep(db, res->path);
                }
                res = res->next;
                continue;
            }
            
            char *status = dav_get_string_property(res, "idav:status");
            if(status && !strcmp(status, "broken")) {
                localres_keep(db, res->path);
                cxListAdd(res_broken, res);
                res = res->next;
                continue;
            }
            
            // check if a resource has changed on the server
            int change = resource_get_remote_change(a, res, dir, db);
            switch(change) {
                case REMOTE_NO_CHANGE: break;
                case REMOTE_CHANGE_MODIFIED: {
                    cxListAdd(res_modified, res);
                    break;
                }
                case REMOTE_CHANGE_NEW: {
                    cxListAdd(res_new, res);
                    break;
                }
                case REMOTE_CHANGE_DELETED: break; // never happens
                case REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED: {
                    cxListAdd(res_conflict, res);
                    break;
                }
                case REMOTE_CHANGE_METADATA: {
                    cxListAdd(res_metadata, res);
                    break;
                }
                case REMOTE_CHANGE_MKDIR: {
                    cxListAdd(res_mkdir, res);
                    break;
                }
                case REMOTE_CHANGE_LINK: {
                    cxListAdd(res_link, res);
                    break;
                }
            }
            
            // remove every server resource from dbres
            // all remaining elements are the resources that are removed
            // on the server
            cxMapRemove(dbres, resource_path_key(res));
            
            if(!dav_get_property_ns(res, DAV_NS, "split") && res->children) {
                cxListInsert(stack, 0, res->children);
            }
            res = res->next;
        }
    }
    
    // find deleted resources
    // svrres currently contains all resources from the server
    // and will replace the current db->resources map later
    CxIterator i = mapIteratorValues(dbres);
    cx_foreach(LocalResource *, local, i) {
        if (res_matches_dir_filter(dir, local->path)) {
            continue;
        }
        if(!local->keep) {
            cxMapPut(lres_removed, cx_hash_key_str(local->path), local);

            // TODO: what is the meaning of this code? without overflow the condition is never true
            // if(lres_removed->size > lres_removed->size * 2) {
            //     cxMapRehash(lres_removed);
            // }
        }
    }
    
    //
    // BEGIN PULL
    //
    
    // the first thing we need are all directories to put the files in
    i = cxListIterator(res_mkdir);
    cx_foreach(DavResource *, res, i) {
        if(sync_get_collection(a, dir, res, db)) {
            sync_error++;
        }
    }
    
    // we need a map for all conflicts for fast lookups
    CxMap *conflicts = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, cxListSize(res_conflict)+16);
    i = cxListIterator(res_conflict);
    cx_foreach(DavResource *, res, i) {
        cxMapPut(conflicts, cx_hash_key_str(res->path), res);
    }
    
    if(SYNC_HASHING(dir)) {
        // check for moved/copied files
        SYS_STAT s;
        CxIterator mut_iter = cxListMutIterator(res_new);
        cx_foreach(DavResource *, res, mut_iter) {
            if(dav_get_property_ns(res, DAV_PROPS_NS, "link")) {
                continue;
            }
            
            char *hash = sync_get_content_hash(res);
            if(!hash) {
                continue;
            }
            
            LocalResource *local = cxMapGet(hashes, cx_hash_key_str(hash));
            if(!local) {
                continue;
            }
            
            char *local_path = util_concat_path(dir->path, local_resource_path(local));
            int staterr = sys_stat(local_path, &s);
            free(local_path);
            if(staterr) {
                // origin doesn't exist or is inaccessible
                continue;
            }
            
            MovedFile *mf = malloc(sizeof(MovedFile));
            mf->content = local;
            mf->resource = res;
            if(cxMapRemoveAndGet(lres_removed, cx_hash_key_str(local->path))) {
                mf->copy = FALSE;
            } else {
                mf->copy = TRUE;
            }
            
            cxListAdd(res_moved, mf);
            
            // remove item from res_new
            cxIteratorFlagRemoval(mut_iter);
        }
    }
    
    // do copy/move operations
    i = cxListIterator(res_moved);
    cx_foreach(MovedFile *, mf, i) {
        if(sync_shutdown) {
            break;
        }
        
        DavBool issplit = dav_get_property_ns(mf->resource, DAV_NS, "split") ? 1 : 0;  
        if(cxMapGet(conflicts, cx_hash_key_str(mf->resource->path))) {
            rename_conflict_file(dir, db, mf->resource->path, issplit);
            sync_conflict++;
        }
        
        // move file
        if(sync_move_resource(a, dir, mf->resource, mf->content, mf->copy, db, &sync_success)) {
            fprintf(stderr, "%s failed: %s\n", mf->copy?"copy":"move", mf->resource->path);
            sync_error++;
        }
    }
    
    // download all new, modified and conflict files
    for(int n=0;n<4;n++) {
        CxList *list;
        if(n == 0) {
            list = res_new;
        } else if(n == 1) {
            list = res_modified;
        } else if(n == 2) {
            list = res_conflict;
        } else {
            list = res_link;
        }
        CxIterator iter = cxListIterator(list);
        cx_foreach(DavResource *, res, iter) {
            if(sync_shutdown) {
                break;
            }
            
            DavBool issplit = dav_get_property_ns(res, DAV_NS, "split") ? 1 : 0;  
            if(cxMapGet(conflicts, cx_hash_key_str(res->path))) {
                rename_conflict_file(dir, db, res->path, issplit);
                sync_conflict++;
            }

            // download the resource
            if(sync_get_resource(a, dir, res->path, res, db, TRUE, &sync_success)) {
                fprintf(stderr, "resource download failed: %s\n", res->path);
                sync_error++;
            }
        }
    }
    
    // update metadata
    i = cxListIterator(res_metadata);
    cx_foreach(DavResource *, res, i) {
        if(sync_shutdown) {
            break;
        }
        
        LocalResource *local = cxMapGet(db->resources, resource_path_key(res));
        if(local) {
            log_printf("update: %s\n", res->path);
            char *res_path = resource_local_path(res);
            char *local_path = create_local_path(dir, res->path);
            free(res_path);
            if(sync_store_metadata(dir, local_path, local, res)) {
                fprintf(stderr, "Metadata update failed: %s\n", res->path);
                sync_error++;
            } else {
                SYS_STAT s;
                if(sys_stat(local_path, &s)) {
                    fprintf(stderr, "Cannot stat file after update: %s\n", strerror(errno));
                }
                sync_set_metadata_from_stat(local, &s);
                sync_success++;
            }
            free(local_path);
        } else {
            // this should never happen but who knows
            fprintf(stderr,
                    "Cannot update metadata of file %s: not in database\n",
                    res->path);
        }
    }
    
    CxList *rmdirs = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_pathlen_cmp, CX_STORE_POINTERS);
    i = cxMapIteratorValues(lres_removed);
    cx_foreach(LocalResource *, removed_res, i) {
        if(sync_shutdown) {
            break;
        }
        
        int rmlocal_ret = sync_remove_local_resource(dir, removed_res);
        if(rmlocal_ret == -1) {
            cxListAdd(rmdirs, removed_res);
        } else if(rmlocal_ret == 0) {
            LocalResource *local = cxMapRemoveAndGet(db->resources, cx_hash_key_str(removed_res->path));
            if(local) {
                local_resource_free(local);
            }
            sync_delete++;
        }     
    }
    cxMapDestroy(lres_removed);
    
    // sort dir list, we need to delete dirs with higher depth first
    cxListSort(rmdirs);
    // delete dirs
    i = cxListIterator(rmdirs);
    cx_foreach(LocalResource *, local_dir, i) {
        if(!sync_remove_local_directory(dir, local_dir)) {
            // dir successfully removed, now remove the related db entry
            LocalResource *local = cxMapRemoveAndGet(db->resources, cx_hash_key_str(local_dir->path));
            if(local) {
                local_resource_free(local);
            }
            sync_delete++;
        }
    }
    
    // unlock repository
    if(locked) {
        if(dav_unlock(root)) {
            log_resource_error(sn, "/");
            ret = -1;
        } else {
            locked = FALSE;
        }
    }
    
    // store db
    if(store_db(db, dir->database, dir->db_settings)) {
        fprintf(stderr, "Cannot store sync db\n");
        ret = -2;
    }
    
    // cleanup
    dav_session_destroy(sn);
    
    if(!locked && locktokenfile) {
        remove(locktokenfile);
    }
    
    // Report
    if(ret != -2) {
        char *str_success = sync_success == 1 ? "file" : "files";
        char *str_delete = sync_delete == 1 ? "file" : "files";
        char *str_error = sync_error == 1 ? "error" : "errors";
        char *str_conflict = sync_conflict == 1 ? "conflict" : "conflicts";
        log_printf("Result: %d %s pulled, %d %s deleted, %d %s, %d %s\n",
                sync_success, str_success,
                sync_delete,str_delete,
                sync_conflict, str_conflict,
                sync_error, str_error);
    }
    
    return ret;
}

RemoteChangeType resource_get_remote_change(
        CmdArgs *a,
        DavResource *res,
        SyncDirectory *dir,
        SyncDatabase *db)
{
    DavBool update_db = FALSE;
    
    char *etag = dav_get_string_property(res, "D:getetag");
    if(!etag) {
        fprintf(stderr, "Error: resource %s has no etag\n", res->path);
        return REMOTE_NO_CHANGE;
    }
    char *hash = sync_get_content_hash(res);
      
    DavBool issplit = dav_get_property(res, "idav:split") ? TRUE : FALSE;
    if(issplit) {
        util_remove_trailing_pathseparator(res->path);
    }
    DavBool iscollection = res->iscollection && !issplit;
    
    RemoteChangeType type = cmd_getoption(a, "conflict") ?
            REMOTE_CHANGE_MODIFIED : REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
    
    LocalResource *local = cxMapGet(db->resources, resource_path_key(res));
    char *local_path = create_local_path(dir, res->path);
    
    char *link = SYNC_SYMLINK(dir) ? 
            dav_get_string_property_ns(res, DAV_PROPS_NS, "link") : NULL;
    
    SYS_STAT s;
    DavBool exists = 1;
    if(sys_stat(local_path, &s)) {
        if(errno != ENOENT) {
            fprintf(stderr, "Cannot stat file: %s\n", local_path);
            free(local_path);
            return REMOTE_NO_CHANGE;
        }
        exists = 0;
    }
    
    RemoteChangeType ret = REMOTE_NO_CHANGE;
    if(iscollection) {
        if(!exists) {
            ret = REMOTE_CHANGE_MKDIR;
        } else if(local && S_ISDIR(s.st_mode)) {
            local->isdirectory = 1; // make sure isdirectory is set
        } else {
            // set change to REMOTE_CHANGE_MKDIR, which will fail later
            ret = REMOTE_CHANGE_MKDIR;
        }
    } else if(local) {
        DavBool nochange = FALSE;
        if(SYNC_SYMLINK(dir) && nullstrcmp(link, local->link_target)) {
            ret = REMOTE_CHANGE_LINK;
            nochange = TRUE;
            
            if(local->link_target) {
                LocalResource *local2 = local_resource_new(dir, db, local->path);
                if(type == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED && nullstrcmp(local->link_target, local2->link_target)) {
                    ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
                }            
                local_resource_free(local2);

                if(!nullstrcmp(link, local->link_target)) {
                    ret = REMOTE_NO_CHANGE;
                    update_db = TRUE;
                }
            } 
        } else if(issplit && local->hash && hash) {
            if(!strcmp(local->hash, hash)) {
                // resource is already up-to-date on the client
                nochange = TRUE;
            }
        } else if(local->etag) {
            cxstring e = cx_str(etag);
            if(cx_strprefix(e, CX_STR("W/"))) {
                e = cx_strsubs(e, 2);
            }
            if(!strcmp(e.ptr, local->etag)) {
                // resource is already up-to-date on the client
                nochange = TRUE;
            }
        }
        
        if(!nochange) {
            if(!(exists && s.st_mtime != local->last_modified)) {
                type = REMOTE_CHANGE_MODIFIED;
            }
            ret = type;
        }
    } else if(link) {
        // new file is a link
        ret = REMOTE_CHANGE_LINK;
        
        if(exists && type == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED) {
            // a file with the same name already exists
            // if it is a link, compare the targets
            LocalResource *local2 = local_resource_new(dir, db, res->path);
            if(local2) {
                if(local2->link_target) {
                    if(strcmp(link, local2->link_target)) {
                        ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
                    }
                } else {
                    ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
                }
                
                local_resource_free(local2);
            }
        }
        
    } else if(exists) {
        ret = type;
    } else {
        ret = REMOTE_CHANGE_NEW;
    }
     
    // if hashing is enabled we can compare the hash of the remote file
    // with the local file to test if a file is really modified
    char *update_hash = NULL;
    if (!iscollection &&
        !link &&
        (ret == REMOTE_CHANGE_MODIFIED || ret == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED) &&
        exists &&
        hash &&
        !dir->pull_skip_hashing)
    {
        // because rehashing a file is slow, there is a config element for
        // disabling this (pull-skip-hashing)
        char *local_hash = util_file_hash(local_path);
        if(local_hash) {
            if(!strcmp(hash, local_hash)) {
                ret = REMOTE_NO_CHANGE;
                update_db = TRUE;
                update_hash = local_hash;
                
                // if local already exists, update the hash here
                // because it is possible that there are metadata updates
                // and in this case the db will updated later and needs
                // the current hash
                if(local) {
                    if(local->hash) {
                        free(local->hash);
                    }
                    local->hash = local_hash;
                }
            } else {
                free(local_hash);
            }
        }
    }
    
    // if a file is not modified, check if the metadata has changed
    while(ret == REMOTE_NO_CHANGE && local) {
        // check if tags have changed
        if(dir->tagconfig) {
            DavXmlNode *tagsprop = dav_get_property_ns(res, DAV_PROPS_NS, "tags");
            CxList *remote_tags = NULL;
            if(tagsprop) {
                remote_tags = parse_dav_xml_taglist(tagsprop);
            }
            char *remote_hash = create_tags_hash(remote_tags);
            if(nullstrcmp(remote_hash, local->remote_tags_hash)) {
                ret = REMOTE_CHANGE_METADATA;
            }
            if(remote_hash) {
                free(remote_hash);
            }
            free_taglist(remote_tags);
            
            if(ret == REMOTE_CHANGE_METADATA) {
                break;
            }
        }
        
        // check if extended attributes have changed
        if((dir->metadata & FINFO_XATTR) == FINFO_XATTR) {
            DavXmlNode *xattr = dav_get_property_ns(res, DAV_PROPS_NS, "xattributes");
            char *xattr_hash = get_xattr_hash(xattr);
            if(nullstrcmp(xattr_hash, local->xattr_hash)) {
                ret = REMOTE_CHANGE_METADATA;
                break;
            }
        } 
        
        // check if finfo has changed
        DavXmlNode *finfo = dav_get_property_ns(res, DAV_PROPS_NS, "finfo");
        if((dir->metadata & FINFO_MODE) == FINFO_MODE) {
            FileInfo f;
            finfo_get_values(finfo, &f);
            if(f.mode_set && f.mode != local->mode) {
                ret = REMOTE_CHANGE_METADATA;
                break;
            }
        }
        
        break;
    }
    
    // if update_db is set, a file was modified on the server, but we already
    // have the file content, but we need to update the db
    if(ret == REMOTE_NO_CHANGE && update_db) {
        if(!local) {
            local = calloc(1, sizeof(LocalResource));
            local->path = strdup(res->path);
            
            cxMapPut(db->resources, cx_hash_key_str(local->path), local);
        }
        
        // update local res
        SYS_STAT statdata;
        if(!sys_stat(local_path, &statdata)) {
            sync_set_metadata_from_stat(local, &statdata);
        } else {
            fprintf(stderr, "stat failed for file: %s : %s", local_path, strerror(errno));
        }
        local_resource_set_etag(local, etag);
        if(!local->hash) {
            local->hash = update_hash;
        } // else: hash already updated
        if(link) {
            nullfree(local->link_target);
            local->link_target = link;
        }
    }
        
    free(local_path);
    return ret;
}

void sync_set_metadata_from_stat(LocalResource *local, SYS_STAT *s) {
    local->last_modified = s->st_mtime;
    local->mode = s->st_mode & 07777;
    local->uid = s->st_uid;
    local->gid = s->st_gid;
    local->size = s->st_size;
}

#ifdef _WIN32
#define fseeko fseek
#define ftello ftell
#endif

static CxList* sync_download_changed_parts(
        DavResource *res,
        LocalResource *local,
        FILE *out,
        size_t blocksize,
        uint64_t *blockcount,
        int64_t *truncate_file,
        int *err)
{
    CxList *updates = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    cxDefineDestructor(updates, filepart_free);
    
    size_t local_numparts = local ? local->numparts : 0;
    fseeko(out, 0, SEEK_END);
    off_t end = ftello(out);
    
    int error = 0;
    
    CxBuffer *buf = cxBufferCreate(NULL, blocksize, cxDefaultAllocator, 0);
    
    int64_t maxsize = -1;
    
    DavResource *part = res->children;
    uint64_t i = 0;
    while(part) {
        char *res_name = part->name;
        while(res_name[0] == '0' && res_name[1] != '\0') {
            res_name++;
        }
        uint64_t partnum = 0;
        if(util_strtouint(res_name, &partnum)) {
            DavBool download_part = FALSE;
            char *etag = dav_get_string_property_ns(part, "DAV:", "getetag");
            if(partnum >= local_numparts) {
                // new part
                download_part = TRUE;
            } else {
                FilePart p = local->parts[partnum]; // local is always non-null here
                if(etag) {
                    if(nullstrcmp(etag, p.etag)) {
                        download_part = TRUE;
                    }
                }
            }
            
            uint64_t offset;
            int mul_err = util_uint_mul(partnum, blocksize, &offset);
            if(mul_err || offset >= INT64_MAX) {
                error = 1;
                fprintf(stderr, "Error: part number too high\n");
                break;
            }
            
            int64_t block_end = 0;
            if(download_part) {
                if(fseeko(out, offset, SEEK_SET)) {
                    error = 1;
                    fprintf(stderr, "Error: fseek failed: %s\n", strerror(errno));
                    break;
                }
                buf->pos = 0;
                buf->size = 0;
                if(dav_get_content(part, buf,(dav_write_func)cxBufferWrite)) {
                    fprintf(stderr, "Error: cannot download part: %s\n", part->name);
                    error = 1;
                    break;
                }
                if(fwrite(buf->space, 1, buf->size, out) == 0) {
                    perror("write");
                    error = 1;
                    break;
                }
                
                FilePart *update = calloc(1, sizeof(FilePart));
                update->block = partnum;
                update->etag = etag ? strdup(etag) : NULL;
                update->hash = dav_create_hash(buf->space, buf->size);
                cxListAdd(updates, update);
                
                block_end = offset+buf->size;
            } else {
                if(offset+blocksize > end) {
                    // if we don't download the block, we don't know the size
                    // but it can't be bigger than the file
                    block_end = end;
                } else {
                    block_end = offset+blocksize;
                }
            }
            
            if(block_end > maxsize) {
                maxsize = block_end;
            }
            
            i++;
        } // else: res is not a regular file part
        part = part->next;
    }
    
    cxBufferFree(buf);
    
    if(error) {
        *err = 1;
        cxListDestroy(updates);
        return NULL;
    }
    
    if(maxsize < end) {
        *truncate_file = maxsize;
    } else {
        *truncate_file = -1;
    }
    
    *err = 0;
    *blockcount = i;
    return updates;
}

int copy_file(const char *from, const char *to) {
    FILE *in = sys_fopen(from, "rb");
    if(!in) {
        return 1;
    }
    FILE *out = sys_fopen(to, "wb");
    if(!out) {
        fclose(in);
        return 1;
    }
    
    cx_stream_copy(in, out, (cx_read_func)fread, (cx_write_func)fwrite);
    fclose(in);
    fclose(out);
    
    return 0;
}

typedef int (*renamefunc)(const char*,const char*);

int sync_move_resource(
        CmdArgs *a,
        SyncDirectory *dir,
        DavResource *res,
        LocalResource *content,
        DavBool copy,
        SyncDatabase *db,
        int *counter)
{
    renamefunc fn = copy ? copy_file : sys_rename;
    
    char *new_path = util_concat_path(dir->path, res->path);
    char *old_path = util_concat_path(dir->path, local_resource_path(content));
    
    log_printf("%s: %s -> %s\n", copy?"copy":"move", content->path, res->path);
    if(fn(old_path, new_path)) {
        free(new_path);
        free(old_path);
        return 1;
    }
    (*counter)++;
    
    char *etag = dav_get_string_property(res, "D:getetag");
    char *content_hash = sync_get_content_hash(res);
    
    LocalResource *local = NULL;
    if(copy) {
        // TODO: maybe we should not copy the whole resource
        // with all metadata hashes
        local = local_resource_copy(content, res->path);
    } else {
        // reuse previous LocalResource (content)
        // remove it from db->resources, change path and put it back
        local = cxMapRemoveAndGet(db->resources, cx_hash_key_str(content->path));
        if(!local) {
            // can't happen, but handle it nevertheless
            local = content;
        }
        free(content->path);
        local->path = strdup(res->path);
    }
    cxMapPut(db->resources, cx_hash_key_str(local->path), local);
    
    if(sync_store_metadata(dir, new_path, local, res)) {
        fprintf(stderr, "Cannot store metadata: %s\n", res->path);
    }
    
    // dont free local->etag, because local_resource_set_etag will do that
    
    if(local->hash) {
        free(local->hash);
    }
    
    SYS_STAT s;
    if(sys_stat(new_path, &s)) {
        fprintf(stderr,
                "Cannot stat file %s: %s\n", new_path, strerror(errno));
    }
    
    // set metadata from stat
    local_resource_set_etag(local, etag);
    local->hash = nullstrdup(content_hash);
    
    sync_set_metadata_from_stat(local, &s);
    local->skipped = FALSE;
    
    return 0;
}

int sync_get_resource(
        CmdArgs *a,
        SyncDirectory *dir,
        const char *path,
        DavResource *res,
        SyncDatabase *db,
        DavBool update_db,
        int *counter)
{ 
    char *link = SYNC_SYMLINK(dir) ?
            dav_get_string_property_ns(res, DAV_PROPS_NS, "link") : NULL;
    
    LocalResource *local = cxMapGet(db->resources, cx_hash_key_str(path));
    
    char *local_path;
    if(link) {
        char *res_path = resource_local_path(res);
        local_path = create_local_path(dir, res_path);
        free(res_path);
    } else {
        local_path = create_local_path(dir, path);
    }
    
    char *etag = dav_get_string_property(res, "D:getetag");
    SYS_STAT s;
    memset(&s, 0, sizeof(SYS_STAT));
    
    char *blocksize_str = dav_get_string_property_ns(res, DAV_NS, "split");
    uint64_t blocksize = 0;
    DavBool issplit = FALSE;
    if(blocksize_str && !link) {
        if(!util_strtouint(blocksize_str, &blocksize)) {
            fprintf(stderr, "Error: split property does not contain an integer.\n");
            return 1;
        }
        issplit = TRUE;
    }
    CxList *part_updates = NULL;
    uint64_t blockcount = 0;
    char *content_hash = NULL;
       
    if(res->iscollection && !issplit) {
        // why are we here?
        return 0;
    }
      
    int ret = 0;
    
    char *tmp_path = NULL;
    FILE *out = NULL;
    if(!link) {
        if(!issplit) {
            tmp_path = create_tmp_download_path(local_path);
            if(!tmp_path) {
                fprintf(stderr, "Cannot create tmp path for %s\n", local_path);
                free(local_path);
                return -1;
            }
            out = sys_fopen(tmp_path , "wb");
        } else {
            out = sys_fopen(local_path, "r+b");
            if(!out && errno == ENOENT) {
                out = sys_fopen(local_path, "wb");
            }
        }
        if(!out) {
            fprintf(stderr, "Cannot open output file: %s\n", local_path);
            free(local_path);
            if(tmp_path) {
                free(tmp_path);
            }
            return -1;
        }
    }
    
    int64_t truncate_file = -1;
    if(!link) {
        log_printf("get: %s\n", path);
        if(issplit) {
            part_updates = sync_download_changed_parts(res, local, out, blocksize, &blockcount, &truncate_file, &ret);
        } else {
            ret = dav_get_content(res, out, (dav_write_func)fwrite);
        }
        fclose(out);
    } else {
        log_printf("link: %s -> %s\n", path, link);
        if(sys_symlink(link, local_path)) {
            perror("symlink");
            ret = 1;
        }
    }
    
    if(issplit || (SYNC_HASHING(dir) && !link)) {
        if(truncate_file >= 0) {
            // only true if issplit is true
            if(sys_truncate(local_path, truncate_file)) {
                perror("truncate");
            }
        }
        
        char *res_hash = sync_get_content_hash(res);
        if(res_hash) {
            content_hash = res_hash;
        } else {
            content_hash = util_file_hash(local_path);
        }
    }

    if(ret == 0) {
        (*counter)++;
        
        if(tmp_path) {
            if(dir->trash && dir->backuppull) {
                move_to_trash(dir, local_path);
            }
            if(sys_rename(tmp_path, local_path)) {
                fprintf(
                        stderr,
                        "Cannot rename file %s to %s\n",
                        tmp_path,
                        local_path);
                perror("");
                free(tmp_path);
                free(local_path);
                return -1;
            }
        }
        
    } else if(tmp_path) {
        if(sys_unlink(tmp_path)) {
            fprintf(stderr, "Cannot remove tmp file: %s\n", tmp_path);
        }
    }
    
    if(update_db && ret == 0) {
        if(!local) {
            // new local resource
            local = calloc(1, sizeof(LocalResource));
            local->path = strdup(path);
            cxMapPut(db->resources, cx_hash_key_str(local->path), local);
        }
        
        if(sync_store_metadata(dir, local_path, local, res)) {
            fprintf(stderr, "Cannot store metadata: %s\n", path);
        }

        if(local->etag) {
            free(local->etag);
            local->etag = NULL;
        }
        if(local->hash) {
            free(local->hash);
            local->hash = NULL;
        }
        if(local->link_target) {
            free(local->link_target);
            local->link_target = NULL;
        }
        
        stat_func statfn;
        if(link) {
            local->link_target = strdup(link);
            statfn = sys_lstat;
        } else {
            statfn = sys_stat;
        }
        
        update_parts(local, part_updates, blockcount);
        
        if(statfn(local_path, &s)) {
            fprintf(stderr,
                    "Cannot stat file %s: %s\n", local_path, strerror(errno));
        }
        
        // set metadata from stat
        if(!issplit) {
            local_resource_set_etag(local, etag);
        }
        if(content_hash) {
            local->hash = content_hash;
        }
        sync_set_metadata_from_stat(local, &s);
        local->skipped = FALSE;
    }
    
    if(tmp_path) {
        free(tmp_path);
    }
    free(local_path);
    return ret;
}

int sync_get_collection(
        CmdArgs *a,
        SyncDirectory *dir,
        DavResource *res,
        SyncDatabase *db)
{
    cxstring res_path = cx_str(res->path);
    if(res_path.length > 0 && res_path.ptr[res_path.length-1] == '/') {
        res_path.length--;
    }
    
    // TODO: use resource_local_path return value (necessary for creating links on windows)
    //char *res_path = resource_local_path(res);
    char *local_path = create_local_path(dir, res->path);
    //free(res_path);
     
    log_printf("get: %s\n", res->path);
    // create directory
    // ignore error if it already exists
    if(sys_mkdir(local_path) && errno != EEXIST) {
        fprintf(stderr,
                "Cannot create directory %s: %s",
                local_path, strerror(errno));
        free(local_path);
        return 1;
    }
    
    // stat for getting metadata
    SYS_STAT s;
    if(sys_stat(local_path, &s)) {
        fprintf(stderr, "Cannot stat directory %s: %s", local_path, strerror(errno));
        free(local_path);
        return 1;
    }
      
    // if it doesn't exist in the db, create an entry for the dir
    LocalResource *local = cxMapGet(db->resources, cx_hash_key(res_path.ptr, res_path.length));
    if(!local) {
        local = calloc(1, sizeof(LocalResource));
        local->path = cx_strdup(res_path).ptr;
        cxMapPut(db->resources, cx_hash_key_str(local->path), local);
    }
    local->isdirectory = 1;
    
    // cleanup LocalResource
    if(local->etag) {
        free(local->etag);
        local->etag = NULL;
    }
    if(local->hash) {
        free(local->hash);
        local->hash = NULL;
    }
    if(local->link_target) {
        free(local->link_target);
        local->link_target = NULL;
    }
    local->skipped = 0;
    local->size = 0;
    
    // set metadata
    sync_set_metadata_from_stat(local, &s);
    if(sync_store_metadata(dir, local_path, local, res)) {
        fprintf(stderr, "Cannot store metadata: %s\n", res->path);
    }
    
    // cleanup
    free(local_path);
    return 0;
}

int sync_remove_local_resource(SyncDirectory *dir, LocalResource *res) {
    char *local_path = create_local_path(dir, res->path);
    SYS_STAT s;
    if(sys_stat(local_path, &s)) {
        free(local_path);
        return -2;
    }
    
    if(S_ISDIR(s.st_mode)) {
        free(local_path);
        return -1;
    }
    
    if(s.st_mtime != res->last_modified) {
        free(local_path);
        return -2;
    }
    
    log_printf("delete: %s\n", res->path);
    int ret = 0;
    if(dir->trash) {
        move_to_trash(dir, local_path);
    } else if(sys_unlink(local_path)) {
        fprintf(stderr, "Cannot remove file %s\n", local_path);
        ret = -2;
    }
    free(local_path);
    
    return ret;
}

int sync_remove_local_directory(SyncDirectory *dir, LocalResource *res) {
    int ret = 0;
    char *local_path = create_local_path(dir, res->path);
    
    log_printf("delete: %s\n", res->path);
    if(rmdir(local_path)) {
        // don't print error when dirs are not empty
        // because that can regulary happen, because the pull conflict
        // detection can prevent files from being deleted and in this case
        // the parent dir is not empty
        if(errno != ENOTEMPTY) {
            fprintf(stderr, "rmdir: %s : %s\n", local_path, strerror(errno));
        }
        ret = 1;
    }
    
    free(local_path);
    return ret;
}

void rename_conflict_file(SyncDirectory *dir, SyncDatabase *db, char *path, DavBool copy) {
    char *local_path = create_local_path(dir, path);
    char *parent = util_parent_path(local_path);
    
    renamefunc fn = copy ? copy_file : sys_rename;
    
    int rev = 0;
    SYS_STAT s;
    int loop = 1;
    do {
        char *res_parent = util_parent_path(path);
        const char *res_name = util_resource_name(path);
        
        cxmutstr new_path = cx_asprintf(
            "%sorig.%d.%s",
            parent,
            rev,
            res_name);
        cxmutstr new_res_path = cx_asprintf(
            "%sorig.%d.%s",
            res_parent,
            rev,
            res_name);
        
        
        if(sys_stat(new_path.ptr, &s)) {
            if(errno == ENOENT) {
                loop = 0;
                log_printf("conflict: %s\n", local_path);
                if(fn(local_path, new_path.ptr)) {
                    //printf("errno: %d\n", errno);
                    fprintf(
                            stderr,
                            "Cannot rename file %s to %s\n",
                            local_path,
                            new_path.ptr);
                } else {
                    LocalResource *conflict = calloc(1, sizeof(LocalResource));
                    conflict->path = strdup(new_res_path.ptr);
                    conflict->conflict_source = strdup(path);
                    cxMapPut(db->conflict, cx_hash_key_str(new_res_path.ptr), conflict);
                }
            }
        }
        rev++;
        free(res_parent);
        free(new_path.ptr);
        free(new_res_path.ptr);
        
    } while(loop);
    free(parent);
    free(local_path);
}

char* create_tmp_download_path(char *path) {
    char *new_path = NULL;
    char *parent = util_parent_path(path);
    for (int i=0;;i++) {
        cxmutstr np = cx_asprintf(
            "%sdownload%d-%s",
            parent,
            i,
            util_resource_name(path));
        
        SYS_STAT s;
        if(sys_stat(np.ptr, &s)) {
            if(errno == ENOENT) {
                new_path = np.ptr;
            }
            break;
        }
        free(np.ptr);
    }
    
    free(parent);
    return new_path;
}

void move_to_trash(SyncDirectory *dir, char *path) {
    char *new_path = NULL;
    for (int i=0;;i++) {
        cxmutstr np = cx_asprintf(
            "%s%d-%s",
            dir->trash,
            i,
            util_resource_name(path));
        
        SYS_STAT s;
        if(sys_stat(np.ptr, &s)) {
            if(errno == ENOENT) {
                new_path = np.ptr;
            }
            break;
        }
        free(np.ptr);
    }
    
    if(!new_path) {
        fprintf(stderr, "Cannot move file %s to trash.\n", path);
        return;
    }
    
    if(sys_rename(path, new_path)) {
        //printf("errno: %d\n", errno);
        fprintf(
                stderr,
                "Cannot rename file %s to %s\n",
                path,
                new_path);
    }
    
    free(new_path);
}

static int res_isconflict(SyncDatabase *db, LocalResource *res) {
    return cxMapGet(db->conflict, cx_hash_key_str(res->path)) ? 1 : 0;
}

int cmd_push(CmdArgs *a, DavBool outgoing, DavBool archive) {
    if(a->argc != 1) {
        fprintf(stderr, "Too %s arguments\n", a->argc < 1 ? "few" : "many");
        return -1;
    }
    
    // if there are syntax errors in the command line, fail asap.
    SyncTagFilter* tagfilter = parse_tagfilter_string(
            cmd_getoption(a, "tags"), DAV_SYNC_TAGFILTER_SCOPE_RESOURCE);
    if (!tagfilter) {
        fprintf(stderr, "Malformed tag filter\n");
        return -1;
    }
    
    SyncDirectory *dir = scfg_get_dir(a->argv[0]);
    if(!dir) {
        fprintf(stderr, "Unknown sync dir: %s\n", a->argv[0]);
        return -1;
    }
    if(scfg_check_dir(dir)) {
        return -1;
    }
    if(logfile_open(dir)) {
        return -1;
    }
    if(cmd_getoption(a, "snapshot")) {
        if(dir->versioning) {
            dir->versioning->always = TRUE;
        } else {
            fprintf(stderr, "Error: versioning not configured for the sync directory\nAbort.\n");
            return -1;
        }
    }
    
    int cmd = archive ? SYNC_CMD_ARCHIVE : SYNC_CMD_PUSH;
    if((dir->allow_cmd & cmd) != cmd) {
        fprintf(stderr, "Command '%s' is not allowed for this sync dir\n", archive ? "archive" : "push");
        print_allowed_cmds(dir);
        return -1;
    }
    
    DavCfgRepository *repo = dav_config_get_repository(get_config(), cx_str(dir->repository));
    if(!repo) {
        fprintf(stderr, "Unkown repository %s\n", dir->name);
        return -1;
    }
    
    SyncDatabase *db = load_db(dir->database);
    if(!db) {
        fprintf(stderr, "Cannot load database file: %s\n", dir->database);
        return -1;
    }
    remove_deleted_conflicts(dir, db);
    
    DavSession *sn = create_session(a, ctx, repo, dir->collection);
    cxMempoolRegister(sn->mp, db, (cx_destructor_func)destroy_db);
    if (cmd_getoption(a, "verbose")) {
        curl_easy_setopt(sn->handle, CURLOPT_VERBOSE, 1L);
        curl_easy_setopt(sn->handle, CURLOPT_STDERR, stderr);
    }
    if(SYNC_STORE_HASH(dir)) {
        sn->flags |= DAV_SESSION_STORE_HASH;
    }
    
    DavBool restore_removed = cmd_getoption(a, "restore-removed") ? 1 : 0;
    DavBool restore_modified = cmd_getoption(a, "restore-modified") ? 1 : 0;
    DavBool restore = restore_removed || restore_modified;
    int depth = restore ? -1 : 0;
    
    DavResource *root = dav_query(sn, "select D:getetag,idav:status from / with depth = %d", depth);
    if(!root) {
        log_resource_error(sn, "/");
        dav_session_destroy(sn);
        log_error("Abort\n");
        return -1;
    }
    
    CxMap *svrres = NULL;
    if(restore) {
        svrres = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 1024);
        res2map(root, svrres);
    }
    
    int cdt = cmd_getoption(a, "conflict") ? 0 : 1; // conflict detection
    
    // lock repository
    DavBool locked = FALSE;
    char *locktokenfile = NULL;
    if((dir->lockpush || cmd_getoption(a, "lock")) && !cmd_getoption(a, "nolock") && !outgoing) {
        if(dav_lock_t(root, dir->lock_timeout)) {
            log_resource_error(sn, "/");
            dav_session_destroy(sn);
            log_error("Abort\n");
            return -1;
        }
        DavLock *lock = dav_get_lock(sn, "/");
        if(lock) {
            log_printf("Lock-Token: %s\n", lock->token);
        }
        locked = TRUE;
        locktokenfile = create_locktoken_file(dir->name, lock->token);
    }
    
    DavBool remove_file = cmd_getoption(a, "remove") ? 1 : 0;
    
    CxMap *db_hashes = NULL;
    if(SYNC_HASHING(dir)) {
        db_hashes = create_hash_index(db);
    }
    
    int sync_success = 0;
    int sync_delete = 0;
    int sync_conflict = 0;
    int sync_error = 0;
    
    CxList *ls_new = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_path_cmp, CX_STORE_POINTERS);
    CxList *ls_modified = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_path_cmp, CX_STORE_POINTERS);
    CxList *ls_conflict = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_path_cmp, CX_STORE_POINTERS);
    CxList *ls_update = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_path_cmp, CX_STORE_POINTERS);
    CxList *ls_delete = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_path_cmp, CX_STORE_POINTERS);
    CxList *ls_move = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_path_cmp, CX_STORE_POINTERS);
    CxList *ls_copy = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_path_cmp, CX_STORE_POINTERS);
    CxList *ls_mkcol = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)resource_path_cmp, CX_STORE_POINTERS);
    
      
    // upload all changed files
    //UcxList *resources = cmd_getoption(a, "read") ?
    //        read_changes(dir, db) : local_scan(dir, db);
    CxList *resources = local_scan(dir, db);
    CxMap *resources_map = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, cxListSize(resources)+16);
    
    CxIterator iter = cxListIterator(resources);
    cx_foreach(LocalResource *, local_res, iter) {
        // ignore all files, that are excluded by a static filter (sync.xml) 
        
        // static include/exclude filter
        if(res_matches_dir_filter(dir, local_res->path+1)) {
            continue;
        }
        // static tag filter
        if(dir->filter.tags) {
            CxIterator tag_iter = cxListIterator(dir->filter.tags);
            cx_foreach(SyncTagFilter *, tf, tag_iter) {
                if(!localres_matches_tags(dir, local_res, tf)) {
                    continue;
                }
            }
        }
        
       
        // we need a fast file lookup map later to detect deleted files
        cxMapPut(resources_map, cx_hash_key_str(local_res->path), local_res);
        
        // dynamic tag filter
        if(!localres_matches_tags(dir, local_res, tagfilter)) {
            if(!remove_file) {
                LocalResource *dbres = cxMapGet(
                        db->resources,
                        cx_hash_key_str(local_res->path));
                if(dbres) {
                    // this makes sure the file will not be deleted later
                    dbres->keep = TRUE;
                }
            }
            continue;
        }
        
        // skip conflict backups silently
        if(res_isconflict(db, local_res)) {
            cxListAdd(ls_conflict, local_res);
            continue;
        }
        
        int is_changed = local_resource_is_changed(
                    dir,
                    db,
                    local_res,
                    svrres,
                    restore_removed,
                    restore_modified);
        if(is_changed) {
            if(local_res->isdirectory) {
                cxListAdd(ls_mkcol, local_res);
            } else if(local_res->isnew) {
                cxListAdd(ls_new, local_res);
            } else {
                cxListAdd(ls_modified, local_res);
            }
        } else if(local_res->metadata_updated) {
            cxListAdd(ls_update, local_res);
        }
        
        if(local_res->isnew) {
            if(local_resource_load_metadata(dir, local_res)) {
                log_error(
                        "Failed to load metadata: %s\n",
                        local_resource_path(local_res));
            }
        }
    }
    
    if(SYNC_STORE_HASH(dir)) {
        // calculate hashes of all new files and check if a file
        // was moved or is a copy
        CxIterator mut_iter = cxListMutIterator(ls_new);
        cx_foreach(LocalResource *, local, mut_iter) {
            if(local->isdirectory || local->link_target) {
                continue;
            }
            
            char *local_path = util_concat_path(dir->path, local_resource_path(local));
            char *hash = util_file_hash(local_path);
            local->hash = hash;
            // check if a file with this hash already exists
            LocalResource *origin = cxMapGet(db_hashes, cx_hash_key_str(hash));
            if(origin) {
                local->origin = local_resource_copy(origin, origin->path);
                // the file is a copied/moved file
                // check if the file is in the resources_map, because then
                // it still exists
                if(cxMapGet(resources_map, cx_hash_key_str(origin->path))) {
                    cxListAdd(ls_copy, local);
                } else {
                    cxListAdd(ls_move, local);
                    // put file in resources_map to prevent deletion
                    cxMapPut(resources_map, cx_hash_key_str(origin->path), local);
                }
                // remove list elemend from ls_new
                cxIteratorFlagRemoval(mut_iter);
            }
            
            free(local_path);
        }
    }
    
    // find all deleted files and cleanup the database
    iter = cxMapIterator(db->resources);
    CxList *removed_res = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    cx_foreach(CxMapEntry *, entry, iter) { 
        LocalResource *local = entry->value;
        // all filtered files should be removed from the database
        if(res_matches_dir_filter(dir, local->path+1)) {
            cxMapRemove(db->resources, local->path);
            continue;
        }
        CxIterator tag_iter = cxListIterator(dir->filter.tags);
        cx_foreach(SyncTagFilter *, tf, tag_iter) {
            if(!localres_matches_tags(dir, local, tf)) {
                cxMapRemove(db->resources, local->path);
                continue;
            }
        }
        
        if(!cxMapGet(resources_map, *entry->key)) {
            // The current LocalResource is in the database but doesn't exist
            // in the filesystem anymore. This means the file was deleted
            // and should be deleted on the server
            if(!archive) {
                cxListAdd(ls_delete, local);
            } else {
                cxListInsert(removed_res, 0, local);
            }
        }
    }
    iter = cxListIterator(removed_res);
    cx_foreach(LocalResource *, local, iter) {
        cxMapRemove(db->resources, local->path);
    }
    
    cxListSort(ls_new);
    cxListSort(ls_modified);
    cxListSort(ls_conflict);
    cxListSort(ls_update);
    cxListSort(ls_delete);
    cxListSort(ls_move);
    cxListSort(ls_copy);
    cxListSort(ls_mkcol);
    
    if(outgoing) {
        print_outgoing(
                a,
                ls_new,
                ls_modified,
                ls_conflict,
                ls_update,
                ls_delete,
                ls_move,
                ls_copy,
                ls_mkcol);
        return 0;
    }
    
    //
    // BEGIN PUSH
    //
    
    int ret = 0;
    int error = 0;
    
    // create collections
    iter = cxListIterator(ls_mkcol);
    cx_foreach(LocalResource *, local_res, iter) {
        if(sync_shutdown) {
            break;
        }
        
        DavResource *res = dav_resource_new(sn, local_res->path);
        if(!res) {
            log_resource_error(sn, local_res->path);
            ret = -1;
            sync_error++;
        }
        
        int abort = 0;
        
        dav_exists(res);
        if(sn->error == DAV_NOT_FOUND) {
            // create collection
            // TODO: show 405
            log_printf("mkcol: %s\n", local_res->path);
            if(sync_mkdir(dir, res, local_res) && sn->error != DAV_METHOD_NOT_ALLOWED) {
                log_resource_error(sn, res->path);
                ret = -1;
                sync_error++;
                error = 1;
                abort = 1;
            }
        } else if(sn->error != DAV_OK) {
            // dav_exists() failed
            log_resource_error(sn, local_res->path);
            ret = -1;
            sync_error++;
            error = 1;
            abort = 1;
        }
        
        if(!abort) {
            // success
            if(local_res->metadata_updated) {
                sync_update_metadata(dir, sn, res, local_res);
            }

            // remove old db entry (if it exists)
            // and add add new entry
            // TODO: free??
            LocalResource *dbres = cxMapGet(db->resources, cx_hash_key_str(local_res->path));
            cxMapPut(db->resources, cx_hash_key_str(local_res->path), local_res);
        }
        
        dav_resource_free(res);
    }
    
    // copy/move files
    DavBool copy = TRUE;
    if(!ls_copy) {
        copy = FALSE;
        ls_copy = ls_move;
    }
    iter = cxListIterator(ls_copy);
    for(int i=0;i<2;i++) {
        cx_foreach(LocalResource*, local, iter) {
            if(sync_shutdown) {
                break;
            }

            int err = 0;
            DavResource *res = dav_resource_new(sn, local->path);
            if(dav_exists(res)) {
                log_printf("conflict: %s\n", local->path);
                local->last_modified = 0;
                nullfree(local->etag);
                local->etag = NULL;
                nullfree(local->hash);
                local->hash = NULL;
                local->skipped = TRUE;
                sync_conflict++;
            } else {
                DavResource *origin_res = dav_resource_new(sn, local->origin->path);
                int origin_changed = remote_resource_is_changed(
                        sn,
                        dir,
                        db,
                        origin_res,
                        local->origin,
                        NULL);
                if(origin_changed) {
                    // upload with put
                    cxListInsert(ls_modified, 0, local);               
                } else {
                    log_printf("%s: %s -> %s\n", copy ? "copy":"move", local->origin->path, local->path);
                    err = sync_move_remote_resource(
                            dir,
                            db,
                            origin_res,
                            local,
                            copy,
                            &sync_success);
                }
            }

            if(err) {
                sync_error++;
                log_resource_error(sn, res->path);
                ret = -1;
                error = 1;
            } else {
                LocalResource *dbres = cxMapGet(db->resources, cx_hash_key_str(local->path));
                cxMapPut(db->resources, cx_hash_key_str(local->path), local);
            }
        }
        copy = FALSE;
        iter = cxListIterator(ls_move);
    }
    
    // upload changed files 
    //ls_modified = ucx_list_concat(ls_new, ls_modified);
    iter = cxListIterator(ls_new);
    for(int i=0;i<2;i++) {
        cx_foreach(LocalResource*, local_res, iter) {
            if(sync_shutdown) {
                break;
            }
            
            int err = 0;

            DavResource *res = dav_resource_new(sn, local_res->path);
            if(!res) {
                log_resource_error(sn, local_res->path);
                ret = -1;
                sync_error++;
            } else {
                DavBool equal = FALSE;
                DavBool res_conflict = FALSE;
                int changed = remote_resource_is_changed(sn, dir, db, res, local_res, &equal);
                if(equal) {
                    char *etag = dav_get_string_property(res, "D:getetag");
                    if(local_res->metadata_updated) {
                        cxListInsert(ls_update, 0, local_res);
                    } else if(etag) {
                        // update etag in db
                        if(local_res->etag) {
                            free(local_res->etag);
                        }
                        local_res->etag = strdup(etag);
                    }
                } else if(cdt && changed) {
                    log_printf("conflict: %s\n", local_res->path);
                    local_res->last_modified = 0;
                    nullfree(local_res->etag);
                    local_res->etag = NULL;
                    nullfree(local_res->hash);
                    local_res->hash = NULL;
                    local_res->skipped = TRUE;
                    sync_conflict++;

                    if(local_res->link_target) {
                        free(local_res->link_target);
                        local_res->link_target = local_res->link_target_db;
                        local_res->link_target_db = NULL;
                    }

                    res_conflict = TRUE;
                } else {
                    if(local_res->link_target) {
                        log_printf(
                                "link: %s -> %s\n",
                                local_res->path,
                                local_res->link_target);
                    } else {
                        log_printf("put: %s\n", local_res->path);
                    }
                    if(sync_put_resource(dir, res, local_res, &sync_success)) {
                        sync_error++;
                        log_resource_error(sn, res->path);
                        ret = -1;
                        error = 1;

                        err = 1;
                    }
                }

                if(!err) {
                    LocalResource *dbres = cxMapRemoveAndGet(db->resources, cx_hash_key_str(local_res->path));
                    // in case of a conflict, don't store the resource
                    // in the db, if it is new
                    if(!res_conflict || dbres) {
                        cxMapPut(db->resources, cx_hash_key_str(local_res->path), local_res);
                    }
                }
            }

            dav_resource_free(res);
        }
        iter = cxListIterator(ls_modified);
    }
    
    // metadata updates
    iter = cxListIterator(ls_update);
    cx_foreach(LocalResource *, local_res, iter) {
        if(sync_shutdown) {
            break;
        }
        
        DavResource *res = dav_resource_new(sn, local_res->path);       
        if(local_res->metadata_updated) {
            log_printf("update: %s\n", local_res->path);
            if(!sync_update_metadata(dir, sn, res, local_res)) {
                LocalResource *dbres = cxMapGet(db->resources, cx_hash_key_str(local_res->path));
                cxMapPut(db->resources, cx_hash_key_str(local_res->path), local_res);
            }
        }
    }  

    // delete all removed files
    cxListSort(ls_delete);
    
    CxList *cols = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)localres_cmp_path_desc, CX_STORE_POINTERS);
    CxList *cols_del = cols; // remember pointer for cleanup
    CxList *col_list = cols;
    CxList *deletelist = ls_delete;
    for(int i=0;i<2;i++) {
        // the first iteration deletes everything from ls_delete except
        // all collections, which are stored in cols
        // in the second run all collections will be deleted
        iter = cxListIterator(deletelist);
        cx_foreach(LocalResource *, local, iter) {
            if(sync_shutdown) {
                break;
            }
            if(local->keep) {
                continue;
            }
            if(sync_delete_remote_resource(dir, sn, local, &sync_delete, col_list)) {
                if(sn->error != DAV_NOT_FOUND) {
                    log_resource_error(sn, local->path);
                    sync_error++;
                    break;
                }
            } else {
                LocalResource *dbres = cxMapRemoveAndGet(db->resources, cx_hash_key_str(local->path));
                //local_resource_free(dbres);
            }
        }
        cxListSort(cols);
        deletelist = cols;
        col_list = NULL;
    }
    
    cxListDestroy(cols_del);
    
    // unlock repository
    if(locked) {
        if(dav_unlock(root)) {
            log_resource_error(sn, "/");
            ret = -1;
        } else {
            locked = FALSE;
        }
    }
    
    // store db
    if(store_db(db, dir->database, dir->db_settings)) {
        log_error("Cannot store sync db\n");
        ret = -2;
    }
    
    // cleanup
    if(!locked && locktokenfile) {
        remove(locktokenfile);
    }
    
    dav_session_destroy(sn);
    
    // Report
    if(ret != -2) {
        char *str_success = sync_success == 1 ? "file" : "files";
        char *str_delete = sync_delete == 1 ? "file" : "files";
        char *str_conflict = sync_conflict == 1 ? "conflict" : "conflicts";
        char *str_error = sync_error == 1 ? "error" : "errors";
        
        log_printf("Result: %d %s pushed, ", sync_success, str_success);
        if(!archive) {
            log_printf("%d %s deleted, ", sync_delete, str_delete);
        }
        log_printf("%d %s, %d %s\n",
                sync_conflict, str_conflict, sync_error, str_error);
    }
    
    return ret;
}

int cmd_restore(CmdArgs *a) {
    char *syncdir = cmd_getoption(a, "syncdir");
    
    if(!syncdir && a->argc == 0) {
        fprintf(stderr, "No syncdir or files specified\n");
        return -1;
    }
    
    char *version = cmd_getoption(a, "version");
    if(version) {
        if(a->argc != 1) {
            fprintf(stderr, "If the -V option is enabled, only one file can be specified\n");
            return -1;
        }
    }
    
    SyncDirectory *dir = NULL;
    CxMap *files = NULL;
    if(syncdir) {
        dir = scfg_get_dir(syncdir);
    }
    if(logfile_open(dir)) {
        return -1;
    }
    
    LocalResource nres;
    if(a->argc > 0) {
        files = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, a->argc+8);
        // get all specified files and check the syncdir
        SyncDirectory *sd = NULL;
        for(int i=0;i<a->argc;i++) {
            SyncFile f;
            int err = sync_get_file(a, a->argv[i], &f, FALSE);
            if(err) {
                sync_print_get_file_err(a->argv[i], err);
                return 1;
            }
            if(!sd) {
                sd = f.dir;
            } else {
                if(f.dir != sd) {
                    fprintf(stderr, "Not all files are in the same syncdir\n");
                    return 1;
                }
            }
            
            cxMapPut(files, cx_hash_key_str(f.path), &nres);
        }
        dir = sd;
    }
    
    if(!dir) {
        fprintf(stderr, "Unknown sync dir: %s\n", syncdir);
        return -1;
    }
    if(scfg_check_dir(dir)) {
        return -1;
    }
    
    if((dir->allow_cmd & SYNC_CMD_RESTORE) != SYNC_CMD_RESTORE) {
        fprintf(stderr, "Command ''restore'' is not allowed for this sync dir\n");
        print_allowed_cmds(dir);
        return -1;
    }
    
    DavBool restore_modified = cmd_getoption(a, "restore-modified") ? 1 : 0;
    DavBool restore_removed = cmd_getoption(a, "restore-removed") ? 1 : 0;
    if(!restore_modified && !restore_removed) {
        restore_modified = 1;
        restore_removed = 1;
    }
    
    SyncDatabase *db = load_db(dir->database);
    if(!db) {
        log_error("Cannot load database file: %s\n", dir->database);
        return -1;
    }
    remove_deleted_conflicts(dir, db);
    
    CxList *modified = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)localres_cmp_path, CX_STORE_POINTERS);
    CxList *deleted = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)localres_cmp_path, CX_STORE_POINTERS);
    
    // iterate over all db resources and check if any resource is
    // modified or deleted
    CxIterator resiter = cxMapIterator(files ? files : db->resources);
    cx_foreach(CxMapEntry *, entry, resiter) {
        LocalResource *resource = entry->value;
        if(resource == &nres) {
            resource = cxMapGet(db->resources, *entry->key);
            if(!resource) {
                continue;
            }
        }
        
        char *file_path = create_local_path(dir, resource->path);
        SYS_STAT s;
        if(sys_stat(file_path, &s)) {
            if(errno == ENOENT) {
                if(restore_removed) {
                    cxListAdd(deleted, resource);
                }
            } else {
                log_error("Cannot stat file: %s\n", file_path);
                log_error("%s\n", strerror(errno));
            }
        } else {
            if(files) {
                cxListAdd(modified, resource);
            } else if(!resource->isdirectory && !S_ISDIR(s.st_mode)) {
                if(resource->last_modified != s.st_mtime || resource->size != s.st_size) {
                    if(restore_modified) {
                        cxListAdd(modified, resource);
                    }
                }
            }
        }

        free(file_path);
    }  
    
    if(files) {
        cxMapDestroy(files);
    }
    
    int ret = 0;
    
    // create DavSession
    DavCfgRepository *repo = dav_config_get_repository(get_config(), cx_str(dir->repository));
    if(!repo) {
        log_error("Unkown repository %s\n", dir->name);
        return -1;
    }
    DavSession *sn = create_session(a, ctx, repo, dir->collection);
    cxMempoolRegister(sn->mp, db, (cx_destructor_func)destroy_db);
    if (cmd_getoption(a, "verbose")) {
        curl_easy_setopt(sn->handle, CURLOPT_VERBOSE, 1L);
        curl_easy_setopt(sn->handle, CURLOPT_STDERR, stderr);
    }
    
    // lock repository
    char *locktokenfile = NULL;
    DavBool locked = FALSE;
    DavResource *root = dav_resource_new(sn, "/");
    root->iscollection = TRUE;
    if((dir->lockpush || cmd_getoption(a, "lock")) && !cmd_getoption(a, "nolock")) {
        if(dav_lock_t(root, dir->lock_timeout)) {
            log_resource_error(sn, "/");
            dav_session_destroy(sn);
            log_error("Abort\n");
            return -1;
        }
        DavLock *lock = dav_get_lock(sn, "/");
        if(lock) {
            log_printf("Lock-Token: %s\n", lock->token);
        }
        locked = TRUE;
        locktokenfile = create_locktoken_file(dir->name, lock->token);
    }
    
    int sync_success = 0;
    int sync_error = 0;
    
    // TODO: old code sorted both modified and deleted, is this necessary?
    //UcxList *resources = ucx_list_concat(modified, deleted);
    //resources = ucx_list_sort(resources, (cmp_func)localres_cmp_path, NULL);
    cxListSort(deleted);
    
    CxIterator iter = cxListIterator(modified);
    for(int i=0;i<2;i++) {
        cx_foreach(LocalResource *, resource, iter) {
            DavResource *res = dav_get(sn, resource->path, "D:getetag,idav:status,idav:version-collection,idav:split,`idav:content-hash`,idavprops:tags,idavprops:finfo,idavprops:xattributes,idavprops:link");
            if(!res) {
                log_printf("skip: %s\n", resource->path);
                continue;
            }
            char *status = dav_get_string_property(res, "idav:status");
            if(status && !strcmp(status, "broken")) {
                log_error("Resource %s broken\n", res->path);
                continue;
            }

            DavResource *vres = NULL;
            DavBool update_local_entry = TRUE;
            if(version) {
                if(dir->versioning->type == VERSIONING_SIMPLE) {
                    vres = versioning_simple_find(res, version);
                } else if(dir->versioning->type == VERSIONING_DELTAV) {
                    vres = versioning_deltav_find(res, version);
                }
                if(!vres) {
                    log_error("Cannot find specified version for resource %s\n", res->path);
                    ret = 1;
                    break;
                }

                // By restoring an old version of a file, the local dir is not
                // in sync with the server anymore. Mark this file to change
                // the metadata later, to make sure, the file will be detected
                // as locally modified, on the next push/pull
                update_local_entry = FALSE;
            } else {
                vres = res;
            }

            // download the resource
            if(!sync_shutdown) {
                if(resource->isdirectory) {
                    char *local_path = create_local_path(dir, res->path);
                    if(sys_mkdir(local_path) && errno != EEXIST) {
                        log_error(
                                "Cannot create directory %s: %s",
                                local_path, strerror(errno));
                    }
                    free(local_path);
                } else {
                    if(sync_get_resource(a, dir, res->path, vres, db, update_local_entry, &sync_success)) {
                        log_error("sync_get_resource failed for resource: %s\n", res->path);
                        sync_error++;
                    } else if(!update_local_entry) {
                        LocalResource *lr = cxMapGet(db->resources, cx_hash_key_str(res->path));
                        if(lr) {
                            lr->last_modified = 0;
                            nullfree(lr->hash);
                            lr->hash = NULL;
                        } // else should not happen
                    }
                }
            }
        }
        iter = cxListIterator(deleted);
    }
    
    // unlock repository
    if(locked) {
        if(dav_unlock(root)) {
            log_resource_error(sn, "/");
            ret = -1;
        } else {
            locked = FALSE;
        }
    }
    
    // store db
    if(store_db(db, dir->database, dir->db_settings)) {
        log_error("Cannot store sync db\n");
        ret = -2;
    }
    
    // cleanup
    dav_session_destroy(sn);
    
    if(!locked && locktokenfile) {
        remove(locktokenfile);
    }
    
    // Report
    if(ret != -2) {
        char *str_success = sync_success == 1 ? "file" : "files";
        char *str_error = sync_error == 1 ? "error" : "errors";
        log_printf("Result: %d %s pulled, %d %s\n",
                sync_success, str_success,
                sync_error, str_error);
    }
    
    return ret;
}

static void print_outgoging_file(LocalResource *res) {
    char *lastmodified = util_date_str(res->last_modified);
    char *size = util_size_str(FALSE, res->size);
    log_printf(" %-49s  %12s  %10s\n", res->path+1, lastmodified, size);
    free(lastmodified);
    free(size);
}

void print_outgoing(
        CmdArgs *args,
        CxList *ls_new,
        CxList *ls_modified,
        CxList *ls_conflict,
        CxList *ls_update,
        CxList *ls_delete,
        CxList *ls_move,
        CxList *ls_copy,
        CxList *ls_mkcol)
{
    int64_t total_size = 0;
    
    size_t len_new = cxListSize(ls_new);
    size_t len_mod = cxListSize(ls_modified);
    size_t len_cnf = cxListSize(ls_conflict);
    size_t len_upd = cxListSize(ls_update);
    size_t len_del = cxListSize(ls_delete);
    size_t len_mov = cxListSize(ls_move);
    size_t len_cpy = cxListSize(ls_copy);
    size_t len_mkc = cxListSize(ls_mkcol);
    
    size_t total = len_new + len_mod + len_cnf + len_upd + len_del + len_mov + len_cpy + len_mkc;
    if(total == 0) {
        log_printf("no changes\n");
        return;
    }
    
    log_printf("%s\n", "File                                                Last Modified    Size");
    log_printf("%s\n", "==============================================================================");
       
    if(cxListSize(ls_mkcol) > 0) {
        log_printf("Directories:\n");
        CxIterator i = cxListIterator(ls_mkcol);
        cx_foreach(LocalResource *, res, i) {
            log_printf(" %-49s\n", res->path+1);
            total_size += res->size;
        }
    }
    if(cxListSize(ls_new) > 0) {
        log_printf("New:\n");
        CxIterator i = cxListIterator(ls_new);
        cx_foreach(LocalResource *, res, i) {
            print_outgoging_file(res);
            total_size += res->size;
        }
    }
    if(cxListSize(ls_modified) > 0) {
        log_printf("Modified:\n");
        CxIterator i = cxListIterator(ls_modified);
        cx_foreach(LocalResource *, res, i) {
            print_outgoging_file(res);
            total_size += res->size;
        }
    }
    if(cxListSize(ls_update) > 0) {
        log_printf("Update:\n");
        CxIterator i = cxListIterator(ls_update);
        cx_foreach(LocalResource *, res, i) {
            char *lastmodified = util_date_str(res->last_modified);
            log_printf(" %-49s  %12s\n", res->path+1, lastmodified);
            free(lastmodified);
        }
    }
    if(cxListSize(ls_delete) > 0) {
        log_printf("Delete:\n");
        CxIterator i = cxListIterator(ls_delete);
        cx_foreach(LocalResource *, res, i) {
            log_printf(" %s\n", res->path+1);
        }
    }
    if(cxListSize(ls_copy) > 0) {
        log_printf("Copy:\n");
        CxIterator i = cxListIterator(ls_copy);
        cx_foreach(LocalResource *, res, i) {
            log_printf("%s -> %s\n", res->origin->path+1, res->path);
        }
    }
    if(cxListSize(ls_move) > 0) {
        log_printf("Move:\n");
        CxIterator i = cxListIterator(ls_move);
        cx_foreach(LocalResource *, res, i) {
            log_printf("%s -> %s\n", res->origin->path+1, res->path);
        }
    }
    if(cxListSize(ls_conflict) > 0) {
        log_printf("Conflict\n");
        CxIterator i = cxListIterator(ls_conflict);
        cx_foreach(LocalResource *, res, i) {
            log_printf(" %s\n", res->path+1);
        }
    }
    
    char *total_size_str = util_size_str(FALSE, total_size);
    
    log_printf("\n");
    if(len_new > 0) log_printf("new: %zu, ", len_new);
    if(len_mod > 0) log_printf("modified: %zu, ", len_mod);
    if(len_upd > 0) log_printf("updated: %zu, ", len_upd);
    if(len_cpy > 0) log_printf("copied: %zu, ", len_cpy);
    if(len_mov > 0) log_printf("moved: %zu, ", len_mov);
    if(len_del > 0) log_printf("deleted: %zu, ", len_del);
    if(len_cnf > 0) log_printf("conflicts: %zu, ", len_cnf);
    log_printf("total size: %s\n", total_size_str);
    
    free(total_size_str);
}

CxList* local_scan(SyncDirectory *dir, SyncDatabase *db) {
    CxList *resources = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    
    char *path = strdup("/");
    CxList *stack = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    cxListInsert(stack, 0, path);
    while(cxListSize(stack) > 0) {
        // get a directory path from the stack and read all entries
        // if an entry is a directory, put it on the stack
        
        char *p = cxListAt(stack, 0);
        cxListRemove(stack, 0);
        char *local_path = create_local_path(dir, p);
        SYS_DIR local_dir = sys_opendir(local_path);
        
        if(!local_dir) {
            log_error("Cannot open directory %s: %s\n", local_path, strerror(errno));
        } else {
            SysDirEnt *ent;
            while((ent = sys_readdir(local_dir)) != NULL) {
                if(!strcmp(ent->name, ".") || !strcmp(ent->name, "..")) {
                    continue;
                }
                
                char *new_path = util_concat_path(p, ent->name);
                DavBool free_new_path = TRUE;
                LocalResource *res = local_resource_new(dir, db, new_path);
                if(res) {
                    if(res->isdirectory) {
                        cxListAdd(resources, res);
                        cxListInsert(stack, 0, new_path);
                        free_new_path = FALSE;
                    } else {
                        cxListAdd(resources, res);
                    }
                }
                if(free_new_path) {
                    free(new_path);
                }
            }
            sys_closedir(local_dir);
 
        }
        free(local_path);
        free(p);
    }
    
    return resources;
}


LocalResource* local_resource_new(SyncDirectory *dir, SyncDatabase *db, char *path) {
    char *file_path = create_local_path(dir, path);
    SYS_STAT s;
    if(sys_lstat(file_path, &s)) {
        log_error("Cannot stat file %s: %s\n", file_path, strerror(errno));
        free(file_path);
        return NULL;
    }

    LocalResource *res = calloc(1, sizeof(LocalResource));
    res->mode = s.st_mode & 07777;
    res->uid = s.st_uid;
    res->gid = s.st_gid;
    res->last_modified = s.st_mtime;
    res->path = strdup(path);
    if(!S_ISDIR(s.st_mode)) {
        //res->path = strdup(path);
        res->size = s.st_size;
    } else {
        //res->path = util_concat_path(path, "/");
        res->isdirectory = 1;
    }
    
    int skip_file = 0;
    if(SYS_ISLINK(file_path, s)) {
        char *lnkbuf = sys_readlink(file_path, &s);
#ifdef SYS_LINK_EXT
        // on Windows, we interpret .lnk files as links
        // we dont want resource names with this extension
        // and possibly sync this to other operating systems
        // therefore we remove the .lnk extension from the file name
        // change res->path
        // we only do this, if there isn't any other file with this name
        cxstring fpath = cx_str(file_path);
        cxstring rpath = cx_str(path);
        // remove last 4 chars (.lnk)
        cxmutstr new_file_path = cx_strdup(cx_strsubsl(fpath, 0 , fpath.length-4));
        // check if a file with this name exists
        SYS_STAT nfp_s;
        if(!sys_stat(new_file_path.ptr, &nfp_s)) {
            // we can't upload the link without the file extension, because
            // there is another file with this name
            free(lnkbuf);
            lnkbuf = NULL;
        } else {
            cxmutstr new_path = cx_strdup(cx_strsubsl(rpath, 0, rpath.length-4));
            res->local_path = res->path;
            res->path = new_path.ptr; // remove .lnk  ext from resource path
        }
        free(new_file_path.ptr);
#endif
        
        if(lnkbuf) {
            // readlink successful           
            char *normalized = NULL;
            if(!util_path_isabsolut(lnkbuf)) {
                char *link_parent = util_parent_path(res->path);
                char *abs_link_parent = util_concat_path(dir->path, link_parent);
                char *link = util_concat_path(abs_link_parent, lnkbuf);
                normalized = util_path_normalize(link);
                free(abs_link_parent);
                free(link_parent);
                free(link);
            } else {
                normalized = util_path_normalize(lnkbuf);
            }
            
            char *dirpath = util_path_normalize(dir->path);
            int isintern = util_path_isrelated(dirpath, normalized);
            
            
            if(SYNC_SYMLINK(dir) && isintern) {
                // the link points to a file inside the syncdir
                char *rel = util_create_relative_path(normalized, file_path);
                res->link_target = rel;
            } else {
#ifndef SYS_LINK_EXT // if not windows
                SYS_STAT targetstat;
                if(!sys_stat(file_path, &targetstat)) {
                    res->isdirectory = S_ISDIR(targetstat.st_mode);
                    
                    int nofollowextern = (dir->symlink & SYNC_SYMLINK_IGNORE_EXTERN) == SYNC_SYMLINK_IGNORE_EXTERN;
                    int nofollowintern = (dir->symlink & SYNC_SYMLINK_IGNORE_INTERN) == SYNC_SYMLINK_IGNORE_INTERN;
                    if(isintern && nofollowintern) {
                        skip_file = TRUE;
                    } else if(!isintern && nofollowextern) {
                        skip_file = TRUE;
                    }
                } else {
                    // can't access target, therefore we skip this link
                    skip_file = TRUE;
                }
#endif
            }
            free(dirpath);
            free(normalized);
            free(lnkbuf);
        }
    }
    
    if(!res->isdirectory && dir->push_strategy == PUSH_STRATEGY_HASH) {
        res->hash = util_file_hash(file_path);
    }
    
    free(file_path);
    
    if(skip_file) {
        local_resource_free(res);
        res = NULL;
    }
    
    return res;
}

char* local_resource_path(LocalResource *res) {
    return res->local_path ? res->local_path : res->path;
}

int local_resource_is_changed(
        SyncDirectory *dir,
        SyncDatabase *db,
        LocalResource *res,
        CxMap *svrres,
        DavBool restore_removed,
        DavBool restore_modified)
{
    LocalResource *db_res = cxMapGet(db->resources, cx_hash_key_str(res->path));
    res->tags_updated = 0;
    if(db_res) { 
        // copy some metadata from db_res, that localscan does not deliver
        res->tags_updated = db_res->tags_updated;
        if(db_res->etag) {
            res->etag = strdup(db_res->etag);
        }
        if(db_res->tags_hash) {
            res->tags_hash = strdup(db_res->tags_hash);
        }
        if(db_res->remote_tags_hash) {
            res->remote_tags_hash = strdup(db_res->remote_tags_hash);
        }
        if(db_res->xattr_hash) {
            res->xattr_hash = strdup(db_res->xattr_hash);
        }
        if(db_res->hash) {
            res->prev_hash = strdup(db_res->hash);
        }
        if(db_res->link_target) {
            res->link_target_db = db_res->link_target;
        }
        res->versioncontrol = db_res->versioncontrol;
        
        // if the resource is splitted, copy the part info to the new
        // LocalResource obj, because we need it later
        if(db_res->parts) {
            local_resource_copy_parts(db_res, res);
        }
        
        // check if the file must be restored on the server
        if(svrres) {
            DavResource *remote = cxMapGet(svrres, cx_hash_key_str(res->path));
            if(restore_removed && !remote) {
                return 1;
            }
            if(!res->isdirectory && restore_modified && remote) {
                char *etag = dav_get_string_property(remote, "D:getetag");
                if(!etag || (db_res->etag && strcmp(etag, db_res->etag))) {
                    res->restore = TRUE;
                    return 1;
                }
            }
        }
        
        // check if metadata has changed
        // metadata are tags, mode, owner, xattr
        // set res->metadata_updated to 1 in case any metadata has changed
        
        // check if tags have changed
        if(db_res->tags_updated) {
            res->tags_updated = 1;
            res->metadata_updated = 1;
        } else if(dir->tagconfig && dir->tagconfig->detect_changes ) {
            CxBuffer *tags = sync_get_file_tag_data(dir, res);
            if(tags) {
                if(db_res->tags_hash) {
                    char *hash = dav_create_hash(tags->space, tags->size);
                    if(strcmp(hash, db_res->tags_hash)) {
                        res->tags_updated = 1;
                    }
                    free(hash);
                } else {
                    res->tags_updated = 1;
                }
            } else if(db_res->tags_hash) {
                res->tags_updated = 1; // tags removed
            }
            res->metadata_updated = res->tags_updated;
        }
        
        // check if mtime has changed
        if((dir->metadata & FINFO_MTIME) == FINFO_MTIME) {
            if(db_res->last_modified != res->last_modified) {
                res->finfo_updated = 1;
                res->metadata_updated = 1;
            }
        }
        // check if mode has changed
        if((dir->metadata & FINFO_MODE) == FINFO_MODE) {
            if(db_res->mode != res->mode) {
                res->finfo_updated = 1;
                res->metadata_updated = 1;
            }
        }
        // check if owner has changed
        if((dir->metadata & FINFO_OWNER) == FINFO_OWNER) {
            if(db_res->uid != res->uid || db_res->gid != res->gid) {
                res->finfo_updated = 1;
                res->metadata_updated = 1;
            }
        }
        
        // check if xattr have changed
        if((dir->metadata & FINFO_XATTR) == FINFO_XATTR) {
            char *path = create_local_path(dir, local_resource_path(db_res));
            XAttributes *xattr = file_get_attributes(path, (xattr_filter_func)xattr_filter, dir);
            // test if xattr are added, removed or changed
            if((db_res->xattr_hash && !xattr) ||
               (!db_res->xattr_hash && xattr) ||
                (xattr && db_res->xattr_hash && strcmp(xattr->hash, db_res->xattr_hash)))
            {
                res->metadata_updated = 1;
                res->xattr_updated = 1;
                res->xattr = xattr;
            } else if(xattr) {
                xattributes_free(xattr);
            }
        }
        
        // check if the content of the file was modified
        // in case of links, just check if the link target has changed
        // for normal files, check last modified and size
        // or compare content hashes
        if(nullstrcmp(db_res->link_target, res->link_target)) {
            res->link_updated = 1;
        } else {
            if(db_res->hash && res->hash) {
                // we already have hashes
                if(!strcmp(db_res->hash, res->hash)) {
                    return 0; // hashes equal -> file content unchanged
                }                
            } else if(
                db_res->last_modified == res->last_modified &&
                db_res->size == res->size &&
                db_res->isdirectory == res->isdirectory)
            {
                // mtime and size unchanged, content also likely unchanged
                return 0;
            } else if(SYNC_HASHING(dir)) {
                // res->hash missing (see above)
                char *local_path = util_concat_path(dir->path, local_resource_path(res));
                res->hash = util_file_hash(local_path);
                free(local_path);
                
                // check if the content has really changed
                if(res->hash && db_res->hash && !strcmp(res->hash, db_res->hash)) {
                    return 0;
                }
            }
        }
    } else {
        res->tags_updated = 1;
        res->finfo_updated = 1;
        res->xattr_updated = 1;
        res->metadata_updated = 1;
        res->isnew = 1;
    }
    return 1;
}

int remote_resource_is_changed(
        DavSession *sn,
        SyncDirectory *dir,
        SyncDatabase *db,
        DavResource *remote,
        LocalResource *res,
        DavBool *equal)
{
    if(equal) {
        *equal = FALSE;
    }
    
    DavPropName properties[] = {
        {"DAV:", "getetag"},
        {DAV_NS, "version-collection"},
        {DAV_NS, "content-hash"},
        {DAV_NS, "split" },
        {DAV_PROPS_NS, "tags"},
        {DAV_PROPS_NS, "link" }
    };
    int err = dav_load_prop(remote, properties, 6);
    
    if(res->restore) {
        return 0;
    }
    
    if(remote->iscollection && !res->parts) {
        return 1;
    }
    
    char *link = dav_get_string_property_ns(remote, DAV_PROPS_NS, "link");
    
    int ret = 0;
    if(err == 0) {
        char *etag = dav_get_string_property(remote, "D:getetag");
        char *hash = sync_get_content_hash(remote);
        
        if(res->link_target_db || link) {
            ret = nullstrcmp(res->link_target_db, link);
            if(ret && equal) {
                *equal = !nullstrcmp(res->link_target, link);
            }
            return ret;
        }
        
        if(hash && res->hash && equal) {
            // if requested, check if the local and remote res are equal
            if(!strcmp(hash, res->hash)) {
                *equal = TRUE;
                return 0;
            }
        }
        
        if(hash && res->prev_hash) {
            if(strcmp(hash, res->prev_hash)) {
                ret = 1;
            }
        } else if(!res->etag) {
            // the resource is on the server and the client has no etag
            ret = 1;
        } else if(etag) {
            cxstring e = cx_str(etag);
            if(cx_strprefix(e, CX_STR("W/"))) {
                e = cx_strsubs(e, 2);
            }
            if(strcmp(e.ptr, res->etag)) {
                ret = 1;
            }
        } else {
            // something weird is happening, the server must support etags
            fprintf(stderr, "Warning: resource %s has no etag\n", remote->href);
        }
    }
    return ret;
}

int local_resource_load_metadata(SyncDirectory *dir, LocalResource *res) {
    // currently only xattr needed
    if((dir->metadata & FINFO_XATTR) == FINFO_XATTR) {
        char *path = create_local_path(dir, local_resource_path(res));
        XAttributes *xattr = file_get_attributes(path, (xattr_filter_func)xattr_filter, dir);
        res->xattr = xattr;
        free(path);
    }
    
    return 0;
}

void local_resource_set_etag(LocalResource *local, const char *etag) {
    // free old etag
    if(local->etag) {
        free(local->etag);
    }
    
    if(!etag) {
        local->etag = NULL;
        return;
    }
    
    cxstring e = cx_str(etag);
    if(cx_strprefix(e, CX_STR("W/"))) {
        e = cx_strsubs(e, 2);
    }
    local->etag = cx_strdup(e).ptr;
}

char* resource_local_path(DavResource *res) {
    cxstring path = cx_str(res->path);
    if(path.length > 0 && path.ptr[path.length-1] == '/') {
        path.length--;
    }
#ifdef SYS_LINK_EXT
    // on Windows, add .lnk extension to links
    if(dav_get_property_ns(res, DAV_PROPS_NS, "link")) {
        return cx_asprintf("%.*s%s", (int)path.length, path.ptr, SYS_LINK_EXT).ptr;
    } else {
        // not a link
        return cx_strdup(path).ptr;
    }
#else
    return cx_strdup(path).ptr;
#endif
}

size_t resource_get_blocksize(SyncDirectory *dir, LocalResource *local, DavResource *res, off_t filesize) {
    size_t local_blocksize = 0;
    if(local->blocksize < 0) {
        // file splitting disabled
        return 0;
    } else if(local->blocksize > 0) {
        local_blocksize = (size_t)local->blocksize;
    } else if(dir->splitconfig) {
        CxIterator i = cxListIterator(dir->splitconfig);
        cx_foreach(SplitConfig *, sc, i) {
            if(sc->filter) {
                if(res_matches_filter(sc->filter, local->path)) {
                    continue;
                }
            }
            
            if(sc->minsize > 0) {
                if(filesize < sc->minsize) {
                    continue;
                }
            }
            
            local_blocksize = sc->blocksize;
            break;
        }
    }
    
    size_t svr_blocksize = 0;
    char *svr_blocksize_str = dav_get_string_property_ns(res, DAV_NS, "split");
    if(svr_blocksize_str) {
        uint64_t i = 0;
        if(util_strtouint(svr_blocksize_str, &i)) {
            svr_blocksize = (size_t)i;
        }
    }
    
    if(local_blocksize > 0 && svr_blocksize > 0 && local_blocksize != svr_blocksize) {
        fprintf(stderr, "Warning: Blocksize mismatch: %s: local: %zu server: %zu\n", local->path, local_blocksize, svr_blocksize);
        return svr_blocksize;
    } else if(local_blocksize > 0) {
        return local_blocksize;
    } else if(svr_blocksize > 0) {
        return svr_blocksize;
    }
    
    return 0;
    
}

int resource_pathlen_cmp(LocalResource *res1, LocalResource *res2, void *n) {
    size_t s1 = strlen(res1->path);
    size_t s2 = strlen(res2->path);
    if(s1 < s2) {
        return 1;
    } else if(s1 > s2) {
        return -1;
    } else {
        return 0;
    }
}

int resource_path_cmp(LocalResource *res1, LocalResource *res2, void *n) {
    return strcmp(res1->path, res2->path);
}


DavResource *versioning_simple_find(DavResource *res, const char *version) {
    char *vcol_href = dav_get_string_property_ns(res, DAV_NS, VERSION_PATH_PROPERTY);
    if(!vcol_href) {
        return NULL;
    }
    DavResource *vcol = dav_resource_new_href(res->session, vcol_href);
    if(!vcol) {
        return NULL;
    }
    
    
    if(dav_load_prop(vcol, defprops, numdefprops)) {
        log_resource_error(res->session, vcol->path);
        dav_resource_free(vcol);
        return NULL;
    }
    
    DavResource *ret = NULL;
    DavResource *child = vcol->children;
    while(child) {
        DavResource *next = child->next;
        if(!strcmp(child->name, version)) {
            ret = child;
        } else {
            dav_resource_free(child);
        }
        child = next;
    }
    dav_resource_free(vcol);
    
    return ret;
}

// TODO: remove code dup (main.c: find_version)
DavResource* versioning_deltav_find(DavResource *res, const char *version) {
    DavResource *list = dav_versiontree(res, "D:getetag,idav:status,idav:split,idavprops:link,idavprops:finfo,idavprops:xattributes,idavprops:tags");
    DavResource *ret = NULL;
    while(list) {
        DavResource *next = list->next;
        if(!ret) {
            char *vname = dav_get_string_property(list, "D:version-name");
            if(vname && !strcmp(vname, version)) {
                ret = list;
            }
        }
        if(list != ret) {
            dav_resource_free(list);
        }
        list = next;
    }
    return ret;
}

int sync_set_status(DavResource *res, char *status) {
    DavResource *resource = dav_resource_new(res->session, res->path);
    dav_set_string_property(resource, "idav:status", status);
    int ret = dav_store(resource);
    dav_resource_free(resource);
    return ret;
}

int sync_remove_status(DavResource *res) {
    DavResource *resource = dav_resource_new(res->session, res->path);
    dav_remove_property(resource, "idav:status");
    int ret = dav_store(resource);
    dav_resource_free(resource);
    return ret;
}

int sync_store_metadata(SyncDirectory *dir, const char *path, LocalResource *local, DavResource *res) {
    int ret = 0;
    
    DavXmlNode *fileinfo = dav_get_property_ns(res, DAV_PROPS_NS, "finfo");
    if(fileinfo) {
        FileInfo f;
        finfo_get_values(fileinfo, &f);
        if((dir->metadata & FINFO_MTIME) == FINFO_MTIME && f.date_set) {
            // TODO: implement on windows
#ifndef _WIN32
            // set mtime
            struct utimbuf t;
            t.actime = f.last_modified;
            t.modtime = f.last_modified;
            if(utime(path, &t)) {
                log_error("utime failed for file: %s : %s\n", path, strerror(errno));
                ret = 1;
            } else {
                local->last_modified = f.last_modified;
            }
#else
            local->last_modified = 0;
#endif
        }
        if((dir->metadata & FINFO_MODE) == FINFO_MODE && f.mode_set) {
            // set mode
            if(chmod(path, f.mode)) {
                log_error("chmod failed for file: %s : %s\n", path, strerror(errno));
                ret = 1;
            } else {
                local->mode = f.mode;
            }
        }
    }
    
    if((dir->metadata & FINFO_XATTR) == FINFO_XATTR) {
        DavXmlNode *xattr_prop = dav_get_property_ns(res, DAV_PROPS_NS, "xattributes");
        XAttributes *xattr = NULL;
        if(xattr_prop) {
            xattr = xml_get_attributes(xattr_prop);
        }
        if(!sync_store_xattr(dir, path, xattr)) {
            if(local->xattr_hash) {
                free(local->xattr_hash);
            }
            local->xattr_hash = xattr ? xattr->hash : NULL;
        }
    }
    
    if(sync_store_tags(dir, path, local, res)) {
        ret = 1;
    }
    
    return ret;
}

int sync_store_xattr(SyncDirectory *dir, const char *path, XAttributes *xattr) {
    // create a map of all currently available local attributes
    ssize_t nelm = 0;
    char **list = xattr_list(path, &nelm);
    CxMap *current_xattr = NULL;
    if(nelm > 0) {
        current_xattr = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, nelm + 8);
        for(int i=0;i<nelm;i++) {
            // use the xattr name as key and store any value
            cxMapPut(current_xattr, cx_hash_key_str(list[i]), list[i]);
        }
    }
    if(list) {
        free(list);
    }
    
    // store extended attributes
    size_t nattr = xattr ? xattr->nattr : 0;
    for(int i=0;i<nattr;i++) {
        cxmutstr value = xattr->values[i];
        if(xattr_set(path, xattr->names[i], value.ptr, value.length)) {
            log_error(
                    "Cannot store xattr '%s' for file: %s\n",
                    xattr->names[i],
                    path);
        }
        
        if(current_xattr) {
            // to detect which xattributes are removed, we remove all new
            // attributes from the map and all remaining attributes must
            // be removed with xattr_remove
            char *removed_value = cxMapRemoveAndGet(current_xattr, cx_hash_key_str(xattr->names[i]));
            if(removed_value) {
                free(removed_value);
            }
        }
    }
    
    if(current_xattr) {
        CxIterator i = cxMapIteratorValues(current_xattr);
        cx_foreach(char *, value, i) {
            (void)xattr_remove(path, value); // don't print error
            free(value);
        }
        cxMapDestroy(current_xattr);
    }
    
    return 0;
}

int sync_store_tags(SyncDirectory *dir, const char *path, LocalResource *local, DavResource *res) {
    if(!dir->tagconfig) {
        return 0;
    }
    
    char *remote_hash = NULL;
    CxList *tags = NULL;
    if(dir->tagconfig) {
        DavXmlNode *tagsprop = dav_get_property_ns(res, DAV_PROPS_NS, "tags");
        if(tagsprop) {
            tags = parse_dav_xml_taglist(tagsprop);
            remote_hash = create_tags_hash(tags);
        }
    }
    
    DavBool store_tags = FALSE;
    DavBool tags_changed = FALSE;
    CxList *local_tags = sync_get_file_tags(dir, local, &tags_changed, NULL);
    if(tags_changed) {
        switch(dir->tagconfig->conflict) {
            case TAG_NO_CONFLICT: {
                store_tags = TRUE;
                break;
            }
            case TAG_KEEP_LOCAL: {
                store_tags = FALSE;
                break;
            }
            case TAG_KEEP_REMOTE: {
                store_tags = TRUE;
                local->tags_updated = FALSE;
                break;
            }
            case TAG_MERGE: {
                CxList *new_tags = merge_tags(local_tags, tags);
                // TODO: free tags and local_tags
                tags = new_tags;
                store_tags = TRUE;   
                // make sure the merged tags will be pushed the next time
                local->tags_updated = TRUE;
                break;
            }
        }
    } else {
        if(!compare_taglists(tags, local_tags)) {
            store_tags = TRUE;
        }
        // TODO: free local_tags
    }
    
    if(!store_tags) {
        nullfree(local->remote_tags_hash);
        local->remote_tags_hash = remote_hash;
        return 0;
    }
    
    int ret = sync_store_tags_local(dir, local, path, tags);
    if(!ret) {
        if(local->remote_tags_hash) {
            free(local->remote_tags_hash);
        }
        local->remote_tags_hash = remote_hash;
    }
    
    // TODO: free stuff
    
    return ret;
}

int sync_store_tags_local(SyncDirectory *dir, LocalResource *local, const char *path, CxList *tags) {
    int ret = 0;
    if(dir->tagconfig->store == TAG_STORE_XATTR) {
        CxBuffer *data = NULL;
        if(tags) {
            switch(dir->tagconfig->local_format) {
                default: break;
                case TAG_FORMAT_TEXT: {
                    data = create_text_taglist(tags);
                    break;
                }
                case TAG_FORMAT_CSV: {
                    data = create_csv_taglist(tags);
                    break;
                }
                case TAG_FORMAT_MACOS: {
                    data = create_macos_taglist(tags);
                    break;
                }
            }
            
            if(data) {
                char *data_hash = dav_create_hash(data->space, data->size);
                int update = 1;
                if(local) {
                    if(!local->tags_hash || strcmp(data_hash, local->tags_hash)) {
                        //printf("update: %s\n", local->path);
                    } else {
                        update = 0;
                    }
                }
                if(update) {
                    ret = xattr_set(path, dir->tagconfig->xattr_name, data->space, data->pos);
                    if(local) {
                        if(local->tags_hash) {
                            free(local->tags_hash);
                            local->tags_hash = NULL;
                        }
                        local->tags_hash = data_hash;
                    }
                } else {
                    free(data_hash);
                }
                cxBufferFree(data);
            } else {
                ret = -1;
            }
        } else {
            if(local) {
                //printf("update: %s\n", local->path);
            }
            // ignore errors on remove
            xattr_remove(path, dir->tagconfig->xattr_name);
        }
    }
    
    if(!ret && local) {
        local->tags_updated = 0;
    }
    
    return ret;
}

CxBuffer* sync_get_file_tag_data(SyncDirectory *dir, LocalResource *res) {
    if(!dir->tagconfig) {
        return NULL;
    }
    if(res->cached_tags) {
        return res->cached_tags;
    }
    CxBuffer *buf = NULL;
    if(dir->tagconfig->store == TAG_STORE_XATTR) {
        ssize_t tag_length = 0;
        char *local_path = create_local_path(dir, local_resource_path(res));
        char* tag_data = xattr_get(
                local_path,
                dir->tagconfig->xattr_name,
                &tag_length);
        free(local_path);
        
        if(tag_length > 0) {
            buf = cxBufferCreate(tag_data, (size_t)tag_length, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS);
            buf->size = (size_t)tag_length;
        }
    }
    res->cached_tags = buf;
    return buf;
}

CxList* sync_get_file_tags(SyncDirectory *dir, LocalResource *res, DavBool *changed, char **newhash) {
    if(changed) *changed = FALSE;
    
    CxList *tags = NULL;
    
    if(!res) {
        return NULL;
    }
    
    if(!dir->tagconfig) {
        return NULL;
    }
    if(changed && res->tags_updated) {
        *changed = TRUE;
    }
    if(dir->tagconfig->store == TAG_STORE_XATTR) {
        CxBuffer *tag_buf = res->cached_tags ?
                res->cached_tags :
                sync_get_file_tag_data(dir, res);
        
        if(tag_buf) {
            char *new_hash = dav_create_hash(tag_buf->space, tag_buf->size);
            if(res->tags_hash) {
                if(changed && strcmp(res->tags_hash, new_hash)) {
                    *changed = TRUE;
                }
                free(res->tags_hash);
                res->tags_hash = NULL;
            } else {
                if(changed) *changed = TRUE;
            }
            if(newhash) {
                *newhash = new_hash;
            } else {
                free(new_hash);
            }
            
            switch(dir->tagconfig->local_format) {
                default: break;
                case TAG_FORMAT_TEXT: {
                    tags = parse_text_taglist(tag_buf->space, tag_buf->size);
                    break;
                }
                case TAG_FORMAT_CSV: {
                    tags = parse_csv_taglist(tag_buf->space, tag_buf->size);
                    break;
                }
                case TAG_FORMAT_MACOS: {
                    tags = parse_macos_taglist(tag_buf->space, tag_buf->size);
                    break;
                }
            }
            res->cached_tags = tag_buf;
        } else if(res->tags_hash) {
            if(changed) *changed = TRUE;
        }
    }
    
    return tags;
}

static int file_seek(FILE *f, curl_off_t offset, int origin) {
    int ret = fseek(f, offset, origin);
    return ret == 0 ? CURL_SEEKFUNC_OK : CURL_SEEKFUNC_CANTSEEK;
}

size_t myread(void *ptr, size_t size, size_t nmemb, FILE *f) {
    size_t ret = fread(ptr, size, nmemb, f);
    return ret;
}

int gen_random_name(char *buf, size_t len) {
    unsigned char name_prefix[8];
    memset(name_prefix, 0, 8);
    dav_rand_bytes(name_prefix, 8);   
    char *pre = util_hexstr(name_prefix, 8);
    int64_t ts = (int64_t)time(NULL);
    int w = snprintf(buf, len, "%"PRId64"-%s", ts, pre);
    free(pre);
    return w >= len;
}

#define VBEGIN_ERROR_MKCOL     1
#define VBEGIN_ERROR_MOVE      2
#define VBEGIN_ERROR_PROPPATCH 3
#define VBEGIN_ERROR_CHECKOUT  4
int versioning_begin(SyncDirectory *dir, DavResource *res, int *exists, int *versionized) {
    int ret = 0;
    *versionized = 0;
    
    if(dir->versioning->type == VERSIONING_SIMPLE && res->exists) {
        DavResource *history_collection = dav_resource_new(
                    res->session,
                    dir->versioning->collection);
        
        // get the path to the version history collection for this resource
        // if propfind fails we just assume that it doesn't exist
        // better error handling is done later (sync_put_resource)
        // if there is no history collection for this resource, we create one
        
        char *history_href = NULL;
        char *vcol_path = dav_get_string_property_ns(res, DAV_NS, VERSION_PATH_PROPERTY);
        if(!vcol_path) {
            DavResource *history_res = NULL;
            
            // create a new collection for version history
            // the name is a combination of a random prefix and a timestamp
            while(!history_res) {
                char history_res_name[128];
                gen_random_name(history_res_name, 128);

                history_res = dav_resource_new_child(
                        res->session,
                        history_collection,
                        history_res_name);
                if(dav_exists(history_res)) {
                    dav_resource_free(history_res);
                    history_res = NULL;
                }
            }
            
            history_res->iscollection = TRUE;
            if(dav_create(history_res)) {
                dav_resource_free(history_res);
                dav_resource_free(history_collection);
                return VBEGIN_ERROR_MKCOL;
            }
            
            history_href = strdup(history_res->href);
            
            dav_resource_free(history_res);
        } else {
            history_href = vcol_path;
        }
        
        // find a free url and move 'res' to this location
        DavResource *version_res = NULL;
        while(!version_res) {
            char version_name[128];
            gen_random_name(version_name, 128);

            char *href = util_concat_path(history_href, version_name);
            version_res = dav_resource_new_href(res->session, href);
            free(href);

            char *dest = util_get_url(res->session, version_res->href);
            int err = dav_moveto(res, dest, FALSE);
            free(dest);

            if(err) {
                dav_resource_free(version_res);
                version_res = NULL;
                if(res->session->error != DAV_PRECONDITION_FAILED) {
                    ret = VBEGIN_ERROR_MOVE;
                    break;
                }
            } else {
                // tell the caller the resource does not exist anymore at
                // the original location
                *exists = 0;
            }
        }

        if(!ret) {
            *versionized = 1;
            
            dav_set_string_property_ns(version_res, DAV_NS, "origin", res->href);
            if(dav_store(version_res)) {
                ret = VBEGIN_ERROR_PROPPATCH;
            }
            dav_resource_free(version_res);
            
            // we can just set the property here and don't need dav_store
            // because sync_put_resource will call dav_store(res) later
            dav_set_string_property_ns(
                    res,
                    DAV_NS,
                    VERSION_PATH_PROPERTY,
                    history_href);
        }
        
        if(vcol_path != history_href) {
            free(history_href);
        }
        
        dav_resource_free(history_collection);
    } else if(dir->versioning->type == VERSIONING_DELTAV && res->exists){
        // DeltaV is so much easier :) 
        if(dav_checkout(res)) {
            ret = VBEGIN_ERROR_CHECKOUT;
        } else {
            *versionized = 1;
        }
    }
    
    return ret;
}

int versioning_init(SyncDirectory *dir, LocalResource *local, DavResource *res) {
    if(local->versioncontrol) {
        return 0;
    }
    int ret = 0;
    if(dir->versioning->type == VERSIONING_DELTAV) {
        if(dav_versioncontrol(res)) {
            ret = 1;
        } else {
            local->versioncontrol = 1;
        }
    }
    return ret;
}

int versioning_end(SyncDirectory *dir, DavResource *res) {
    if(dir->versioning->type == VERSIONING_DELTAV) {
        return dav_checkin(res);
    } else {
        return 0;
    }
}

int versioning_delete_begin(SyncDirectory *dir, DavResource *res, int *exists, int *versionized) {
    *versionized = 0;
    if(dir->versioning->type == VERSIONING_SIMPLE) {
        versioning_begin(dir, res, exists, versionized);
    } else {
        // versioning delete with DeltaV currently not supported in dav-sync
        *exists = 1;
    }
    return 0;
}

int versioning_delete_end(SyncDirectory *dir, DavResource *res) {
    return 0;
}

static void update_metadata_hashes(LocalResource *local, MetadataHashes hashes) {
    if(hashes.update_tags) {
        if(local->tags_hash) {
            free(local->tags_hash);
            local->tags_hash = NULL;
        }
        local->tags_hash = hashes.tags;
    }
    if(hashes.update_tags_remote) {
        if(local->remote_tags_hash) {
            free(local->remote_tags_hash);
        }
        local->remote_tags_hash = hashes.tags_remote;
    }
    if(hashes.update_xattr) {
        if(local->xattr_hash) {
            free(local->xattr_hash);
        }
        local->xattr_hash = hashes.xattr;
    }
}

// this macro is only a workaround for a netbeans bug    
#define LOG10 log10

static CxList* upload_parts(
        LocalResource *local,
        DavResource *res,
        FILE *in,
        uint64_t filesize,
        size_t blocksize,
        uint64_t *blockcount,
        int *err)
{ 
    // Make sure the resource is a collection. If it was a normal
    // resource until now, delete it and recreate it as collection
    if(res->exists) {
        if(!res->iscollection) {
            if(dav_delete(res)) {
                log_resource_error(res->session, res->path);
                *err = 1;
                return NULL;
            }
            res->exists = 0;
            return upload_parts(local, res, in, filesize, blocksize, blockcount, err);
        }
    } else {
        res->iscollection = 1;
        if(dav_create(res)) {
            log_resource_error(res->session, res->path);
            *err = 1;
            return NULL;
        }
    }
    res->exists = 1;
    
    if(!res->href) {
        // this should never happen, but just make sure it doesn't crash
        log_error("href is NULL\n");
        *err = 1;
        return NULL;
    }
    
    char *buffer = malloc(blocksize);
    if(!buffer) {
        fprintf(stderr, "Out of memory\n");
        *err = 1;
        return NULL;
    }
    
    // calculate the maximal length of resource names
    // names should have all the same length and contain the block number
    int nblocks = filesize / blocksize;
    int digits = LOG10((double)nblocks) + 1;
    if(digits > 127) {
        log_error("Too many parts\n");
        *err = 1;
	free(buffer);
        return NULL;
    }
    
    CxMap *updated_parts_map = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, (nblocks/2)+64);
    cxDefineDestructor(updated_parts_map, filepart_free);
    
    int blockindex = 0;
    int uploaded_parts = 0;
    size_t r;
    
    // temporarly disable name encryption, because we don't need it for
    // part names
    uint32_t session_flags = res->session->flags;
    res->session->flags ^= DAV_SESSION_ENCRYPT_NAME;
    
    DAV_SHA_CTX *sha = dav_hash_init();
    
    while((r = fread(buffer, 1, blocksize, in)) > 0) {
        dav_hash_update(sha, buffer, r);
        
        int upload_block = 0;
        char *block_hash = dav_create_hash(buffer, r);
        if(blockindex >= local->numparts) {
            // we don't have a hash for this block, therefore it must be new
            upload_block = 1;
        } else {
            FilePart part = local->parts[blockindex];
            if(!strcmp(part.hash, block_hash)) {
                // no change
                free(block_hash);
                block_hash = NULL;
            } else {
                // block has changed
                upload_block = 1;
            }
        }
        
        if(upload_block) {
            char name[128];
            snprintf(name, 128, "%0*d", digits, blockindex);
            
            char *part_href = util_concat_path(res->href, name);
            DavResource *part = dav_resource_new_href(res->session, part_href);
            free(part_href);
            
            // upload part
            dav_set_content_data(part, buffer, r);
            if(dav_store(part)) {
                *err = 1;
                log_resource_error(res->session, part->path);
            } else {
                // successfully uploaded part
                
                // store the FilePart in a map
                // later we do a propfind and add the etag
                FilePart *f = calloc(1, sizeof(FilePart));
                f->block = blockindex;
                f->hash = block_hash;
                cxMapPut(updated_parts_map, cx_hash_key_str(name), f);
            }
            dav_resource_free(part);
            uploaded_parts++;
        }
        if(*err) {
            break;
        }
        blockindex++;        
    }
    *blockcount = blockindex;
    
    // restore flags
    res->session->flags = session_flags;
    
    free(buffer);
    if(*err) {
        cxMapDestroy(updated_parts_map);
        return NULL;
    }
    
    // set content-hash
    unsigned char content_hash[DAV_SHA256_DIGEST_LENGTH];
    dav_hash_final(sha, content_hash);
    sync_set_content_hash(res, content_hash);
    local->hash = util_hexstr(content_hash, DAV_SHA256_DIGEST_LENGTH);
    
    // get etags from uploaded resources
    // also delete everything, that is not part of the file
    CxList *updated_parts = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    DavResource *parts = dav_query(res->session, "select D:getetag from %s order by name", res->path);
    if(!parts) {
        log_resource_error(res->session, parts->path);
        *err = 1;
        cxMapDestroy(updated_parts_map);
        return NULL;
    }
    DavResource *part = parts->children;
    while(part) {
        FilePart *fp = cxMapRemoveAndGet(updated_parts_map, cx_hash_key_str(part->name));
        // every part we uploaded is in the map
        // if we get parts that are not in the map, someone else uploaded it
        if(fp) {
            char *etag = dav_get_string_property(part, "D:getetag");
            if(etag) {
                if(strlen(etag) > 2 && etag[0] == 'W' && etag[1] == '/') {
                    etag = etag + 2;
                }

                fp->etag = strdup(etag);
                cxListAdd(updated_parts, fp);
            } // else { wtf is wrong with this resource }
        } else {
            uint64_t name_partnum = 0;
            char *res_name = part->name;
            while(res_name[0] == '0' && res_name[1] != '\0') {
                res_name++;
            }
            DavBool delete_part = 0;
            if(strlen(part->name) != digits) {
                delete_part = 1;
            } else if(util_strtouint(res_name, &name_partnum)) {
                if(name_partnum >= blockindex) {
                    delete_part = 1;
                }
            }
            
            if(delete_part) {
                if(dav_delete(part)) {
                    log_resource_error(part->session, part->path);
                }
            }
        }
        part = part->next;
    }
    dav_resource_free_all(parts);
        
    cxMapDestroy(updated_parts_map);
    
    *err = 0;
    return updated_parts;
}

void update_parts(LocalResource *local, CxList *updates, uint64_t numparts) {
    size_t old_num = local->numparts;
    if(old_num > numparts) {
        // free old parts
        for(size_t i=numparts;i<old_num;i++) {
            FilePart p = local->parts[i];
            if(p.etag) {
                free(p.etag);
            }
            if(p.hash) {
                free(p.hash);
            }
        }
    }
    if(numparts != local->numparts) {
        local->parts = realloc(local->parts, numparts * sizeof(FilePart));
        local->numparts = numparts;
    }
    
    if(!updates) {
        return;
    }
    
    CxIterator i = cxListIterator(updates);
    cx_foreach(FilePart *, p, i) {
        if(p->block >= numparts) {
            // just make sure things don't explode in case some weird stuff
            // is going on
            continue;
        }
        
        FilePart *old = &local->parts[p->block];
        if(p->block < old_num) {
            // cleanup existing part
            if(old->hash) {
                free(old->hash);
                old->hash = NULL;
            }
            if(old->etag) {
                free(old->etag);
                old->etag = NULL;
            }
        }
        old->block = p->block;
        old->hash = p->hash;
        old->etag = p->etag;
        free(p);
    }
}

int sync_put_resource(
        SyncDirectory *dir,
        DavResource *res,
        LocalResource *local,
        int *counter)
{
    char *local_path = create_local_path(dir, local_resource_path(local));
    
    SYS_STAT s;
    if(sys_stat(local_path, &s)) {
        log_error("Cannot stat file: %s: %s\n", local_path, strerror(errno));
        free(local_path);
        return -1;
    }
    
    DavBool islink = local->link_target ? 1 : 0;
    if(!local->link_target && local->link_updated) {
        dav_remove_property_ns(res, DAV_PROPS_NS, "link");
    }
    
    size_t split_blocksize = resource_get_blocksize(dir, local, res, s.st_size);
    
    FILE *in = sys_fopen(local_path, "rb");
    if(!in) {
        log_error("Cannot open file %s: %s\n", local_path, strerror(errno));
        free(local_path);
        return -1;
    }
    
    DavBool issplit = split_blocksize == 0 ? FALSE : TRUE;
    int split_err = 0;
    CxList *parts = NULL;
    uint64_t blockcount = 0;
    
    if(islink) {
        dav_set_string_property_ns(res, DAV_PROPS_NS, "link", local->link_target);
    } else if(issplit) {
        // set split property
        char blocksize_str[32];
        snprintf(blocksize_str, 32, "%zu", split_blocksize);
        dav_set_string_property_ns(res, DAV_NS, "split", blocksize_str);
        
        // splitted/partial upload
        parts = upload_parts(
                local,
                res,
                in,
                s.st_size,
                split_blocksize,
                &blockcount,
                &split_err);
    } else {
        // regular file upload 
        dav_set_content(res, in, (dav_read_func)myread, (dav_seek_func)file_seek);
        dav_set_content_length(res, s.st_size);
    }
    if(split_err) {
        free(local_path);
        return -1;
    }
    
    MetadataHashes hashes;
    hashes = sync_set_metadata_properties(dir, res->session, res, local, FALSE);
    
    // before sync_put_resource, remote_resource_is_changed does a propfind
    // and sets res->exists
    int exists = res->exists;
    int vend_required = 0;
    if(dir->versioning && dir->versioning->always && !issplit) {
        // in case the file exists, we need to put the file under
        // versioncontrol (DeltaV only, does nothing with simple versioning)
        if(exists && versioning_init(dir, local, res)) {
            // init failed
            log_error("Cannot activate versioncontrol for resource: %s\n", res->href);
            free(local_path);
            return -1;
        } else {
            int err = versioning_begin(dir, res, &exists, &vend_required);
            if(err) {
                log_error("Cannot store version for resource: %s\n", res->href);
                free(local_path);
                return -1;
            }
        }
    }
    
    int ret = -2;
    dir->max_retry = 2;
    for(int i=0;i<=dir->max_retry;i++) {
        if(!exists && dav_create(res)) {
            continue;
        }
        exists = 1;
        if(dav_store(res)) {
            continue;
        }
        ret = 0;
        break;
    }
    
    if(vend_required) {
        if(versioning_end(dir, res)) {
            log_error("Cannot checkin resource\n");
            ret = 1;
        }
    }

    if(ret == 0) {
        (*counter)++;
        
        local->tags_updated = 0;
        
        update_metadata_hashes(local, hashes);
        update_parts(local, parts, blockcount);
        
        // check contentlength and get new etag
        DavResource *up_res = dav_get(res->session, res->path, "D:getetag,idav:status");
        
        if(up_res) {
            // the new content length must be equal or greater than the file size
            if(up_res->contentlength < s.st_size && !issplit && !islink) {
                log_error("Incomplete Upload: %s\n", local_path);
                ret = -1;
                // try to set the resource status to 'broken'
                sync_set_status(res, "broken");
            } else {
                // everything seems fine, we can update the local resource
                char *etag = dav_get_string_property(up_res, "D:getetag");
                local_resource_set_etag(local, etag);
                
                if(!issplit && SYNC_STORE_HASH(dir)) {
                    if(local->hash) {
                        free(local->hash);
                    }
                    // TODO: calculate hash on upload
                    local->hash = util_file_hash(local_path);
                }
                
                if(dav_get_string_property(up_res, "idav:status")) {
                    sync_remove_status(up_res);
                }
                
                dav_resource_free(up_res);
            }
        }
    } else {
        ret = -1;
        sync_set_status(res, "broken");
    }
    
    fclose(in);
    free(local_path);
    
    return ret;
}

int sync_mkdir(SyncDirectory *dir, DavResource *res, LocalResource *local) {
    res->iscollection = 1;
    int ret = -1;
    for(int i=0;i<=dir->max_retry;i++) {
        if(dav_create(res)) {
            continue;
        }
        ret = 0;
        break;
    }
    return ret;
}

int sync_move_remote_resource(
        SyncDirectory *dir,
        SyncDatabase *db,
        DavResource *origin,
        LocalResource *local,
        DavBool copy,
        int *counter)
{
    char *local_path = create_local_path(dir, local->path);
    
    SYS_STAT s;
    if(sys_stat(local_path, &s)) {
        log_error("Cannot stat file: %s: %s\n", local_path, strerror(errno));
        free(local_path);
        return -1;
    }
    free(local_path);
    
    int result = 0;
    if(copy) {
        result = dav_copy_o(origin, local->path, FALSE);
    } else {
        result = dav_move_o(origin, local->path, FALSE);
    }
    
    if(result != 0) {
        return result;
    }
    
    LocalResource *local_origin = local->origin;
    if(!copy) {
        cxMapRemove(db->resources, cx_hash_key_str(local_origin->path));
    }
    
    // set resource metadata
    DavResource *up_res = dav_resource_new(origin->session, local->path);
    if(!up_res) {
        return 1;
    }
    
    sync_set_metadata_from_stat(local, &s);
    MetadataHashes hashes;
    hashes = sync_set_metadata_properties(dir, up_res->session, up_res, local, TRUE);
    if(dav_store(up_res)) {
        log_error("Error: cannot store resource metadata\n");
    }
    
    // get new etag
    DavPropName p;
    p.ns = "DAV:";
    p.name = "getetag";
    if(!dav_load_prop(up_res, &p, 1)) {
        (*counter)++;
        
        // everything seems fine, we can update the local resource
        char *etag = dav_get_string_property(up_res, "D:getetag");
        local_resource_set_etag(local, etag);
        
        local->last_modified = s.st_mtime;
    } else {
        result = 1;
    }
    
    dav_resource_free(up_res);   
    return result;
}

int sync_delete_remote_resource(
        SyncDirectory *dir,
        DavSession *sn,
        LocalResource *local_res,
        int *counter,
        CxList *cols)
{
    DavResource *res = dav_get(sn, local_res->path, "D:getetag,idav:split");
    if(!res) {
        return sn->error == DAV_NOT_FOUND ? 0 : 1;
    }
    
    int ret = 0;
    sn->error = DAV_OK;
    if(res->iscollection) {
        DavXmlNode *split = dav_get_property_ns(res, DAV_NS, "split");
        if(cols) {
            cxListAdd(cols, local_res);
        } else if(split || !res->children) {
            log_printf("delete: %s\n", res->path);
            if(dav_delete(res)) {
                ret = 1;
                log_error("Cannot delete collection %s\n", res->path);
            } else {
                (*counter)++;
            }
        }
    } else {
        char *etag = dav_get_string_property(res, "D:getetag");
        if(etag) {
            if(strlen(etag) > 2 && etag[0] == 'W' && etag[1] == '/') {
                etag = etag + 2;
            } 
        }

        if(!nullstrcmp(etag, local_res->etag)) {
            // local resource metadata == remote resource metadata
            // resource can be deleted
            log_printf("delete: %s\n", res->path);
            int exists = 1;
            int vend_required = 0;
            if(dir->versioning && dir->versioning->always) {
                if(versioning_delete_begin(dir, res, &exists, &vend_required)) {
                    log_error("Cannot save resource version before deletion\n");
                    ret = 1;
                }
            }
            
            if(!ret && dav_delete(res) && exists) {
                if(sn->error != DAV_NOT_FOUND) {
                    log_error("Cannot delete resource %s\n", res->path);
                    ret = 1;
                }
            } else {
                (*counter)++;
            }
            
            if(vend_required) {
                versioning_delete_end(dir, res);
            }
        }
        // else TODO: should we inform the user that the file was modified on
        // the server and delete was skipped?
    }
    
    // cleanup
    dav_resource_free(res);
    
    return ret;
}

MetadataHashes sync_set_metadata_properties(
        SyncDirectory *dir,
        DavSession *sn,
        DavResource *res,
        LocalResource *local,
        DavBool force)
{
    if(force) {
        local->tags_updated = 1;
        local->finfo_updated = 1;
        local->xattr_updated = 1;
    }
    
    MetadataHashes hashes = {NULL, NULL, NULL, 0, 0, 0};
    if(dir->tagconfig) {     
        // get local tags
        DavBool changed = 0;
        char *tags_hash = NULL;
        CxList *tags = sync_get_file_tags(dir, local, &changed, &tags_hash);
        char *new_remote_hash = nullstrdup(tags_hash);
        if(changed || local->tags_updated) { 
            DavBool store_tags = TRUE;
            
            // get remote tags
            DavPropName p;
            p.ns = DAV_PROPS_NS;
            p.name = "tags";
            if(dav_load_prop(res, &p, 1) && sn->error != DAV_NOT_FOUND) {
                log_resource_error(sn, res->path);
            }
            CxList *remote_tags = NULL;
            DavXmlNode *tagsprop = dav_get_property_ns(res, DAV_PROPS_NS, "tags");
            if(tagsprop) {
                remote_tags = parse_dav_xml_taglist(tagsprop);
            }
            char *remote_hash = create_tags_hash(remote_tags);
            
            if(nullstrcmp(remote_hash, local->remote_tags_hash)) {
                // the tags have changed on the server
                int conflict_resolution = force ? TAG_NO_CONFLICT : dir->tagconfig->conflict;
                switch(conflict_resolution) {
                    case TAG_NO_CONFLICT: break;
                    case TAG_KEEP_LOCAL: break;
                    case TAG_KEEP_REMOTE: {
                        store_tags = FALSE;
                        local->tags_updated = FALSE;
                        break;
                    }
                    case TAG_MERGE: {
                        CxList *new_tags = merge_tags(tags, remote_tags);
                        free_taglist(tags);
                        tags = new_tags;
                        
                        nullfree(tags_hash);
                        nullfree(new_remote_hash);
                        tags_hash = create_tags_hash(tags);
                        new_remote_hash = nullstrdup(tags_hash);
                        
                        break;
                    }
                }
            }
            nullfree(remote_hash);
            
            if(dir->tagconfig->local_format == TAG_FORMAT_CSV) {
                // csv tag lists don't have colors, so we have to add
                // the colors from the remote tag list
                add_tag_colors(tags, remote_tags);
            }
            
            if(store_tags) {
                if(tags) {
                    DavXmlNode *tagprop = create_xml_taglist(tags);
                    dav_set_property_ns(res, DAV_PROPS_NS, "tags", tagprop);
                } else {
                    dav_remove_property_ns(res, DAV_PROPS_NS, "tags");
                }
                
                hashes.tags = tags_hash;      
                hashes.update_tags = 1;
                hashes.tags_remote = new_remote_hash;
                hashes.update_tags_remote = 1;
            }
            
            free_taglist(remote_tags);
        } else {
            if(tags_hash) {
                free(tags_hash);
            }
        }
        free_taglist(tags);
    }
    
    if(local->finfo_updated) {
        struct stat s;
        s.st_mode = local->mode;
        s.st_mtime = local->last_modified;
        s.st_uid = local->uid;
        s.st_gid = local->gid;
        resource_set_finfo_s(&s, res, dir->metadata);
    }
    
    if(local->xattr_updated) {
        if(local->xattr) {
            resource_set_xattr(res, local->xattr);
            hashes.xattr = local->xattr ? strdup(local->xattr->hash) : NULL;
            hashes.update_xattr = 1;
        } else {
            dav_remove_property(res, "idavprops:xattributes");
            if(local->xattr_hash) {
                free(local->xattr_hash);
                local->xattr_hash = NULL;
            }
        }
    }
    
    local->tags_updated = 0;
    
    return hashes;
}

int sync_update_metadata(
        SyncDirectory *dir,
        DavSession *sn,
        DavResource *res,
        LocalResource *local)
{
    MetadataHashes hashes = sync_set_metadata_properties(dir, sn, res, local, FALSE);
    
    int err = 0;
    if(dav_store(res)) {
        log_resource_error(sn, local->path);
        err = 1;
    } else {
        update_metadata_hashes(local, hashes);
        local->tags_updated = 0;
    }
    
    return err;
}

void remove_deleted_conflicts(SyncDirectory *dir, SyncDatabase *db) {
    char **dc = calloc(sizeof(void*), cxMapSize(db->conflict));
    int numdc = 0;
    
    CxIterator i = cxMapIteratorValues(db->conflict);
    cx_foreach(LocalResource *, res, i) {
        char *path = create_local_path(dir, res->path);
        SYS_STAT s;
        if(sys_stat(path, &s)) {
            if(errno == ENOENT) {
                dc[numdc] = res->path;
                numdc++;
            } else {
                log_error("Cannot stat file: %s: %s\n", path, strerror(errno));
            }
        }
        free(path);
    }
    
    for(int i=0;i<numdc;i++) {
        cxMapRemove(db->conflict, cx_hash_key_str(dc[i]));
    }
    
    free(dc);
}

static void resolve_skipped(SyncDatabase *db) {
    CxIterator i = cxMapIteratorValues(db->resources);
    int skipped = 0;
    cx_foreach(LocalResource *, res, i) {
        if(res->skipped) {
            skipped++;
            log_error("skipped from push: %s\n", res->path);
        }
    }
    if(skipped > 0) {
        log_error(
                "  To resolve conflict resources skipped by push run dav-sync pull first\n"
                "  before resolve-conflicts or delete-conflicts.\n\n");
    }
}

int cmd_resolve_conflicts(CmdArgs *a) {
    if(a->argc != 1) {
        log_error("Too %s arguments\n", a->argc < 1 ? "few" : "many");
        return -1;
    }
    
    SyncDirectory *dir = scfg_get_dir(a->argv[0]);
    if(!dir) {
        log_error("Unknown sync dir: %s\n", a->argv[0]);
        return -1;
    }
    if(scfg_check_dir(dir)) {
        return -1;
    }
    if(logfile_open(dir)) {
        return -1;
    }
    
    SyncDatabase *db = load_db(dir->database);
    if(!db) {
        log_error("Cannot load database file: %s\n", dir->database);
        return -1;
    }
    
    resolve_skipped(db);
    
    int ret = 0;
    
    // remove conflicts
    size_t num_conflict = cxMapSize(db->conflict);
    //ucx_map_free_content(db->conflict, (ucx_destructor)local_resource_free);
    cxMapClear(db->conflict);
    
    // store db
    if(store_db(db, dir->database, dir->db_settings)) {
        log_error("Cannot store sync db\n");
        log_error("Abort\n");
        ret = -2;
    }
    
    // cleanup
    destroy_db(db);
    
    // Report
    if(ret != -2) {
        char *str_conflict = num_conflict == 1 ? "conflict" : "conflicts";
        log_printf("Result: %zu %s resolved\n", num_conflict, str_conflict);
    }
    
    return ret;
}

int cmd_delete_conflicts(CmdArgs *a) {
    if(a->argc != 1) {
        fprintf(stderr, "Too %s arguments\n", a->argc < 1 ? "few" : "many");
        return -1;
    }
    
    SyncDirectory *dir = scfg_get_dir(a->argv[0]);
    if(!dir) {
        fprintf(stderr, "Unknown sync dir: %s\n", a->argv[0]);
        return -1;
    }
    if(scfg_check_dir(dir)) {
        return -1;
    }
    if(logfile_open(dir)) {
        return -1;
    }
    
    SyncDatabase *db = load_db(dir->database);
    if(!db) {
        log_error("Cannot load database file: %s\n", dir->database);
        return -1;
    }
    
    resolve_skipped(db);
    
    int num_del = 0;
    int num_err = 0;
    
    int ret = 0;
    
    // delete all conflict files
    CxIterator i = cxMapIteratorValues(db->conflict);
    cx_foreach(LocalResource*, res, i) {
        log_printf("delete: %s\n", res->path);
        char *path = create_local_path(dir, res->path);
        if(sys_unlink(path)) {
            if(errno != ENOENT) {
                log_error("unlink: %s", strerror(errno));
                num_err++;
            }
        } else {
            num_del++;
        }
        free(path);
    }
    //ucx_map_free_content(db->conflict, (ucx_destructor)local_resource_free);
    cxMapClear(db->conflict);
    
    // store db
    if(store_db(db, dir->database, dir->db_settings)) {
        log_error("Cannot store sync db\n");
        log_error("Abort\n");
        ret = -1;
    }
    
    // cleanup
    destroy_db(db);
    
    // Report
    if(ret == 0) {
        char *str_delete = num_del == 1 ? "file" : "files";
        char *str_error = num_err == 1 ? "error" : "errors";
        log_printf("Result: %d conflict %s deleted, %d %s\n",
                num_del, str_delete,
                num_err, str_error);
    }
    
    return ret;
}

int cmd_list_conflicts(CmdArgs *a) {
    if(a->argc != 1) {
        fprintf(stderr, "Too %s arguments\n", a->argc < 1 ? "few" : "many");
        return -1;
    }
    
    SyncDirectory *dir = scfg_get_dir(a->argv[0]);
    if(!dir) {
        fprintf(stderr, "Unknown sync dir: %s\n", a->argv[0]);
        return -1;
    }
    if(scfg_check_dir(dir)) {
        return -1;
    }
    
    SyncDatabase *db = load_db(dir->database);
    if(!db) {
        fprintf(stderr, "Cannot load database file: %s\n", dir->database);
        return -1;
    }
    
    remove_deleted_conflicts(dir, db);
    
    // get all conflict sources
    CxIterator i = cxMapIteratorValues(db->conflict);
    CxList* conflict_sources = cxLinkedListCreate(
            cxDefaultAllocator,
            (cx_compare_func) strcmp,
            CX_STORE_POINTERS
    );
    cx_foreach(LocalResource *, res, i) {
        cxListAdd(conflict_sources, res->conflict_source);
    }
    
    // print unique conflict sources
    cxListSort(conflict_sources);
    i = cxListIterator(conflict_sources);
    char *prev = "";
    cx_foreach(char *, path, i) {
        // TODO: implement verbose print   if(cmd_getoption(a, "verbose"))
        //       log_printf("%s (%d)\n", path, confl_count);
        if(!strcmp(path, prev)) {
            continue;
        }
        
        log_printf("%s\n", path);
        
        prev = path;
    }
    
    // cleanup
    destroy_db(db);    
    
    return 0;
}


// TODO: remove code dup (main.c ls_size_str)
static char* size_str(uint64_t size) {
    char *str = malloc(16);
    
    if(size < 0x400) {
        snprintf(str, 16, "%" PRIu64 " bytes", size);
    } else if(size < 0x100000) {
        float s = (float)size/0x400;
        int diff = (s*100 - (int)s*100);
        if(diff > 90) {
            diff = 0;
            s += 0.10f;
        }
        if(size < 0x2800 && diff != 0) {
            // size < 10 KiB
            snprintf(str, 16, "%.1f KiB", s);
        } else {
            snprintf(str, 16, "%.0f KiB", s);
        }
    } else if(size < 0x40000000) {
        float s = (float)size/0x100000;
        int diff = (s*100 - (int)s*100);
        if(diff > 90) {
            diff = 0;
            s += 0.10f;
        }
        if(size < 0xa00000 && diff != 0) {
            // size < 10 MiB
            snprintf(str, 16, "%.1f MiB", s);
        } else {
            size /= 0x100000;
            snprintf(str, 16, "%.0f MiB", s);
        }
    } else if(size < 0x1000000000ULL) {
        float s = (float)size/0x40000000;
        int diff = (s*100 - (int)s*100);
        if(diff > 90) {
            diff = 0;
            s += 0.10f;
        }
        if(size < 0x280000000 && diff != 0) {
            // size < 10 GiB
            snprintf(str, 16, "%.1f GiB", s);
        } else {
            size /= 0x40000000;
            snprintf(str, 16, "%.0f GiB", s);
        }
    } else {
        size /= 1024;
        float s = (float)size/0x40000000;
        int diff = (s*100 - (int)s*100);
        if(diff > 90) {
            diff = 0;
            s += 0.10f;
        }
        if(size < 0x280000000 && diff != 0) {
            // size < 10 TiB
            snprintf(str, 16, "%.1f TiB", s);
        } else {
            size /= 0x40000000;
            snprintf(str, 16, "%.0f TiB", s);
        }
    }
    return str;
}

void print_resource_version(DavResource *res, char *name) {
    time_t now = res->lastmodified;
    struct tm *date = gmtime(&now);
    char str[32];
    putenv("LC_TIME=C");
    size_t len = strftime(str, 32, "%a, %d %b %Y %H:%M:%S GMT", date);

    log_printf("name: %s\n", name);
    log_printf("lastmodified: %s\n", str);
    char *server = util_url_base(res->session->base_url);
    char *url = util_concat_path(server, res->href);
    log_printf("url: %s\n", url);
    free(server);
    free(url);
}

int cmd_list_versions(CmdArgs *a) {
    if(a->argc != 1) {
        fprintf(stderr, "Too %s arguments\n", a->argc < 1 ? "few" : "many");
        return -1;
    }
    
    SyncFile file;
    int ret = 0;
    char *path = a->argv[0];
    
    int err = sync_get_file(a, path, &file, TRUE);
    if(err) {
        sync_print_get_file_err(path, err);
        return 1;
    }
    SyncDirectory *dir = file.dir;
    
    if(!dir->versioning) {
        fprintf(stderr, "No versioning configured for syncdir %s\n", dir->name);
    }
    
    DavCfgRepository *repo = dav_config_get_repository(get_config(), cx_str(dir->repository));
    if(!repo) {
        fprintf(stderr, "Unknown repository %s\n", dir->repository);
        return -1;
    }
    
    SyncDatabase *db = load_db(dir->database);
    if(!db) {
        fprintf(stderr, "Cannot load database file: %s\n", dir->database);
        return -1;
    }
    remove_deleted_conflicts(dir, db);
    
    DavSession *sn = create_session(a, ctx, repo, dir->collection);
    cxMempoolRegister(sn->mp, db, (cx_destructor_func)destroy_db);
    if (cmd_getoption(a, "verbose")) {
        curl_easy_setopt(sn->handle, CURLOPT_VERBOSE, 1L);
        curl_easy_setopt(sn->handle, CURLOPT_STDERR, stderr);
    }
    
    DavResource *res = dav_resource_new(sn, file.path);
    if(dir->versioning->type == VERSIONING_SIMPLE) {
        do {
            DavPropName p;
            p.ns = DAV_NS;
            p.name = VERSION_PATH_PROPERTY;
            if(dav_load_prop(res, &p, 1)) {
                print_resource_error(sn, file.path);
                ret = 1;
                break;
            }
            char *vcol_href = dav_get_string_property_ns(res, DAV_NS, VERSION_PATH_PROPERTY); 
            if(!vcol_href) {
                ret = 1;
                break;
            }
            
            DavResource *vcol = dav_resource_new_href(sn, vcol_href);
            if(!vcol) {
                ret = 1;
                break;
            }
                       
            if(dav_load_prop(vcol, NULL, 0)) {
                print_resource_error(sn, vcol->path);
                ret = 1;
                break;
            }
            
            DavResource *child = vcol->children;
            CxList *children = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)strcmp, CX_STORE_POINTERS);
            while(child) {
                cxListAdd(children, child);
                child = child->next;
            }
            cxListSort(children);
            
            DavBool first = 1;
            CxIterator i = cxListIterator(children);
            cx_foreach(DavResource *, c, i) {
                if(!first) {
                    putchar('\n');
                }
                print_resource_version(c, c->name);
                first = 0;
            }
            cxListDestroy(children);
        } while(0);
    } else if(dir->versioning->type == VERSIONING_DELTAV) {
        DavResource *versions = dav_versiontree(res, NULL);
        DavResource *v = versions;
        DavBool first = 1;
        while(v) {
            if(!first) {
                putchar('\n');
            }
            char *vname = dav_get_string_property(v, "D:version-name");
            print_resource_version(v, vname);
            first = 0;
            v = v->next;
        }
    }
    
    free(file.path);
    dav_session_destroy(sn);
    
    return ret;
}


int cmd_trash_info(CmdArgs *a) {
    if(a->argc != 1) {
        fprintf(stderr, "Too %s arguments\n", a->argc < 1 ? "few" : "many");
        return -1;
    }
    
    SyncDirectory *syncdir = scfg_get_dir(a->argv[0]);
    if(!syncdir) {
        fprintf(stderr, "Unknown sync dir: %s\n", a->argv[0]);
        return -1;
    }
    if(scfg_check_dir(syncdir)) {
        return -1;
    }
    
    if(!syncdir->trash) {
        log_printf("trash not configured for %s\n", syncdir->name);
        return 0;
    }
    
    SYS_DIR dir = sys_opendir(syncdir->trash);
    if(!dir) {
        fprintf(stderr, "cannot open trash directory: %s\n", syncdir->trash);
        perror("opendir");
        return -1;
    }
    
    uint64_t trashsize = 0;
    int count = 0;
    SysDirEnt *ent;
    while((ent = sys_readdir(dir)) != NULL) {
        if(!strcmp(ent->name, ".") || !strcmp(ent->name, "..")) {
            continue;
        }
        
        char *path = util_concat_path(syncdir->trash, ent->name);
        
        SYS_STAT s;
        if(sys_stat(path, &s)) {
            perror("stat");
        } else {
            trashsize += s.st_size;
        }
        count++;
        
        free(path);
    }
    sys_closedir(dir);
    
    log_printf("path: %s\n", syncdir->trash);
    log_printf("%d %s\n", count, count == 1 ? "file" : "files");
    char *sizestr = size_str(trashsize);
    log_printf("%s\n", sizestr);
    free(sizestr);
    
    return 0;
}


int cmd_empty_trash(CmdArgs *a) {
    if(a->argc != 1) {
        fprintf(stderr, "Too %s arguments\n", a->argc < 1 ? "few" : "many");
        return -1;
    }
    
    SyncDirectory *syncdir = scfg_get_dir(a->argv[0]);
    if(!syncdir) {
        fprintf(stderr, "Unknown sync dir: %s\n", a->argv[0]);
        return -1;
    }
    if(logfile_open(syncdir)) {
        return -1;
    }
    
    if(!syncdir->trash) {
        log_error("trash not configured for %s\n", syncdir->name);
        return -1;
    }
    
    SYS_DIR dir = sys_opendir(syncdir->trash);
    if(!dir) {
        log_error("cannot open trash directory: %s\n", syncdir->trash);
        log_error("opendir: %s\n", strerror(errno));
        return -1;
    }
    
    SysDirEnt *ent;
    while((ent = sys_readdir(dir)) != NULL) {
        if(!strcmp(ent->name, ".") || !strcmp(ent->name, "..")) {
            continue;
        }
        
        char *path = util_concat_path(syncdir->trash, ent->name);
        log_printf("delete: %s\n", path);
        
        SYS_STAT s;
        if(sys_stat(path, &s)) {
            log_error("stat: %s\n", strerror(errno));
            free(path);
            continue;
        }
        if(S_ISDIR(s.st_mode)) {
            if(rmdir(path)) {
                log_error("rmdir: %s\n", strerror(errno));
            }
        } else {
            if(sys_unlink(path)) {
                log_error("unlink: %s\n", strerror(errno));
            }
        }
        
        free(path);
    }
    sys_closedir(dir);
    
    return 0;
}

#define CMD_TAG_ADD    0
#define CMD_TAG_REMOVE 1
#define CMD_TAG_SET 2
#define CMD_TAG_LIST   3
int cmd_add_tag(CmdArgs *args) {
    if(args->argc != 2) {
        fprintf(stderr, "Too %s arguments\n", args->argc <= 1 ? "few" : "many");
        return -1;
    }
    return cmd_tagop(args, CMD_TAG_ADD);
}

int cmd_remove_tag(CmdArgs *args) {
    if(args->argc != 2) {
        fprintf(stderr, "Too %s arguments\n", args->argc <= 1 ? "few" : "many");
        return -1;
    }
    return cmd_tagop(args, CMD_TAG_REMOVE);
}

int cmd_set_tags(CmdArgs *args) {
    if(args->argc < 1 || args->argc > 2) {
        fprintf(stderr, "Too %s arguments\n", args->argc < 1 ? "few" : "many");
        return -1;
    }
    return cmd_tagop(args, CMD_TAG_SET);
}

int cmd_list_tags(CmdArgs *args) {
    if(args->argc != 1) {
        fprintf(stderr, "Too %s arguments\n", args->argc <= 1 ? "few" : "many");
        return -1;
    }
    return cmd_tagop(args, CMD_TAG_LIST);
}

int cmd_tagop(CmdArgs *args, int cmd) {
    SyncFile file;
    int ret = 0;
    char *path = args->argv[0];
    
    int err = sync_get_file(args, path, &file, TRUE);
    if(err) {
        sync_print_get_file_err(path, err);
        return -1;
    }
    
    if(!file.dir->tagconfig) {
        fprintf(stderr, "Tags are not supported for this sync directory\n");
        return -1;
    }
    
    SyncDatabase *db = load_db(file.dir->database);
    if(!db) {
        fprintf(stderr, "Cannot load sync directory database\n");
        return -1;
    }
    
    LocalResource *newres = NULL;
    LocalResource *localres = cxMapGet(db->resources, cx_hash_key_str(file.path));
    if(!localres) {
        newres = calloc(1, sizeof(LocalResource));
        newres->path = strdup(file.path);
        localres = newres;
    }
    CxList *tags = NULL;
    DavBool store_tags = FALSE;
    
    if(cmd != CMD_TAG_SET) {
        char *tag = args->argv[1];
        char *tagcolor = NULL; // TODO: get color

        tags = sync_get_file_tags(file.dir, localres, NULL, NULL);
        CxIterator i = cxListIterator(tags ? tags : cxEmptyList);
        int x = -1;
        cx_foreach(DavTag *, t, i) {
            if(cmd == CMD_TAG_LIST) {
                log_printf("%s\n", t->name);
            } else if(!strcmp(t->name, tag)) {
                x = i.index;
                break;
            }
        }

        if(cmd == CMD_TAG_ADD) {
            if(x < 0) {
                DavTag *newtag = malloc(sizeof(DavTag));
                newtag->name = tag;
                newtag->color = tagcolor;
                if(!tags) {
                    tags = cxLinkedListCreateSimple(CX_STORE_POINTERS);
                }
                cxListAdd(tags, newtag);
                store_tags = TRUE;
            }
        } else if(cmd == CMD_TAG_REMOVE) {
            if(x >= 0) {
                cxListRemove(tags, x);
            }
            store_tags = TRUE;
        }
    } else {
        if(args->argc == 2) {
            char *tags_str = args->argv[1];
            tags = parse_csv_taglist(tags_str, strlen(tags_str));
            store_tags = TRUE;
            // TODO: read from stdin if tags_str is "-"
        } else if (args->argc == 1) {
            store_tags = TRUE;
        } else {
            fprintf(stderr, "Too many arguments\n");
            ret = -1;
        }
    }
    
    if(store_tags) {
        if(sync_store_tags_local(file.dir, NULL, path, tags)) {
            fprintf(stderr, "Cannot store tags\n");
        }
        if(localres) {
            localres->tags_updated = TRUE;
            if(!tags) {
                if(localres->tags_hash) {
                    free(localres->tags_hash);
                    localres->tags_hash = NULL;
                }
                localres->tags_hash = NULL;
            }
        }
    }
    
    if(newres) {
        local_resource_free(newres);
    }
    
    // store db
    if(store_db(db, file.dir->database, file.dir->db_settings)) {
        fprintf(stderr, "Cannot store sync db\n");
        ret = -2;
    }
    
    free(file.path);
    return ret;
}

int isfileindir(SyncDirectory *dir, const char *path, SyncFile *f) {
    char *fullpath;
    if(path[0] != '/') {
        size_t wdlen = 256;
        char *wd = malloc(wdlen);
        while(!getcwd(wd, wdlen)) {
            if(errno == ERANGE) {
                wdlen *= 2;
                char *newbuf = realloc(wd, wdlen);
                if (newbuf) {
                    wd = newbuf;
                } else {
                    free(wd);
                    return 0;
                }
            } else {
                free(wd);
                return 0;
            }
        }
        
        fullpath = util_concat_path(wd, path);
        free(wd);
    } else {
        fullpath = strdup(path);
    }
    
    // TODO: normalize path
    DavBool not_in_dir = 0;
    
    cxstring fp = cx_str(fullpath);
    cxstring dp = cx_str(dir->path);
    if(fp.length == dp.length) {
        if(cx_strcmp(fp, dp)) {
            not_in_dir = 1;
        }
    } else if(fp.length < dp.length) {
        not_in_dir = 1;
    } else {
        if(!cx_strprefix(fp, dp)) {
            not_in_dir = 1;
        } else {
            if(dp.ptr[dp.length-1] == '/') {
                dp.length--;
            }
            if(fp.ptr[dp.length] != '/') {
                not_in_dir = 1;
            }
        }
    }
    
    if(not_in_dir) {
        free(fullpath);
        return 0;
    }
    
    // TODO: check filter
    
    f->dir = dir;
    f->path = util_concat_path("/", fullpath + strlen(dir->path));
    
    free(fullpath);
    return 1;
}

int sync_get_file(CmdArgs *args, const char *path, SyncFile *f, DavBool dostat) {
    if(dostat) {
        SYS_STAT s;
        if(sys_stat(path, &s)) {
            switch(errno) {
                case EACCES: return 2;
                case ENOENT: return 1;
                default: return 3;
            }
        }
    }
    
    char *sdir = cmd_getoption(args, "syncdir");
    
    if(sdir) {
        SyncDirectory *dir = scfg_get_dir(sdir);
        if(!dir) {
            return 6;
        }
        if(!isfileindir(dir, path, f)) {
            return 4;
        }
    } else {
        SyncDirectory *target = NULL;
        
        CxIterator i = scfg_directory_iterator();
        cx_foreach(SyncDirectory *, dir, i) {
            if(isfileindir(dir, path, f)) {
                if(target) {
                    return 5;
                } else {
                    target = dir;
                }
            }
        }
        
        if(!target) {
            return 4;
        }
    }
    
    return 0;
}

void sync_print_get_file_err(const char *path, int err) {
    switch(err) {
        case 1: log_error("File %s: not found\n", path); break;
        case 2: log_error("File %s: permission denied\n", path); break;
        case 3: log_error("File %s: stat failed: %s\n", path, strerror(errno)); break;
        case 4: log_error("File %s is not in any syncdir\n", path); break;
        case 5: log_error("File %s is in multiple syncdirs\n", path); break;
        case 6: log_error("Syncdir not found\n"); break;
    }
}


int cmd_add_directory(CmdArgs *args) {
    DavConfig *davconfig = get_config();

    if(!davconfig->repositories) {
        fprintf(stderr, "No repositories available. Run 'dav add-repository' first.\n");
        fprintf(stderr, "Abort\n");
        return -1;
    }

    log_printf("Each sync directory must have an unique name.\n");
    char *name = assistant_getcfg("name");
    if(!name) {
        fprintf(stderr, "Abort\n");
        return -1;
    }
    if(scfg_get_dir(name)) {
        fprintf(stderr, "Directory %s already exists.\nAbort\n", name);
        return -1;
    }
    
    log_printf("Enter local directory path.\n");
    char *path = assistant_getcfg("path");
    if(!path) {
        fprintf(stderr, "Abort\n");
        return -1;
    }
    
    log_printf("Specify webdav repository.\n");
    int i = 0;
    for (DavCfgRepository *r = davconfig->repositories; r != NULL; r = r->next) {
        log_printf("%d) %s\n", i, r->name.value.ptr);
        i++;
    }
    char *repository = assistant_getcfg("repository");
    char *reponame = NULL;
    if(!repository) {
        fprintf(stderr, "Abort\n");
        return -1;
    }
    int64_t reponum = 0;
    if(util_strtoint(repository, &reponum)) {
        if(reponum < 0) {
            fprintf(stderr, "Wrong input.\nAbort\n");
            return -1;
        }
        DavCfgRepository *r = cx_linked_list_at(davconfig->repositories, 0,
                                                offsetof(DavCfgRepository, next),
                                                reponum);
        if(r != NULL) {
            reponame = r->name.value.ptr;
        } else {
            fprintf(stderr, "Wrong input.\nAbort\n");
            return -1;
        }
    } else {
        if(dav_config_get_repository(davconfig, cx_str(repository))) {
            reponame = repository;
        } else {
            fprintf(stderr, "Repository %s doesn't exist.\nAbort\n", repository);
            return -1;
        }
    }
    
    log_printf("Enter collection relative to the repository base url.\n");
    char *collection = assistant_getdefcfg("collection", "/");
    
    char *db = generate_db_name(name);
    
    SyncDirectory dir;
    memset(&dir, 0, sizeof(SyncDirectory));
    dir.name = name;
    dir.path = path;
    dir.repository = reponame;
    dir.collection = collection;
    dir.trash = ".trash";
    dir.database = db;
    
    int ret = 0;
    if(add_directory(&dir)) {
        fprintf(stderr, "Cannot write sync.xml\n");
        ret = -1;
    } else {
        log_printf("\nAdded directory: %s (%s)\n", name, path);
    }
    
    free(name);
    free(path);
    free(repository);
    free(collection);
    free(db);
    
    return ret;
}

int cmd_list_dirs() {
    CxIterator iter = scfg_directory_iterator();
    cx_foreach(SyncDirectory *, dir, iter) {
        log_printf("%s\n", dir->name);
    }
    return 0;
}

int cmd_check_repositories(CmdArgs *a) {
    int ret = EXIT_SUCCESS;

    CxList *reponames = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    {
        CxIterator iter = scfg_directory_iterator();
        cx_foreach(SyncDirectory *, dir, iter) {
            cxListAdd(reponames, dir->repository);
        }
    }
    
    CxIterator iter = cxListIterator(reponames);
    cx_foreach(char *, reponame, iter) {
        log_printf("Checking %s... ", reponame);
        DavCfgRepository *repo = dav_config_get_repository(get_config(), cx_str(reponame));
        if (!repo) {
            log_printf(" not found in config.xml!\n");
            ret = EXIT_FAILURE;
        } else {
            DavSession *sn = create_session(a, ctx, repo, repo->url.value.ptr);
            if (sn) {
                DavResource *res = dav_query(sn,
                        "select - from / with depth = 0");
                if (res) {
                    log_printf("OK.\n");
                    dav_resource_free(res);
                } else {
                    log_printf("unavailable!\n");
                    ret = EXIT_FAILURE;
                }
                dav_session_destroy(sn);
            } else {
                log_printf("cannot create session!\n");
                ret = EXIT_FAILURE;
            }
        }
    }
    
    cxListDestroy(reponames);
    
    return ret;
}

char* create_locktoken_file(const char *syncdirname, const char *locktoken) {
    cxmutstr fname = cx_asprintf("locktoken-%s.txt", syncdirname);
    char *path = config_file_path(fname.ptr);
    free(fname.ptr);
    
    FILE *file = sys_fopen(path, "w");
    if(file) {
        log_error("%s\n", locktoken);
        fclose(file);
        return path;
    } else {
        log_error("Cannot create locktoken file: %s", strerror(errno));
        free(path);
        return NULL;
    }
}

char* sync_get_content_hash(DavResource *res) {
    uint32_t flags = res->session->flags;
    if((flags & DAV_SESSION_ENCRYPT_CONTENT) == DAV_SESSION_ENCRYPT_CONTENT) {
        char *enc_hash = dav_get_string_property_ns(res, DAV_NS, "crypto-hash");
        char *keyname = dav_get_string_property_ns(res, DAV_NS, "crypto-key");
        if(enc_hash && keyname) {
            DavKey *key = dav_context_get_key(res->session->context, keyname);
            if(!key) {
                return NULL;
            }
            
            size_t len = 0;
            char *dec_hash = aes_decrypt(enc_hash, &len, key);
            if(!dec_hash) {
                return NULL;
            }   
            
            char *hex_hash = util_hexstr((unsigned char*)dec_hash, len);
            free(dec_hash);
            return hex_hash;
        }
    } else {
        char *hash = dav_get_string_property_ns(res, DAV_NS, "content-hash");
        if(hash) {
            return strdup(hash);
        }
    }
    return NULL;
}

void sync_set_content_hash(DavResource *res, const unsigned char *hashdata) {
    uint32_t flags = res->session->flags;
    if((flags & DAV_SESSION_ENCRYPT_CONTENT) == DAV_SESSION_ENCRYPT_CONTENT) {
        if(res->session->key) {
            char *enc_hash = aes_encrypt((const char*)hashdata, DAV_SHA256_DIGEST_LENGTH, res->session->key);
            if(enc_hash) {
                dav_set_string_property_ns(res, DAV_NS, "crypto-hash", enc_hash);
                free(enc_hash);
            }
        }
    } else {
        char *hex_hash = util_hexstr(hashdata, DAV_SHA256_DIGEST_LENGTH);
        dav_set_string_property_ns(res, DAV_NS, "content-hash", hex_hash);
        free(hex_hash);
    }
}

mercurial