FlutterでFirebase StorageにアップロードするとiOSではcontent-typeが自動で推測されない

# 気がするんだけど本当？
同じ画像をアップロードした際、iOSでは`content-type`が`application/octet-stream`になって、Androidでは`image/jpeg`になった。なぜiOSで推測されないのか気になるから追ってみました。ソースを見てきちんとそうなることを確認したかったのです。

ただ先に言っておくとiOSで推測されないのは確かなので、解決法としては自分でcontent-typeをつけるのがメジャーになるとは思います。この記事はcontent-typeがつかない問題の解決法を教えるのではなく、ソースを追って、確かに推測されてない！！！ってなるための記事です。

Flutterのdartからネイティブソースを呼び出す流れに詳しくない人は、これを読むと　こんな感じで呼び出してるんだ〜〜へーーってなれるかも

# iOS側の呼び出しを追っていく
dartの `StorageReference#putFile` の中の `_StorageFileUploadTask` の中から。たまにオーバーロードしてるメソッドとかは飛ばしてます。

ほないくで

## firebase_storage plugin dart - invokeMethod
https://github.com/flutter/plugins/blob/master/packages/firebase_storage/lib/src/upload_task.dart#L139

```dart:upload_task.dart
  @override
  Future<dynamic> _platformStart() {
    return FirebaseStorage.channel.invokeMethod( // <- method channelを使って
      'StorageReference#putFile', // <- StorageReference#putFile を蹴りたい
      <String, dynamic>{
        'app': _firebaseStorage.app?.name,
        'bucket': _firebaseStorage.storageBucket,
        'filename': _file.absolute.path,
        'path': _ref.path,
        'metadata':
            _metadata == null ? null : _buildMetadataUploadMap(_metadata),
      },
    );
  }
```

## firebase_storage plugin - create FlutterMethodChannel
https://github.com/flutter/plugins/blob/master/packages/firebase_storage/ios/Classes/FirebaseStoragePlugin.m#L28

```objective_c:FireaseStoragePlugin.m
  FlutterMethodChannel *channel = // 蹴るためにFlutterMethodChannelを作っておく
      [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_storage"
                                  binaryMessenger:[registrar messenger]];
  FLTFirebaseStoragePlugin *instance = [[FLTFirebaseStoragePlugin alloc] init];
  instance.channel = channel;
  [registrar addMethodCallDelegate:instance channel:channel];
}
```

## firebase_storage plugin - handle method call
https://github.com/flutter/plugins/blob/master/packages/firebase_storage/ios/Classes/FirebaseStoragePlugin.m#L67

```objective_c:FirebaseStoragePlugin.m
  } else if ([@"StorageReference#putFile" isEqualToString:call.method]) {
    [self putFile:call result:result]; // self.putFileを呼び出す
  }
```

## firebase_storage plugin - put file
https://github.com/flutter/plugins/blob/master/packages/firebase_storage/ios/Classes/FirebaseStoragePlugin.m#L182

```objective_c:FirebaseStoragePlugin.m
- (void)putFile:(FlutterMethodCall *)call result:(FlutterResult)result {
  NSData *data = [NSData dataWithContentsOfFile:call.arguments[@"filename"]];
  [self put:data call:call result:result]; // self.putを呼び出す
}
```

## firebase_storage plugin - put
https://github.com/flutter/plugins/blob/master/packages/firebase_storage/ios/Classes/FirebaseStoragePlugin.m#L192

```objective_c:FirebaseStoragePlugin.m
- (void)put:(NSData *)data call:(FlutterMethodCall *)call result:(FlutterResult)result {
  NSString *path = call.arguments[@"path"];
  NSDictionary *metadataDictionary = call.arguments[@"metadata"];
  FIRStorageMetadata *metadata;
  if (![metadataDictionary isEqual:[NSNull null]]) {
    metadata = [self buildMetadataFromDictionary:metadataDictionary];
  }
  FIRStorageReference *fileRef = [storage.reference child:path];
  FIRStorageUploadTask *uploadTask = [fileRef putData:data metadata:metadata]; // FIRStorageReference#putData を呼び出す
  
  // ...some procedure...
}
```

## firebase-ios-sdk - put data

**ここから、flutterのプラグインではなく、firebase-ios-sdk側のソースになります**

https://github.com/firebase/firebase-ios-sdk/blob/Storage-3.1.1/Firebase/Storage/FIRStorageReference.m#L158

```objective_c:FIRStorageReference.m

- (FIRStorageUploadTask *)putData:(NSData *)uploadData
                         metadata:(nullable FIRStorageMetadata *)metadata
                       completion:(nullable FIRStorageVoidMetadataError)completion {
  if (!metadata) {
    metadata = [[FIRStorageMetadata alloc] init];
  }

  metadata.path = _path.object;
  metadata.name = [_path.object lastPathComponent];
  FIRStorageUploadTask *task =
      [[FIRStorageUploadTask alloc] initWithReference:self  // FIRStorageUploadTaskを初期化
                                       fetcherService:_storage.fetcherServiceForApp
                                        dispatchQueue:_storage.dispatchQueue
                                                 data:uploadData // 引数がfileじゃなくてdata
                                             metadata:metadata];

  // ...some procedure...
}
```

## 元凶:firebase-ios-sdk - init FIRStorageUploadTask with `data`
https://github.com/firebase/firebase-ios-sdk/blob/Storage-3.1.1/Firebase/Storage/FIRStorageUploadTask.m#L42

```objective_c:FIRStorageUploadTask.m

- (instancetype)initWithReference:(FIRStorageReference *)reference
                   fetcherService:(GTMSessionFetcherService *)service
                    dispatchQueue:(dispatch_queue_t)queue
                             data:(NSData *)uploadData
                         metadata:(FIRStorageMetadata *)metadata {
  self = [super initWithReference:reference fetcherService:service dispatchQueue:queue];
  if (self) {
    _uploadMetadata = [metadata copy];
    _uploadData = [uploadData copy];
    _progress = [NSProgress progressWithTotalUnitCount:[_uploadData length]];

    if (!_uploadMetadata.contentType) {
      _uploadMetadata.contentType = @"application/octet-stream"; // あー!!!!!!!!!!!
    }
  }
  return self;
}
```

## ちなみに
`FIRStorageReference#putData`じゃなくて`putFile`を呼び出してると以下のソースにたどり着き、きちんと`content-type`がつく

https://github.com/firebase/firebase-ios-sdk/blob/Storage-3.1.1/Firebase/Storage/FIRStorageUploadTask.m#L59

```objective_c:FIRStorageUploadTask.m

- (instancetype)initWithReference:(FIRStorageReference *)reference
                   fetcherService:(GTMSessionFetcherService *)service
                    dispatchQueue:(dispatch_queue_t)queue
                             file:(NSURL *)fileURL // <- dataじゃなくてfile
                         metadata:(FIRStorageMetadata *)metadata {
  self = [super initWithReference:reference fetcherService:service dispatchQueue:queue];
  if (self) {
    _uploadMetadata = [metadata copy];
    _fileURL = [fileURL copy];
    _progress = [NSProgress progressWithTotalUnitCount:0];

    // イケメン
    NSString *mimeType = [FIRStorageUtils MIMETypeForExtension:[_fileURL pathExtension]];

    if (!_uploadMetadata.contentType) {
      _uploadMetadata.contentType = mimeType ?: @"application/octet-stream";
    }
  }
  return self;
}
```

## firebase docでは
https://firebase.google.com/docs/storage/ios/upload-files#add_file_metadata

> putFile: メソッドは NSURL ファイル名拡張子からコンテンツ タイプを自動的に推測します

って書いてるが、flutterのplugin自体がそっち呼んでないから推測しないんだなぁ〜

# Androidは？

力尽きた

# 雑記
firebase-ios-sdkはTags見たらそれっぽいバージョンあったからそのURL引っ張ってきたんだけど、flutterの方はそれっぽいバージョン見つけられなくてmasterになっちゃってる。バージョンとかでTag切られてないのかな？よくわかんないっす