t-sanoブログ

メモです。マイペースにアウトプットします。

S3にファイルを直接アップロードする(署名付きURL)

目的

サーバから署名付きURLを発行し、クライアントは署名付きURLを利用してファイルをアップロードする

環境

目指す構成

f:id:t-sanox:20160814161940p:plain

検証

署名付きURLの発行

gemはAWS-SDKを利用した。

gem 'aws-sdk'

下記のコードで署名付きURLを発行することができた。

Aws.config.update({
    region: ENV['REGION'],
    credentials: Aws::Credentials.new(ENV['KEY_ID'], ENV['KEY'])
  })
s3_bucket = Aws::S3::Resource.new.bucket(ENV['BUCKET'])

path = 'バケットに配置する際のパス(ファイル名を含む)'
s3_bucket.object(path).presigned_url(:put, expires_in: 'URLの有効期限')

ファイルのアップロード

発行した署名付きURLに対して、ファイルデータを送ってみると、ファイルがアップロードされていた。

curl -X PUT -d @hoge.json '署名付きURL'

Rails5のAPIモードで画像ファイルをS3にアップロードする。(carrierwave利用)

前回、carrierwaveを利用し、S3に画像ファイルをアップロードする機能を作りました。

ubw.hatenablog.com

Rails5のAPIモードで作っていたので、今回は、APIクライアントとの連携を試してみます。 APIクライアントからS3に画像アップロードができればゴールです。

前回と同じように、実施プロセスをメモしていきます。

早速、試してみる

HTMLフォームにもろもろ入力して、いざ、submit!

・・・

下記のようなエラーが出て、うまくいかなかった・・・。

Encoding::UndefinedConversionError ("\x89" from ASCII-8BIT to UTF-8)

UndefinedConversionError

どうやら、encodingの食い違いが原因らしい。 画像ファイルのencodingがASCII-8BITだが、UTF-8に変換しようとしているため発生するらしい。 ならばと、UTF-8エンコードして渡してやったが、渡した先でのデータの持ち方がよくわからん状態になっていた。

次の手として、下記2つを思いついた。

  • Redisに格納しちゃえ(APIクライアントとAPIサービスが同じRedisを利用している前提)
  • base64で渡す

画像データをsession(Redis)に持たせてみる

次に、画像データをsession(Redis)に格納してみた。 やっぱりエラー・・・。

TypeError (can't dump File)

Redisに格納する際に、Marshal.dumpをしているようなのだが、

qiita.com

上記のサイトによると、

Marshal.dumpは 名前のついてないオブジェクト システムがオブジェクトを保持するもの などを書きだそうとするとTypeErrorを起こす

らしい。

APIクライアント側Base64エンコードして渡す

下記の流れでやってみる。

  1. APIクライアントでBase64エンコード
  2. APIサービスでBase64エンコードされた画像データを受け取り、デコード

APIクライアントでBase64エンコード

まず、前提として、 HTTPで画像データをアップロードする際は下記のライブラリを利用する。

ActionDispatch::Http::UploadedFile

このライブラリの使い方がよくわからずハマったのですが、 ライブラリには下記のようなパラメータがあり、

@original_filename
@header
@tempfile
@content_type

@tempfileというパラメータで、画像データをバイナリで保持していた。 なので、それぞれのパラメータをAPIで受け取れればよいのではないかと考えた。 @tempfileだけはバイナリなので、Base64エンコードして渡すこととした。

def image_params
  {
    filename: form_param[:image].original_filename,
    type: form_param[:image].content_type,
    tempfile: encode64_tempfile
  } unless form_param[:image].blank?
end

def encode64_tempfile
  tempfile = Base64.strict_encode64(form_param[:image].tempfile.read)
  URI.escape(tempfile)
end

APIサービスでBase64エンコードされた画像データを受け取り、デコード

def decode64_image
  image = params[:image]
  return unless image.present?
  img_params = {
    filename: image[:filename],
    type: image[:type],
    tempfile: decode64_tempfile(image[:tempfile], image[:filename])
  }
  ActionDispatch::Http::UploadedFile.new(img_params)
end

def decode64_tempfile(file, filename)
  tempfile = URI.decode(file)
  tempfile = Base64.decode64(tempfile)
  file = Tempfile.new(filename)
  file.binmode
  file << tempfile
  file.rewind
  file
end

結果

アップロードできた! 画像をアップロードできるAPIができました。

参考

class Encoding::UndefinedConversionError (Ruby 2.0.0)

Railsでファイルをアップロードする際のencodingエラー | ぐんまのたなかの備忘録

qiita.com

qiita.com

carrierwaveの動作確認

先日、Railsを使ってS3への画像アップロードをやってみた。

ubw.hatenablog.com

アップロードに際して、carrierwaveを利用しているのだが、 アップロード以外の動作はどのように処理してくれるのかが気になったので確認してみる。

具体的には、下記の動作をしたら、どうなるか確認する。

  1. カラム更新
  2. レコード削除

カラム更新

結論、カラムを更新すると、画像ファイル自体も更新された。
具体的には、下記のとおりである。

  1. レコードAのカラムaには、image.jpegが格納されている
  2. カラムaをimage_ver2.jpegで更新した
  3. S3には、image.jpegは削除され、image_ver2.jpegが格納された

レコード削除

  1. delete_allで削除した場合は、画像が削除されなかった。 その後、同じレコードIDで画像をアップロードしてやると、 以前アップロードした画像が残っていた。
  2. destroy.allで削除した場合は、画像が(ディレクトリごと)削除された。
  3. destroyで削除した場合は、画像が(ディレクトリごと)削除された。

deleteで削除すると、画像が残ってしまうので、destroyで削除するのがよさそうだ。

Railsを使って、S3にファイルをアップロードする。

Railsを使って、S3へのファイル直接アップロードを試している。 最終ゴールはAPIとして、クライアントから画像データを受け取って、S3にアップロードすることである。 今回は、第一ステップとして、RailsからS3へのアップロード実現を目指す。

試している工程をメモしておく。

やりたいこと

Railsから画像データをS3にアップロードする。

環境

Rails 5.0.0(APIモード)

早速やってみる

やりたいことを満たせそうなgemがいくつかありそうだったが、 今回はcarrierwaveを利用する。

ということで、まずは、Gemfileに下記を追加した。

gem 'carrierwave'
gem 'fog-aws'
gem 'rmagick'

carrierwaveは画像ファイルをアップロードに利用するため。 fog-awsはS3に画像ファイルをアップロードするため。 rmagickは画像ファイルの操作に利用するため。

gemをインストールした後は、railsコマンドでアップローダというものを作れるらしい。 下記コマンドでアップローダを生成した。

rails g uploader Image

コマンドを流したら、アップローダができた。 appの下にuploadersというディレクトリができており、配下にアップローダが生成されている。

生成したアップローダはもう利用できるらしい。 あらかじめ作成しているモデルに、アップローダをマウントしてやる。 カラムとアップローダを紐付けるようなイメージ。

class Test < ApplicationRecord
  mount_uploader :image, ImageUploader

これでどうやら、紐付けたカラム(上記例だと、imageカラム)に対して画像を保存できるようだ。 試しにやってみた。

test.image = Rack::Test::UploadedFile.new('/path/to/test_upload.jpeg','image/jpeg')

登録されたみたい。saveしたら、うまく入ってくれた。 saveした中身を見てみると、imageカラムにはファイル名が入っていた。 単純にfindでレコードを見ても、imageカラムには、ファイル名、test_upload.jpegが入っていた。 カラム指定でimageカラムを見ると、アップロードしたファイルの詳細が確認できた。

とりあえず、画像オブジェクトを渡せば、画像の情報をデータベースに格納してくれてるっぽいところは確認できた。 また、public/uploadsというディレクトリができており、アップロードしたファイルはそこに格納されていた。 恐らく、carrierwaveのデフォルト設定で保存先がローカルストレージになっているためだと思う。

しかし! 目的は、S3にアップロードすること!

ということで、S3へアップロードするために、carriewaveの設定を変更する。

まずは、画像の保存先を変更。 initializersにcarrierwave.rbというファイルを作成し、下記のような設定を追加した。

CarrierWave.configure do |config|
  config.fog_provider = 'fog/aws'
  config.fog_credentials = {
    provider: 'AWS',
    aws_access_key_id: 'ACCESS_KEY_ID(ENV)',
    aws_secret_access_key: 'ACCESS_KEY(ENV)',
    region: 'ap-northeast-1',
    host: 's3.example.com', ,
    endpoint: 'https://s3.example.com:8080'
  }
  config.cache_storage = :fog

  # S3のURLに直アクセス禁止
  # config.fog_public = false

  # S3のURLに有効期限を60秒で設定する
  # config.fog_authenticated_url_expiration = 60

  # S3バケットを指定
  config.fog_directory  = 'BUCKET_NAME(ENV)'
end

# 日本語入力を可能にするため。
CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/

今回は利用していないが、fog_publicfog_authenticated_url_expirationについて説明する。 fog_publicを利用することで、URLへの直アクセスを制御できる。 デフォルトはtrueらしい。 これがtrueの場合は、アップロードしたオブジェクトのアクセス許可に、 全員ダウンロードができる権限が付与される。 faluseにすると、アクセス許可は設定されず、公開されない。 fog_authenticated_url_expirationを利用することで、URLの有効期限を設定できるみたい。(秒数単位で) 設定した有効期限以降は、画像のURLに直アクセスしても閲覧できなくなる・・・と思っている。

さて、次はアップローダの設定も。 今回は、画像サイズの制御はしないので、シンプルに。

class ImageUploader < CarrierWave::Uploader::Base
  storage :fog

  # 格納するディレクトリを指定
  def store_dir
    "#{model.id}"
  end

  # キャッシュを格納ディレクトリを指定
  def cache_dir
    "cache"
  end
end

この時点でアップロードできるのか試す。 が、S3にバケットの準備を忘れていた。 ので、バケットと作成したバケットにのみアップロードできるIAMユーザを作成する。

で、railsを起動したら怒られた。 initializerでメソッドのnotfoundが発生した。(fog_providerがない。) どうやら、もろもろ、githubからインストールしないと使えないみたいだ。 githubからgemを取得するように変更したら、正常に起動した。

gem 'carrierwave', github: 'carrierwaveuploader/carrierwave'

さて、次こそ試してみる。

・・・

なんか、アップロードされてる! まず、カラムに突っ込んだ始点で、キャッシュが指定したディレクトリ構成でS3に格納されていた。

次に、いよいよ、saveしてみるとどうなるか。 結果、指定したとおりのディレクトリ構成でアップロードされた! カラムも拡張子込みのファイル名になっている。

格納したパスは

カラム名.path

で取得できそう。 他、いろいろ変数を持っているので使えそう。 カラムの中を覗くと、何があるか確認できる。

さて、S3への格納はできた。 しかし、まだAPIとして使えるようにしなければ・・・。 あとは、画像サイズのバリデーション処理とかね。

APIとして機能するようには、別途、実施します。

参考にしたサイト

github.com

paranishian.hateblo.jp

S3へのファイルアップロード方法を検討する

S3へのファイルアップロード方法はいくつかある。 それぞれの特徴を検討してみる。

前提

ファイルをアップロードするケースとして、今回は下記2つを想定している。

  1. サービス管理者はサーバ(またはローカル環境)からファイルをアップロードしたい。
  2. サービス利用者はローカル環境からファイルをアップロードしたい。

1.マウントしてアップロードする(SFTP等)

マウントしている環境からファイルをアップロードする場合は、利用用途としては良さそうだ。バッチ処理などには向いているのではないだろうか。 ただ、Webサービスなどのように、クライアントからサーバ経由でS3ファイルアップロードとなると、場合によっては無駄な処理を挟むことになる。

方法

S3をマウントしてアプロードする方法で代表的なものは、下記の2つだと考える。

  • s3fs
  • goofys

こちらについては、様々な記事で検証しているので、そちらを参考にした。

dev.classmethod.jp

s3fsについては、利用したことがあるので、参考にした記事に大きく頷くものがあった。 使うなら、goofysだろうか。s3fsよりもかなり早そうだし。

2.HTTP(S)でアップロードする

特に制約がなければ、こちらの方法でS3に直接アップロードするのが効率的だと思う。 ただし、利用者を制限する必要がある。(管理者のみ利用可能にする等) CDP:Direct Object Uploadパターンを想像している。

方法

HTTP(S)を利用してアプロードする方法としては、下記の2つがあると考える。

DragonDiskを利用すれば、エクスプローラーで利用するのと同じようにファイルをアップロードするのは簡単そうだ。 素人でもファイルをアップロードできるだろう。 AWS SDKを利用すれば、HTMLのフォームで選択したファイルをS3に直接アップロードすることができるだろう。 AWS SDKについては、別途、試してみる予定。

3.アプリケーションサーバでアップロードする

アプリケーションサーバで何らかの処理をして、ファイルをアップロードする方法。 例えば、画像のサイズを加工してからS3にアップロードする等。 ファイルサイズが大きくなければ、問題ないと思う。rubyであれば、用途に合うgemもあるし。 動画等、ファイルサイズが大きいものであれば、転送時間がかかってしまうので工夫が必要かも。

方法

rubyを利用するのであれば、下記のような便利なgemがある。

  • CarrerWave
  • fog
  • refile

こちらについても、別途、試してみる予定。