Skip to content

Commit a1fad45

Browse files
authored
fix(storage,web): contentType inference for web (#18078)
* fix(storage,web): contentType inference for web * format
1 parent 904249e commit a1fad45

File tree

5 files changed

+192
-5
lines changed

5 files changed

+192
-5
lines changed

packages/firebase_storage/firebase_storage/lib/firebase_storage.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@ library firebase_storage;
88
import 'dart:async';
99
import 'dart:convert' show utf8, base64;
1010
import 'dart:io' show File;
11-
// TODO(Lyokone): remove once we bump Flutter SDK min version to 3.3
12-
// ignore: unnecessary_import
13-
import 'dart:typed_data' show Uint8List;
1411

1512
// import 'package:flutter/foundation.dart';
1613
import 'package:firebase_core/firebase_core.dart';
1714
import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'
1815
show FirebasePluginPlatform;
1916
import 'package:firebase_storage_platform_interface/firebase_storage_platform_interface.dart';
2017
import 'package:flutter/foundation.dart';
18+
import 'package:mime/mime.dart';
2119

2220
import 'src/utils.dart';
2321

packages/firebase_storage/firebase_storage/lib/src/reference.dart

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,21 +103,44 @@ class Reference {
103103
return _delegate.getData(maxSize);
104104
}
105105

106+
/// Infers the content type from the reference [name] if not already set.
107+
SettableMetadata? _withInferredContentType(SettableMetadata? metadata) {
108+
if (metadata?.contentType != null) return metadata;
109+
110+
final inferred = lookupMimeType(name);
111+
if (inferred == null) return metadata;
112+
113+
if (metadata == null) {
114+
return SettableMetadata(contentType: inferred);
115+
}
116+
117+
return SettableMetadata(
118+
cacheControl: metadata.cacheControl,
119+
contentDisposition: metadata.contentDisposition,
120+
contentEncoding: metadata.contentEncoding,
121+
contentLanguage: metadata.contentLanguage,
122+
contentType: inferred,
123+
customMetadata: metadata.customMetadata,
124+
);
125+
}
126+
106127
/// Uploads data to this reference's location.
107128
///
108129
/// Use this method to upload fixed sized data as a [Uint8List].
109130
///
110131
/// Optionally, you can also set metadata onto the uploaded object.
111132
UploadTask putData(Uint8List data, [SettableMetadata? metadata]) {
112-
return UploadTask._(storage, _delegate.putData(data, metadata));
133+
return UploadTask._(
134+
storage, _delegate.putData(data, _withInferredContentType(metadata)));
113135
}
114136

115137
/// Upload a [Blob]. Note; this is only supported on web platforms.
116138
///
117139
/// Optionally, you can also set metadata onto the uploaded object.
118140
UploadTask putBlob(dynamic blob, [SettableMetadata? metadata]) {
119141
assert(blob != null);
120-
return UploadTask._(storage, _delegate.putBlob(blob, metadata));
142+
return UploadTask._(
143+
storage, _delegate.putBlob(blob, _withInferredContentType(metadata)));
121144
}
122145

123146
/// Upload a [File] from the filesystem. The file must exist.

packages/firebase_storage/firebase_storage/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies:
2525
firebase_storage_web: ^3.11.3
2626
flutter:
2727
sdk: flutter
28+
mime: ^2.0.0
2829

2930
dev_dependencies:
3031
flutter_test:

packages/firebase_storage/firebase_storage/test/reference_test.dart

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:mockito/mockito.dart';
1616
import 'mock.dart';
1717

1818
MockReferencePlatform mockReference = MockReferencePlatform();
19+
MockReferencePlatform mockJpgReference = MockReferencePlatform();
1920
MockListResultPlatform mockListResultPlatform = MockListResultPlatform();
2021
MockUploadTaskPlatform mockUploadTaskPlatform = MockUploadTaskPlatform();
2122
MockDownloadTaskPlatform mockDownloadTaskPlatform = MockDownloadTaskPlatform();
@@ -308,6 +309,130 @@ Future<void> main() async {
308309
});
309310
});
310311

312+
group('putData() contentType inference', () {
313+
late Reference jpgRef;
314+
315+
setUp(() {
316+
when(kMockStoragePlatform.ref(any)).thenReturn(mockJpgReference);
317+
when(mockJpgReference.bucket).thenReturn(testBucket);
318+
when(mockJpgReference.fullPath).thenReturn('foo/photo.jpg');
319+
when(mockJpgReference.name).thenReturn('photo.jpg');
320+
jpgRef = storage.ref('foo/photo.jpg');
321+
});
322+
323+
test('infers contentType from ref name when no metadata', () {
324+
List<int> list = utf8.encode('hello');
325+
Uint8List data = Uint8List.fromList(list);
326+
when(mockJpgReference.putData(data, any))
327+
.thenReturn(mockUploadTaskPlatform);
328+
329+
jpgRef.putData(data);
330+
331+
final captured = verify(mockJpgReference.putData(data, captureAny))
332+
.captured
333+
.single as SettableMetadata;
334+
expect(captured.contentType, 'image/jpeg');
335+
});
336+
337+
test('infers contentType when metadata has no contentType', () {
338+
List<int> list = utf8.encode('hello');
339+
Uint8List data = Uint8List.fromList(list);
340+
when(mockJpgReference.putData(data, any))
341+
.thenReturn(mockUploadTaskPlatform);
342+
343+
jpgRef.putData(data, SettableMetadata(contentLanguage: 'en'));
344+
345+
final captured = verify(mockJpgReference.putData(data, captureAny))
346+
.captured
347+
.single as SettableMetadata;
348+
expect(captured.contentType, 'image/jpeg');
349+
expect(captured.contentLanguage, 'en');
350+
});
351+
352+
test('preserves explicit contentType', () {
353+
List<int> list = utf8.encode('hello');
354+
Uint8List data = Uint8List.fromList(list);
355+
when(mockJpgReference.putData(data, any))
356+
.thenReturn(mockUploadTaskPlatform);
357+
358+
jpgRef.putData(
359+
data, SettableMetadata(contentType: 'application/octet-stream'));
360+
361+
final captured = verify(mockJpgReference.putData(data, captureAny))
362+
.captured
363+
.single as SettableMetadata;
364+
expect(captured.contentType, 'application/octet-stream');
365+
});
366+
367+
test('preserves customMetadata when inferring contentType', () {
368+
List<int> list = utf8.encode('hello');
369+
Uint8List data = Uint8List.fromList(list);
370+
when(mockJpgReference.putData(data, any))
371+
.thenReturn(mockUploadTaskPlatform);
372+
373+
jpgRef.putData(
374+
data, SettableMetadata(customMetadata: {'activity': 'test'}));
375+
376+
final captured = verify(mockJpgReference.putData(data, captureAny))
377+
.captured
378+
.single as SettableMetadata;
379+
expect(captured.contentType, 'image/jpeg');
380+
expect(captured.customMetadata, {'activity': 'test'});
381+
});
382+
383+
test('no inference when ref has no extension', () {
384+
// Reset to the default mock with no extension
385+
when(kMockStoragePlatform.ref(any)).thenReturn(mockReference);
386+
when(mockReference.name).thenReturn(testName);
387+
final noExtRef = storage.ref();
388+
389+
List<int> list = utf8.encode('hello');
390+
Uint8List data = Uint8List.fromList(list);
391+
when(mockReference.putData(data)).thenReturn(mockUploadTaskPlatform);
392+
393+
noExtRef.putData(data);
394+
395+
verify(mockReference.putData(data));
396+
});
397+
});
398+
399+
group('putBlob() contentType inference', () {
400+
late Reference jpgRef;
401+
402+
setUp(() {
403+
when(kMockStoragePlatform.ref(any)).thenReturn(mockJpgReference);
404+
when(mockJpgReference.bucket).thenReturn(testBucket);
405+
when(mockJpgReference.fullPath).thenReturn('foo/photo.jpg');
406+
when(mockJpgReference.name).thenReturn('photo.jpg');
407+
jpgRef = storage.ref('foo/photo.jpg');
408+
});
409+
410+
test('infers contentType from ref name when no metadata', () {
411+
when(mockJpgReference.putBlob(any, any))
412+
.thenReturn(mockUploadTaskPlatform);
413+
414+
jpgRef.putBlob('blob-data');
415+
416+
final captured = verify(mockJpgReference.putBlob(any, captureAny))
417+
.captured
418+
.single as SettableMetadata;
419+
expect(captured.contentType, 'image/jpeg');
420+
});
421+
422+
test('preserves explicit contentType', () {
423+
when(mockJpgReference.putBlob(any, any))
424+
.thenReturn(mockUploadTaskPlatform);
425+
426+
jpgRef.putBlob(
427+
'blob-data', SettableMetadata(contentType: 'text/plain'));
428+
429+
final captured = verify(mockJpgReference.putBlob(any, captureAny))
430+
.captured
431+
.single as SettableMetadata;
432+
expect(captured.contentType, 'text/plain');
433+
});
434+
});
435+
311436
test('hashCode()', () {
312437
expect(testRef.hashCode, Object.hash(storage, testFullPath));
313438
});

tests/integration_test/firebase_storage/reference_e2e.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,46 @@ void setupReferenceTests() {
307307
expect(complete.metadata?.contentType, 'application/json');
308308
},
309309
);
310+
311+
test(
312+
'infers contentType from .json ref path when no contentType set',
313+
() async {
314+
final Uint8List jsonData =
315+
utf8.encode(jsonEncode({'key': 'value'}));
316+
final Reference ref =
317+
storage.ref('flutter-tests').child('flt-infer.json');
318+
final TaskSnapshot complete = await ref.putData(jsonData);
319+
expect(complete.metadata?.contentType, 'application/json');
320+
},
321+
);
322+
323+
test(
324+
'infers contentType from .txt ref path and preserves customMetadata',
325+
() async {
326+
final Uint8List txtData = utf8.encode('hello world');
327+
final Reference ref =
328+
storage.ref('flutter-tests').child('flt-infer.txt');
329+
final TaskSnapshot complete = await ref.putData(
330+
txtData,
331+
SettableMetadata(
332+
customMetadata: {'activity': 'test'},
333+
),
334+
);
335+
expect(complete.metadata?.contentType, 'text/plain');
336+
expect(complete.metadata?.customMetadata?['activity'], 'test');
337+
},
338+
);
339+
340+
test(
341+
'infers contentType from .jpg ref path when no metadata provided',
342+
() async {
343+
final Uint8List imgData = Uint8List.fromList([0xFF, 0xD8, 0xFF]);
344+
final Reference ref =
345+
storage.ref('flutter-tests').child('flt-infer.jpg');
346+
final TaskSnapshot complete = await ref.putData(imgData);
347+
expect(complete.metadata?.contentType, 'image/jpeg');
348+
},
349+
);
310350
},
311351
);
312352

0 commit comments

Comments
 (0)