diff --git a/src/get_handler.c b/src/get_handler.c new file mode 100644 index 0000000..86ee33c --- /dev/null +++ b/src/get_handler.c @@ -0,0 +1,6 @@ +static enum MHD_Result handle_get(struct MHD_Connection *connection, SV url, SV base_path, SV username) { + (void)url; + (void)username; + (void)base_path; + return send_response(connection, 200, sv("TODO: Get\n"), false); +} diff --git a/src/id_store.c b/src/id_store.c new file mode 100644 index 0000000..de58e58 --- /dev/null +++ b/src/id_store.c @@ -0,0 +1,35 @@ + +typedef struct { + Noh_String path; + Noh_String id; +} Id; + +typedef struct { + Id *elems; + size_t count; + size_t capacity; +} Id_Store; + +Id_Store id_store = {0}; + +SV get_id(SV path) { + for (size_t i = 0; i < id_store.count; i++) { + if (noh_sv_eq(path, noh_sv_from_string(id_store.elems[i].path))) + return noh_sv_from_string(id_store.elems[i].id); + } + + uuid_t uuid; + uuid_generate(uuid); + char uuid_str[36]; + uuid_unparse(uuid, uuid_str); + + Noh_String new_id = {0}; + noh_string_append_cstr(&new_id, "urn:uuid:"); + noh_string_append_cstr(&new_id, uuid_str); + + Noh_String stored_path = {0}; + noh_string_append_sv(&stored_path, path); + + noh_da_append(&id_store, ((Id){ .path = stored_path, .id = new_id })); + return noh_sv_from_string(new_id); +} diff --git a/src/library.c b/src/library.c new file mode 100644 index 0000000..ca718a9 --- /dev/null +++ b/src/library.c @@ -0,0 +1,93 @@ +typedef struct { + bool is_dir; + SV name; + SV full_path; + struct tm modified_at; +} Library_Entry; + +typedef struct { + Library_Entry *elems; + size_t count; + size_t capacity; +} Library_Entries; + +/* + * Helper to format an entry's last modified date into a temporary buffer. + * The same buffer will be reused the next time this function is called. + */ +static char *format_time(Library_Entry entry) { + static char time_buffer[32]; + strftime((char*)&time_buffer, 32, "%Y-%m-%dT%H:%M:%S.0000000Z", &entry.modified_at); + return (char*)time_buffer; +} + +static int compare_library_entries(const void *a, const void *b) { + Library_Entry lea = *(Library_Entry*)a; + Library_Entry leb = *(Library_Entry*)b; + + if (lea.is_dir && !leb.is_dir) return -1; + if (!lea.is_dir && leb.is_dir) return 1; + + return noh_sv_compare(lea.name, leb.name); +} + +static void set_modified_time(Library_Entry *entry) { + struct stat attr; + if (stat(noh_arena_sprintf(&temp, Nsv_Fmt, entry->full_path), &attr) == 0) { + struct tm *timeinfo = localtime(&attr.st_mtime); + entry->modified_at = *timeinfo; + } else { + entry->modified_at = (struct tm){0}; + } +} + +#define build_path(url, first, ...) build_path_((url), (first), __VA_ARGS__, NULL) +static Noh_String build_path_(bool url, SV *first, ...) { + va_list args; + SV *elem_ = first; + va_start(args, first); + bool start = true; + Noh_String result = {0}; + if (url) noh_string_append_cstr(&result, "/"); + + while (elem_ != NULL) { + SV elem = *elem_; + while (elem.count > 0) { + SV component = noh_sv_chop_by_delim(&elem, '/'); + if (noh_sv_eq(component, sv(".."))) continue; + + if (component.count > 0) { + if (start) start = false; else noh_string_append_cstr(&result, "/"); + noh_string_append_sv(&result, component); + } + } + + elem_ = va_arg(args, SV *); + } + va_end(args); + + return result; +} + +/** + * Creates a Library entry from a dirent structure. + */ +static bool create_entry(struct dirent *dir_entry, const char *parent_path, Library_Entry *result) { + SV name = sv(dir_entry->d_name); + SV parent_path_sv = sv(parent_path); + Noh_String full_path = build_path(false, &parent_path_sv, &name); + int type = dir_entry->d_type; + + // Check if the entry is relevant. + if (type != DT_DIR && type != DT_REG) return false; + if (noh_sv_eq(name, sv(".")) || noh_sv_eq(name, sv(".."))) return false; + if (type == DT_REG && !noh_sv_ends_with_ci(name, sv(".epub"))) return false; + + // Set entry data. + result->is_dir = type == DT_DIR; + result->full_path = noh_sv_from_string(full_path); + result->name = noh_sv_copy_cstr(&temp, dir_entry->d_name); + set_modified_time(result); + + return true; +} diff --git a/src/main.c b/src/main.c index 2fe7f9d..0d7aa92 100644 --- a/src/main.c +++ b/src/main.c @@ -16,39 +16,13 @@ This will ignore any auth headers and always log in with username demo. Noh_Arena temp; -typedef struct { - Noh_String path; - Noh_String id; -} Id; +#include "id_store.c" +#include "library.c" typedef struct { - Id *elems; - size_t count; - size_t capacity; -} Id_Store; - -Id_Store id_store = {0}; - -static SV get_id(SV path) { - for (size_t i = 0; i < id_store.count; i++) { - if (noh_sv_eq(path, noh_sv_from_string(id_store.elems[i].path))) return noh_sv_from_string(id_store.elems[i].id); - } - - uuid_t uuid; - uuid_generate(uuid); - char uuid_str[36]; - uuid_unparse(uuid, uuid_str); - - Noh_String new_id = {0}; - noh_string_append_cstr(&new_id, "urn:uuid:"); - noh_string_append_cstr(&new_id, uuid_str); - - Noh_String stored_path = {0}; - noh_string_append_sv(&stored_path, path); - - noh_da_append(&id_store, ((Id){ .path = stored_path, .id = new_id })); - return noh_sv_from_string(new_id); -} + SV base_path; + SV library_name; +} Settings; static enum MHD_Result send_response(struct MHD_Connection *connection, int status, SV message, bool xml) { struct MHD_Response *response = MHD_create_response_from_buffer( @@ -62,212 +36,8 @@ static enum MHD_Result send_response(struct MHD_Connection *connection, int stat return ret; } -typedef struct { - bool is_dir; - SV name; - SV full_path; - struct tm modified_at; -} Library_Entry; - -typedef struct { - Library_Entry *elems; - size_t count; - size_t capacity; -} Library_Entries; - -int compare_library_entries(const void *a, const void *b) { - Library_Entry lea = *(Library_Entry*)a; - Library_Entry leb = *(Library_Entry*)b; - - if (lea.is_dir && !leb.is_dir) return -1; - if (!lea.is_dir && leb.is_dir) return 1; - - return noh_sv_compare(lea.name, leb.name); -} - -#define build_path(url, first, ...) build_path_((url), (first), __VA_ARGS__, NULL) -static Noh_String build_path_(bool url, SV *first, ...) { - va_list args; - SV *elem_ = first; - va_start(args, first); - bool start = true; - Noh_String result = {0}; - if (url) noh_string_append_cstr(&result, "/"); - - while (elem_ != NULL) { - SV elem = *elem_; - while (elem.count > 0) { - SV component = noh_sv_chop_by_delim(&elem, '/'); - if (noh_sv_eq(component, sv(".."))) continue; - - if (component.count > 0) { - if (start) start = false; else noh_string_append_cstr(&result, "/"); - noh_string_append_sv(&result, component); - } - } - - elem_ = va_arg(args, SV *); - } - va_end(args); - - return result; -} - -static SV path_to_name(SV path) { - while (true) { - SV component = noh_sv_chop_by_delim(&path, '/'); - if (path.count == 0) return component; - } -} - -static void set_modified_time(Library_Entry *entry) { - struct stat attr; - if (stat(noh_arena_sprintf(&temp, Nsv_Fmt, entry->full_path), &attr) == 0) { - struct tm *timeinfo = localtime(&attr.st_mtime); - entry->modified_at = *timeinfo; - } else { - entry->modified_at = (struct tm){0}; - } -} - -/** - * Creates a Library entry from a dirent structure. - */ -static bool create_entry(struct dirent *dir_entry, const char *parent_path, Library_Entry *result) { - SV name = sv(dir_entry->d_name); - SV parent_path_sv = sv(parent_path); - Noh_String full_path = build_path(false, &parent_path_sv, &name); - int type = dir_entry->d_type; - - // Check if the entry is relevant. - if (type != DT_DIR && type != DT_REG) return false; - if (noh_sv_eq(name, sv(".")) || noh_sv_eq(name, sv(".."))) return false; - if (type == DT_REG && !noh_sv_ends_with_ci(name, sv(".epub"))) return false; - - // Set entry data. - result->is_dir = type == DT_DIR; - result->full_path = noh_sv_from_string(full_path); - result->name = noh_sv_copy_cstr(&temp, dir_entry->d_name); - set_modified_time(result); - - return true; -} - -/* - * Helper to format an entry's last modified date into a temporary buffer. - * The same buffer will be reused the next time this function is called. - */ -static char *format_time(Library_Entry entry) { - static char time_buffer[32]; - strftime((char*)&time_buffer, 32, "%Y-%m-%dT%H:%M:%S.0000000Z", &entry.modified_at); - return (char*)time_buffer; -} - -static void append_entry(Noh_String *response, SV url, Library_Entry entry) { - noh_string_append_cstr(response, ""); - noh_string_append_cstr(response, noh_arena_sprintf(&temp, ""Nsv_Fmt"", Nsv_Arg(get_id(entry.full_path)))); - noh_string_append_cstr(response, noh_arena_sprintf(&temp, ""Nsv_Fmt"", entry.name)); - noh_string_append_cstr(response, noh_arena_sprintf(&temp, "%s", format_time(entry))); - if (entry.is_dir) { - SV opds = sv("opds"); - Noh_String entry_url = build_path(true, &opds, &url, &entry.name); - noh_string_append_cstr(response, noh_arena_sprintf(&temp, "", Nsv_Arg(entry_url))); - noh_string_free(&entry_url); - } else { - SV get = sv("get"); - Noh_String entry_url = build_path(true, &get, &url, &entry.name); - noh_string_append_cstr(response, noh_arena_sprintf(&temp, "", Nsv_Arg(entry_url))); - noh_string_free(&entry_url); - } - noh_string_append_cstr(response, ""); -} - -static Noh_String create_feed_response(Library_Entry self_path, SV url, Library_Entries list) { - (void)self_path; - Noh_String response = {0}; - - noh_string_append_cstr(&response, ""); - noh_string_append_cstr(&response, noh_arena_sprintf(&temp, ""Nsv_Fmt"", Nsv_Arg(get_id(self_path.full_path)))); - noh_string_append_cstr(&response, noh_arena_sprintf(&temp, ""Nsv_Fmt"", self_path.name)); - noh_string_append_cstr(&response, noh_arena_sprintf(&temp, "%s", format_time(self_path))); - - SV opds = sv("opds"); - Noh_String self_url = build_path(true, &opds, &url); - noh_string_append_cstr(&response, noh_arena_sprintf(&temp, "", Nsv_Arg(self_url))); - noh_string_free(&self_url); - - noh_string_append_cstr(&response, ""); - - for (size_t i = 0; i < list.count; i++) append_entry(&response, url, list.elems[i]); - noh_string_append_cstr(&response, ""); - - return response; -} - -typedef struct { - SV base_path; - SV library_name; -} Settings; - -static enum MHD_Result handle_opds(struct MHD_Connection *connection, SV url, Settings settings, SV username) { - noh_arena_save(&temp); - Noh_String fs_path = build_path(false, &settings.base_path, &username, &url); - Noh_String fs_path_cstr = fs_path; - noh_string_append_null(&fs_path_cstr); - - Library_Entry self_entry = { - .is_dir = true, - .full_path = noh_sv_from_string(fs_path), - .name = (url.count == 0) ? settings.library_name : path_to_name(noh_sv_from_string(fs_path)), - .modified_at = {0}, - }; - set_modified_time(&self_entry); - noh_log(NOH_INFO, "Physical path: "Nsv_Fmt, Nsv_Arg(self_entry.full_path)); - - DIR *dir = opendir(fs_path_cstr.elems); - if (dir) { - Library_Entries list = {0}; - - // Build up list of entries. - struct dirent *entry = noh_arena_alloc(&temp, sizeof(struct dirent)); - while ((entry = readdir(dir)) != NULL) { - Library_Entry result = {0}; - if (create_entry(entry, fs_path_cstr.elems, &result)) noh_da_append(&list, result); - } - - // Sort entries, folders first, then by name. - if (list.count > 1) qsort(list.elems, list.count, sizeof(list.elems[0]), compare_library_entries); - - // Create and send response. - Noh_String response = create_feed_response(self_entry, url, list); - bool result = send_response(connection, 200, noh_sv_from_string(response), true); - - // Cleanup. - noh_da_free(&list); - noh_string_free(&response); - noh_arena_reset(&temp); - closedir(dir); - - return result; - - } else if (errno == ENOENT) { - closedir(dir); - noh_arena_reset(&temp); - return send_response(connection, 404, sv(""), false); - - } else { - noh_log(NOH_ERROR, "Opening directory failed."); - noh_arena_reset(&temp); - return send_response(connection, 500, sv(""), false); - } -} - -static enum MHD_Result handle_get(struct MHD_Connection *connection, SV url, SV base_path, SV username) { - (void)url; - (void)username; - (void)base_path; - return send_response(connection, 200, sv("TODO: Get\n"), false); -} +#include "opds_handler.c" +#include "get_handler.c" bool is_slash(const char c) { return c == '/'; } diff --git a/src/opds_handler.c b/src/opds_handler.c new file mode 100644 index 0000000..1836849 --- /dev/null +++ b/src/opds_handler.c @@ -0,0 +1,100 @@ +static SV path_to_name(SV path) { + while (true) { + SV component = noh_sv_chop_by_delim(&path, '/'); + if (path.count == 0) return component; + } +} + +static void append_entry(Noh_String *response, SV url, Library_Entry entry) { + noh_string_append_cstr(response, ""); + noh_string_append_cstr(response, noh_arena_sprintf(&temp, ""Nsv_Fmt"", Nsv_Arg(get_id(entry.full_path)))); + noh_string_append_cstr(response, noh_arena_sprintf(&temp, ""Nsv_Fmt"", entry.name)); + noh_string_append_cstr(response, noh_arena_sprintf(&temp, "%s", format_time(entry))); + if (entry.is_dir) { + SV opds = sv("opds"); + Noh_String entry_url = build_path(true, &opds, &url, &entry.name); + noh_string_append_cstr(response, noh_arena_sprintf(&temp, "", Nsv_Arg(entry_url))); + noh_string_free(&entry_url); + } else { + SV get = sv("get"); + Noh_String entry_url = build_path(true, &get, &url, &entry.name); + noh_string_append_cstr(response, noh_arena_sprintf(&temp, "", Nsv_Arg(entry_url))); + noh_string_free(&entry_url); + } + noh_string_append_cstr(response, ""); +} + +static Noh_String create_feed_response(Library_Entry self_path, SV url, Library_Entries list) { + (void)self_path; + Noh_String response = {0}; + + noh_string_append_cstr(&response, ""); + noh_string_append_cstr(&response, noh_arena_sprintf(&temp, ""Nsv_Fmt"", Nsv_Arg(get_id(self_path.full_path)))); + noh_string_append_cstr(&response, noh_arena_sprintf(&temp, ""Nsv_Fmt"", self_path.name)); + noh_string_append_cstr(&response, noh_arena_sprintf(&temp, "%s", format_time(self_path))); + + SV opds = sv("opds"); + Noh_String self_url = build_path(true, &opds, &url); + noh_string_append_cstr(&response, noh_arena_sprintf(&temp, "", Nsv_Arg(self_url))); + noh_string_free(&self_url); + + noh_string_append_cstr(&response, ""); + + for (size_t i = 0; i < list.count; i++) append_entry(&response, url, list.elems[i]); + noh_string_append_cstr(&response, ""); + + return response; +} + +static enum MHD_Result handle_opds(struct MHD_Connection *connection, SV url, Settings settings, SV username) { + noh_arena_save(&temp); + Noh_String fs_path = build_path(false, &settings.base_path, &username, &url); + Noh_String fs_path_cstr = fs_path; + noh_string_append_null(&fs_path_cstr); + + Library_Entry self_entry = { + .is_dir = true, + .full_path = noh_sv_from_string(fs_path), + .name = (url.count == 0) ? settings.library_name : path_to_name(noh_sv_from_string(fs_path)), + .modified_at = {0}, + }; + set_modified_time(&self_entry); + noh_log(NOH_INFO, "Physical path: "Nsv_Fmt, Nsv_Arg(self_entry.full_path)); + + DIR *dir = opendir(fs_path_cstr.elems); + if (dir) { + Library_Entries list = {0}; + + // Build up list of entries. + struct dirent *entry = noh_arena_alloc(&temp, sizeof(struct dirent)); + while ((entry = readdir(dir)) != NULL) { + Library_Entry result = {0}; + if (create_entry(entry, fs_path_cstr.elems, &result)) noh_da_append(&list, result); + } + + // Sort entries, folders first, then by name. + if (list.count > 1) qsort(list.elems, list.count, sizeof(list.elems[0]), compare_library_entries); + + // Create and send response. + Noh_String response = create_feed_response(self_entry, url, list); + bool result = send_response(connection, 200, noh_sv_from_string(response), true); + + // Cleanup. + noh_da_free(&list); + noh_string_free(&response); + noh_arena_reset(&temp); + closedir(dir); + + return result; + + } else if (errno == ENOENT) { + closedir(dir); + noh_arena_reset(&temp); + return send_response(connection, 404, sv(""), false); + + } else { + noh_log(NOH_ERROR, "Opening directory failed."); + noh_arena_reset(&temp); + return send_response(connection, 500, sv(""), false); + } +}