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 /README.md | |
| 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 'README.md')
| -rw-r--r-- | README.md | 20 |
1 files changed, 12 insertions, 8 deletions
@@ -4,9 +4,12 @@ A small multi-tenant blog host. One Go binary + Postgres. Bloggers log in at `example.com` to manage their blog; each blog is served at `<name>.example.com`. Server-rendered HTML, no JavaScript required, works on old browsers and phones. -- Posts and page intros are written in **Markdown** (sanitized on save). Images can be - inserted straight from the editor (file picker, paste or drag-and-drop; with JavaScript - off the file is appended on save). +- Posts and page intros are written in **Markdown** (sanitized on save). Files of any + kind — images, PDFs, archives, audio… — can be inserted straight from the editor (file + picker, paste or drag-and-drop; with JavaScript off the file is appended on save): + images are shown, everything else becomes a download link. The **Files** tab lists + them by kind with search, rename and delete. Uploads are 10 MB per file by default; + the superadmin can set a different limit per blog. - **Announcements**: blog-wide notices (next meeting, this month's book, a closure) shown at the top or bottom of the main content or of a side column; each can be hidden without deleting. - Every blog has **pages** (Home, About, News, …); each page holds posts. @@ -22,7 +25,7 @@ Server-rendered HTML, no JavaScript required, works on old browsers and phones. the 404 page. What the blogger writes is never translated. - A **superadmin** creates bloggers, resets passwords, disables or deletes accounts. - The **root domain is itself a blog**, owned by the superadmin and managed like any other. -- **Each blog is its own Postgres database** (`blog_<name>`), images included, so one +- **Each blog is its own Postgres database** (`blog_<name>`), uploaded files included, so one `pg_dump` is a complete backup of a blog and one `psql` restores it. A small control database holds the users and the list of blogs. @@ -64,7 +67,7 @@ Go changes need a restart. | `DATABASE_URL` | local dev DSN | Connection string of the **control** database; blog databases are created next to it by the same role | | `JWT_SECRET` | — | **Required** outside dev; long random string (`openssl rand -hex 32`) | | `SUPERADMIN_USERNAME` / `SUPERADMIN_PASSWORD` | `admin` / — | Created on first start if no superadmin exists | -| `MAX_UPLOAD_MB` | `5` | Image upload limit | +| `MAX_UPLOAD_MB` | `10` | Per-file upload limit; the superadmin can override it per blog in `/admin/` | | `DEV` | `false` | Hot-reload templates, allow missing secrets | Migrations run automatically at startup, for the control database and for every blog database. @@ -87,7 +90,7 @@ nginx example: server { listen 443 ssl; server_name example.com *.example.com; # wildcard certificate - client_max_body_size 8m; # >= MAX_UPLOAD_MB + client_max_body_size 101m; # the Files page sends up to 10 files per request: >= 10 x the largest blog limit + 1 MB location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; @@ -104,7 +107,7 @@ certificates need the DNS challenge). Every blog lives in its own database, `blog_<name>` (dashes become underscores: `my-blog` → `blog_my_blog`). The control database (`blogspace`) holds the users -and the blog registry. Images are in the blog database, so one dump is the whole blog. +and the blog registry. Uploaded files are in the blog database, so one dump is the whole blog. ```sh # one blog @@ -136,7 +139,8 @@ the release that contains the split once (it moves each blog into its own database at start-up), then clear the old migration history so later migrations apply: `docker compose exec db psql -U blogspace -c "DELETE FROM goose_db_version WHERE version_id > 1"`. Current releases no longer carry the -split code. Blog pools are small (4 connections each, closed when +split code, and the first control migration after the split (the per-blog +upload limit) is refused as "missing" until that history is cleared. Blog pools are small (4 connections each, closed when idle); with many blogs busy at once, raise `max_connections` on the `db` service. ## Layout |
