GoogleCloudStorage(GCS)へ安全にアップロードする


##環境
* ruby歴 初心者
* rails5
* ruby 2.4.1p111

##これはなに？
GCSへのアップロードフローを[BlueGreenDeployment](http://www.publickey1.jp/blog/14/blue-green_deployment.html)に見立てて組んでみました。
本来は、切り替え役の`Router`なる存在が必要ですが、今回は`Router`なしでフローを組まざるを得ず、このような内容となっていますが了承ください。

##概要
まず、前提としてアプリはローンチ済みで
アプリは GCS のバケットに置かれているリソースを見ているという状態です。

ざっくりのフローは下記の通りです。

■STEP.1

```
└── storage
    └── main # ここにアプリが参照しているリソースが配置されている
```

■STEP.2
アップロードするファイル群を別パスにアップロード

```
└── storage
    ├── main  
    └── prepare # ここにまずはアップロード
```

■STEP.3
STEP.2 が完了したら、つぎに`main`の中身を別のパスにコピー

```
└── storage
    ├── main  
    ├── prepare
    └── tmp # mainのコピーをここに置く
```

■STEP.4
STEP.3 が完了したら、つぎに`main`を削除し、`prepare` の中身を`main`へコピー

```
└── storage
    ├── main # 削除して、prepare をここにコピーする
    ├── prepare
    └── tmp 
```

■STEP.5
STEP.4 が完了したら、`prepare`、`tmp` を削除

```
└── storage
    └── main
```

##途中で失敗したら？
それぞれ STEP で何かしらの理由で失敗しても
ゴミがのこるだけで、`main`への影響はありません。
「STEP4. の`main`を削除したあとに失敗したら結局だめじゃん」という意見もあるかと思いますが
そこは「限りなく安全に」というポリシーなので、 手動で`tmp`に残っているファイルを移動して元に戻すということになります。

※もし、この状況でよりよい方法があれば教えていただきたいです！

##実装
``` rb:publisher.rb

	# @storage_keys: すでに存在するファイル群のパスが入った配列
	# @upload_keys: これからアップロードするファイル群のパスが入った配列

    MAIN_PREFIX = "main".freeze    
    PREPARE_PREFIX = "prepare".freeze
    TMP_PREFIX = "tmp".freeze
    
    def publish
      begin
        upload_to_prepare
      rescue => e
        clean(:prepare)

        Rails.logger.error(e)
        Rails.logger.error(e.backtrace)
        raise UploadError
      end

      begin
        copy(from: :main, to: :tmp)
      rescue => e
        clean(:prepare)
        clean(:tmp)

        Rails.logger.error(e)
        Rails.logger.error(e.backtrace)
        raise CopyCleanError
      end

      begin
        clean(:main)
        copy(from: :prepare, to: :main)
        clean(:prepare)
        clean(:tmp)
      rescue => e
        clean(:main)
        clean(:prepare)
        copy(from: :tmp, to: :main)
        clean(:tmp)

        Rails.logger.error(e)
        Rails.logger.error(e.backtrace)
        raise CopyCleanError
      end
    end
    
    def copy(from:, to:)
      case from
      when :main
        from_key_prefix = MAIN_PREFIX
        keys = @storage_keys
      when :tmp
        from_key_prefix = TMP_PREFIX
        keys = @storage_keys
      when :prepare
        from_key_prefix = PREPARE_PREFIX
        keys = @upload_keys
      else
        raise "target not found"
      end

      case to
      when :main
        to_key_prefix = MAIN_PREFIX
      when :tmp
        to_key_prefix = TMP_PREFIX
      when :prepare
        to_key_prefix = PREPARE_PREFIX
      else
        raise "target not found"
      end

      keys.each do |key|
        from_key = from_key_prefix.blank? ? key : "#{from_key_prefix}/#{key}"
        file = StorageBucket.files.get(from_key)
        if file.present?
          to_key = to_key_prefix.blank? ? key : "#{to_key_prefix}/#{key}"
          copyed_file = file.copy(StorageBucket.key, to_key)

		   # 注意! 
		   # fog が用意している copy メソッドはデフォルトで公開設定にしています。
		   # もし元のファイルが非公開ファイルの場合は、下記の処理を挟まないと
		   # 公開ファイルになってしましますので気をつけて
          if file.public_url.present?
            copyed_file.public = true
            copyed_file.save
          end
        end
      end
    end

    def clean(target)
      case target
      when :main
        prefix = MAIN_PREFIX
        keys = @storage_keys
      when :tmp
        prefix = TMP_PREFIX
        keys = @storage_keys
      when :prepare
        prefix = PREPARE_PREFIX
        keys = @upload_keys
      else
        raise "target not found"
      end

      keys.each do |key|
        key = prefix.blank? ? key : "#{prefix}/#{key}"
        file = StorageBucket.files.get(key)
        if file.present?
          file.destroy
        end
      end
    end

```

##説明
リリース済みのアプリが見ているリソースの更新をかけようとして
GCSへアップロードする際に、そのまま直接更新をかけてしまっては
途中で処理がエラーした時はどうするんだ？という、サーバーのデプロイと同じような問題があがり
今回の実装に至りました。

わりとゴリゴリな実装なのと
「copy が失敗し、元に戻す処理が失敗したら結局だめじゃん」という意見もあるかと思いますが
そこは「限りなく安全に」というポリシーなので起こったらしゃーないということでm(_ _)m

※もし、この状況でよりよい方法があれば教えていただきたいです！


##学んだこと
・ストレージにおける BlueGreenDeployment の適用

##参考
・[How to list files in a Google Storage bucket with fog-google](https://stackoverflow.com/questions/36584515/how-to-list-files-in-a-google-storage-bucket-with-fog-google)
・[How do I rename a file with Fog?](https://stackoverflow.com/questions/13981069/how-do-i-rename-a-file-with-fog)