RubyXLを使ってExcelを編集してクライアントに返す

railsでエクセルを編集してクライアントに返そうとしたときのやり方を残しておきます。

## 作るもの
1. クライアントがエクセルファイルをアップロードする
2. サーバが受け取ったエクセルに何かしらの変更を加える
3. クライアントが変更されたファイルをダウンロードする

といった動きになる機能をRailsで作ります。
[出来上がったソースコードはこちら](https://github.com/ytnk531/excel-example)


## Excelの編集
RubyXLというgemを使います。

https://github.com/weshatheleopard/rubyXL

他にもエクセルを開けるgemはあるのですが、

- Roo: 読み込みのみ
- AXSLX: 新規作成のみ

という感じで用途が絞られています。
今回は、編集ができるRubyXLを使います。

単体だと、以下のような感じで使えます。

```ruby

require 'rubyXL'

# ファイルを読み込んでRubyXL::Workbookにデシリアライズ
workbook = RubyXL::Parser.parse("path/to/Excel/file.xlsx")
# ブック→シート→行→セルという構造になっている
worksheet1 = workbook[0]
row1 = worksheet1[0]
cell1 = row1[0]
# 書き込むときはWorksheetのメソッドが使える
worksheet1.add_cell 0, 0, 'changed'
# 保存
workbook.save
```

## rails new
railsプロジェクトを作ります。
今回は横着してモデルを作らないので、ActiveRecordなどいらないモジュールを生成しないようオプションを付けます。

```
rails new -MOCJ
```

## ルーティング
excelリソースへのルートを作ります。
加えて、ルートをexcelsにします。

下記とおりルーティングされます

- GET / -> ExcelsController#show
- GET /excels -> ExcelsController#show
- POST /excels -> ExcelsController#create

GET /excelsでファイルを受け取るフォームを表示し、POST /excelsでサーバへのファイルアップロードと編集後ファイルの送信を行います。

```ruby
Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root to: "excels#show"
  resource :excel, only: [:show, :create]
end
```

https://github.com/ytnk531/excel-example/blob/master/config/routes.rb

## コントローラ
showメソッドでは何もせず、ページを表示するだけです。
createにはエクセルファイルが送られて来るので、ファイルの編集とクライアントへの送信を行います。

```ruby
class ExcelsController < ApplicationController
 def show
 end

 def create
   file = params[:file]
   # ファイルを開く
   workbook = RubyXL::Parser.parse file.path
   # 編集する
   workbook[0].add_cell 0, 0, 'changed'
   # 編集したファイルを送る
   send_data workbook.stream.string, type: file.content_type, filename: 'modified.xlsx'
 end
end
```

アップロードされたファイルは[UploadedFile](https://edgeapi.rubyonrails.org/classes/ActionDispatch/Http/UploadedFile.html)オブジェクトとして受け取ることができます。
RubyXL::Parser.parseはFileオブジェクトではなくパスを引数に取るので、アップロードされたファイルのパスを指定してRubyXL::Workbookオブジェクトに変換します。
アップロードされたファイルは、tempfileとして保存されていて、
UploadedFile#pathでこのtempfileのパスを取得できます。

編集したファイルは、send_dataメソッドを使って送信します。

データとして渡すのは、Workbook#streamで取得できるバイナリのストリームを、Stream#stringで文字列にしたものです。
コンテントタイプを指定する必要がありますが、UploadedFileは受信したときのContent-Typeを記録しているので、同じものを指定しておけばいいでしょう。

https://github.com/ytnk531/excel-example/blob/master/app/controllers/excels_controller.rb



## ビュー
エクセルファイルをアップロードするためのビューを作ります。

```erb
<%= form_tag({action: :create}, multipart: true) do %>
  <div>
  <%= file_field_tag :file %>
  </div>
  <div>
  <%= submit_tag "Send" %>
  </div>
<% end %>
```
https://github.com/ytnk531/excel-example/blob/master/app/views/excels/show.html.erb

ファイル選択と送信ができます。
送信したファイルはExcelsController#createで処理します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58411/52d38d1a-5d58-9f6e-8b52-29dba3447b99.png)

## 試してみる
このようなEXCELファイルを用意し、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58411/e3f6512e-0ffc-c0f1-d946-87dbde35de0b.png)

送ってみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58411/d2bd63e8-4d6a-c551-046b-da8a72240ca5.png)

編集されたファイルのダウンロードが始まります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58411/c28c5a34-c15c-6511-ec8e-f55a801a2cf8.png)

開いてみます。期待通り編集されたエクセルをダウンロードできました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58411/96549ea9-f3d8-fe50-59b2-7d456f4f27bd.png)

## まとめ
- RubyXLでエクセルの編集ができる
- アップロードされたファイルはUploadedFileオブジェクトに記録される
- RubyXLで保存したワークブックは、保存しなくてもデータ送信できる

UploadedFileの存在を知らなかったので、うまくいくかわからなかったのですが、うまくrailsと一緒に使うことができました。