タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

Flutter/Dart で S3 へのダイレクトアップロードを実装する

f:id:uzuki05:20181215091555j:plain

ダイレクトアップロードのクライアント側実装では次の2つのステップが必要です。

  1. ダイレクトアップロード用のURLを取得する
  2. S3にダイレクトアップロードする

Flutter /Dart でどう書けば良いのか?私が試した方法を紹介します。

この記事ではサーバ側の実装については述べません。

続きを読む

codemagicのiOSビルドで Did not find matching provisioning profiles for code signing!

f:id:uzuki05:20210124010608p:plain

事象

Flutter向けのCI/CDサービスの Codemagic を使っていて、 ios/Runner.xcodeproj/project.pbxprojPRODUCT_BUNDLE_IDENTIFIERjp.co.takeyuweb.app.${DEFINE_BUILD_ENV} のように変数を埋め込む形にしたところ、

== Building for iOS ==

> xcode-project use-profiles
Configure code signing settings
Searching for files matching /Users/builder/Library/MobileDevice/Provisioning Profiles/*.mobileprovision
List available code signing certificates in keychain /private/var/folders/pj/2d8_b7sn6f37c48z4jj09xk80000gn/T/build_k021qytp.keychain
Searching for files matching /Users/builder/clone/**/*.xcodeproj
Completed configuring code signing settings
Did not find matching provisioning profiles for code signing!
Generated options for exporting IPA
 - Method: ad-hoc
 - Provisioning Profiles: []
 - Signing Certificate: 
 - Signing Style: manual
 - Team Id: 
Saved export options to /Users/builder/export_options.plist

のようなエラーが出て正しくプロビジョニングプロファイルを選択できなくなりました。

続きを読む

Ngrokなどのトンネリングツールを使うときは、 default_url_options をトンネリングURLのものにしたい

f:id:uzuki05:20181215091555j:plain

ローカル開発中のRailsアプリに外部からのリクエストを届かせる必要があるとき、Ngrokは便利です。

たとえば

  • 開発中のアプリなどを実機で動かしてて、ローカルのRails APIサーバーと通信したい
  • 外部のサービスからのリクエストを受け取る必要がある

といったときです。

通常、Railsでは、url_forホスト部などはHTTPリクエストのヘッダー情報から取得して組み立ててくれるのですが、たとえば graphql-ruby のフィールドなど、コントローラー以外で Rails.application.routes.url_helpers.url_for(user) を使った場合など、 default_url_options がないとうまくいかないケースもあります。

そんなとき、いちいち config/environments/development.rb を編集するのは面倒ですし、うっかり変更したままコミットする悲しい事故も起こります。

そこでこんなコードを書きました。 こうしておけば必要なときに default_url_options を変えることができます。

# config/environments/development.rb

Rails.application.configure do

   # (省略)

  # echo "https://xxxxxxxxxxxx.jp.ngrok.io" > tmp/default_url_options.txt
  # bin/rails restart
  default_url_options_path = Rails.root.join('tmp', 'default_url_options.txt')
  if default_url_options_path.exist?
    default_url = default_url_options_path.read.strip
    uri = URI(default_url)
    config.hosts << uri.host

    Rails.application.routes.default_url_options = {
      host: uri.host,
      port: uri.port,
      protocol: uri.scheme
    }
  else
    Rails.application.routes.default_url_options = {
      host: 'localhost',
      port: 3000
    }
  end
end

Railsのアセット(ActiveStorage , Webpacker, Asset Pipeline)をCloudFront経由で配信する

f:id:uzuki05:20210121025234j:plain

業務でRailsアプリを作る場合、処理負荷的にも転送量的にも重い画像等は、CDNから配信するようにすることは、最早当たり前といって良いと思います。

最近は CDK によって簡単にCloudFront Distribution設定を維持管理でき、さらに ActiveStorage 等、Rails側の機能改善もあって以前に比べて、簡単に、レールから外れない範囲で書けるようになりました。

今回は以下のような構成で、CloudFront 経由での配信によるエッジキャッシュの利用を実現する方法について紹介します。

f:id:uzuki05:20210121011247p:plain
構成イメージ

手順

  1. CDKでCloudFrontを作成する
  2. ActiveStorage の URL を CloudFront経由のものにする
  3. Webpacker, Asset Pipeline の成果物のURLを CloudFront経由のものにする
続きを読む

Rails 6.1 の rails_storage_proxy_url でActiveStorage のリダイレクトURL問題を解決する

f:id:uzuki05:20151102155000j:plain

Rails 6.1 の新機能 rails_storage_proxy_url を使うと、ActiveStorage で添付したファイルへのリンクが署名付きURLへのリダイレクトにならず、RailsアプリのURLのままファイルをダウンロードできるようになります。

どういうこと?

ActiveStorageはこれまで、S3をバックエンドとして使った場合、S3への署名付きURL=タイムスタンプなどが付与されたURLへのリダイレクトを行ってきました。 しかしこれは扱いづらいことも少なくなく、悩みの種の1つでした。

Rails 6.1 でこの問題に対する回答が(ようやく)公式に用意されたことになります。

続きを読む

his CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.

f:id:uzuki05:20210120161627p:plain
npx cdk deploy したら発生したエラー

$ npm update aws-cdk
$ npx cdk deploy

グローバルの cdk cli を使っていなかったので npm -g ではない

ActiveStorageのダイレクトアップロードを付属のJavaScriptライブラリ以外で使う

たとえば、graphqlなどアップロード機能を提供したいとき、

  • ダイレクトアップロード用の ActiveStorage::Blob とURL等を生成
  • 結果を受け取ってレコードにファイルを添付する

をMutationで実装したいことがあります。

  1. クライアントはアップロードしようとするファイルのMD5チェックサムとファイルサイズ、MIMEタイプを1のAPIサーバに送信します。
  2. APIサーバは、受け取ったパラメータからアップロード用の情報を返します。
  3. クライアントは受け取った情報を使ってクラウドストレージへダイレクトアップロードします。アップロードが終わったらAPIサーバにアップロードしたファイルを特定できる情報を送ります。
  4. APIサーバは対象のファイルを特定しレコードに紐付けます。

試したバージョン: Rails 6.1

1. ファイルの情報を取得する

Rubyによるサンプルコードです。 実際にはクライアントで実装することになるので、JavaだったりSwiftだったりします。 ファイルの情報を何らかの方法でサーバに送信します。

file_path = "/src/spec/fixtures/files/male.jpg"
file_data = File.read(file_path)
byte_size = file_data.bytes.size # => 57937
checksum = Base64.strict_encode64(Digest::MD5.digest(file_data)) # => "0Nq1WCcyKbbw4wipYw1xag=="
content_type = Mime[File.extname(file_path).split('.').last].to_s # => "image/jpeg"

2. サーバでダイレクトアップロード用の情報を生成する

create_before_direct_upload!ActiveStorage::Blob レコードを作成します。 ActiveStorage::Blob#service_url_for_direct_upload および ActiveStorage::Blob#service_headers_for_direct_upload でダイレクトアップロードに使用するURLとHTTPヘッダーを生成することができます。

blob = ActiveStorage::Blob.create_before_direct_upload!(
  filename: "file.jpg",
  byte_size: byte_size,
  checksum: checksum,
  content_type: content_type,
  service_name: "amazon"  # 今回はS3を使う
)

# アップロード用のURLとHTTPヘッダーを生成
blob_signed_id = blob.signed_id # アップロード後の紐付けに使う署名付きURL
direct_upload_url =  blob.service_url_for_direct_upload
direct_upload_headers = blob.service_headers_for_direct_upload

アップロード用の情報には、MD5チェックサムMIMEタイプなどのデジタル署名が含まれ、一致しないファイルのアップロードを防ぐことができます。 なので、サーバ側で事前にファイルサイズやMIMEタイプなどをみてアップロード許可を出すこともできます。

pp direct_upload_url 
# => "https://your_bucket_name.s3.ap-northeast-1.amazonaws.com/b6msshsvihnanisrlfwgpab9miqk?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AK000000000000000000%2F20201219%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20201219T021918Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bcontent-type%3Bhost&X-Amz-Signature=c43651b09080ed543b1deb2f0baa8971251f06ff52360f8d31a1da7d6bc992ee"
pp direct_upload_headers 
# => {"Content-Type"=>"image/jpeg",
# "Content-MD5"=>"0Nq1WCcyKbbw4wipYw1xag==",
# "Content-Disposition"=>
 # "inline; filename=\"file.jpg\"; filename*=UTF-8''file.jpg"}
# config/storage.yml
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: <%= Rails.application.credentials.dig(:aws, :region) %>
  bucket: your_bucket_name 

3. クラウドストレージへダイレクトアップロード

Rubyによるサンプルコード。 ファイルのバイナリデータを、受け取ったURLあてに、受け取ったHTTPヘッダーと共に送ります。 一点注意が必要なのが Content-Length ヘッダーでHTTPクライアントによっては自動でつけてくれないんので、その場合は送信するバイナリのサイズ(最初にサーバに伝えたバイト数)を自分で付けないと SignatureDoesNotMatch になります。

url = URI.parse(direct_upload_url)
req = Net::HTTP::Put.new(url.request_uri)
req.initialize_http_header(direct_upload_headers)
req.body = file_data  # RubyのHTTP::Requestクラスはこれで content-length もつけてくれる
req.each_header { |k,v| p "#{k}=#{v}" }
# => "content-type=image/jpeg"
# "content-md5=0Nq1WCcyKbbw4wipYw1xag=="
# "content-disposition=inline; filename=\"file.jpg\"; filename*=UTF-8''file.jpg"
# "connection=close"
# "host=your_bucket_name .s3.ap-northeast-1.amazonaws.com"
# "content-length=57937"

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
res = http.request(req)
# => #<Net::HTTPOK 200 OK readbody=true>

4. サーバでファイルを特定しレコードに紐付ける

class User < ApplicationRecord
  has_one_attached :photo
end

blob = ActiveStorage::Blob.find_signed!(blob_signed_id)
User.find(user_id).photo.attach(blob)