Rails でファイルのアップロードとダウンロード 2

前回「CarrierWave」を使って AWS S3 にファイルをアップロードをしましたが、
S3 にアップロードしたファイルは、AWS CloudFront を使って配信するのがベターみたいです。

今回は、AWS CloudFront を介したファイルダウンロードを作ってみます。

目次


参考


AWS の設定

CloudFront 用キーペア取得

AWS マネジメントコンソールにログインします。

アカウントメニューを開いて「マイセキュリティ資格情報」を開きます。
(ここは、公式だと[account-name] メニューで、[Security Credentials]をクリックとなっています。)

「CroudFront のキーペア」という項目を開いて「新しいキーペアの作成」をします。

プライベートキーファイル、パブリックキーファイルのダウンロードができるのでそれぞれダウンロードしておきます。
私は、後に作成する Rails アプリケーションの、アプリケーションルートに保存しておきました。

プライベートキーは「pk-」、パブリックキーの方は「rsa-」から始まるファイルでした。

作成が終わるとアクセスキー ID が表示されるようになるので、こちらも控えておきます。

CloudFlont 設定

AWS マネジメントコンソールで、CloudFront の設定画面を開きます。
AWS マネジメントコンソールの「ネットワークとコンテンツ配信」という項目にリンクがあります。検索でもいいです。

「Create Distribution」をクリックします。

Web と RTMP がありますが、Web 側の「Get Started」をクリックします。

「Origin Domein Name」をクリックすると、ログインしている AWS のアカウントが所有している S3 のバケットが表示されるので、それを選びます。

Restrict Viewer Access(Use Signed URLs or Signed Cookies)を「Yes」に変更します。

以上ができたら、一番下の「Create Distribution」をクリックします。

作成後すぐには使えないので、しばらく待ちます。
CloudFront 設定画面で現在のステータスが「In Progress」から「Deployed」に変わるまでの間待つことになります。(ざっくり 15 分くらい待ちました。)

後で使うので Domain Name の項目を控えておきます。
ID の項目をクリックして、作成した CloudFront Distribution の詳細を開きます。

Origin の中から選択して、Edit を押します。

Restrict Bucket Access を「Yes」、Grant Read Permissions on Bucket を「Yes」にして「Yes, Edit」を押します。

ここまでで AWS マネジメントコンソールでの操作は終了です。


Rails で実装

今回は、前回記事のRails でファイルのアップロードとダウンロードの「CarrierWave」を使用した S3 へのアップロードを改変します。
そちらも参考ください。

CarrierWave で実装してダウンロードをする仕組みを作ったとき、コントローラーのダウンロードメソッドは以下のようにしてました。
こちらを改変して、CloudFront からファイルを受け取るようにします。

app/controllers/accounts_controller.rb
1
2
3
def download
send_data(@account.icon.read,:filename=>@account.icon_identifier)
end

パッケージ導入

Gemfile に以下を追記して、bundle installを実行してインストールします。

Gemfile
1
gem 'aws-sdk-cloudfront'

コントローラのメソッドの修正

以下のように改変しました。

app/controllers/accounts_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def download
#CloudFrontドメイン情報
domain_name="xxxxxxxxxxxxxxxxx.cloudfront.net"

# UrlSignerを初期化
signer = Aws::CloudFront::UrlSigner.new(
key_pair_id: "XXXXXXXXXXXXXXXXXXXXXXXXXX", # アクセスキー
private_key_path: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.pem" # シークレットキーファイル
)

# 署名付きURLを発行
signed_url = signer.signed_url(
"https://#{domain_name}/#{@account.icon.path}",
expires: Time.now.getutc + 3.minute
)

# 取得したURLへリダイレクト
redirect_to signed_url
end

参考:ビューの作成

ダウンロード処理を要求するビューは以下の通りです。

app/views/accounts/show.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<p id="notice"><%= notice %></p>

<p>
<strong>Name:</strong>
<%= @account.name %>
</p>

<p>
<strong>Icon:</strong>
<%= image_tag @account.icon.url %>
<%= link_to 'ダウンロード',download_icon_path(@account) %>
</p>

<%= link_to 'Edit', edit_account_path(@account) %> |
<%= link_to 'Back', accounts_path %>

確認

以上ができたら、rails sで実行して該当のページでダウンロードします。
ダウンロードできたでしょうか?

正直言ってダメでした。
リダイレクトした先へページ遷移してしまいました。
意図した動作ではないのが痛いところです。

ただ、表示されたとき URL を見るとアドレスがhttps://XXXXXXXXXXXXX.cloudfront.net/から始まるようになったのではないでしょうか。
CloudFront から署名付き URL を要求するという目標は達成できました。


それでもダウンロードを僕はしたいんだ

それでもダウンロードはしたいので、調べます。

参考

どうやら、レスポンスヘッダーに'Content-type: application/force-download'が付いているとよいそうです。
S3 にはアップロードしたファイル(オブジェクトと言っていたりどっち?)は、レスポンスヘッダーのカスタマイズができます。
この設定は「CarrierWave」でファイルアップロード時に適用可能です。

アップローダーを編集してゆきます。

アップローダーの編集

app/uploaders/icon_uploader.rb に以下を追記します。

app/uploaders/icon_uploader.rb[追記部分]
1
2
3
4
5
def fog_attributes
{
'Content-Type' => 'application/force-download',
}
end

追記したものは以下の通りです。コメントはすべて削ってあります。

app/uploaders/icon_uploader.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
class IconUploader < CarrierWave::Uploader::Base
storage :fog

def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end

def fog_attributes
{
'Content-Type' => 'application/force-download',
}
end
end

コントローラーの修正

以下のように取得した URL を json で返すように変更します。
render json: {url:signed_url}の部分です。

app/controllers/accounts_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def download
#CloudFrontドメイン情報
domain_name="xxxxxxxxxxxxxxxxx.cloudfront.net"

# UrlSignerを初期化
signer = Aws::CloudFront::UrlSigner.new(
key_pair_id: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX", # アクセスキー
private_key_path: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.pem" # シークレットキーファイル
)

# 署名付きURLを発行
signed_url = signer.signed_url(
"https://#{domain_name}/#{@account.icon.path}",
expires: Time.now.getutc + 3.minute
)

# 取得したURLへリダイレクト
#redirect_to signed_url
render json: {url:signed_url}

end

ビューの修正

ダウンロード処理を要求するビューを書き換えます。
ポイントは、<%= link_to 'ダウンロード',download_icon_path(@account),id: 'downloadfile' %>と script タグ内です。
link_to で作成される a 要素に id を割り当てて、クリックした際に jQuery で処理させます。
コントローラーから帰ってきた署名付き URL を割り当ててきた a 要素を jQuery で発火させて、ダウンロードさせる仕組みです。

app/views/accounts/show.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<p id="notice"><%= notice %></p>

<p>
<strong>Name:</strong>
<%= @account.name %>
</p>

<p>
<strong>Icon:</strong>
<%= image_tag @account.icon.url %>
<%= link_to 'ダウンロード',download_icon_path(@account),id: 'downloadfile' %>
</p>

<%= link_to 'Edit', edit_account_path(@account) %> |
<%= link_to 'Back', accounts_path %>

<script type="text/javascript">
$("a#downloadfile").click(function(event) {
event.preventDefault();
//aタグのURLを使ってjqueryで問い合わせ
$.get(this.href)
.done(function(data){
//受け取ったURLでa要素を作成
$(`body`).append('<a id="download" href="'+data.url+'"></a>')

//DOMの作成直後はうまく動作できないので1秒後の実行を設定
setTimeout(() => {
$('#download')[0].click();
$('#download').remove();
}, 1000);
})
})
</script>

確認

以上ができたら、rails s で実行してファイルのアップロードから行います。
``’Content-type: application/force-download’`が設定されているのは、アップローダーの編集をした以後アップロードしたファイルだけです。
それ以前のファイルはページ遷移するはずです。
必要で有れば AWS マネジメントコンソールで S3 の設定をすればよいでしょう。

今度はダウンロードできました。
問答無用でファイルはダウンロードされるようになりました。


今回は「CarrierWave」を使用して AWS S3 にアップロード、AWS CloudFront を介してファイルのダウンロードをやってみました。
AWS マネジメントコンソールが急に英語のみの管理画面になったりしますが、根気よく進めましよう。

ではでは。