feedc0de fa39ed5b10
Publish container image / Build and push (push) Successful in 35s
Second try with CI
2026-08-03 23:17:22 +02:00
2026-08-03 23:17:22 +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 23:13:26 +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 23:17:22 +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.

After both share URLs are valid, starting a job updates the browser fragment in the form #left=<encoded-share-url>&right=<encoded-share-url>. Reloading or bookmarking that URL restores both fields and automatically starts a new inspection. Passwords are intentionally excluded from the fragment and must be entered again for protected shares.

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.

Continuous delivery

The Gitea Actions workflow in .gitea/workflows/container.yml builds and pushes registry.brunner.ninja/feedc0de/immich-sync. Every pushed commit receives an immutable tag containing its full Git SHA. A commit pushed to main receives both its SHA tag and latest from the same image build.

The workflow deliberately loads the completed image into the Docker engine and pushes it with a separate plain docker push. It does not request provenance attestations, SBOM manifests, or OCI referrers, retaining compatibility with the self-hosted Quay registry and with Gitea runners whose default builder uses BuildKit's container driver.

The workflow expects these Gitea Actions repository secrets:

  • QUAY_USERNAME: the complete Quay robot account name, including the feedc0de+ prefix.
  • QUAY_TOKEN: the robot account token.

Give the robot account Write permission only on the feedc0de/immich-sync repository. Public image pulls do not use these credentials and do not require a Gitea secret.

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%