immich
immich copied to clipboard
feat(server, web): API and UI to replace asset data without changing record id
A new API to update an existing asset:
PUT /api/assets/:id/upload
The asset will continue to have the same id and any album memberships. Only the asset data will be updated. The existing asset data will be copied into a new asset (with a new id) and immediately put into the trash. Reused the same parameters as POST /api/asset that pertain to the asset binary itself.
This is to support the Lightroom workflow using the Immich Publisher plugin: (https://github.com/midzelis/mi.Immich.Publisher) Lightroom is a non-destructive editor. If you make edits to an image that has been published to immich, lightroom will attempt to send the new updates to the destination. Immich didn't support updating existing pictures until now.
Since the asset thumbnails are served with a very long cache, updating the asset in this way needs a way to tell the client to ignore the cache. Easiest way to do this is with a cache-buster query param. This optional parameter is set to the asset checksum, which changes when the asset it updated.
Algorithm:
Retrieve the existing asset
Retrieve the existing livePhotoAsset (if present)
Update or Create the existing livePhotoAsset with new livePhotoAsset (if present)
Update the existing asset with uploaded file path, and new livePhotoAsset (if present)
"Clone" the existing asset (copy all properties to a new asset, but with a newid), run the standard jobs, except don't inform client of a new successful upload, and then immediately trash it.
This adds a new api:
PUT /assets/:id/upload
It accepts the following properties, declared in a new UpdateAssetDataDto which differs slightly from CreateAssetDto in that it does not allow you to specify the albumId, isFavorite, isArchived, isVisible, isOffline, isReadonly parameters, since these properties are taken from the existing record.
assetData is always required. livePhotoData is optional, and sidecarData is optional.
If an existing asset has livePhotoData or sidecarData, and the request does not contain a new livePhotoData or sidecarData - these fields are removed from the existing asset record. The thought process behind this is that the livePhotoData is derived from the asset, and if you are replacing the main data, it doesn't make sense to keep the livePhotoData.
Note
Nothing is deleted in any case. A new asset is created as a backup using the original photo data, livePhotoData, and sidecar, if present.
Related artifacts such as: stack information, faces, smart search data, favorite status, etc are not modified by the replace action. This information is also NOT copied to the backup asset. This means you can not perfectly undo this action if you "restore" the backup copy. The backup is really just a safety measure. You'd be better off downloading the backup copy, and replacing the original again.
Job behavior changes:
A new enum value was added to the source property of IEntityJob. In addition to upload and sidecar-write - there is a clone value, which represents that an asset record was copied into a backup record.
The Jobs execution pattern for a new asset is as follows: An update of the existing record will perform exactly the same steps, so that the existing record has new thumbnails, faces, indexes, etc.
METADATA_EXTRACTION
LINK_LIVE_PHOTOS
STORAGE_TEMPLATE_MIGRATION_SINGLE
GENERATE_PREVIEW
FACE_DETECTION
GENERATE_THUMBNAIL (send UPLOAD_SUCCESS to client)
GENERATE_THUMBHASH
SMART_SEARCH
VIDEO_CONVERSION
FACE_DETECTION
For the backup (the clone), since its being trashed, we can skip some of these steps, so for the cloned asset, it looks like this:
METADATA_EXTRACTION
LINK_LIVE_PHOTOS
STORAGE_TEMPLATE_MIGRATION_SINGLE
GENERATE_PREVIEW
GENERATE_THUMBNAIL
GENERATE_THUMBHASH
UI changes:
A new option "Replace photo" was added to menu on asset-viewer-nav-bar
The file uploader was modified to accept a new parameter, assetId that will cause it to use the updateFile method instead of uploadFile. Fancy typescript was added to ensure that assetId OR albumId can be specified, but not both. The bare-params were turned into a param-object.
A cache-buster was added to all thumbnail, video-thumbnail urls, which uses the checksum of the image as query param. When the client receives and event, it will regen the url with a new query param - this causes the browser to treat this like a new image, and bypass the cache.
In photo-viewer and video-viewer, it doesn't use <img src> to load the images. So a websocket listener was added in asset-viewer, and it will update the displayed asset if any after the upload is done. The photo-viewer and video-viewer were modified to use a svelte effect to listen to changes to the asset, and update the url or data for the asset. video required adding a .load() on the <video> tag after updating the src.
Questions
I have noticed the key param used in several places, but not clear on what it is. Is this similar to a cache buster ? If so, we could maybe use that instead of the checksum/cachebuster.
The cachebuster might fix https://github.com/immich-app/immich/issues/6955
The key query param is used for shared link authentication. So routes that require authentication even when you are not logged in.
Hello, thank you for the PR. I wonder if we need the replace photos option on the UI at all. This work mainly supports the Lightroom workflow, I don't see normal usage behavior to use this mechanism. Let me know what you think!
Hello, thank you for the PR. I wonder if we need the
replace photosoption on the UI at all. This work mainly supports the Lightroom workflow, I don't see normal usage behavior to use this mechanism. Let me know what you think!
I agree - I think it would be relatively rare to want to do this from the UI, but there may be times where a user would want to do it manually - manually syncing/replacing an asset with an edit, for example. Most users won't be using API, so they won't even know what they would be missing.
Anyways, for now, I've hidden behind a query param, because if nothing else, it is useful for testing purposes. To show the menu option, append ?showDevUI=true to the URL.