chore: initialize project structure with empty repository scaffold

This commit is contained in:
2026-01-06 13:23:31 +00:00
commit f88c75fb05
23 changed files with 2698 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
# retoor <retoor@molodetz.nl>
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"
+147
View File
@@ -0,0 +1,147 @@
# fusedav
Author: retoor <retoor@molodetz.nl>
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
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
+55
View File
@@ -0,0 +1,55 @@
/* retoor <retoor@molodetz.nl> */
#ifndef FUSEDAV_CACHE_H
#define FUSEDAV_CACHE_H
#include <pthread.h>
#include <stdint.h>
#include <sys/stat.h>
#include <time.h>
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
+28
View File
@@ -0,0 +1,28 @@
/* retoor <retoor@molodetz.nl> */
#ifndef FUSEDAV_CONFIG_H
#define FUSEDAV_CONFIG_H
#include <stdint.h>
#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
+15
View File
@@ -0,0 +1,15 @@
/* retoor <retoor@molodetz.nl> */
#ifndef FUSEDAV_OPERATIONS_H
#define FUSEDAV_OPERATIONS_H
#define FUSE_USE_VERSION 31
#include <fuse3/fuse.h>
#include "webdav.h"
extern struct fuse_operations fusedav_operations;
void operations_init(webdav_context_t *ctx);
#endif
+28
View File
@@ -0,0 +1,28 @@
/* retoor <retoor@molodetz.nl> */
#ifndef FUSEDAV_UTILS_H
#define FUSEDAV_UTILS_H
#include <stddef.h>
#include <sys/stat.h>
#include <libxml/parser.h>
#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
+56
View File
@@ -0,0 +1,56 @@
/* retoor <retoor@molodetz.nl> */
#ifndef FUSEDAV_WEBDAV_H
#define FUSEDAV_WEBDAV_H
#include <curl/curl.h>
#include <pthread.h>
#include <stdint.h>
#include <sys/stat.h>
#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
+368
View File
@@ -0,0 +1,368 @@
/* retoor <retoor@molodetz.nl> */
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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);
}
+174
View File
@@ -0,0 +1,174 @@
/* retoor <retoor@molodetz.nl> */
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#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));
}
+124
View File
@@ -0,0 +1,124 @@
/* retoor <retoor@molodetz.nl> */
#define FUSE_USE_VERSION 31
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/stat.h>
#include <curl/curl.h>
#include <fuse3/fuse.h>
#include <libxml/parser.h>
#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;
}
+350
View File
@@ -0,0 +1,350 @@
/* retoor <retoor@molodetz.nl> */
#define FUSE_USE_VERSION 31
#define _GNU_SOURCE
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fuse3/fuse.h>
#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;
}
+403
View File
@@ -0,0 +1,403 @@
/* retoor <retoor@molodetz.nl> */
#define _GNU_SOURCE
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>
#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;
}
+860
View File
@@ -0,0 +1,860 @@
/* retoor <retoor@molodetz.nl> */
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#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 =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
"<D:propfind xmlns:D=\"DAV:\">"
"<D:prop>"
"<D:resourcetype/>"
"<D:getcontentlength/>"
"<D:getlastmodified/>"
"<D:getetag/>"
"<D:displayname/>"
"</D:prop>"
"</D:propfind>";
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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+24
View File
@@ -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"
+23
View File
@@ -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"