Initial commit.
This commit is contained in:
commit
c1afdc3448
43
Makefile
Normal file
43
Makefile
Normal 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
README.md
Normal file
147
README.md
Normal 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
build/cache.o
Normal file
BIN
build/cache.o
Normal file
Binary file not shown.
BIN
build/config.o
Normal file
BIN
build/config.o
Normal file
Binary file not shown.
BIN
build/main.o
Normal file
BIN
build/main.o
Normal file
Binary file not shown.
BIN
build/operations.o
Normal file
BIN
build/operations.o
Normal file
Binary file not shown.
BIN
build/utils.o
Normal file
BIN
build/utils.o
Normal file
Binary file not shown.
BIN
build/webdav.o
Normal file
BIN
build/webdav.o
Normal file
Binary file not shown.
55
include/cache.h
Normal file
55
include/cache.h
Normal 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
include/config.h
Normal file
28
include/config.h
Normal 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
include/operations.h
Normal file
15
include/operations.h
Normal 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
include/utils.h
Normal file
28
include/utils.h
Normal 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
include/webdav.h
Normal file
56
include/webdav.h
Normal 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
src/cache.c
Normal file
368
src/cache.c
Normal 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
src/config.c
Normal file
174
src/config.c
Normal 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
src/main.c
Normal file
124
src/main.c
Normal 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
src/operations.c
Normal file
350
src/operations.c
Normal 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
src/utils.c
Normal file
403
src/utils.c
Normal 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
src/webdav.c
Normal file
860
src/webdav.c
Normal 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;
|
||||
}
|
||||
BIN
webdav/SmartSelect_20260103_165740_Chrome.jpg
Normal file
BIN
webdav/SmartSelect_20260103_165740_Chrome.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
24
webdav/keys.txt
Normal file
24
webdav/keys.txt
Normal 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
webdav/tea/docker-compose.yaml~
Normal file
23
webdav/tea/docker-compose.yaml~
Normal 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"
|
||||
Loading…
Reference in New Issue
Block a user