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/web_test.go | |
| 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/web_test.go')
| -rw-r--r-- | internal/web/web_test.go | 28 |
1 files changed, 18 insertions, 10 deletions
diff --git a/internal/web/web_test.go b/internal/web/web_test.go index d7de43a..e2bd335 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -63,7 +63,7 @@ func TestSubdomainLength(t *testing.T) { func TestRootRoutePrecedence(t *testing.T) { cfg := &config.Config{BaseDomain: "example.com", JWTSecret: []byte("x"), MaxUploadBytes: 1 << 20} s := NewServer(cfg, nil) - for path, want := range map[string]int{"/webadmin": 200, "/healthz": 200, "/dashboard": 303, "/admin/": 303, "/b/alice/posts": 303, "/b/alice/announcements": 303, "/b/alice/layout": 303, "/b/alice/media/6ba7b810-9dad-11d1-80b4-00c04fd430c8": 303, "/account/password": 303} { + for path, want := range map[string]int{"/webadmin": 200, "/healthz": 200, "/dashboard": 303, "/admin/": 303, "/b/alice/posts": 303, "/b/alice/announcements": 303, "/b/alice/layout": 303, "/b/alice/media/6ba7b810-9dad-11d1-80b4-00c04fd430c8": 303, "/b/alice/files": 303, "/account/password": 303} { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", path, nil) req.Host = "example.com" @@ -320,7 +320,7 @@ func TestPresets(t *testing.T) { func TestAllTemplatesParse(t *testing.T) { tpl := newTemplates(false) for _, name := range []string{"auth/login.html", "dashboard/overview.html", "dashboard/pages.html", "dashboard/page_form.html", - "dashboard/posts.html", "dashboard/post_form.html", "dashboard/design.html", "dashboard/images.html", "dashboard/settings.html", + "dashboard/posts.html", "dashboard/post_form.html", "dashboard/design.html", "dashboard/files.html", "dashboard/settings.html", "dashboard/password.html", "dashboard/confirm.html", "dashboard/sections.html", "dashboard/section_form.html", "dashboard/layout.html", "dashboard/module_form.html", "admin/index.html", "admin/new_user.html", "admin/delete_user.html", "blog/page.html", "blog/post.html", "blog/404.html"} { for _, l := range i18n.Languages() { @@ -365,7 +365,7 @@ func TestGreekCatalogComplete(t *testing.T) { add(goKey, string(b)) } // Keys that reach t/tr through a variable rather than a literal. - for _, m := range []map[string]string{moduleNames, areaNames} { + for _, m := range []map[string]string{moduleNames, areaNames, fileKindNames} { for _, v := range m { used[v] = true } @@ -422,17 +422,25 @@ func TestSubdomainWebadminRedirect(t *testing.T) { } } -func TestAppendImageMD(t *testing.T) { - img := &store.Image{ID: uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), Filename: "cat].png"} +func TestFileMarkdown(t *testing.T) { + id := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + img := &store.File{ID: id, Filename: "cat].png", Kind: "image"} + doc := &store.File{ID: id, Filename: "notes\nv2.pdf", Kind: "document"} line := "" - if got := appendImageMD("", img); got != line+"\n" { + if got := fileMarkdown(img); got != line { + t.Errorf("image: %q", got) + } + if got := fileMarkdown(doc); got != "[notes v2.pdf](/media/6ba7b810-9dad-11d1-80b4-00c04fd430c8)" { + t.Errorf("document should be a link: %q", got) + } + if got := appendFileMD("", img); got != line+"\n" { t.Errorf("empty body: %q", got) } - if got := appendImageMD("hello\n", img); got != "hello\n\n"+line+"\n" { - t.Errorf("with body: %q", got) + if got := appendFileMD("hello\n", img); got != "hello\n\n"+line+"\n" { + t.Errorf("appended: %q", got) } - if got := appendImageMD("hello", nil); got != "hello" { - t.Errorf("nil image should not change the body: %q", got) + if got := appendFileMD("hello", nil); got != "hello" { + t.Errorf("nil file should not change the body: %q", got) } } |
