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
39 lines
2.4 KiB
Text
39 lines
2.4 KiB
Text
<%== erb :'admin/_nav', layout: false %>
|
|
<div class="header">
|
|
<h1><%= @file.filename %></h1>
|
|
<p class="tagline"><a href="/admin/files">← files</a> · <%= @file.inline ? 'opens in the browser' : 'served as a download' %></p>
|
|
</div>
|
|
|
|
<% if @flash %><div class="form-error-base"><div><%= @flash %></div></div><% end %>
|
|
|
|
<section class="section">
|
|
<p class="mono"><a href="<%= @file.url %>" target="_blank" rel="noopener"><%= @file.url %></a></p>
|
|
<% if @file.content_type.start_with?('image/') %>
|
|
<p><img src="<%= @file.url %>" alt="" style="max-width: 100%; max-height: 280px" /></p>
|
|
<% end %>
|
|
<p class="optional"><%= fmt_bytes(@file.size) %> · <%= @file.content_type %> · sha256 <span class="mono"><%= @file.sha256 %></span></p>
|
|
<p class="optional">Published <%= @file.created_at.strftime('%Y-%m-%d %H:%M') %> UTC, last changed <%= @file.updated_at.strftime('%Y-%m-%d %H:%M') %> UTC by <%= @file.uploaded_by %></p>
|
|
</section>
|
|
|
|
<section class="section">
|
|
<h2>Edit</h2>
|
|
<form class="invoice-form" method="post" action="/admin/files/<%= @file.id %>" enctype="multipart/form-data">
|
|
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
|
|
<fieldset class="plain" <%= 'disabled' unless can?('files', 'write') %>>
|
|
<div class="field"><label for="note">Note <span class="optional">(only shown here)</span></label><input type="text" id="note" name="note" value="<%= @file.note %>" maxlength="500" /></div>
|
|
<div class="field"><label for="file">Replace the contents <span class="optional">(same link; must be a .<%= PublicFile.ext_of(@file.filename) %> file; caches may show the old one for 5 minutes)</span></label><input type="file" id="file" name="file" /></div>
|
|
<p><label><input type="checkbox" name="keep_metadata" value="1" /> keep image metadata</label></p>
|
|
<% if can?('files', 'write') %><button type="submit" class="download-button">Save</button><% end %>
|
|
</fieldset>
|
|
</form>
|
|
</section>
|
|
|
|
<% if can?('files', 'delete') %>
|
|
<section class="section">
|
|
<h2>Danger zone</h2>
|
|
<form method="post" action="/admin/files/<%= @file.id %>/delete" onsubmit="return confirm('Delete this file? Its link stops working.')">
|
|
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
|
|
<button type="submit" class="link-button danger">Delete file</button>
|
|
</form>
|
|
</section>
|
|
<% end %>
|