# fdroidserver-ipfs A drop-in replacement for `registry.gitlab.com/fdroid/docker-executable-fdroidserver:master` tailored for publishing F-Droid repositories over IPFS. The upstream image works for HTTP-served repos but has two gaps that break the IPFS workflow. This image patches both. ## Problem Pinning an F-Droid repo to IPFS and serving it through public HTTP gateways (ipfs.io, dweb.link, cloudflare-ipfs.com, …) exposes two upstream limitations: 1. The `ipfs-cid` Python module isn't installed, so any fdroidserver code path that computes a CIDv1 for repo artifacts fails on the stock image. 2. `index-v2.json` is published under a stable filename. Public IPFS gateways cache aggressively by URL path, so clients keep getting the previous index for hours after a republish even though the underlying CID has changed. ## Scope This repo produces one OCI image — call it `fdroidserver-ipfs` — built `FROM` the upstream `docker-executable-fdroidserver:master` and applying the minimum set of changes needed for the IPFS workflow. **Everything not listed below is intentionally inherited unchanged from upstream.** ### Change 1 — install `ipfs-cid` Install the **Debian** `ipfs-cid` package (available in Trixie, which the upstream image is based on): ```dockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends ipfs-cid \ && rm -rf /var/lib/apt/lists/* ``` Note: the **PyPI** package of the same name is a Python library only and ships no binary, so `pip install ipfs-cid` silently leaves CID generation disabled. fdroidserver auto-detects the binary on PATH (`fdroidserver/common.py::shutil.which('ipfs_cid')`) and, when present, calls `calculate_IPFS_cid()` for every published file — APKs, source tarballs, icons, screenshots, etc. — writing the resulting CIDv1 into each file's `ipfsCIDv1` JSON field (`fdroidserver/update.py:1661-1663`, `:1717-1719`). On the stock upstream image the binary is missing, so the field is silently omitted and the IPFS-routing code path on the client is never exercised. **Why this matters for the client.** fdroidclient has native IPFS support: `org.fdroid.download.MirrorChooser` (in `libs/download/`) reads the `ipfsCIDv1` field on each file and, when a configured mirror is flagged `isIpfsGateway`, fetches the file by CID via that gateway instead of by URL. So once `ipfs-cid` is installed, every per-package asset (APK, icon, …) is automatically reachable via any IPFS gateway by any client with one configured — no patching required. ### Change 2 — add `ipfsCIDv1` to the index entry + cache-busted filename Change 1 fixes per-package files but **does not** fix the index itself. The `index` block inside `entry.json` is built by `fdroidserver/common.py::file_entry()`, which only emits `name` / `sha256` / `size` and never adds `ipfsCIDv1` — even when one could be computed. So fdroidclient has no CID for `index-v2.json` to feed into `MirrorChooser`, and falls back to the stable HTTP path → public IPFS gateways serve a stale cached copy for hours after a republish. The post-process wrapper closes both halves of this gap in one pass: 1. Computes the IPFS CIDv1 of `repo/index-v2.json` (using `calculate_IPFS_cid` from `fdroidserver.common`, which is the same code path fdroidserver itself uses for APKs). 2. Renames `repo/index-v2.json` → `repo/index-v2-.json`, where `` is the **last 12 hex characters of the SHA-256** of the file's content. This image is **IPFS-only** — no stable copy is kept alongside. 3. Rewrites `repo/entry.json` so that `entry["index"]`: - Updates `name` to `/index-v2-.json` - Adds `ipfsCIDv1` with the value computed in step 1 - Leaves `sha256` and `size` unchanged (the bytes didn't move) 4. Re-signs `entry.jar` by calling `fdroidserver.signindex.sign_index(repodir, "entry.json")` — reuses the existing `config.yml` keystore + `FDROID_KEY_STORE_PASS` / `FDROID_KEY_PASS` env vars; no extra config surface. 5. If GPG signing is enabled in `config.yml`, regenerates `entry.json.asc`. 6. Applies the same CID/rename/rewrite to any `diff/*.json` files published alongside (each is referenced by its own `name` field in entry.json). Two-layer cache strategy, on purpose: - **IPFS-aware clients** (any user with an IPFS gateway configured as a mirror) follow the `ipfsCIDv1` and fetch the index by CID. Always fresh, never gateway-cached by URL. - **Plain-HTTP clients** still hit a new URL on every publish thanks to the `` suffix → public gateway path caches naturally miss → fresh index without waiting for TTLs. `entry.jar` itself stays at the fixed `/entry.jar` URL — gateways will still cache it, but it's tiny and clients fetch it first to discover both the current index filename and its CID, which are the cache-busted references we actually care about. `entry.json` does not currently carry an `ipfsCIDv1` for the index entry, but fdroidclient's parser is configured with `ignoreUnknownKeys = true` (`libs/index/.../IndexParser.kt`) and `EntryFileV2` already declares the field (`libs/index/.../IndexV2.kt`), so adding it is both backwards-safe and natively consumed by current client versions. Delivery: a small Python script (`fdroidserver-ipfs-rehash`, or similar) installed into the image's PATH that imports `fdroidserver.signindex` / `fdroidserver.common` directly. Either invoked manually after `fdroid update`, or wired into a container entrypoint that runs both. Entrypoint design is deferred until we know how the image gets driven in production. ## Out of scope - Changes to `index-v1.*` or the legacy `index.jar` flow. - Pinning to IPFS, gateway selection, DNSLink updates — handled by whatever drives the container, not by the image itself. - Anything unrelated to the two issues above. ## Non-goals for this spec This document captures *what* the image needs to do and *why*. The concrete Dockerfile layering, the exact rehash/re-sign script, and the workflow YAML are implementation details and land in follow-up changes.