<feed xmlns='http://www.w3.org/2005/Atom'>
<title>blogspace/internal/store/files.go, branch master</title>
<subtitle>blogspace</subtitle>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/'/>
<entry>
<title>Revert "Special pages: a gallery page of the blog's images"</title>
<updated>2026-09-18T11:36:17+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T11:35:59+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=7ac90f8e99dbf763b0fe04a17d7eef19dd22cae8'/>
<id>7ac90f8e99dbf763b0fe04a17d7eef19dd22cae8</id>
<content type='text'>
This reverts commit 0fc470a. The feature was not wanted. Migration
00013 stays on disk because databases have already run it (goose refuses
a missing applied migration); 00014 drops the column and table it added.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
This reverts commit 0fc470a. The feature was not wanted. Migration
00013 stays on disk because databases have already run it (goose refuses
a missing applied migration); 00014 drops the column and table it added.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Special pages: a gallery page of the blog's images</title>
<updated>2026-09-18T11:32:22+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T11:32:22+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=0fc470a8d588cac1a51cc4b3e1a2237dc56041c9'/>
<id>0fc470a8d588cac1a51cc4b3e1a2237dc56041c9</id>
<content type='text'>
A page's kind says what it shows between its intro and outro: posts,
as before, or something special. The first special kind is a gallery —
every image in the files library as a masonry wall (CSS columns, no
script), newest first, each linked to its full-size file. The page form
lists the library's images with a Hide tick per picture; exclusions are
stored (page_hidden_files) rather than inclusions so a new upload shows
up without editing the page, and the design's own pictures — logo,
favicon, header and background image — are never shown. Special pages
keep the page chrome (menu, announcements, home, ordering) but hold no
posts: the post forms skip them and a forced page id is refused.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
A page's kind says what it shows between its intro and outro: posts,
as before, or something special. The first special kind is a gallery —
every image in the files library as a masonry wall (CSS columns, no
script), newest first, each linked to its full-size file. The page form
lists the library's images with a Hide tick per picture; exclusions are
stored (page_hidden_files) rather than inclusions so a new upload shows
up without editing the page, and the design's own pictures — logo,
favicon, header and background image — are never shown. Special pages
keep the page chrome (menu, announcements, home, ordering) but hold no
posts: the post forms skip them and a forced page id is refused.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Turn the image library into a file library, with a per-blog upload limit</title>
<updated>2026-09-14T20:51:55+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-14T20:51:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=778cb72c8a0902bd0b8159ebd3bb7eff93f28c83'/>
<id>778cb72c8a0902bd0b8159ebd3bb7eff93f28c83</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
</feed>
