Mercurial
diff mrjunejune/main.c @ 201:6cdee35a7ba9
[MrJuneJune] notes
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 07:07:50 -0800 |
| parents | 90dfcef375fb |
| children | b9b184b3303c |
line wrap: on
line diff
--- a/mrjunejune/main.c Sat Feb 14 16:32:24 2026 -0800 +++ b/mrjunejune/main.c Sun Feb 15 07:07:50 2026 -0800 @@ -1,7 +1,11 @@ #include "seobeo/seobeo.h" #include "markdown_converter/markdown_to_html.h" #include "s3/s3_uploader.h" +#include "deita/deita.h" #include <time.h> +#include <sys/stat.h> +#include <stdarg.h> +#include <pthread.h> // UUID + /tmp/ + format (max 4) #define TMP_FILE_LENGTH 47 @@ -10,12 +14,25 @@ volatile sig_atomic_t stop_server = 0; static _Atomic uint32_t counter = 0; +// Media Processing Context for background threads +typedef struct { + int64 media_id; + char s3_key_original[512]; + char s3_key_processed[512]; + char content_type[128]; + char access_token[256]; + S3_Config s3_config; +} Media_Processing_Context; + // Server configuration (loaded from .config) static char g_upload_auth_token[256] = {0}; static char g_s3_region[64] = "us-west-2"; static char g_s3_bucket[128] = "mrjunejune"; +static char g_s3_cloudfront_url[256] = {0}; +static char g_db_path[256] = "mrjunejune/data/mrjunejune.db"; static int g_s3_url_expires = 3600; static S3_Config g_s3_config = {0}; +static Deita_Connection *g_db_connection = NULL; static void load_config(const char *config_path) { @@ -60,12 +77,105 @@ { g_s3_url_expires = atoi(value); } + else if (strcmp(key, "S3_CLOUDFRONT_URL") == 0) + { + strncpy(g_s3_cloudfront_url, value, sizeof(g_s3_cloudfront_url) - 1); + } + else if (strcmp(key, "DB_PATH") == 0) + { + strncpy(g_db_path, value, sizeof(g_db_path) - 1); + } } fclose(f); - printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d\n", + printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d, cloudfront=%s, db=%s\n", g_upload_auth_token[0] ? "***" : "(empty)", - g_s3_region, g_s3_bucket, g_s3_url_expires); + g_s3_region, g_s3_bucket, g_s3_url_expires, + g_s3_cloudfront_url[0] ? g_s3_cloudfront_url : "(none)", + g_db_path); +} + +static void init_database(void) +{ + // Create data directory if needed + char *last_slash = strrchr(g_db_path, '/'); + if (last_slash) + { + char dir_path[256]; + size_t dir_len = last_slash - g_db_path; + strncpy(dir_path, g_db_path, dir_len); + dir_path[dir_len] = '\0'; + mkdir(dir_path, 0755); + } + + g_db_connection = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, g_db_path); + if (!g_db_connection || !Deita_Connection_Is_Open(g_db_connection)) + { + printf("[DB] ERROR: Failed to open database at %s\n", g_db_path); + return; + } + + // Create editor_content table + const char *create_table = + "CREATE TABLE IF NOT EXISTS editor_content (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " access_token TEXT NOT NULL," + " doc_id TEXT NOT NULL," + " content TEXT," + " created_at INTEGER DEFAULT (strftime('%s', 'now'))," + " updated_at INTEGER DEFAULT (strftime('%s', 'now'))," + " UNIQUE(access_token, doc_id)" + ")"; + + int32 result = Deita_Query_Execute_Update(g_db_connection, create_table); + if (result < 0) + { + printf("[DB] ERROR: Failed to create editor_content table\n"); + } + + // Create media_uploads table + const char *create_media_uploads = + "CREATE TABLE IF NOT EXISTS media_uploads (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " access_token TEXT NOT NULL," + " original_filename TEXT NOT NULL," + " content_type TEXT NOT NULL," + " s3_key_original TEXT NOT NULL," + " s3_key_processed TEXT," + " file_size INTEGER," + " status TEXT NOT NULL DEFAULT 'pending'," + " error_message TEXT," + " created_at INTEGER DEFAULT (strftime('%s', 'now'))," + " updated_at INTEGER DEFAULT (strftime('%s', 'now'))" + ")"; + + result = Deita_Query_Execute_Update(g_db_connection, create_media_uploads); + if (result < 0) + { + printf("[DB] ERROR: Failed to create media_uploads table\n"); + } + + // Create indices for media_uploads + const char *create_status_idx = + "CREATE INDEX IF NOT EXISTS idx_media_uploads_status ON media_uploads(status)"; + result = Deita_Query_Execute_Update(g_db_connection, create_status_idx); + if (result < 0) + { + printf("[DB] ERROR: Failed to create status index\n"); + } + + const char *create_token_status_idx = + "CREATE INDEX IF NOT EXISTS idx_media_uploads_token_status " + "ON media_uploads(access_token, status)"; + result = Deita_Query_Execute_Update(g_db_connection, create_token_status_idx); + if (result < 0) + { + printf("[DB] ERROR: Failed to create token_status index\n"); + } + else + { + printf("[DB] Initialized: %s\n", g_db_path); + } } void handle_sigint(int sig) @@ -93,6 +203,8 @@ char *end_tag = strstr(start_tag, "}}"); if (!end_tag) break; + Seobeo_Log(SEOBEO_INFO, "[Curr] Life\n"); + size_t leading_len = start_tag - cursor; memcpy(final_body + current_offset, cursor, leading_len); current_offset += leading_len; @@ -104,7 +216,8 @@ size_t sub_file_size = 0; char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size); - Seobeo_Log(SEOBEO_DEBUG, "[Curr] Sub content: %s\n", sub_content); + Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading include: '%s' -> %s (size=%zu)\n", + include_name, sub_content ? "OK" : "FAILED", sub_file_size); if (sub_content) { memcpy(final_body + current_offset, sub_content, sub_file_size); @@ -117,15 +230,15 @@ strcpy(final_body + current_offset, cursor); } - void Seobeo_Render_Html_FilePath( char *final_body, char *path, Dowa_Arena *arena ) { - Seobeo_Log(SEOBEO_DEBUG, "[Curr] %s\n", path); + Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading main template: '%s'\n", path); size_t html_size = 0; char *template = Seobeo_Web_LoadFile(path, &html_size); + Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Main template loaded: %s (size=%zu)\n", template ? "OK" : "FAILED", html_size); if (!template) return; Seobeo_Render_Html(final_body, template, arena); } @@ -182,7 +295,7 @@ if (!req) { - printf("ERROR: Request is NULL\n"); + Seobeo_Log(SEOBEO_ERROR, "Request is NULL\n"); char *error_msg = "Internal error: no request data"; Dowa_HashMap_Push_Arena(resp, "status", "500", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); @@ -195,7 +308,7 @@ for (size_t i = 0; i < req_length; i++) { - printf(" Key[%zu]: '%s'\n", i, req[i].key); + Seobeo_Log(SEOBEO_INFO, " Key[%zu]: '%s'\n", i, req[i].key); } void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); @@ -555,12 +668,50 @@ return resp; } +Seobeo_Request_Entry *GetEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); + Seobeo_Render_Html_FilePath(final_body, "/editor/index.html", arena); + Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); + return resp; +} + +Seobeo_Request_Entry *GetNotesLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); + Seobeo_Render_Html_FilePath(final_body, "/notes/login.html", arena); + Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); + return resp; +} + +Seobeo_Request_Entry *GetNotes(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); + Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena); + Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); + return resp; +} + +Seobeo_Request_Entry *GetNoteById(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); + // Same template - JavaScript handles the note_id from URL + Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena); + Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); + return resp; +} + CREATE_REDIRECT_HANDLER(HomePage, "/") CREATE_REDIRECT_HANDLER(Resume, "/resume") CREATE_REDIRECT_HANDLER(Tools, "/tools") CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html") CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter") CREATE_REDIRECT_HANDLER(Talk, "/talk") +CREATE_REDIRECT_HANDLER(Editor, "/editor") // S3 Upload URL API // POST /api/s3/upload-url @@ -681,11 +832,23 @@ return resp; } + // Build public URL using CloudFront + char public_url[512]; + if (g_s3_cloudfront_url[0]) + { + snprintf(public_url, sizeof(public_url), "%s/%s", g_s3_cloudfront_url, s3_key); + } + else + { + snprintf(public_url, sizeof(public_url), "https://%s.s3.%s.amazonaws.com/%s", + g_s3_bucket, g_s3_region, s3_key); + } + // Build response - char *response_body = Dowa_Arena_Allocate(arena, 2048 + strlen(presigned.url)); - snprintf(response_body, 2048 + strlen(presigned.url), - "{\"upload_url\":\"%s\",\"key\":\"%s\",\"expires\":%d}", - presigned.url, s3_key, g_s3_url_expires); + char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url)); + snprintf(response_body, 4096 + strlen(presigned.url), + "{\"upload_url\":\"%s\",\"public_url\":\"%s\",\"key\":\"%s\",\"expires\":%d}", + presigned.url, public_url, s3_key, g_s3_url_expires); S3_Presigned_URL_Destroy(&presigned); @@ -698,6 +861,816 @@ return resp; } +// Editor Content Save API +// POST /api/editor/save +// Headers: Authorization: Bearer <token> +// Body: {"doc_id": "my-doc", "content": "<html content>"} +Seobeo_Request_Entry *EditorSave(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + + // Check auth token + void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); + if (!auth_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); + return resp; + } + + const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; + if (strncmp(auth_header, "Bearer ", 7) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); + return resp; + } + + const char *token = auth_header + 7; + if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "403", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); + return resp; + } + + if (!g_db_connection) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); + return resp; + } + + // Parse request body + void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); + if (!body_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena); + return resp; + } + + const char *body = ((Seobeo_Request_Entry*)body_kv)->value; + + // Parse doc_id and content from JSON + char doc_id[256] = "default"; + char *content = NULL; + size_t content_len = 0; + + // Find "doc_id":"value" + const char *doc_key = strstr(body, "\"doc_id\""); + if (doc_key) + { + const char *doc_start = strchr(doc_key + 8, '"'); + if (doc_start) + { + doc_start++; + const char *doc_end = strchr(doc_start, '"'); + if (doc_end && (size_t)(doc_end - doc_start) < sizeof(doc_id)) + { + memcpy(doc_id, doc_start, doc_end - doc_start); + doc_id[doc_end - doc_start] = '\0'; + } + } + } + + // Find "content":"value" - content can be large and contain escaped characters + const char *content_key = strstr(body, "\"content\""); + if (content_key) + { + const char *content_start = strchr(content_key + 9, '"'); + if (content_start) + { + content_start++; + // Find closing quote (accounting for escaped quotes) + const char *p = content_start; + while (*p) + { + if (*p == '\\' && *(p+1)) + { + p += 2; + continue; + } + if (*p == '"') break; + p++; + } + content_len = p - content_start; + content = Dowa_Arena_Allocate(arena, content_len + 1); + memcpy(content, content_start, content_len); + content[content_len] = '\0'; + } + } + + if (!content) + { + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing content\"}", arena); + return resp; + } + + // Upsert content + const char *upsert_query = + "INSERT INTO editor_content (access_token, doc_id, content, updated_at) " + "VALUES (?, ?, ?, strftime('%s', 'now')) " + "ON CONFLICT(access_token, doc_id) DO UPDATE SET " + "content = excluded.content, updated_at = strftime('%s', 'now')"; + + const char *params[] = { token, doc_id, content }; + int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, upsert_query, 3, params); + + if (result < 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to save\"}", arena); + return resp; + } + + printf("[EDITOR] Saved doc_id=%s, content_len=%zu\n", doc_id, content_len); + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"success\":true}", arena); + return resp; +} + +// Editor Content Load API +// GET /api/editor/load/:doc_id +// Headers: Authorization: Bearer <token> +Seobeo_Request_Entry *EditorLoad(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + + // Check auth token + void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); + if (!auth_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); + return resp; + } + + const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; + if (strncmp(auth_header, "Bearer ", 7) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); + return resp; + } + + const char *token = auth_header + 7; + if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "403", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); + return resp; + } + + if (!g_db_connection) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); + return resp; + } + + // Get doc_id from URL parameter + void *doc_id_kv = Dowa_HashMap_Get_Ptr(req, ":doc_id"); + const char *doc_id = "default"; + if (doc_id_kv) + { + doc_id = ((Seobeo_Request_Entry*)doc_id_kv)->value; + } + + // Query content + const char *select_query = + "SELECT content, updated_at FROM editor_content WHERE access_token = ? AND doc_id = ?"; + const char *params[] = { token, doc_id }; + + Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, params, arena); + + if (p_result && Deita_Result_Set_Next(p_result)) + { + const char *content = Deita_Result_Set_Get_Text(p_result, 0); + int64 updated_at = Deita_Result_Set_Get_Integer(p_result, 1); + + // Build JSON response - escape content + size_t content_len = content ? strlen(content) : 0; + char *response_body = Dowa_Arena_Allocate(arena, content_len + 256); + snprintf(response_body, content_len + 256, + "{\"doc_id\":\"%s\",\"content\":\"%s\",\"updated_at\":%lld}", + doc_id, content ? content : "", (long long)updated_at); + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); + + printf("[EDITOR] Loaded doc_id=%s\n", doc_id); + } + else + { + // No content found, return empty + char *response_body = Dowa_Arena_Allocate(arena, 128); + snprintf(response_body, 128, "{\"doc_id\":\"%s\",\"content\":\"\",\"updated_at\":0}", doc_id); + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); + } + + if (p_result) Deita_Result_Set_Free(p_result); + return resp; +} + +// Media Upload API - Create media record +// POST /api/media/create +// Headers: Authorization: Bearer <token>, Content-Type: application/json +// Body: {"filename": "photo.jpg", "content_type": "image/jpeg"} +// Returns: {"media_id": 123, "upload_url": "https://...", "expires": 3600} +Seobeo_Request_Entry *MediaCreate(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + + // Check auth token + void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); + if (!auth_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); + return resp; + } + + const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; + if (strncmp(auth_header, "Bearer ", 7) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); + return resp; + } + + const char *token = auth_header + 7; + if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "403", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); + return resp; + } + + if (!g_db_connection) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); + return resp; + } + + // Parse request body + void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); + if (!body_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena); + return resp; + } + + const char *body = ((Seobeo_Request_Entry*)body_kv)->value; + + // Parse filename and content_type + char filename[256] = {0}; + char content_type[128] = "application/octet-stream"; + + // Find "filename":"value" + const char *fn_key = strstr(body, "\"filename\""); + if (fn_key) + { + const char *fn_start = strchr(fn_key + 10, '"'); + if (fn_start) + { + fn_start++; + const char *fn_end = strchr(fn_start, '"'); + if (fn_end && (size_t)(fn_end - fn_start) < sizeof(filename)) + { + memcpy(filename, fn_start, fn_end - fn_start); + filename[fn_end - fn_start] = '\0'; + } + } + } + + // Find "content_type":"value" + const char *ct_key = strstr(body, "\"content_type\""); + if (ct_key) + { + const char *ct_start = strchr(ct_key + 14, '"'); + if (ct_start) + { + ct_start++; + const char *ct_end = strchr(ct_start, '"'); + if (ct_end && (size_t)(ct_end - ct_start) < sizeof(content_type)) + { + memcpy(content_type, ct_start, ct_end - ct_start); + content_type[ct_end - ct_start] = '\0'; + } + } + } + + if (strlen(filename) == 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing filename\"}", arena); + return resp; + } + + // Generate UUID for this upload + char *uuid = Dowa_Arena_Allocate(arena, UUID_LEN); + uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++; + Dowa_String_UUID(seed, uuid); + + // Generate S3 keys + char s3_key_original[512]; + char s3_key_processed[512]; + snprintf(s3_key_original, sizeof(s3_key_original), "uploads/%s/%s", uuid, filename); + snprintf(s3_key_processed, sizeof(s3_key_processed), "uploads/%s/processed.webp", uuid); + + // Insert into database + const char *insert_query = + "INSERT INTO media_uploads (access_token, original_filename, content_type, s3_key_original, s3_key_processed, status) " + "VALUES (?, ?, ?, ?, ?, 'pending')"; + + const char *params[] = { token, filename, content_type, s3_key_original, s3_key_processed }; + int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, insert_query, 5, params); + + if (result < 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to create media record\"}", arena); + return resp; + } + + // Get the inserted media_id using last_insert_rowid() + const char *last_id_query = "SELECT last_insert_rowid()"; + Deita_Result_Set *id_result = Deita_Query_Execute(g_db_connection, last_id_query, arena); + int64 media_id = 0; + if (id_result && Deita_Result_Set_Next(id_result)) + { + media_id = Deita_Result_Set_Get_Integer(id_result, 0); + } + if (id_result) Deita_Result_Set_Free(id_result); + + // Generate presigned PUT URL + S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key_original, content_type, g_s3_url_expires); + + if (!presigned.success) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + char *error_body = Dowa_Arena_Allocate(arena, 256); + snprintf(error_body, 256, "{\"error\":\"Failed to generate upload URL: %s\"}", + presigned.error_message ? presigned.error_message : "unknown"); + Dowa_HashMap_Push_Arena(resp, "body", error_body, arena); + S3_Presigned_URL_Destroy(&presigned); + return resp; + } + + // Build response + char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url)); + snprintf(response_body, 4096 + strlen(presigned.url), + "{\"media_id\":%lld,\"upload_url\":\"%s\",\"expires\":%d}", + (long long)media_id, presigned.url, g_s3_url_expires); + + S3_Presigned_URL_Destroy(&presigned); + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); + + printf("[MEDIA] Created media_id=%lld, file=%s\n", (long long)media_id, filename); + + return resp; +} + +// Background thread function for media processing +void *Media_Process_Background(void *arg) +{ + Media_Processing_Context *ctx = (Media_Processing_Context *)arg; + + // Open thread-local DB connection + Deita_Connection *db_conn = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, g_db_path); + if (!db_conn || !Deita_Connection_Is_Open(db_conn)) + { + printf("[MEDIA] Thread ERROR: Failed to open database for media_id=%lld\n", (long long)ctx->media_id); + free(ctx); + return NULL; + } + + // Update status to 'processing' + const char *update_processing = + "UPDATE media_uploads SET status='processing', updated_at=strftime('%s','now') WHERE id=?"; + char media_id_str[32]; + snprintf(media_id_str, sizeof(media_id_str), "%lld", (long long)ctx->media_id); + const char *params[] = { media_id_str }; + Deita_Query_Execute_Update_Prepared(db_conn, update_processing, 1, params); + + printf("[MEDIA] Processing media_id=%lld\n", (long long)ctx->media_id); + + // Generate presigned GET URL for download (10 min expiry) + S3_Presigned_URL download_url = S3_Presign_Get(&ctx->s3_config, ctx->s3_key_original, 600); + if (!download_url.success) + { + const char *update_error = + "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?"; + const char *error_params[] = { "Failed to generate download URL", media_id_str }; + Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params); + printf("[MEDIA] ERROR: Failed to generate download URL for media_id=%lld\n", (long long)ctx->media_id); + S3_Presigned_URL_Destroy(&download_url); + Deita_Connection_Close(db_conn); + free(ctx); + return NULL; + } + + // Generate temp file paths + char tmp_input[256]; + char tmp_output[256]; + char *uuid_input = malloc(UUID_LEN); + char *uuid_output = malloc(UUID_LEN); + uint32 seed1 = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++; + uint32 seed2 = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++; + Dowa_String_UUID(seed1, uuid_input); + Dowa_String_UUID(seed2, uuid_output); + snprintf(tmp_input, sizeof(tmp_input), "/tmp/%s", uuid_input); + snprintf(tmp_output, sizeof(tmp_output), "/tmp/%s.webp", uuid_output); + free(uuid_input); + free(uuid_output); + + // Download from S3 + Seobeo_Client_Request *download_req = Seobeo_Client_Request_Create(download_url.url); + Seobeo_Client_Request_Set_Download_Path(download_req, tmp_input); + Seobeo_Client_Response *download_resp = Seobeo_Client_Request_Execute(download_req); + + S3_Presigned_URL_Destroy(&download_url); + + if (!download_resp || download_resp->status_code != 200) + { + const char *update_error = + "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?"; + const char *error_params[] = { "Failed to download from S3", media_id_str }; + Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params); + printf("[MEDIA] ERROR: Failed to download from S3 for media_id=%lld\n", (long long)ctx->media_id); + if (download_req) Seobeo_Client_Request_Destroy(download_req); + if (download_resp) Seobeo_Client_Response_Destroy(download_resp); + unlink(tmp_input); + Deita_Connection_Close(db_conn); + free(ctx); + return NULL; + } + + Seobeo_Client_Request_Destroy(download_req); + Seobeo_Client_Response_Destroy(download_resp); + + // Convert to webp using FFmpeg + char cmd[1024]; + char log_file[256]; + snprintf(log_file, sizeof(log_file), "/tmp/ffmpeg_%lld.log", (long long)ctx->media_id); + snprintf(cmd, sizeof(cmd), "ffmpeg -y -i %s -quality 80 %s 2>%s", + tmp_input, tmp_output, log_file); + + int ffmpeg_result = system(cmd); + if (ffmpeg_result != 0) + { + const char *update_error = + "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?"; + const char *error_params[] = { "Image conversion failed", media_id_str }; + Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params); + printf("[MEDIA] ERROR: FFmpeg conversion failed for media_id=%lld\n", (long long)ctx->media_id); + unlink(tmp_input); + unlink(tmp_output); + Deita_Connection_Close(db_conn); + free(ctx); + return NULL; + } + + // Upload processed file to S3 + S3_Result upload_result = S3_Upload_File_With_Content_Type( + &ctx->s3_config, tmp_output, ctx->s3_key_processed, "image/webp"); + + if (!upload_result.success) + { + const char *update_error = + "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?"; + const char *error_params[] = { "Failed to upload processed file", media_id_str }; + Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params); + printf("[MEDIA] ERROR: Failed to upload processed file for media_id=%lld\n", (long long)ctx->media_id); + unlink(tmp_input); + unlink(tmp_output); + Deita_Connection_Close(db_conn); + free(ctx); + return NULL; + } + + // Update status to 'finished' + const char *update_finished = + "UPDATE media_uploads SET status='finished', updated_at=strftime('%s','now') WHERE id=?"; + Deita_Query_Execute_Update_Prepared(db_conn, update_finished, 1, params); + + printf("[MEDIA] Successfully processed media_id=%lld\n", (long long)ctx->media_id); + + // Cleanup + unlink(tmp_input); + unlink(tmp_output); + Deita_Connection_Close(db_conn); + free(ctx); + + return NULL; +} + +// Media Upload API - Mark uploaded +// POST /api/media/:id/uploaded +// Headers: Authorization: Bearer <token> +Seobeo_Request_Entry *MediaUploaded(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + + // Check auth token + void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); + if (!auth_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); + return resp; + } + + const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; + if (strncmp(auth_header, "Bearer ", 7) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); + return resp; + } + + const char *token = auth_header + 7; + if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "403", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); + return resp; + } + + if (!g_db_connection) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); + return resp; + } + + // Extract media_id from URL params + void *id_kv = Dowa_HashMap_Get_Ptr(req, ":id"); + if (!id_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing media ID\"}", arena); + return resp; + } + + const char *media_id_str = ((Seobeo_Request_Entry*)id_kv)->value; + int64 media_id = atoll(media_id_str); + + // Verify access_token matches and get content_type + const char *select_query = + "SELECT content_type, s3_key_original, s3_key_processed FROM media_uploads WHERE id = ? AND access_token = ?"; + const char *select_params[] = { media_id_str, token }; + + Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, select_params, arena); + + if (!p_result || !Deita_Result_Set_Next(p_result)) + { + if (p_result) Deita_Result_Set_Free(p_result); + Dowa_HashMap_Push_Arena(resp, "status", "404", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Media not found or access denied\"}", arena); + return resp; + } + + const char *content_type = Deita_Result_Set_Get_Text(p_result, 0); + const char *s3_key_original = Deita_Result_Set_Get_Text(p_result, 1); + const char *s3_key_processed = Deita_Result_Set_Get_Text(p_result, 2); + + // Copy values before freeing result set + char content_type_copy[128]; + char s3_key_original_copy[512]; + char s3_key_processed_copy[512]; + strncpy(content_type_copy, content_type, sizeof(content_type_copy) - 1); + strncpy(s3_key_original_copy, s3_key_original, sizeof(s3_key_original_copy) - 1); + strncpy(s3_key_processed_copy, s3_key_processed, sizeof(s3_key_processed_copy) - 1); + content_type_copy[sizeof(content_type_copy) - 1] = '\0'; + s3_key_original_copy[sizeof(s3_key_original_copy) - 1] = '\0'; + s3_key_processed_copy[sizeof(s3_key_processed_copy) - 1] = '\0'; + + Deita_Result_Set_Free(p_result); + + // Update status to 'uploaded' + const char *update_query = + "UPDATE media_uploads SET status='uploaded', updated_at=strftime('%s','now') WHERE id=?"; + const char *update_params[] = { media_id_str }; + int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, update_query, 1, update_params); + + if (result < 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to update status\"}", arena); + return resp; + } + + // If content_type starts with "image/", spawn background processing thread + if (strncmp(content_type_copy, "image/", 6) == 0) + { + // Create context for background thread (heap allocated) + Media_Processing_Context *ctx = malloc(sizeof(Media_Processing_Context)); + ctx->media_id = media_id; + strncpy(ctx->s3_key_original, s3_key_original_copy, sizeof(ctx->s3_key_original) - 1); + strncpy(ctx->s3_key_processed, s3_key_processed_copy, sizeof(ctx->s3_key_processed) - 1); + strncpy(ctx->content_type, content_type_copy, sizeof(ctx->content_type) - 1); + strncpy(ctx->access_token, token, sizeof(ctx->access_token) - 1); + ctx->s3_key_original[sizeof(ctx->s3_key_original) - 1] = '\0'; + ctx->s3_key_processed[sizeof(ctx->s3_key_processed) - 1] = '\0'; + ctx->content_type[sizeof(ctx->content_type) - 1] = '\0'; + ctx->access_token[sizeof(ctx->access_token) - 1] = '\0'; + ctx->s3_config = g_s3_config; + + // Spawn detached thread + pthread_t thread_id; + int thread_result = pthread_create(&thread_id, NULL, Media_Process_Background, ctx); + + if (thread_result != 0) + { + Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: Failed to spawn processing thread for media_id=%lld\n", (long long)media_id); + free(ctx); + } + else + { + // Detach thread so it cleans up automatically when done + pthread_detach(thread_id); + Seobeo_Log(SEOBEO_INFO, "[MEDIA] Spawned processing thread for media_id=%lld\n", (long long)media_id); + } + } + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"success\":true,\"status\":\"uploaded\"}", arena); + + Seobeo_Log(SEOBEO_INFO, "[MEDIA] Marked uploaded media_id=%lld\n", (long long)media_id); + + return resp; +} + +// Media Upload API - Get status +// GET /api/media/:id/status +// Headers: Authorization: Bearer <token> +// Returns: {"id": 123, "status": "finished", "processed_url": "https://...", "error_message": null} +Seobeo_Request_Entry *MediaStatus(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + + // Check auth token + void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); + if (!auth_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); + return resp; + } + + const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; + if (strncmp(auth_header, "Bearer ", 7) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); + return resp; + } + + const char *token = auth_header + 7; + if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "403", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); + return resp; + } + + if (!g_db_connection) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); + return resp; + } + + // Extract media_id from URL params + void *id_kv = Dowa_HashMap_Get_Ptr(req, ":id"); + if (!id_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing media ID\"}", arena); + return resp; + } + + const char *media_id_str = ((Seobeo_Request_Entry*)id_kv)->value; + + // Query media status + const char *select_query = + "SELECT id, status, s3_key_processed, error_message FROM media_uploads WHERE id = ? AND access_token = ?"; + const char *select_params[] = { media_id_str, token }; + + Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, select_params, arena); + + if (!p_result || !Deita_Result_Set_Next(p_result)) + { + if (p_result) Deita_Result_Set_Free(p_result); + Dowa_HashMap_Push_Arena(resp, "status", "404", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Media not found\"}", arena); + return resp; + } + + int64 id = Deita_Result_Set_Get_Integer(p_result, 0); + const char *status = Deita_Result_Set_Get_Text(p_result, 1); + const char *s3_key_processed = Deita_Result_Set_Get_Text(p_result, 2); + const char *error_message = Deita_Result_Set_Get_Text(p_result, 3); + + // Build CloudFront URL if status is 'finished' and s3_key_processed exists + char processed_url[1024] = {0}; + if (strcmp(status, "finished") == 0 && s3_key_processed && strlen(s3_key_processed) > 0) + { + if (g_s3_cloudfront_url[0]) + { + snprintf(processed_url, sizeof(processed_url), "%s/%s", g_s3_cloudfront_url, s3_key_processed); + } + else + { + snprintf(processed_url, sizeof(processed_url), "https://%s.s3.%s.amazonaws.com/%s", + g_s3_bucket, g_s3_region, s3_key_processed); + } + } + + // Build JSON response + char *response_body = Dowa_Arena_Allocate(arena, 2048); + if (strlen(processed_url) > 0) + { + snprintf(response_body, 2048, + "{\"id\":%lld,\"status\":\"%s\",\"processed_url\":\"%s\",\"error_message\":%s}", + (long long)id, status, processed_url, + error_message ? "\"" : "null"); + if (error_message) + { + // Append error message if exists + size_t len = strlen(response_body); + snprintf(response_body + len - 1, 2048 - len + 1, "%s\"}", error_message); + } + } + else + { + snprintf(response_body, 2048, + "{\"id\":%lld,\"status\":\"%s\",\"processed_url\":null,\"error_message\":%s}", + (long long)id, status, + error_message ? "\"" : "null"); + if (error_message) + { + size_t len = strlen(response_body); + snprintf(response_body + len - 1, 2048 - len + 1, "%s\"}", error_message); + } + } + + Deita_Result_Set_Free(p_result); + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); + + return resp; +} + int main(void) { // Load server config @@ -742,6 +1715,9 @@ printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n", g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)"); + // Initialize database + init_database(); + Seobeo_Router_Init(); Seobeo_Router_Register("GET", "/", GetHomePage); @@ -767,6 +1743,15 @@ // -- S3 Upload --/ Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl); + // -- Media Upload --/ + Seobeo_Router_Register("POST", "/api/media/create", MediaCreate); + Seobeo_Router_Register("POST", "/api/media/:id/uploaded", MediaUploaded); + Seobeo_Router_Register("GET", "/api/media/:id/status", MediaStatus); + + // -- Editor --/ + Seobeo_Router_Register("POST", "/api/editor/save", EditorSave); + Seobeo_Router_Register("GET", "/api/editor/load/:doc_id", EditorLoad); + // -- Blog --/ Seobeo_Router_Register("GET", "/blog", RenderBlogList); Seobeo_Router_Register("GET", "/blog/:blog_id", RenderBlog); @@ -775,8 +1760,21 @@ Seobeo_Router_Register("GET", "/talk", GetTalk); Seobeo_Router_Register("GET", "/talk/index.html", GetRedirectTalk); + // -- Editor (legacy) --/ + Seobeo_Router_Register("GET", "/editor", GetEditor); + Seobeo_Router_Register("GET", "/editor/index.html", GetRedirectEditor); + + // -- Notes --/ + Seobeo_Router_Register("GET", "/notes", GetNotes); + Seobeo_Router_Register("GET", "/notes/", GetNotes); + Seobeo_Router_Register("GET", "/notes/index.html", GetNotes); + Seobeo_Router_Register("GET", "/notes/login", GetNotesLogin); + Seobeo_Router_Register("GET", "/notes/login/", GetNotesLogin); + Seobeo_Router_Register("GET", "/notes/:note_id", GetNoteById); + Seobeo_WebSocket_Server_Init(); Seobeo_WebSocket_Server_Register("/chat", Chat_Handler, NULL); - Seobeo_Web_Server_Start("mrjunejune/src", "6969", SEOBEO_MODE_EDGE, 3); + Seobeo_Log(SEOBEO_INFO, "WTF is going on\n"); + Seobeo_Web_Server_Start("mrjunejune/src", "6969", SEOBEO_MODE_EDGE, 1); }