Skip to content

CMM-2427: Keep the original file name when uploading media - #23328

Merged
adalpari merged 7 commits into
trunkfrom
cmm-2427-file-names-are-not-retained-when-uploading-images
Sep 17, 2026
Merged

adalpari merged 7 commits into
trunkfrom
cmm-2427-file-names-are-not-retained-when-uploading-images

Conversation

@adalpari

@adalpari adalpari commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

TL;DR

There was a reported bug about the app adding a prefix or suffix when uploading images and videos. The name the media gets on the site comes from the file we upload, not from the title we set on it. The image optimizer and the rotation were writing their result to a new file with random digits added, and the video optimizer was naming its output after a timestamp. We were also copying shared media to the cache twice, and the second copy lost the original name and got a wp- prefix instead.

Description

Fixes CMM-2427 / #20468: media uploaded from the app arrives on the site renamed to something like wp-1710415123456938475.

The name a media item gets on the site is decided by the on-disk file name, not by MediaModel.fileName: MediaRSApiRestClient.uploadMedia builds MediaCreateParams(title, filePath) and the wordpress-rs binding has no filename parameter, so for application-password and self-hosted sites the basename of filePath is what the server sees. Any fix therefore has to rename the file itself.

Two separate things mangle that name, both in the external org.wordpress:utils library:

  1. MediaUtils.downloadExternalMedia names the cache copy after OpenableColumns.DISPLAY_NAME, falling back to generateTimeStampedFileName()wp-<currentTimeMillis>.<ext> when that lookup comes back empty — which it always does for a file:// URI, since the content resolver resolves no provider for the file scheme and returns null.
  2. ImageUtils.optimizeImage / rotateImageIfNecessary write their result with File.createTempFile(prefix, ext), which appends 10–19 random digits. Image optimization is on by default, so this hit every image upload, even when the name survived step 1. VideoOptimizer did the same thing deliberately, generating a wp-<timestamp>.mp4 name.

wp- + 13 digits + ~10 random digits is exactly the reported string.

The app walks into cause 1 by itself, by copying shared media to the cache twice. ShareIntentReceiverActivity.addLocalMediaUri copies it once and gets the name right, then forwards a file:// URI. MediaBrowserActivity.uploadList passes that to WPMediaUtils.fetchMedia, whose MediaUtils.isInMediaStore() guard only matches content://media/ — so the file:// URI falls through and is copied again, and that second copy is where the name is lost. No exotic content provider is needed: sharing a perfectly named local photo into the app reproduced it.

This PR fixes both causes (and the video equivalent):

  • WPMediaUtils.fetchMedia(): returns a file:// URI as it is instead of downloading it again, since it already points at a local copy. That removes the second copy, so the name the share activity resolved is the one that gets uploaded — and saves a redundant copy of every shared file. A file:// URI whose file is missing still takes the download path, so its failure is still reported the way it is today.
  • MediaFileNameUtils (new, Kotlin): combines the original base name with the extension of the processed file, taken verbatim. ImageUtils only writes PNG when it managed to detect that format and writes JPEG otherwise, leaving a bare trailing dot when detection fails — which it does for names containing spaces, the case the existing FluxCUtils.mediaModelFromLocalUri workaround comments on. Reusing the original extension there would label JPEG bytes as a PNG, so the missing extension is left missing and repaired from the mime type by FluxCUtils, exactly as it already is today. Covered by unit tests.
  • WPMediaUtils: getOptimizedMedia() and fixOrientationIssue() now move their result into cacheDir/processed-media/<counter>/, one directory per file, so the original name can be restored verbatim without a collision-dodging suffix. No-op when the util returned the input path unchanged (gif, unreadable dimensions, optimization disabled); falls back to the temp path if the rename fails.
  • VideoOptimizer: names the optimized video after the source media (clip.movclip.mp4) instead of wp-<timestamp>.mp4, writing it through the same helper. Keeps the timestamped name as a fallback when the media has no file name.
  • WPMediaUtils.deleteOldProcessedMedia(), called from AppInitializer: nothing else prunes that new directory — ImageEditorInitializer only handles cache/media-editing and WordPressDB.clearEmptyCacheFiles only removes zero-length files from the cache root, non-recursively — and a failed video optimization leaves its output behind. Entries older than a week are now deleted at app init, matching the image editor's retention. A week is deliberately generous so it can't race an upload being retried across restarts.

Not covered

Cause 1 is only fixed where the app caused it. A content provider that genuinely reports no DISPLAY_NAME still leaves no name to keep, so media coming from one is still called wp-1710415123456.jpg. Removing that last fallback needs a change in WordPress-Utils-Android.

downloadExternalMedia also dodges collisions in the cache root by appending -1, -2, ... to the name. Now that the share activity's copy is the one being uploaded, re-sharing a file whose name is still in the cache uploads it as photo-1.jpg. That is pre-existing behaviour and matches what the server does with duplicate names, so it is left alone.

VideoOptimizerBase has the identical defect but has no subclasses (only VideoOptimizer is instantiated, from MediaUploadHandler), so it was left alone as dead code.

Testing instructions

Image optimization is on by default; leave it enabled (Me → App Settings → Optimize images).

Upload from the Media library:

  1. In Google Photos (or Files), note the exact file name of a photo, e.g. PXL_20240314_120000.jpg.
  2. Open My Site → Media and tap + → Choose file, then pick that photo.
  3. Wait for the upload to finish and tap the item to open its details.
  • Verify the file name matches the original, with no digits appended and no wp- prefix.

Share into the app (the wp- prefix):

  1. Open Google Photos or Files, pick a photo whose name you know, and share it to Jetpack/WordPress.
  2. Choose a site and tap Add to media library.
  3. Open My Site → Media and tap the new item.
  • Verify the file name matches the original, rather than wp- followed by digits.

Upload from the post editor:

  1. Create a new post and insert an image from the device.
  2. Publish or save the post, then open My Site → Media.
  • Verify the new item kept its original file name.

File names with spaces (the case where format detection fails):

  1. Rename a photo on the device to something containing spaces, e.g. my holiday photo.jpg.
  2. Upload it from the Media library.
  • Verify the name is retained and the extension is still .jpg (this used to be dropped by path resolution).
  1. Repeat with a PNG whose name contains spaces, e.g. my holiday photo.png.
  • Verify the uploaded file is a working image that the site accepts, rather than JPEG data labelled .png.

Video optimization:

  1. Enable Me → App Settings → Optimize videos.
  2. Upload a video large enough to actually shrink when optimized.
  • Verify the uploaded video keeps its original name with an .mp4 extension.

Image optimization and rotation write their output with File.createTempFile,
which appends random digits to the name, and the uploaded media is named after
the file we send to the server. Move the processed file to a directory of its
own so it can keep the name of the file it was created from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dangermattic

dangermattic commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@wpmobilebot

wpmobilebot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr23328-c104cfc
Build Number1498
Application IDcom.jetpack.android.prealpha
Commitc104cfc
Installation URL65t0j2pio6m6o
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr23328-c104cfc
Build Number1498
Application IDorg.wordpress.android.prealpha
Commitc104cfc
Installation URL397pm822qijmg
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 13.51351% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.91%. Comparing base (ef6c59c) to head (c104cfc).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
.../java/org/wordpress/android/util/WPMediaUtils.java 2.00% 49 Missing ⚠️
...g/wordpress/android/ui/uploads/VideoOptimizer.java 0.00% 9 Missing ⚠️
.../main/java/org/wordpress/android/AppInitializer.kt 0.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #23328      +/-   ##
==========================================
- Coverage   37.92%   37.91%   -0.02%     
==========================================
  Files        2270     2271       +1     
  Lines      127515   127584      +69     
  Branches    17927    17950      +23     
==========================================
+ Hits        48366    48376      +10     
- Misses      75150    75209      +59     
  Partials     3999     3999              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

adalpari and others added 3 commits September 15, 2026 10:42
Reusing the original extension when the processing step produced none would
label JPEG bytes as a PNG, since those steps only write PNG when they managed
to detect that format. Keep the extension they produced so the mime based
repair in FluxCUtils runs as before, and delete the processed media left behind
by previous sessions, which nothing else cleans up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ShareIntentReceiverActivity already copies the shared media to the cache,
naming it after the provider's display name, and forwards a file:// URI.
MediaBrowserActivity.uploadList then passes that URI to fetchMedia(), whose
isInMediaStore() guard only matches content://media/, so the file:// URI falls
through and is copied a second time. That copy reads its name from
OpenableColumns.DISPLAY_NAME, and querying the content resolver for a file://
URI resolves no provider and returns null, so the name drops to the
wp-<timestamp> fallback. It is the app, not an exotic content provider, that
renames media shared into it.

Return a file:// URI as it is instead, so the name the share activity resolved
is the one that gets uploaded. A URI whose file is missing still takes the
download path, so its failure is reported the way it is today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adalpari
adalpari marked this pull request as ready for review September 16, 2026 09:45
adalpari and others added 2 commits September 16, 2026 12:09
The cropped image is now uploaded straight from disk, so the fixed
cacheDir path meant a second icon change overwrote the bytes the
previous upload was still streaming.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The purge only knew about the file age, so it dropped the only local
copy of media queued offline or failing to upload, leaving it with
nothing left to send.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adalpari
adalpari requested a review from nbradbury September 16, 2026 10:33

@nbradbury nbradbury left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adalpari This looks good and I'll approve it. Claude located some low priority findings, leaving it to you to determine if any need to be addressed. :shipit:

@adalpari
adalpari enabled auto-merge (squash) September 17, 2026 12:35
@adalpari
adalpari merged commit dcac639 into trunk Sep 17, 2026
21 of 23 checks passed
@adalpari
adalpari deleted the cmm-2427-file-names-are-not-retained-when-uploading-images branch September 17, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants