dav/sync.c

Sun, 01 Apr 2018 12:40:48 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 01 Apr 2018 12:40:48 +0200
changeset 372
2e15ff88a0ab
parent 370
ab9c5afdc243
child 374
38ae05d46f9a
permissions
-rw-r--r--

adds tag management commands to dav-sync

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2018 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 <unistd.h>
#include <signal.h>
#include <time.h>
#include <libxml/xmlerror.h>
#include <sys/types.h>
#include <ucx/string.h>
#include <ucx/utils.h>
#include <ucx/properties.h>
#include <dirent.h>

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

#include "config.h"
#include "scfg.h"
#include "sopt.h"
#include "db.h"
#include "error.h"
#include "assistant.h"
#include "libxattr.h"
#include "tags.h"
#include "sync.h"
#include "libidav/session.h"

#include <pthread.h>

static DavContext *ctx;

static int sync_shutdown = 0;

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

int main(int argc, char **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);
    ctx = dav_context_new();
    int cfgret = load_config(ctx) || load_sync_config();
    
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    pthread_t tid;
    
    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);
            stop_sighandler(&mutex, tid);
        } else if(!strcmp(cmd, "push")) {
            tid = start_sighandler(&mutex);
            ret = cmd_push(args, FALSE);
            stop_sighandler(&mutex, tid);
        } else if(!strcmp(cmd, "archive")) {
            tid = start_sighandler(&mutex);
            ret = cmd_push(args, TRUE);
            stop_sighandler(&mutex, tid);
        } 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, "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, "update-tags")) {
            ret = cmd_update_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();
        } else {
            print_usage(argv[0]);
        }
    }
    
    // cleanup
    cmd_args_free(args);
    dav_context_destroy(ctx);
    
    free_config();
    free_sync_config();
    
    curl_global_cleanup();
    xmlCleanupParser();
    
    return ret;
}

void print_usage(char *cmd) {
    fprintf(stderr, "Usage: %s command [options] arguments...\n\n", cmd);
    
    fprintf(stderr, "Commands:\n");
    fprintf(stderr, "        pull [-cld] <directory>\n");
    fprintf(stderr, "        push [-cld] <directory>\n");
    fprintf(stderr, "        archive [-cld] <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, "        add-tag [-s <syncdir>] <file> <tag>\n");
    fprintf(stderr, "        remove-tag [-s <syncdir>] <file> <tag>\n");
    fprintf(stderr, "        update-tags [-s <syncdir>] <file> [tags]\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, "        -r         Read changes from stdin\n\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;
}

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);
}

static int res_matches_filter(SyncDirectory *dir, char *res_path) {
    // trash filter
    if (dir->trash) {
        sstr_t rpath = sstr(util_concat_path(dir->path, res_path));
        if (sstrprefix(rpath, sstr(dir->trash))) {
            free(rpath.ptr);
            return 1;
        }
        free(rpath.ptr);
    }
    
    // include/exclude filter
    UCX_FOREACH(inc, dir->include) {
        regex_t* pattern = (regex_t*) inc->data;
        if (regexec(pattern, res_path, 0, NULL, 0) == 0) {
            UCX_FOREACH(exc, dir->exclude) {
                regex_t* pattern = (regex_t*) exc->data;
                if (regexec(pattern, res_path, 0, NULL, 0) == 0) {
                    return 1;
                }
            }
            return 0;
        }
    }
    return 1;
}

static DavSession* create_session(DavContext *ctx, Repository *repo, char *url) {
    DavSession *sn = dav_session_new_auth(
            ctx,
            url,
            repo->user,
            repo->password);
    sn->flags = get_repository_flags(repo);
    sn->key = dav_context_get_key(ctx, repo->default_key);
    curl_easy_setopt(sn->handle, CURLOPT_HTTPAUTH, repo->authmethods);
    curl_easy_setopt(sn->handle, CURLOPT_SSLVERSION, repo->ssl_version);
    if(repo->cert) {
        curl_easy_setopt(sn->handle, CURLOPT_CAPATH, repo->cert);
    }
    if(!repo->verification) {
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYPEER, 0);
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYHOST, 0);
    }
    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");
}

int cmd_pull(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((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;
    }
    
    Repository *repo = get_repository(sstr(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);
    
    char *new_url = NULL;
    if(dir->collection) {
        new_url = util_concat_path(repo->url, dir->collection);
    }
    DavSession *sn = create_session(ctx, repo, new_url ? new_url : repo->url);
    ucx_mempool_reg_destr(sn->mp, db, (ucx_destructor)destroy_db);
    if(new_url) {
        free(new_url);
    }
    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)) {
            print_resource_error(sn, "/");
            dav_session_destroy(sn);
            fprintf(stderr, "Abort\n");
            return -1;
        }
        DavLock *lock = dav_get_lock(sn, "/");
        if(lock) {
            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:status,idav:tags from / with depth = infinity");
    if(!ls) {
        print_resource_error(sn, "/");
        if(locked) {
            if(dav_unlock(root)) {
                print_resource_error(sn, "/");
            } else {
                locked = FALSE;
            }
        }
        
        fprintf(stderr, "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)) {
                print_resource_error(sn, "/");
            } else {
                locked = FALSE;
            }
        }
        // TODO: free
        dav_session_destroy(sn);
        
        if(!locked && locktokenfile) {
            remove(locktokenfile);
        }
        
        return -1;
    }
    
    int sync_success = 0;
    int sync_delete = 0;
    int sync_error = 0;
    
    UcxMap *svrres = ucx_map_new(db->resources->count);
    
    UcxList *statls = NULL;
    
    UcxList *stack = ucx_list_prepend(NULL, ls->children);
    while(stack) {
        DavResource *res = stack->data;
        stack = ucx_list_remove(stack, stack);
         
        while(res) {
            if (res_matches_filter(dir, res->path)) {
                res = res->next;
                continue;
            }
            
            char *status = dav_get_string_property(res, "idav:status");
            if(status && !strcmp(status, "broken")) {
                res = res->next;
                continue;
            }
            
            // download the resource
            if(!sync_shutdown && sync_get_resource(a, dir, res, db, &sync_success)) {
                fprintf(stderr, "sync_get_resource failed for resource: %s\n", res->path);
                sync_error++;
            }
            
            // add every resource from the server to svrres
            // then db-resources contains only resources which are not on the
            // server
            LocalResource *local = ucx_map_cstr_remove(db->resources, res->path);
            if(local) {
                ucx_map_cstr_put(svrres, res->path, local);
                
                if(local->last_modified == 0) {
                    // stat this file later (directory)
                    statls = ucx_list_prepend(statls, local);
                }
            } // else: sync_shutdown is TRUE
            
            if(res->children) {
                stack = ucx_list_prepend(stack, res->children);
            }
            res = res->next;
        }
    }
    
    // stat all files with unknown lastmodified date
    UCX_FOREACH(elm, statls) {
        LocalResource *l = elm->data;
        char *local_path = util_concat_path(dir->path, l->path);
        struct stat s;
        if(!stat(local_path, &s)) {
            l->last_modified = s.st_mtime;
        }
        free(local_path);
    }
    ucx_list_free(statls);
    
    // delete every remotely removed resource
    UcxMapIterator i = ucx_map_iterator(db->resources);
    LocalResource *local;
    UcxList *rmdirs = NULL;
    UCX_MAP_FOREACH(key, local, i) {
        if (res_matches_filter(dir, local->path)) {
            continue;
        }
        
        if(sync_shutdown) {
            ucx_map_cstr_put(svrres, local->path, local_resource_copy(local));
        } else {
            // sync_remove_resource does all necessary tests
            int ret = sync_remove_local_resource(dir, local);
            if(ret == -1) {
                rmdirs = ucx_list_append(rmdirs, local);
            } else if(ret == 0) {
                sync_delete++;
            }
        }
    }
    UCX_FOREACH(elm, rmdirs) {
        LocalResource *local_dir = elm->data;
        sync_remove_local_directory(dir, local_dir);
    }
    ucx_map_free_content(db->resources, (ucx_destructor)local_resource_free);
    ucx_map_free(db->resources);
    db->resources = svrres;
    
    // unlock repository
    if(locked) {
        if(dav_unlock(root)) {
            print_resource_error(sn, "/");
            ret = -1;
        } else {
            locked = FALSE;
        }
    }
    
    // store db
    if(store_db(db, dir->database)) {
        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";
        printf("Result: %d %s pulled, %d %s deleted, %d %s\n",
                sync_success, str_success,
                sync_delete,str_delete,
                sync_error, str_error);
    }
    
    return ret;
}

int sync_get_resource(
        CmdArgs *a,
        SyncDirectory *dir,
        DavResource *res,
        SyncDatabase *db,
        int *counter)
{
    int cdt = cmd_getoption(a, "conflict") ? 0 : 1; // conflict detection
    
    LocalResource *local = ucx_map_cstr_get(db->resources, res->path);
    char *local_path = util_concat_path(dir->path, res->path);
    
    char *etag = dav_get_string_property(res, "D:getetag");
    struct stat s;
    memset(&s, 0, sizeof(struct stat));
    if(local && !res->iscollection) {
        int exists = 1;
        if(stat(local_path, &s)) {
            // Ignore the fact, that the file is locally removed. If the
            // server has an updated version, we read the file or the
            // next push will delete it on the server.
            if(errno != ENOENT) {
                fprintf(stderr, "Cannot stat file: %s\n", local_path);
                free(local_path);
                return -1;
            } else {
                exists = 0;
            }
        }
              
        if(local->etag) {
            sstr_t e = sstr(etag);
            if(sstrprefix(e, S("W/"))) {
                e = sstrsubs(e, 2);
            }
            if(!strcmp(e.ptr, local->etag)) {
                // resource is already up-to-date on the client
                sync_store_tags(dir, local_path, local, res);
                free(local_path);
                return 0;
            }
        }
        
        if(cdt && exists && s.st_mtime != local->last_modified) {
            // file modified on the server and on the client
            rename_conflict_file(dir, db, local->path);
        }
    } else {
        if(stat(local_path, &s)) {
            if(errno != ENOENT) {
                fprintf(stderr, "Cannot stat file: %s\n", local_path);
            }
        } else if(S_ISDIR(s.st_mode)) {
            //fprintf(stderr, "Error: file %s is a directory\n", local_path);
        } else if(cdt) {
            // rename file on conflict
            rename_conflict_file(dir, db, res->path);
        }
    }
      
    int ret = 0;
    char *tmp_path = create_tmp_download_path(local_path);
    if(res->iscollection) {
        mode_t mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
        if(util_mkdir(local_path, mode) && errno != EEXIST) {
            ret = -1;
        }
        
        if(ret == 0) {
            if(!local) {
                // new local resource
                local = calloc(1, sizeof(LocalResource));
                local->path = util_concat_path(res->path, "/");
                local->last_modified = 0;
                if(local->etag) {
                    free(local->etag);
                }
                local->etag = strdup(etag);
                ucx_map_cstr_put(db->resources, local->path, local);
            }
            
            sync_store_tags(dir, local_path, local, res);
        }
    } else {
        if(!tmp_path) {
            fprintf(stderr, "Cannot create tmp path for %s\n", local_path);
            free(local_path);
            return -1;
        }
        FILE *out = fopen(tmp_path, "wb");
        if(!out) {
            fprintf(stderr, "Cannot open output file: %s\n", local_path);
            free(local_path);
            free(tmp_path);
            return -1;
        }
        printf("get: %s\n", res->path);
        if(dav_get_content(res, out, (dav_write_func)fwrite)) {
            ret = -1;
        }
        fclose(out);
        
        sync_store_tags(dir, tmp_path, local, res);
        
        if(ret == 0) {
            (*counter)++;
            
            if(dir->trash && dir->backuppull) {
                move_to_trash(dir, local_path);
            }
            if(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;
            }
            
            if(stat(local_path, &s)) {
                fprintf(stderr, "Cannot stat file: %s\n", local_path);
                perror("");
            }
            
            if(!local) {
                // new local resource
                local = calloc(1, sizeof(LocalResource));
                local->path = strdup(res->path);
                ucx_map_cstr_put(db->resources, local->path, local);
            }
            
            if(local->etag) {
                free(local->etag);
            }
            // set metadata from stat
            local->etag = strdup(etag);
            local->last_modified = s.st_mtime;
            local->size = s.st_size;
            local->skipped = FALSE;
        } else {
            if(unlink(tmp_path)) {
                fprintf(stderr, "Cannot remove tmp file: %s\n", tmp_path);
            }
        }
    }
    
    free(tmp_path);
    free(local_path);
    return ret;
}

int sync_remove_local_resource(SyncDirectory *dir, LocalResource *res) {
    char *local_path = util_concat_path(dir->path, res->path);
    struct stat s;
    if(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;
    }
    
    printf("delete: %s\n", res->path);
    int ret = 0;
    if(dir->trash) {
        move_to_trash(dir, local_path);
    } else if(unlink(local_path)) {
        fprintf(stderr, "Cannot remove file %s\n", local_path);
        ret = -2;
    }
    free(local_path);
    
    return ret;
}

void sync_remove_local_directory(SyncDirectory *dir, LocalResource *res) {
    char *local_path = util_concat_path(dir->path, res->path);
    
    printf("delete: %s\n", res->path);
    if(rmdir(local_path)) {
        fprintf(stderr, "rmdir: %s : ", local_path);
        perror(NULL);
    }
    
    free(local_path);
}

void rename_conflict_file(SyncDirectory *dir, SyncDatabase *db, char *path) {
    char *local_path = util_concat_path(dir->path, path);
    char *parent = util_parent_path(local_path);
    
    int rev = 0;
    struct stat s;
    int loop = 1;
    do {
        char *res_parent = util_parent_path(path);
        char *res_name = util_resource_name(path);
        
        sstr_t new_path = ucx_sprintf(
            "%sorig.%d.%s",
            parent,
            rev,
            res_name);
        sstr_t new_res_path = ucx_sprintf(
            "%sorig.%d.%s",
            res_parent,
            rev,
            res_name);
        
        
        if(stat(new_path.ptr, &s)) {
            if(errno == ENOENT) {
                loop = 0;
                printf("conflict: %s\n", local_path);
                if(rename(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);
                    ucx_map_cstr_put(db->conflict, 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++) {
        sstr_t np = ucx_asprintf(
        ucx_default_allocator(),
            "%sdownload%d-%s",
            parent,
            i,
            util_resource_name(path));
        
        struct stat s;
        if(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++) {
        sstr_t np = ucx_asprintf(
        ucx_default_allocator(),
            "%s%d-%s",
            dir->trash,
            i,
            util_resource_name(path));
        
        struct stat s;
        if(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(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 ucx_map_cstr_get(db->conflict, res->path) ? 1 : 0;
}

int cmd_push(CmdArgs *a, DavBool archive) {
    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;
    }
    
    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;
    }
    
    Repository *repo = get_repository(sstr(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);
    
    char *new_url = NULL;
    if(dir->collection) {
        new_url = util_concat_path(repo->url, dir->collection);
    }
    DavSession *sn = create_session(ctx, repo, new_url ? new_url : repo->url);
    ucx_mempool_reg_destr(sn->mp, db, (ucx_destructor)destroy_db);
    if(new_url) {
        free(new_url);
    }
    if (cmd_getoption(a, "verbose")) {
        curl_easy_setopt(sn->handle, CURLOPT_VERBOSE, 1L);
        curl_easy_setopt(sn->handle, CURLOPT_STDERR, stderr);
    }
    
    DavResource *root = dav_query(sn, "select - from / with depth = 0");
    if(!root) {
        print_resource_error(sn, "/");
        dav_session_destroy(sn);
        fprintf(stderr, "Abort\n");
        return -1;
    }
    
    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")) {
        if(dav_lock_t(root, dir->lock_timeout)) {
            print_resource_error(sn, "/");
            dav_session_destroy(sn);
            fprintf(stderr, "Abort\n");
            return -1;
        }
        DavLock *lock = dav_get_lock(sn, "/");
        if(lock) {
            printf("Lock-Token: %s\n", lock->token);
        }
        locked = TRUE;
        locktokenfile = create_locktoken_file(dir->name, lock->token);
    }
    
    int sync_success = 0;
    int sync_delete = 0;
    int sync_skipped = 0;
    int sync_error = 0;
    
    // upload all changed files
    //UcxList *resources = cmd_getoption(a, "read") ?
    //        read_changes(dir, db) : local_scan(dir, db);
    UcxList *resources = local_scan(dir, db);
    
    UcxMap *lclres = ucx_map_new(db->resources->count);
    int ret = 0;
    UCX_FOREACH(elm, resources) {
        LocalResource *local_res = elm->data;
        if (!res_matches_filter(dir, local_res->path+1)) {
            if(sync_shutdown) {
                LocalResource *lr = ucx_map_cstr_remove(db->resources, local_res->path);
                if(lr) {
                    local_res->size = lr->size;
                    local_res->last_modified = lr->last_modified;
                    local_res->etag = lr->etag ? strdup(lr->etag) : NULL;
                    local_resource_free(lr);
                    ucx_map_cstr_put(lclres, local_res->path, local_res);
                }
                elm->data = NULL;
                continue;
            }
            
            
            if(res_isconflict(db, local_res)) {
                printf("skip: %s\n", local_res->path);
                sync_skipped++;
                continue;
            }
                       
            // upload every changed file
            int error = 0;
            int is_changed = local_resource_is_changed(dir, db, local_res);
            if (is_changed || local_res->tags_updated) {
                DavResource *res = dav_resource_new(sn, local_res->path);
                if(!res) {
                    print_resource_error(sn, local_res->path);
                    ret = -1;
                    sync_error++;
                }
                
                if(local_res->isdirectory || !is_changed) {
                    // only check existence if the resource is supposed
                    // to be a collection
                    int exists = local_res->isdirectory ? dav_exists(res) : 1;
                    
                    // continue if the resource exists or is not found
                    // because a 404 is not an unexpected error
                    if(exists || sn->error == DAV_NOT_FOUND) {
                        int abort = 0;
                        if(!exists) {
                            // make sure to store tags for newly created cols
                            local_res->tags_updated = 1;
                            // create collection
                            // TODO: show 405
                            printf("mkcol: %s\n", local_res->path);
                            if(sync_mkdir(dir, res, local_res) && sn->error != DAV_METHOD_NOT_ALLOWED) {
                                print_resource_error(sn, res->path);
                                ret = -1;
                                sync_error++;
                                error = 1;
                                abort = 1;
                            }
                        }
                        
                        if(dir->tagconfig && local_res->tags_updated && !abort) {
                            sync_update_tags(dir, sn, res, local_res);
                        }
                    } else {
                        // dav_exists() failed
                        print_resource_error(sn, local_res->path);
                        ret = -1;
                        sync_error++;
                        error = 1;
                    }
                } else {
                    if(cdt && remote_resource_is_changed(sn, dir, db, local_res)) {
                        printf("conflict: %s\n", local_res->path);
                        local_res->last_modified = 0;
                        local_res->skipped = TRUE;
                        sync_skipped++;
                    } else {
                        printf("put: %s\n", local_res->path);
                        if(sync_put_resource(dir, res, local_res, &sync_success)) {
                            sync_error++;
                            print_resource_error(sn, res->path);
                            ret = -1;
                            error = 1;
                        }
                    }
                }
                dav_resource_free(res);
            }
            
            // remove every locally available resource from db->resource
            // the remaining elements are all deleted files
            elm->data = NULL;
            if(!error) {
                ucx_map_cstr_put(lclres, local_res->path, local_res);
            }
            
            LocalResource *lr = ucx_map_cstr_remove(db->resources, local_res->path);
            if(lr) {
                local_resource_free(lr);
            }
        }
    }
    
    // delete all removed files
    if(ret == 0 && !archive) {
        UcxMapIterator i = ucx_map_iterator(db->resources);
        LocalResource *local;
        UCX_MAP_FOREACH(key, local, i) {
            if (!res_matches_filter(dir, local->path+1)) {
                if(sync_shutdown) {
                    ucx_map_cstr_put(lclres, local->path, local_resource_copy(local));
                } else if(sync_delete_remote_resource(sn, local, &sync_delete)) {
                    ucx_map_cstr_put(lclres, local->path, local_resource_copy(local));
                    if(sn->error != DAV_NOT_FOUND) {
                        print_resource_error(sn, local->path);
                        sync_error++;
                        break;
                    }
                }
            }
        }
    }
    ucx_map_free_content(db->resources, (ucx_destructor)local_resource_free);
    ucx_map_free(db->resources);
    db->resources = lclres;
    
    // unlock repository
    if(locked) {
        if(dav_unlock(root)) {
            print_resource_error(sn, "/");
            ret = -1;
        } else {
            locked = FALSE;
        }
    }
    
    // store db
    if(store_db(db, dir->database)) {
        fprintf(stderr, "Cannot store sync db\n");
        ret = -2;
    }
    
    // cleanup
    if(!locked && locktokenfile) {
        remove(locktokenfile);
    }
    
    dav_session_destroy(sn);
    while(resources) {
        UcxList *next = resources->next;
        if(resources->data) {
            local_resource_free(resources->data);
        }
        free(resources);
        resources = next;
    }
    
    // Report
    if(ret != -2) {
        char *str_success = sync_success == 1 ? "file" : "files";
        char *str_delete = sync_delete == 1 ? "file" : "files";
        char *str_skipped = sync_delete == 1 ? "file" : "files";
        char *str_error = sync_error == 1 ? "error" : "errors";
        
        printf("Result: %d %s pushed, ", sync_success, str_success);
        if(!archive) {
            printf("%d %s deleted, ", sync_delete, str_delete);
        }
        printf("%d %s skipped, %d %s\n",
                sync_skipped, str_skipped, sync_error, str_error);
    }
    
    return ret;
}

UcxList* local_scan(SyncDirectory *dir, SyncDatabase *db) {
    UcxList *resources = NULL;
    
    char *path = strdup("/");
    UcxList *stack = ucx_list_prepend(NULL, path);
    while(stack) {
        // get a directory path from the stack and read all entries
        // if an entry is a directory, put it on the stack
        
        char *p = stack->data;
        stack = ucx_list_remove(stack, stack);
        char *local_path = util_concat_path(dir->path, p);
        DIR *local_dir = opendir(local_path);
        
        if(!local_dir) {
            fprintf(stderr, "Cannot open directory %s\n", local_path);
        } else {
            struct dirent *ent;
            while((ent = readdir(local_dir)) != NULL) {
                if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
                    continue;
                }
                
                char *new_path = util_concat_path(p, ent->d_name);
                int isdir;
                LocalResource *res = local_resource_new(dir, db, new_path, &isdir);
                if(isdir) {
                    resources = ucx_list_append(resources, res);
                    stack = ucx_list_prepend(stack, new_path);
                } else if(res) {
                    resources = ucx_list_append(resources, res);
                    free(new_path);
                } else {
                    free(new_path);
                }
            }
            closedir(local_dir);
 
        }
        free(local_path);
        free(p);
    }
    
    return resources;
}

UcxList* read_changes(SyncDirectory *dir, SyncDatabase *db) {
    UcxProperties *parser = ucx_properties_new();
    parser->delimiter = ':';
    
    UcxList *resources = NULL;
    sstr_t name;
    sstr_t value;
    
    char buf[STDIN_BUF_SIZE];
    size_t r;
    while(!feof(stdin)) {
        r = fread(buf, 1, STDIN_BUF_SIZE, stdin);
        ucx_properties_fill(parser, buf, r);
        while(ucx_properties_next(parser, &name, &value)) {
            if(value.length == 0) {
                fprintf(stderr, "Wrong input\n");
                continue;
            }
            if(value.ptr[0] == '"'
                    && value.length > 2
                    && value.ptr[value.length - 1] == '"')
            {
                value.ptr[value.length - 1] = '\0';
                value.ptr++;
                value.length -= 2;
            }
            value = sstrdup(value);
            
            if(!sstrcmp(name, S("put"))) {
                int isdir;
                LocalResource *res = local_resource_new(dir, db, value.ptr, &isdir);
                if(res) {
                    resources = ucx_list_append(resources, res);
                }
            } else if(!sstrcmp(name, S("remove"))) {
                ucx_map_sstr_remove(db->resources, value);
            }
            
            free(value.ptr);
        }
    }
    ucx_properties_free(parser);    
    
    return resources;
}

LocalResource* local_resource_new(SyncDirectory *dir, SyncDatabase *db, char *path, int *isdir) {
    char *file_path = util_concat_path(dir->path, path);
    struct stat s;
    if(stat(file_path, &s)) {
        fprintf(stderr, "Cannot stat file %s\n", file_path);
        free(file_path);
        return NULL;
    }
    free(file_path);

    if(!S_ISDIR(s.st_mode)) {
        *isdir = 0;
        LocalResource *res = calloc(1, sizeof(LocalResource));
        res->path = strdup(path);
        res->etag = NULL;
        res->last_modified = s.st_mtime;
        res->size = s.st_size;
        return res;
    } else {
        *isdir = 1;
        LocalResource *res = calloc(1, sizeof(LocalResource));
        res->path = util_concat_path(path, "/");
        res->last_modified = s.st_mtime;
        res->isdirectory = 1;
        return res;
    }
}

LocalResource* local_resource_copy(LocalResource *res) {
    LocalResource *newres = calloc(1, sizeof(LocalResource));
    if(res->name) {
        newres->name = strdup(res->name);
    }
    if(res->path) {
        newres->path = strdup(res->path);
    }
    if(res->etag) {
        newres->etag = strdup(res->etag);
    }
    newres->skipped = res->skipped;
    newres->size = res->size;
    newres->last_modified = res->last_modified;
    newres->isdirectory = res->isdirectory;
    return newres;
}

int local_resource_is_changed(SyncDirectory *dir, SyncDatabase *db, LocalResource *res) {
    LocalResource *db_res = ucx_map_cstr_get(db->resources, res->path);
    res->tags_updated = 0;
    if(db_res) {
        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(dir->tagconfig && dir->tagconfig->detect_changes && !res->tags_updated) {
            UcxBuffer *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
            }
        }
        
        if(db_res->last_modified == res->last_modified && db_res->size == res->size) {
            return 0;
        }
    }
    return 1;
}

int remote_resource_is_changed(
        DavSession *sn,
        SyncDirectory *dir,
        SyncDatabase *db,
        LocalResource *res)
{
    DavResource *remote = dav_get(sn, res->path, "D:getetag");
    int ret = 0;
    if(remote) {
        char *etag = dav_get_string_property(remote, "D:getetag");
        if(!res->etag) {
            // the resource is on the server and the client has no etag
            ret = 1;
        } else if(etag) {
            sstr_t e = sstr(etag);
            if(sstrprefix(e, S("W/"))) {
                e = sstrsubs(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);
        }
        dav_resource_free(remote);
    }
    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;
}

UcxList* sync_merge_tags(UcxList *tags1, UcxList *tags2) {
    // this map is used to check the existence of tags
    UcxMap *tag_map = ucx_map_new(32);
    // merged taglist
    UcxList *new_tags = NULL;

    // add all local tags
    UCX_FOREACH(elm, tags1) {
        DavTag *t = elm->data;
        ucx_map_cstr_put(tag_map, t->name, t);
        DavTag *newt = calloc(1, sizeof(DavTag));
        newt->color = t->color ? strdup(t->color) : NULL;
        newt->name = strdup(t->name);
        new_tags = ucx_list_append(new_tags, newt);
    }
    // check if a remote tag is already in the map
    // and if not add it to the new taglist
    UCX_FOREACH(elm, tags2) {
        DavTag *t = elm->data;
        if(!ucx_map_cstr_get(tag_map, t->name)) {
            DavTag *newt = calloc(1, sizeof(DavTag));
            newt->color = t->color ? strdup(t->color) : NULL;
            newt->name = strdup(t->name);
            new_tags = ucx_list_append(new_tags, newt);
        }
    }

    ucx_map_free(tag_map);

    return new_tags;
}

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

int sync_store_tags_local(SyncDirectory *dir, LocalResource *local, const char *path, UcxList *tags) {
    int ret = 0;
    if(dir->tagconfig->store == TAG_STORE_XATTR) {
        UcxBuffer *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);
                        // TODO: update hash in localres??
                    } else {
                        update = 0;
                    }
                }
                if(update) {
                    ret = xattr_set(path, dir->tagconfig->xattr_name, data->space, data->pos);
                }
                ucx_buffer_free(data);
            } else {
                ret = -1;
            }
        } else {
            // TODO: relete xattr
            //ret = xattr_remove(path, dir->tagconfig->xattr_name);
        }
    }
    
    return ret;
}

UcxBuffer* sync_get_file_tag_data(SyncDirectory *dir, LocalResource *res) {
    if(!dir->tagconfig) {
        return NULL;
    }
    if(res->cached_tags) {
        return res->cached_tags;
    }
    UcxBuffer *buf = NULL;
    if(dir->tagconfig->store == TAG_STORE_XATTR) {
        ssize_t tag_length = 0;
        char *local_path = util_concat_path(dir->path, res->path);
        char* tag_data = xattr_get(
                local_path,
                dir->tagconfig->xattr_name,
                &tag_length);
        free(local_path);
        
        if(tag_length > 0) {
            buf = ucx_buffer_new(tag_data, (size_t)tag_length, UCX_BUFFER_AUTOFREE);
            buf->size = (size_t)tag_length;
        }
    }
    res->cached_tags = buf;
    return buf;
}

UcxList* sync_get_file_tags(SyncDirectory *dir, LocalResource *res, DavBool *changed) {
    if(changed) *changed = FALSE;
    
    UcxList *tags = NULL;
    
    if(!res) {
        return NULL;
    }
    
    if(!dir->tagconfig) {
        return NULL;
    }
    if(dir->tagconfig->store == TAG_STORE_XATTR) {
        UcxBuffer *tag_buf = res->cached_tags ?
                res->cached_tags :
                sync_get_file_tag_data(dir, res);
        
        if(tag_buf) {
            char *newhash = dav_create_hash(tag_buf->space, tag_buf->size);
            if(res->tags_hash) {
                if(changed && strcmp(res->tags_hash, newhash)) {
                    *changed = TRUE;
                }
                free(res->tags_hash);
            } else {
                if(changed) *changed = TRUE;
            }
            res->tags_hash = newhash;
            
            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;
}

int sync_put_resource(
        SyncDirectory *dir,
        DavResource *res,
        LocalResource *local,
        int *counter)
{
    char *local_path = util_concat_path(dir->path, res->path);
    
    struct stat s;
    if(stat(local_path, &s)) {
        fprintf(stderr, "cannot stat file: %s\n", local_path);
        perror("");
        free(local_path);
        return -1;
    }
    
    FILE *in = fopen(local_path, "rb");
    if(!in) {
        fprintf(stderr, "Cannot open file %s\n", local_path);
        free(local_path);
        return -1;
    }
    
    dav_set_content(res, in, (dav_read_func)fread);
    
    if(dir->tagconfig) {
        UcxList *tags = sync_get_file_tags(dir, local, NULL);
        DavXmlNode *prop = create_xml_taglist(tags);
        if(prop) {
            dav_set_property_ns(res, DAV_NS, "tags", prop);
        }
    }
    
    int ret = -1;
    int created = 0;
    for(int i=0;i<=dir->max_retry;i++) {
        if(!created && dav_create(res)) {
            continue;
        }
        created = 1;
        if(dav_store(res)) {
            continue;
        }
        ret = 0;
        break;
    }
    


    if(ret == 0) {
        (*counter)++;
        
        // check contentlength and get new etag
        DavResource *up_res = dav_get(res->session, res->path, "D:getetag,idav:status,idav:tags");
        
        if(up_res) {
            // the new content length must be equal or greater than the file size
            if(up_res->contentlength < s.st_size) {
                fprintf(stderr, "Incomplete Upload: %s", 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");
                if(etag) {
                    if(strlen(etag) > 2 && etag[0] == 'W' && etag[1] == '/') {
                        etag = etag + 2;
                    } 
                }

                if(local->etag) {
                    free(local->etag);
                }

                if(etag) {
                    local->etag = strdup(etag);
                } else {
                    local->etag = NULL;
                }
                
                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_delete_remote_resource(
        DavSession *sn,
        LocalResource *local_res,
        int *counter)
{
    DavResource *res = dav_get(sn, local_res->path, "D:getetag");
    if(!res) {
        return sn->error == DAV_NOT_FOUND ? 0 : 1;
    }
    
    int ret = 0;
    if(res->iscollection) {
        printf("delete: %s\n", res->path);
        if(dav_delete(res)) {
            ret = 1;
            fprintf(stderr, "Cannot delete resource %s\n", res->path);
        }
    } 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(etag && !strcmp(etag, local_res->etag)) {
            // local resource metadata == remote resource metadata
            // resource can be deleted
            printf("delete: %s\n", res->path);
            if(dav_delete(res)) {
                if(sn->error != DAV_NOT_FOUND) {
                    fprintf(stderr, "Cannot delete resource %s\n", res->path);
                    ret = 1;
                }
            } else {
                (*counter)++;
            }
        }
    }
    
    // cleanup
    dav_resource_free(res);
    
    return ret;
}

int sync_update_tags(SyncDirectory *dir, DavSession *sn, DavResource *res, LocalResource *local) {
    if(!dir->tagconfig || !local->tags_updated) {
        return 0;
    }
    
    // get local tags
    UcxList *tags = sync_get_file_tags(dir, local, NULL);
    
    DavXmlNode *prop = create_xml_taglist(tags);
    if(prop) {
        dav_set_property_ns(res, DAV_NS, "tags", prop);
    } else {
        dav_remove_property_ns(res, DAV_NS, "tags");
    }
    
    printf("update: %s\n", local->path);
    if(dav_store(res)) {
        print_resource_error(sn, local->path);
    } else {
        UcxBuffer *tag_data = local->cached_tags;
        if(local->tags_hash) {
            free(local->tags_hash);
            local->tags_hash = NULL;
        }
        if(tag_data) {
            char *hash = dav_create_hash(tag_data->space, tag_data->size);
            local->tags_hash = hash;
        }
        local->tags_updated = FALSE;
    }
    
    // TODO: free stuff
    return 0;
}


void remove_deleted_conflicts(SyncDirectory *dir, SyncDatabase *db) {
    char **dc = calloc(sizeof(void*), db->conflict->count);
    int numdc = 0;
    
    UcxMapIterator i = ucx_map_iterator(db->conflict);
    LocalResource *res;
    UCX_MAP_FOREACH(key, res, i) {
        char *path = util_concat_path(dir->path, res->path);
        struct stat s;
        if(stat(path, &s)) {
            if(errno == ENOENT) {
                dc[numdc] = res->path;
                numdc++;
            } else {
                fprintf(stderr, "Cannot stat file: %s\n", path);
                perror("");
            }
        }
        free(path);
    }
    
    for(int i=0;i<numdc;i++) {
        ucx_map_cstr_remove(db->conflict, dc[i]);
    }
    
    free(dc);
}

static void resolve_skipped(SyncDatabase *db) {
    UcxKey k;
    LocalResource *res;
    UcxMapIterator i = ucx_map_iterator(db->resources);
    int skipped = 0;
    UCX_MAP_FOREACH(k, res, i) {
        if(res->skipped) {
            skipped++;
            fprintf(stderr, "skipped from push: %s\n", res->path);
        }
    }
    if(skipped > 0) {
        fprintf(stderr,
                "  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) {
        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;
    }
    
    resolve_skipped(db);
    
    int ret = 0;
    
    // remove conflicts
    int num_conflict = db->conflict->count;
    ucx_map_free_content(db->conflict, (ucx_destructor)local_resource_free);
    ucx_map_clear(db->conflict);
    
    // store db
    if(store_db(db, dir->database)) {
        fprintf(stderr, "Cannot store sync db\n");
        fprintf(stderr, "Abort\n");
        ret = -2;
    }
    
    // cleanup
    destroy_db(db);
    
    // Report
    if(ret != -2) {
        char *str_conflict = num_conflict == 1 ? "conflict" : "conflicts";
        printf("Result: %d %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;
    }
    
    SyncDatabase *db = load_db(dir->database);
    if(!db) {
        fprintf(stderr, "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
    UcxMapIterator i = ucx_map_iterator(db->conflict);
    LocalResource *res;
    UCX_MAP_FOREACH(key, res, i) {
        printf("delete: %s\n", res->path);
        char *path = util_concat_path(dir->path, res->path);
        if(unlink(path)) {
            if(errno != ENOENT) {
                perror("unlink");
                num_err++;
            }
        } else {
            num_del++;
        }
        free(path);
    }
    ucx_map_free_content(db->conflict, (ucx_destructor)local_resource_free);
    ucx_map_clear(db->conflict);
    
    // store db
    if(store_db(db, dir->database)) {
        fprintf(stderr, "Cannot store sync db\n");
        fprintf(stderr, "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";
        printf("Result: %d conflict %s deleted, %d %s\n",
                num_del, str_delete,
                num_err, str_error);
    }
    
    return ret;
}


// 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;
}

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) {
        printf("trash not configured for %s\n", syncdir->name);
        return 0;
    }
    
    DIR *dir = 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;
    struct dirent *ent;
    while((ent = readdir(dir)) != NULL) {
        if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
            continue;
        }
        
        char *path = util_concat_path(syncdir->trash, ent->d_name);
        
        struct stat s;
        if(stat(path, &s)) {
            perror("stat");
        } else {
            trashsize += s.st_size;
        }
        count++;
        
        free(path);
    }
    closedir(dir);
    
    printf("path: %s\n", syncdir->trash);
    printf("%d %s\n", count, count == 1 ? "file" : "files");
    char *sizestr = size_str(trashsize);
    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(!syncdir->trash) {
        fprintf(stderr, "trash not configured for %s\n", syncdir->name);
        return -1;
    }
    
    DIR *dir = opendir(syncdir->trash);
    if(!dir) {
        fprintf(stderr, "cannot open trash directory: %s\n", syncdir->trash);
        perror("opendir");
        return -1;
    }
    
    struct dirent *ent;
    while((ent = readdir(dir)) != NULL) {
        if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
            continue;
        }
        
        char *path = util_concat_path(syncdir->trash, ent->d_name);
        printf("delete: %s\n", path);
        
        struct stat s;
        if(stat(path, &s)) {
            perror("stat");
            free(path);
            continue;
        }
        if(S_ISDIR(s.st_mode)) {
            if(rmdir(path)) {
                perror("rmdir");
            }
        } else {
            if(unlink(path)) {
                perror("unlink");
            }
        }
        
        free(path);
    }
    closedir(dir);
    
    return 0;
}

#define CMD_TAG_ADD    0
#define CMD_TAG_REMOVE 1
#define CMD_TAG_UPDATE 2
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_tagopt(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_tagopt(args, CMD_TAG_REMOVE);
}

int cmd_update_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_tagopt(args, CMD_TAG_UPDATE);
}

int cmd_tagopt(CmdArgs *args, int cmd) {
    SyncFile file;
    int ret = 0;
    char *path = args->argv[0];
    
    int err = sync_get_file(args, path, NULL, &file);
    if(err) {
        fprintf(stderr, "err: %d\n", err); // TODO: print nice err msg
        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 *localres = ucx_map_cstr_get(db->resources, file.path);
    UcxList *tags = NULL;
    DavBool store_tags = FALSE;
    
    if(cmd == CMD_TAG_ADD || cmd == CMD_TAG_REMOVE) {
        char *tag = args->argv[1];
        char *tagcolor = NULL; // TODO: get color

        tags = sync_get_file_tags(file.dir, localres, NULL);
        UcxList *x = NULL;
        UCX_FOREACH(elm, tags) {
            DavTag *t = elm->data;
            if(!strcmp(t->name, tag)) {
                x = elm;
                break;
            }
        }

        if(cmd == CMD_TAG_ADD) {
            if(!x) {
                DavTag *newtag = malloc(sizeof(DavTag));
                newtag->name = tag;
                newtag->color = tagcolor;
                tags = ucx_list_append(tags, newtag);
                store_tags = TRUE;
            }
        } else {
            if(tags) {
                tags = ucx_list_remove(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));
            // TODO: read from stdin if tags_str is "-"
        }
    }
    
    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;
        }
    }
    
    // store db
    if(store_db(db, file.dir->database)) {
        fprintf(stderr, "Cannot store sync db\n");
        ret = -2;
    }
    
    free(file.path);
    return ret;
}

static int isfileindir(SyncDirectory *dir, const char *path, SyncFile *f) {
    char *fullpath;
    if(path[0] != '/') {
        size_t wdlen = 1024;
        char *wd = malloc(1024);
        while(!getcwd(wd, wdlen)) {
            if(errno == ERANGE) {
                wdlen *= 2;
                wd = realloc(wd, wdlen);
            } else {
                free(wd);
                return 0;
            }
        }
        
        fullpath = util_concat_path(wd, path);
    } else {
        fullpath = strdup(path);
    }
    
    // TODO: normalize path
    
    if(!sstrprefix(sstr((char*)fullpath), sstr(dir->path))) {
        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, const char *dir, SyncFile *f) {
    struct stat s;
    if(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;
        
        UcxMapIterator i = scfg_directory_iterator();
        UcxKey k;
        SyncDirectory *dir;
        UCX_MAP_FOREACH(key, dir, i) {
            if(isfileindir(dir, path, f)) {
                if(target) {
                    return 5;
                } else {
                    target = dir;
                }
            }
        }
        
        if(!target) {
            return 4;
        }
    }
    
    return 0;
}


int cmd_add_directory(CmdArgs *args) {
    if(!get_repositories()) {
        fprintf(stderr, "No repositories available. Run 'dav add-repository' first.\n");
        fprintf(stderr, "Abort\n");
        return -1;
    }
    
    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;
    }
    
    printf("Enter local directory path.\n");
    char *path = assistant_getcfg("path");
    if(!path) {
        fprintf(stderr, "Abort\n");
        return -1;
    }
    
    printf("Specify webdav repository.\n");
    UcxList *repos = get_repositories();
    int i = 0;
    UCX_FOREACH(elm, repos) {
        Repository *r = elm->data;
        printf("%d) %s\n", i, r->name);
        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;
        }
        UcxList *elm = ucx_list_get(repos, reponum);
        if(elm) {
            Repository *r = elm->data;
            reponame = r->name;
        } else {
            fprintf(stderr, "Wrong input.\nAbort\n");
            return -1;
        }
    } else {
        if(get_repository(sstr(repository))) {
            reponame = repository;
        } else {
            fprintf(stderr, "Repository %s doesn't exist.\nAbort\n", repository);
            return -1;
        }
    }
    
    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 {
        printf("\nAdded directory: %s (%s)\n", name, path);
    }
    
    free(name);
    free(path);
    free(repository);
    free(collection);
    free(db);
    
    return ret;
}

int cmd_list_dirs() {
    UcxMapIterator iter = scfg_directory_iterator();
    SyncDirectory *dir;
    UCX_MAP_FOREACH(key, dir, iter) {
        printf("%s\n", dir->name);
    }
    return 0;
}

int cmd_check_repositories() {
    int ret = EXIT_SUCCESS;

    UcxList *reponames = NULL;
    {
        UcxMapIterator iter = scfg_directory_iterator();
        SyncDirectory *dir;
        UCX_MAP_FOREACH(key, dir, iter) {
            reponames = ucx_list_append_once(reponames,
                    dir->repository, ucx_strcmp, NULL);
        }
    }

    UCX_FOREACH(listelem, reponames) {
        char *reponame = listelem->data;
        printf("Checking %s... ", reponame);
        Repository* repo = get_repository(sstr(reponame));
        if (!repo) {
            printf(" not found in config.xml!\n");
            ret = EXIT_FAILURE;
        } else {
            DavSession *sn = create_session(ctx, repo, repo->url);
            if (sn) {
                DavResource *res = dav_query(sn,
                        "select - from / with depth = 0");
                if (res) {
                    printf("OK.\n");
                    dav_resource_free(res);
                } else {
                    printf("unavailable!\n");
                    ret = EXIT_FAILURE;
                }
                dav_session_destroy(sn);
            } else {
                printf("cannot create session!\n");
                ret = EXIT_FAILURE;
            }
        }
    }
    
    ucx_list_free(reponames);
    
    return ret;
}

char* create_locktoken_file(const char *syncdirname, const char *locktoken) {
    sstr_t fname = ucx_sprintf("locktoken-%s.txt", syncdirname);
    char *path = config_file_path(fname.ptr);
    free(fname.ptr);
    
    FILE *file = fopen(path, "w");
    if(file) {
        fprintf(file, "%s\n", locktoken);
        fclose(file);
        return path;
    } else {
        perror("Cannot create locktoken file");
        free(path);
        return NULL;
    }
}

mercurial