Proposal: Enhance the DirectUpload JS API to support more useful custom blob names (like the ActiveStorage::Attached::*attach(:key) argument)

TL;DR

It Would Be Nice If™ if DirectUpload accepted a key argument to customize and override the default hash-based blob naming and to support sub-dirs within buckets.

(Others have asked for this before, e.g. in this thread that began in 2022))

Proposal

The Edge Rails docs illustrate how ActiveStorage::Attached::Many#attach accepts an optional :key argument to name and organize blobs (section 3.3 Attaching File/IO Objects). As the docs describe,

AWS S3 otherwise uses a random key to name your files. [Specifying a key] is helpful if you want to organize your S3 Bucket files better.

I :heart: this feature of ActiveStorage in Rails — in my experience, legible filenames + directory structures can be quite helpful in debugging or troubleshooting scenarios. An opaque tree of checksum hashes feels a little more fragile to me personally.

But for DirectUpload, I wasn’t able to find docs in support of equivalent customization options for JS use cases (e.g. from a Vue or Svelte or React app with a Rails back-end). Does the JS DirectUpload api support passing a custom key function the way the Rails one does?

Context

Mapping files to specific ActiveStorage sub-directories and/or specific file/blob naming schemas is supported and can be helpful eg. for some use cases, it may be valuable to preserve the filename, or to organize by date, or whatever.

For example, the following s3 key method:

  • preserves the original input filename
  • adds a statistically unique-ish suffix (not saying it’s great code, just an example)
  • organizes files into two-digit year / two-digit month directories for archival purposes (and easier reverse-engineering in the case of a debugging scenario):
def attach_image(img_path)
  images.attach(
    io: File.open(img_path),
    filename: File.basename(img_path),
    key: image_storage_key(img_path)
  )
end

private

def image_storage_key(img_path)
  filename = File.basename(img_path, ".*")
  ext = img_path.match( %r{\w{3,4}$} )
  t = Date.today
  month_year = t.year.to_s.slice(2,2) + "%02d" % t.month
  "#{month_year}-#{filename}-#{SecureRandom.hex.slice(0,6)}.#{ext}"
end

System configuration

Rails version: 7.2.2

1 Like

I think you could implement the key assignment if you replace the direct upload Blob creation endpoint with your own. It’s just a call to create! after all. What you would want to do though is some kind of validation on that value - or giving it to the client signed after validation. Since the direct uploads are basically PUTs, if this is not treated carefully an attacker could trick your blob creation action into giving it a signed URL to an object in the bucket and overwrite it, even though ti was already there.

I like the idea of readable prefixes for debugging, but I would be cautious about making the final storage key entirely client-controlled.I think a useful middle ground would be to let the client provide some logical information, such as a project or desired filename, while Rails constructs the actual key and appends a server-generated unique value.

That would preserve readable paths without making the key itself something the application has to trust for uniqueness or authorization. I would treat the S3 prefix mainly as an organizational detail and keep ownership tied to the Active Storage records. The overwrite risk mentioned above is the part I would be most careful about. If DirectUpload gains support for custom naming, I think the API should make it easy to customize the readable part without encouraging clients to choose the complete object key.