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
66 lines
1.9 KiB
Ruby
66 lines
1.9 KiB
Ruby
require 'aws-sdk-s3'
|
|
|
|
module S3
|
|
module_function
|
|
|
|
def client
|
|
@client ||= Aws::S3::Client.new(
|
|
region: ENV.fetch('S3_REGION', 'us-east-1'),
|
|
endpoint: ENV.fetch('S3_ENDPOINT'),
|
|
access_key_id: ENV.fetch('S3_ACCESS_KEY'),
|
|
secret_access_key: ENV.fetch('S3_SECRET_KEY'),
|
|
force_path_style: true
|
|
)
|
|
end
|
|
|
|
# Separate client whose endpoint is the public hostname; used only to mint
|
|
# presigned URLs that a browser can dereference. Bytes never travel through
|
|
# this client.
|
|
def presign_client
|
|
@presign_client ||= Aws::S3::Client.new(
|
|
region: ENV.fetch('S3_REGION', 'us-east-1'),
|
|
endpoint: ENV.fetch('S3_PUBLIC_ENDPOINT'),
|
|
access_key_id: ENV.fetch('S3_ACCESS_KEY'),
|
|
secret_access_key: ENV.fetch('S3_SECRET_KEY'),
|
|
force_path_style: true
|
|
)
|
|
end
|
|
|
|
def bucket
|
|
ENV.fetch('S3_BUCKET')
|
|
end
|
|
|
|
def put(key, bytes, content_type: 'application/pdf', bucket: self.bucket, **headers)
|
|
client.put_object(
|
|
bucket: bucket,
|
|
key: key,
|
|
body: bytes,
|
|
content_type: content_type,
|
|
**headers # content_disposition, cache_control
|
|
)
|
|
key
|
|
end
|
|
|
|
def delete(key, bucket: self.bucket)
|
|
client.delete_object(bucket: bucket, key: key)
|
|
end
|
|
|
|
def get(key, bucket: self.bucket)
|
|
client.get_object(bucket: bucket, key: key).body.read.b # body is a UTF-8 StringIO
|
|
end
|
|
|
|
def exists?(key)
|
|
client.head_object(bucket: bucket, key: key)
|
|
true
|
|
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::NoSuchKey
|
|
false
|
|
end
|
|
|
|
def presigned_url(key, expires_in: 300, filename: nil)
|
|
params = { bucket: bucket, key: key }
|
|
if filename
|
|
params[:response_content_disposition] = %(attachment; filename="#{filename}")
|
|
end
|
|
Aws::S3::Presigner.new(client: presign_client).presigned_url(:get_object, **params, expires_in: expires_in)
|
|
end
|
|
end
|