Files
fdroidserver-ipfs/SPEC.md
T
mrambossekandClaude Opus 4.7 a6168221d1
Build fdroidserver-ipfs image / build (push) Successful in 22s
initial: fdroidserver-ipfs image
Downstream of registry.gitlab.com/fdroid/docker-executable-fdroidserver:master,
patched for publishing F-Droid repos over IPFS:

  - install Debian ipfs-cid so fdroidserver populates ipfsCIDv1 on every
    package file (APKs, icons, screenshots), which fdroidclient with an
    IPFS-gateway mirror routes via CID natively
  - rehash.py wrapper that adds ipfsCIDv1 to entry["index"], renames the
    index to a content-addressed filename, and re-signs entry.jar after
    `fdroid update` — closes the cache-busting gap on the index file itself
  - entrypoint.sh chains rehash automatically after any `fdroid update`,
    so existing fdroid-via-docker wrappers need no changes

See SPEC.md for the rationale and Change 2's algorithm; README.md for
end-user usage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:48:39 +02:00

5.9 KiB

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):

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.jsonrepo/index-v2-<h>.json, where <h> 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-<h>.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 <h> 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.