Mercurial
changeset 200:90dfcef375fb
Added my own s3 bucket uploader url to mrjunejune.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 14 Feb 2026 16:32:24 -0800 |
| parents | b4a070994b54 |
| children | 6cdee35a7ba9 |
| files | .hgignore mrjunejune/.config.development mrjunejune/BUILD mrjunejune/main.c |
| diffstat | 4 files changed, 276 insertions(+), 6 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Sat Feb 14 16:18:25 2026 -0800 +++ b/.hgignore Sat Feb 14 16:32:24 2026 -0800 @@ -41,5 +41,7 @@ # OS related .DS_Store -# environment var. +# Environment var. .env +# Server config +.config
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/.config.development Sat Feb 14 16:32:24 2026 -0800 @@ -0,0 +1,12 @@ +# MrJuneJune Server Configuration + +# Auth token for S3 upload API +# Client must send this in Authorization header: Bearer <token> +UPLOAD_AUTH_TOKEN=THIS + +# S3 Configuration +S3_REGION=THIS +S3_BUCKET=THIS + +# Presigned URL expiration (seconds) +S3_URL_EXPIRES=SECONDS
--- a/mrjunejune/BUILD Sat Feb 14 16:18:25 2026 -0800 +++ b/mrjunejune/BUILD Sat Feb 14 16:32:24 2026 -0800 @@ -51,10 +51,15 @@ name = "mrjunejune_server", srcs = ["main.c"], deps = [ - "//seobeo:seobeo_tcp_server_ws", + "//seobeo:seobeo", "//markdown_converter:markdown_to_html_c", + "//s3:s3", ], - data = [":src_files"], + data = [ + ":src_files", + ":config_file", + "//:env_file", + ], visibility = ["//mrjunejune/test:__pkg__"], ) @@ -63,10 +68,21 @@ name = "mrjunejune_server_debug", srcs = ["main.c"], deps = [ - "//seobeo:seobeo_tcp_server_ws_debug", - "//markdown_converter:markdown_to_html_c" + "//seobeo:seobeo_debug", + "//markdown_converter:markdown_to_html_c", + "//s3:s3", ], - data = [":src_files"], + data = [ + ":src_files", + ":config_file", + "//:env_file", + ], +) + +filegroup( + name = "config_file", + srcs = [".config"], + visibility = ["//visibility:public"], ) # Run this to create html files
--- a/mrjunejune/main.c Sat Feb 14 16:18:25 2026 -0800 +++ b/mrjunejune/main.c Sat Feb 14 16:32:24 2026 -0800 @@ -1,5 +1,6 @@ #include "seobeo/seobeo.h" #include "markdown_converter/markdown_to_html.h" +#include "s3/s3_uploader.h" #include <time.h> // UUID + /tmp/ + format (max 4) @@ -9,6 +10,64 @@ volatile sig_atomic_t stop_server = 0; static _Atomic uint32_t counter = 0; +// Server configuration (loaded from .config) +static char g_upload_auth_token[256] = {0}; +static char g_s3_region[64] = "us-west-2"; +static char g_s3_bucket[128] = "mrjunejune"; +static int g_s3_url_expires = 3600; +static S3_Config g_s3_config = {0}; + +static void load_config(const char *config_path) +{ + FILE *f = fopen(config_path, "r"); + if (!f) + { + printf("[CONFIG] Warning: Could not open %s, using defaults\n", config_path); + return; + } + + char line[512]; + while (fgets(line, sizeof(line), f)) + { + // Skip comments and empty lines + if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue; + + char *eq = strchr(line, '='); + if (!eq) continue; + + *eq = '\0'; + char *key = line; + char *value = eq + 1; + + // Trim newline from value + size_t vlen = strlen(value); + while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r')) + value[--vlen] = '\0'; + + if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0) + { + strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1); + } + else if (strcmp(key, "S3_REGION") == 0) + { + strncpy(g_s3_region, value, sizeof(g_s3_region) - 1); + } + else if (strcmp(key, "S3_BUCKET") == 0) + { + strncpy(g_s3_bucket, value, sizeof(g_s3_bucket) - 1); + } + else if (strcmp(key, "S3_URL_EXPIRES") == 0) + { + g_s3_url_expires = atoi(value); + } + } + fclose(f); + + printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d\n", + g_upload_auth_token[0] ? "***" : "(empty)", + g_s3_region, g_s3_bucket, g_s3_url_expires); +} + void handle_sigint(int sig) { printf("Failed\n"); @@ -503,8 +562,186 @@ CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter") CREATE_REDIRECT_HANDLER(Talk, "/talk") +// S3 Upload URL API +// POST /api/s3/upload-url +// Headers: Authorization: Bearer <token>, Content-Type: application/json +// Body: {"filename": "photo.png", "content_type": "image/png"} +// Returns: {"upload_url": "https://...", "key": "uploads/..."} +Seobeo_Request_Entry *GetS3UploadUrl(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + + // Check auth token + void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); + if (!auth_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); + return resp; + } + + const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; + + // Expect "Bearer <token>" + if (strncmp(auth_header, "Bearer ", 7) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "401", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format, use Bearer token\"}", arena); + return resp; + } + + const char *token = auth_header + 7; + if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "403", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); + return resp; + } + + // Parse request body for filename and content_type + void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); + if (!body_kv) + { + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena); + return resp; + } + + const char *body = ((Seobeo_Request_Entry*)body_kv)->value; + + // Simple JSON parsing for filename and content_type + char filename[256] = {0}; + char content_type[128] = "application/octet-stream"; + + // Find "filename":"value" + const char *fn_key = strstr(body, "\"filename\""); + if (fn_key) + { + const char *fn_start = strchr(fn_key + 10, '"'); + if (fn_start) + { + fn_start++; + const char *fn_end = strchr(fn_start, '"'); + if (fn_end && (size_t)(fn_end - fn_start) < sizeof(filename)) + { + memcpy(filename, fn_start, fn_end - fn_start); + filename[fn_end - fn_start] = '\0'; + } + } + } + + // Find "content_type":"value" + const char *ct_key = strstr(body, "\"content_type\""); + if (ct_key) + { + const char *ct_start = strchr(ct_key + 14, '"'); + if (ct_start) + { + ct_start++; + const char *ct_end = strchr(ct_start, '"'); + if (ct_end && (size_t)(ct_end - ct_start) < sizeof(content_type)) + { + memcpy(content_type, ct_start, ct_end - ct_start); + content_type[ct_end - ct_start] = '\0'; + } + } + } + + if (strlen(filename) == 0) + { + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing filename in request body\"}", arena); + return resp; + } + + // Generate unique S3 key with timestamp + char s3_key[512]; + char *uuid = Dowa_Arena_Allocate(arena, UUID_LEN); + uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++; + Dowa_String_UUID(seed, uuid); + snprintf(s3_key, sizeof(s3_key), "uploads/%s/%s", uuid, filename); + + // Generate presigned URL + S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key, content_type, g_s3_url_expires); + + if (!presigned.success) + { + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + char *error_body = Dowa_Arena_Allocate(arena, 256); + snprintf(error_body, 256, "{\"error\":\"Failed to generate upload URL: %s\"}", + presigned.error_message ? presigned.error_message : "unknown"); + Dowa_HashMap_Push_Arena(resp, "body", error_body, arena); + S3_Presigned_URL_Destroy(&presigned); + return resp; + } + + // Build response + char *response_body = Dowa_Arena_Allocate(arena, 2048 + strlen(presigned.url)); + snprintf(response_body, 2048 + strlen(presigned.url), + "{\"upload_url\":\"%s\",\"key\":\"%s\",\"expires\":%d}", + presigned.url, s3_key, g_s3_url_expires); + + S3_Presigned_URL_Destroy(&presigned); + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); + + printf("[S3] Generated upload URL for: %s\n", s3_key); + + return resp; +} + int main(void) { + // Load server config + load_config("mrjunejune/.config"); + + // Load S3 credentials from .env + FILE *env_file = fopen(".env", "r"); + static char s3_access_key[128] = {0}; + static char s3_secret_key[128] = {0}; + + if (env_file) + { + char line[512]; + while (fgets(line, sizeof(line), env_file)) + { + if (strncmp(line, "AWS_MRJUNEJUNE_ACCESS_KEY=", 26) == 0) + { + char *val = line + 26; + size_t len = strlen(val); + while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0'; + strncpy(s3_access_key, val, sizeof(s3_access_key) - 1); + } + else if (strncmp(line, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY=", 33) == 0) + { + char *val = line + 33; + size_t len = strlen(val); + while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0'; + strncpy(s3_secret_key, val, sizeof(s3_secret_key) - 1); + } + } + fclose(env_file); + } + + // Initialize S3 config + g_s3_config.access_key_id = s3_access_key; + g_s3_config.secret_access_key = s3_secret_key; + g_s3_config.region = g_s3_region; + g_s3_config.bucket = g_s3_bucket; + g_s3_config.endpoint = NULL; + g_s3_config.use_path_style = FALSE; + + printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n", + g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)"); + Seobeo_Router_Init(); Seobeo_Router_Register("GET", "/", GetHomePage); @@ -527,6 +764,9 @@ Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4); Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile); + // -- S3 Upload --/ + Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl); + // -- Blog --/ Seobeo_Router_Register("GET", "/blog", RenderBlogList); Seobeo_Router_Register("GET", "/blog/:blog_id", RenderBlog);