aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-14 23:51:55 +0300
committergrm <grm@eyesin.space>2026-09-14 23:51:55 +0300
commit778cb72c8a0902bd0b8159ebd3bb7eff93f28c83 (patch)
treede21228c247e735591e88acbbf3bf4c83f2142e0 /README.md
parent5f9fc2a8667a9438b6336d75026f9a455fc9c4ce (diff)
downloadblogspace-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.md20
1 files changed, 12 insertions, 8 deletions
diff --git a/README.md b/README.md
index 6af352a..f0b3fd8 100644
--- a/README.md
+++ b/README.md
@@ -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