changeset 206:240337164a80

[Seobeo] SSL should be used for large file as well lol.
author MrJuneJune <me@mrjunejune.com>
date Sun, 15 Feb 2026 11:41:53 -0800
parents e07b4b5a66bb
children 58d9b64d8dca
files mrjunejune/main.c rich_editor/rich_editor.js s3/s3_uploader.c seobeo/s_network.c
diffstat 4 files changed, 276 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/mrjunejune/main.c	Sun Feb 15 11:07:52 2026 -0800
+++ b/mrjunejune/main.c	Sun Feb 15 11:41:53 2026 -0800
@@ -1402,8 +1402,8 @@
   if (!upload_result.success)
   {
     const char *error_msg = upload_result.error_message ? upload_result.error_message : "Failed to upload processed file";
-    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: Failed to upload processed file for media_id=%lld: %s\n",
-               (long long)ctx->media_id, error_msg);
+    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: Failed to upload processed file for media_id=%lld: %s (HTTP status: %d)\n",
+               (long long)ctx->media_id, error_msg, upload_result.status_code);
     const char *update_error =
       "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?";
     const char *error_params[] = { error_msg, media_id_str };
--- a/rich_editor/rich_editor.js	Sun Feb 15 11:07:52 2026 -0800
+++ b/rich_editor/rich_editor.js	Sun Feb 15 11:41:53 2026 -0800
@@ -160,6 +160,83 @@
         height: auto;
         border-radius: 4px;
         margin: 8px 0;
+        cursor: pointer;
+      }
+
+      .rich-editor-content img.selected {
+        outline: 2px solid #0078ff;
+        outline-offset: 2px;
+      }
+
+      .rich-editor-resize-wrapper {
+        position: relative;
+        display: inline-block;
+        margin: 8px 0;
+      }
+
+      .rich-editor-resize-handle {
+        position: absolute;
+        width: 12px;
+        height: 12px;
+        background: #0078ff;
+        border: 2px solid white;
+        border-radius: 50%;
+        cursor: nwse-resize;
+        z-index: 10;
+      }
+
+      .rich-editor-resize-handle.nw {
+        top: -6px;
+        left: -6px;
+        cursor: nwse-resize;
+      }
+
+      .rich-editor-resize-handle.ne {
+        top: -6px;
+        right: -6px;
+        cursor: nesw-resize;
+      }
+
+      .rich-editor-resize-handle.sw {
+        bottom: -6px;
+        left: -6px;
+        cursor: nesw-resize;
+      }
+
+      .rich-editor-resize-handle.se {
+        bottom: -6px;
+        right: -6px;
+        cursor: nwse-resize;
+      }
+
+      .rich-editor-size-input {
+        position: absolute;
+        top: -35px;
+        left: 50%;
+        transform: translateX(-50%);
+        background: white;
+        border: 1px solid #ccc;
+        padding: 4px 8px;
+        border-radius: 4px;
+        font-size: 12px;
+        display: flex;
+        gap: 8px;
+        align-items: center;
+        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+        z-index: 11;
+      }
+
+      .rich-editor-size-input input {
+        width: 60px;
+        padding: 2px 4px;
+        border: 1px solid #ccc;
+        border-radius: 2px;
+        font-size: 12px;
+      }
+
+      .rich-editor-size-input label {
+        font-size: 11px;
+        color: #666;
       }
 
       .rich-editor-content a {
@@ -242,7 +319,11 @@
     constructor(elementId, options) {
       this.options = { ...DEFAULT_OPTIONS, ...options };
       this.container = document.getElementById(elementId);
-      this.state = { readOnly: false };
+      this.state = {
+        readOnly: false,
+        selectedImage: null,
+        resizing: false
+      };
 
       if (!this.container) {
         throw new Error(`Element with id "${elementId}" not found`);
@@ -391,6 +472,23 @@
           }
         }
       });
+
+      // Handle image clicks for resizing
+      this.content.addEventListener('click', (e) => {
+        if (e.target.tagName === 'IMG') {
+          e.preventDefault();
+          this.selectImage(e.target);
+        } else if (!e.target.closest('.rich-editor-resize-wrapper')) {
+          this.deselectImage();
+        }
+      });
+
+      // Deselect image when clicking outside
+      document.addEventListener('click', (e) => {
+        if (!this.content.contains(e.target)) {
+          this.deselectImage();
+        }
+      });
     }
 
     showDropOverlay() {
@@ -468,6 +566,13 @@
             const img = document.createElement('img');
             img.src = result.url;
             img.alt = file.name;
+
+            // Store original dimensions when image loads
+            img.onload = () => {
+              img.dataset.originalWidth = img.naturalWidth;
+              img.dataset.originalHeight = img.naturalHeight;
+            };
+
             placeholder.replaceWith(img);
           } else {
             const link = document.createElement('a');
@@ -636,6 +741,152 @@
     focus() {
       this.content.focus();
     }
+
+    selectImage(img) {
+      if (this.state.readOnly) return;
+
+      // Deselect previous image
+      this.deselectImage();
+
+      this.state.selectedImage = img;
+      img.classList.add('selected');
+
+      // Wrap image in resize container if not already wrapped
+      if (!img.parentElement.classList.contains('rich-editor-resize-wrapper')) {
+        const wrapper = document.createElement('div');
+        wrapper.className = 'rich-editor-resize-wrapper';
+        wrapper.contentEditable = false;
+
+        // Store original dimensions if not set
+        if (!img.dataset.originalWidth) {
+          img.dataset.originalWidth = img.naturalWidth;
+          img.dataset.originalHeight = img.naturalHeight;
+        }
+
+        img.parentNode.insertBefore(wrapper, img);
+        wrapper.appendChild(img);
+      }
+
+      const wrapper = img.parentElement;
+
+      // Add resize handles
+      const corners = ['nw', 'ne', 'sw', 'se'];
+      corners.forEach(corner => {
+        const handle = document.createElement('div');
+        handle.className = `rich-editor-resize-handle ${corner}`;
+        handle.dataset.corner = corner;
+
+        handle.addEventListener('mousedown', (e) => {
+          e.preventDefault();
+          e.stopPropagation();
+          this.startResize(e, img, corner);
+        });
+
+        wrapper.appendChild(handle);
+      });
+
+      // Add size input
+      const sizeInput = document.createElement('div');
+      sizeInput.className = 'rich-editor-size-input';
+      sizeInput.innerHTML = `
+        <label>Width:</label>
+        <input type="number" class="width-input" value="${img.width}" min="50" max="2000">
+        <label>px</label>
+      `;
+
+      const widthInput = sizeInput.querySelector('.width-input');
+      widthInput.addEventListener('input', (e) => {
+        const newWidth = parseInt(e.target.value);
+        if (newWidth && newWidth > 0) {
+          this.resizeImage(img, newWidth);
+        }
+      });
+
+      wrapper.appendChild(sizeInput);
+    }
+
+    deselectImage() {
+      if (!this.state.selectedImage) return;
+
+      const img = this.state.selectedImage;
+      img.classList.remove('selected');
+
+      const wrapper = img.parentElement;
+      if (wrapper && wrapper.classList.contains('rich-editor-resize-wrapper')) {
+        // Remove resize handles and size input
+        wrapper.querySelectorAll('.rich-editor-resize-handle, .rich-editor-size-input').forEach(el => el.remove());
+
+        // Unwrap image
+        wrapper.parentNode.insertBefore(img, wrapper);
+        wrapper.remove();
+      }
+
+      this.state.selectedImage = null;
+    }
+
+    startResize(e, img, corner) {
+      this.state.resizing = true;
+
+      const startX = e.clientX;
+      const startY = e.clientY;
+      const startWidth = img.width;
+      const startHeight = img.height;
+      const aspectRatio = startWidth / startHeight;
+
+      const onMouseMove = (e) => {
+        if (!this.state.resizing) return;
+
+        let deltaX = e.clientX - startX;
+        let deltaY = e.clientY - startY;
+
+        // Adjust delta based on corner
+        if (corner === 'nw' || corner === 'sw') {
+          deltaX = -deltaX;
+        }
+        if (corner === 'nw' || corner === 'ne') {
+          deltaY = -deltaY;
+        }
+
+        // Use the larger delta to maintain aspect ratio
+        const delta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY;
+        const newWidth = Math.max(50, startWidth + delta);
+
+        this.resizeImage(img, newWidth, aspectRatio);
+      };
+
+      const onMouseUp = () => {
+        this.state.resizing = false;
+        document.removeEventListener('mousemove', onMouseMove);
+        document.removeEventListener('mouseup', onMouseUp);
+
+        // Update width input
+        const wrapper = img.parentElement;
+        const widthInput = wrapper.querySelector('.width-input');
+        if (widthInput) {
+          widthInput.value = img.width;
+        }
+
+        if (this.debouncedSave) this.debouncedSave();
+      };
+
+      document.addEventListener('mousemove', onMouseMove);
+      document.addEventListener('mouseup', onMouseUp);
+    }
+
+    resizeImage(img, newWidth, aspectRatio = null) {
+      if (!aspectRatio) {
+        const originalWidth = parseInt(img.dataset.originalWidth) || img.naturalWidth;
+        const originalHeight = parseInt(img.dataset.originalHeight) || img.naturalHeight;
+        aspectRatio = originalWidth / originalHeight;
+      }
+
+      const newHeight = newWidth / aspectRatio;
+
+      img.width = newWidth;
+      img.height = newHeight;
+      img.style.width = newWidth + 'px';
+      img.style.height = newHeight + 'px';
+    }
   }
 
   return {
--- a/s3/s3_uploader.c	Sun Feb 15 11:07:52 2026 -0800
+++ b/s3/s3_uploader.c	Sun Feb 15 11:41:53 2026 -0800
@@ -153,24 +153,20 @@
   }
 
   // Build canonical headers (must be sorted alphabetically)
-  char content_length_str[32];
-  snprintf(content_length_str, sizeof(content_length_str), "%zu", data_length);
-
-  size_t headers_len = 512 + strlen(host) + strlen(content_type) + strlen(content_length_str);
+  // Note: Content-Length is NOT signed for S3 PUT requests (it's sent but not in signature)
+  size_t headers_len = 512 + strlen(host) + strlen(content_type);
   char *canonical_headers = Dowa_Arena_Allocate(p_arena, headers_len);
   snprintf(canonical_headers, headers_len,
-           "content-length:%s\n"
            "content-type:%s\n"
            "host:%s\n"
            "x-amz-content-sha256:%s\n"
            "x-amz-date:%s\n",
-           content_length_str,
            content_type,
            host,
            payload_hash,
            ts.datetime);
 
-  const char *signed_headers = "content-length;content-type;host;x-amz-content-sha256;x-amz-date";
+  const char *signed_headers = "content-type;host;x-amz-content-sha256;x-amz-date";
 
   // Build canonical request
   char *canonical_request = s3__build_canonical_request("PUT",
@@ -213,11 +209,18 @@
   Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(url);
   Seobeo_Client_Request_Set_Method(p_req, "PUT");
   Seobeo_Client_Request_Set_Body(p_req, (const char *)data, data_length);
+  Seobeo_Client_Request_Add_Header_Map(p_req, "Connection", "keep-alive");
   Seobeo_Client_Request_Add_Header_Map(p_req, "Content-Type", content_type);
   Seobeo_Client_Request_Add_Header_Map(p_req, "x-amz-date", ts.datetime);
   Seobeo_Client_Request_Add_Header_Map(p_req, "x-amz-content-sha256", payload_hash);
   Seobeo_Client_Request_Add_Header_Map(p_req, "Authorization", auth_header);
 
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] Uploading %zu bytes to %s\n", data_length, url);
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] Content-Type: %s\n", content_type);
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] x-amz-date: %s\n", ts.datetime);
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] x-amz-content-sha256: %s\n", payload_hash);
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] Authorization: %.80s...\n", auth_header);
+
   Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
 
   if (p_resp)
--- a/seobeo/s_network.c	Sun Feb 15 11:07:52 2026 -0800
+++ b/seobeo/s_network.c	Sun Feb 15 11:41:53 2026 -0800
@@ -268,9 +268,18 @@
     uint32 offset = 0;
     while (offset < data_size)
     {
-      ssize_t n = write(p_handle->socket,
-                data + offset,
-                data_size - offset);
+      ssize_t n;
+      if (p_handle->ssl)
+      {
+        n = Seobeo_SSL_Write(p_handle, data + offset, data_size - offset);
+      }
+      else
+      {
+        n = write(p_handle->socket,
+                  data + offset,
+                  data_size - offset);
+      }
+
       if (n==0)
       {
         // DEBUG