Skip to content

Commit 7acac90

Browse files
chrisbobbegnprice
authored andcommitted
compose: Include content-type in image-upload requests
Fixes: zulip#829
1 parent 8aaa944 commit 7acac90

File tree

2 files changed

+55
-5
lines changed

2 files changed

+55
-5
lines changed

lib/widgets/compose_box.dart

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import 'dart:math';
2+
13
import 'package:app_settings/app_settings.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter/services.dart';
46
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
7+
import 'package:mime/mime.dart';
58

69
import '../api/exception.dart';
710
import '../api/model/model.dart';
@@ -421,11 +424,17 @@ class _FixedDestinationContentInput extends StatelessWidget {
421424
/// A convenience class to represent data from the generic file picker,
422425
/// the media library, and the camera, in a single form.
423426
class _File {
424-
_File({required this.content, required this.length, required this.filename});
427+
_File({
428+
required this.content,
429+
required this.length,
430+
required this.filename,
431+
required this.mimeType,
432+
});
425433

426434
final Stream<List<int>> content;
427435
final int length;
428436
final String filename;
437+
final String? mimeType;
429438
}
430439

431440
Future<void> _uploadFiles({
@@ -472,14 +481,14 @@ Future<void> _uploadFiles({
472481
}
473482

474483
for (final (tag, file) in uploadsInProgress) {
475-
final _File(:content, :length, :filename) = file;
484+
final _File(:content, :length, :filename, :mimeType) = file;
476485
Uri? url;
477486
try {
478487
final result = await uploadFile(store.connection,
479488
content: content,
480489
length: length,
481490
filename: filename,
482-
contentType: null, // TODO(#829)
491+
contentType: mimeType,
483492
);
484493
url = Uri.parse(result.uri);
485494
} catch (e) {
@@ -577,7 +586,22 @@ Future<Iterable<_File>> _getFilePickerFiles(BuildContext context, FileType type)
577586

578587
return result.files.map((f) {
579588
assert(f.readStream != null); // We passed `withReadStream: true` to pickFiles.
580-
return _File(content: f.readStream!, length: f.size, filename: f.name);
589+
final mimeType = lookupMimeType(
590+
// Seems like the path shouldn't be required; we still want to look for
591+
// matches on `headerBytes`. Thankfully we can still do that, by calling
592+
// lookupMimeType with the empty string as the path. That's a value that
593+
// doesn't map to any particular type, so the path will be effectively
594+
// ignored, as desired. Upstream comment:
595+
// https://github.com/dart-lang/mime/issues/11#issuecomment-2246824452
596+
f.path ?? '',
597+
headerBytes: f.bytes?.take(defaultMagicNumbersMaxLength).toList(),
598+
);
599+
return _File(
600+
content: f.readStream!,
601+
length: f.size,
602+
filename: f.name,
603+
mimeType: mimeType,
604+
);
581605
});
582606
}
583607

@@ -662,7 +686,27 @@ class _AttachFromCameraButton extends _AttachUploadsButton {
662686
}
663687
final length = await result.length();
664688

665-
return [_File(content: result.openRead(), length: length, filename: result.name)];
689+
List<int>? headerBytes;
690+
try {
691+
headerBytes = await result.openRead(
692+
0,
693+
// Despite its dartdoc, [XFile.openRead] can throw if `end` is greater
694+
// than the file's length. We can *probably* trust our `length` to be
695+
// accurate, but it's nontrivial to verify. If it's inaccurate, we'd
696+
// rather sacrifice this part of the MIME lookup than throw the whole
697+
// upload. So, the try/catch.
698+
min(defaultMagicNumbersMaxLength, length)
699+
).expand((l) => l).toList();
700+
} catch (e) {
701+
// TODO(log)
702+
}
703+
return [_File(
704+
content: result.openRead(),
705+
length: length,
706+
filename: result.name,
707+
mimeType: result.mimeType
708+
?? lookupMimeType(result.path, headerBytes: headerBytes),
709+
)];
666710
}
667711
}
668712

test/widgets/compose_box_test.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ void main() {
270270

271271
testBinding.pickFilesResult = FilePickerResult([PlatformFile(
272272
readStream: Stream.fromIterable(['asdf'.codeUnits]),
273+
// TODO test inference of MIME type from initial bytes, when
274+
// it can't be inferred from path
273275
path: '/private/var/mobile/Containers/Data/Application/foo/tmp/image.jpg',
274276
name: 'image.jpg',
275277
size: 12345,
@@ -295,6 +297,7 @@ void main() {
295297
..field.equals('file')
296298
..length.equals(12345)
297299
..filename.equals('image.jpg')
300+
..contentType.asString.equals('image/jpeg')
298301
..has<Future<List<int>>>((f) => f.finalize().toBytes(), 'contents')
299302
.completes((it) => it.deepEquals(['asdf'.codeUnits].expand((l) => l)))
300303
);
@@ -322,6 +325,8 @@ void main() {
322325
checkAppearsLoading(tester, false);
323326

324327
testBinding.pickImageResult = XFile.fromData(
328+
// TODO test inference of MIME type when it's missing here
329+
mimeType: 'image/jpeg',
325330
utf8.encode('asdf'),
326331
name: 'image.jpg',
327332
length: 12345,
@@ -348,6 +353,7 @@ void main() {
348353
..field.equals('file')
349354
..length.equals(12345)
350355
..filename.equals('image.jpg')
356+
..contentType.asString.equals('image/jpeg')
351357
..has<Future<List<int>>>((f) => f.finalize().toBytes(), 'contents')
352358
.completes((it) => it.deepEquals(['asdf'.codeUnits].expand((l) => l)))
353359
);

0 commit comments

Comments
 (0)