comparison mrjunejune/main.c @ 231:09a96dcb2b4c hg-web

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