How to specify a prefix when uploading to S3 using activestorage's direct upload?

My current workaround (at least until ActiveStorage introduces the option to pass a path for the has_one_attached and has_many_attached macros) on S3 is to implement the move_to method.

So I'm letting ActiveStorage save the image to S3 as it normally does right now (at the top of the bucket), then moving the file into a folder structure.

The move_to method basically copies the file into the folder structure you pass then deletes the file that was put at the root of the bucket. This way your file ends up where you want it.

So for instance if we were storing driver details: name and drivers_license, save them as you're already doing it so that it's at the top of the bucket.

Then implement the following (I put mine in a helper):

        module DriversHelper

          def restructure_attachment(driver_object, new_structure)

          old_key = driver_object.image.key

          begin
            # Passing S3 Configs
            config = YAML.load_file(Rails.root.join('config', 'storage.yml'))

            s3 = Aws::S3::Resource.new(region: config['amazon']['region'],
                                       credentials: Aws::Credentials.new(config['amazon']['access_key_id'], config['amazon']['secret_access_key']))

            # Fetching the licence's Aws::S3::Object
            old_obj = s3.bucket(config['amazon']['bucket']).object(old_key)

            # Moving the license into the new folder structure
            old_obj.move_to(bucket: config['amazon']['bucket'], key: "#{new_structure}")


            update_blob_key(driver_object, new_structure)
          rescue => ex
            driver_helper_logger.error("Error restructuring license belonging to driver with id #{driver_object.id}: #{ex.full_message}")
          end
          end

          private

          # The new structure becomes the new ActiveStorage Blob key
          def update_blob_key(driver_object, new_key)
            blob = driver_object.image_attachment.blob
            begin
              blob.key = new_key
              blob.save!
            rescue => ex
              driver_helper_logger.error("Error reassigning the new key to the blob object of the driver with id #{driver_object.id}: #{ex.full_message}")
            end
          end

          def driver_helper_logger
            @driver_helper_logger ||= Logger.new("#{Rails.root}/log/driver_helper.log")
          end
        end

It's important to update the blob key so that references to the key don't return errors.

If the key is not updated any function attempting to reference the image will look for it in it's former location (at the top of the bucket) rather than in it's new location.

I'm calling this function from my controller as soon as the file is saved (that is, in the create action) so that it looks seamless even though it isn't.

While this may not be the best way, it works for now.

FYI: Based on the example you gave, the new_structure variable would be new_structure = "development/#{driver_object.image.key}".

I hope this helps! :)


Thank you, Sonia, for your answer.

I tried your solution and it works great, but I encountered problems with overwriting attachments. I often got IntegrityError while doing it. I think, that this and checksum handling may be the reason why the Rails core team don't want to add passing pathname feature. It would require changing the entire logic of the upload method.

ActiveStorage::Attached#create_from_blob method, could also accepts an ActiveStorage::Blob object. So I tried a different approach:

  1. Create a Blob manually with a key that represents desired file structure and uploaded attachment.
  2. Attach created Blob with the ActiveStorage method.

In my usage, the solution was something like that:

def attach file # method for attaching in the model
  blob_key = destination_pathname(file)
  blob = ActiveStorage::Blob.find_by(key: blob_key.to_s)

  unless blob
    blob = ActiveStorage::Blob.new.tap do |blob|
      blob.filename = blob_key.basename.to_s
      blob.key = blob_key
      blob.upload file
      blob.save!
    end
  end

  # Attach method from ActiveStorage
  self.file.attach blob
end

Thanks to passing a full pathname to Blob's key I received desired file structure on a server.


Sorry, that’s not currently possible. I’d suggest creating a bucket for Active Storage to use exclusively.