FlutterからSynology NASに画像をアップロードしようとしたらつまづいた話

## はじめに
[FileStationAPI](https://global.download.synology.com/download/Document/Software/DeveloperGuide/Package/FileStation/All/enu/Synology_File_Station_API_Guide.pdf)には、ファイルアップロードする機能(SYNO.FileStation.Upload)も提供されているのですが、自作のFlutterアプリから画像ファイルのアップロードを試みたところ、少し苦戦したのでその記録です。

## httpパッケージでの試み
FlutterでREST APIにアクセスするとなると、一番有名なのが[httpパッケージ](https://pub.dev/packages/http)かと思われます。
該当のAPIはPOSTメソッドなので、httpのpostを使ってアクセスしてみます。
リクエストボディのパラメーターについては以下のようになります。
| パラメーター名 | 型 | 説明 |
|:-:|:-:|:-:|
| path | String | アップロード先のFileStationのフォルダのパス |
| create_parents | Boolean | pathで指定したフォルダの親フォルダが存在しない時に作成するか |
| overwrite | String("overwrite"か"skip") | 同名のファイルがアップロード済みの場合、置き換えるかスキップするか |
| filepart | バイナリ(何型？) | ファイルデータをバイナリ化したもの |

ではリクエストボディのデータをそれぞれ設定してアクセスしてみましょう。
```dart
final file = File('target_image.JPG');

// ファイルデータをバイナリとして読み込み
final fileData = file.readAsBytesSync();

final response = await http.post(
    Uri(
      scheme: 'http',
      host: '192.xxx.yyy.zzz',
      port: 5000,
      path: 'webapi/entry.cgi',
      queryParameters: <String, dynamic>{
        "api": "SYNO.FileStation.Upload",
        "version": "3",
        "method": "upload",
        "_sid": sid,         // ログインAPIで得られるsid
      },
    ),
    body: <String, dynamic>{
      "path": "/home",
      "create_parents": false,
      "overwrite": "overwrite",
      "filename": fileData,
    },
);
```
で、結果がこちら
```
200
{"error":{"code":401},"success":false}
```
上がHttp通信でのステータスコード、下が返却されたレスポンスボディです。
通信自体は成功してますが、内部で失敗してますね...

で、FileStationAPIにおけるエラーコード401は何を示すのかというと
![スクリーンショット 2025-01-25 2.19.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667865/b0564225-cf21-b28d-d5ae-1ed8f44a05e1.png)
（API公式リファレンスより）

「**ファイル操作において不明なエラー**」
原因分かんないじゃん笑

まあ、十中八九設定したファイルデータに問題がありそうだなと思い、他のバイナリ形式に変更してみました。FileのreadAsBytesSyncメソッドでは「Uint8List」という型になります。
Dartではこれ以外にも「Base64」や「ByteData」など様々なバイナリ形式が提供されており、それぞれ相互変換できるようになっているため、色んなデータに変換して試してみたのですが、いずれの形式でも上記エラーから解消しませんでした。
困ったなぁと...

## アップロードの成功例を探してみる
アップロードAPIが動作せず、途方に暮れていたのですが、このAPIがちゃんと動作している成功例を探すことに。
そこで見つけたのが[FileStationAPIのPythonのラッパーパッケージ](https://github.com/N4S4/synology-api)でした。
github上に上げられており、pipでインストール可能なのでこれを使用してみることにしました。
アップロード処理はfilestationクラスのupload_fileメソッドから実行できるようです。

```python
from synology_api import filestation

# FileStationのオブジェクト生成
fileStationApi = filestation.FileStation("192.xxx.yyy.zzz", "5000", "[ユーザー名]", "[パスワード]")

# アップロード処理
fileStationApi.upload_file(
    dest_path = "/home",
    file_path = "target_image.JPG",
    create_parents = False
)
```
上記コードを実行したところ...
```
Upload Progress: 100%|████████████████████████████| 7.99M/7.99M [00:03<00:00, 2.77MB/s]
```
プログレスバーがコンソール上に表示され、成功した模様ですね。
ではFileStationで確認すると
![スクリーンショット 2025-01-25 4.05.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667865/da7f7e8d-7d33-3e02-296a-d351829d0f54.png)
アップロードされてますね！
というわけで、アップロードAPIでアップロード処理はちゃんとできるみたい。

では次に、アップロード処理のソースを見ていきます。
```python:/synology_api/filestation.py(592〜597)
encoder = MultipartEncoder({
    'path': dest_path,
    'create_parents': str(create_parents).lower(),
    'overwrite': str(overwrite).lower(),
    'files': (filename, payload, 'application/octet-stream')
})
```
何やらリクエストボディ設定部分で「MultipartEncoder」なるものを使用している模様。
調べていると、どうやら画像ファイルをアップロードする際は今回のFileStationAPIに**関係なく**、基本的にContent-Typeを「**multipart/form-data**」というものにしなければならないそう。
先ほどの「MultipartEncoder」はリクエストボディの辞書型データをMultipartの形式にしてくれるもの(超ざっくり)だったようです。

## Dioの使用
ではDartでMultipartを使うにはどうすれば良いのかなと調べていると、[dioパッケージ](https://pub.dev/packages/dio)で実現できるとのこと。
こちらもhttpと同じくらい通信処理で使用されることが多いパッケージですね。
dioにはMultipartFileという、ファイルデータをMultipartにしてくれる機能が提供されているため、これを使えば実装できるはずです。
というわけで以下のように実装しました。
```dart
final dio = Dio();

// MultipartFileでファイルデータ生成
final fileData = await MultipartFile.fromFile('target_image.JPG');
final response = await dio.post(
    'http://192.xxx.yyy.zzz/webapi/entry.cgi',
    queryParameters: <String, dynamic>{
      "api": "SYNO.FileStation.Upload",
      "version": "3",
      "method": "upload",
      "_sid": sid,
    },
    // FormDataがリクエストボディに該当するためデータを設定
    data: FormData.fromMap(<String, dynamic>{
      "path": "/home",
      "create_parents": false,
      "overwrite": "overwrite",
      "filepart": fileData,
    }),
);
```
得られた結果がこちら
```
200
{"data":{"blSkip":false,"file":"target_image.JPG","pid":30875,"progress":1},"success":true}
```
httpの時と同様に上がHttp通信でのステータスコード、下が返却されたレスポンスボディです。
今回は通信と内部共に成功しているっぽいですね！

ではFileStationで確かめると
![スクリーンショット 2025-01-25 3.40.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667865/82b184ae-6d95-181f-458b-4d89631f2ce1.png)
ちゃんとアップロードされてますね！

というわけで、アップロードのAPIを使う場合はdioとMultipartFileを使用しないといけないというお話でした。