Home > Mobile >  S3 save old url, change paperclip config, set new url as old
S3 save old url, change paperclip config, set new url as old

Time:09-24

So here is the thing: currently our files, when user downloads them, have names like 897123uiojdkashdu182uiej.pdf. I need to change that to file-name.pdf. And logically I go and change paperclip.rb config from this:

Paperclip::Attachment.default_options.update({
                                               path: '/:hash.:extension',
                                               hash_secret: Rails.application.secrets.secret_key_base
                                             })

to this:

Paperclip::Attachment.default_options.update({
                                               path: "/attachment/#{SecureRandom.urlsafe_base64(64)}/:filename",
                                               hash_secret: Rails.application.secrets.secret_key_base
                                             })

which works just fine, filenames are great. However, old files are now unaccessable due to the change in the path. So I came up with the following decision First I made a rake task which will store the old paths in the database:

namespace :paperclip do
  desc "Set old urls for attachments"
  task :update_old_urls => :environment do

    Asset.find_each do |asset|
      if asset.attachment
        attachment_url = asset.attachment.try!(:url)
        file_url = "https:#{attachment_url}"

        puts "Set old url asset attachment #{asset.id} - #{file_url}"
        asset.update(old_url: file_url)
      else
        puts "No attachment found in asset #{asset.id}"
      end
    end
  end
end

Now the asset.old_url stores the current url of the file. Then I go and change the config, making the file unaccessable. Now it's time for the new rake task:

require 'uri'
require 'open-uri'

namespace :paperclip do
  desc "Recreate attachments and save them to new destination"
  task :move_attachments => :environment do

    Asset.find_each do |asset|
      unless asset.old_url.blank?
        url = asset.old_url
        filename = File.basename(asset.attachment.path)
        file = File.new("#{Rails.root}/tmp/#{filename}", "wb")
        file.write(open(url).read)

        if File.exists? file
          puts "Re-saving asset attachment #{asset.id} - #{filename}"
          asset.attachment = file
          asset.save
          # if there are multiple styles, you want to recreate them :
          asset.attachment.reprocess!
          file.close
        else
          puts "Missing file attachment #{asset.id} - #{filename}"
        end
        File.delete(file)
      end
    end
  end
end

But my plan didn't work at all, I didn't get access to the files, and the asset.url still isn't equal to asset.old_url.

Would appreciate help very much!

CodePudding user response:

With S3, you can set the "filename upon saving" as a header. Specifically, the user will get to an url https://foo.bar.com/mangled/path/some/weird/hash/whatever?options and when the browser will offer to save, you can control the filename (not the url).

The trick to that relies on the browser reading the Content-Disposition header from the response, if it reads Content-Disposition: attachment; filename="filename.jpg" it will save (or ask the user to save as) filename.jpg, independently on the original URL.

You can force S3 to add this header by adding one more parameter to the URL or by setting a metadata on the file.

The former can be done by passing it to the url method:

has_attached_file :attachment, 
                  s3_url_options: ->(instance) { 
                    {response_content_disposition: "attachment; filename=\"#{instance.filename}\""}
                  }

Check https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/storage/s3.rb#L221-L225 for the relevant source code.

The latter can be done in bulk via paperclip (and you should also configure it to do it on new uploads). It will also take a long time!!

Asset.find_each do |asset|
  next unless asset.attachment
  
  s3_object = asset.attachment.s3_object
  s3_object.copy_to(
    s3_object,
    metadata_directive: 'REPLACE',
    content_disposition: "attachment; filename=\"#{asset.filename}\")"
  )
end
# for new uploads
has_attached_file :attachment, 
                  s3_headers: ->(att) {
                    {content_disposition: "attachment; filename=\"#{att.model.filename}\""}
                  }
  • Related