2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00
2026-08-03 22:58:30 +02:00

Immich Share Sync

An ephemeral Qt 6 web service that compares two Immich public shares by SHA-1 checksum and streams missing originals in either direction. It never deletes assets and never writes transfer data to disk.

How it works

  • A QHttpServer serves an embedded vanilla HTML/CSS/JavaScript frontend.
  • The same HTTP listener upgrades /ws with Qt WebSockets.
  • Each short-lived WebSocket owns exactly one job, one SyncSession, two ImmichClient instances, and their QNetworkAccessManager objects.
  • Link inspection tries Immich's share key and custom-slug authentication forms. Current password-protected shares use POST /api/shared-links/login; Qt's cookie jar retains the short-lived share cookie for that socket session.
  • The service reads QJsonValue/QJsonObject responses and compares the base64-encoded SHA-1 checksums in Immich's shared-link response.
  • Each missing original is a sequential QNetworkReply used as the QIODevice body of the destination multipart request. A 1 MiB source read buffer provides backpressure; there is no temporary file or whole-asset buffer.
  • The browser sends one complete job request immediately after connecting. The service closes the WebSocket when that inspection or synchronization finishes.
  • Closing the WebSocket early destroys the session and aborts every active network reply. No inspection ID, plan, credentials, or recovery state survives the connection.

The destination upload deliberately omits Immich's x-immich-checksum optimization. That optimization returns duplicates before Immich associates an already-owned asset with the destination shared album. The normal duplicate path both detects the checksum and adds the existing asset to the share.

Build and run

Requirements: CMake 3.28+, a C++23 compiler, and Qt 6.9+ with Core, Network, HttpServer, and WebSockets.

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure
PORT=8090 ./build/immich-sync

Open http://localhost:8090.

WebSocket protocol

All messages are JSON. Each WebSocket accepts exactly one complete job request and closes after its terminal response. A client that does not send a request within 10 seconds is disconnected. Passwords are accepted only in client messages and are never placed in URLs or echoed in status responses.

Inspect two links:

{
  "type": "inspect",
  "left": {"url": "https://one.example/share/key", "password": ""},
  "right": {"url": "https://two.example/share/key", "password": ""}
}

The service emits granular side-status messages. Stages include connecting, authenticating, password-required, ready, and error. Once both sides resolve, an inspection response includes public share details, upload/download permissions, missing counts, and the allowed left-to-right, right-to-left, and bidirectional options. The service then closes the inspection socket.

To synchronize, open a new WebSocket and send both shares again along with one of the directions offered by inspection:

{
  "type": "sync",
  "direction": "bidirectional",
  "left": {"url": "https://one.example/share/key", "password": ""},
  "right": {"url": "https://two.example/share/key", "password": ""}
}

The synchronization job repeats inspection so its checksum plan and permissions reflect current Immich state. Status messages include the inspection events followed by sync-status, asset-status, and throttled asset-progress events. The service closes the socket after completion; close it from the client to abort and discard the whole session immediately.

Container and Kubernetes

docker build -t registry.brunner.ninja/feedc0de/immich-sync:latest .
docker run --rm -p 8090:8090 registry.brunner.ninja/feedc0de/immich-sync:latest
./install.sh

The Kubernetes manifest follows the neighboring brunner-ninja and visual-studio-code layout. It assumes the image name and immich-sync.brunner.ninja hostname shown in the manifest. It enables the existing Authentik Traefik middleware because accepting arbitrary server URLs creates an SSRF/bandwidth-abuse surface; remove that annotation only if intentionally exposing the service publicly.

Current scope

  • Assets without a checksum are ignored by planning.
  • Metadata processing on the destination is left to Immich after upload.
  • Sidecar files and Live Photo pairing are not reconstructed yet; the primary original asset is transferred.
  • Immich serves the edited rendition for edited assets through a shared link even when the original endpoint is requested. Such an asset can acquire a different destination checksum and may be proposed again on a later, stateless run. This cannot be fully resolved with current shared-link permissions alone.
  • TLS certificates must be valid. The service never ignores SSL errors.
S
Description
Since immich refuses to federate and we need to cross-share albums, this web tool was created to synchronize albums remotely (just needs two sharing links with write permission)
Readme 85 KiB
Languages
C++ 63.4%
JavaScript 13.2%
CSS 12.8%
HTML 5.5%
CMake 2%
Other 3%