diff options
| author | grm <grm@eyesin.space> | 2026-09-14 23:51:55 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-14 23:51:55 +0300 |
| commit | 778cb72c8a0902bd0b8159ebd3bb7eff93f28c83 (patch) | |
| tree | de21228c247e735591e88acbbf3bf4c83f2142e0 /internal/web/templates/partials/editor.html | |
| parent | 5f9fc2a8667a9438b6336d75026f9a455fc9c4ce (diff) | |
| download | blogspace-778cb72c8a0902bd0b8159ebd3bb7eff93f28c83.tar.gz blogspace-778cb72c8a0902bd0b8159ebd3bb7eff93f28c83.tar.bz2 blogspace-778cb72c8a0902bd0b8159ebd3bb7eff93f28c83.zip | |
Turn the image library into a file library, with a per-blog upload limit
Bloggers want to attach PDFs, archives, audio and other files to posts,
not only images. The Images tab becomes Files: any type is accepted,
listed by kind with search, paging, rename and multi-file upload, and the
editor's paste/drop/"Insert file" takes anything (images are shown,
everything else becomes a link). The default limit goes from 5 to 10 MB
and the superadmin can override it per blog from /admin/.
Files stay in Postgres so one pg_dump is still the whole blog. The bytea
column is STORAGE EXTERNAL and /media streams it in substring() slices,
so serving never holds a whole file in memory whatever limit a blog gets.
Serving any type on the root domain, which carries the session cookie,
needs a policy: uploads are typed by sniffing (the extension may only
refine a generic sniff to an allowlisted type) and only images, PDF,
plain text, audio and video render inline; HTML, SVG, XML, scripts,
archives and binaries always go out as application/octet-stream with
Content-Disposition: attachment.
The body cap moves out of requireAuth into guardPOST, which runs after
withBlog has resolved the blog and so knows its limit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/templates/partials/editor.html')
| -rw-r--r-- | internal/web/templates/partials/editor.html | 18 |
1 files changed, 11 insertions, 7 deletions
diff --git a/internal/web/templates/partials/editor.html b/internal/web/templates/partials/editor.html index eb2e315..4371624 100644 --- a/internal/web/templates/partials/editor.html +++ b/internal/web/templates/partials/editor.html @@ -1,9 +1,9 @@ {{define "editor"}}<textarea name="{{.name}}" id="ed-{{.name}}" rows="{{.rows}}"{{if .tall}} class="editor"{{end}}>{{.value}}</textarea> <div class="editor-tools"> - <label class="upload">{{t "Insert image"}} <input type="file" name="inline_image" id="ed-{{.name}}-file" accept="image/*"></label> + <label class="upload">{{t "Insert file"}} <input type="file" name="inline_file" id="ed-{{.name}}-file"></label> <span class="upload-status" id="ed-{{.name}}-status"></span> - <span class="muted small">{{t "…or paste / drop an image into the text."}}</span> - <noscript><span class="muted small">{{t "The image is added at the end of the text when you save."}}</span></noscript> + <span class="muted small">{{t "…or paste / drop a file into the text. Images are shown, other files linked."}}</span> + <noscript><span class="muted small">{{t "The file is added at the end of the text when you save."}}</span></noscript> </div> {{template "mdhelp"}} <script> @@ -22,23 +22,27 @@ var fd = new FormData(); fd.append("_csrf", {{.csrf}}); fd.append("file", file); - fetch({{.upload}}, { method: "POST", body: fd, credentials: "same-origin", headers: { Accept: "application/json" } }) + return fetch({{.upload}}, { method: "POST", body: fd, credentials: "same-origin", headers: { Accept: "application/json" } }) .then(function (r) { return r.json().then(function (j) { if (!r.ok || j.error) throw new Error(j.error || {{t "upload failed"}}); return j; }); }) .then(function (j) { insert(j.markdown); st.textContent = {{t "Inserted"}} + " " + j.filename + "."; inp.value = ""; }) .catch(function (err) { st.textContent = {{t "Could not upload:"}} + " " + err.message; }); } + function uploadAll(files) { // one after the other, so each lands where the caret is + var i = 0; + (function next() { if (i < files.length) upload(files[i++]).then(next); })(); + } inp.addEventListener("change", function () { if (inp.files && inp.files[0]) upload(inp.files[0]); }); ta.addEventListener("paste", function (ev) { var items = ev.clipboardData && ev.clipboardData.items; if (!items) return; for (var i = 0; i < items.length; i++) { - if (items[i].kind === "file" && /^image\//.test(items[i].type)) { ev.preventDefault(); upload(items[i].getAsFile()); return; } + if (items[i].kind === "file") { ev.preventDefault(); upload(items[i].getAsFile()); return; } } }); ta.addEventListener("dragover", function (ev) { ev.preventDefault(); }); ta.addEventListener("drop", function (ev) { - var f = ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files[0]; - if (f && /^image\//.test(f.type)) { ev.preventDefault(); upload(f); } + var fs = ev.dataTransfer && ev.dataTransfer.files; + if (fs && fs.length) { ev.preventDefault(); uploadAll(Array.prototype.slice.call(fs)); } }); })(); </script>{{end}} |
