Paperclip にてファイル名にハッシュ値を利用する

# 概要

Paperclip ではファイル名にハッシュ値を利用することができます。

これにより、

- ユーザがアップロードしたファイル名をそのまま利用したくない
- URL に日本語が入ってしまうのを避けたい
- リソースの ID が漏れることを避けたい

などといった要望に答えることができます。

# 実装

```ruby:app/models/users.rb
has_attached_file :avatar, {
    :url => "/system/:hash.:extension",
    :hash_secret => "longSecretString"
}
```

`hash_secret` を指定しない場合はエラーとなってしまいます。

Rails Console 等から `SecureRandom.base64(128)` で採番すると良いでしょう。

`Rails.application.secrets.secret_key_base` は利用するのはあまりオススメしません。
例えば一律でセッションをクリアしたい場合に変更する可能性があるので。


# 既存データを移動したい

ファイル名にハッシュ値使えるのは良いのだけど、URL を変更すると当然これまでの画像を参照できなくなってしまいます。移行が必要ですね。

S3 を利用している場合は、以下の rake タスクを実行するとハッシュ値を利用した新しいファイル名でファイルを **コピー** してくれます。

`old_path_option`、`new_path_option` および `Rails.application.secrets` の各種値は適宜書き換えてください。

```ruby:lib/tasks/paperclip_migration.rake
# coding: utf-8
# 添付画像のファイル名を変更する
# http://stackoverflow.com/questions/8875065/migrating-paperclip-s3-images-to-new-url-path-format

namespace :paperclip_migration do

  desc 'Migrate data'
  task :migrate_s3 => :environment do
    # Make sure that all of the models have been loaded so any attachments are registered
    puts 'Loading models...'
    Dir[Rails.root.join('app', 'models', '**/*')].each { |file| File.basename(file, '.rb').camelize.constantize }

    # Iterate through all of the registered attachments
    puts 'Migrating attachments...'
    attachment_registry.each_definition do |klass, name, options|
      puts "Migrating #{klass}: #{name}"
      klass.find_each(batch_size: 100) do |instance|
        attachment = instance.send(name)

        unless attachment.blank?
          attachment.styles.each do |style_name, style|
            old_path = interpolator.interpolate(old_path_option, attachment, style_name)
            new_path = interpolator.interpolate(new_path_option, attachment, style_name)
            # puts "#{style_name}:\n\told: #{old_path}\n\tnew: #{new_path}"
            s3_copy(s3_bucket, old_path, new_path)
          end
        end
      end
    end

    puts 'Completed migration.'
  end

  #############################################################################
  private

  # Paperclip Configuration
  def attachment_registry
    Paperclip::AttachmentRegistry
  end

  def s3_bucket
    Rails.application.secrets.s3_bucket
  end

  def old_path_option
    'paperclip/:class/:attachment/:id_partition/:style/:filename'
  end

  def new_path_option
    'paperclip/:class/:attachment/:id_partition/:style/:hash.:extension'
  end

  def interpolator
    Paperclip::Interpolations
  end

  # S3
  def s3
    AWS::S3.new(access_key_id: Rails.application.secrets.s3_access_key, secret_access_key: Rails.application.secrets.secret_access_key)
  end

  def s3_copy(bucket, source, destination)
    source_object = s3.buckets[bucket].objects[source]
    destination_object = source_object.copy_to(destination, {metadata: source_object.metadata.to_h})
    destination_object.acl = source_object.acl
    puts "Copied #{source} to #{destination}"
  rescue Exception => e
    puts "*Unable to copy #{source} - #{e.message}"
  end

end
```

**実行方法**

```bash
./bin/rake paperclip_migration:migrate_s3 RAILS_ENV=production
```

# 参考

- [thoughtbot/paperclip](https://github.com/thoughtbot/paperclip)
- [Hashing · thoughtbot/paperclip Wiki](https://github.com/thoughtbot/paperclip/wiki/Hashing)
- [ruby on rails - Migrating paperclip S3 images to new url/path format - Stack Overflow](http://stackoverflow.com/questions/8875065/migrating-paperclip-s3-images-to-new-url-path-format)