JavaScriptでfileをfetchしてFlaskで処理させる

## はじめに

これは，[SLP KBIT Advent Calendar 2018](https://qiita.com/advent-calendar/2018/slp_kbit) 3日目の記事です．
前日の記事は [こちら](https://qiita.com/ue_knnk/items/b1b0bc3affe8ee257f67)

## 今回やること
JavaScriptで画像ファイルを**/api/pics**にfetch(POST)して，
(Flaskで処理した後に)画像を**/pics**で表示させること

## 環境
Windows10, 1803
WSL, Ubuntu, 16.04.4 LTS
Python, 3.5.2
Firefox, 63.0.3

## ディレクトリ構造
```
app.py
templates/
 ┣ layout.html #ベースとなるHTML
 ┗ pic.html #layout.htmlを継承させている
static/
 ┣ js/
 ┃ ┗ main.js
 ┣ css/
 ┃ ┗ styles.css
 ┗ pic/
   ┗ アップロードされた画像ファイル
```
## 送信ボタンを押してファイルを送信する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/309105/75c6c423-f522-7f88-6fc8-4b6728054680.png)
ここで，ファイルを選択して送信すると，**/api/pics**にファイルをPOSTして，
**/pics**に画像が表示されるようにします．

```templates/pic.html
<!--画像投稿フォーム-->
<input type="file" name="img_file" id="img_file" class="col-sm-5">
<input type="submit" id="submit_btn" value="送信">

<div id="alter-album"></div>
<script src="/static/js/main.js"></script>
```

### formからPOSTしてみる
```templates/pic.html
<form id="file_post" method="post" action="/api/pics" enctype="multipart/form-data">
  <input type="file" name="img_file" id="img_file" class="col-sm-4">
  <input type="submit" id="submit_btn" value="送信">
</form>

```
とすると，POSTはできますが，**/api/pics**に移動します．
actionを使っているので当然ですね．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/309105/ead67d08-c8ff-9778-dde9-b48fdea0495e.png)

### fetchで投げてみる

```static/js/main.js
let formData = new FormData();

const upload = () => {
  const btn = document.getElementById('submit_btn'); //送信ボタン
  file = document.getElementById('img_file'); //file
  btn.disabled = true; //連打防止
  btn.value="送信中";
  fetch('/api/pics', {
    method: 'POST',
    body: formData ,
  }).then(res => res.json()
  ).then(json => {
    if(json["status"] == "false"){ //Flask側で"false"と判断されたらアラートする
      alert(json["message"])
    }
    afterPost(); //POST後の処理
  })
};

const onSelectFile = () => upload();
document.getElementById('submit_btn').addEventListener('click', onSelectFile, false);

/*--------------------------------------------*/

window.addEventListener("load", () => {
  const input = document.getElementById('img_file');
  input.addEventListener("change", () => {
    formData.append('img_file', input.files[0]);
  });
})

```
投げられたので，再読み込みします．

```static/js/main.js
function afterPost() {
  const btn = document.getElementById('submit_btn');
  btn.disabled = false;
  btn.value="送信";
  renderPictures();
}

/*------画像を読み込む------*/
function renderPictures() {
  const album = document.getElementById('alter-album');
  while (album.firstChild) album.removeChild(album.firstChild);
  getPictures().then(pictures => {
    pictures.forEach(picture => {
      let img = document.createElement('img')
      img.src = picture
      img.addEventListener("click", () => deletePicture(picture))
      album.appendChild(img)
    })
  })
}

function getPictures() {
  return fetch("/api/pics")
    .then(res => res.json())
}
```

## 送信後フォームがリセットされるようにする
このままだと，送信後も下のように送信したファイルが残ってしまいます．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/309105/115d4a55-d606-c5d9-8866-e6cc46f9e064.png)

送信後，ファイル名を消しましょう．

```js
input.value = "";
```

ファイル名はリセットされますが，送信ボタンを押すと，以前送信したファイルが送信されてしまいました．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/309105/d682a919-fbf6-3902-ac45-041f61c97540.png)

## FormDataをnewしよう
ファイルを送信したら，FormDataをnewするようにします．

```static/js/main.js
function get_func(url) {
  const input = document.getElementById('img_file');
  const btn = document.getElementById('submit_btn');
  btn.value="送信";
  input.value = "";
  formData = new FormData(); //ここでnewさせる
  fetch(url)
    .then(() => renderPictures())
}
```
これで，ファイルを送信した後にフォームがリセットされました．

## ボタンを押せないようにする
エラーを防ぐためにも，そもそもファイル名が無い場合，ボタンを押せないようにすれば良いということに気づきました．const upload内に`value = "";`のときは送信されないように記述します．

```static/js/main.js
  if (!file.value){
    return false;
  }
```

ただ，押せなくなったので，formDataをnewしなくても問題ないというわけではなく，次に述べる問題が発生した際，newしないと正常なファイルが送信できなくなってしまいます．
## 問題点
### ファイルサイズの上限
```app.py
#アップロードの上限サイズを1MBにする
app.config['MAX_CONTENT_LENGTH'] = 1*1024*1024
```
このように，上限を1MBに設定しました．

```app.py
try:
    img_file = request.files['img_file']
except RequestEntityTooLarge as err:
    return jsonify({'status': "false",
                    'message': "アップロード可能なファイルサイズは1MBまでです"})
```
fileサイズが1.3MB程度の時は，**app.py**でエラー(413)と判断してjsonをresponseしていたのですが，3MBなど超過が大きくなると，下のようにFlaskからresponseが返ってこなくなりました．
`TypeError: "NetworkError when attempting to fetch resource."`
試しに他のブラウザでも検証．

|ブラウザ     |結果                                             |
|------------|-------------------------------------------------|
|Firefox, 63.*|上限を越えていても，サイズが1MB程度の時はres有り(413)|
|Edge, 42.*   |responseが返ってくる(200)                         |
|Chrome, 70.*|responseが返ってこない                            |

検証してみた結果として，ファイルサイズの上限を500KBにして，投稿するファイルのサイズが1MB程度の時は，413のresponseが返ってきました．上限が1MBのときと同じく，ファイルサイズが3MB程度になると，responseが返ってこなくなりました．このことから，ブラウザ側のセキュリティ関連の問題かと疑ったのですが解決には至りませんでした．原因がわかり次第追記します．

### responseがとれないのでcatchする
```static/js/main.js
  fetch('/api/pics', {
    mode: 'cors',
    method: 'POST',
    body: formData ,
  }).then(res => res.json()
  ).then(json => {
    if(json["status"] == "false"){
      alert(json["message"])
    }
    afterPost();
  }).catch(err => {
      alert("ファイルサイズが1MBを超えていませんか?")
      afterPost();
  })
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/309105/52f18d67-9bf9-029a-a969-6434bad73f14.png)

とりあえず，アラートを出すことはできました．
応答が無いのは問題ですが……．

## 感想
fetchを用いたファイルの送信を理解することができました．しかし，上で述べましたエラーの原因については解明できなかったので，助言を頂ければ幸いです．

## GitHub
[Amakuchisan/pictures-warehouse](https://github.com/Amakuchisan/pictures-warehouse/tree/qiita)

## 参考
[How do I upload a file with the JS fetch API?](https://stackoverflow.com/questions/36067767/how-do-i-upload-a-file-with-the-js-fetch-api)
[FlaskでAPI作って素のJavaScriptでAPI実行する](https://qiita.com/S_SenSq/items/3b5ddf44b691c26336b1)