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 }