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/templates/dashboard/files.html | 95 ++++++++++++++++++++++ internal/web/templates/dashboard/images.html | 24 ------ internal/web/templates/dashboard/overview.html | 2 +- internal/web/templates/dashboard/page_form.html | 2 +- internal/web/templates/dashboard/post_form.html | 2 +- internal/web/templates/dashboard/section_form.html | 2 +- 6 files changed, 99 insertions(+), 28 deletions(-) create mode 100644 internal/web/templates/dashboard/files.html delete mode 100644 internal/web/templates/dashboard/images.html (limited to 'internal/web/templates/dashboard') diff --git a/internal/web/templates/dashboard/files.html b/internal/web/templates/dashboard/files.html new file mode 100644 index 0000000..b42cec2 --- /dev/null +++ b/internal/web/templates/dashboard/files.html @@ -0,0 +1,95 @@ +{{define "title"}}{{t "Files"}} · {{.Blog.Title}}{{end}} +{{define "content"}} +{{$b := .Blog.Subdomain}} +
+

{{t "Files"}}

+

{{t "Files are stored with your blog. Put one in a post with"}} {{t "Insert file"}} {{t "in the editor, or copy its line from the table."}} {{tf "%d files, %s in use." .Data.count (size .Data.bytes)}}

+
+
+
+ + +

+
    +
    +
    +
    + + {{t "All"}} + {{range .Data.kinds}}{{t (index $.Data.kindNames .)}}{{end}} + + {{if .Data.kind}}{{end}} + +
    +
    +{{if .Data.files}} + + + {{range .Data.files}} + + + + + + + + {{end}} +
    {{t "Name"}}{{t "Kind"}}{{t "Size"}}{{t "Date"}}Markdown
    {{if eq .Kind "image"}}{{else}}{{.Badge}}{{end}}{{.Filename}} +
    {{t "rename"}} +
    + + + +
    {{t (index $.Data.kindNames .Kind)}}{{size .Size}}{{date .CreatedAt}}{{t "download"}} · +
    +{{else if or .Data.q .Data.kind}}

    {{t "No files match."}}

    +{{else}}

    {{t "No files yet."}}

    {{end}} +
    +{{if gt .Data.lastPage 1}} +
    + {{if gt .Data.pageNum 1}}← {{t "Newer"}}{{end}} + {{tf "Page %d of %d" .Data.pageNum .Data.lastPage}} + {{if lt .Data.pageNum .Data.lastPage}}{{t "Older"}} →{{end}} +
    +{{end}} + +{{end}} diff --git a/internal/web/templates/dashboard/images.html b/internal/web/templates/dashboard/images.html deleted file mode 100644 index 8b9faa3..0000000 --- a/internal/web/templates/dashboard/images.html +++ /dev/null @@ -1,24 +0,0 @@ -{{define "title"}}{{t "Images"}} · {{.Blog.Title}}{{end}} -{{define "content"}} -
    -

    {{t "Images"}}

    -

    {{t "Images are stored with your blog. Put one in a post with"}} {{t "Insert image"}} {{t "in the editor, or copy its line from below."}}

    -
    -
    -
    - - -

    -
    -
    - -{{end}} diff --git a/internal/web/templates/dashboard/overview.html b/internal/web/templates/dashboard/overview.html index e021495..0f611cd 100644 --- a/internal/web/templates/dashboard/overview.html +++ b/internal/web/templates/dashboard/overview.html @@ -7,7 +7,7 @@

    {{t "Pages"}}

    diff --git a/internal/web/templates/dashboard/page_form.html b/internal/web/templates/dashboard/page_form.html index 08c4a43..18d1119 100644 --- a/internal/web/templates/dashboard/page_form.html +++ b/internal/web/templates/dashboard/page_form.html @@ -8,7 +8,7 @@ - {{template "editor" (dict "name" "intro" "value" .Data.page.IntroMD "rows" 8 "tall" false "upload" (print "/b/" .Blog.Subdomain "/images/upload") "csrf" .CSRF)}} + {{template "editor" (dict "name" "intro" "value" .Data.page.IntroMD "rows" 8 "tall" false "upload" (print "/b/" .Blog.Subdomain "/files/upload") "csrf" .CSRF)}}

    diff --git a/internal/web/templates/dashboard/post_form.html b/internal/web/templates/dashboard/post_form.html index 8069a9a..d12d15f 100644 --- a/internal/web/templates/dashboard/post_form.html +++ b/internal/web/templates/dashboard/post_form.html @@ -15,7 +15,7 @@

    - {{template "editor" (dict "name" "body" "value" .Data.post.BodyMD "rows" 24 "tall" true "upload" (print "/b/" .Blog.Subdomain "/images/upload") "csrf" .CSRF)}} + {{template "editor" (dict "name" "body" "value" .Data.post.BodyMD "rows" 24 "tall" true "upload" (print "/b/" .Blog.Subdomain "/files/upload") "csrf" .CSRF)}}

    diff --git a/internal/web/templates/dashboard/section_form.html b/internal/web/templates/dashboard/section_form.html index bc3ba34..b401d9c 100644 --- a/internal/web/templates/dashboard/section_form.html +++ b/internal/web/templates/dashboard/section_form.html @@ -24,7 +24,7 @@

    {{t "“Top” of a side column means above its modules, “bottom” below them. If that column is not shown on your blog (see"}} {{t "Layout"}}) {{t "the announcement moves to the main content instead."}}

    - {{template "editor" (dict "name" "body" "value" $s.BodyMD "rows" 10 "tall" false "upload" (print "/b/" .Blog.Subdomain "/images/upload") "csrf" .CSRF)}} + {{template "editor" (dict "name" "body" "value" $s.BodyMD "rows" 10 "tall" false "upload" (print "/b/" .Blog.Subdomain "/files/upload") "csrf" .CSRF)}}

    -- cgit v1.2.3