retrofit.dart icon indicating copy to clipboard operation
retrofit.dart copied to clipboard

Upload multiple files along with filenames and content-type in a single Object or even in two separate ones for all platforms

Open zjjt opened this issue 3 years ago • 1 comments

Is your feature request related to a problem? Please describe.

I ve combed the issues posted on this repo in order to find a way to upload a list of files with different extensions. I ve not been able to properly implement this in a good way. I am working on the web using the FilePicker plugin to choose the files and uploading it to a Ktor Server. While looking up solutions i have found the following ones but the generated code was not correct. I am using

retrofit: ^3.0.1+1
retrofit_generator: ^4.0.3+1
build_runner: ^2.2.0 
json_serializable: ^6.3.1

1- Having a Map<String,List<int>> to store filenames along with the list of bytes. The #433 PR seems to not be merged and part of the retrofit_generator code thus this solution fails (using ) For a signature of :

 @MultiPart()
  @POST("/subscribe")
  Future<SubscriptionResponse> subscribe(
     @Part() SubscriptionModel model,
    @Part(name: 'files[]') Map<String,List<int>> files,
  );

The generated code is

@override
  Future<SubscriptionResponse> subscribe(model, files) async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final _data = FormData.fromMap(files);
    final _result = await _dio.fetch<Map<String, dynamic>>(
        _setStreamType<SubscriptionResponse>(Options(
                method: 'POST',
                headers: _headers,
                extra: _extra,
                contentType: 'multipart/form-data')
            .compose(_dio.options, '/subscribe',
                queryParameters: queryParameters, data: _data)
            .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = SubscriptionResponse.fromJson(_result.data!);
    return value;
  }

As you can see it doesn't do anything with the model parameter and fails once reaching the server since the files are interpreted as Form items

2- Having a List<List<int>> works but the generator does not include the filename and content-type and this fails on the server which does not know how to derive the file extension from octet/stream thus it cant do any form of validation server side for an un proper file submitted. This time around the generator produces the following

final _data = FormData();
    _data.fields.add(MapEntry('model', jsonEncode(model)));
    _data.files.addAll(files.map((i) => MapEntry(
        'files',
        MultipartFile.fromBytes(
          i,
          filename: null,
        ))));

The model is mapped to a field with a json Object which the server can't process and still the issues with the filenames and content-type persists

Describe the solution you'd like

I would like to know if there is a way to make the process of uploading a or multiple different files along with other fields a little bit smoother by having either a single Object model which encompasses the list of files to upload and the data related to it(filename, mimetype etc...) or separate the model and the list of files or even just properly generate the files properties with filename and content-type as we can deal with the jsonEncoded model String server side.

Describe alternatives you've considered

To be able to upload my list of files, i was forced to create Lists of String to store the filenames and extensions and separate my model into many @Part() annotated variables.

@MultiPart()
  @POST("/subscribe")
  Future<SubscriptionResponse> subscribe(
    @Part() String offerNo,
    @Part() String companyName,
    @Part() String email,
    @Part() String? telephone,
    @Part() String? website,
    @Part() String numberOfFiles,
    @Part() List<String> filenames, 
    @Part() List<String> fileExtensions,
    @Part(fileName: "customfiles") List<List<int>> files, //forced by the server to add a filename here otherwise fails
  );

and modify (which i shouldn't have done but for my process it was a must) the generated file with

_data.files.addAll(files.asMap().entries.map((entry) => MapEntry(
        'files',
        MultipartFile.fromBytes(
          entry.value,
          filename: '${filenames[entry.key]}.${fileExtensions[entry.key]}',
        ))));

Additional context

@trevorwang Please, is there a better way to handle multiple file upload with the library ?

Regards

zjjt avatar Aug 31 '22 19:08 zjjt

I have the same issue , please consider fixing this.

parsa10 avatar Jan 10 '23 12:01 parsa10