# Immich Share Sync An ephemeral Qt 6 web service that compares two Immich public shares by SHA-1 checksum and copies missing originals in either direction. It never deletes assets or keeps recovery state. ## 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 downloaded into a `QTemporaryFile`, rewound, and used as the seekable `QIODevice` body of the destination multipart request. Only one asset per job is staged, and the file is removed after success, failure, or WebSocket abort. - 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. - Aggregate counters are the only persistent state. Kubernetes replicas update a small locked JSON file on the shared CephFS volume; it contains only numeric totals and never contains links, passwords, filenames, or job recovery data. 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. ```sh 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=&right=`. 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: ```json { "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: ```json { "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. After upload reaches 100%, a `processing` asset stage makes it clear that the destination Immich is still handling the request. The service closes the socket after completion; close it from the client to abort, delete the temporary asset, and discard the whole session immediately. ## Container and Kubernetes ```sh docker build -t registry.brunner.ninja/feedc0de/immich-sync:latest . docker run --rm -p 8090:8090 registry.brunner.ninja/feedc0de/immich-sync:latest kubectl apply -f deploy/example.yaml kubectl port-forward service/immich-sync 8090:80 ``` The maintained Helm chart is published with the exact image revision in its `appVersion`: ```sh helm repo add brunner.ninja https://brunner.ninja/charts helm repo update brunner.ninja helm upgrade --install immich-sync brunner.ninja/immich-sync ``` `deploy/example.yaml` is a portable starting point for self-hosting. It creates one Deployment and a ClusterIP Service, uses the public image, and does not require an image-pull secret. Add persistent storage, an Ingress, and monitoring to match your cluster. Because the service can make outbound requests to user-supplied URLs, operators should monitor it for SSRF and bandwidth abuse. The author's complete [`apps/immich-sync` GitOps values](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/immich-sync) are public and show one Argo CD installation of the chart. They include homelab-specific CephFS storage, multiple replicas, an Ingress hostname, and Prometheus Operator monitoring, so fork it and adjust those assumptions before deploying it to another cluster. WebSockets need no sticky-session setting: after the HTTP upgrade, the selected pod owns that connection until the short-lived job finishes or disconnects. ## Statistics and Prometheus The homepage reads `/api/stats` every 30 seconds and displays shared totals for completed synchronization runs, all successfully synchronized assets, photos, videos, and bytes. Counters begin at zero when statistics are first enabled. Each replica exposes Prometheus text metrics at `/metrics`. A deployment can add a `ServiceMonitor` selecting the `immich-sync` Service. Useful Grafana queries include: ```promql sum(immich_sync_active_synchronizations) sum(rate(immich_sync_assets_total{result="success"}[5m])) sum by (media_type) (rate(immich_sync_asset_bytes_total[5m])) max(immich_sync_lifetime_synchronizations_total) max by (media_type) (immich_sync_lifetime_assets_total) ``` The lifetime series contains the same shared value on both replicas, so use `max`, not `sum`, for those metrics. Per-process operational counters and active gauges should be summed across replicas. ## Continuous delivery The Gitea Actions workflow in `.gitea/workflows/container.yml` builds and pushes `registry.brunner.ninja/feedc0de/immich-sync`. Every release receives an immutable image tag containing its full Git SHA. The same run packages the chart with that SHA as `appVersion`. The repository tag follows the application's `CMakeLists.txt` version: `v0.2.0` produces chart `0.2.0`; later chart commits produce unique versions such as `0.2.0-1-g41627ab` through `git describe`. 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. - `PACKAGE_USERNAME` and `PACKAGE_TOKEN`: credentials that can publish to the public `feedc0de` Helm repository. - `GITOPS_TOKEN`: a `write:repository` token belonging to a dedicated account with write access only to `feedc0de/homelab-gitops`. 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. After a successful `main` build, the promotion job updates the Argo Application's exact chart `targetRevision` in `homelab-gitops` and pushes a deployment commit. Argo CD observes that commit and reconciles Kubernetes. The workflow has no kubeconfig and no direct Kubernetes access. ## 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.