···1212 POST /<path> body=N bytes, Content-Range: bytes <start>-<end>/<total>
1313 Chunked upload. `start` must equal the current staging size, or 0
1414 to truncate and start fresh. When the staging size reaches `total`,
1515- the staging file is moved to /var/lib/ipfs/download/<path> and
1616- `ipfs add --nocopy --pin` is run. Returns 200 with the CID on
1717- completion, 308 with `Upload-Offset: <size>` while incomplete.
1515+ the staging file is moved to /var/lib/ipfs/download/<path>; the
1616+ ipfs-pin-watcher then pins it (it is the sole pinner). This handler
1717+ only computes the CID (`ipfs add --only-hash`) to return. Returns 200
1818+ with the CID on completion, 308 with `Upload-Offset: <size>` while
1919+ incomplete.
18201921 POST /<path> body=0, no Content-Range
2022 Status check. Returns 200 with `Upload-Offset: <size>` if a
···202204 self.send_error(500, f"truncate failed: {e}")
203205 return
204206205205- # Complete: move staging → dst and pin.
207207+ # Complete: move staging → dst. The os.replace() below fires the
208208+ # ipfs-pin-watcher's inotify MOVED_TO, and the watcher is the SOLE
209209+ # pinner (`ipfs add --nocopy --pin`). We must NOT also pin here: two
210210+ # concurrent `--nocopy` adds of the same file corrupt the filestore
211211+ # into an incomplete DAG. So we only compute the CID to return to the
212212+ # client (--only-hash writes nothing). The watcher's single-threaded
213213+ # loop is then the only thing mutating the filestore. Pinning is
214214+ # therefore async — the watcher finishes a beat after this returns.
206215 dst = os.path.join(DOWNLOAD_ROOT, rel)
207216 dst_dir = os.path.dirname(dst) or DOWNLOAD_ROOT
208217 try:
···214223215224 try:
216225 result = subprocess.run(
217217- [IPFS_BIN, "add", "--nocopy", "--pin", "--quieter", dst],
226226+ [IPFS_BIN, "add", "--only-hash", "--raw-leaves", "--quieter", dst],
218227 check=True,
219228 capture_output=True,
220229 text=True,