From 4355fa78faa9945d0187f0668b4126f6876964ac Mon Sep 17 00:00:00 2001 From: Paul Spenke Date: Wed, 18 Mar 2026 19:33:09 +0100 Subject: [PATCH] update attachent upload --- CLAUDE.md | 7 ++-- src/popup.js | 103 ++++++++++++++++++++++++--------------------------- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 941f381..c294af4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Load the extension in Chrome: **Extensions → Load unpacked → select `dist/`* - Memos API v1: `/api/v1/memos`, `/api/v1/attachments` - Requires Memos v0.22+ - Bearer token auth via `chrome.storage.sync` -- Attachment flow: upload via `POST /api/v1/attachments` (JSON + base64 `content`), create memo, then link each attachment to the memo via `PATCH /api/v1/attachments/{id}` with `{ memo: "memos/{id}" }` +- Attachment flow: create memo first (`POST /api/v1/memos`), then upload each attachment via `POST /api/v1/attachments` (JSON + base64 `content` + `memo: "memos/{id}"`), then patch the memo content to replace original image URLs with attachment URLs (`PATCH /api/v1/memos/{id}`) ### Content Extraction - Removes boilerplate: nav, ads, sidebars, cookie banners (45+ selectors) @@ -49,8 +49,9 @@ Load the extension in Chrome: **Extensions → Load unpacked → select `dist/`* - Filters images smaller than 32px (icons/tracking pixels) - Deduplicates images - Supports data URIs -- Uploads images as attachments (`POST /api/v1/attachments`) with base64-encoded content -- After memo creation, links each attachment to the memo via `PATCH /api/v1/attachments/{id}` +- Uploads images as attachments (`POST /api/v1/attachments`) with base64-encoded content and `memo` reference +- Memo is created first; attachment uploads include `memo: "memos/{id}"` to associate them immediately +- After all uploads, memo content is patched to replace original image URLs with attachment file URLs - Attachment file URL pattern: `{memosUrl}/file/attachments/{id}` ### Storage diff --git a/src/popup.js b/src/popup.js index 794f286..a08708b 100644 --- a/src/popup.js +++ b/src/popup.js @@ -295,55 +295,16 @@ sendBtn.addEventListener("click", async () => { sendBtn.disabled = true; try { - // 1. Upload images as attachments if requested (without memo link yet) - const imageMap = new Map(); // originalUrl -> attachment name ("attachments/{id}") - if (attachCheck.checked) { - const toUpload = state.images.filter((img) => img.keep); - let uploaded = 0; - for (const img of toUpload) { - sendBtn.textContent = `Uploading image ${uploaded + 1}/${toUpload.length}…`; - try { - const attachment = await uploadAttachment(baseUrl, token, img); - imageMap.set(img.src, attachment.name); - uploaded++; - } catch (e) { - console.warn("Failed to upload image:", img.src, e.message); - } - } - } - + // 1. Create the memo first (with original image URLs) sendBtn.textContent = "Creating memo…"; - // 2. Replace original image URLs in markdown with attachment external links - let contentWithImages = finalContent; - for (const [origUrl, attachName] of imageMap.entries()) { - const attachmentUrl = `${baseUrl}/file/${attachName}`; - const escapedUrl = origUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const re = new RegExp(`!\\[(.*?)\\]\\(${escapedUrl}\\)`, 'g'); - contentWithImages = contentWithImages.replace(re, `![$1](${attachmentUrl})`); - } - - // Append any uploaded attachments not already referenced in the markdown - for (const attachName of imageMap.values()) { - const attachmentUrl = `${baseUrl}/file/${attachName}`; - if (!contentWithImages.includes(attachmentUrl)) { - contentWithImages += `\n\n![attachment](${attachmentUrl})`; - } - } - - // 3. Create the memo - const body = { - content: contentWithImages, - visibility, - }; - const res = await fetch(`${baseUrl}/api/v1/memos`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify(body), + body: JSON.stringify({ content: finalContent, visibility }), }); if (!res.ok) { @@ -355,22 +316,53 @@ sendBtn.addEventListener("click", async () => { // memo.name is "memos/{id}" const memoName = memo.name; - // 4. Link each attachment to the created memo via PATCH - for (const attachName of imageMap.values()) { - try { - await fetch(`${baseUrl}/api/v1/${attachName}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ memo: memoName }), - }); - } catch (e) { - console.warn("Failed to link attachment to memo:", attachName, e.message); + // 2. Upload images as attachments referencing the created memo + const imageMap = new Map(); // originalUrl -> attachment name ("attachments/{id}") + if (attachCheck.checked) { + const toUpload = state.images.filter((img) => img.keep); + let uploaded = 0; + for (const img of toUpload) { + sendBtn.textContent = `Uploading image ${uploaded + 1}/${toUpload.length}…`; + try { + const attachment = await uploadAttachment(baseUrl, token, img, memoName); + imageMap.set(img.src, attachment.name); + uploaded++; + } catch (e) { + console.warn("Failed to upload image:", img.src, e.message); + } } } + // 3. If images were uploaded, update the memo content to reference attachment URLs + if (imageMap.size > 0) { + sendBtn.textContent = "Updating memo…"; + + let contentWithImages = finalContent; + for (const [origUrl, attachName] of imageMap.entries()) { + const attachmentUrl = `${baseUrl}/file/${attachName}`; + const escapedUrl = origUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const re = new RegExp(`!\\[(.*?)\\]\\(${escapedUrl}\\)`, 'g'); + contentWithImages = contentWithImages.replace(re, `![$1](${attachmentUrl})`); + } + + // Append any uploaded attachments not already referenced in the markdown + for (const attachName of imageMap.values()) { + const attachmentUrl = `${baseUrl}/file/${attachName}`; + if (!contentWithImages.includes(attachmentUrl)) { + contentWithImages += `\n\n![attachment](${attachmentUrl})`; + } + } + + await fetch(`${baseUrl}/api/v1/${memoName}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ content: contentWithImages }), + }); + } + let memoId = ""; if (memoName) { memoId = memoName.replace(/^memos\//, ""); @@ -392,7 +384,7 @@ sendBtn.addEventListener("click", async () => { }); // ── Upload a single image as an attachment ──────────────────────────────────── -async function uploadAttachment(baseUrl, token, img) { +async function uploadAttachment(baseUrl, token, img, memoName) { // Validate URL scheme to prevent SSRF via crafted page image URLs if (!img.src.startsWith("data:")) { let parsedUrl; @@ -444,6 +436,7 @@ async function uploadAttachment(baseUrl, token, img) { filename, type: blob.type || "application/octet-stream", content: base64, + memo: memoName, }), });