12

With a standard S3 configuration:

AWS_ACCESS_KEY_ID:        [AWS ID]
AWS_BUCKET:               [bucket name]
AWS_REGION:               [region]
AWS_SECRET_ACCESS_KEY:    [secret]

I can upload a file to S3 (using direct upload) with this Rails 5.2 code (only relevant code shown):

form.file_field :my_asset, direct_upload: true

This will effectively put my asset in the root of my S3 bucket, upon submitting the form.

How can I specify a prefix (e.g. "development/", so that I can mimic a folder on S3)?

Martin Carel
  • 271
  • 4
  • 14
  • Not sure if that's currently possible - have a look at the source code here: https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/service/s3_service.rb#L19 – Adil B Jan 22 '18 at 22:24

4 Answers4

11

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! :)

Sonia Nkatha
  • 111
  • 1
  • 5
  • 1
    It's a shame work-arounds like these are needed for actions like this. This is also needed to change the acl permissions and storage type. – frillybob Feb 15 '19 at 16:32
4

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

George Claghorn
  • 24,999
  • 3
  • 40
  • 45
  • 1
    I believe there are [no plans to add this](https://github.com/rails/rails/issues/32790) and we would need to implement something like Sonia's answer below if we require this functionality. – Marklar Jun 12 '18 at 05:22
  • 6
    Oh... you again. That's just not a viable option, @George Claghorn . So, so many people want this, I don't understand why you're just nixing an option that literally every competing library has always had the ability to do. – mpowered Apr 28 '19 at 15:00
  • 2
    Same as @mpowered... I just can't understand why you wouldn't want to add that feature. At least give us a reason, not just say "no". Even Carrierwave makes it easy to do it. – javierojeda Jun 07 '19 at 03:11
4

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.

mizinsky
  • 91
  • 1
  • 8
0

The above solution will still give IntegrityError, need to use File.open(file). Thank Though for idea.

class History < ApplicationRecord
  has_one_attached :gs_history_file

  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.to_s
        blob.key = blob_key
        #blob.byte_size = 123123
        #blob.checksum = Time.new.strftime("%Y%m%d-") + Faker::Alphanumeric.alpha(6)
        blob.upload File.open(file)
        blob.save!
      end
    end

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

  def destination_pathname(file)
    "testing/filename-#{Time.now}.xlsx"
  end
end
Amit Doshi
  • 11
  • 1