admin: publish files to the public bucket
All checks were successful
Build and Deploy to Production / test (push) Successful in 27s
Build and Deploy to Production / build (push) Successful in 1m3s
Build and Deploy to Production / deploy (push) Successful in 40s

There was no way to put a file up for public use short of running an
S3 client with the uploader key. Add a "files" area to the admin:
upload one or more files, copy the link, edit the note, replace the
contents under the same link, delete. It has its own read/write/delete
permission flags like the other areas, and every action is audited.

The objects go to the storage stack's public bucket and are served by
Garage at s3.asxp.io/public/<key>, not by this app. The app's key can
only put and delete there, so a public_files table is the record of
what was published. Keys get a random prefix: links are not guessable
and names never collide.

That host is same-site with the admin, so a file that can carry script
must not render there. Only a fixed list of types (images, PDF, plain
text, audio, video) is served inline; the rest, HTML and SVG included,
is stored as an attachment. Image metadata is stripped by default, as
for the bingo photos, because of GPS tags.

A per-file limit and a total quota keep the small disk on asxpio-ops
from filling up.

Assisted-by: Claude:fable-5-2
This commit is contained in:
Sergei Poljanski 2026-11-12 05:52:10 +04:00
commit 4c10221727
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
15 changed files with 511 additions and 7 deletions

View file

@ -50,7 +50,31 @@
<p><a href="/admin/bingo">manage</a><% if bingo_public_url %> · <a href="<%= bingo_public_url %>" target="_blank" rel="noopener">public card ↗</a><% end %></p>
</section>
<% end %>
<% unless @inv || @bingo %>
<% if @files %>
<section class="section dash-tile">
<h2><a href="/admin/files">Files</a></h2>
<div class="stats">
<div><b><%= @files[:count] %></b><span>published</span></div>
<div><b><%= fmt_bytes(@files[:bytes]) %></b><span>used</span></div>
<div><b><%= fmt_bytes(PublicFile.quota_bytes) %></b><span>quota</span></div>
</div>
<% if @files[:recent].any? %>
<table class="invoice-list">
<tbody>
<% @files[:recent].each do |f| %>
<tr>
<td class="mono"><a href="/admin/files/<%= f.id %>"><%= f.filename %></a></td>
<td class="num mono"><%= fmt_bytes(f.size) %></td>
<td class="mono"><%= f.updated_at.strftime('%Y-%m-%d') %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<p><a href="/admin/files">manage</a></p>
</section>
<% end %>
<% unless @inv || @bingo || @files %>
<section class="section"><p>No areas are assigned to your account yet. Ask an administrator for access.</p></section>
<% end %>
</div>