update attachent upload

This commit is contained in:
2026-03-18 19:33:09 +01:00
parent 84b3dd69f1
commit 4355fa78fa
2 changed files with 52 additions and 58 deletions

View File

@@ -38,7 +38,7 @@ Load the extension in Chrome: **Extensions → Load unpacked → select `dist/`*
- Memos API v1: `/api/v1/memos`, `/api/v1/attachments` - Memos API v1: `/api/v1/memos`, `/api/v1/attachments`
- Requires Memos v0.22+ - Requires Memos v0.22+
- Bearer token auth via `chrome.storage.sync` - 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 ### Content Extraction
- Removes boilerplate: nav, ads, sidebars, cookie banners (45+ selectors) - 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) - Filters images smaller than 32px (icons/tracking pixels)
- Deduplicates images - Deduplicates images
- Supports data URIs - Supports data URIs
- Uploads images as attachments (`POST /api/v1/attachments`) with base64-encoded content - Uploads images as attachments (`POST /api/v1/attachments`) with base64-encoded content and `memo` reference
- After memo creation, links each attachment to the memo via `PATCH /api/v1/attachments/{id}` - 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}` - Attachment file URL pattern: `{memosUrl}/file/attachments/{id}`
### Storage ### Storage

View File

@@ -295,55 +295,16 @@ sendBtn.addEventListener("click", async () => {
sendBtn.disabled = true; sendBtn.disabled = true;
try { try {
// 1. Upload images as attachments if requested (without memo link yet) // 1. Create the memo first (with original image URLs)
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);
}
}
}
sendBtn.textContent = "Creating memo…"; 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`, { const res = await fetch(`${baseUrl}/api/v1/memos`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(body), body: JSON.stringify({ content: finalContent, visibility }),
}); });
if (!res.ok) { if (!res.ok) {
@@ -355,22 +316,53 @@ sendBtn.addEventListener("click", async () => {
// memo.name is "memos/{id}" // memo.name is "memos/{id}"
const memoName = memo.name; const memoName = memo.name;
// 4. Link each attachment to the created memo via PATCH // 2. Upload images as attachments referencing the created memo
for (const attachName of imageMap.values()) { const imageMap = new Map(); // originalUrl -> attachment name ("attachments/{id}")
try { if (attachCheck.checked) {
await fetch(`${baseUrl}/api/v1/${attachName}`, { const toUpload = state.images.filter((img) => img.keep);
method: "PATCH", let uploaded = 0;
headers: { for (const img of toUpload) {
"Content-Type": "application/json", sendBtn.textContent = `Uploading image ${uploaded + 1}/${toUpload.length}`;
Authorization: `Bearer ${token}`, try {
}, const attachment = await uploadAttachment(baseUrl, token, img, memoName);
body: JSON.stringify({ memo: memoName }), imageMap.set(img.src, attachment.name);
}); uploaded++;
} catch (e) { } catch (e) {
console.warn("Failed to link attachment to memo:", attachName, e.message); 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 = ""; let memoId = "";
if (memoName) { if (memoName) {
memoId = memoName.replace(/^memos\//, ""); memoId = memoName.replace(/^memos\//, "");
@@ -392,7 +384,7 @@ sendBtn.addEventListener("click", async () => {
}); });
// ── Upload a single image as an attachment ──────────────────────────────────── // ── 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 // Validate URL scheme to prevent SSRF via crafted page image URLs
if (!img.src.startsWith("data:")) { if (!img.src.startsWith("data:")) {
let parsedUrl; let parsedUrl;
@@ -444,6 +436,7 @@ async function uploadAttachment(baseUrl, token, img) {
filename, filename,
type: blob.type || "application/octet-stream", type: blob.type || "application/octet-stream",
content: base64, content: base64,
memo: memoName,
}), }),
}); });