Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 200:90dfcef375fb | 201:6cdee35a7ba9 |
|---|---|
| 1 #include "seobeo/seobeo.h" | 1 #include "seobeo/seobeo.h" |
| 2 #include "markdown_converter/markdown_to_html.h" | 2 #include "markdown_converter/markdown_to_html.h" |
| 3 #include "s3/s3_uploader.h" | 3 #include "s3/s3_uploader.h" |
| 4 #include "deita/deita.h" | |
| 4 #include <time.h> | 5 #include <time.h> |
| 6 #include <sys/stat.h> | |
| 7 #include <stdarg.h> | |
| 8 #include <pthread.h> | |
| 5 | 9 |
| 6 // UUID + /tmp/ + format (max 4) | 10 // UUID + /tmp/ + format (max 4) |
| 7 #define TMP_FILE_LENGTH 47 | 11 #define TMP_FILE_LENGTH 47 |
| 8 #define UUID_LEN 37 | 12 #define UUID_LEN 37 |
| 9 | 13 |
| 10 volatile sig_atomic_t stop_server = 0; | 14 volatile sig_atomic_t stop_server = 0; |
| 11 static _Atomic uint32_t counter = 0; | 15 static _Atomic uint32_t counter = 0; |
| 16 | |
| 17 // Media Processing Context for background threads | |
| 18 typedef struct { | |
| 19 int64 media_id; | |
| 20 char s3_key_original[512]; | |
| 21 char s3_key_processed[512]; | |
| 22 char content_type[128]; | |
| 23 char access_token[256]; | |
| 24 S3_Config s3_config; | |
| 25 } Media_Processing_Context; | |
| 12 | 26 |
| 13 // Server configuration (loaded from .config) | 27 // Server configuration (loaded from .config) |
| 14 static char g_upload_auth_token[256] = {0}; | 28 static char g_upload_auth_token[256] = {0}; |
| 15 static char g_s3_region[64] = "us-west-2"; | 29 static char g_s3_region[64] = "us-west-2"; |
| 16 static char g_s3_bucket[128] = "mrjunejune"; | 30 static char g_s3_bucket[128] = "mrjunejune"; |
| 31 static char g_s3_cloudfront_url[256] = {0}; | |
| 32 static char g_db_path[256] = "mrjunejune/data/mrjunejune.db"; | |
| 17 static int g_s3_url_expires = 3600; | 33 static int g_s3_url_expires = 3600; |
| 18 static S3_Config g_s3_config = {0}; | 34 static S3_Config g_s3_config = {0}; |
| 35 static Deita_Connection *g_db_connection = NULL; | |
| 19 | 36 |
| 20 static void load_config(const char *config_path) | 37 static void load_config(const char *config_path) |
| 21 { | 38 { |
| 22 FILE *f = fopen(config_path, "r"); | 39 FILE *f = fopen(config_path, "r"); |
| 23 if (!f) | 40 if (!f) |
| 58 } | 75 } |
| 59 else if (strcmp(key, "S3_URL_EXPIRES") == 0) | 76 else if (strcmp(key, "S3_URL_EXPIRES") == 0) |
| 60 { | 77 { |
| 61 g_s3_url_expires = atoi(value); | 78 g_s3_url_expires = atoi(value); |
| 62 } | 79 } |
| 80 else if (strcmp(key, "S3_CLOUDFRONT_URL") == 0) | |
| 81 { | |
| 82 strncpy(g_s3_cloudfront_url, value, sizeof(g_s3_cloudfront_url) - 1); | |
| 83 } | |
| 84 else if (strcmp(key, "DB_PATH") == 0) | |
| 85 { | |
| 86 strncpy(g_db_path, value, sizeof(g_db_path) - 1); | |
| 87 } | |
| 63 } | 88 } |
| 64 fclose(f); | 89 fclose(f); |
| 65 | 90 |
| 66 printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d\n", | 91 printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d, cloudfront=%s, db=%s\n", |
| 67 g_upload_auth_token[0] ? "***" : "(empty)", | 92 g_upload_auth_token[0] ? "***" : "(empty)", |
| 68 g_s3_region, g_s3_bucket, g_s3_url_expires); | 93 g_s3_region, g_s3_bucket, g_s3_url_expires, |
| 94 g_s3_cloudfront_url[0] ? g_s3_cloudfront_url : "(none)", | |
| 95 g_db_path); | |
| 96 } | |
| 97 | |
| 98 static void init_database(void) | |
| 99 { | |
| 100 // Create data directory if needed | |
| 101 char *last_slash = strrchr(g_db_path, '/'); | |
| 102 if (last_slash) | |
| 103 { | |
| 104 char dir_path[256]; | |
| 105 size_t dir_len = last_slash - g_db_path; | |
| 106 strncpy(dir_path, g_db_path, dir_len); | |
| 107 dir_path[dir_len] = '\0'; | |
| 108 mkdir(dir_path, 0755); | |
| 109 } | |
| 110 | |
| 111 g_db_connection = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, g_db_path); | |
| 112 if (!g_db_connection || !Deita_Connection_Is_Open(g_db_connection)) | |
| 113 { | |
| 114 printf("[DB] ERROR: Failed to open database at %s\n", g_db_path); | |
| 115 return; | |
| 116 } | |
| 117 | |
| 118 // Create editor_content table | |
| 119 const char *create_table = | |
| 120 "CREATE TABLE IF NOT EXISTS editor_content (" | |
| 121 " id INTEGER PRIMARY KEY AUTOINCREMENT," | |
| 122 " access_token TEXT NOT NULL," | |
| 123 " doc_id TEXT NOT NULL," | |
| 124 " content TEXT," | |
| 125 " created_at INTEGER DEFAULT (strftime('%s', 'now'))," | |
| 126 " updated_at INTEGER DEFAULT (strftime('%s', 'now'))," | |
| 127 " UNIQUE(access_token, doc_id)" | |
| 128 ")"; | |
| 129 | |
| 130 int32 result = Deita_Query_Execute_Update(g_db_connection, create_table); | |
| 131 if (result < 0) | |
| 132 { | |
| 133 printf("[DB] ERROR: Failed to create editor_content table\n"); | |
| 134 } | |
| 135 | |
| 136 // Create media_uploads table | |
| 137 const char *create_media_uploads = | |
| 138 "CREATE TABLE IF NOT EXISTS media_uploads (" | |
| 139 " id INTEGER PRIMARY KEY AUTOINCREMENT," | |
| 140 " access_token TEXT NOT NULL," | |
| 141 " original_filename TEXT NOT NULL," | |
| 142 " content_type TEXT NOT NULL," | |
| 143 " s3_key_original TEXT NOT NULL," | |
| 144 " s3_key_processed TEXT," | |
| 145 " file_size INTEGER," | |
| 146 " status TEXT NOT NULL DEFAULT 'pending'," | |
| 147 " error_message TEXT," | |
| 148 " created_at INTEGER DEFAULT (strftime('%s', 'now'))," | |
| 149 " updated_at INTEGER DEFAULT (strftime('%s', 'now'))" | |
| 150 ")"; | |
| 151 | |
| 152 result = Deita_Query_Execute_Update(g_db_connection, create_media_uploads); | |
| 153 if (result < 0) | |
| 154 { | |
| 155 printf("[DB] ERROR: Failed to create media_uploads table\n"); | |
| 156 } | |
| 157 | |
| 158 // Create indices for media_uploads | |
| 159 const char *create_status_idx = | |
| 160 "CREATE INDEX IF NOT EXISTS idx_media_uploads_status ON media_uploads(status)"; | |
| 161 result = Deita_Query_Execute_Update(g_db_connection, create_status_idx); | |
| 162 if (result < 0) | |
| 163 { | |
| 164 printf("[DB] ERROR: Failed to create status index\n"); | |
| 165 } | |
| 166 | |
| 167 const char *create_token_status_idx = | |
| 168 "CREATE INDEX IF NOT EXISTS idx_media_uploads_token_status " | |
| 169 "ON media_uploads(access_token, status)"; | |
| 170 result = Deita_Query_Execute_Update(g_db_connection, create_token_status_idx); | |
| 171 if (result < 0) | |
| 172 { | |
| 173 printf("[DB] ERROR: Failed to create token_status index\n"); | |
| 174 } | |
| 175 else | |
| 176 { | |
| 177 printf("[DB] Initialized: %s\n", g_db_path); | |
| 178 } | |
| 69 } | 179 } |
| 70 | 180 |
| 71 void handle_sigint(int sig) | 181 void handle_sigint(int sig) |
| 72 { | 182 { |
| 73 printf("Failed\n"); | 183 printf("Failed\n"); |
| 91 if (!start_tag) break; | 201 if (!start_tag) break; |
| 92 | 202 |
| 93 char *end_tag = strstr(start_tag, "}}"); | 203 char *end_tag = strstr(start_tag, "}}"); |
| 94 if (!end_tag) break; | 204 if (!end_tag) break; |
| 95 | 205 |
| 206 Seobeo_Log(SEOBEO_INFO, "[Curr] Life\n"); | |
| 207 | |
| 96 size_t leading_len = start_tag - cursor; | 208 size_t leading_len = start_tag - cursor; |
| 97 memcpy(final_body + current_offset, cursor, leading_len); | 209 memcpy(final_body + current_offset, cursor, leading_len); |
| 98 current_offset += leading_len; | 210 current_offset += leading_len; |
| 99 | 211 |
| 100 size_t name_len = end_tag - (start_tag + token_len); | 212 size_t name_len = end_tag - (start_tag + token_len); |
| 102 memcpy(include_name, start_tag + token_len, name_len); | 214 memcpy(include_name, start_tag + token_len, name_len); |
| 103 include_name[name_len] = '\0'; | 215 include_name[name_len] = '\0'; |
| 104 | 216 |
| 105 size_t sub_file_size = 0; | 217 size_t sub_file_size = 0; |
| 106 char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size); | 218 char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size); |
| 107 Seobeo_Log(SEOBEO_DEBUG, "[Curr] Sub content: %s\n", sub_content); | 219 Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading include: '%s' -> %s (size=%zu)\n", |
| 220 include_name, sub_content ? "OK" : "FAILED", sub_file_size); | |
| 108 if (sub_content) | 221 if (sub_content) |
| 109 { | 222 { |
| 110 memcpy(final_body + current_offset, sub_content, sub_file_size); | 223 memcpy(final_body + current_offset, sub_content, sub_file_size); |
| 111 current_offset += sub_file_size; | 224 current_offset += sub_file_size; |
| 112 free(sub_content); | 225 free(sub_content); |
| 114 | 227 |
| 115 cursor = end_tag + 2; | 228 cursor = end_tag + 2; |
| 116 } | 229 } |
| 117 strcpy(final_body + current_offset, cursor); | 230 strcpy(final_body + current_offset, cursor); |
| 118 } | 231 } |
| 119 | |
| 120 | 232 |
| 121 void Seobeo_Render_Html_FilePath( | 233 void Seobeo_Render_Html_FilePath( |
| 122 char *final_body, | 234 char *final_body, |
| 123 char *path, | 235 char *path, |
| 124 Dowa_Arena *arena | 236 Dowa_Arena *arena |
| 125 ) { | 237 ) { |
| 126 Seobeo_Log(SEOBEO_DEBUG, "[Curr] %s\n", path); | 238 Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading main template: '%s'\n", path); |
| 127 size_t html_size = 0; | 239 size_t html_size = 0; |
| 128 char *template = Seobeo_Web_LoadFile(path, &html_size); | 240 char *template = Seobeo_Web_LoadFile(path, &html_size); |
| 241 Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Main template loaded: %s (size=%zu)\n", template ? "OK" : "FAILED", html_size); | |
| 129 if (!template) return; | 242 if (!template) return; |
| 130 Seobeo_Render_Html(final_body, template, arena); | 243 Seobeo_Render_Html(final_body, template, arena); |
| 131 } | 244 } |
| 132 | 245 |
| 133 Seobeo_Request_Entry* GetHomePage(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 246 Seobeo_Request_Entry* GetHomePage(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 180 { | 293 { |
| 181 Seobeo_Request_Entry *resp = NULL; | 294 Seobeo_Request_Entry *resp = NULL; |
| 182 | 295 |
| 183 if (!req) | 296 if (!req) |
| 184 { | 297 { |
| 185 printf("ERROR: Request is NULL\n"); | 298 Seobeo_Log(SEOBEO_ERROR, "Request is NULL\n"); |
| 186 char *error_msg = "Internal error: no request data"; | 299 char *error_msg = "Internal error: no request data"; |
| 187 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | 300 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); |
| 188 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 301 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 189 Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); | 302 Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); |
| 190 return resp; | 303 return resp; |
| 193 size_t req_length = Dowa_Array_Length(req); | 306 size_t req_length = Dowa_Array_Length(req); |
| 194 printf("Request has %zu entries\n", req_length); | 307 printf("Request has %zu entries\n", req_length); |
| 195 | 308 |
| 196 for (size_t i = 0; i < req_length; i++) | 309 for (size_t i = 0; i < req_length; i++) |
| 197 { | 310 { |
| 198 printf(" Key[%zu]: '%s'\n", i, req[i].key); | 311 Seobeo_Log(SEOBEO_INFO, " Key[%zu]: '%s'\n", i, req[i].key); |
| 199 } | 312 } |
| 200 | 313 |
| 201 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | 314 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); |
| 202 if (!body_kv) | 315 if (!body_kv) |
| 203 { | 316 { |
| 553 Seobeo_Render_Html_FilePath(final_body, "/talk/index.html", arena); | 666 Seobeo_Render_Html_FilePath(final_body, "/talk/index.html", arena); |
| 554 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); | 667 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); |
| 555 return resp; | 668 return resp; |
| 556 } | 669 } |
| 557 | 670 |
| 671 Seobeo_Request_Entry *GetEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 672 { | |
| 673 Seobeo_Request_Entry *resp = NULL; | |
| 674 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); | |
| 675 Seobeo_Render_Html_FilePath(final_body, "/editor/index.html", arena); | |
| 676 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); | |
| 677 return resp; | |
| 678 } | |
| 679 | |
| 680 Seobeo_Request_Entry *GetNotesLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 681 { | |
| 682 Seobeo_Request_Entry *resp = NULL; | |
| 683 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); | |
| 684 Seobeo_Render_Html_FilePath(final_body, "/notes/login.html", arena); | |
| 685 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); | |
| 686 return resp; | |
| 687 } | |
| 688 | |
| 689 Seobeo_Request_Entry *GetNotes(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 690 { | |
| 691 Seobeo_Request_Entry *resp = NULL; | |
| 692 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); | |
| 693 Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena); | |
| 694 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); | |
| 695 return resp; | |
| 696 } | |
| 697 | |
| 698 Seobeo_Request_Entry *GetNoteById(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 699 { | |
| 700 Seobeo_Request_Entry *resp = NULL; | |
| 701 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); | |
| 702 // Same template - JavaScript handles the note_id from URL | |
| 703 Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena); | |
| 704 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); | |
| 705 return resp; | |
| 706 } | |
| 707 | |
| 558 CREATE_REDIRECT_HANDLER(HomePage, "/") | 708 CREATE_REDIRECT_HANDLER(HomePage, "/") |
| 559 CREATE_REDIRECT_HANDLER(Resume, "/resume") | 709 CREATE_REDIRECT_HANDLER(Resume, "/resume") |
| 560 CREATE_REDIRECT_HANDLER(Tools, "/tools") | 710 CREATE_REDIRECT_HANDLER(Tools, "/tools") |
| 561 CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html") | 711 CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html") |
| 562 CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter") | 712 CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter") |
| 563 CREATE_REDIRECT_HANDLER(Talk, "/talk") | 713 CREATE_REDIRECT_HANDLER(Talk, "/talk") |
| 714 CREATE_REDIRECT_HANDLER(Editor, "/editor") | |
| 564 | 715 |
| 565 // S3 Upload URL API | 716 // S3 Upload URL API |
| 566 // POST /api/s3/upload-url | 717 // POST /api/s3/upload-url |
| 567 // Headers: Authorization: Bearer <token>, Content-Type: application/json | 718 // Headers: Authorization: Bearer <token>, Content-Type: application/json |
| 568 // Body: {"filename": "photo.png", "content_type": "image/png"} | 719 // Body: {"filename": "photo.png", "content_type": "image/png"} |
| 679 Dowa_HashMap_Push_Arena(resp, "body", error_body, arena); | 830 Dowa_HashMap_Push_Arena(resp, "body", error_body, arena); |
| 680 S3_Presigned_URL_Destroy(&presigned); | 831 S3_Presigned_URL_Destroy(&presigned); |
| 681 return resp; | 832 return resp; |
| 682 } | 833 } |
| 683 | 834 |
| 835 // Build public URL using CloudFront | |
| 836 char public_url[512]; | |
| 837 if (g_s3_cloudfront_url[0]) | |
| 838 { | |
| 839 snprintf(public_url, sizeof(public_url), "%s/%s", g_s3_cloudfront_url, s3_key); | |
| 840 } | |
| 841 else | |
| 842 { | |
| 843 snprintf(public_url, sizeof(public_url), "https://%s.s3.%s.amazonaws.com/%s", | |
| 844 g_s3_bucket, g_s3_region, s3_key); | |
| 845 } | |
| 846 | |
| 684 // Build response | 847 // Build response |
| 685 char *response_body = Dowa_Arena_Allocate(arena, 2048 + strlen(presigned.url)); | 848 char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url)); |
| 686 snprintf(response_body, 2048 + strlen(presigned.url), | 849 snprintf(response_body, 4096 + strlen(presigned.url), |
| 687 "{\"upload_url\":\"%s\",\"key\":\"%s\",\"expires\":%d}", | 850 "{\"upload_url\":\"%s\",\"public_url\":\"%s\",\"key\":\"%s\",\"expires\":%d}", |
| 688 presigned.url, s3_key, g_s3_url_expires); | 851 presigned.url, public_url, s3_key, g_s3_url_expires); |
| 689 | 852 |
| 690 S3_Presigned_URL_Destroy(&presigned); | 853 S3_Presigned_URL_Destroy(&presigned); |
| 691 | 854 |
| 692 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | 855 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); |
| 693 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | 856 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); |
| 694 Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); | 857 Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); |
| 695 | 858 |
| 696 printf("[S3] Generated upload URL for: %s\n", s3_key); | 859 printf("[S3] Generated upload URL for: %s\n", s3_key); |
| 860 | |
| 861 return resp; | |
| 862 } | |
| 863 | |
| 864 // Editor Content Save API | |
| 865 // POST /api/editor/save | |
| 866 // Headers: Authorization: Bearer <token> | |
| 867 // Body: {"doc_id": "my-doc", "content": "<html content>"} | |
| 868 Seobeo_Request_Entry *EditorSave(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 869 { | |
| 870 Seobeo_Request_Entry *resp = NULL; | |
| 871 | |
| 872 // Check auth token | |
| 873 void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); | |
| 874 if (!auth_kv) | |
| 875 { | |
| 876 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 877 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 878 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); | |
| 879 return resp; | |
| 880 } | |
| 881 | |
| 882 const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; | |
| 883 if (strncmp(auth_header, "Bearer ", 7) != 0) | |
| 884 { | |
| 885 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 886 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 887 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); | |
| 888 return resp; | |
| 889 } | |
| 890 | |
| 891 const char *token = auth_header + 7; | |
| 892 if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) | |
| 893 { | |
| 894 Dowa_HashMap_Push_Arena(resp, "status", "403", arena); | |
| 895 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 896 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); | |
| 897 return resp; | |
| 898 } | |
| 899 | |
| 900 if (!g_db_connection) | |
| 901 { | |
| 902 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 903 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 904 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); | |
| 905 return resp; | |
| 906 } | |
| 907 | |
| 908 // Parse request body | |
| 909 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 910 if (!body_kv) | |
| 911 { | |
| 912 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 913 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 914 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena); | |
| 915 return resp; | |
| 916 } | |
| 917 | |
| 918 const char *body = ((Seobeo_Request_Entry*)body_kv)->value; | |
| 919 | |
| 920 // Parse doc_id and content from JSON | |
| 921 char doc_id[256] = "default"; | |
| 922 char *content = NULL; | |
| 923 size_t content_len = 0; | |
| 924 | |
| 925 // Find "doc_id":"value" | |
| 926 const char *doc_key = strstr(body, "\"doc_id\""); | |
| 927 if (doc_key) | |
| 928 { | |
| 929 const char *doc_start = strchr(doc_key + 8, '"'); | |
| 930 if (doc_start) | |
| 931 { | |
| 932 doc_start++; | |
| 933 const char *doc_end = strchr(doc_start, '"'); | |
| 934 if (doc_end && (size_t)(doc_end - doc_start) < sizeof(doc_id)) | |
| 935 { | |
| 936 memcpy(doc_id, doc_start, doc_end - doc_start); | |
| 937 doc_id[doc_end - doc_start] = '\0'; | |
| 938 } | |
| 939 } | |
| 940 } | |
| 941 | |
| 942 // Find "content":"value" - content can be large and contain escaped characters | |
| 943 const char *content_key = strstr(body, "\"content\""); | |
| 944 if (content_key) | |
| 945 { | |
| 946 const char *content_start = strchr(content_key + 9, '"'); | |
| 947 if (content_start) | |
| 948 { | |
| 949 content_start++; | |
| 950 // Find closing quote (accounting for escaped quotes) | |
| 951 const char *p = content_start; | |
| 952 while (*p) | |
| 953 { | |
| 954 if (*p == '\\' && *(p+1)) | |
| 955 { | |
| 956 p += 2; | |
| 957 continue; | |
| 958 } | |
| 959 if (*p == '"') break; | |
| 960 p++; | |
| 961 } | |
| 962 content_len = p - content_start; | |
| 963 content = Dowa_Arena_Allocate(arena, content_len + 1); | |
| 964 memcpy(content, content_start, content_len); | |
| 965 content[content_len] = '\0'; | |
| 966 } | |
| 967 } | |
| 968 | |
| 969 if (!content) | |
| 970 { | |
| 971 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 972 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 973 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing content\"}", arena); | |
| 974 return resp; | |
| 975 } | |
| 976 | |
| 977 // Upsert content | |
| 978 const char *upsert_query = | |
| 979 "INSERT INTO editor_content (access_token, doc_id, content, updated_at) " | |
| 980 "VALUES (?, ?, ?, strftime('%s', 'now')) " | |
| 981 "ON CONFLICT(access_token, doc_id) DO UPDATE SET " | |
| 982 "content = excluded.content, updated_at = strftime('%s', 'now')"; | |
| 983 | |
| 984 const char *params[] = { token, doc_id, content }; | |
| 985 int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, upsert_query, 3, params); | |
| 986 | |
| 987 if (result < 0) | |
| 988 { | |
| 989 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 990 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 991 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to save\"}", arena); | |
| 992 return resp; | |
| 993 } | |
| 994 | |
| 995 printf("[EDITOR] Saved doc_id=%s, content_len=%zu\n", doc_id, content_len); | |
| 996 | |
| 997 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 998 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 999 Dowa_HashMap_Push_Arena(resp, "body", "{\"success\":true}", arena); | |
| 1000 return resp; | |
| 1001 } | |
| 1002 | |
| 1003 // Editor Content Load API | |
| 1004 // GET /api/editor/load/:doc_id | |
| 1005 // Headers: Authorization: Bearer <token> | |
| 1006 Seobeo_Request_Entry *EditorLoad(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 1007 { | |
| 1008 Seobeo_Request_Entry *resp = NULL; | |
| 1009 | |
| 1010 // Check auth token | |
| 1011 void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); | |
| 1012 if (!auth_kv) | |
| 1013 { | |
| 1014 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 1015 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1016 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); | |
| 1017 return resp; | |
| 1018 } | |
| 1019 | |
| 1020 const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; | |
| 1021 if (strncmp(auth_header, "Bearer ", 7) != 0) | |
| 1022 { | |
| 1023 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 1024 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1025 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); | |
| 1026 return resp; | |
| 1027 } | |
| 1028 | |
| 1029 const char *token = auth_header + 7; | |
| 1030 if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) | |
| 1031 { | |
| 1032 Dowa_HashMap_Push_Arena(resp, "status", "403", arena); | |
| 1033 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1034 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); | |
| 1035 return resp; | |
| 1036 } | |
| 1037 | |
| 1038 if (!g_db_connection) | |
| 1039 { | |
| 1040 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 1041 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1042 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); | |
| 1043 return resp; | |
| 1044 } | |
| 1045 | |
| 1046 // Get doc_id from URL parameter | |
| 1047 void *doc_id_kv = Dowa_HashMap_Get_Ptr(req, ":doc_id"); | |
| 1048 const char *doc_id = "default"; | |
| 1049 if (doc_id_kv) | |
| 1050 { | |
| 1051 doc_id = ((Seobeo_Request_Entry*)doc_id_kv)->value; | |
| 1052 } | |
| 1053 | |
| 1054 // Query content | |
| 1055 const char *select_query = | |
| 1056 "SELECT content, updated_at FROM editor_content WHERE access_token = ? AND doc_id = ?"; | |
| 1057 const char *params[] = { token, doc_id }; | |
| 1058 | |
| 1059 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, params, arena); | |
| 1060 | |
| 1061 if (p_result && Deita_Result_Set_Next(p_result)) | |
| 1062 { | |
| 1063 const char *content = Deita_Result_Set_Get_Text(p_result, 0); | |
| 1064 int64 updated_at = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 1065 | |
| 1066 // Build JSON response - escape content | |
| 1067 size_t content_len = content ? strlen(content) : 0; | |
| 1068 char *response_body = Dowa_Arena_Allocate(arena, content_len + 256); | |
| 1069 snprintf(response_body, content_len + 256, | |
| 1070 "{\"doc_id\":\"%s\",\"content\":\"%s\",\"updated_at\":%lld}", | |
| 1071 doc_id, content ? content : "", (long long)updated_at); | |
| 1072 | |
| 1073 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 1074 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1075 Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); | |
| 1076 | |
| 1077 printf("[EDITOR] Loaded doc_id=%s\n", doc_id); | |
| 1078 } | |
| 1079 else | |
| 1080 { | |
| 1081 // No content found, return empty | |
| 1082 char *response_body = Dowa_Arena_Allocate(arena, 128); | |
| 1083 snprintf(response_body, 128, "{\"doc_id\":\"%s\",\"content\":\"\",\"updated_at\":0}", doc_id); | |
| 1084 | |
| 1085 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 1086 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1087 Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); | |
| 1088 } | |
| 1089 | |
| 1090 if (p_result) Deita_Result_Set_Free(p_result); | |
| 1091 return resp; | |
| 1092 } | |
| 1093 | |
| 1094 // Media Upload API - Create media record | |
| 1095 // POST /api/media/create | |
| 1096 // Headers: Authorization: Bearer <token>, Content-Type: application/json | |
| 1097 // Body: {"filename": "photo.jpg", "content_type": "image/jpeg"} | |
| 1098 // Returns: {"media_id": 123, "upload_url": "https://...", "expires": 3600} | |
| 1099 Seobeo_Request_Entry *MediaCreate(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 1100 { | |
| 1101 Seobeo_Request_Entry *resp = NULL; | |
| 1102 | |
| 1103 // Check auth token | |
| 1104 void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); | |
| 1105 if (!auth_kv) | |
| 1106 { | |
| 1107 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 1108 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1109 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); | |
| 1110 return resp; | |
| 1111 } | |
| 1112 | |
| 1113 const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; | |
| 1114 if (strncmp(auth_header, "Bearer ", 7) != 0) | |
| 1115 { | |
| 1116 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 1117 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1118 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); | |
| 1119 return resp; | |
| 1120 } | |
| 1121 | |
| 1122 const char *token = auth_header + 7; | |
| 1123 if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) | |
| 1124 { | |
| 1125 Dowa_HashMap_Push_Arena(resp, "status", "403", arena); | |
| 1126 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1127 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); | |
| 1128 return resp; | |
| 1129 } | |
| 1130 | |
| 1131 if (!g_db_connection) | |
| 1132 { | |
| 1133 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 1134 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1135 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); | |
| 1136 return resp; | |
| 1137 } | |
| 1138 | |
| 1139 // Parse request body | |
| 1140 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 1141 if (!body_kv) | |
| 1142 { | |
| 1143 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 1144 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1145 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena); | |
| 1146 return resp; | |
| 1147 } | |
| 1148 | |
| 1149 const char *body = ((Seobeo_Request_Entry*)body_kv)->value; | |
| 1150 | |
| 1151 // Parse filename and content_type | |
| 1152 char filename[256] = {0}; | |
| 1153 char content_type[128] = "application/octet-stream"; | |
| 1154 | |
| 1155 // Find "filename":"value" | |
| 1156 const char *fn_key = strstr(body, "\"filename\""); | |
| 1157 if (fn_key) | |
| 1158 { | |
| 1159 const char *fn_start = strchr(fn_key + 10, '"'); | |
| 1160 if (fn_start) | |
| 1161 { | |
| 1162 fn_start++; | |
| 1163 const char *fn_end = strchr(fn_start, '"'); | |
| 1164 if (fn_end && (size_t)(fn_end - fn_start) < sizeof(filename)) | |
| 1165 { | |
| 1166 memcpy(filename, fn_start, fn_end - fn_start); | |
| 1167 filename[fn_end - fn_start] = '\0'; | |
| 1168 } | |
| 1169 } | |
| 1170 } | |
| 1171 | |
| 1172 // Find "content_type":"value" | |
| 1173 const char *ct_key = strstr(body, "\"content_type\""); | |
| 1174 if (ct_key) | |
| 1175 { | |
| 1176 const char *ct_start = strchr(ct_key + 14, '"'); | |
| 1177 if (ct_start) | |
| 1178 { | |
| 1179 ct_start++; | |
| 1180 const char *ct_end = strchr(ct_start, '"'); | |
| 1181 if (ct_end && (size_t)(ct_end - ct_start) < sizeof(content_type)) | |
| 1182 { | |
| 1183 memcpy(content_type, ct_start, ct_end - ct_start); | |
| 1184 content_type[ct_end - ct_start] = '\0'; | |
| 1185 } | |
| 1186 } | |
| 1187 } | |
| 1188 | |
| 1189 if (strlen(filename) == 0) | |
| 1190 { | |
| 1191 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 1192 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1193 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing filename\"}", arena); | |
| 1194 return resp; | |
| 1195 } | |
| 1196 | |
| 1197 // Generate UUID for this upload | |
| 1198 char *uuid = Dowa_Arena_Allocate(arena, UUID_LEN); | |
| 1199 uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++; | |
| 1200 Dowa_String_UUID(seed, uuid); | |
| 1201 | |
| 1202 // Generate S3 keys | |
| 1203 char s3_key_original[512]; | |
| 1204 char s3_key_processed[512]; | |
| 1205 snprintf(s3_key_original, sizeof(s3_key_original), "uploads/%s/%s", uuid, filename); | |
| 1206 snprintf(s3_key_processed, sizeof(s3_key_processed), "uploads/%s/processed.webp", uuid); | |
| 1207 | |
| 1208 // Insert into database | |
| 1209 const char *insert_query = | |
| 1210 "INSERT INTO media_uploads (access_token, original_filename, content_type, s3_key_original, s3_key_processed, status) " | |
| 1211 "VALUES (?, ?, ?, ?, ?, 'pending')"; | |
| 1212 | |
| 1213 const char *params[] = { token, filename, content_type, s3_key_original, s3_key_processed }; | |
| 1214 int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, insert_query, 5, params); | |
| 1215 | |
| 1216 if (result < 0) | |
| 1217 { | |
| 1218 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 1219 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1220 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to create media record\"}", arena); | |
| 1221 return resp; | |
| 1222 } | |
| 1223 | |
| 1224 // Get the inserted media_id using last_insert_rowid() | |
| 1225 const char *last_id_query = "SELECT last_insert_rowid()"; | |
| 1226 Deita_Result_Set *id_result = Deita_Query_Execute(g_db_connection, last_id_query, arena); | |
| 1227 int64 media_id = 0; | |
| 1228 if (id_result && Deita_Result_Set_Next(id_result)) | |
| 1229 { | |
| 1230 media_id = Deita_Result_Set_Get_Integer(id_result, 0); | |
| 1231 } | |
| 1232 if (id_result) Deita_Result_Set_Free(id_result); | |
| 1233 | |
| 1234 // Generate presigned PUT URL | |
| 1235 S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key_original, content_type, g_s3_url_expires); | |
| 1236 | |
| 1237 if (!presigned.success) | |
| 1238 { | |
| 1239 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 1240 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1241 char *error_body = Dowa_Arena_Allocate(arena, 256); | |
| 1242 snprintf(error_body, 256, "{\"error\":\"Failed to generate upload URL: %s\"}", | |
| 1243 presigned.error_message ? presigned.error_message : "unknown"); | |
| 1244 Dowa_HashMap_Push_Arena(resp, "body", error_body, arena); | |
| 1245 S3_Presigned_URL_Destroy(&presigned); | |
| 1246 return resp; | |
| 1247 } | |
| 1248 | |
| 1249 // Build response | |
| 1250 char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url)); | |
| 1251 snprintf(response_body, 4096 + strlen(presigned.url), | |
| 1252 "{\"media_id\":%lld,\"upload_url\":\"%s\",\"expires\":%d}", | |
| 1253 (long long)media_id, presigned.url, g_s3_url_expires); | |
| 1254 | |
| 1255 S3_Presigned_URL_Destroy(&presigned); | |
| 1256 | |
| 1257 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 1258 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1259 Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); | |
| 1260 | |
| 1261 printf("[MEDIA] Created media_id=%lld, file=%s\n", (long long)media_id, filename); | |
| 1262 | |
| 1263 return resp; | |
| 1264 } | |
| 1265 | |
| 1266 // Background thread function for media processing | |
| 1267 void *Media_Process_Background(void *arg) | |
| 1268 { | |
| 1269 Media_Processing_Context *ctx = (Media_Processing_Context *)arg; | |
| 1270 | |
| 1271 // Open thread-local DB connection | |
| 1272 Deita_Connection *db_conn = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, g_db_path); | |
| 1273 if (!db_conn || !Deita_Connection_Is_Open(db_conn)) | |
| 1274 { | |
| 1275 printf("[MEDIA] Thread ERROR: Failed to open database for media_id=%lld\n", (long long)ctx->media_id); | |
| 1276 free(ctx); | |
| 1277 return NULL; | |
| 1278 } | |
| 1279 | |
| 1280 // Update status to 'processing' | |
| 1281 const char *update_processing = | |
| 1282 "UPDATE media_uploads SET status='processing', updated_at=strftime('%s','now') WHERE id=?"; | |
| 1283 char media_id_str[32]; | |
| 1284 snprintf(media_id_str, sizeof(media_id_str), "%lld", (long long)ctx->media_id); | |
| 1285 const char *params[] = { media_id_str }; | |
| 1286 Deita_Query_Execute_Update_Prepared(db_conn, update_processing, 1, params); | |
| 1287 | |
| 1288 printf("[MEDIA] Processing media_id=%lld\n", (long long)ctx->media_id); | |
| 1289 | |
| 1290 // Generate presigned GET URL for download (10 min expiry) | |
| 1291 S3_Presigned_URL download_url = S3_Presign_Get(&ctx->s3_config, ctx->s3_key_original, 600); | |
| 1292 if (!download_url.success) | |
| 1293 { | |
| 1294 const char *update_error = | |
| 1295 "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?"; | |
| 1296 const char *error_params[] = { "Failed to generate download URL", media_id_str }; | |
| 1297 Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params); | |
| 1298 printf("[MEDIA] ERROR: Failed to generate download URL for media_id=%lld\n", (long long)ctx->media_id); | |
| 1299 S3_Presigned_URL_Destroy(&download_url); | |
| 1300 Deita_Connection_Close(db_conn); | |
| 1301 free(ctx); | |
| 1302 return NULL; | |
| 1303 } | |
| 1304 | |
| 1305 // Generate temp file paths | |
| 1306 char tmp_input[256]; | |
| 1307 char tmp_output[256]; | |
| 1308 char *uuid_input = malloc(UUID_LEN); | |
| 1309 char *uuid_output = malloc(UUID_LEN); | |
| 1310 uint32 seed1 = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++; | |
| 1311 uint32 seed2 = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++; | |
| 1312 Dowa_String_UUID(seed1, uuid_input); | |
| 1313 Dowa_String_UUID(seed2, uuid_output); | |
| 1314 snprintf(tmp_input, sizeof(tmp_input), "/tmp/%s", uuid_input); | |
| 1315 snprintf(tmp_output, sizeof(tmp_output), "/tmp/%s.webp", uuid_output); | |
| 1316 free(uuid_input); | |
| 1317 free(uuid_output); | |
| 1318 | |
| 1319 // Download from S3 | |
| 1320 Seobeo_Client_Request *download_req = Seobeo_Client_Request_Create(download_url.url); | |
| 1321 Seobeo_Client_Request_Set_Download_Path(download_req, tmp_input); | |
| 1322 Seobeo_Client_Response *download_resp = Seobeo_Client_Request_Execute(download_req); | |
| 1323 | |
| 1324 S3_Presigned_URL_Destroy(&download_url); | |
| 1325 | |
| 1326 if (!download_resp || download_resp->status_code != 200) | |
| 1327 { | |
| 1328 const char *update_error = | |
| 1329 "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?"; | |
| 1330 const char *error_params[] = { "Failed to download from S3", media_id_str }; | |
| 1331 Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params); | |
| 1332 printf("[MEDIA] ERROR: Failed to download from S3 for media_id=%lld\n", (long long)ctx->media_id); | |
| 1333 if (download_req) Seobeo_Client_Request_Destroy(download_req); | |
| 1334 if (download_resp) Seobeo_Client_Response_Destroy(download_resp); | |
| 1335 unlink(tmp_input); | |
| 1336 Deita_Connection_Close(db_conn); | |
| 1337 free(ctx); | |
| 1338 return NULL; | |
| 1339 } | |
| 1340 | |
| 1341 Seobeo_Client_Request_Destroy(download_req); | |
| 1342 Seobeo_Client_Response_Destroy(download_resp); | |
| 1343 | |
| 1344 // Convert to webp using FFmpeg | |
| 1345 char cmd[1024]; | |
| 1346 char log_file[256]; | |
| 1347 snprintf(log_file, sizeof(log_file), "/tmp/ffmpeg_%lld.log", (long long)ctx->media_id); | |
| 1348 snprintf(cmd, sizeof(cmd), "ffmpeg -y -i %s -quality 80 %s 2>%s", | |
| 1349 tmp_input, tmp_output, log_file); | |
| 1350 | |
| 1351 int ffmpeg_result = system(cmd); | |
| 1352 if (ffmpeg_result != 0) | |
| 1353 { | |
| 1354 const char *update_error = | |
| 1355 "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?"; | |
| 1356 const char *error_params[] = { "Image conversion failed", media_id_str }; | |
| 1357 Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params); | |
| 1358 printf("[MEDIA] ERROR: FFmpeg conversion failed for media_id=%lld\n", (long long)ctx->media_id); | |
| 1359 unlink(tmp_input); | |
| 1360 unlink(tmp_output); | |
| 1361 Deita_Connection_Close(db_conn); | |
| 1362 free(ctx); | |
| 1363 return NULL; | |
| 1364 } | |
| 1365 | |
| 1366 // Upload processed file to S3 | |
| 1367 S3_Result upload_result = S3_Upload_File_With_Content_Type( | |
| 1368 &ctx->s3_config, tmp_output, ctx->s3_key_processed, "image/webp"); | |
| 1369 | |
| 1370 if (!upload_result.success) | |
| 1371 { | |
| 1372 const char *update_error = | |
| 1373 "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?"; | |
| 1374 const char *error_params[] = { "Failed to upload processed file", media_id_str }; | |
| 1375 Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params); | |
| 1376 printf("[MEDIA] ERROR: Failed to upload processed file for media_id=%lld\n", (long long)ctx->media_id); | |
| 1377 unlink(tmp_input); | |
| 1378 unlink(tmp_output); | |
| 1379 Deita_Connection_Close(db_conn); | |
| 1380 free(ctx); | |
| 1381 return NULL; | |
| 1382 } | |
| 1383 | |
| 1384 // Update status to 'finished' | |
| 1385 const char *update_finished = | |
| 1386 "UPDATE media_uploads SET status='finished', updated_at=strftime('%s','now') WHERE id=?"; | |
| 1387 Deita_Query_Execute_Update_Prepared(db_conn, update_finished, 1, params); | |
| 1388 | |
| 1389 printf("[MEDIA] Successfully processed media_id=%lld\n", (long long)ctx->media_id); | |
| 1390 | |
| 1391 // Cleanup | |
| 1392 unlink(tmp_input); | |
| 1393 unlink(tmp_output); | |
| 1394 Deita_Connection_Close(db_conn); | |
| 1395 free(ctx); | |
| 1396 | |
| 1397 return NULL; | |
| 1398 } | |
| 1399 | |
| 1400 // Media Upload API - Mark uploaded | |
| 1401 // POST /api/media/:id/uploaded | |
| 1402 // Headers: Authorization: Bearer <token> | |
| 1403 Seobeo_Request_Entry *MediaUploaded(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 1404 { | |
| 1405 Seobeo_Request_Entry *resp = NULL; | |
| 1406 | |
| 1407 // Check auth token | |
| 1408 void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); | |
| 1409 if (!auth_kv) | |
| 1410 { | |
| 1411 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 1412 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1413 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); | |
| 1414 return resp; | |
| 1415 } | |
| 1416 | |
| 1417 const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; | |
| 1418 if (strncmp(auth_header, "Bearer ", 7) != 0) | |
| 1419 { | |
| 1420 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 1421 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1422 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); | |
| 1423 return resp; | |
| 1424 } | |
| 1425 | |
| 1426 const char *token = auth_header + 7; | |
| 1427 if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) | |
| 1428 { | |
| 1429 Dowa_HashMap_Push_Arena(resp, "status", "403", arena); | |
| 1430 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1431 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); | |
| 1432 return resp; | |
| 1433 } | |
| 1434 | |
| 1435 if (!g_db_connection) | |
| 1436 { | |
| 1437 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 1438 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1439 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); | |
| 1440 return resp; | |
| 1441 } | |
| 1442 | |
| 1443 // Extract media_id from URL params | |
| 1444 void *id_kv = Dowa_HashMap_Get_Ptr(req, ":id"); | |
| 1445 if (!id_kv) | |
| 1446 { | |
| 1447 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 1448 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1449 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing media ID\"}", arena); | |
| 1450 return resp; | |
| 1451 } | |
| 1452 | |
| 1453 const char *media_id_str = ((Seobeo_Request_Entry*)id_kv)->value; | |
| 1454 int64 media_id = atoll(media_id_str); | |
| 1455 | |
| 1456 // Verify access_token matches and get content_type | |
| 1457 const char *select_query = | |
| 1458 "SELECT content_type, s3_key_original, s3_key_processed FROM media_uploads WHERE id = ? AND access_token = ?"; | |
| 1459 const char *select_params[] = { media_id_str, token }; | |
| 1460 | |
| 1461 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, select_params, arena); | |
| 1462 | |
| 1463 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 1464 { | |
| 1465 if (p_result) Deita_Result_Set_Free(p_result); | |
| 1466 Dowa_HashMap_Push_Arena(resp, "status", "404", arena); | |
| 1467 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1468 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Media not found or access denied\"}", arena); | |
| 1469 return resp; | |
| 1470 } | |
| 1471 | |
| 1472 const char *content_type = Deita_Result_Set_Get_Text(p_result, 0); | |
| 1473 const char *s3_key_original = Deita_Result_Set_Get_Text(p_result, 1); | |
| 1474 const char *s3_key_processed = Deita_Result_Set_Get_Text(p_result, 2); | |
| 1475 | |
| 1476 // Copy values before freeing result set | |
| 1477 char content_type_copy[128]; | |
| 1478 char s3_key_original_copy[512]; | |
| 1479 char s3_key_processed_copy[512]; | |
| 1480 strncpy(content_type_copy, content_type, sizeof(content_type_copy) - 1); | |
| 1481 strncpy(s3_key_original_copy, s3_key_original, sizeof(s3_key_original_copy) - 1); | |
| 1482 strncpy(s3_key_processed_copy, s3_key_processed, sizeof(s3_key_processed_copy) - 1); | |
| 1483 content_type_copy[sizeof(content_type_copy) - 1] = '\0'; | |
| 1484 s3_key_original_copy[sizeof(s3_key_original_copy) - 1] = '\0'; | |
| 1485 s3_key_processed_copy[sizeof(s3_key_processed_copy) - 1] = '\0'; | |
| 1486 | |
| 1487 Deita_Result_Set_Free(p_result); | |
| 1488 | |
| 1489 // Update status to 'uploaded' | |
| 1490 const char *update_query = | |
| 1491 "UPDATE media_uploads SET status='uploaded', updated_at=strftime('%s','now') WHERE id=?"; | |
| 1492 const char *update_params[] = { media_id_str }; | |
| 1493 int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, update_query, 1, update_params); | |
| 1494 | |
| 1495 if (result < 0) | |
| 1496 { | |
| 1497 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 1498 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1499 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to update status\"}", arena); | |
| 1500 return resp; | |
| 1501 } | |
| 1502 | |
| 1503 // If content_type starts with "image/", spawn background processing thread | |
| 1504 if (strncmp(content_type_copy, "image/", 6) == 0) | |
| 1505 { | |
| 1506 // Create context for background thread (heap allocated) | |
| 1507 Media_Processing_Context *ctx = malloc(sizeof(Media_Processing_Context)); | |
| 1508 ctx->media_id = media_id; | |
| 1509 strncpy(ctx->s3_key_original, s3_key_original_copy, sizeof(ctx->s3_key_original) - 1); | |
| 1510 strncpy(ctx->s3_key_processed, s3_key_processed_copy, sizeof(ctx->s3_key_processed) - 1); | |
| 1511 strncpy(ctx->content_type, content_type_copy, sizeof(ctx->content_type) - 1); | |
| 1512 strncpy(ctx->access_token, token, sizeof(ctx->access_token) - 1); | |
| 1513 ctx->s3_key_original[sizeof(ctx->s3_key_original) - 1] = '\0'; | |
| 1514 ctx->s3_key_processed[sizeof(ctx->s3_key_processed) - 1] = '\0'; | |
| 1515 ctx->content_type[sizeof(ctx->content_type) - 1] = '\0'; | |
| 1516 ctx->access_token[sizeof(ctx->access_token) - 1] = '\0'; | |
| 1517 ctx->s3_config = g_s3_config; | |
| 1518 | |
| 1519 // Spawn detached thread | |
| 1520 pthread_t thread_id; | |
| 1521 int thread_result = pthread_create(&thread_id, NULL, Media_Process_Background, ctx); | |
| 1522 | |
| 1523 if (thread_result != 0) | |
| 1524 { | |
| 1525 Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: Failed to spawn processing thread for media_id=%lld\n", (long long)media_id); | |
| 1526 free(ctx); | |
| 1527 } | |
| 1528 else | |
| 1529 { | |
| 1530 // Detach thread so it cleans up automatically when done | |
| 1531 pthread_detach(thread_id); | |
| 1532 Seobeo_Log(SEOBEO_INFO, "[MEDIA] Spawned processing thread for media_id=%lld\n", (long long)media_id); | |
| 1533 } | |
| 1534 } | |
| 1535 | |
| 1536 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 1537 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1538 Dowa_HashMap_Push_Arena(resp, "body", "{\"success\":true,\"status\":\"uploaded\"}", arena); | |
| 1539 | |
| 1540 Seobeo_Log(SEOBEO_INFO, "[MEDIA] Marked uploaded media_id=%lld\n", (long long)media_id); | |
| 1541 | |
| 1542 return resp; | |
| 1543 } | |
| 1544 | |
| 1545 // Media Upload API - Get status | |
| 1546 // GET /api/media/:id/status | |
| 1547 // Headers: Authorization: Bearer <token> | |
| 1548 // Returns: {"id": 123, "status": "finished", "processed_url": "https://...", "error_message": null} | |
| 1549 Seobeo_Request_Entry *MediaStatus(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 1550 { | |
| 1551 Seobeo_Request_Entry *resp = NULL; | |
| 1552 | |
| 1553 // Check auth token | |
| 1554 void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); | |
| 1555 if (!auth_kv) | |
| 1556 { | |
| 1557 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 1558 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1559 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); | |
| 1560 return resp; | |
| 1561 } | |
| 1562 | |
| 1563 const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; | |
| 1564 if (strncmp(auth_header, "Bearer ", 7) != 0) | |
| 1565 { | |
| 1566 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 1567 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1568 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena); | |
| 1569 return resp; | |
| 1570 } | |
| 1571 | |
| 1572 const char *token = auth_header + 7; | |
| 1573 if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) | |
| 1574 { | |
| 1575 Dowa_HashMap_Push_Arena(resp, "status", "403", arena); | |
| 1576 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1577 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); | |
| 1578 return resp; | |
| 1579 } | |
| 1580 | |
| 1581 if (!g_db_connection) | |
| 1582 { | |
| 1583 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 1584 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1585 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena); | |
| 1586 return resp; | |
| 1587 } | |
| 1588 | |
| 1589 // Extract media_id from URL params | |
| 1590 void *id_kv = Dowa_HashMap_Get_Ptr(req, ":id"); | |
| 1591 if (!id_kv) | |
| 1592 { | |
| 1593 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 1594 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1595 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing media ID\"}", arena); | |
| 1596 return resp; | |
| 1597 } | |
| 1598 | |
| 1599 const char *media_id_str = ((Seobeo_Request_Entry*)id_kv)->value; | |
| 1600 | |
| 1601 // Query media status | |
| 1602 const char *select_query = | |
| 1603 "SELECT id, status, s3_key_processed, error_message FROM media_uploads WHERE id = ? AND access_token = ?"; | |
| 1604 const char *select_params[] = { media_id_str, token }; | |
| 1605 | |
| 1606 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, select_params, arena); | |
| 1607 | |
| 1608 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 1609 { | |
| 1610 if (p_result) Deita_Result_Set_Free(p_result); | |
| 1611 Dowa_HashMap_Push_Arena(resp, "status", "404", arena); | |
| 1612 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1613 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Media not found\"}", arena); | |
| 1614 return resp; | |
| 1615 } | |
| 1616 | |
| 1617 int64 id = Deita_Result_Set_Get_Integer(p_result, 0); | |
| 1618 const char *status = Deita_Result_Set_Get_Text(p_result, 1); | |
| 1619 const char *s3_key_processed = Deita_Result_Set_Get_Text(p_result, 2); | |
| 1620 const char *error_message = Deita_Result_Set_Get_Text(p_result, 3); | |
| 1621 | |
| 1622 // Build CloudFront URL if status is 'finished' and s3_key_processed exists | |
| 1623 char processed_url[1024] = {0}; | |
| 1624 if (strcmp(status, "finished") == 0 && s3_key_processed && strlen(s3_key_processed) > 0) | |
| 1625 { | |
| 1626 if (g_s3_cloudfront_url[0]) | |
| 1627 { | |
| 1628 snprintf(processed_url, sizeof(processed_url), "%s/%s", g_s3_cloudfront_url, s3_key_processed); | |
| 1629 } | |
| 1630 else | |
| 1631 { | |
| 1632 snprintf(processed_url, sizeof(processed_url), "https://%s.s3.%s.amazonaws.com/%s", | |
| 1633 g_s3_bucket, g_s3_region, s3_key_processed); | |
| 1634 } | |
| 1635 } | |
| 1636 | |
| 1637 // Build JSON response | |
| 1638 char *response_body = Dowa_Arena_Allocate(arena, 2048); | |
| 1639 if (strlen(processed_url) > 0) | |
| 1640 { | |
| 1641 snprintf(response_body, 2048, | |
| 1642 "{\"id\":%lld,\"status\":\"%s\",\"processed_url\":\"%s\",\"error_message\":%s}", | |
| 1643 (long long)id, status, processed_url, | |
| 1644 error_message ? "\"" : "null"); | |
| 1645 if (error_message) | |
| 1646 { | |
| 1647 // Append error message if exists | |
| 1648 size_t len = strlen(response_body); | |
| 1649 snprintf(response_body + len - 1, 2048 - len + 1, "%s\"}", error_message); | |
| 1650 } | |
| 1651 } | |
| 1652 else | |
| 1653 { | |
| 1654 snprintf(response_body, 2048, | |
| 1655 "{\"id\":%lld,\"status\":\"%s\",\"processed_url\":null,\"error_message\":%s}", | |
| 1656 (long long)id, status, | |
| 1657 error_message ? "\"" : "null"); | |
| 1658 if (error_message) | |
| 1659 { | |
| 1660 size_t len = strlen(response_body); | |
| 1661 snprintf(response_body + len - 1, 2048 - len + 1, "%s\"}", error_message); | |
| 1662 } | |
| 1663 } | |
| 1664 | |
| 1665 Deita_Result_Set_Free(p_result); | |
| 1666 | |
| 1667 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 1668 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 1669 Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); | |
| 697 | 1670 |
| 698 return resp; | 1671 return resp; |
| 699 } | 1672 } |
| 700 | 1673 |
| 701 int main(void) | 1674 int main(void) |
| 740 g_s3_config.use_path_style = FALSE; | 1713 g_s3_config.use_path_style = FALSE; |
| 741 | 1714 |
| 742 printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n", | 1715 printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n", |
| 743 g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)"); | 1716 g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)"); |
| 744 | 1717 |
| 1718 // Initialize database | |
| 1719 init_database(); | |
| 1720 | |
| 745 Seobeo_Router_Init(); | 1721 Seobeo_Router_Init(); |
| 746 | 1722 |
| 747 Seobeo_Router_Register("GET", "/", GetHomePage); | 1723 Seobeo_Router_Register("GET", "/", GetHomePage); |
| 748 Seobeo_Router_Register("GET", "/index.html", GetRedirectHomePage); | 1724 Seobeo_Router_Register("GET", "/index.html", GetRedirectHomePage); |
| 749 | 1725 |
| 765 Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile); | 1741 Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile); |
| 766 | 1742 |
| 767 // -- S3 Upload --/ | 1743 // -- S3 Upload --/ |
| 768 Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl); | 1744 Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl); |
| 769 | 1745 |
| 1746 // -- Media Upload --/ | |
| 1747 Seobeo_Router_Register("POST", "/api/media/create", MediaCreate); | |
| 1748 Seobeo_Router_Register("POST", "/api/media/:id/uploaded", MediaUploaded); | |
| 1749 Seobeo_Router_Register("GET", "/api/media/:id/status", MediaStatus); | |
| 1750 | |
| 1751 // -- Editor --/ | |
| 1752 Seobeo_Router_Register("POST", "/api/editor/save", EditorSave); | |
| 1753 Seobeo_Router_Register("GET", "/api/editor/load/:doc_id", EditorLoad); | |
| 1754 | |
| 770 // -- Blog --/ | 1755 // -- Blog --/ |
| 771 Seobeo_Router_Register("GET", "/blog", RenderBlogList); | 1756 Seobeo_Router_Register("GET", "/blog", RenderBlogList); |
| 772 Seobeo_Router_Register("GET", "/blog/:blog_id", RenderBlog); | 1757 Seobeo_Router_Register("GET", "/blog/:blog_id", RenderBlog); |
| 773 | 1758 |
| 774 // -- Talk --/ | 1759 // -- Talk --/ |
| 775 Seobeo_Router_Register("GET", "/talk", GetTalk); | 1760 Seobeo_Router_Register("GET", "/talk", GetTalk); |
| 776 Seobeo_Router_Register("GET", "/talk/index.html", GetRedirectTalk); | 1761 Seobeo_Router_Register("GET", "/talk/index.html", GetRedirectTalk); |
| 777 | 1762 |
| 1763 // -- Editor (legacy) --/ | |
| 1764 Seobeo_Router_Register("GET", "/editor", GetEditor); | |
| 1765 Seobeo_Router_Register("GET", "/editor/index.html", GetRedirectEditor); | |
| 1766 | |
| 1767 // -- Notes --/ | |
| 1768 Seobeo_Router_Register("GET", "/notes", GetNotes); | |
| 1769 Seobeo_Router_Register("GET", "/notes/", GetNotes); | |
| 1770 Seobeo_Router_Register("GET", "/notes/index.html", GetNotes); | |
| 1771 Seobeo_Router_Register("GET", "/notes/login", GetNotesLogin); | |
| 1772 Seobeo_Router_Register("GET", "/notes/login/", GetNotesLogin); | |
| 1773 Seobeo_Router_Register("GET", "/notes/:note_id", GetNoteById); | |
| 1774 | |
| 778 Seobeo_WebSocket_Server_Init(); | 1775 Seobeo_WebSocket_Server_Init(); |
| 779 Seobeo_WebSocket_Server_Register("/chat", Chat_Handler, NULL); | 1776 Seobeo_WebSocket_Server_Register("/chat", Chat_Handler, NULL); |
| 780 | 1777 |
| 781 Seobeo_Web_Server_Start("mrjunejune/src", "6969", SEOBEO_MODE_EDGE, 3); | 1778 Seobeo_Log(SEOBEO_INFO, "WTF is going on\n"); |
| 782 } | 1779 Seobeo_Web_Server_Start("mrjunejune/src", "6969", SEOBEO_MODE_EDGE, 1); |
| 1780 } |