commit c1afdc34484e2eed611e51fbe339c2e26a900dda Author: retoor Date: Tue Jan 6 14:23:31 2026 +0100 Initial commit. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0bac7ed --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +# retoor + +CC = gcc +CFLAGS = -Wall -Wextra -Werror -O2 -fPIC -D_FILE_OFFSET_BITS=64 +CFLAGS += $(shell pkg-config --cflags fuse3 libxml-2.0 libcurl) +LDFLAGS = $(shell pkg-config --libs fuse3 libxml-2.0 libcurl) -lpthread + +TARGET = fusedav +SRC_DIR = src +INC_DIR = include +BUILD_DIR = build + +SOURCES = $(wildcard $(SRC_DIR)/*.c) +OBJECTS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCES)) + +.PHONY: all clean install uninstall + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(CC) $(OBJECTS) -o $@ $(LDFLAGS) + +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@ + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +clean: + rm -rf $(BUILD_DIR) $(TARGET) + +install: $(TARGET) + install -d $(DESTDIR)/usr/local/bin + install -m 755 $(TARGET) $(DESTDIR)/usr/local/bin/ + +uninstall: + rm -f $(DESTDIR)/usr/local/bin/$(TARGET) + +deps: + @echo "Required packages:" + @echo " Debian/Ubuntu: sudo apt install libfuse3-dev libcurl4-openssl-dev libxml2-dev" + @echo " Fedora/RHEL: sudo dnf install fuse3-devel libcurl-devel libxml2-devel" + @echo " Arch: sudo pacman -S fuse3 curl libxml2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbe68d8 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# fusedav + +Author: retoor + +WebDAV filesystem client for Linux using FUSE3. Mounts remote WebDAV servers as local directories. + +## Architecture + +``` ++------------------+ +----------------+ +------------------+ +| Local Apps | | FUSE Layer | | WebDAV Server | +| (ls, cp, vim) | --> | (libfuse3) | --> | (Nextcloud, | +| | | | | Apache, etc) | ++------------------+ +----------------+ +------------------+ + | + +-----+-----+ + | | + +--------+ +---------+ + | Cache | | libcurl | + +--------+ +---------+ +``` + +## Features + +- RFC 4918 WebDAV protocol support +- FUSE3 filesystem interface +- Metadata and directory caching with configurable TTL +- HTTP Basic authentication +- HTTPS with certificate verification +- Range requests for partial file reads +- Thread-safe operations + +## Dependencies + +- libfuse3 (FUSE filesystem library) +- libcurl (HTTP client with HTTPS support) +- libxml2 (XML parsing for WebDAV responses) +- pthread (POSIX threads) + +### Installation + +Debian/Ubuntu: +``` +sudo apt install libfuse3-dev libcurl4-openssl-dev libxml2-dev build-essential pkg-config +``` + +Fedora/RHEL: +``` +sudo dnf install fuse3-devel libcurl-devel libxml2-devel gcc make pkg-config +``` + +Arch Linux: +``` +sudo pacman -S fuse3 curl libxml2 base-devel pkg-config +``` + +## Build + +``` +make +``` + +## Usage + +``` +./fusedav --url https://example.com/dav/ --mount-point /mnt/webdav +``` + +### Options + +| Option | Description | +|--------|-------------| +| `-u, --url URL` | WebDAV server URL (required) | +| `-m, --mount-point PATH` | Local mount point directory (required) | +| `-U, --username USER` | HTTP Basic auth username | +| `-p, --password PASS` | HTTP Basic auth password | +| `-c, --cache-ttl MS` | Cache TTL in milliseconds (default: 30000) | +| `-t, --timeout SEC` | Request timeout in seconds (default: 10) | +| `-f, --foreground` | Run in foreground | +| `-d, --debug` | Enable debug output | +| `-h, --help` | Show help | + +### Examples + +Mount with authentication: +``` +./fusedav --url https://cloud.example.com/remote.php/dav/files/user/ \ + --username user \ + --password secret \ + --mount-point /mnt/cloud +``` + +Mount with custom cache settings: +``` +./fusedav --url https://webdav.example.com/ \ + --mount-point /mnt/dav \ + --cache-ttl 60000 \ + --timeout 30 +``` + +Debug mode: +``` +./fusedav --url https://example.com/dav/ \ + --mount-point /mnt/dav \ + --debug +``` + +## Unmounting + +``` +fusermount -u /mnt/webdav +``` + +Or if mounted as root: +``` +sudo umount /mnt/webdav +``` + +## Limitations + +- No WebDAV Class 2 locking (concurrent writes may conflict) +- No symlink or extended attribute support +- Authentication credentials visible in process list +- Sequential write assumption (random writes fetch entire file first) + +## Security Notes + +- Credentials passed via command line are visible in `ps` output +- Consider using environment variables for sensitive data +- SSL certificate verification is enabled by default +- Input paths are validated to prevent directory traversal + +## Error Codes + +| HTTP Status | POSIX Error | +|-------------|-------------| +| 404 Not Found | ENOENT | +| 403 Forbidden | EACCES | +| 401 Unauthorized | EACCES | +| 405 Method Not Allowed | ENOTSUP | +| 409 Conflict | EEXIST | +| 507 Insufficient Storage | ENOSPC | +| 5xx Server Error | EIO | + +## License + +MIT diff --git a/build/cache.o b/build/cache.o new file mode 100644 index 0000000..3089931 Binary files /dev/null and b/build/cache.o differ diff --git a/build/config.o b/build/config.o new file mode 100644 index 0000000..8938882 Binary files /dev/null and b/build/config.o differ diff --git a/build/main.o b/build/main.o new file mode 100644 index 0000000..5a7f6c1 Binary files /dev/null and b/build/main.o differ diff --git a/build/operations.o b/build/operations.o new file mode 100644 index 0000000..80aeab4 Binary files /dev/null and b/build/operations.o differ diff --git a/build/utils.o b/build/utils.o new file mode 100644 index 0000000..58d7399 Binary files /dev/null and b/build/utils.o differ diff --git a/build/webdav.o b/build/webdav.o new file mode 100644 index 0000000..6deef26 Binary files /dev/null and b/build/webdav.o differ diff --git a/fusedav b/fusedav new file mode 100755 index 0000000..0d93ca0 Binary files /dev/null and b/fusedav differ diff --git a/include/cache.h b/include/cache.h new file mode 100644 index 0000000..8c5adfb --- /dev/null +++ b/include/cache.h @@ -0,0 +1,55 @@ +/* retoor */ + +#ifndef FUSEDAV_CACHE_H +#define FUSEDAV_CACHE_H + +#include +#include +#include +#include + +typedef struct cache_entry { + char *key; + void *value; + size_t value_size; + time_t created_at; + int ttl_ms; + struct cache_entry *next; +} cache_entry_t; + +typedef struct { + cache_entry_t *head; + int max_entries; + int current_entries; + int default_ttl_ms; + pthread_mutex_t lock; +} cache_t; + +typedef struct { + char *name; + int is_directory; + off_t size; + time_t mtime; +} dir_entry_t; + +typedef struct { + dir_entry_t *entries; + int count; +} dir_listing_t; + +cache_t *cache_init(int max_entries, int default_ttl_ms); +void cache_destroy(cache_t *cache); + +int cache_get_stat(cache_t *cache, const char *key, struct stat *st); +int cache_put_stat(cache_t *cache, const char *key, const struct stat *st); + +int cache_get_dir(cache_t *cache, const char *key, dir_listing_t **listing); +int cache_put_dir(cache_t *cache, const char *key, const dir_listing_t *listing); + +void cache_invalidate(cache_t *cache, const char *key); +void cache_invalidate_prefix(cache_t *cache, const char *prefix); +void cache_clear(cache_t *cache); + +void dir_listing_free(dir_listing_t *listing); + +#endif diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..0b02b28 --- /dev/null +++ b/include/config.h @@ -0,0 +1,28 @@ +/* retoor */ + +#ifndef FUSEDAV_CONFIG_H +#define FUSEDAV_CONFIG_H + +#include + +#define DEFAULT_CACHE_TTL_MS 30000 +#define DEFAULT_TIMEOUT_SEC 10 +#define DEFAULT_MAX_CACHE_ENTRIES 1024 +#define DEFAULT_WRITE_BUFFER_SIZE (1024 * 1024) + +typedef struct { + char *webdav_url; + char *username; + char *password; + char *mount_point; + int cache_ttl_ms; + int request_timeout_sec; + int foreground; + int debug; +} config_t; + +int config_parse(int argc, char *argv[], config_t *cfg); +void config_free(config_t *cfg); +void config_print_usage(const char *program_name); + +#endif diff --git a/include/operations.h b/include/operations.h new file mode 100644 index 0000000..1f7ed4c --- /dev/null +++ b/include/operations.h @@ -0,0 +1,15 @@ +/* retoor */ + +#ifndef FUSEDAV_OPERATIONS_H +#define FUSEDAV_OPERATIONS_H + +#define FUSE_USE_VERSION 31 + +#include +#include "webdav.h" + +extern struct fuse_operations fusedav_operations; + +void operations_init(webdav_context_t *ctx); + +#endif diff --git a/include/utils.h b/include/utils.h new file mode 100644 index 0000000..72ad90d --- /dev/null +++ b/include/utils.h @@ -0,0 +1,28 @@ +/* retoor */ + +#ifndef FUSEDAV_UTILS_H +#define FUSEDAV_UTILS_H + +#include +#include +#include +#include "cache.h" + +char *url_encode(const char *str); +char *url_decode(const char *str); +char *path_join(const char *base, const char *path); +char *path_parent(const char *path); +char *path_basename(const char *path); +int path_is_root(const char *path); + +int http_status_to_errno(long http_code); + +int parse_propfind_response(const char *xml_data, size_t xml_len, + struct stat *st, dir_listing_t *listing); + +time_t parse_http_date(const char *date_str); + +char *xml_get_text(xmlNodePtr node); +int xml_is_collection(xmlNodePtr node); + +#endif diff --git a/include/webdav.h b/include/webdav.h new file mode 100644 index 0000000..4d2943a --- /dev/null +++ b/include/webdav.h @@ -0,0 +1,56 @@ +/* retoor */ + +#ifndef FUSEDAV_WEBDAV_H +#define FUSEDAV_WEBDAV_H + +#include +#include +#include +#include +#include "cache.h" +#include "config.h" + +typedef struct { + char *webdav_url; + char *username; + char *password; + CURL *curl_handle; + cache_t *metadata_cache; + cache_t *dir_cache; + int cache_ttl_ms; + int request_timeout_sec; + int debug; + pthread_mutex_t lock; +} webdav_context_t; + +typedef struct { + char *path; + char *etag; + off_t file_size; + time_t last_modified; + uint8_t *write_buffer; + size_t write_buffer_pos; + size_t write_buffer_size; + int dirty; +} file_handle_t; + +int webdav_init(webdav_context_t *ctx, const config_t *cfg); +void webdav_cleanup(webdav_context_t *ctx); + +int webdav_get_stat(webdav_context_t *ctx, const char *path, struct stat *st); +int webdav_list_directory(webdav_context_t *ctx, const char *path, dir_listing_t *listing); +ssize_t webdav_read_file(webdav_context_t *ctx, const char *path, + off_t offset, size_t size, uint8_t *buffer); +ssize_t webdav_write_file(webdav_context_t *ctx, const char *path, + off_t offset, size_t size, const uint8_t *buffer); +int webdav_create_file(webdav_context_t *ctx, const char *path); +int webdav_mkdir(webdav_context_t *ctx, const char *path); +int webdav_delete(webdav_context_t *ctx, const char *path); +int webdav_rename(webdav_context_t *ctx, const char *old_path, const char *new_path); +int webdav_truncate(webdav_context_t *ctx, const char *path, off_t size); + +file_handle_t *file_handle_create(const char *path); +void file_handle_destroy(file_handle_t *fh); +int file_handle_flush(webdav_context_t *ctx, file_handle_t *fh); + +#endif diff --git a/src/cache.c b/src/cache.c new file mode 100644 index 0000000..1895136 --- /dev/null +++ b/src/cache.c @@ -0,0 +1,368 @@ +/* retoor */ + +#include +#include +#include +#include "cache.h" + +static int64_t current_time_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int entry_is_valid(cache_entry_t *entry) { + if (!entry) return 0; + int64_t now = current_time_ms(); + int64_t age = now - (int64_t)entry->created_at; + return age < entry->ttl_ms; +} + +static void entry_free(cache_entry_t *entry) { + if (!entry) return; + free(entry->key); + free(entry->value); + free(entry); +} + +cache_t *cache_init(int max_entries, int default_ttl_ms) { + cache_t *cache = calloc(1, sizeof(cache_t)); + if (!cache) return NULL; + + cache->max_entries = max_entries; + cache->default_ttl_ms = default_ttl_ms; + cache->current_entries = 0; + cache->head = NULL; + + if (pthread_mutex_init(&cache->lock, NULL) != 0) { + free(cache); + return NULL; + } + + return cache; +} + +void cache_destroy(cache_t *cache) { + if (!cache) return; + + pthread_mutex_lock(&cache->lock); + + cache_entry_t *entry = cache->head; + while (entry) { + cache_entry_t *next = entry->next; + entry_free(entry); + entry = next; + } + + pthread_mutex_unlock(&cache->lock); + pthread_mutex_destroy(&cache->lock); + free(cache); +} + +static cache_entry_t *find_entry(cache_t *cache, const char *key, + cache_entry_t **prev) { + if (prev) *prev = NULL; + + cache_entry_t *entry = cache->head; + cache_entry_t *previous = NULL; + + while (entry) { + if (strcmp(entry->key, key) == 0) { + if (prev) *prev = previous; + return entry; + } + previous = entry; + entry = entry->next; + } + + return NULL; +} + +static void remove_entry(cache_t *cache, cache_entry_t *entry, + cache_entry_t *prev) { + if (prev) { + prev->next = entry->next; + } else { + cache->head = entry->next; + } + cache->current_entries--; + entry_free(entry); +} + +static void evict_oldest(cache_t *cache) { + if (!cache->head) return; + + cache_entry_t *oldest = cache->head; + cache_entry_t *oldest_prev = NULL; + cache_entry_t *entry = cache->head->next; + cache_entry_t *prev = cache->head; + + while (entry) { + if (entry->created_at < oldest->created_at) { + oldest = entry; + oldest_prev = prev; + } + prev = entry; + entry = entry->next; + } + + remove_entry(cache, oldest, oldest_prev); +} + +int cache_get_stat(cache_t *cache, const char *key, struct stat *st) { + if (!cache || !key || !st) return -1; + + pthread_mutex_lock(&cache->lock); + + cache_entry_t *prev; + cache_entry_t *entry = find_entry(cache, key, &prev); + + if (!entry) { + pthread_mutex_unlock(&cache->lock); + return -1; + } + + if (!entry_is_valid(entry)) { + remove_entry(cache, entry, prev); + pthread_mutex_unlock(&cache->lock); + return -1; + } + + memcpy(st, entry->value, sizeof(struct stat)); + + pthread_mutex_unlock(&cache->lock); + return 0; +} + +int cache_put_stat(cache_t *cache, const char *key, const struct stat *st) { + if (!cache || !key || !st) return -1; + + pthread_mutex_lock(&cache->lock); + + cache_entry_t *prev; + cache_entry_t *existing = find_entry(cache, key, &prev); + + if (existing) { + memcpy(existing->value, st, sizeof(struct stat)); + existing->created_at = (time_t)current_time_ms(); + pthread_mutex_unlock(&cache->lock); + return 0; + } + + if (cache->current_entries >= cache->max_entries) { + evict_oldest(cache); + } + + cache_entry_t *entry = calloc(1, sizeof(cache_entry_t)); + if (!entry) { + pthread_mutex_unlock(&cache->lock); + return -1; + } + + entry->key = strdup(key); + entry->value = malloc(sizeof(struct stat)); + if (!entry->key || !entry->value) { + free(entry->key); + free(entry->value); + free(entry); + pthread_mutex_unlock(&cache->lock); + return -1; + } + + memcpy(entry->value, st, sizeof(struct stat)); + entry->value_size = sizeof(struct stat); + entry->created_at = (time_t)current_time_ms(); + entry->ttl_ms = cache->default_ttl_ms; + + entry->next = cache->head; + cache->head = entry; + cache->current_entries++; + + pthread_mutex_unlock(&cache->lock); + return 0; +} + +static dir_listing_t *copy_dir_listing(const dir_listing_t *src) { + if (!src) return NULL; + + dir_listing_t *dst = calloc(1, sizeof(dir_listing_t)); + if (!dst) return NULL; + + if (src->count == 0) { + dst->entries = NULL; + dst->count = 0; + return dst; + } + + dst->entries = calloc((size_t)src->count, sizeof(dir_entry_t)); + if (!dst->entries) { + free(dst); + return NULL; + } + + dst->count = src->count; + + for (int i = 0; i < src->count; i++) { + dst->entries[i].name = strdup(src->entries[i].name); + dst->entries[i].is_directory = src->entries[i].is_directory; + dst->entries[i].size = src->entries[i].size; + dst->entries[i].mtime = src->entries[i].mtime; + + if (!dst->entries[i].name) { + for (int j = 0; j < i; j++) { + free(dst->entries[j].name); + } + free(dst->entries); + free(dst); + return NULL; + } + } + + return dst; +} + +void dir_listing_free(dir_listing_t *listing) { + if (!listing) return; + + for (int i = 0; i < listing->count; i++) { + free(listing->entries[i].name); + } + free(listing->entries); + free(listing); +} + +int cache_get_dir(cache_t *cache, const char *key, dir_listing_t **listing) { + if (!cache || !key || !listing) return -1; + + pthread_mutex_lock(&cache->lock); + + cache_entry_t *prev; + cache_entry_t *entry = find_entry(cache, key, &prev); + + if (!entry) { + pthread_mutex_unlock(&cache->lock); + return -1; + } + + if (!entry_is_valid(entry)) { + remove_entry(cache, entry, prev); + pthread_mutex_unlock(&cache->lock); + return -1; + } + + *listing = copy_dir_listing((dir_listing_t *)entry->value); + + pthread_mutex_unlock(&cache->lock); + return *listing ? 0 : -1; +} + +int cache_put_dir(cache_t *cache, const char *key, const dir_listing_t *listing) { + if (!cache || !key || !listing) return -1; + + pthread_mutex_lock(&cache->lock); + + cache_entry_t *prev; + cache_entry_t *existing = find_entry(cache, key, &prev); + + if (existing) { + dir_listing_free((dir_listing_t *)existing->value); + existing->value = copy_dir_listing(listing); + existing->created_at = (time_t)current_time_ms(); + pthread_mutex_unlock(&cache->lock); + return existing->value ? 0 : -1; + } + + if (cache->current_entries >= cache->max_entries) { + evict_oldest(cache); + } + + cache_entry_t *entry = calloc(1, sizeof(cache_entry_t)); + if (!entry) { + pthread_mutex_unlock(&cache->lock); + return -1; + } + + entry->key = strdup(key); + entry->value = copy_dir_listing(listing); + if (!entry->key || !entry->value) { + free(entry->key); + dir_listing_free((dir_listing_t *)entry->value); + free(entry); + pthread_mutex_unlock(&cache->lock); + return -1; + } + + entry->created_at = (time_t)current_time_ms(); + entry->ttl_ms = cache->default_ttl_ms; + + entry->next = cache->head; + cache->head = entry; + cache->current_entries++; + + pthread_mutex_unlock(&cache->lock); + return 0; +} + +void cache_invalidate(cache_t *cache, const char *key) { + if (!cache || !key) return; + + pthread_mutex_lock(&cache->lock); + + cache_entry_t *prev; + cache_entry_t *entry = find_entry(cache, key, &prev); + + if (entry) { + remove_entry(cache, entry, prev); + } + + pthread_mutex_unlock(&cache->lock); +} + +void cache_invalidate_prefix(cache_t *cache, const char *prefix) { + if (!cache || !prefix) return; + + size_t prefix_len = strlen(prefix); + + pthread_mutex_lock(&cache->lock); + + cache_entry_t *entry = cache->head; + cache_entry_t *prev = NULL; + + while (entry) { + cache_entry_t *next = entry->next; + + if (strncmp(entry->key, prefix, prefix_len) == 0) { + if (prev) { + prev->next = next; + } else { + cache->head = next; + } + cache->current_entries--; + entry_free(entry); + } else { + prev = entry; + } + + entry = next; + } + + pthread_mutex_unlock(&cache->lock); +} + +void cache_clear(cache_t *cache) { + if (!cache) return; + + pthread_mutex_lock(&cache->lock); + + cache_entry_t *entry = cache->head; + while (entry) { + cache_entry_t *next = entry->next; + entry_free(entry); + entry = next; + } + + cache->head = NULL; + cache->current_entries = 0; + + pthread_mutex_unlock(&cache->lock); +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..9811c2b --- /dev/null +++ b/src/config.c @@ -0,0 +1,174 @@ +/* retoor */ + +#include +#include +#include +#include +#include +#include +#include "config.h" + +static struct option long_options[] = { + {"url", required_argument, 0, 'u'}, + {"username", required_argument, 0, 'U'}, + {"password", required_argument, 0, 'p'}, + {"mount-point", required_argument, 0, 'm'}, + {"cache-ttl", required_argument, 0, 'c'}, + {"timeout", required_argument, 0, 't'}, + {"foreground", no_argument, 0, 'f'}, + {"debug", no_argument, 0, 'd'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} +}; + +void config_print_usage(const char *program_name) { + fprintf(stderr, "Usage: %s [options]\n\n", program_name); + fprintf(stderr, "Required options:\n"); + fprintf(stderr, " -u, --url URL WebDAV server URL\n"); + fprintf(stderr, " -m, --mount-point PATH Local mount point\n\n"); + fprintf(stderr, "Authentication:\n"); + fprintf(stderr, " -U, --username USER HTTP Basic auth username\n"); + fprintf(stderr, " -p, --password PASS HTTP Basic auth password\n\n"); + fprintf(stderr, "Performance:\n"); + fprintf(stderr, " -c, --cache-ttl MS Cache TTL in milliseconds (default: %d)\n", + DEFAULT_CACHE_TTL_MS); + fprintf(stderr, " -t, --timeout SEC Request timeout in seconds (default: %d)\n", + DEFAULT_TIMEOUT_SEC); + fprintf(stderr, "\nDebug:\n"); + fprintf(stderr, " -f, --foreground Run in foreground\n"); + fprintf(stderr, " -d, --debug Enable debug output\n"); + fprintf(stderr, " -h, --help Show this help\n"); +} + +static int validate_url(const char *url) { + if (!url || strlen(url) == 0) return 0; + return (strncmp(url, "http://", 7) == 0 || strncmp(url, "https://", 8) == 0); +} + +static int validate_mount_point(const char *path) { + if (!path || strlen(path) == 0) return 0; + + struct stat st; + if (stat(path, &st) != 0) { + fprintf(stderr, "Error: Mount point '%s' does not exist\n", path); + return 0; + } + + if (!S_ISDIR(st.st_mode)) { + fprintf(stderr, "Error: Mount point '%s' is not a directory\n", path); + return 0; + } + + return 1; +} + +static char *normalize_url(const char *url) { + if (!url) return NULL; + + size_t len = strlen(url); + char *result; + + if (len > 0 && url[len - 1] != '/') { + result = malloc(len + 2); + if (!result) return NULL; + strcpy(result, url); + result[len] = '/'; + result[len + 1] = '\0'; + } else { + result = strdup(url); + } + + return result; +} + +int config_parse(int argc, char *argv[], config_t *cfg) { + if (!cfg) return -1; + + memset(cfg, 0, sizeof(*cfg)); + cfg->cache_ttl_ms = DEFAULT_CACHE_TTL_MS; + cfg->request_timeout_sec = DEFAULT_TIMEOUT_SEC; + cfg->foreground = 0; + cfg->debug = 0; + + int opt; + int option_index = 0; + + while ((opt = getopt_long(argc, argv, "u:U:p:m:c:t:fdh", + long_options, &option_index)) != -1) { + switch (opt) { + case 'u': + cfg->webdav_url = normalize_url(optarg); + break; + case 'U': + cfg->username = strdup(optarg); + break; + case 'p': + cfg->password = strdup(optarg); + break; + case 'm': + cfg->mount_point = strdup(optarg); + break; + case 'c': + cfg->cache_ttl_ms = atoi(optarg); + if (cfg->cache_ttl_ms < 0) cfg->cache_ttl_ms = 0; + break; + case 't': + cfg->request_timeout_sec = atoi(optarg); + if (cfg->request_timeout_sec < 1) cfg->request_timeout_sec = 1; + break; + case 'f': + cfg->foreground = 1; + break; + case 'd': + cfg->debug = 1; + cfg->foreground = 1; + break; + case 'h': + config_print_usage(argv[0]); + return 1; + default: + config_print_usage(argv[0]); + return -1; + } + } + + if (!cfg->webdav_url) { + fprintf(stderr, "Error: WebDAV URL is required\n"); + config_print_usage(argv[0]); + return -1; + } + + if (!validate_url(cfg->webdav_url)) { + fprintf(stderr, "Error: Invalid URL '%s' (must start with http:// or https://)\n", + cfg->webdav_url); + return -1; + } + + if (!cfg->mount_point) { + fprintf(stderr, "Error: Mount point is required\n"); + config_print_usage(argv[0]); + return -1; + } + + if (!validate_mount_point(cfg->mount_point)) { + return -1; + } + + if ((cfg->username && !cfg->password) || (!cfg->username && cfg->password)) { + fprintf(stderr, "Error: Both username and password must be provided together\n"); + return -1; + } + + return 0; +} + +void config_free(config_t *cfg) { + if (!cfg) return; + + free(cfg->webdav_url); + free(cfg->username); + free(cfg->password); + free(cfg->mount_point); + + memset(cfg, 0, sizeof(*cfg)); +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..ed317fc --- /dev/null +++ b/src/main.c @@ -0,0 +1,124 @@ +/* retoor */ + +#define FUSE_USE_VERSION 31 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "webdav.h" +#include "operations.h" + +static webdav_context_t g_webdav_ctx; +static volatile sig_atomic_t g_running = 1; + +static void signal_handler(int sig) { + (void)sig; + g_running = 0; +} + +static void setup_signals(void) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); +} + +static int test_connection(webdav_context_t *ctx) { + struct stat st; + int result = webdav_get_stat(ctx, "/", &st); + + if (result != 0) { + fprintf(stderr, "Error: Cannot connect to WebDAV server\n"); + if (result == -EACCES) { + fprintf(stderr, " Authentication failed. Check credentials.\n"); + } else if (result == -ENOENT) { + fprintf(stderr, " Server returned 404. Check URL path.\n"); + } else { + fprintf(stderr, " Error code: %d\n", result); + } + return -1; + } + + if (!S_ISDIR(st.st_mode)) { + fprintf(stderr, "Error: WebDAV URL does not point to a directory\n"); + return -1; + } + + return 0; +} + +int main(int argc, char *argv[]) { + config_t cfg; + int result; + + result = config_parse(argc, argv, &cfg); + if (result != 0) { + return (result > 0) ? 0 : 1; + } + + curl_global_init(CURL_GLOBAL_ALL); + xmlInitParser(); + LIBXML_TEST_VERSION + + result = webdav_init(&g_webdav_ctx, &cfg); + if (result != 0) { + fprintf(stderr, "Error: Failed to initialize WebDAV client\n"); + config_free(&cfg); + curl_global_cleanup(); + xmlCleanupParser(); + return 1; + } + + result = test_connection(&g_webdav_ctx); + if (result != 0) { + webdav_cleanup(&g_webdav_ctx); + config_free(&cfg); + curl_global_cleanup(); + xmlCleanupParser(); + return 1; + } + + operations_init(&g_webdav_ctx); + + setup_signals(); + + int fuse_argc = 0; + char *fuse_argv[10]; + + fuse_argv[fuse_argc++] = argv[0]; + + if (cfg.foreground) { + fuse_argv[fuse_argc++] = "-f"; + } + + if (cfg.debug) { + fuse_argv[fuse_argc++] = "-d"; + } + + fuse_argv[fuse_argc++] = "-o"; + fuse_argv[fuse_argc++] = "default_permissions"; + + fuse_argv[fuse_argc++] = cfg.mount_point; + + struct fuse_args args = FUSE_ARGS_INIT(fuse_argc, fuse_argv); + + result = fuse_main(args.argc, args.argv, &fusedav_operations, NULL); + + webdav_cleanup(&g_webdav_ctx); + config_free(&cfg); + curl_global_cleanup(); + xmlCleanupParser(); + + return result; +} diff --git a/src/operations.c b/src/operations.c new file mode 100644 index 0000000..8fe20ad --- /dev/null +++ b/src/operations.c @@ -0,0 +1,350 @@ +/* retoor */ + +#define FUSE_USE_VERSION 31 +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include "operations.h" +#include "webdav.h" +#include "cache.h" +#include "utils.h" + +static webdav_context_t *g_ctx = NULL; + +static webdav_context_t *get_context(void) { + return g_ctx; +} + +static int fusedav_getattr(const char *path, struct stat *stbuf, + struct fuse_file_info *fi) { + (void)fi; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + memset(stbuf, 0, sizeof(struct stat)); + + if (path_is_root(path)) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 2; + stbuf->st_uid = getuid(); + stbuf->st_gid = getgid(); + return 0; + } + + return webdav_get_stat(ctx, path, stbuf); +} + +static int fusedav_readdir(const char *path, void *buf, + fuse_fill_dir_t filler, + off_t offset, struct fuse_file_info *fi, + enum fuse_readdir_flags flags) { + (void)offset; + (void)fi; + (void)flags; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + filler(buf, ".", NULL, 0, 0); + filler(buf, "..", NULL, 0, 0); + + dir_listing_t listing = {0}; + int result = webdav_list_directory(ctx, path, &listing); + + if (result != 0) { + return result; + } + + for (int i = 0; i < listing.count; i++) { + struct stat st = {0}; + st.st_mode = listing.entries[i].is_directory ? + (S_IFDIR | 0755) : (S_IFREG | 0644); + st.st_size = listing.entries[i].size; + st.st_mtime = listing.entries[i].mtime; + st.st_atime = listing.entries[i].mtime; + st.st_ctime = listing.entries[i].mtime; + st.st_uid = getuid(); + st.st_gid = getgid(); + st.st_nlink = listing.entries[i].is_directory ? 2 : 1; + + if (filler(buf, listing.entries[i].name, &st, 0, 0) != 0) { + break; + } + } + + for (int i = 0; i < listing.count; i++) { + free(listing.entries[i].name); + } + free(listing.entries); + + return 0; +} + +static int fusedav_open(const char *path, struct fuse_file_info *fi) { + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + struct stat st; + int result = webdav_get_stat(ctx, path, &st); + + if (result != 0) { + if ((fi->flags & O_CREAT) && result == -ENOENT) { + result = webdav_create_file(ctx, path); + if (result != 0) return result; + } else { + return result; + } + } + + if (S_ISDIR(st.st_mode)) { + return -EISDIR; + } + + file_handle_t *fh = file_handle_create(path); + if (!fh) return -ENOMEM; + + fh->file_size = st.st_size; + fh->last_modified = st.st_mtime; + + fi->fh = (uint64_t)(uintptr_t)fh; + return 0; +} + +static int fusedav_create(const char *path, mode_t mode, + struct fuse_file_info *fi) { + (void)mode; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + int result = webdav_create_file(ctx, path); + if (result != 0) return result; + + file_handle_t *fh = file_handle_create(path); + if (!fh) return -ENOMEM; + + fh->file_size = 0; + fh->last_modified = time(NULL); + + fi->fh = (uint64_t)(uintptr_t)fh; + return 0; +} + +static int fusedav_read(const char *path, char *buf, size_t size, + off_t offset, struct fuse_file_info *fi) { + (void)path; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + file_handle_t *fh = (file_handle_t *)(uintptr_t)fi->fh; + const char *read_path = fh ? fh->path : path; + + ssize_t result = webdav_read_file(ctx, read_path, offset, size, + (uint8_t *)buf); + return (int)result; +} + +static int fusedav_write(const char *path, const char *buf, size_t size, + off_t offset, struct fuse_file_info *fi) { + (void)path; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + file_handle_t *fh = (file_handle_t *)(uintptr_t)fi->fh; + if (!fh) return -EBADF; + + if (fh->write_buffer_pos + size > fh->write_buffer_size) { + int result = file_handle_flush(ctx, fh); + if (result != 0) return result; + + if (size > fh->write_buffer_size) { + ssize_t written = webdav_write_file(ctx, fh->path, offset, + size, (uint8_t *)buf); + return (int)written; + } + } + + memcpy(fh->write_buffer + fh->write_buffer_pos, buf, size); + fh->write_buffer_pos += size; + fh->dirty = 1; + + off_t new_end = offset + (off_t)size; + if (new_end > fh->file_size) { + fh->file_size = new_end; + } + + return (int)size; +} + +static int fusedav_flush(const char *path, struct fuse_file_info *fi) { + (void)path; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + file_handle_t *fh = (file_handle_t *)(uintptr_t)fi->fh; + if (!fh) return 0; + + return file_handle_flush(ctx, fh); +} + +static int fusedav_fsync(const char *path, int isdatasync, + struct fuse_file_info *fi) { + (void)isdatasync; + return fusedav_flush(path, fi); +} + +static int fusedav_release(const char *path, struct fuse_file_info *fi) { + (void)path; + + webdav_context_t *ctx = get_context(); + file_handle_t *fh = (file_handle_t *)(uintptr_t)fi->fh; + + if (fh) { + if (ctx && fh->dirty) { + file_handle_flush(ctx, fh); + } + file_handle_destroy(fh); + } + + fi->fh = 0; + return 0; +} + +static int fusedav_mkdir(const char *path, mode_t mode) { + (void)mode; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + return webdav_mkdir(ctx, path); +} + +static int fusedav_unlink(const char *path) { + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + return webdav_delete(ctx, path); +} + +static int fusedav_rmdir(const char *path) { + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + return webdav_delete(ctx, path); +} + +static int fusedav_rename(const char *from, const char *to, + unsigned int flags) { + (void)flags; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + return webdav_rename(ctx, from, to); +} + +static int fusedav_truncate(const char *path, off_t size, + struct fuse_file_info *fi) { + (void)fi; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + return webdav_truncate(ctx, path, size); +} + +static int fusedav_statfs(const char *path, struct statvfs *stbuf) { + (void)path; + + stbuf->f_bsize = 4096; + stbuf->f_frsize = 4096; + stbuf->f_blocks = 1000000000; + stbuf->f_bfree = 500000000; + stbuf->f_bavail = 500000000; + stbuf->f_files = 1000000; + stbuf->f_ffree = 500000; + stbuf->f_favail = 500000; + stbuf->f_fsid = 0; + stbuf->f_flag = 0; + stbuf->f_namemax = 255; + + return 0; +} + +static int fusedav_utimens(const char *path, const struct timespec tv[2], + struct fuse_file_info *fi) { + (void)path; + (void)tv; + (void)fi; + return 0; +} + +static int fusedav_access(const char *path, int mask) { + (void)mask; + + webdav_context_t *ctx = get_context(); + if (!ctx) return -EIO; + + if (path_is_root(path)) { + return 0; + } + + struct stat st; + return webdav_get_stat(ctx, path, &st); +} + +static void *fusedav_init(struct fuse_conn_info *conn, + struct fuse_config *cfg) { + (void)conn; + cfg->kernel_cache = 1; + cfg->auto_cache = 1; + cfg->entry_timeout = 30.0; + cfg->attr_timeout = 30.0; + cfg->negative_timeout = 5.0; + return NULL; +} + +static void fusedav_destroy(void *private_data) { + (void)private_data; + + webdav_context_t *ctx = get_context(); + if (ctx) { + cache_clear(ctx->metadata_cache); + cache_clear(ctx->dir_cache); + } +} + +struct fuse_operations fusedav_operations = { + .getattr = fusedav_getattr, + .readdir = fusedav_readdir, + .open = fusedav_open, + .create = fusedav_create, + .read = fusedav_read, + .write = fusedav_write, + .flush = fusedav_flush, + .fsync = fusedav_fsync, + .release = fusedav_release, + .mkdir = fusedav_mkdir, + .unlink = fusedav_unlink, + .rmdir = fusedav_rmdir, + .rename = fusedav_rename, + .truncate = fusedav_truncate, + .statfs = fusedav_statfs, + .utimens = fusedav_utimens, + .access = fusedav_access, + .init = fusedav_init, + .destroy = fusedav_destroy, +}; + +void operations_init(webdav_context_t *ctx) { + g_ctx = ctx; +} diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..f57ffe2 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,403 @@ +/* retoor */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "utils.h" +#include "cache.h" + +static const char *DAV_NS = "DAV:"; + +char *url_encode(const char *str) { + if (!str) return NULL; + + size_t len = strlen(str); + char *encoded = malloc(len * 3 + 1); + if (!encoded) return NULL; + + char *p = encoded; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)str[i]; + if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') { + *p++ = c; + } else { + sprintf(p, "%%%02X", c); + p += 3; + } + } + *p = '\0'; + return encoded; +} + +char *url_decode(const char *str) { + if (!str) return NULL; + + size_t len = strlen(str); + char *decoded = malloc(len + 1); + if (!decoded) return NULL; + + char *p = decoded; + for (size_t i = 0; i < len; i++) { + if (str[i] == '%' && i + 2 < len) { + char hex[3] = {str[i + 1], str[i + 2], '\0'}; + *p++ = (char)strtol(hex, NULL, 16); + i += 2; + } else if (str[i] == '+') { + *p++ = ' '; + } else { + *p++ = str[i]; + } + } + *p = '\0'; + return decoded; +} + +char *path_join(const char *base, const char *path) { + if (!base || !path) return NULL; + + size_t base_len = strlen(base); + size_t path_len = strlen(path); + + while (base_len > 0 && base[base_len - 1] == '/') { + base_len--; + } + while (*path == '/') { + path++; + path_len--; + } + + char *result = malloc(base_len + 1 + path_len + 1); + if (!result) return NULL; + + memcpy(result, base, base_len); + result[base_len] = '/'; + memcpy(result + base_len + 1, path, path_len); + result[base_len + 1 + path_len] = '\0'; + + return result; +} + +char *path_parent(const char *path) { + if (!path) return NULL; + + size_t len = strlen(path); + while (len > 0 && path[len - 1] == '/') { + len--; + } + + while (len > 0 && path[len - 1] != '/') { + len--; + } + + while (len > 1 && path[len - 1] == '/') { + len--; + } + + if (len == 0) { + return strdup("/"); + } + + char *parent = malloc(len + 1); + if (!parent) return NULL; + + memcpy(parent, path, len); + parent[len] = '\0'; + return parent; +} + +char *path_basename(const char *path) { + if (!path) return NULL; + + size_t len = strlen(path); + while (len > 0 && path[len - 1] == '/') { + len--; + } + + size_t end = len; + while (len > 0 && path[len - 1] != '/') { + len--; + } + + size_t name_len = end - len; + if (name_len == 0) { + return strdup("/"); + } + + char *name = malloc(name_len + 1); + if (!name) return NULL; + + memcpy(name, path + len, name_len); + name[name_len] = '\0'; + return name; +} + +int path_is_root(const char *path) { + if (!path) return 0; + while (*path == '/') path++; + return *path == '\0'; +} + +int http_status_to_errno(long http_code) { + if (http_code >= 200 && http_code < 300) { + return 0; + } + + switch (http_code) { + case 400: return -EINVAL; + case 401: return -EACCES; + case 403: return -EACCES; + case 404: return -ENOENT; + case 405: return -ENOTSUP; + case 409: return -EEXIST; + case 412: return -ESTALE; + case 423: return -EBUSY; + case 507: return -ENOSPC; + default: + if (http_code >= 500) return -EIO; + return -EIO; + } +} + +static const char *MONTHS[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" +}; + +time_t parse_http_date(const char *date_str) { + if (!date_str) return 0; + + struct tm tm = {0}; + + if (strptime(date_str, "%a, %d %b %Y %H:%M:%S", &tm) || + strptime(date_str, "%Y-%m-%dT%H:%M:%S", &tm)) { + return timegm(&tm); + } + + char month_str[4] = {0}; + int day, year, hour, min, sec; + + if (sscanf(date_str, "%*[^,], %d %3s %d %d:%d:%d", + &day, month_str, &year, &hour, &min, &sec) == 6) { + tm.tm_mday = day; + tm.tm_year = year - 1900; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + + for (int i = 0; i < 12; i++) { + if (strcasecmp(month_str, MONTHS[i]) == 0) { + tm.tm_mon = i; + break; + } + } + return timegm(&tm); + } + + return 0; +} + +char *xml_get_text(xmlNodePtr node) { + if (!node) return NULL; + + xmlChar *content = xmlNodeGetContent(node); + if (!content) return NULL; + + char *result = strdup((char *)content); + xmlFree(content); + return result; +} + +int xml_is_collection(xmlNodePtr node) { + for (xmlNodePtr child = node->children; child; child = child->next) { + if (child->type == XML_ELEMENT_NODE) { + if (xmlStrcmp(child->name, (xmlChar *)"collection") == 0) { + return 1; + } + } + } + return 0; +} + +static xmlNodePtr find_child(xmlNodePtr parent, const char *name, const char *ns) { + for (xmlNodePtr child = parent->children; child; child = child->next) { + if (child->type != XML_ELEMENT_NODE) continue; + + if (xmlStrcmp(child->name, (xmlChar *)name) != 0) continue; + + if (ns) { + if (child->ns && xmlStrcmp(child->ns->href, (xmlChar *)ns) == 0) { + return child; + } + } else { + return child; + } + } + return NULL; +} + +static int parse_response_entry(xmlNodePtr response, + struct stat *st, char **entry_name) { + xmlNodePtr href = find_child(response, "href", DAV_NS); + if (!href) return -1; + + char *href_text = xml_get_text(href); + if (!href_text) return -1; + + char *decoded_href = url_decode(href_text); + free(href_text); + if (!decoded_href) return -1; + + char *name = path_basename(decoded_href); + free(decoded_href); + if (!name) return -1; + + if (entry_name) { + *entry_name = name; + } + + xmlNodePtr propstat = find_child(response, "propstat", DAV_NS); + if (!propstat) { + if (!entry_name) free(name); + return -1; + } + + xmlNodePtr prop = find_child(propstat, "prop", DAV_NS); + if (!prop) { + if (!entry_name) free(name); + return -1; + } + + memset(st, 0, sizeof(*st)); + st->st_uid = getuid(); + st->st_gid = getgid(); + st->st_nlink = 1; + + xmlNodePtr resourcetype = find_child(prop, "resourcetype", DAV_NS); + if (resourcetype && xml_is_collection(resourcetype)) { + st->st_mode = S_IFDIR | 0755; + st->st_nlink = 2; + } else { + st->st_mode = S_IFREG | 0644; + } + + xmlNodePtr contentlength = find_child(prop, "getcontentlength", DAV_NS); + if (contentlength) { + char *len_str = xml_get_text(contentlength); + if (len_str) { + st->st_size = (off_t)strtoll(len_str, NULL, 10); + free(len_str); + } + } + + xmlNodePtr lastmodified = find_child(prop, "getlastmodified", DAV_NS); + if (lastmodified) { + char *date_str = xml_get_text(lastmodified); + if (date_str) { + st->st_mtime = parse_http_date(date_str); + st->st_atime = st->st_mtime; + st->st_ctime = st->st_mtime; + free(date_str); + } + } + + if (st->st_mtime == 0) { + st->st_mtime = time(NULL); + st->st_atime = st->st_mtime; + st->st_ctime = st->st_mtime; + } + + st->st_blksize = 4096; + st->st_blocks = (st->st_size + 511) / 512; + + if (!entry_name) free(name); + return 0; +} + +int parse_propfind_response(const char *xml_data, size_t xml_len, + struct stat *st, dir_listing_t *listing) { + if (!xml_data || xml_len == 0) return -EINVAL; + + xmlDocPtr doc = xmlReadMemory(xml_data, (int)xml_len, NULL, NULL, + XML_PARSE_NOERROR | XML_PARSE_NOWARNING); + if (!doc) return -EIO; + + xmlNodePtr root = xmlDocGetRootElement(doc); + if (!root) { + xmlFreeDoc(doc); + return -EIO; + } + + int count = 0; + for (xmlNodePtr node = root->children; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE && + xmlStrcmp(node->name, (xmlChar *)"response") == 0) { + count++; + } + } + + if (count == 0) { + xmlFreeDoc(doc); + return -ENOENT; + } + + if (listing) { + listing->entries = calloc((size_t)count, sizeof(dir_entry_t)); + if (!listing->entries) { + xmlFreeDoc(doc); + return -ENOMEM; + } + listing->count = 0; + } + + int first = 1; + for (xmlNodePtr node = root->children; node; node = node->next) { + if (node->type != XML_ELEMENT_NODE || + xmlStrcmp(node->name, (xmlChar *)"response") != 0) { + continue; + } + + struct stat entry_st; + char *entry_name = NULL; + + if (parse_response_entry(node, &entry_st, &entry_name) != 0) { + continue; + } + + if (first && st) { + *st = entry_st; + first = 0; + free(entry_name); + continue; + } + first = 0; + + if (listing && entry_name) { + if (strcmp(entry_name, ".") == 0 || strcmp(entry_name, "..") == 0 || + strlen(entry_name) == 0) { + free(entry_name); + continue; + } + + dir_entry_t *entry = &listing->entries[listing->count]; + entry->name = entry_name; + entry->is_directory = S_ISDIR(entry_st.st_mode); + entry->size = entry_st.st_size; + entry->mtime = entry_st.st_mtime; + listing->count++; + } else { + free(entry_name); + } + } + + xmlFreeDoc(doc); + return 0; +} diff --git a/src/webdav.c b/src/webdav.c new file mode 100644 index 0000000..6db0515 --- /dev/null +++ b/src/webdav.c @@ -0,0 +1,860 @@ +/* retoor */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include "webdav.h" +#include "utils.h" +#include "config.h" + +typedef struct { + uint8_t *data; + size_t size; + size_t capacity; +} response_buffer_t; + +static const char *PROPFIND_BODY = + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + +static size_t write_callback(void *contents, size_t size, size_t nmemb, + void *userp) { + size_t total = size * nmemb; + response_buffer_t *buf = (response_buffer_t *)userp; + + if (buf->size + total + 1 > buf->capacity) { + size_t new_cap = buf->capacity * 2; + if (new_cap < buf->size + total + 1) { + new_cap = buf->size + total + 1024; + } + + uint8_t *new_data = realloc(buf->data, new_cap); + if (!new_data) return 0; + + buf->data = new_data; + buf->capacity = new_cap; + } + + memcpy(buf->data + buf->size, contents, total); + buf->size += total; + buf->data[buf->size] = '\0'; + + return total; +} + +static size_t read_callback(void *ptr, size_t size, size_t nmemb, + void *userp) { + response_buffer_t *buf = (response_buffer_t *)userp; + size_t total = size * nmemb; + size_t remaining = buf->capacity - buf->size; + + if (remaining == 0) return 0; + + size_t to_copy = (remaining < total) ? remaining : total; + memcpy(ptr, buf->data + buf->size, to_copy); + buf->size += to_copy; + + return to_copy; +} + +static void response_buffer_init(response_buffer_t *buf, size_t initial_cap) { + buf->data = malloc(initial_cap); + buf->size = 0; + buf->capacity = initial_cap; + if (buf->data) buf->data[0] = '\0'; +} + +static void response_buffer_free(response_buffer_t *buf) { + free(buf->data); + buf->data = NULL; + buf->size = 0; + buf->capacity = 0; +} + +static char *build_url(webdav_context_t *ctx, const char *path) { + char *encoded_path = url_encode(path); + if (!encoded_path) return NULL; + + char *url = path_join(ctx->webdav_url, encoded_path); + free(encoded_path); + return url; +} + +static void setup_curl_common(webdav_context_t *ctx, CURL *curl, + const char *url) { + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, ctx->request_timeout_sec); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, ctx->request_timeout_sec); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + + if (ctx->debug) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + } + + if (ctx->username && ctx->password) { + char *auth = NULL; + if (asprintf(&auth, "%s:%s", ctx->username, ctx->password) > 0) { + curl_easy_setopt(curl, CURLOPT_USERPWD, auth); + free(auth); + } + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + } +} + +int webdav_init(webdav_context_t *ctx, const config_t *cfg) { + if (!ctx || !cfg) return -EINVAL; + + memset(ctx, 0, sizeof(*ctx)); + + ctx->webdav_url = strdup(cfg->webdav_url); + if (cfg->username) ctx->username = strdup(cfg->username); + if (cfg->password) ctx->password = strdup(cfg->password); + ctx->cache_ttl_ms = cfg->cache_ttl_ms; + ctx->request_timeout_sec = cfg->request_timeout_sec; + ctx->debug = cfg->debug; + + if (!ctx->webdav_url) { + webdav_cleanup(ctx); + return -ENOMEM; + } + + if (pthread_mutex_init(&ctx->lock, NULL) != 0) { + webdav_cleanup(ctx); + return -ENOMEM; + } + + ctx->curl_handle = curl_easy_init(); + if (!ctx->curl_handle) { + webdav_cleanup(ctx); + return -ENOMEM; + } + + ctx->metadata_cache = cache_init(DEFAULT_MAX_CACHE_ENTRIES, cfg->cache_ttl_ms); + ctx->dir_cache = cache_init(DEFAULT_MAX_CACHE_ENTRIES, cfg->cache_ttl_ms); + + if (!ctx->metadata_cache || !ctx->dir_cache) { + webdav_cleanup(ctx); + return -ENOMEM; + } + + return 0; +} + +void webdav_cleanup(webdav_context_t *ctx) { + if (!ctx) return; + + if (ctx->curl_handle) { + curl_easy_cleanup(ctx->curl_handle); + ctx->curl_handle = NULL; + } + + if (ctx->metadata_cache) { + cache_destroy(ctx->metadata_cache); + ctx->metadata_cache = NULL; + } + + if (ctx->dir_cache) { + cache_destroy(ctx->dir_cache); + ctx->dir_cache = NULL; + } + + free(ctx->webdav_url); + free(ctx->username); + free(ctx->password); + + pthread_mutex_destroy(&ctx->lock); + memset(ctx, 0, sizeof(*ctx)); +} + +int webdav_get_stat(webdav_context_t *ctx, const char *path, struct stat *st) { + if (!ctx || !path || !st) return -EINVAL; + + if (cache_get_stat(ctx->metadata_cache, path, st) == 0) { + return 0; + } + + char *url = build_url(ctx, path); + if (!url) return -ENOMEM; + + pthread_mutex_lock(&ctx->lock); + + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + response_buffer_t response; + response_buffer_init(&response, 4096); + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PROPFIND"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, PROPFIND_BODY); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, "Depth: 0"); + headers = curl_slist_append(headers, "Content-Type: application/xml"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + pthread_mutex_unlock(&ctx->lock); + free(url); + + int result; + if (res != CURLE_OK) { + if (ctx->debug) { + fprintf(stderr, "CURL error: %s\n", curl_easy_strerror(res)); + } + result = -EIO; + } else if (http_code == 207 || http_code == 200) { + result = parse_propfind_response((char *)response.data, response.size, + st, NULL); + if (result == 0) { + cache_put_stat(ctx->metadata_cache, path, st); + } + } else { + if (ctx->debug) { + fprintf(stderr, "HTTP error: %ld\n", http_code); + } + result = http_status_to_errno(http_code); + } + + response_buffer_free(&response); + return result; +} + +int webdav_list_directory(webdav_context_t *ctx, const char *path, + dir_listing_t *listing) { + if (!ctx || !path || !listing) return -EINVAL; + + dir_listing_t *cached = NULL; + if (cache_get_dir(ctx->dir_cache, path, &cached) == 0) { + *listing = *cached; + free(cached); + return 0; + } + + char *url = build_url(ctx, path); + if (!url) return -ENOMEM; + + pthread_mutex_lock(&ctx->lock); + + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + response_buffer_t response; + response_buffer_init(&response, 8192); + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PROPFIND"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, PROPFIND_BODY); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, "Depth: 1"); + headers = curl_slist_append(headers, "Content-Type: application/xml"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + pthread_mutex_unlock(&ctx->lock); + free(url); + + int result; + if (res != CURLE_OK) { + result = -EIO; + } else if (http_code == 207 || http_code == 200) { + struct stat st; + result = parse_propfind_response((char *)response.data, response.size, + &st, listing); + if (result == 0) { + cache_put_dir(ctx->dir_cache, path, listing); + } + } else { + result = http_status_to_errno(http_code); + } + + response_buffer_free(&response); + return result; +} + +ssize_t webdav_read_file(webdav_context_t *ctx, const char *path, + off_t offset, size_t size, uint8_t *buffer) { + if (!ctx || !path || !buffer) return -EINVAL; + + char *url = build_url(ctx, path); + if (!url) return -ENOMEM; + + pthread_mutex_lock(&ctx->lock); + + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + response_buffer_t response; + response_buffer_init(&response, size + 1); + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + char range_header[64]; + snprintf(range_header, sizeof(range_header), "Range: bytes=%lld-%lld", + (long long)offset, (long long)(offset + size - 1)); + + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, range_header); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + pthread_mutex_unlock(&ctx->lock); + free(url); + + ssize_t result; + if (res != CURLE_OK) { + result = -EIO; + } else if (http_code == 206 || http_code == 200) { + size_t to_copy = (response.size < size) ? response.size : size; + memcpy(buffer, response.data, to_copy); + result = (ssize_t)to_copy; + } else { + result = http_status_to_errno(http_code); + } + + response_buffer_free(&response); + return result; +} + +ssize_t webdav_write_file(webdav_context_t *ctx, const char *path, + off_t offset, size_t size, const uint8_t *buffer) { + if (!ctx || !path || !buffer) return -EINVAL; + + char *url = build_url(ctx, path); + if (!url) return -ENOMEM; + + pthread_mutex_lock(&ctx->lock); + + uint8_t *full_content = NULL; + size_t full_size = 0; + + if (offset > 0) { + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + response_buffer_t response; + response_buffer_init(&response, 65536); + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + CURLcode res = curl_easy_perform(curl); + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && (http_code == 200 || http_code == 404)) { + full_size = (size_t)offset + size; + if (response.size > full_size) { + full_size = response.size; + } + + full_content = calloc(1, full_size); + if (!full_content) { + response_buffer_free(&response); + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + if (http_code == 200 && response.size > 0) { + memcpy(full_content, response.data, response.size); + } + + memcpy(full_content + offset, buffer, size); + } + + response_buffer_free(&response); + } else { + full_content = malloc(size); + if (!full_content) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + memcpy(full_content, buffer, size); + full_size = size; + } + + CURL *curl = curl_easy_init(); + if (!curl) { + free(full_content); + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)full_size); + + response_buffer_t upload_buf; + upload_buf.data = full_content; + upload_buf.size = 0; + upload_buf.capacity = full_size; + + curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback); + curl_easy_setopt(curl, CURLOPT_READDATA, &upload_buf); + + CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + free(full_content); + pthread_mutex_unlock(&ctx->lock); + free(url); + + cache_invalidate(ctx->metadata_cache, path); + char *parent = path_parent(path); + if (parent) { + cache_invalidate(ctx->dir_cache, parent); + free(parent); + } + + if (res != CURLE_OK) { + return -EIO; + } + + if (http_code == 200 || http_code == 201 || http_code == 204) { + return (ssize_t)size; + } + + return http_status_to_errno(http_code); +} + +int webdav_create_file(webdav_context_t *ctx, const char *path) { + if (!ctx || !path) return -EINVAL; + + char *url = build_url(ctx, path); + if (!url) return -ENOMEM; + + pthread_mutex_lock(&ctx->lock); + + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)0); + + CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + pthread_mutex_unlock(&ctx->lock); + free(url); + + cache_invalidate(ctx->metadata_cache, path); + char *parent = path_parent(path); + if (parent) { + cache_invalidate(ctx->dir_cache, parent); + free(parent); + } + + if (res != CURLE_OK) { + return -EIO; + } + + if (http_code == 200 || http_code == 201 || http_code == 204) { + return 0; + } + + return http_status_to_errno(http_code); +} + +int webdav_mkdir(webdav_context_t *ctx, const char *path) { + if (!ctx || !path) return -EINVAL; + + char *url = build_url(ctx, path); + if (!url) return -ENOMEM; + + size_t len = strlen(url); + if (len > 0 && url[len - 1] != '/') { + char *new_url = realloc(url, len + 2); + if (!new_url) { + free(url); + return -ENOMEM; + } + url = new_url; + url[len] = '/'; + url[len + 1] = '\0'; + } + + pthread_mutex_lock(&ctx->lock); + + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "MKCOL"); + + CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + pthread_mutex_unlock(&ctx->lock); + free(url); + + cache_invalidate(ctx->metadata_cache, path); + char *parent = path_parent(path); + if (parent) { + cache_invalidate(ctx->dir_cache, parent); + free(parent); + } + + if (res != CURLE_OK) { + return -EIO; + } + + if (http_code == 201) { + return 0; + } + + if (http_code == 405) { + return -EEXIST; + } + + return http_status_to_errno(http_code); +} + +int webdav_delete(webdav_context_t *ctx, const char *path) { + if (!ctx || !path) return -EINVAL; + + char *url = build_url(ctx, path); + if (!url) return -ENOMEM; + + pthread_mutex_lock(&ctx->lock); + + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + + CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + pthread_mutex_unlock(&ctx->lock); + free(url); + + cache_invalidate(ctx->metadata_cache, path); + cache_invalidate_prefix(ctx->dir_cache, path); + char *parent = path_parent(path); + if (parent) { + cache_invalidate(ctx->dir_cache, parent); + free(parent); + } + + if (res != CURLE_OK) { + return -EIO; + } + + if (http_code == 200 || http_code == 204) { + return 0; + } + + return http_status_to_errno(http_code); +} + +int webdav_rename(webdav_context_t *ctx, const char *old_path, + const char *new_path) { + if (!ctx || !old_path || !new_path) return -EINVAL; + + char *src_url = build_url(ctx, old_path); + char *dst_url = build_url(ctx, new_path); + + if (!src_url || !dst_url) { + free(src_url); + free(dst_url); + return -ENOMEM; + } + + pthread_mutex_lock(&ctx->lock); + + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(src_url); + free(dst_url); + return -ENOMEM; + } + + setup_curl_common(ctx, curl, src_url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "MOVE"); + + char *dest_header = NULL; + if (asprintf(&dest_header, "Destination: %s", dst_url) < 0) { + curl_easy_cleanup(curl); + pthread_mutex_unlock(&ctx->lock); + free(src_url); + free(dst_url); + return -ENOMEM; + } + + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, dest_header); + headers = curl_slist_append(headers, "Overwrite: T"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_slist_free_all(headers); + free(dest_header); + curl_easy_cleanup(curl); + pthread_mutex_unlock(&ctx->lock); + free(src_url); + free(dst_url); + + cache_invalidate(ctx->metadata_cache, old_path); + cache_invalidate(ctx->metadata_cache, new_path); + cache_invalidate_prefix(ctx->dir_cache, old_path); + + char *old_parent = path_parent(old_path); + char *new_parent = path_parent(new_path); + if (old_parent) { + cache_invalidate(ctx->dir_cache, old_parent); + free(old_parent); + } + if (new_parent) { + cache_invalidate(ctx->dir_cache, new_parent); + free(new_parent); + } + + if (res != CURLE_OK) { + return -EIO; + } + + if (http_code == 200 || http_code == 201 || http_code == 204) { + return 0; + } + + return http_status_to_errno(http_code); +} + +int webdav_truncate(webdav_context_t *ctx, const char *path, off_t size) { + if (!ctx || !path) return -EINVAL; + + if (size == 0) { + return webdav_create_file(ctx, path); + } + + char *url = build_url(ctx, path); + if (!url) return -ENOMEM; + + pthread_mutex_lock(&ctx->lock); + + CURL *curl = curl_easy_init(); + if (!curl) { + pthread_mutex_unlock(&ctx->lock); + free(url); + return -ENOMEM; + } + + response_buffer_t response; + response_buffer_init(&response, (size_t)size + 1); + + setup_curl_common(ctx, curl, url); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + char range_header[64]; + snprintf(range_header, sizeof(range_header), "Range: bytes=0-%lld", + (long long)(size - 1)); + + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, range_header); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl); + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + int result = 0; + if (res != CURLE_OK) { + result = -EIO; + } else if (http_code == 206 || http_code == 200) { + CURL *put_curl = curl_easy_init(); + if (!put_curl) { + result = -ENOMEM; + } else { + setup_curl_common(ctx, put_curl, url); + curl_easy_setopt(put_curl, CURLOPT_UPLOAD, 1L); + + size_t upload_size = (response.size < (size_t)size) ? + response.size : (size_t)size; + curl_easy_setopt(put_curl, CURLOPT_INFILESIZE_LARGE, + (curl_off_t)upload_size); + + response_buffer_t upload_buf; + upload_buf.data = response.data; + upload_buf.size = 0; + upload_buf.capacity = upload_size; + + curl_easy_setopt(put_curl, CURLOPT_READFUNCTION, read_callback); + curl_easy_setopt(put_curl, CURLOPT_READDATA, &upload_buf); + + res = curl_easy_perform(put_curl); + curl_easy_getinfo(put_curl, CURLINFO_RESPONSE_CODE, &http_code); + curl_easy_cleanup(put_curl); + + if (res != CURLE_OK) { + result = -EIO; + } else if (http_code == 200 || http_code == 201 || + http_code == 204) { + result = 0; + } else { + result = http_status_to_errno(http_code); + } + } + } else { + result = http_status_to_errno(http_code); + } + + response_buffer_free(&response); + pthread_mutex_unlock(&ctx->lock); + free(url); + + cache_invalidate(ctx->metadata_cache, path); + + return result; +} + +file_handle_t *file_handle_create(const char *path) { + if (!path) return NULL; + + file_handle_t *fh = calloc(1, sizeof(file_handle_t)); + if (!fh) return NULL; + + fh->path = strdup(path); + if (!fh->path) { + free(fh); + return NULL; + } + + fh->write_buffer_size = DEFAULT_WRITE_BUFFER_SIZE; + fh->write_buffer = malloc(fh->write_buffer_size); + if (!fh->write_buffer) { + free(fh->path); + free(fh); + return NULL; + } + + fh->write_buffer_pos = 0; + fh->dirty = 0; + + return fh; +} + +void file_handle_destroy(file_handle_t *fh) { + if (!fh) return; + + free(fh->path); + free(fh->etag); + free(fh->write_buffer); + free(fh); +} + +int file_handle_flush(webdav_context_t *ctx, file_handle_t *fh) { + if (!ctx || !fh) return -EINVAL; + + if (!fh->dirty || fh->write_buffer_pos == 0) { + return 0; + } + + ssize_t written = webdav_write_file(ctx, fh->path, 0, + fh->write_buffer_pos, + fh->write_buffer); + + if (written < 0) { + return (int)written; + } + + fh->write_buffer_pos = 0; + fh->dirty = 0; + return 0; +} diff --git a/webdav/SmartSelect_20260103_165740_Chrome.jpg b/webdav/SmartSelect_20260103_165740_Chrome.jpg new file mode 100644 index 0000000..1a01999 Binary files /dev/null and b/webdav/SmartSelect_20260103_165740_Chrome.jpg differ diff --git a/webdav/keys.txt b/webdav/keys.txt new file mode 100644 index 0000000..bcb066c --- /dev/null +++ b/webdav/keys.txt @@ -0,0 +1,24 @@ +export OPENAI_API_ADMIN_KEY="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" + +export OPENROUTER_API_KEY="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" + +export OPENAI_API_KEY="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" +export R_KEY="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" +export ANTHROPIC_API_KEY="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" +export PINECONE_API_KEY="pcsk_cmhgd_MHwBJk8XCcipoyXQdSGo2Rq56xZDTRKjYPbrvFzREeJgQLJed3g5uBoxTzRzGb7" +export GEMINI_API_KEY="AIzaSyCLJQuAzBCVKPgVKn4IsDXkOBPUjqH7dR0" +export TOGETHER_AI_API_KEY="daeb23017d865e8aa657643d6d2f11d69e099d3a5a1036534da28d7db6b47792" +export DEM_MOLODETZ_TOKEN="sk-5ea2bcf6e30c4a819f405963d7191c84" + +# Related keys/tokens from the file +export R__KEY="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" +export ANAKIN="APS-8GtkHpQVNqNYP6bJGU7PcAbPJJH4KEJ6" + +export OR_KEY="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" + +export EXA_API_KEY="d09b922b-7946-42f6-a95e-e421faa68663" +export ZAI_API_KEY=aaf5fb68732c40de9472d980b23054c9.eAJs7s74sh7VDm9O +# Related model configuration +export R_MODEL="gpt-4.1-mini" + +export EYE_API_KEY="none-of-your-business" diff --git a/webdav/tea/docker-compose.yaml~ b/webdav/tea/docker-compose.yaml~ new file mode 100644 index 0000000..6020179 --- /dev/null +++ b/webdav/tea/docker-compose.yaml~ @@ -0,0 +1,23 @@ +version: "3" + +networks: + gitea: + external: false + +services: + server: + image: gitea/gitea:1.22.4 + container_name: tea + environment: + - USER_UID=1000 + - USER_GID=1000 + restart: always + networks: + - gitea + volumes: + - ./data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "127.0.0.1:8082:3000" + - "0.0.0.0:22:22"