GoでAPIサーバー建ててiosから通信してみたInHeroku

初めまして。普段NLPを触っている海老原です。今回はGoでAPIサーバーを作り、herokuへデプロイし、iosのショートカットからアクセスしてみた一連の流れと詰まった個所を記述します。

# 製作物データ

### 作ったもの
pdfのurlを受け取り、ダウンロードした後Dropboxに送信するサーバー
[githubへのリンク](https://github.com/EbichanOO/EssayForDropbox/)
[今回のサイトへのリンク](https://dl-essay-for-dropbox.herokuapp.com/)

### 製作期間
丸1日

### 制作者レベル
* チュートリアル終了レベル
* Qiita初投稿

### モチベーション
* 通信量的な問題で論文のpdf(大きい)をサーバー上でクラウドへ上げてwifi接続時に端末にインスコしたかった
* Goのチュートリアルが終わった
* 就活がやばい(アウトプットォ...)

### やったこと
* ginを使ったpost, get通信
* DropboxのAPIを使ったpdfのアップロード
* godepを使ったvendor管理
* herokuへのアップロードと起動

# 本編
## APIサーバー作成
### 環境構築
まず初めに goをインストールし、```go get github.com/tools/godep```、```go get github.com/gin-gonic/gin```を行います。godep周辺を知りたい方は[ここ](https://qiita.com/mumoshu/items/0d2f2a13c6e9fc8da2a4)を参照してください。また私の環境はwindowsなのですが、デフォルトGOPATHがわけわからん所(Go的には正解?)に通されていたので使いやすい所に設定しなおしました。GOPATHを通し、go getを実行した段階でGOPATH以下にsrc, bin, pkg...のファイル群が生成されました。ここのsrc/github.com/(自分のgithub)/(アプリ)というファイル構成で開発するのが正解っぽいです。
### ginを利用した開発
ソースコードはgithubにありますが、mainから見ていこうと思います。(vendor以下は普通gitignoreで排除するっぽい)

```go
func main() {
  router := gin.Default()
  router.GET("/", func(c *gin.Context) {
    c.String(http.StatusOK, "send token and url and filename to /load use post")
  })
  router.POST("/load", Load)
  port := os.Getenv("PORT")
  if port == "" {
    port = "3000"
  }
  router.Run(":"+port)
}
```
ginすごいですねgin.Default()とrouter.Run()さえあれば動きますよ、詳しくは[ここ](https://github.com/gin-gonic/gin)。POST通信をしているLoadが今回のメインの関数です。また、今回はherokuを利用するので空いているポートが分かりませんでした。なので空いてるポートを探して、無かったら3000番で立ち上げる様にしてあります。

```Go
func Load(c *gin.Context) {
  filename, err := get_pdf(c.PostForm("url"), c.PostForm("filename"))
  if err != nil {
    c.String(http.StatusInternalServerError, "server error")
  }

  err = send_pdf(c.PostForm("token"), filename)
  if err != nil {
    c.String(http.StatusInternalServerError, "server error")
  }

  err = del_file(filename)
  if err != nil {
    c.String(http.StatusInternalServerError, "server error")
  }
  c.String(http.StatusOK, filename+" is done")
}
```
基本的に受け取ったcontextにステータスやエラーを詰めて返しています。またここではフォーム形式の入力を必要な部分だけ次の関数へ渡しています。

```Go
func get_pdf(URL,name string) (string, error){
  client := &http.Client{}
  url := URL
  token := c.PostForm("token")
  filename := name+".pdf"

  req, err := http.NewRequest("GET", url, nil)
  if err != nil {
    return "", err
  }

  res, err := client.Do(req)
  if err != nil {
    os.Exit(1)
    return "", err
  }

  body, err := ioutil.ReadAll(res.Body)
  if err != nil {
    os.Exit(1)
    return "", err
  }

  file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, os.ModePerm)
  if err != nil {
    return "", err
  }

  defer func() {
    file.Close()
  }()

  file.Write(body)
  return filename, nil
}
```
上記は受け取ったURLを開き、ローカルにfilenameを名前として保存しています。この時ファイルはos.File形式なのですが、os.Fileをreturnすることができなかったのでローカルに保存しファイル名のみを返しています。

```Go
func send_pdf(token, filename string) (err error){
  type Send_json struct {
    Path string `json:"path"`
    Mode string `json:"mode"`
  }

  client := &http.Client{}
  url := "https://content.dropboxapi.com/2/files/upload"
  param, err := json.Marshal(Send_json{Path:"/"+filename, Mode:"add"})
  if err != nil {
    return
  }
  file, err := ioutil.ReadFile(filename)
  if err != nil {
    return
  }
  req, err := http.NewRequest("POST", url, bytes.NewBuffer(file))
  if err != nil {
    return
  }

  req.Header.Set("Authorization", "Bearer "+token)
  req.Header.Set("Dropbox-API-Arg", string(param))
  req.Header.Set("Content-Type", "application/octet-stream")

  _, err := client.Do(req)
  return err
}
```
セキュリティに自信がなかったのでトークンは随時入力を求め、保留しないようにしています。また、ここではdropboxのapiを利用してファイルをアップロードしています。golangにはdropbox用のapiライブラリがあり、それを利用すれば[簡単にアップロード等できる](https://qiita.com/smith-30/items/9312b5c384c4d4133103)のですが非公式で本家の方が信頼できるので[直接公式api](https://www.dropbox.com/developers/documentation/http/documentation#files-upload)を叩いています。apiの場合150MBまでしか送れない(?)様なので150MB以上のファイルを送信しようとする場合上手くいかないかもしれません。パラメータですが、一旦jsonにしてstringに直すというとても助長的な事をしていますがご容赦を...。[参考元](https://bithamster.blogspot.com/2017/04/golang-dropbox-api_7.html)。

```Go
func del_file(filename string) (err error){
  err = os.Remove(filename)
  return
}
```
最後にファイルを削除して終了です。
ここまでで半日です、ここからがGo感マシマシで大変でした。。

## herokuへのアップロード
始めはgithubとの連携でアップロードしていました。ですがherokuでgoを動かす為にはvendor/が必要でvendor以下には依存パッケージが含まれるのでgithub連携でなく、CLIでアップロードした方がよさそうです。[詳しくはここ](https://jp.heroku.com/go)。というわけでvendor以下を用意する為に今回はgodepを利用していきます。

## godepを利用したvendor
```godep save```をソースコードが置いてあるディレクトリで行うと、vendor/とGodeps/が生成され、依存パッケージがグローバルからそのプロジェクトディレクトリにコピーされます。Godeps.jsonをvendor以下にコピーします(これはいらなかったかも)。Procfileを作成して```web: EssayForDropbox```、go特有の過程は終了です。(Procfileのweb:は実行されるコマンドではなく、アプリディレクトリ名だという事に気づかず半ギレでやってました)

## herokuへのアップロード(続)
[ここ](https://t32k.me/mol/log/go-on-heroku/)とherokuの公式ページを参照してgitとherokuを連携した後、全てをアップロードします。この際[postman](https://www.getpostman.com/products)でテストしたのですが、H10とH13エラーが起きました。H13はサーバーが何も返さない、H10はサーバーが落ちているエラーです。H13が起きた際、入力を失敗していたのでプログラムのエラーでクラッシュしていました。またH10が起きた際は```heroku restart```でappを再起動出来るのでそれで解消できます。[できない場合](https://qiita.com/Oakbow/items/1565922ddcdea0ce9ab5)。```cannot find package "github.com/gin-gonic/gin" ```というエラーが起きていたので色々調べたのですが、どうもgitでginがファイルとして認識されていた様で```git ls-files```で分かりました。なのでいったん消した ```git rm --cached vendor/github.com/gin-gonic/gin``` 後、もう一回追加 ```git add .``` したら直りました。

## iosからの接続
今回はiosのショートカットを利用してアプリ化したいと思います。具体的にはクリップボードへコピーしたurlとファイル名(.pdf無し)、dropboxのトークンをpost通信で送信します。Dropboxのuploadトークンは[ここ](https://dropbox.github.io/dropbox-api-v2-explorer/#files_upload)。[参考ページ](http://tech-blog.rakus.co.jp/entry/20180925/iphone/shortcut/automation)を参照してPOST通信のアプリを作成しました。利用例を以下に示します。
1. pdfのurlをクリップボードにコピーする
<img width="200" alt="IMG_4833.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265021/61c2ef15-ac76-fed5-ce52-5abb2d51aa91.png">
2. ショートカットを起動する
<img width="200" alt="IMG_4832.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265021/3997c5fe-c14e-de05-7270-9926c188ddcc.png">
3. ファイルの名前を入力する
<img width="200" alt="IMG_4834.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265021/9afc6929-d578-24f8-bd31-2d5667fd3485.png">
4. Dropboxに追加されている
<img width="200" alt="IMG_4835.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265021/acbf2471-add6-b1a2-6584-61a80421a4c0.png">

### 展望
* tokenをユーザーに用意させるの心苦しいから自動取得とかやりたい
* いつでも落ちますwelcomeって感じだからもっと入力チェックとかできるようにしないと
* dockerで運用したい