update attachent upload
This commit is contained in:
@@ -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
|
||||||
|
|||||||
103
src/popup.js
103
src/popup.js
@@ -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, ``);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, ``);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user