Skip to content

Replacing Image Files

padams edited this page Oct 14, 2026 · 4 revisions

Replacing Image Files

A REST route gives an existing image a new file while keeping its ID, attachment page and, where the file type allows, its URLs. It is for publishing tools that keep a site's images up to date. PhotoPress also acts on replacements made with the Enable Media Replace plugin.

Requirements

  • WordPress 6.6 or later.
  • A user with upload_files who can edit the image (edit_post on the attachment). Requests use any REST authentication WordPress accepts, such as an application password.
  • The image must be an image attachment. The new file must be an image this site can display.
  • Optional: Enable Media Replace, for replacements made in the media library. See Enable Media Replace.

Using it

PhotoPress has no screen for this. A client sends the new file to:

POST /wp-json/photopress/v1/media/<id>/file

The body is the file, as for POST /wp/v2/media: the raw file with a Content-Disposition: attachment; filename="photo.jpg" header, or a multipart field named file. Other arguments go in the query string or form fields.

Argument Type What it does Default
update_references boolean Rewrites stored links to files whose names changed. true
reprocess_metadata boolean Reads the new file's metadata as on upload: image taxonomy terms, alt text and description. false leaves them as they are. true
sync_embedded boolean Gives every image block showing this image, on its own or in a gallery, the image's current alt text and caption, replacing what is there. false
photopress_gallery object Also adds the image to a post's gallery: post (required), gallery (its HTML anchor), replace. Added by the gallery module. none

The response is the image as GET /wp/v2/media/<id>?context=edit returns it, plus photopress_replaced:

Field Meaning
files Old upload-relative paths mapped to new ones, for files whose names changed.
posts_updated IDs of posts whose stored links or image blocks were rewritten.
text_updated Whether the title, caption, description or alt text changed.
metadata_reprocessed The reprocess_metadata argument.
embedded_synced IDs of posts whose image blocks were synced.

A client can find the image to replace with core's GET /wp/v2/media?search=<file name>, which searches file names.

Errors

Code Status Cause
rest_post_invalid_id 404 Not an image attachment.
rest_upload_no_data, rest_upload_no_content_disposition 400 No body, or no file name.
rest_upload_invalid_type 400 The file is not an image, judged by its contents.
rest_upload_invalid_image 400 Not an image this site can display.
photopress_replace_under_way 409 Another replacement of this image began less than 10 minutes ago and has not ended.
photopress_sizes_not_made 500 WordPress skipped sizes the new file should have. The image keeps its old file. data.sizes lists them.
photopress_replace_failed, rest_upload_unknown_error 500 The file could not be saved, backed up, moved, or given its sizes. The image keeps its old file.

How it works

replaceFile runs these steps in the request.

1. File type and name

The type is read from the file's contents with wp_check_filetype_and_ext() and wp_getimagesize() before anything is written.

New file Where it goes URLs
Same MIME type Over the original upload (the file before WordPress scaled it). Unchanged. Sizes with the same dimensions keep their names.
Another type Same folder, same base name, the new extension. If a file that is not this image's has that name, the next free name (wp_unique_filename()). The image's MIME type is updated. New. Stored links are rewritten.

Size names carry dimensions, so a new shape or size also renames the sizes.

The file is first saved next to the old ones as <name>-photopress-replacing.<ext> through wp_handle_sideload(). Upload checks and the filters other plugins hook on uploads run as for any upload, including PhotoPress's license embedding.

2. Backups, sizes, and the way back

install:

  1. A restore record is added as the post meta _photopress_replace_restore: start time, the old file, metadata and MIME type, the temporary file, and each file's backup. It is added only if none exists, so a second replace of the image at the same time gets 409.
  2. An Action Scheduler action, photopress_replace_settle, is scheduled 11 minutes later.
  3. Each of the image's files is backed up as <file>.photopress-backup. The file about to be overwritten is hard-linked. The others are copied.
  4. The new file is renamed into place in one step.
  5. WordPress makes the sizes with wp_generate_attachment_metadata(), over the old files where names match. The big image threshold applies, so a large file gets a -scaled copy. With WP Offload Media active, its upload waits for the finished set.
  6. PhotoPress checks for sizes WordPress skipped (missingSizes): sizes from intermediate_image_sizes_advanced that the file is large enough for, either way up, and that the new metadata lacks. WordPress skips a size it fails to make, as when short of memory, without an error.
  7. Success: the metadata is saved, the record is marked committed, the backups are deleted, then the record.
  8. A failure, an exception or a skipped size: the image is put back (restore). Files written for the new image are deleted, each backup is moved back, and the file, metadata and MIME type are restored.

If PHP dies midway, the record stays. The next replace of the image, or photopress_replace_settle, undoes it once 10 minutes have passed since it began. A committed replace is finished instead (endPending). Until then, other replaces of the image get 409.

3. Old files removed

Files of the image that are not in the new set are deleted: renamed sizes, a -scaled copy no longer needed, the old type's original, and copies kept by WordPress's image editor (_wp_attachment_backup_sizes, which is deleted). Their URLs, as served, go to photopress_attachment_files_removed. With WP Offload Media, that can delete them from the bucket. See Offload Media and CDN.

4. Stored links rewritten

With update_references, each old file is mapped to the new file of the same size. A size the new image lacks maps to the new file closest in width (fileReplacements).

updateReferences rewrites:

  • post content and excerpts, except revisions and attachments;
  • post meta of those posts (page builders, custom fields);
  • comment meta, term meta, user meta, and options (block widgets, site logo, plugin settings).

Matching is by file name after a slash:

  • Candidate rows contain /<name>. or /<name>-, or the JSON-escaped \/<name>. or \/<name>-.
  • A match is the file's uploads path (2024/05/photo.jpg), with / or \/ between parts. Local URLs, CDN URLs with the same path, WP Offload Media URLs with a version folder (2024/05/12345678/photo.jpg) and JSON-escaped URLs in block attributes all match. photo.jpg in another month's folder, or photo.jpg.webp, does not (code).
  • Serialized values are unserialized without classes, rewritten and serialized again. A value holding an object is left alone (code).

Rows are written with $wpdb->update(), not wp_update_post(). Posts keep their modified date, get no revision, and are not run through kses again. Their caches are cleaned.

5. Title, caption and description

Set from the new file by WordPress's upload rules, with wp_read_image_metadata() (updateText):

  • Title: the file's title. Blank or numeric, the file name.
  • Caption: the file's caption. Blank clears it.
  • Description: from photopress_attachment_description. The metadata module returns the description template's result when the description is on. Otherwise the description is left alone.

6. Metadata, licensing, CDN

photopress_attachment_file_replaced fires. PhotoPress itself acts on it in three ways.

  • The metadata module reads the new file into the image's description and alt text, unless reprocess_metadata is false. It also sets the image taxonomy terms while Image Taxonomies is on in the Metadata tab.
  • While licensing is on, the metadata module records whether the license could be written into the new file.
  • The CDN invalidator queues the image's path. See Offload Media and CDN.

7. Image blocks synced

With sync_embedded, core/image blocks whose id is this image, at any depth, get its alt text on the img and its caption as a figcaption (syncEmbedded). Only posts the requesting user can edit are changed. Link rewrites in step 4 go into every post, so images do not break. Posts are written as in step 4.

8. Caches told

_photopress_replaced_at is set to the time. photopress_post_changed fires for:

  • the image, with the URLs of its old and new files (files);
  • each post rewritten or synced (content: true);
  • when text changed, each other post showing the image (by wp-image-<id>, a link to its files, or as featured image);
  • its parent post (gallery: true), as a dynamic gallery there shows it without a link in the content.

On a site with a page cache and Proxy Cache Purge, PhotoPress purges these. See Page Cache.

WordPress integration

Hook Type Code Purpose
rest_api_init action MediaRest.php#L80 Registers the routes.
rest_namespace_index filter MediaRest.php#L81 Adds versions to GET /photopress/v1.
photopress_replace_settle Action Scheduler action MediaRest.php#L82 Undoes or finishes a replace that did not end. Argument: attachment ID. Group photopress.
upload_dir filter MediaRest.php#L310 During the sideload only: the new file goes in the image's folder.
image_make_intermediate_size filter MediaRest.php#L549 During size making only: tracks files written, for cleanup on failure.
big_image_size_threshold filter (applied) MediaRest.php#L196 Read for /media/limits.

Data stored

Name Kind Purpose
_photopress_replaced_at post meta When the image's file was last replaced (Unix time).
_photopress_replace_restore post meta The restore record of a replace under way. Deleted when it ends.
<file>.photopress-backup files Backups during a replace. Deleted when it ends.

REST routes

Route Method Permission Returns
/photopress/v1/media/<id>/file POST upload_files and edit_post on the image The image, with photopress_replaced.
/photopress/v1/media/limits GET upload_files image_size_threshold: the size images are scaled down to on upload, 0 when off (code).
/photopress/v1 GET public The namespace index. For users with upload_files, also versions: wordpress and photopress (code).

Third-party integrations

Enable Media Replace

PhotoPress uses only its enable-media-replace-upload-done action, which it fires once a replacement is done (EnableMediaReplace.php). Arguments: target URL, source URL, attachment ID. Versions before 4 pass the URLs only. PhotoPress then finds the image with attachment_url_to_postid().

For an image, PhotoPress then:

  1. fires photopress_attachment_file_replaced with the old and new upload-relative paths (when they differ) and reprocess_metadata true. The metadata is re-read and the CDN path queued, as for PhotoPress's route;
  2. fires photopress_post_changed for the image (with both URLs), for each post showing it, and for its parent (gallery: true).

PhotoPress does not rewrite links, back up files, or fire photopress_attachment_files_removed for these replacements. Enable Media Replace does its own work.

Without Enable Media Replace, the action never fires and nothing happens. PhotoPress's route does not depend on it.

WP Offload Media

While sizes are made, PhotoPress returns true from as3cf_pre_update_attachment_metadata so the finished set is uploaded once, to the same keys. See Offload Media and CDN.

Hooks PhotoPress provides

Hook Type Arguments When Code
photopress_attachment_file_replaced action int $id, string[] $replacements (old path => new path), int[] $rewritten (post IDs), array $options (reprocess_metadata) After a new file is in place and links are rewritten. Also fired for Enable Media Replace, with $rewritten empty. MediaRest.php#L388
photopress_attachment_files_removed action int $id, string[] $urls (as served) After the replacement deleted files the new file does not have. MediaRest.php#L364
photopress_attachment_description filter null $description, int $id, string $file When the text is set from the new file. Return a string to set the description. Return null to leave it. MediaRest.php#L726
photopress_replace_file_args filter array $args (route arguments by name) When the route is registered. MediaRest.php#L140
photopress_replace_file_response filter array $data, int $id, WP_REST_Request $request Before the response is returned. MediaRest.php#L457
photopress_post_changed action int $id, array $context (files, content, gallery) For the image and each post whose display changed. GalleryAdd.php#L789

Limits, quirks and known issues

  • The whole replacement, including making every size and rewriting links, runs in the one request.
  • A new file type changes the image's URLs. Links outside the tables listed in step 4, such as other sites or hard-coded theme files, are not rewritten.
  • Revisions are not rewritten.
  • sync_embedded changes only core/image blocks. Classic content keeps its alt text and captions.
  • The skipped-size check is not made when no image editor on the server handles the file's type.
  • A replace that dies holds the image for 10 minutes. Requests in that time get 409.

Related

Clone this wiki locally