RailsとiOS(Swift)とAWS間をAPIモードで、データファイル保存呼び出しさせるまで④

### 前書き
初心者向けに自分なりの言葉で柔らかく書いたつもりです。非エンジニアですが、メモ目的とアウトプットの恩恵のために記事にしました。間違っているところなどありましたら、コメントをください。
あとコードにコメントが多く可読性が悪いかもしれません。そこは申し訳なく…

iosからAWSまでの一連の流れを記事にしているためかなり長くなっているので、記事を分けて書いています。

- [RailsとiOS(Swift)とAWS間をAPI通信で、データファイル保存させるまで①devise token auth編](https://qiita.com/Rafflesia/items/a80a495a8488755a4891)
- [RailsとiOS(Swift)とAWS間をAPI通信で、データファイル保存させるまで②Alamofire、Swiftyjson編 devise_token_auth(認証)のデータの送受信](https://qiita.com/Rafflesia/items/a175a15a46c10d5a8609)
- [RailsとiOS(Swift)とAWS間をAPI通信で、データファイル保存させるまで③Alamofire、Swiftyjson編 apiモードを使ってdevise_token_auth(認証)以外のデータの送受信](https://qiita.com/Rafflesia/items/2fd37cd064aad7bd085f)

③までのものがこちら
https://github.com/ToruMizuno/railswiftapi
④までのものがこちら
https://github.com/ToruMizuno/railswiftapi2

# carrierwave編


## 参考記事

- [【Rails】S3へ『CarrierWave+fog』を使って画像アップロードする方法](http://vdeep.net/rubyonrails-carrierwave-s3)
- [Railsを使って、S3にファイルをアップロードする。](http://ubw.hatenablog.com/entry/2016/07/23/212841)
- [Railsのjbuilderの書き方と便利なイディオムやメソッド](https://qiita.com/ryouzi/items/06cb0d4aa7b6527b3645)
- [rails　ユーザー画像アップロードの手順](https://qiita.com/Kaisyou/items/5add9d8b38177c45333c)
- [[Rails5] Carrierwave + S3 + CloudFrontで画像アップロードを実装する方法](https://qiita.com/onoxeve/items/c4b4cb3b77ddc775d129)
- [RailsにCloudFront噛ませたら10倍速くなった。RailsのエッジサーバとしてのCloudFrontの有用性について](https://qiita.com/kaiinui/items/49da3b22c4de0a36e143)
- [carrierwaveでS3にアップした画像に有効期限をつけると共にCloud Frontで配信するための手順](https://qiita.com/junara/items/d0b9bcea73613d32fede#_reference-b03618e30c448a5adfb7)
- [Rails(4.2) + CarrierWaveにiOSからAlamofireを用いて画像をPOSTする](https://qiita.com/moco3/items/c5a2f5c3301623565096)







## 事前準備

### (メールアドレス作成と)AWSアカウント作成

[AWS アカウント作成の流れ](https://aws.amazon.com/jp/register-flow/)
からAWSアカウントを作成するが、先にメールアドレス(Googleアカウント)の作成からしていく。
実際、複数のメールアドレスで複数のAWSアカウントを作成できてしまうので、練習でAWSを使用する場合などを考えて、メールアドレスを複数取得しておけば便利。
画像では、スマホからGoogleアカウント(メールアドレス)を取得しているが好きな方法で取得すれば良いと思う。



最初の画面
自分のアイコンから
￼<img width="375" alt="google1.png.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/9500e2f1-71ca-6db2-1a8e-37656e465ff8.png">


アカウントを追加を押す
￼<img width="375" alt="google2.png.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/07b641f5-bb14-cc84-00dc-c77713a8bffc.png">


アカウントを作成を押す
￼<img width="375" alt="google3.png.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/ff6d0f3f-9658-fa4a-4c8d-0cdc8dbd23e3.png">


適当にサクサクっと
￼<img width="375" alt="google4.png.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/56c7ff5b-7c63-4a80-a4fe-97ea78ecdd57.png">


同じ電話番号でOK
￼<img width="375" alt="google5.png.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/c8fc6daa-39e8-8d80-4545-602bf2ded775.png">


ショートメールで送られてくる数字を記入して確認を押す
!
￼<img width="375" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3234353435382f32623939306533392d306338642d653262622d396534352d3930653632363764376434642e706e67.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/8a704546-eb1b-7d54-0836-55373832dbc0.png">


生年月日も同じものでOK
￼<img width="375" alt="google7.png.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/325a506e-5ae6-773f-de6e-32e8a5a717bd.png">


スキップでOK
￼<img width="375" alt="google8.png.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/97103b5f-92b0-147f-dc29-694bd71e9829.png">


( ´Д`)y━･~~
￼<img width="375" alt="google9.png.png" src="https://qiita-image-store.s3.amazonaws.com/0/245458/b8c55cbe-ac13-309e-1649-5f95ff8c063c.png">


これらを繰り返して複数アドレスを作成しておくと、色々と便利
次にAWSアカウントを作成

最初の画面
作成したメールアドレスと考えたパスワードを記入。AWSアカウントはプロジェクト名など好きなものを記入すると良い。
￼![aws1.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/6640e58c-e300-f42f-bc03-76937a4a90c6.png)


個人で使用する場合などはパーソナルを選んで住所などの必要情報を記入。
複数回アカウントを作成する場合でも同じ情報を使用してもアカウントは作成できる。
￼![aws2.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/230f28ce-85ba-69b9-528b-bc7ee243f6ff.png)


クレジットカード情報を記入。
複数回アカウントを作成する場合でも同じ情報を使用してもアカウントは作成できる。
￼![aws3.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/411054ee-60c2-3cc8-a84f-725e9e40511b.png)


電話番号を記入して送信を押す。
電話番号に電話が鳴るので電話番号の確認がとれ、ブラウザに表示された4桁の番号を記入欄に記入
￼![aws4.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/d513b5ba-36c1-e1f3-146d-48ec17014453.png)


ベーシックプランを選択で、1年間無料でAWSのサービスが使用できる
￼![aws5.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/55be0af5-1e01-fea0-703b-5a2dbad79730.png)


次の画面で完了画面になり、コンソールへログインで登録したメールアドレスとパスワードでログインできる
￼![aws6.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/3e14f8be-428d-4106-d157-a07fb8123a53.png)









### IAMの設定とアクセスキー、シークレットアクセスキーの取得

[AWSアカウントを取得したら速攻でやっておくべき初期設定まとめ](https://qiita.com/tmknom/items/303db2d1d928db720888)
こちらの記事を参考に必須事項のIAMの設定をする
![スクリーンショット 2018-09-20 23.53.47.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/d4b7c072-dd50-f185-2ad4-30332d2fab70.png)

￼

- ルートアカウントのMAFを有効化
- 個々のIAMユーザーの作成
- グループを使用したアクセス許可の割り当て
- IAMパスワードポリシーの適用

わかりやすく言うと

- AWSアカウントを二段階認証化(通常のAWSアカウントに二段階認証アプリとか使ってサインインできるようにする)
- IAMユーザ作成(通常のAWSアカウントは使わず、このユーザーでパスワードを設定、二段階認証を設定(MFA)して、そのパスワードや二段階認証でサインインするようにする)
- IAMグループの作成(グループごとに権限を設定して、そこにユーザーを所属させる)
  - AdministratorAccess(AWSアカウントに次ぐ強力な権限)
  - AmazonS3FullAccess(S3をフルに使える)

- IAMパスワードポリシーの適用(パスワードのルールを決める)

まとめると、作成したIAMユーザーと二段階認証でサインイン(ユーザー名、パスワード、二段階認証番号)できるようにして、そのサインインしたユーザーは限定された機能しか使えないグループに所属させることにより、このIAMユーザーでAWSを使用していくように設定する。という言い方で間違いないと思う。
ここのIAMユーザーをグループに所属させた時に、アクセスキーとシークレットアクセスキーを取得しておく







### S3のバケットの作成

AWSコンソールから、サービス、S3を選択
![スクリーンショット 2018-09-20 12.17.10.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/a8eeef90-a2ec-404b-f96d-4c35c1bab839.png)

￼

バケットを作成する
￼![スクリーンショット 2018-09-20 15.05.19.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/1fee917d-1da7-85ab-7016-869c054584be.png)


バケット名に好きな名前をつける、リージョンはTokyo、作成を押すと作成される
￼![スクリーンショット 2018-09-20 15.05.47.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/519c5275-5c6c-2a9e-0e61-25352ee1a5bf.png)














## Rails側の実装




### ImageMagickの導入

```:
$ brew install imagemagick@6
```




### gem編集

```:gemfile
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'# コメントアウトを外す
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 3.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'# コメントアウトを外す


###############################
# ユーザー認証をするために追加したgem
gem 'omniauth'
gem 'devise'
gem 'devise_token_auth'
# 画像をS3に保存をするために追加したgem
# brew install imagemagick@6でimagemagickのバージョン6をインストールしないとrmagickが使えない模様
gem 'carrierwave'# , github: 'carrierwaveuploader/carrierwave'
gem 'fog-aws'
gem 'rmagick'
###############################
```






### gemのインストール

```:
$ bundle install
```






### アップローダーを作成
名前を変えて幾つでもアップローダーを作成できる

```:
$ rails g uploader Image
```






### アップローダーを編集
名前の違うアップローダーを作成した場合は、設定の違うアップの仕方ができる。
store_dirメソッド内で好きなフォルダー名を設定する

```rb:app/uploaders/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base

  # Include RMagick or MiniMagick support:
  # リサイズしたり画像形式を変更するのに必要
  include CarrierWave::RMagick# コメントを外す
  # include CarrierWave::MiniMagick

  # ストレージの設定
  # storage :file# コメント
  # storage :fog# コメントを外す
  if Rails.env.development?# 開発環境だった場合
    storage :fog# 開発が終わったらfileに戻す
  elsif Rails.env.test?# テスト環境だった場合
    storage :file
  else# それ以外の環境だった場合
    storage :fog
  end

  # iPhoneから画像投稿した際に、画像の向きがおかしい場合があるので、
  # Rmagickのauto_orientメソッドで向きを正す。
  process :fix_rotate
  def fix_rotate
    manipulate! do |img|
      img = img.auto_orient
      img = yield(img) if block_given?
      img
    end
  end

  # S3のディレクトリ名(フォルダー名)# 格納するディレクトリを指定
  def store_dir
    # #{}で文字列挿入
    # "uploads/#①{model.class.to_s.underscore}/#②{mounted_as}/#③{model.id}"# uploads/①モデル名/②カラム名/③モデルID
    "ディレクトリ名/#{model.id}"
    # "#{model.id}"# コメント
  end

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

  # デフォルト画像は1200x5000に収まるようリサイズ
  process resize_to_limit: [1200, 5000]

  # サムネイル画像だった場合
  # version :thumb do
  #   process resize_to_fill: [100, 100]
  # end

  # 許可する画像の拡張子
  def extension_whitelist
    %w(jpg jpeg gif png)
  end

  # 保存するファイルの命名規則。上のディレクトリー+このファイル名で保存される
  def filename
    # 下のトークンを作成するメソッドでファイル名を作るパターンのコード
    # "#{secure_token(10)}.#{file.extension}" if original_filename.present?
    original_filename if original_filename# 送られて来たファイル名をダイレクトに使う
  end

  # 一意となるトークンを作成。特に使わないかも
  # ファイル名で使うメソッド
  # protected
  # def secure_token(length=16)
  #   var = :"@#{mounted_as}_secure_token"
  #   model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.hex(length/2))
  # end
end
```






### ModelにUploaderを紐つける
Postのpost_imageカラムにImageUploaderを紐つける

```rb:app/models/api/post.rb
class Api::Post < ApplicationRecord

  # どのカラムにどのアップローダーを使うか
  # Postのpost_imageカラムに対して、アップローダーのImageUploaderの設定でアップする
  mount_uploader :post_image, ImageUploader

  # S3の画像を削除
  before_destroy :clean_s3

  private
  # S3の画像を削除
  def clean_s3
    image.remove!       #オリジナルの画像を削除
    image.thumb.remove! #thumb画像を削除
  rescue Excon::Errors::Error => error
    puts "Something gone wrong"
    false
  end
end
```








### carrierwave.rbの作成
IAMユーザー設定時に取得してきたアクセスキーとシークレットアクセスキーをここで使用
S3で作成したバケット名もここで使用する


```rb:config/initializers/carrierwave.rb
CarrierWave.configure do |config|
  config.fog_provider = 'fog/aws'
  config.fog_credentials = {
    #AWSに保存するために必要
    #  IAMの設定
    # アクセスキー
    # シークレットアクセスキー
    provider:              'AWS',
    # アクセスキー
    aws_access_key_id:     'アクセスキー',
    # シークレットキー
    aws_secret_access_key: 'シークレットキー',
    # Tokyo
    region:                'ap-northeast-1'
    # host: 's3.example.com',
    # endpoint: 'https://s3.example.com:8080'
  }

  # 公開・非公開の切り替え# S3のURLに直アクセスしていいか
  config.fog_public     = true

  # キャッシュをS3に保存
  # config.cache_storage = :fog
  # キャッシュの保存期間(1年)
  # config.fog_attributes = { 'Cache-Control' => "max-age=#{365.day.to_i}" }
  # キャッシュの保存場所
  # config.cache_dir = 'tmp/image-cache'

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

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


  # 環境ごとにS3のバケットを指定
  # 
  # 保存先URL
  # asset_host名/fog_directory(バケット)名/uploaderに定義したディレクトリー名/postID/送られてくるファイル名
  case Rails.env
    when 'production'
      config.fog_directory = 'バケット名'# バケット名
      config.asset_host = 'https://バケット名.s3-ap-northeast-1.amazonaws.com'
      
    when 'development'
      config.fog_directory = 'バケット名'# バケット名
      config.asset_host = 'https://バケット名.s3-ap-northeast-1.amazonaws.com'

    when 'test'
      config.fog_directory = 'バケット名'# バケット名
      config.asset_host = 'https://バケット名.s3-ap-northeast-1.amazonaws.com'
  end
end

# 日本語ファイル名の設定# 日本語入力を可能にするため。
CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/
```






## Swift側の実装


### ストーリーボードとの接続と、各種変数
画像表示用のUIImageViewと、画像関係の変数を追加

```swift:PostViewController
    @IBOutlet weak var idTextField: UITextField!
    @IBOutlet weak var imageView: UIImageView!
    
    var accesstoken: String!
    var client: String!
    var uid:String!
    var webuser: Webuser!
    
    //画像を保存する際に取得する画像名を入れる変数
    fileprivate var postImagerandomString: String!
    //画像を保存する際に取得するタイトルを入れる変数
    fileprivate var titleNamerandomString: String!
    
    var posts = [Post]()//全てのPostデータを入れる変数
    var myposts = [Post]()//自分のPostデータを入れる変数
```







### 送信部分

画像送信用のボタンを追加
PostViewController画面から各種データの送信を行う

```swift:PostViewController
//画像とpostのパラメーターを保存
    @IBAction func postImageSaveButton_Clicked(_ sender: AnyObject) {
        
        //画像名を作る
        postImagerandomString = generate(length: 30) as String
        //タイトルを作る
        titleNamerandomString = generate(length: 30) as String
        
        //画像を用意してね！
        let image = UIImage(named: "IMG_0002.JPG")
        let data = UIImageJPEGRepresentation(image!, 1.0)
        
        //画像を保存する関数を実行する
        self.createImage(postimagedata: data!, fileName: postImagerandomString)
        
    }
```







### 画像保存関数

```swift:PostViewController
    //dataには画像、wihtNameには保存するカラム名、fileNameにはランダムに作った画像名を入れる
    func createImage(postimagedata: Data, fileName: String) {
        
        //ログインやサインアップでこれらの情報を取ってこれていた場合は
        if self.accesstoken != nil && self.client != nil && self.uid != nil {
            
            let accesstoken: String = self.accesstoken//optionalをStringに変換
            let client: String = self.client//optionalをStringに変換
            let uid: String = self.uid//optionalをStringに変換
            
            //取り出したトークン情報をヘッダーに格納
            var headers: [String : String] = [:]
            headers["access-token"] = accesstoken
            headers["client"] = client
            headers["uid"] = uid
            
            print("accesstoken: \(String(describing: accesstoken))")
            print("client: \(String(describing: client))")
            print("uid: \(String(describing: uid))")
            
            Alamofire.upload(multipartFormData: { (multipartFormData) in
                //画像を送りたい場合はこれを使用*******************************
                //複数個指定可
                //dataにはData型でUIImageJPEGRepresentationなどでUIImageから変換する
                //withNameはサーバーで受け取る際、使用する名前。つまりはRails側のカラム名
                //fileNameはファイルの名前。
                //mimeTypeはファイルのフォーマット
                multipartFormData.append(postimagedata, withName: "post_image", fileName: "\(fileName).jpeg", mimeType: "image/jpeg")
                
                /*
                 //送りたいテキストがある場合はこれを使用*************************
                 //テキストをdata型に変換
                 if let data = "文字列".data(using: String.Encoding.utf8) {
                 //withNameはサーバーで受け取る際、使用する名前。つまりはRails側のカラム名
                 multipartFormData.append(data, withName: "name")
                 }
                 */
                
                //ここにRailsのコントローラーアクションへのルートを入れる
            }, to: "http://localhost:3000/api/posts", method: .post, headers: headers) { (encodingResult) in
                switch encodingResult {
                case .success(request: let upload, streamingFromDisk: _, streamFileURL: _):
                    upload.responseJSON { response in
                        //debugPrint(response)
                        print("Request: \(String(describing: response.request))")
                        print("Response: \(String(describing: response.response))")
                        print("Error: \(String(describing: response.error))")
                        
                        let json:JSON = JSON(response.result.value ?? kill)
                        
                        //保存した画像のpostのIDを取り出す。次に保存(更新)するpostデータのために必要
                        let postid = json["id"].intValue
                        print("id: \(String(describing: postid))")
                        
                        //データを保存
                        self.createNewPost(id: postid)
                        
                    }
                case .failure(let encodingError):
                    print("エラーだよ")
                    print(encodingError)
                    
                }
            }
            
        }
    }
```





### 画像を新規で保存した後なので、データの保存は更新で保存する
データの保存後、Postのパラメータセット時に画像保存先のURLをpostImageで保持させるのだが、URL作成にRails側で設定したバケット名やディレクトリー名を使用するので注意

```swift:PostViewController
    //postデータを保存する関数
    func createNewPost(id: Int) {
        
        print("画像は\(postImagerandomString)")
        print("タイトルは\(titleNamerandomString)")
        
        let accesstoken: String = self.accesstoken//optionalをStringに変換
        let client: String = self.client//optionalをStringに変換
        let uid: String = self.uid//optionalをStringに変換
        
        //取り出したトークン情報をヘッダーに格納
        var headers: [String : String] = [:]
        headers["access-token"] = accesstoken
        headers["client"] = client
        headers["uid"] = uid
        
        //パラメーターを辞書で入れていく//左辺がカラム名、右辺が値
        var params: [String: Any] = [:]
        params["web_user_id"] = self.webuser.web_user_id
        params["title_name"] = self.postImagerandomString
        //画像アップロードでcreateをしたのでデータを作成する場合はcreateしたレコードのIDにアップデートする
        
        //PUT /api/posts/:id/update_post_id(.:format)
        Alamofire.request("http://localhost:3000/api/posts/\(id)/update_post_id", method: .put, parameters: params, headers: headers).responseJSON { response in
            print("Request: \(String(describing: response.request))")
            print("Response: \(String(describing: response.response))")
            print("Error: \(String(describing: response.error))")
            
            //型を指定した定数にいれないと比較演算子が使えない
            let res: Int = (response.response?.statusCode)!
            
            //アクセスできた場合(更新ができた場合)
            if res >= 200 && res < 300 {
                print("通信に成功しました")
                print("post saved")
                
                print("Request: \(String(describing: response.request))")
                print("Response: \(String(describing: response.response))")
                print("Error: \(String(describing: response.error))")
                print("アップデートなので、レスポンスデータはありません")
                
                let newPost = Post()
                
                //パラメーターセット
                newPost.id = id
                newPost.web_user_id = self.webuser.web_user_id
                newPost.postImage = "https://バケット名.s3-ap-northeast-1.amazonaws.com/fog_directory(バケット)名/uploaderに定義したディレクトリー名/\(id)/\(self.postImagerandomString).jpeg"
                newPost.titlename = self.titleNamerandomString
                
                self.myposts.append(newPost)
                
                //アクセスできなかった場合
            } else {
                
                print("アクセスできませんでした")
                
            }
            
        }
        
    }
```








### 受信部分

画像の取得をパラメーターのpostImageのURLで取得する
index、show、mypostshowボタンで取得したpostの最後のpostのpostImageで画像を取得している

```swift:PostViewController
//画像の取得
    @IBAction func postImagegetButton_Clicked(_ sender: AnyObject) {
        
        // タイムアウトの設定
        let TIME_OUT: TimeInterval = 1 * 300
        
        let post = myposts.last!
        
        let url: String = post.postImage
        
        print("url:\(url)")
        // URLRequestの生成
        let req = URLRequest(url: NSURL(string: url)! as URL,
                             cachePolicy: .returnCacheDataElseLoad,
                             timeoutInterval: TIME_OUT)
        let conf =  URLSessionConfiguration.default
        // URLSessionの生成
        let session = URLSession(configuration: conf, delegate: nil, delegateQueue: OperationQueue.main)
        session.dataTask(with: req, completionHandler:
            { imageData, resp, err in
                // 通信結果
                if err == nil {
                    // Success
                    // 画像表示
                    self.imageView.image = UIImage(data: imageData!)!
                    self.view.addSubview(self.imageView)
                    
                }else{
                    // Error
                    
                    print("エラーです")
                    //self.imageView.image = UIImage(named: "代わりの画像")
                }
                
        }).resume()
        
    }
```

![スクリーンショット 2018-09-23 12.02.23.png.png](https://qiita-image-store.s3.amazonaws.com/0/245458/c0165598-ee84-09dc-1396-8305381aea74.png)


## AWS解約(おまけ)

最後にAWSの無料利用枠が1年なので、練習としてAWSを利用する場合などは利用料を徴収されては割に合わないので、利用料を徴収されてしまわないように停止しておきたい。

- [使っていない AWS アカウントを停止してみた](https://dev.classmethod.jp/cloud/aws/delete_unused_aws_account/)
- [Amazon S3のサービスを止めた後に、S3のオブジェクト（ファイル）にアクセスできるかどうか？](http://funasaki.hatenablog.com/entry/2012/11/22/114543)
￼