UNIXworkcode

/* * 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 "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, "%""-%s"PRId64, 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); } }