From 778cb72c8a0902bd0b8159ebd3bb7eff93f28c83 Mon Sep 17 00:00:00 2001 From: grm Date: Mon, 14 Sep 2026 23:51:55 +0300 Subject: 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 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/web/static/dashboard.css | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) (limited to 'internal/web/static/dashboard.css') diff --git a/internal/web/static/dashboard.css b/internal/web/static/dashboard.css index 5c766e7..8fe5f8a 100644 --- a/internal/web/static/dashboard.css +++ b/internal/web/static/dashboard.css @@ -118,12 +118,23 @@ details.help[open] summary { margin-bottom: 0.5em; } .editor-tools .upload input { display: inline-block; width: auto; max-width: 14em; margin: 0 0 0 0.4em; padding: 0; border: 0; background: none; font-size: 0.9em; } .editor-tools .upload-status { font-family: "IBM Plex Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.85em; margin-right: 0.6em; } -/* ---- images -------------------------------------------------------------- */ -.gallery { overflow: hidden; } -.thumb { float: left; width: 200px; margin: 0 1.2em 1.6em 0; padding: 0.6em; text-align: center; } -.thumb img { max-width: 100%; max-height: 140px; border: 1px solid #ddd6c7; } -.thumb .meta { font-size: 0.85em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 0.4em; } -.thumb .copy { font-size: 0.75em; margin: 0.4em 0; padding: 0.3em; font-family: "IBM Plex Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; } +/* ---- files --------------------------------------------------------------- */ +.dropzone.over { border-style: dashed; background: #ebe5d6; } +.progress { list-style: none; margin: 0.6em 0 0; padding: 0; font-family: "IBM Plex Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.85em; } +.progress:empty { display: none; } +.files-filter { overflow: hidden; } +.files-filter input[type=search] { width: auto; margin: 0 0.3em 0.5em 0; padding: 0.3em 0.5em; vertical-align: middle; } +.kinds { display: inline-block; margin: 0 1em 0.5em 0; vertical-align: middle; } +.kinds a { display: inline-block; padding: 0.2em 0.7em; margin: 0 0.3em 0.3em 0; border: 2px solid #1d1a17; color: #1d1a17; text-decoration: none; font-family: "IBM Plex Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.05em; } +.kinds a:hover { background: #ebe5d6; } +.kinds a.active { background: #1d1a17; color: #f4efe4; } +table.files td.icon { width: 56px; } +table.files .thumb { display: block; width: 48px; height: 48px; object-fit: contain; border: 1px solid #ddd6c7; background: #f4efe4; } +table.files .badge { display: block; width: 48px; padding: 0.9em 0; border: 1px solid #1d1a17; background: #fffdf8; text-align: center; font-family: "IBM Plex Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.65em; letter-spacing: 0.05em; overflow: hidden; } +table.files td.name { word-break: break-all; } +table.files .copy { width: 100%; min-width: 11em; padding: 0.3em; font-family: "IBM Plex Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.75em; } +.pager { margin: 0 4px 1.6em 0; text-align: center; font-family: "IBM Plex Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.9em; } +.pager a, .pager span { margin: 0 0.8em; } .imagepick { margin-top: 0.5em; padding-top: 0.5em; border-top: 2px dashed #ddd6c7; } .imagepick > label { margin-bottom: 0.3em; } .imagepick .picks { overflow: hidden; margin: 0.4em 0 0; } @@ -184,7 +195,7 @@ details.reset { margin-top: 1.6em; } .login-brand { text-align: center; font-family: "IBM Plex Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; text-transform: uppercase; letter-spacing: 0.2em; margin: 2em 0 1em; } @media (max-width: 700px) { - .cols .card, .row label, .thumb { float: none; width: auto; margin-right: 4px; } + .cols .card, .row label { float: none; width: auto; margin-right: 4px; } .imagepick .pick { width: 46%; margin-right: 4%; } .presets .preset { width: 46%; margin-right: 4%; } .addrow .add { float: none; width: auto; margin-right: 0; } -- cgit v1.2.3