9

I'm using Active Storage to store files in a Rails 5.2 project. I've got files saving to S3, but they save with random string filenames and directly to the root of the bucket. I don't mind the random filenames (I actually prefer it for my use case) but would like to keep different attachments organized into folders in the bucket.

My model uses has_one_attached :file. I would like to specify to store all these files within a /downloads folder within S3 for example. I can't find any documentation regarding how to set these paths.

Something like has_one_attached :file, folder: '/downloads' would be great if that's possible...

Ryan
  • 17,511
  • 23
  • 63
  • 88
  • Possible duplicate of [How to specify a prefix when uploading to S3 using activestorage's direct upload?](https://stackoverflow.com/questions/48389782/how-to-specify-a-prefix-when-uploading-to-s3-using-activestorages-direct-upload) – George Claghorn Jun 17 '18 at 16:10
  • For S3 it won't matter much but if you want to switch to disk: https://stackoverflow.com/questions/59602764/change-activestorage-directdisk-service-configuration-at-runtime/59812871#59812871 – Nick M Jan 19 '20 at 18:05

7 Answers7

5

The ultimate solution is to add an initializer. You can add a prefix based on an environment variable or your Rails.env :

# config/initializer/active_storage.rb
Rails.configuration.to_prepare do
    ActiveStorage::Blob.class_eval do
      before_create :generate_key_with_prefix
  
      def generate_key_with_prefix
        self.key = if prefix
          File.join prefix, self.class.generate_unique_secure_token
        else
          self.class.generate_unique_secure_token
        end
      end
  
      def prefix
        ENV["SPACES_ROOT_FOLDER"]
      end
    end
  end

It works perfectly with this. Other people suggest using Shrine.

Credit to for this great workaround : https://dev.to/drnic/how-to-isolate-your-rails-blobs-in-subfolders-1n0c

ZazOufUmI
  • 3,212
  • 6
  • 37
  • 67
4

As of now ActiveStorage doesn't support that kind of functionality. Refer to this link. has_one_attached just accepts name and dependent.

Also in one of the GitHub issues, the maintainer clearly mentioned that they have clearly no idea of implementing something like this.

The workaround that I can imagine is, uploading the file from the front-end and then write a service that updates key field in active_storage_blob_statement

Abhilash Reddy
  • 1,499
  • 1
  • 12
  • 24
  • this link now work https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/attached/macros.rb#L30 – Ruan Nawe Sep 29 '20 at 05:17
4

There is no official way to change the path which is determined by ActiveStorage::Blob#key and the source code is:

def key
  self[:key] ||= self.class.generate_unique_secure_token
end

And ActieStorage::Blog.generate_unique_secure_token is

def generate_unique_secure_token
  SecureRandom.base36(28)
end

So a workaround is to override the key method like the following:

# config/initializers/active_storage.rb
ActiveSupport.on_load(:active_storage_blob) do
  def key
    self[:key] ||= "my_folder/#{self.class.generate_unique_secure_token}"
  end
end

Don't worry, this will not affect existing files. But you must be careful ActiveStorage is very new stuff, its source code is variant. When upgrading Rails version, remind yourself to take look whether this patch causes something wrong.

You can read ActiveStorage source code from here: https://github.com/rails/rails/tree/master/activestorage

Yi Feng Xie
  • 4,378
  • 1
  • 26
  • 29
  • you know how to i'm get the object model of my app associated with ActiveStorage::Blob? – Ruan Nawe Sep 29 '20 at 04:47
  • in rails 6.0, it looks like the :key gets set by has_secure_token, so by the time my overridden "def key" code from above was hit, self[:key] was already defined and the subfolder was not prepended. I tweaked that def key method to include an if check to see if a subfolder was defined and if not, add it explicitly (without the ||=). This may not work for everyone, but some variation based on your needs may work. Good luck! – patrickbadley May 26 '22 at 17:05
3

Active Storage by default doesn't contain a path/folder feature but you can override the function by

model.file.attach(key: "downloads/filename", io: File.open(file), content_type: file.content_type, filename: "#{file.original_filename}")

Doing this will store the key with the path where you want to store the file in the s3 subdirectory and upload it at the exact place where you want.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Ankit
  • 91
  • 2
  • 7
2

Solution using Cloudinary service

If you're using Cloudinary you can set the folder on storage.yml:

cloudinary:
  service: Cloudinary
  folder: <%= Rails.env %>

With that, Cloudinary will automatically create folders based on your Rails env:

image

This is a long due issue with Active Storage that seems to have been worked around by the Cloudinary team. Thanks for the amazing work ❤️

Flavio Wuensche
  • 9,460
  • 1
  • 57
  • 54
0
# config/initializers/active_storage.rb
ActiveSupport.on_load(:active_storage_blob) do
  def key
    sql_find_order_id = "select * from active_storage_attachments where blob_id = #{self.id}"
    active_storage_attachment = ActiveRecord::Base.connection.select_one(sql_find_order_id)

    # this variable record_id contains the id of object association in has_one_attached
    record_id = active_storage_attachment['record_id']

    self[:key] = "my_folder/#{self.class.generate_unique_secure_token}"
    self.save

    self[:key]
  end
end
Ruan Nawe
  • 363
  • 6
  • 9
0

If you don't want random filenames (in my project, I expect less than 100 total uploads and I want to be able to tell what they are in S3), here's @ZazOufUmI's answer adjusted for keeping original filenames plus a bit of randomness:

Rails.configuration.to_prepare do
  ActiveStorage::Blob.class_eval do
    before_create :generate_key_with_prefix

    def generate_key_with_prefix
      self.key = if prefix
        File.join prefix, SecureRandom.base36(4) + "-" + self.filename.to_s
      else
        self.class.generate_unique_secure_token
      end
    end

    def prefix
      ENV["SPACES_ROOT_FOLDER"]
    end
  end
end
Sprachprofi
  • 1,229
  • 12
  • 24