diff mrjunejune/main.c @ 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 295ac2e5ec00
children 6cdee35a7ba9
line wrap: on
line diff
--- 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);