Repository navigation
Give downloaded attachments a file extension #101711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
811a448
Give downloaded attachments a file extension
MelvinBot 68c430e
Reword comments to satisfy the spell checker
MelvinBot cef8a7c
Merge remote-tracking branch 'origin/main' into claude-attachmentDown…
MelvinBot 0c7a4e8
Pass fileName to the inline video player so its Download uses the rea…
MelvinBot d0dc5d8
Detect the real MIME type from the file header for extensionless octe…
MelvinBot ac2a3fe
Merge remote-tracking branch 'origin/main' into claude-attachmentDown…
MelvinBot 8428a11
Merge remote-tracking branch 'origin/main' into claude-attachmentDown…
MelvinBot 11a7229
Fix: Oxfmt formatting and add spellcheck words for attachment MIME de…
MelvinBot 3cee69c
Split the file-header MIME sniffer into Android and iOS versions
MelvinBot 9252216
Merge remote-tracking branch 'origin/main' into claude-attachmentDown…
MelvinBot 8620e57
Use words instead of an arrow in the getExtensionFromMimeType comment
MelvinBot 165cbac
Log a warning when the file header can't be read instead of ignoring …
MelvinBot a23340c
Map QuickTime to mov and only treat allowlisted ftyp brands as MP4
MelvinBot 48d7eac
Merge remote-tracking branch 'origin/main' into claude-attachmentDown…
MelvinBot 5b7d424
Merge remote-tracking branch 'origin/main' into claude-attachmentDown…
MelvinBot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
src/libs/getMimeTypeFromFileHeader/getMimeTypeFromHeaderBytes.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // cspell:ignore ftyp isom mmp4 | ||
|
|
||
| /** Number of leading bytes read from a file, enough to cover every signature below. */ | ||
| const FILE_HEADER_LENGTH = 32; | ||
|
|
||
| /** | ||
| * ISO base media brands known to be MP4 videos. Any other brand returns no type, because image formats | ||
| * such as HEIC and AVIF share the `ftyp` box with MP4, and a wrong extension is worse than none. HEIC in | ||
| * particular must keep its extensionless name so the HEIC conversion flow isn't bypassed. | ||
| */ | ||
| const MP4_FTYP_BRANDS = new Set(['isom', 'iso2', 'iso3', 'iso4', 'iso5', 'iso6', 'mp41', 'mp42', 'avc1', 'dash', 'M4V ', 'mmp4']); | ||
|
|
||
| const AUDIO_FTYP_BRANDS = new Set(['M4A ', 'M4B ']); | ||
|
|
||
| function hasBytes(bytes: Uint8Array, signature: number[], offset = 0): boolean { | ||
| return signature.every((byte, index) => bytes[offset + index] === byte); | ||
| } | ||
|
|
||
| function readASCII(bytes: Uint8Array, offset: number, length: number): string { | ||
| return String.fromCharCode(...bytes.subarray(offset, offset + length)); | ||
| } | ||
|
|
||
| function getMimeTypeFromFtypBrand(brand: string): string | undefined { | ||
| if (MP4_FTYP_BRANDS.has(brand)) { | ||
| return 'video/mp4'; | ||
| } | ||
| if (AUDIO_FTYP_BRANDS.has(brand)) { | ||
| return 'audio/mp4'; | ||
| } | ||
| if (brand === 'qt ') { | ||
| return 'video/quicktime'; | ||
| } | ||
| if (brand.startsWith('3gp')) { | ||
| return 'video/3gpp'; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Identifies a file's MIME type from its leading bytes. Used when a file has neither an extension nor a | ||
| * meaningful MIME type, such as an extensionless file picked through the Android document picker. | ||
| * Returns undefined when no known signature matches. | ||
| */ | ||
| function getMimeTypeFromHeaderBytes(bytes: Uint8Array): string | undefined { | ||
| if (readASCII(bytes, 4, 4) === 'ftyp') { | ||
| return getMimeTypeFromFtypBrand(readASCII(bytes, 8, 4)); | ||
| } | ||
| if (hasBytes(bytes, [0x1a, 0x45, 0xdf, 0xa3])) { | ||
| return 'video/webm'; | ||
| } | ||
| if (hasBytes(bytes, [0xff, 0xd8, 0xff])) { | ||
| return 'image/jpeg'; | ||
| } | ||
| if (hasBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) { | ||
| return 'image/png'; | ||
| } | ||
| if (readASCII(bytes, 0, 4) === 'GIF8') { | ||
| return 'image/gif'; | ||
| } | ||
| if (readASCII(bytes, 0, 4) === '%PDF') { | ||
| return 'application/pdf'; | ||
| } | ||
| if (readASCII(bytes, 0, 4) === 'RIFF' && readASCII(bytes, 8, 4) === 'WEBP') { | ||
| return 'image/webp'; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| export default getMimeTypeFromHeaderBytes; | ||
| export {FILE_HEADER_LENGTH}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import fileURIToPath from '@libs/fileURIToPath'; | ||
| import Log from '@libs/Log'; | ||
|
|
||
| import type {FileObject} from '@src/types/utils/Attachment'; | ||
|
|
||
| import {Buffer} from 'buffer'; | ||
| import RNFS from 'react-native-fs'; | ||
|
|
||
| import getMimeTypeFromHeaderBytes, {FILE_HEADER_LENGTH} from './getMimeTypeFromHeaderBytes'; | ||
|
|
||
| /** | ||
| * Reads only the first bytes of a picked file from its URI and identifies its MIME type from them. | ||
| * RNFS reads just the requested byte range, so this stays cheap for large videos. | ||
| * Resolves undefined when the file can't be read or no known signature matches. | ||
| */ | ||
| async function getMimeTypeFromFileHeader(file: FileObject): Promise<string | undefined> { | ||
| if (typeof file.uri !== 'string') { | ||
| return undefined; | ||
| } | ||
| try { | ||
| const header = await RNFS.read(fileURIToPath(file.uri), FILE_HEADER_LENGTH, 0, 'base64'); | ||
| return getMimeTypeFromHeaderBytes(Buffer.from(header, 'base64')); | ||
| } catch (error) { | ||
| Log.warn('[getMimeTypeFromFileHeader] Could not read file header', {error: error instanceof Error ? error.message : String(error)}); | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| export default getMimeTypeFromFileHeader; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
data-nameis absentFor received video attachments whose HTML is the normal
<video …>clip.mp4</video>form withoutdata-name, this falls back to the storage URL rather than the element text. The shared attachment parser explicitly extracts video filenames from that text (getAttachmentDetails), so attachments backed by extensionless storage keys—the case this change targets—will still be passed tofileDownloadwith no extension. Extract the video node's text child before falling back togetFileName(sourceURL).Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid point but this follows the same pattern that already exists at extractAttachments.ts:54, and the current scope is only to fix view/downloads going forward per this confirmation. The wrong-download-name issue also reproduces on latest
main, so we should track it in a separate issue.