download/upload refactoring, add cancel button, resolves #498 fixes #506

Sun, 17 Nov 2024 15:19:32 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 17 Nov 2024 15:19:32 +0100
changeset 87
5360027fb282
parent 86
8e7c57c23133
child 88
e27526429d85

download/upload refactoring, add cancel button, resolves #498 fixes #506

application/Makefile file | annotate | diff | comparison | revisions
application/davcontroller.c file | annotate | diff | comparison | revisions
application/davcontroller.h file | annotate | diff | comparison | revisions
application/download.c file | annotate | diff | comparison | revisions
application/download.h file | annotate | diff | comparison | revisions
application/upload.c file | annotate | diff | comparison | revisions
application/upload.h file | annotate | diff | comparison | revisions
application/window.c file | annotate | diff | comparison | revisions
application/window.h file | annotate | diff | comparison | revisions
libidav/methods.c file | annotate | diff | comparison | revisions
make/vs/idav/idav.vcxproj file | annotate | diff | comparison | revisions
make/vs/idav/idav.vcxproj.filters file | annotate | diff | comparison | revisions
ui/common/context.c file | annotate | diff | comparison | revisions
ui/gtk/toolkit.c file | annotate | diff | comparison | revisions
ui/ui/toolkit.h file | annotate | diff | comparison | revisions
--- a/application/Makefile	Fri Nov 15 21:50:20 2024 +0100
+++ b/application/Makefile	Sun Nov 17 15:19:32 2024 +0100
@@ -35,6 +35,8 @@
 SRC += application.c
 SRC += config.c
 SRC += davcontroller.c
+SRC += download.c
+SRC += upload.c
 SRC += system.c
 SRC += window.c
 SRC += settings.c
--- a/application/davcontroller.c	Fri Nov 15 21:50:20 2024 +0100
+++ b/application/davcontroller.c	Sun Nov 17 15:19:32 2024 +0100
@@ -32,6 +32,8 @@
 #include <cx/printf.h>
 
 #include "config.h"
+#include "upload.h"
+#include "download.h"
 
 #include "system.h"
 #include "common/context.h"
@@ -442,726 +444,28 @@
 }
 
 
-// ------------------------------------- File Upload -------------------------------------
-
-typedef struct DavFileUpload {
-    UiObject *ui;
-    DavBrowser *browser;
-    DavSession *sn;
-    UiFileList files;
-    char *base_path;
-    UiThreadpool *queue;
-
-    size_t total_bytes;
-    size_t total_files;
-    size_t total_directories;
-    size_t uploaded_bytes;
-    size_t uploaded_files;
-    size_t uploaded_directories;
-
-    DavBool upload_file;
-    size_t current_file_size;
-    size_t current_file_upload;
-
-    char *current_file_name;
-
-    UiObject *dialog;
-    UiDouble *progress;
-    UiString *label_top_left;
-    UiString *label_top_right;
-    UiString *label_bottom_left;
-    UiString *label_bottom_right;
-
-    // The collection, the files are uploaded to
-    // It is only safe to access the collection ptr, if
-    // collection == browser->current && collection_ctn == browser->res_counter
-    // and obviously it can only be accessed from the UI thread
-    DavResource *collection;
-
-    // copy of browser->res_counter, used for integrity check
-    int64_t collection_ctn;
-
-    // current uploaded resource, created as part of the browser session
-    // only if collection == browser->current && collection_ctn == browser->res_counter
-    DavResource *current_resource;
-} DavFileUpload;
-
-typedef struct DUFile {
-    char *path;
-    char *upload_path;
-    size_t bytes;
-    DavBool isdirectory;
-    DavFileUpload *upload;
-    DavError error;
-} DUFile;
-
-static double upload_progress(DavFileUpload *upload) {
-    return ((double)upload->uploaded_bytes / (double)upload->total_bytes) * 100;
-}
-
-static void update_upload_labels(DavFileUpload *upload) {
-    char *sz_total = util_size_str(FALSE, upload->total_bytes);
-    char *sz_uploaded = util_size_str2(FALSE, upload->uploaded_bytes, upload->total_bytes, 2);
-    char *sz_uploaded_end = strchr(sz_uploaded, ' ');
-    if (sz_uploaded_end) {
-        *sz_uploaded_end = 0;
-    }
-
-    double progress = upload_progress(upload);
-    ui_set(upload->progress, upload_progress(upload));
-
-    cxmutstr label1;
-    if (upload->total_files + upload->total_directories > 1) {
-        label1 = cx_asprintf(
-            "%s/%s   %zu/%zu files",
-            sz_uploaded,
-            sz_total,
-            upload->uploaded_files+upload->uploaded_directories,
-            upload->total_files+upload->total_directories);
-    } else {
-        label1 = cx_asprintf(
-            "%s/%s",
-            sz_uploaded,
-            sz_total);
-    }
-    ui_set(upload->label_top_left, label1.ptr);
-
-    free(sz_total);
-    free(label1.ptr);
-
-    if (upload->current_file_size > 0) {
-        cxmutstr file_label = cx_asprintf("%s (%.0f%%)", upload->current_file_name, ((float)upload->current_file_upload/(float)upload->current_file_size)*100);
-        ui_set(upload->label_top_right, file_label.ptr);
-        free(file_label.ptr);
-    }
-}
-
-static int uithr_update_upload_labels(void *data) {
-    update_upload_labels(data);
-    return 0;
-}
-
-static void upload_dav_progress(DavResource *res, int64_t total, int64_t now, void *data) {
-    DavFileUpload *upload = data;
-    if (upload->upload_file) {
-        if (now > upload->current_file_size) {
-            // current_file_size is not accurate (either the file was changed after the last stat
-            // or we have some extra bytes because of encryption
-            // adjust current_file_size and the total upload size
-            int64_t extra = now - upload->current_file_size;
-            upload->current_file_size += extra;
-            upload->total_bytes += extra;
-        }
-
-        int64_t new_progress = now - upload->current_file_upload;
-        upload->uploaded_bytes += new_progress;
-        upload->current_file_upload = now;
-
-        ui_call_mainthread(uithr_update_upload_labels, upload);
-    }
-}
-
-
-typedef struct FileNameUpdate {
-    DavFileUpload *upload;
-    char *name;
-    char *path;
-    DavBool iscollection;
-} FileNameUpdate;
-
-static int uithr_update_file_label(FileNameUpdate *update) {
-    // replace upload->current_filename with update->name
-    if (update->upload->current_file_name) {
-        free(update->upload->current_file_name);
-    }
-    update->upload->current_file_name = update->name;
-
-    ui_set(update->upload->label_top_right, update->name);
-
-    DavFileUpload *upload = update->upload;
-    DavBrowser *browser = upload->browser;
-    // update the resource list in the browser, if the current collection has not changed
-    if (upload->collection == browser->current && upload->collection_ctn == browser->res_counter) {
-        char *parent = util_parent_path(update->path);
-        cxstring parent_s = cx_str(parent);
-        cxstring colpath_s = cx_str(upload->collection->path);
-        if (parent_s.length > 0 && parent_s.ptr[parent_s.length - 1] == '/') {
-            parent_s.length--;
-        }
-        if (colpath_s.length > 0 && colpath_s.ptr[colpath_s.length - 1] == '/') {
-            colpath_s.length--;
-        }
-
-        // only update, if the added resource has the current collection as parent
-        if (!cx_strcmp(parent_s, colpath_s)) {
-            DavResource *ui_res = dav_resource_new(upload->collection->session, update->path);
-            ui_res->iscollection = update->iscollection;
-            ui_res->lastmodified = time(NULL);
-            ui_res->creationdate = time(NULL);
-            upload->current_resource = ui_res;
-
-            ui_list_append(browser->resources, ui_res);
-            browser->resources->update(browser->resources, 0);
-        } else {
-            upload->current_resource = NULL; // maybe not necessary
-        }
-        free(parent);
-    }
-
-    free(update->path);
-    free(update);
-    return 0;
-}
-
-static int qthr_file_upload(void *data) {
-    DUFile *f = data;
-    DavFileUpload *upload = f->upload;
-    DavSession *sn = upload->sn;
-
-    FILE *in = sys_fopen(f->path, "rb");
-    if (!in) {
-        // TODO: error msg
-        return 0;
-    }
-
-    upload->upload_file = TRUE;
-    upload->current_file_size = f->bytes;
-    upload->current_file_upload = 0;
-
-    DavResource *res = dav_resource_new(sn, f->upload_path);
-
-    FileNameUpdate *ui_update = malloc(sizeof(FileNameUpdate));
-    ui_update->upload = upload;
-    ui_update->name = strdup(res->name);
-    ui_update->path = strdup(res->path);
-    ui_update->iscollection = FALSE;
-    ui_call_mainthread((ui_threadfunc)uithr_update_file_label, ui_update);
-
-    dav_set_content(res, in, (dav_read_func)fread, (dav_seek_func)fseek);
-    if (dav_store(res)) {
-        f->error = sn->error;
-    }
-    dav_resource_free(res);
-
-    fclose(in);
-
-    upload->upload_file = FALSE;
-
-    return 0;
-}
-
-static void uithr_file_uploaded(UiEvent *event, void *data) {
-    DUFile *file = data;
-    DavFileUpload *upload = file->upload;
-
-    upload->uploaded_files++;
-    //upload->uploaded_bytes += file->bytes;
-
-    double progress = upload_progress(upload);
-    ui_set(upload->progress, upload_progress(upload));
-
-    update_upload_labels(upload);
-
-    // update resource content length in the browser 
-    DavBrowser *browser = upload->browser;
-    if (upload->collection == browser->current && upload->collection_ctn == browser->res_counter) {
-        if (upload->current_resource) {
-            upload->current_resource->contentlength = upload->current_file_upload;
-            browser->resources->update(browser->resources, 0);
-        }
-    }
-    upload->current_resource = NULL;
-
-    free(file->path);
-    free(file->upload_path);
-}
-
-static int qthr_dir_upload(void *data) {
-    DUFile *f = data;
-    DavFileUpload *upload = f->upload;
-    DavSession *sn = upload->sn;
-
-    DavResource *res = dav_resource_new(sn, f->upload_path);
-    res->iscollection = TRUE;
-
-    FileNameUpdate *ui_update = malloc(sizeof(FileNameUpdate));
-    ui_update->upload = upload;
-    ui_update->name = strdup(res->name);
-    ui_update->path = strdup(res->path);
-    ui_update->iscollection = TRUE;
-    ui_call_mainthread((ui_threadfunc)uithr_update_file_label, ui_update);
-
-    if (dav_create(res)) {
-        f->error = sn->error;
-    }
-
-    dav_resource_free(res);
-
-    return 0;
-}
-
-static void uithr_dir_uploaded(UiEvent *event, void *data) {
-    DUFile *file = data;
-    DavFileUpload *upload = file->upload;
-
-    upload->uploaded_directories++;
-
-    update_upload_labels(upload);
-
-    upload->current_resource = NULL;
-
-    free(file->path);
-    free(file->upload_path);
-}
-
-static int qthr_upload_finished(void *data) {
-    return 0;
-}
-
-static void uithr_upload_finished(UiEvent *event, void *data) {
-    DavFileUpload *upload = data;
-    ui_threadpool_destroy(upload->queue);
-
-    free(upload->base_path);
-    dav_session_destroy(upload->sn);
-}
-
-static int jobthr_upload_scan(void *data) {
-    DavFileUpload *upload = data;
-
-    CxList *stack = cxLinkedListCreateSimple(CX_STORE_POINTERS);
-    for (int i = 0; i < upload->files.nfiles; i++) {
-        DUFile *f = malloc(sizeof(DUFile));
-        f->path = strdup(upload->files.files[i]);
-        f->upload_path = util_concat_path(upload->base_path, util_path_file_name(f->path));
-        f->isdirectory = FALSE;
-        f->bytes = 0;
-        f->upload = upload;
-        f->error = 0;
-        cxListInsert(stack, 0, f);
-    }
-
-    while (cxListSize(stack) > 0) {
-        DUFile *f = cxListAt(stack, 0);
-
-        char *path = util_concat_path(upload->base_path, f->upload_path);
-        cxListRemove(stack, 0);
-
-        SYS_STAT s;
-        if (!sys_stat(f->path, &s)) {
-            if (S_ISDIR(s.st_mode)) {
-                f->isdirectory = TRUE;
-                upload->total_directories++;
-                ui_threadpool_job(upload->queue, upload->ui, qthr_dir_upload, f, uithr_dir_uploaded, f);
-
-                SYS_DIR dir = sys_opendir(f->path);
-                if (dir) {
-                    SysDirEnt *entry;
-                    int nument = 0;
-                    while((entry = sys_readdir(dir)) != NULL) {
-                        if(!strcmp(entry->name, ".") || !strcmp(entry->name, "..")) {
-                            continue;
-                        }
-
-                        cxmutstr newpath = util_concat_sys_path(cx_str(f->path), cx_str(entry->name));
-                        char *new_upload_path = util_concat_path(f->upload_path, entry->name);
-
-                        DUFile *child = malloc(sizeof(DUFile));
-                        child->path = newpath.ptr;
-                        child->upload_path = new_upload_path;
-                        child->isdirectory = FALSE;
-                        child->bytes = 0;
-                        child->upload = upload;
-                        child->error = 0;
-                        cxListAdd(stack, child);
-                    }
-
-                    sys_closedir(dir);
-                }
-            } else if (S_ISREG(s.st_mode)) {
-                f->isdirectory = FALSE;
-                f->bytes = s.st_size;
-                upload->total_files++;
-                upload->total_bytes += s.st_size;
-                ui_threadpool_job(upload->queue, upload->ui, qthr_file_upload, f, uithr_file_uploaded, f);
-            }
-        }
-    } // TODO: else error msg
-
-    ui_threadpool_job(upload->queue, upload->ui, qthr_upload_finished, upload, uithr_upload_finished, upload);
-
-    ui_filelist_free(upload->files);
-
-    return 0;
-}
-
-static void uithr_upload_scan_finished(UiEvent *event, void *data) {
-    DavFileUpload *upload = data;
-
-    update_upload_labels(upload);
-}
-
-static void upload_window_closed(UiEvent *event, void *data) {
-    // noop, prevents context destruction
-}
-
 void davbrowser_upload_files(UiObject *ui, DavBrowser *browser, UiFileList files) {
     if (!browser->sn) {
         return; // TODO: error msg
     }
-
-    // we need a clone of the current session, because the upload
-    // is done in a separate thread
-    DavSession *upload_session = dav_session_clone(browser->sn);
-
-    // create upload obj, that contains all relevant data for the upload
-    DavFileUpload *upload = malloc(sizeof(DavFileUpload));
-    memset(upload, 0, sizeof(DavFileUpload));
-
-    dav_session_set_progresscallback(upload_session, NULL, upload_dav_progress, upload);
-
-    upload->ui = ui;
-    upload->browser = browser;
-    upload->sn = upload_session;
-    upload->files = files;
-    upload->base_path = strdup(browser->current->path);
-    upload->queue = ui_threadpool_create(1);
-
-    upload->collection = browser->current;
-    upload->collection_ctn = browser->res_counter;
-
-    // create upload progress window
+    
     cxmutstr wtitle = cx_asprintf("Upload to: %s", ui_get(browser->path));
-    UiObject *dialog = ui_simple_window(wtitle.ptr, upload);
-    ui_context_closefunc(dialog->ctx, upload_window_closed, NULL);
+    UiObject *dialog = ui_simple_window(wtitle.ptr, NULL);
     free(wtitle.ptr);
-    upload->dialog = dialog;
-    ui_window_size(dialog, 550, 120);
-    upload->progress = ui_double_new(dialog->ctx, NULL);
-    upload->label_top_left = ui_string_new(dialog->ctx, NULL);
-    upload->label_top_right = ui_string_new(dialog->ctx, NULL);
-    upload->label_bottom_left = ui_string_new(dialog->ctx, NULL);
-    upload->label_bottom_right = ui_string_new(dialog->ctx, NULL);
-
-    ui_grid(dialog, .margin = 10, .spacing = 10, .fill = TRUE) {
-        ui_llabel(dialog, .value = upload->label_top_left, .hexpand = TRUE);
-        ui_rlabel(dialog, .value = upload->label_top_right);
-        ui_newline(dialog);
-
-        ui_progressbar(dialog, .value = upload->progress, .colspan = 2, .hexpand = TRUE);
-        ui_newline(dialog);
-
-        ui_llabel(dialog, .value = upload->label_bottom_left);
-        ui_rlabel(dialog, .value = upload->label_bottom_right);
-        ui_newline(dialog);
-    }
-
-    ui_set(upload->label_top_left, "");
-    ui_set(upload->label_top_right, "");
-    ui_set(upload->label_bottom_left, "");
-    ui_set(upload->label_bottom_right, "");
-    ui_set(upload->progress, 0);
-
-    ui_show(dialog);
-
-    // start upload and stat threads
-    ui_job(ui, jobthr_upload_scan, upload, uithr_upload_scan_finished, upload);
-}
-
-
-// ------------------------------------- File Download -------------------------------------
-
-typedef struct DavFileDownload {
-    UiObject *ui;
-    DavBrowser *browser;
-
-    DavSession *sn;
-    DavSession *download_sn;
-    DavResource *reslist;
-    char *local_path;
-    DavBool isdirectory;
-
-    UiThreadpool *queue;
 
-    size_t total_bytes;
-    size_t total_files;
-    size_t total_directories;
-    size_t downloaded_bytes;
-    size_t downloaded_files;
-    size_t downloaded_directories;
-
-    size_t current_file_size;
-    size_t current_file_downloaded;
-
-    UiObject *dialog;
-    UiDouble *progress;
-    UiString *label_top_left;
-    UiString *label_top_right;
-    UiString *label_bottom_left;
-    UiString *label_bottom_right;
-} DavFileDownload;
-
-
-static int uithr_download_update_progress(void *data) {
-    DavFileDownload *download = data;
-    char *sz_total = util_size_str(FALSE, download->total_bytes);
-    char *sz_downloaded = util_size_str2(FALSE, download->downloaded_bytes, download->total_bytes, 2);
-    char *sz_downloaded_end = strchr(sz_downloaded, ' ');
-    if (sz_downloaded_end) {
-        *sz_downloaded_end = 0;
-    }
-    
-    if (download->total_bytes > 0) {
-        double progress = (double)download->downloaded_bytes / (double)download->total_bytes;
-        ui_set(download->progress, progress*100);
-    }
-    
-
-    cxmutstr label1;
-    if (download->total_files + download->total_directories > 1) {
-        label1 = cx_asprintf(
-            "%s/%s   %zu/%zu files",
-            sz_downloaded,
-            sz_total,
-            download->downloaded_files+download->downloaded_directories,
-            download->total_files+download->total_directories);
-    } else {
-        label1 = cx_asprintf(
-            "%s/%s",
-            sz_downloaded,
-            sz_total);
-    }
-    ui_set(download->label_top_left, label1.ptr);
-
-    free(sz_total);
-    free(label1.ptr);
-    
-
-    return 1;
-}
-
-// dav download file
-typedef struct DDFile {
-    DavFileDownload *download;
-    size_t size;
-    char *path;
-    char *to;
-    FILE *fd;
-} DDFile;
-
-static size_t ddfile_write(const void *buf, size_t size, size_t count, void *stream) {
-    DDFile *file = stream;
-
-    size_t w = fwrite(buf, size, count, file->fd);
-    file->download->current_file_downloaded += w;
-
-    file->download->downloaded_bytes += w;
-
-    if (file->download->current_file_downloaded > file->download->current_file_size) {
-        size_t diff = file->download->current_file_downloaded - file->download->current_file_size;
-        file->download->current_file_size = file->download->current_file_downloaded;
-        file->download->total_bytes += diff;
-    }
-
-    ui_call_mainthread(uithr_download_update_progress, file->download);
-
-    return w;
+    DavFileUpload *upload = dav_upload_create(browser, dialog, files);
+    transfer_window_init(dialog, action_upload_cancel);
+    dav_upload_start(upload);
 }
 
-static int qthr_download_resource(void *data) {
-    DDFile *file = data;
-
-    file->download->current_file_downloaded = 0;
-    file->download->current_file_size = file->size;
-
-    FILE *f = sys_fopen(file->to, "wb");
-    if (!f) {
-        return 0;
-    }
-    file->fd = f;
-
-    DavResource *res = dav_resource_new(file->download->download_sn, file->path);
-    dav_get_content(res, file, (dav_write_func)ddfile_write);
-
-    file->download->downloaded_files++;
-
-    ui_call_mainthread(uithr_download_update_progress, file->download);
-
-    dav_resource_free(res);
-
-    fclose(f);
-
-    free(file->path);
-    free(file->to);
-    free(file);
-
-    return 0;
-}
-
-static int qthr_download_finished(void *data) {
-    return 0;
-}
-
-static void uithr_download_finished(UiEvent *event, void *data) {
-    DavFileDownload *download = data;
-    printf("download finished\n");
-    ui_object_unref(download->dialog);
-}
-
-
-typedef struct DlStackElm {
-    DavResource *resource;
-    char *sub_path;
-} DlStackElm;
-
-static int jobthr_download_scan(void *data) {
-    DavFileDownload *download = data;
-    DavBrowser *browser = download->browser;
-
-    // check if the specified local location is a directory
-    SYS_STAT s;
-    if (!sys_stat(download->local_path, &s)) {
-        if (S_ISDIR(s.st_mode)) {
-            download->isdirectory = TRUE;
-        }
-    }
-
-    CxList *stack = cxLinkedListCreateSimple(sizeof(DlStackElm));
-
-    // add selected files to the download queue
-    DavResource *res = download->reslist;
-    while (res) {
-        DlStackElm elm;
-        elm.resource = res;
-        elm.sub_path = strdup(res->name);
-        cxListAdd(stack, &elm);
-
-        res = res->next;
-    }
-
-    while (cxListSize(stack) > 0) {
-        DlStackElm *elm = cxListAt(stack, 0);
-        DavResource *res = elm->resource;
-        char *sub_path = elm->sub_path;
-        cxListRemove(stack, 0);
-
-        if (res->iscollection) {
-            if (dav_load(res)) {
-                // TODO: handle error
-                continue;
-            }
-
-            // update ui
-            ui_call_mainthread(uithr_download_update_progress, download);
-            
-            char *path = util_concat_path(download->local_path, sub_path);
-            int err = sys_mkdir(path);
-            free(path);
-            if (err) {
-                // TODO: handle error
-            }
-
-            DavResource *child = res->children;
-            while (child) {
-                char *child_path = util_concat_path(sub_path, child->name);
-                DlStackElm childelm;
-                childelm.resource = child;
-                childelm.sub_path = child_path;
-                cxListAdd(stack, &childelm);
-
-                child = child->next;
-            }
-        } else {
-            // add the file to the download queue
-            DDFile *file = malloc(sizeof(DDFile));
-            file->download = download;
-            file->path = strdup(res->path);
-            file->size = res->contentlength;
-            if (download->isdirectory) {
-                file->to = util_concat_path(download->local_path, sub_path);
-            } else {
-                file->to = strdup(download->local_path);
-            }
-
-            // stats
-            download->total_files++;
-            download->total_bytes += res->contentlength;
-
-            // update ui
-            ui_call_mainthread(uithr_download_update_progress, download);
-
-            ui_threadpool_job(download->queue, download->ui, qthr_download_resource, file, NULL, NULL);
-        } 
-    }
+void davbrowser_download(UiObject *ui, DavBrowser *browser, DavResource *reslist, const char *local_path) {
+    cxmutstr wtitle = cx_asprintf("Download to: %s", local_path);
+    UiObject *dialog = ui_simple_window(wtitle.ptr, NULL);
+    free(wtitle.ptr);
     
-    ui_threadpool_job(download->queue, download->ui, qthr_download_finished, download, uithr_download_finished, download);
-
-    cxListDestroy(stack);
-
-    return 0;
-}
-
-static void uithr_download_scan_finished(UiEvent *event, void *data) {
-    DavFileDownload *download = data;
-
-}
-
-static void download_window_closed(UiEvent *event, void *data) {
-
-}
-
-
-void davbrowser_download(UiObject *ui, DavBrowser *browser, DavResource *reslist, const char *local_path) {
-    DavFileDownload *download = malloc(sizeof(DavFileDownload));
-    memset(download, 0, sizeof(DavFileDownload));
-
-    download->ui = ui;
-    download->browser = browser;
-    download->sn = reslist->session;
-    download->download_sn = dav_session_clone(download->sn);
-    download->reslist = reslist; // TODO: is this safe or do we need a copy?
-    download->local_path = strdup(local_path);
-
-    download->queue = ui_threadpool_create(1);
-
-    // create download progress window
-    cxmutstr wtitle = cx_asprintf("Download to: %s", local_path);
-    UiObject *dialog = ui_simple_window(wtitle.ptr, download);
-    ui_context_closefunc(dialog->ctx, download_window_closed, NULL);
-    free(wtitle.ptr);
-    download->dialog = dialog;
-    ui_window_size(dialog, 550, 120);
-    download->progress = ui_double_new(dialog->ctx, NULL);
-    download->label_top_left = ui_string_new(dialog->ctx, NULL);
-    download->label_top_right = ui_string_new(dialog->ctx, NULL);
-    download->label_bottom_left = ui_string_new(dialog->ctx, NULL);
-    download->label_bottom_right = ui_string_new(dialog->ctx, NULL);
-
-    ui_grid(dialog, .margin = 10, .spacing = 10, .fill = TRUE) {
-        ui_llabel(dialog, .value = download->label_top_left, .hexpand = TRUE);
-        ui_rlabel(dialog, .value = download->label_top_right);
-        ui_newline(dialog);
-
-        ui_progressbar(dialog, .value = download->progress, .min = 0, .max = 100, .colspan = 2, .hexpand = TRUE);
-        ui_newline(dialog);
-
-        ui_llabel(dialog, .value = download->label_bottom_left);
-        ui_rlabel(dialog, .value = download->label_bottom_right);
-        ui_newline(dialog);
-    }
-
-    ui_set(download->label_top_left, "");
-    ui_set(download->label_top_right, "");
-    ui_set(download->label_bottom_left, "");
-    ui_set(download->label_bottom_right, "");
-    ui_set(download->progress, 0);
-
-    ui_show(dialog);
-    
-    ui_object_ref(dialog);
-
-    // start upload and stat threads
-    ui_job(ui, jobthr_download_scan, download, uithr_download_scan_finished, download);
+    DavFileDownload *download = dav_download_create(browser, dialog, reslist, local_path);
+    transfer_window_init(dialog, action_download_cancel);
+    dav_download_start(download);
 }
 
 
--- a/application/davcontroller.h	Fri Nov 15 21:50:20 2024 +0100
+++ b/application/davcontroller.h	Sun Nov 17 15:19:32 2024 +0100
@@ -41,7 +41,17 @@
 
 #define DAVBROWSER_MAX_NAVLIST 128
     
+typedef struct TransferProgress {
+    size_t total_bytes;
+    size_t total_files;
+    size_t total_directories;
+    size_t transferred_bytes;
+    size_t transferred_files;
+    size_t transferred_directories;
 
+    size_t current_file_size;
+    size_t current_file_transferred;
+} TransferProgress;
 
 DavBrowser* davbrowser_create(UiObject *toplevel);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/application/download.c	Sun Nov 17 15:19:32 2024 +0100
@@ -0,0 +1,308 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 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 "download.h"
+
+#include "davcontroller.h"
+#include "window.h"
+
+#include <cx/printf.h>
+
+#include "config.h"
+
+#include "system.h"
+#include "common/context.h"
+
+#include <libidav/config.h>
+#include <libidav/utils.h>
+
+
+static int uithr_download_update_progress(void *data) {
+    DavFileDownload *download = data;
+    if(download->cancel) {
+        return 1;
+    }
+    
+    char *sz_total = util_size_str(FALSE, download->progress.total_bytes);
+    char *sz_downloaded = util_size_str2(FALSE, download->progress.transferred_bytes, download->progress.total_bytes, 2);
+    char *sz_downloaded_end = strchr(sz_downloaded, ' ');
+    if (sz_downloaded_end) {
+        *sz_downloaded_end = 0;
+    }
+    
+    if (download->progress.total_bytes > 0) {
+        double progress = (double)download->progress.transferred_bytes / (double)download->progress.total_bytes;
+        ui_set(download->progressbar, progress*100);
+    }
+    
+
+    cxmutstr label1;
+    if (download->progress.total_files + download->progress.total_directories > 1) {
+        label1 = cx_asprintf(
+            "%s/%s   %zu/%zu files",
+            sz_downloaded,
+            sz_total,
+            download->progress.transferred_files+download->progress.transferred_directories,
+            download->progress.total_files+download->progress.total_directories);
+    } else {
+        label1 = cx_asprintf(
+            "%s/%s",
+            sz_downloaded,
+            sz_total);
+    }
+    ui_set(download->label_top_left, label1.ptr);
+
+    free(sz_total);
+    free(label1.ptr);
+    
+
+    return 1;
+}
+
+static size_t ddfile_write(const void *buf, size_t size, size_t count, void *stream) {
+    DDFile *file = stream;
+    if(file->download->cancel) {
+        return 0;
+    }
+
+    size_t w = fwrite(buf, size, count, file->fd);
+    file->download->progress.current_file_transferred += w;
+
+    file->download->progress.transferred_bytes += w;
+
+    if (file->download->progress.current_file_transferred > file->download->progress.current_file_size) {
+        size_t diff = file->download->progress.current_file_transferred - file->download->progress.current_file_size;
+        file->download->progress.current_file_size = file->download->progress.current_file_transferred;
+        file->download->progress.total_bytes += diff;
+    }
+
+    ui_call_mainthread(uithr_download_update_progress, file->download);
+
+    return w;
+}
+
+static int qthr_download_resource(void *data) {
+    DDFile *file = data;
+    if(file->download->cancel) {
+        return 0;
+    }
+
+    file->download->progress.current_file_transferred = 0;
+    file->download->progress.current_file_size = file->size;
+
+    FILE *f = sys_fopen(file->to, "wb");
+    if (!f) {
+        return 0;
+    }
+    file->fd = f;
+
+    DavResource *res = dav_resource_new(file->download->download_sn, file->path);
+    dav_get_content(res, file, (dav_write_func)ddfile_write);
+
+    file->download->progress.transferred_files++;
+
+    ui_call_mainthread(uithr_download_update_progress, file->download);
+
+    dav_resource_free(res);
+
+    fclose(f);
+
+    free(file->path);
+    free(file->to);
+    free(file);
+
+    return 0;
+}
+
+static int qthr_download_finished(void *data) {
+    return 0;
+}
+
+static void uithr_download_finished(UiEvent *event, void *data) {
+    DavFileDownload *download = data;
+    if(download->cancel) {
+        ui_set(download->label_bottom_left, "Canceled");
+    }
+    if(download->dialog->ref > 1) {
+        ui_close(download->dialog);
+    }
+    ui_object_unref(download->dialog);
+}
+
+
+typedef struct DlStackElm {
+    DavResource *resource;
+    char *sub_path;
+} DlStackElm;
+
+static int jobthr_download_scan(void *data) {
+    DavFileDownload *download = data;
+
+    // check if the specified local location is a directory
+    SYS_STAT s;
+    if (!sys_stat(download->local_path, &s)) {
+        if (S_ISDIR(s.st_mode)) {
+            download->isdirectory = TRUE;
+        }
+    }
+
+    CxList *stack = cxLinkedListCreateSimple(sizeof(DlStackElm));
+
+    // add selected files to the download queue
+    DavResource *res = download->reslist;
+    while (res) {
+        DlStackElm elm;
+        elm.resource = res;
+        elm.sub_path = strdup(res->name);
+        cxListAdd(stack, &elm);
+
+        res = res->next;
+    }
+
+    while (cxListSize(stack) > 0 && !download->cancel) {
+        DlStackElm *elm = cxListAt(stack, 0);
+        DavResource *res = elm->resource;
+        char *sub_path = elm->sub_path;
+        cxListRemove(stack, 0);
+
+        if (res->iscollection) {
+            if (dav_load(res)) {
+                // TODO: handle error
+                continue;
+            }
+
+            // update ui
+            ui_call_mainthread(uithr_download_update_progress, download);
+            
+            char *path = util_concat_path(download->local_path, sub_path);
+            int err = sys_mkdir(path);
+            free(path);
+            if (err) {
+                // TODO: handle error
+            }
+
+            DavResource *child = res->children;
+            while (child) {
+                char *child_path = util_concat_path(sub_path, child->name);
+                DlStackElm childelm;
+                childelm.resource = child;
+                childelm.sub_path = child_path;
+                cxListAdd(stack, &childelm);
+
+                child = child->next;
+            }
+        } else {
+            // add the file to the download queue
+            DDFile *file = malloc(sizeof(DDFile));
+            file->download = download;
+            file->path = strdup(res->path);
+            file->size = res->contentlength;
+            if (download->isdirectory) {
+                file->to = util_concat_path(download->local_path, sub_path);
+            } else {
+                file->to = strdup(download->local_path);
+            }
+
+            // stats
+            download->progress.total_files++;
+            download->progress.total_bytes += res->contentlength;
+
+            // update ui
+            ui_call_mainthread(uithr_download_update_progress, download);
+
+            ui_threadpool_job(download->queue, download->dialog, qthr_download_resource, file, NULL, NULL);
+        } 
+    }
+    
+    ui_threadpool_job(download->queue, download->dialog, qthr_download_finished, download, uithr_download_finished, download);
+
+    cxListDestroy(stack);
+
+    return 0;
+}
+
+static void uithr_download_scan_finished(UiEvent *event, void *data) {
+    DavFileDownload *download = data;
+
+}
+
+static void download_window_closed(UiEvent *event, void *data) {
+    DavFileDownload *download = event->obj->window;
+    
+    dav_session_destroy(download->sn);
+    ui_threadpool_destroy(download->queue);
+}
+
+void action_download_cancel(UiEvent *event, void *data) {
+    DavFileDownload *download = event->window;
+    if(!download->cancel) {
+        ui_set(download->label_bottom_left, "Cancel...");
+        download->cancel = TRUE;
+    }
+}
+
+
+DavFileDownload* dav_download_create(DavBrowser *browser, UiObject *dialog, DavResource *reslist, const char *local_path) {
+    UiContext *ctx = dialog->ctx;
+    DavFileDownload *download = ui_malloc(ctx, sizeof(DavFileDownload));
+    memset(download, 0, sizeof(DavFileDownload));
+    download->dialog = dialog;
+    dialog->window = download;
+    ui_object_ref(dialog);
+    
+    download->browser = browser;
+    download->sn = reslist->session;
+    download->download_sn = dav_session_clone(download->sn);
+    download->reslist = reslist; // TODO: is this safe or do we need a copy?
+    download->local_path = ui_strdup(dialog->ctx, local_path);
+
+    download->queue = ui_threadpool_create(1);
+    
+    CxMempool *mp = ui_cx_mempool(ctx);
+    cxMempoolRegister(mp, download->download_sn, (cx_destructor_func)dav_session_destroy);
+    cxMempoolRegister(mp, download->queue, (cx_destructor_func)ui_threadpool_destroy);
+    
+    download->progressbar = ui_double_new(ctx, "progressbar");
+    download->label_top_left = ui_string_new(ctx, "label_top_left");
+    download->label_top_right = ui_string_new(ctx, "label_top_right");
+    download->label_bottom_left = ui_string_new(ctx, "label_bottom_left");
+    download->label_bottom_right = ui_string_new(ctx, "label_bottom_right");
+    
+    ui_set(download->label_top_left, "");
+    ui_set(download->label_top_right, "");
+    ui_set(download->label_bottom_left, "");
+    ui_set(download->label_bottom_right, "");
+    ui_set(download->progressbar, 0);
+    
+    return download;
+}
+
+void dav_download_start(DavFileDownload *download) {
+    ui_show(download->dialog);
+    ui_job(download->dialog, jobthr_download_scan, download, uithr_download_scan_finished, download);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/application/download.h	Sun Nov 17 15:19:32 2024 +0100
@@ -0,0 +1,83 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 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.
+ */
+
+#ifndef DOWNLOAD_H
+#define DOWNLOAD_H
+
+#include "application.h"
+#include "davcontroller.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+typedef struct DavFileDownload {
+    DavBrowser *browser;
+
+    DavSession *sn;
+    DavSession *download_sn;
+    DavResource *reslist;
+    char *local_path;
+    DavBool isdirectory;
+
+    UiThreadpool *queue;
+    
+    TransferProgress progress;
+
+    UiObject *dialog;
+    UiDouble *progressbar;
+    UiString *label_top_left;
+    UiString *label_top_right;
+    UiString *label_bottom_left;
+    UiString *label_bottom_right;
+    
+    DavBool cancel;
+} DavFileDownload;
+
+// dav download file
+typedef struct DDFile {
+    DavFileDownload *download;
+    size_t size;
+    char *path;
+    char *to;
+    FILE *fd;
+} DDFile;
+
+DavFileDownload* dav_download_create(DavBrowser *browser, UiObject *dialog, DavResource *reslist, const char *local_path);
+
+void dav_download_start(DavFileDownload *download);
+
+void action_download_cancel(UiEvent *event, void *data);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* DOWNLOAD_H */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/application/upload.c	Sun Nov 17 15:19:32 2024 +0100
@@ -0,0 +1,441 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 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 "upload.h"
+
+#include "davcontroller.h"
+#include "window.h"
+
+#include <cx/printf.h>
+
+#include "config.h"
+
+#include "system.h"
+#include "common/context.h"
+
+#include <libidav/config.h>
+#include <libidav/utils.h>
+
+// ------------------------------------- File Upload -------------------------------------
+
+static double upload_progress(DavFileUpload *upload) {
+    return ((double)upload->progress.transferred_bytes / (double)upload->progress.total_bytes) * 100;
+}
+
+static void update_upload_labels(DavFileUpload *upload) {
+    char *sz_total = util_size_str(FALSE, upload->progress.total_bytes);
+    char *sz_uploaded = util_size_str2(FALSE, upload->progress.transferred_bytes, upload->progress.total_bytes, 2);
+    char *sz_uploaded_end = strchr(sz_uploaded, ' ');
+    if (sz_uploaded_end) {
+        *sz_uploaded_end = 0;
+    }
+
+    double progress = upload_progress(upload);
+    ui_set(upload->progressbar, progress);
+
+    cxmutstr label1;
+    if (upload->progress.total_files + upload->progress.total_directories > 1) {
+        label1 = cx_asprintf(
+            "%s/%s   %zu/%zu files",
+            sz_uploaded,
+            sz_total,
+            upload->progress.transferred_files+upload->progress.transferred_directories,
+            upload->progress.total_files+upload->progress.total_directories);
+    } else {
+        label1 = cx_asprintf(
+            "%s/%s",
+            sz_uploaded,
+            sz_total);
+    }
+    ui_set(upload->label_top_left, label1.ptr);
+
+    free(sz_total);
+    free(label1.ptr);
+
+    if (upload->progress.current_file_size > 0) {
+        cxmutstr file_label = cx_asprintf("%s (%.0f%%)", upload->current_file_name, ((float)upload->progress.current_file_transferred/(float)upload->progress.current_file_size)*100);
+        ui_set(upload->label_top_right, file_label.ptr);
+        free(file_label.ptr);
+    }
+}
+
+
+
+static int uithr_update_upload_labels(void *data) {
+    update_upload_labels(data);
+    return 0;
+}
+
+static void upload_dav_progress(DavResource *res, int64_t total, int64_t now, void *data) {
+    DavFileUpload *upload = data;
+    if (upload->upload_file) {
+        if (now > upload->progress.current_file_size) {
+            // current_file_size is not accurate (either the file was changed after the last stat
+            // or we have some extra bytes because of encryption
+            // adjust current_file_size and the total upload size
+            int64_t extra = now - upload->progress.current_file_size;
+            upload->progress.current_file_size += extra;
+            upload->progress.total_bytes += extra;
+        }
+
+        int64_t new_progress = now - upload->progress.current_file_transferred;
+        upload->progress.transferred_bytes += new_progress;
+        upload->progress.current_file_transferred = now;
+
+        ui_call_mainthread(uithr_update_upload_labels, upload);
+    }
+}
+
+
+typedef struct FileNameUpdate {
+    DavFileUpload *upload;
+    char *name;
+    char *path;
+    DavBool iscollection;
+} FileNameUpdate;
+
+static int uithr_update_file_label(FileNameUpdate *update) {
+    if(update->upload->cancel) {
+        return 0;
+    }
+    
+    // replace upload->current_filename with update->name
+    if (update->upload->current_file_name) {
+        free(update->upload->current_file_name);
+    }
+    update->upload->current_file_name = update->name;
+
+    ui_set(update->upload->label_top_right, update->name);
+
+    DavFileUpload *upload = update->upload;
+    DavBrowser *browser = upload->browser;
+    // update the resource list in the browser, if the current collection has not changed
+    if (upload->collection == browser->current && upload->collection_ctn == browser->res_counter) {
+        char *parent = util_parent_path(update->path);
+        cxstring parent_s = cx_str(parent);
+        cxstring colpath_s = cx_str(upload->collection->path);
+        if (parent_s.length > 0 && parent_s.ptr[parent_s.length - 1] == '/') {
+            parent_s.length--;
+        }
+        if (colpath_s.length > 0 && colpath_s.ptr[colpath_s.length - 1] == '/') {
+            colpath_s.length--;
+        }
+
+        // only update, if the added resource has the current collection as parent
+        if (!cx_strcmp(parent_s, colpath_s)) {
+            DavResource *ui_res = dav_resource_new(upload->collection->session, update->path);
+            ui_res->iscollection = update->iscollection;
+            ui_res->lastmodified = time(NULL);
+            ui_res->creationdate = time(NULL);
+            upload->current_resource = ui_res;
+
+            ui_list_append(browser->resources, ui_res);
+            browser->resources->update(browser->resources, 0);
+        } else {
+            upload->current_resource = NULL; // maybe not necessary
+        }
+        free(parent);
+    }
+
+    free(update->path);
+    free(update);
+    return 0;
+}
+
+static size_t dufile_read(void *buf, size_t size, size_t count, void *stream) {
+    DUFile *f = (DUFile*)stream;
+    if(f->upload->cancel) {
+        return 0;
+    }
+    return fread(buf, size, count, f->fd);
+}
+
+static int dufile_seek(const void *stream, long off, int whence) {
+    DUFile *f = (DUFile*)stream;
+    return fseek(f->fd, off, whence);
+}
+
+static int qthr_file_upload(void *data) {
+    DUFile *f = data;
+    DavFileUpload *upload = f->upload;
+    DavSession *sn = upload->sn;
+    
+    if(upload->cancel) {
+        return 0;
+    }
+
+    FILE *in = sys_fopen(f->path, "rb");
+    if (!in) {
+        // TODO: error msg
+        return 0;
+    }
+    f->fd = in;
+
+    upload->upload_file = TRUE;
+    upload->progress.current_file_size = f->bytes;
+    upload->progress.current_file_transferred = 0;
+
+    DavResource *res = dav_resource_new(sn, f->upload_path);
+
+    FileNameUpdate *ui_update = malloc(sizeof(FileNameUpdate));
+    ui_update->upload = upload;
+    ui_update->name = strdup(res->name);
+    ui_update->path = strdup(res->path);
+    ui_update->iscollection = FALSE;
+    ui_call_mainthread((ui_threadfunc)uithr_update_file_label, ui_update);
+    
+    dav_set_content(res, f, (dav_read_func)dufile_read, (dav_seek_func)dufile_seek);
+    if (dav_store(res)) {
+        f->error = sn->error;
+    }
+    dav_resource_free(res);
+
+    fclose(in);
+
+    upload->upload_file = FALSE;
+
+    return 0;
+}
+
+static void uithr_file_uploaded(UiEvent *event, void *data) {
+    DUFile *file = data;
+    DavFileUpload *upload = file->upload;
+
+    upload->progress.transferred_files++;
+    //upload->uploaded_bytes += file->bytes;
+
+    double progress = upload_progress(upload);
+    ui_set(upload->progressbar, upload_progress(upload));
+
+    update_upload_labels(upload);
+
+    // update resource content length in the browser 
+    DavBrowser *browser = upload->browser;
+    if (upload->collection == browser->current && upload->collection_ctn == browser->res_counter) {
+        if (upload->current_resource) {
+            upload->current_resource->contentlength = upload->progress.current_file_transferred;
+            browser->resources->update(browser->resources, 0);
+        }
+    }
+    upload->current_resource = NULL;
+
+    free(file->path);
+    free(file->upload_path);
+}
+
+static int qthr_dir_upload(void *data) {
+    DUFile *f = data;
+    DavFileUpload *upload = f->upload;
+    DavSession *sn = upload->sn;
+
+    DavResource *res = dav_resource_new(sn, f->upload_path);
+    res->iscollection = TRUE;
+
+    FileNameUpdate *ui_update = malloc(sizeof(FileNameUpdate));
+    ui_update->upload = upload;
+    ui_update->name = strdup(res->name);
+    ui_update->path = strdup(res->path);
+    ui_update->iscollection = TRUE;
+    ui_call_mainthread((ui_threadfunc)uithr_update_file_label, ui_update);
+
+    if (dav_create(res)) {
+        f->error = sn->error;
+    }
+
+    dav_resource_free(res);
+
+    return 0;
+}
+
+static void uithr_dir_uploaded(UiEvent *event, void *data) {
+    DUFile *file = data;
+    DavFileUpload *upload = file->upload;
+
+    upload->progress.transferred_directories++;
+
+    update_upload_labels(upload);
+
+    upload->current_resource = NULL;
+
+    free(file->path);
+    free(file->upload_path);
+}
+
+static int qthr_upload_finished(void *data) {
+    return 0;
+}
+
+static void uithr_upload_finished(UiEvent *event, void *data) {
+    DavFileUpload *upload = data;
+    if(upload->cancel) {
+        ui_set(upload->label_bottom_left, "Canceled");
+    }
+    if(upload->dialog->ref > 1) {
+        ui_close(upload->dialog);
+    }
+    ui_object_unref(upload->dialog);
+}
+
+static int jobthr_upload_scan(void *data) {
+    DavFileUpload *upload = data;
+
+    CxList *stack = cxLinkedListCreateSimple(CX_STORE_POINTERS);
+    for (int i = 0; i < upload->files.nfiles; i++) {
+        DUFile *f = malloc(sizeof(DUFile));
+        f->path = strdup(upload->files.files[i]);
+        f->upload_path = util_concat_path(upload->base_path, util_path_file_name(f->path));
+        f->isdirectory = FALSE;
+        f->bytes = 0;
+        f->upload = upload;
+        f->error = 0;
+        cxListInsert(stack, 0, f);
+    }
+
+    while (cxListSize(stack) > 0 && !upload->cancel) {
+        DUFile *f = cxListAt(stack, 0);
+
+        char *path = util_concat_path(upload->base_path, f->upload_path);
+        cxListRemove(stack, 0);
+
+        SYS_STAT s;
+        if (!sys_stat(f->path, &s)) {
+            if (S_ISDIR(s.st_mode)) {
+                f->isdirectory = TRUE;
+                upload->progress.total_directories++;
+                ui_threadpool_job(upload->queue, upload->dialog, qthr_dir_upload, f, uithr_dir_uploaded, f);
+
+                SYS_DIR dir = sys_opendir(f->path);
+                if (dir) {
+                    SysDirEnt *entry;
+                    int nument = 0;
+                    while((entry = sys_readdir(dir)) != NULL) {
+                        if(!strcmp(entry->name, ".") || !strcmp(entry->name, "..")) {
+                            continue;
+                        }
+
+                        cxmutstr newpath = util_concat_sys_path(cx_str(f->path), cx_str(entry->name));
+                        char *new_upload_path = util_concat_path(f->upload_path, entry->name);
+
+                        DUFile *child = malloc(sizeof(DUFile));
+                        child->path = newpath.ptr;
+                        child->upload_path = new_upload_path;
+                        child->isdirectory = FALSE;
+                        child->bytes = 0;
+                        child->upload = upload;
+                        child->error = 0;
+                        cxListAdd(stack, child);
+                    }
+
+                    sys_closedir(dir);
+                }
+            } else if (S_ISREG(s.st_mode)) {
+                f->isdirectory = FALSE;
+                f->bytes = s.st_size;
+                upload->progress.total_files++;
+                upload->progress.total_bytes += s.st_size;
+                ui_threadpool_job(upload->queue, upload->dialog, qthr_file_upload, f, uithr_file_uploaded, f);
+            }
+        }
+    } // TODO: else error msg
+
+    ui_threadpool_job(upload->queue, upload->dialog, qthr_upload_finished, upload, uithr_upload_finished, upload);
+
+    ui_filelist_free(upload->files);
+
+    return 0;
+}
+
+static void uithr_upload_scan_finished(UiEvent *event, void *data) {
+    DavFileUpload *upload = data;
+
+    update_upload_labels(upload);
+}
+
+static void upload_window_closed(UiEvent *event, void *data) {
+    DavFileUpload *upload = event->window;
+    ui_threadpool_destroy(upload->queue);
+
+    dav_session_destroy(upload->sn);
+}
+
+void action_upload_cancel(UiEvent *event, void *data) {
+    DavFileUpload *upload = event->window;
+    if(!upload->cancel) {
+        ui_set(upload->label_bottom_left, "Cancel...");
+        upload->cancel = TRUE;
+    }
+}
+
+DavFileUpload* dav_upload_create(DavBrowser *browser, UiObject *obj, UiFileList files) {
+    UiContext *ctx = obj->ctx;
+    DavFileUpload *upload = ui_malloc(ctx, sizeof(DavFileUpload));
+    memset(upload, 0, sizeof(DavFileUpload));
+    upload->dialog = obj;
+    obj->window = upload;
+    ui_object_ref(obj);
+    
+    upload->progressbar = ui_double_new(ctx, "progressbar");
+    upload->label_top_left = ui_string_new(ctx, "label_top_left");
+    upload->label_top_right = ui_string_new(ctx, "label_top_right");
+    upload->label_bottom_left = ui_string_new(ctx, "label_bottom_left");
+    upload->label_bottom_right = ui_string_new(ctx, "label_bottom_right");
+    
+    // we need a clone of the current session, because the upload
+    // is done in a separate thread
+    DavSession *upload_session = dav_session_clone(browser->sn);
+    
+    dav_session_set_progresscallback(upload_session, NULL, upload_dav_progress, upload);
+
+    upload->browser = browser;
+    upload->sn = upload_session;
+    upload->files = files;
+    upload->base_path = ui_strdup(ctx, browser->current->path);
+    upload->queue = ui_threadpool_create(1);
+
+    upload->collection = browser->current;
+    upload->collection_ctn = browser->res_counter;
+    
+    CxMempool *mp = ui_cx_mempool(ctx);
+    cxMempoolRegister(mp, upload_session, (cx_destructor_func)dav_session_destroy);
+    cxMempoolRegister(mp, upload->queue, (cx_destructor_func)ui_threadpool_destroy);
+    
+    ui_set(upload->label_top_left, "");
+    ui_set(upload->label_top_right, "");
+    ui_set(upload->label_bottom_left, "");
+    ui_set(upload->label_bottom_right, "");
+    ui_set(upload->progressbar, 0);
+    
+    return upload;
+}
+
+void dav_upload_start(DavFileUpload *upload) {
+    ui_show(upload->dialog);
+
+    // start upload and stat threads
+    ui_job(upload->dialog, jobthr_upload_scan, upload, uithr_upload_scan_finished, upload);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/application/upload.h	Sun Nov 17 15:19:32 2024 +0100
@@ -0,0 +1,95 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 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.
+ */
+
+#ifndef UPLOAD_H
+#define UPLOAD_H
+
+#include "application.h"
+#include "davcontroller.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct DavFileUpload {
+    DavBrowser *browser;
+    DavSession *sn;
+    UiFileList files;
+    char *base_path;
+    UiThreadpool *queue;
+
+    TransferProgress progress;
+    DavBool upload_file;
+
+    char *current_file_name;
+
+    UiObject *dialog;
+    UiDouble *progressbar;
+    UiString *label_top_left;
+    UiString *label_top_right;
+    UiString *label_bottom_left;
+    UiString *label_bottom_right;
+
+    // The collection, the files are uploaded to
+    // It is only safe to access the collection ptr, if
+    // collection == browser->current && collection_ctn == browser->res_counter
+    // and obviously it can only be accessed from the UI thread
+    DavResource *collection;
+
+    // copy of browser->res_counter, used for integrity check
+    int64_t collection_ctn;
+
+    // current uploaded resource, created as part of the browser session
+    // only if collection == browser->current && collection_ctn == browser->res_counter
+    DavResource *current_resource;
+    
+    DavBool cancel;
+} DavFileUpload;
+
+typedef struct DUFile {
+    FILE *fd;
+    char *path;
+    char *upload_path;
+    size_t bytes;
+    DavBool isdirectory;
+    DavFileUpload *upload;
+    DavError error;
+} DUFile;
+
+DavFileUpload* dav_upload_create(DavBrowser *browser, UiObject *obj, UiFileList files);
+
+void dav_upload_start(DavFileUpload *upload);
+
+void action_upload_cancel(UiEvent *event, void *data);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UPLOAD_H */
+
--- a/application/window.c	Fri Nov 15 21:50:20 2024 +0100
+++ b/application/window.c	Sun Nov 17 15:19:32 2024 +0100
@@ -282,6 +282,30 @@
 }
 
 
+void transfer_window_init(UiObject *dialog, ui_callback btncallback) {
+    ui_window_size(dialog, 550, 120);
+    ui_grid(dialog, .margin = 10, .spacing = 10, .fill = TRUE) {
+        ui_llabel(dialog, .varname = "label_top_left", .hexpand = TRUE);
+        ui_rlabel(dialog, .varname = "label_top_right");
+        ui_newline(dialog);
+
+        ui_progressbar(dialog, .varname = "progressbar", .min = 0, .max = 100, .colspan = 2, .hexpand = TRUE);
+        ui_newline(dialog);
+
+        ui_llabel(dialog, .varname = "label_bottom_left", .hexpand = TRUE);
+        ui_rlabel(dialog, .varname = "label_bottom_right");
+        ui_newline(dialog);
+        
+        ui_label(dialog, .vexpand = TRUE);
+        ui_newline(dialog);
+        
+        ui_hbox(dialog, .colspan = 2, .hexpand = TRUE) {
+            ui_label(dialog, .hexpand = TRUE);
+            ui_button(dialog, .label = "Cancel", .onclick = btncallback);
+        }
+    }
+}
+
 
 static UiPathElm* dav_get_pathelm(const char *full_path, size_t len, size_t *ret_nelm, void* data) {
     if (len == 0) {
--- a/application/window.h	Fri Nov 15 21:50:20 2024 +0100
+++ b/application/window.h	Sun Nov 17 15:19:32 2024 +0100
@@ -59,6 +59,8 @@
 void resourceviewer_new(DavBrowser *browser, const char *path, DavResourceViewType type);
 void* resourceviewer_proplist_getvalue(DavPropertyList *property, int col);
 
+void transfer_window_init(UiObject *dialog, ui_callback btncallback);
+
 
 void auth_dialog(SessionAuthData *auth);
 
--- a/libidav/methods.c	Fri Nov 15 21:50:20 2024 +0100
+++ b/libidav/methods.c	Sun Nov 17 15:19:32 2024 +0100
@@ -1037,7 +1037,7 @@
         curl_easy_setopt(handle, CURLOPT_INFILESIZE_LARGE, (curl_off_t)length);
     } else if(length == 0) {
         headers = curl_slist_append(headers, "Transfer-Encoding: chunked");
-        curl_easy_setopt(handle, CURLOPT_INFILESIZE_LARGE, (curl_off_t)1);
+        curl_easy_setopt(handle, CURLOPT_INFILESIZE_LARGE, (curl_off_t)-1);
         curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers);
     } else {
         curl_easy_setopt(handle, CURLOPT_INFILESIZE_LARGE, (curl_off_t)length);
--- a/make/vs/idav/idav.vcxproj	Fri Nov 15 21:50:20 2024 +0100
+++ b/make/vs/idav/idav.vcxproj	Sun Nov 17 15:19:32 2024 +0100
@@ -169,6 +169,8 @@
     <ClCompile Include="..\..\..\application\application.c" />
     <ClCompile Include="..\..\..\application\config.c" />
     <ClCompile Include="..\..\..\application\davcontroller.c" />
+	<ClCompile Include="..\..\..\application\download.c" />
+	<ClCompile Include="..\..\..\application\upload.c" />
     <ClCompile Include="..\..\..\application\main.c" />
     <ClCompile Include="..\..\..\application\settings.c" />
     <ClCompile Include="..\..\..\application\system.c" />
@@ -178,6 +180,8 @@
     <ClInclude Include="..\..\..\application\application.h" />
     <ClInclude Include="..\..\..\application\config.h" />
     <ClInclude Include="..\..\..\application\davcontroller.h" />
+	<ClInclude Include="..\..\..\application\download.h" />
+	<ClInclude Include="..\..\..\application\upload.h" />
     <ClInclude Include="..\..\..\application\settings.h" />
     <ClInclude Include="..\..\..\application\system.h" />
     <ClInclude Include="..\..\..\application\window.h" />
@@ -201,4 +205,4 @@
     <Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.1.3.230331000\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.1.3.230331000\build\native\Microsoft.WindowsAppSDK.targets'))" />
     <Error Condition="!Exists('..\packages\Microsoft.Windows.ImplementationLibrary.1.0.230824.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.ImplementationLibrary.1.0.230824.2\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
   </Target>
-</Project>
\ No newline at end of file
+</Project>
--- a/make/vs/idav/idav.vcxproj.filters	Fri Nov 15 21:50:20 2024 +0100
+++ b/make/vs/idav/idav.vcxproj.filters	Sun Nov 17 15:19:32 2024 +0100
@@ -37,6 +37,12 @@
     <ClCompile Include="..\..\..\application\davcontroller.c">
       <Filter>src</Filter>
     </ClCompile>
+	<ClCompile Include="..\..\..\application\download.c">
+      <Filter>src</Filter>
+    </ClCompile>
+	<ClCompile Include="..\..\..\application\upload.c">
+      <Filter>src</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\..\application\settings.c">
       <Filter>src</Filter>
     </ClCompile>
@@ -57,8 +63,14 @@
     <ClInclude Include="..\..\..\application\davcontroller.h">
       <Filter>src</Filter>
     </ClInclude>
+	<ClInclude Include="..\..\..\application\download.h">
+      <Filter>src</Filter>
+    </ClInclude>
+	<ClInclude Include="..\..\..\application\upload.h">
+      <Filter>src</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\..\application\settings.h">
       <Filter>src</Filter>
     </ClInclude>
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
--- a/ui/common/context.c	Fri Nov 15 21:50:20 2024 +0100
+++ b/ui/common/context.c	Sun Nov 17 15:19:32 2024 +0100
@@ -557,7 +557,11 @@
 }
 
 UIEXPORT void *ui_allocator(UiContext *ctx) {
-    return ctx ? (void*)ctx->allocator : NULL;
+    return (void*)ctx->allocator;
+}
+
+void* ui_cx_mempool(UiContext *ctx) {
+    return ctx->mp;
 }
 
 void* ui_malloc(UiContext *ctx, size_t size) {
--- a/ui/gtk/toolkit.c	Fri Nov 15 21:50:20 2024 +0100
+++ b/ui/gtk/toolkit.c	Sun Nov 17 15:19:32 2024 +0100
@@ -364,7 +364,10 @@
 "  background-color: @theme_base_color;\n"
 "  border-radius: 5px;\n"
 "  padding: 0px;\n"
-"}"
+"}\n"
+".pathbar-button-inactive {\n"
+"  color: alpha(currentColor, 0.5);"
+"}\n"
 ".ui_test {\n"
 "  background-color: red;\n"
 "}\n"
--- a/ui/ui/toolkit.h	Fri Nov 15 21:50:20 2024 +0100
+++ b/ui/ui/toolkit.h	Sun Nov 17 15:19:32 2024 +0100
@@ -469,6 +469,8 @@
 UIEXPORT int* ui_active_groups(UiContext *ctx, int *ngroups);
     
 UIEXPORT void* ui_allocator(UiContext *ctx);
+UIEXPORT void* ui_cx_mempool(UiContext *ctx);
+
 UIEXPORT void* ui_malloc(UiContext *ctx, size_t size);
 UIEXPORT void* ui_calloc(UiContext *ctx, size_t nelem, size_t elsize);
 UIEXPORT void  ui_free(UiContext *ctx, void *ptr);

mercurial