diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4d3f906..2b4727c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -10,10 +10,10 @@ jobs: name: Test build on PR runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 14.x + node-version: 18.x cache: yarn - name: Test build run: | diff --git a/docs/pinning-service/ipfs-pinning-service-api.md b/docs/pinning-service/ipfs-pinning-service-api.md index e98217c..1c169ef 100644 --- a/docs/pinning-service/ipfs-pinning-service-api.md +++ b/docs/pinning-service/ipfs-pinning-service-api.md @@ -78,3 +78,114 @@ For more commands and general help: ```bash ipfs pin remote --help ``` + +## Pinning a DAG (recursive pinning) + +In the IPFS Pinning Service API, **pinning a CID is recursive DAG pinning**. There is no separate "DAG pin" operation and no recursive flag — every pin request pins the *entire* DAG rooted at the CID. + +The [IPFS Pinning Services API Spec](https://ipfs.github.io/pinning-services-api-spec/) defines the `cid` of a pin as: + +> Content Identifier (CID) points at the root of a DAG that is pinned recursively. + +So when you pin `bafybeib32…` (a file, a directory, or any IPLD/UnixFS DAG root), Functionland Fula retrieves and pins **every block reachable from that root**. A single CID is all you need to pin an arbitrarily large DAG — the commands in the previous section (`ipfs pin remote add …`) are already performing recursive DAG pins. + +How the service obtains the DAG (per the standard): after you submit a CID, the service finds providers for the involved CIDs across the IPFS network — optionally helped by the `origins` hints you supply — and downloads the DAG over Bitswap. This is why a pin you just created starts in `queued`/`pinning` and only becomes `pinned` once the whole DAG has been fetched. + +## Standard REST API + +The `ipfs` CLI above is a convenience wrapper over the standard REST endpoints. You can call them directly against `https://api.cloud.fx.land` with your access token as a bearer credential. Functionland Fula implements the full IPFS Pinning Services API spec. + +| Operation | Endpoint | Description | +|---|---|---| +| Add pin | `POST /pins` | Request a recursive DAG pin for a CID | +| List pins | `GET /pins` | List/filter pin objects (by `cid`, `name`, `status`, `before`/`after`, `meta`) | +| Get pin | `GET /pins/{requestid}` | Check the status of one pin request | +| Replace pin | `POST /pins/{requestid}` | Atomically remove + re-add (avoids GC of blocks common to both pins) | +| Remove pin | `DELETE /pins/{requestid}` | Remove a pin object | + +All requests send `Authorization: Bearer YOUR_JWT`. + +**Create a pin (recursive DAG pin):** + +```bash +curl -X POST "https://api.cloud.fx.land/pins" \ + -H "Authorization: Bearer YOUR_JWT" \ + -H "Content-Type: application/json" \ + -d '{ + "cid": "bafybeib32tuqzs2wrc52rdt56cz73sqe3qu2deqdudssspnu4gbezmhig4", + "name": "war-and-peace.txt" + }' +``` + +The **Pin** object you send accepts: `cid` (required — the DAG root), `name` (optional label), `origins` (optional multiaddrs the service should try first to fetch the data), and `meta` (optional string-to-string key/value metadata). + +The service responds with a **PinStatus** object: + +```json +{ + "requestid": "UniqueIdOfPinRequest", + "status": "queued", + "created": "2024-06-28T15:02:42Z", + "pin": { + "cid": "bafybeib32tuqzs2wrc52rdt56cz73sqe3qu2deqdudssspnu4gbezmhig4", + "name": "war-and-peace.txt" + }, + "delegates": ["/dnsaddr/.../p2p/Qm..."], + "info": {} +} +``` + +**Pin lifecycle:** `queued` → `pinning` → `pinned` (or `failed`). Poll `GET /pins/{requestid}` until the status settles: + +```bash +curl "https://api.cloud.fx.land/pins/UniqueIdOfPinRequest" \ + -H "Authorization: Bearer YOUR_JWT" +``` + +**List your pins.** `GET /pins` returns a paginated `PinResults` object — a `count` of total matches plus a `results` array of `PinStatus` objects. To page through more than one batch, pass the `before` filter with the `created` timestamp of the oldest item you've seen: + +```bash +curl "https://api.cloud.fx.land/pins?status=pinned&limit=10" \ + -H "Authorization: Bearer YOUR_JWT" +``` + +```json +{ "count": 42, "results": [ { "requestid": "…", "status": "pinned", "pin": { "cid": "…" } } ] } +``` + +To speed up the transfer when your own node already has the data, put your node's multiaddrs in `origins`, and pre-connect to the peers returned in the response's `delegates` array (see [Provider hints](https://ipfs.github.io/pinning-services-api-spec/#section/Provider-hints) in the spec). + +## Importing a DAG by uploading a CAR file + +This is a **Functionland extension**, not part of the IPFS Pinning Service API standard. The standard is pin-by-CID only and has no data-upload endpoint — it assumes the DAG can be fetched from the network. CAR import fills the gap when the data exists **only on your machine** and no other peer is providing it yet. + +The same content-addressed convention used by [web3.storage](https://web3.storage), NFT.storage and Filebase: you package your DAG as a [CAR (Content Addressable aRchive)](https://ipld.io/specs/transport/car/) file and upload it; the service imports the blocks and recursively pins the root CID to your account. + +**1. Export your DAG to a CAR file** (using your local IPFS/Kubo node): + +```bash +# Pin or add your data locally first, then export its DAG to a CAR: +ipfs dag export bafybeib32tuqzs2wrc52rdt56cz73sqe3qu2deqdudssspnu4gbezmhig4 > data.car +``` + +**2. Upload the CAR** to the import endpoint (multipart `file`, optional `name`): + +```bash +curl -X POST "https://api.cloud.fx.land/pins/import/car" \ + -H "Authorization: Bearer YOUR_JWT" \ + -F "file=@data.car" \ + -F "name=my-dataset" +``` + +The service validates the CAR, imports its blocks into the Fula network, and recursively pins the root. It returns the same **PinStatus** object as `POST /pins` (with `status: "queued"`), so you track it the same way via `GET /pins/{requestid}` until it becomes `pinned`. Re-importing a CAR whose root you already have simply returns your existing pin. + +**Requirements and limits:** + +- **Single root** — the CAR must have exactly one root CID (the DAG that gets pinned). Multi-root CARs are rejected. +- **Complete DAG** — every block referenced by the DAG must be present in the CAR. A partial DAG is rejected (it could never finish pinning). +- **Formats** — CARv1 and CARv2 are both accepted (the `ipfs dag export` default is CARv1). +- **Integrity** — every block's bytes are verified against its CID on the way in. +- **Size** — up to 800 MB per CAR by default; blocks above 2 MiB are rejected. +- **Quota** — the imported DAG counts against your storage quota; an import that wouldn't fit your plan is rejected up front with `402`. + +**Error responses:** `400` invalid CAR (bad/truncated, hash mismatch, zero or multiple roots, missing blocks, unsupported codec, oversized/over-deep block); `402` insufficient storage credit; `413` CAR larger than the allowed maximum; `429` too many concurrent imports. The response body carries the specific reason.