@filoz/repair-cli 0.4.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +4 -4
- package/dist/src/api.d.ts +35 -0
- package/dist/src/api.d.ts.map +1 -0
- package/dist/src/api.js +118 -0
- package/dist/src/api.js.map +1 -0
- package/dist/src/commands/datasets.d.ts +5 -7
- package/dist/src/commands/datasets.d.ts.map +1 -1
- package/dist/src/commands/datasets.js +33 -35
- package/dist/src/commands/datasets.js.map +1 -1
- package/dist/src/commands/providers.d.ts +5 -7
- package/dist/src/commands/providers.d.ts.map +1 -1
- package/dist/src/commands/providers.js +32 -33
- package/dist/src/commands/providers.js.map +1 -1
- package/dist/src/commands/repair.d.ts +5 -7
- package/dist/src/commands/repair.d.ts.map +1 -1
- package/dist/src/commands/repair.js +5 -1
- package/dist/src/commands/repair.js.map +1 -1
- package/dist/src/commands/replicate.d.ts +5 -7
- package/dist/src/commands/replicate.d.ts.map +1 -1
- package/dist/src/commands/replicate.js +5 -1
- package/dist/src/commands/replicate.js.map +1 -1
- package/dist/src/commands/second-copy.d.ts +5 -7
- package/dist/src/commands/second-copy.d.ts.map +1 -1
- package/dist/src/commands/second-copy.js +5 -1
- package/dist/src/commands/second-copy.js.map +1 -1
- package/dist/src/commands/session-key.d.ts +5 -7
- package/dist/src/commands/session-key.d.ts.map +1 -1
- package/dist/src/commands/setup.d.ts.map +1 -1
- package/dist/src/commands/setup.js +11 -32
- package/dist/src/commands/setup.js.map +1 -1
- package/dist/src/commands/wallet.d.ts +5 -7
- package/dist/src/commands/wallet.d.ts.map +1 -1
- package/dist/src/db/dedupe-cids.d.ts +6 -7
- package/dist/src/db/dedupe-cids.d.ts.map +1 -1
- package/dist/src/db/dedupe-cids.js +2 -14
- package/dist/src/db/dedupe-cids.js.map +1 -1
- package/dist/src/db/find-repair-dataset.d.ts +3 -3
- package/dist/src/db/find-repair-dataset.d.ts.map +1 -1
- package/dist/src/db/find-repair-dataset.js +16 -14
- package/dist/src/db/find-repair-dataset.js.map +1 -1
- package/dist/src/db/get-active-data-set-ids.d.ts +9 -0
- package/dist/src/db/get-active-data-set-ids.d.ts.map +1 -0
- package/dist/src/db/get-active-data-set-ids.js +9 -0
- package/dist/src/db/get-active-data-set-ids.js.map +1 -0
- package/dist/src/db/get-alternate-provider-urls.d.ts +6 -0
- package/dist/src/db/get-alternate-provider-urls.d.ts.map +1 -0
- package/dist/src/db/get-alternate-provider-urls.js +24 -0
- package/dist/src/db/get-alternate-provider-urls.js.map +1 -0
- package/dist/src/db/get-pieces.d.ts +8 -27
- package/dist/src/db/get-pieces.d.ts.map +1 -1
- package/dist/src/db/get-pieces.js +44 -97
- package/dist/src/db/get-pieces.js.map +1 -1
- package/dist/src/db/get-repair-provider.d.ts +2 -2
- package/dist/src/db/get-repair-provider.d.ts.map +1 -1
- package/dist/src/db/get-repair-provider.js +8 -20
- package/dist/src/db/get-repair-provider.js.map +1 -1
- package/dist/src/db/repair-create.js +3 -3
- package/dist/src/db/repair-create.js.map +1 -1
- package/dist/src/db/replicate-create.d.ts.map +1 -1
- package/dist/src/db/replicate-create.js +13 -21
- package/dist/src/db/replicate-create.js.map +1 -1
- package/dist/src/db/second-copy-create.js +2 -2
- package/dist/src/db/second-copy-create.js.map +1 -1
- package/dist/src/local-schema.d.ts +1 -1
- package/dist/src/middleware.d.ts +12 -16
- package/dist/src/middleware.d.ts.map +1 -1
- package/dist/src/middleware.js +6 -10
- package/dist/src/middleware.js.map +1 -1
- package/dist/src/pipeline/add-pieces.d.ts +4 -4
- package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
- package/dist/src/pipeline/add-pieces.js +32 -19
- package/dist/src/pipeline/add-pieces.js.map +1 -1
- package/dist/src/pipeline/create-datasets.d.ts +4 -4
- package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
- package/dist/src/pipeline/create-datasets.js +7 -14
- package/dist/src/pipeline/create-datasets.js.map +1 -1
- package/dist/src/types.d.ts +8 -9
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +45 -37
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +47 -2
- package/dist/src/utils.js.map +1 -1
- package/package.json +4 -4
- package/readme.md +22 -19
- package/src/api.ts +220 -0
- package/src/commands/datasets.ts +33 -38
- package/src/commands/providers.ts +34 -42
- package/src/commands/repair.ts +5 -1
- package/src/commands/replicate.ts +5 -1
- package/src/commands/second-copy.ts +5 -1
- package/src/commands/setup.ts +12 -35
- package/src/db/dedupe-cids.ts +7 -24
- package/src/db/find-repair-dataset.ts +17 -28
- package/src/db/get-active-data-set-ids.ts +29 -0
- package/src/db/get-alternate-provider-urls.ts +43 -0
- package/src/db/get-pieces.ts +82 -214
- package/src/db/get-repair-provider.ts +10 -30
- package/src/db/repair-create.ts +3 -3
- package/src/db/replicate-create.ts +13 -21
- package/src/db/second-copy-create.ts +2 -2
- package/src/middleware.ts +10 -14
- package/src/pipeline/add-pieces.ts +52 -24
- package/src/pipeline/create-datasets.ts +9 -29
- package/src/types.ts +11 -9
- package/src/utils.ts +71 -7
- package/dist/src/db/find-providers-by-cid.d.ts +0 -9
- package/dist/src/db/find-providers-by-cid.d.ts.map +0 -1
- package/dist/src/db/find-providers-by-cid.js +0 -43
- package/dist/src/db/find-providers-by-cid.js.map +0 -1
- package/dist/src/indexer-schema.d.ts +0 -549
- package/dist/src/indexer-schema.d.ts.map +0 -1
- package/dist/src/indexer-schema.js +0 -56
- package/dist/src/indexer-schema.js.map +0 -1
- package/src/db/find-providers-by-cid.ts +0 -70
- package/src/indexer-schema.ts +0 -77
package/readme.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
The `repair` CLI helps prepare and run repair and replication jobs on Filecoin PDP datasets. It uses:
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- the [Filecoin API](https://github.com/filecoin-project/agents/tree/main/apps/filecoin-api) (`https://api.filecoin.io` by default) as the read-only source of providers, datasets, and pieces
|
|
10
10
|
- a local SQLite database to track repair and replication jobs and per-piece operations
|
|
11
11
|
- a configured Filecoin wallet to create datasets and submit on-chain add-piece transactions
|
|
12
12
|
|
|
@@ -24,7 +24,7 @@ repair --help
|
|
|
24
24
|
|
|
25
25
|
## Setup
|
|
26
26
|
|
|
27
|
-
Run setup before using any command that talks to the
|
|
27
|
+
Run setup before using any command that talks to the Filecoin API, local database, or wallet.
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
repair setup
|
|
@@ -33,8 +33,7 @@ repair setup
|
|
|
33
33
|
Setup prompts for:
|
|
34
34
|
|
|
35
35
|
- private key for the repair wallet
|
|
36
|
-
-
|
|
37
|
-
- calibration indexer Postgres URL
|
|
36
|
+
- Filecoin API URL (defaults to `https://api.filecoin.io`)
|
|
38
37
|
- chain, either Filecoin Mainnet `314` or Filecoin Calibration `314159`
|
|
39
38
|
- local SQLite database path
|
|
40
39
|
- dataset source label, used when creating repair target datasets (defaults to `early-repair`)
|
|
@@ -59,7 +58,7 @@ Interactive configuration and local database setup.
|
|
|
59
58
|
repair setup
|
|
60
59
|
```
|
|
61
60
|
|
|
62
|
-
Use this whenever you need to initialize the CLI, change the active chain,
|
|
61
|
+
Use this whenever you need to initialize the CLI, change the active chain, change the Filecoin API URL, change the dataset source label, or move the local SQLite database.
|
|
63
62
|
|
|
64
63
|
### `repair wallet fund`
|
|
65
64
|
|
|
@@ -136,7 +135,7 @@ The command waits for the revoke transaction to be mined and returns the session
|
|
|
136
135
|
|
|
137
136
|
### `repair providers list`
|
|
138
137
|
|
|
139
|
-
Lists PDP providers from the
|
|
138
|
+
Lists PDP providers from the Filecoin API.
|
|
140
139
|
|
|
141
140
|
```bash
|
|
142
141
|
repair providers list
|
|
@@ -149,8 +148,6 @@ By default, the command returns active PDP providers that are approved or endors
|
|
|
149
148
|
- `serviceUrl`
|
|
150
149
|
- `approved`
|
|
151
150
|
- `endorsed`
|
|
152
|
-
- `pieceCount`, the number of active indexed pieces for that provider
|
|
153
|
-
- `totalSize`, the sum of active piece raw sizes formatted in decimal GB
|
|
154
151
|
|
|
155
152
|
Use `--all` to include every active PDP provider, even if it is not approved or endorsed.
|
|
156
153
|
|
|
@@ -158,6 +155,12 @@ Use `--all` to include every active PDP provider, even if it is not approved or
|
|
|
158
155
|
repair providers list --all
|
|
159
156
|
```
|
|
160
157
|
|
|
158
|
+
Use `--stats` to also return `pieceCount`, the number of active indexed pieces for each provider, and `totalSize`, the sum of their raw sizes in decimal GB. The API has no aggregate endpoint, so this pages through every piece of every listed provider and can take a long time.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
repair providers list --stats
|
|
162
|
+
```
|
|
163
|
+
|
|
161
164
|
### `repair datasets list`
|
|
162
165
|
|
|
163
166
|
Lists datasets owned by the configured repair wallet.
|
|
@@ -166,7 +169,7 @@ Lists datasets owned by the configured repair wallet.
|
|
|
166
169
|
repair datasets list
|
|
167
170
|
```
|
|
168
171
|
|
|
169
|
-
Each dataset includes its ID, CDN/IPFS indexing flags, source, provider URL, PDP end epoch
|
|
172
|
+
Each dataset includes its ID, CDN/IPFS indexing flags, source, provider URL, and PDP end epoch. Add `--stats` to include each dataset's active piece count, which pages through every piece.
|
|
170
173
|
|
|
171
174
|
Filter by provider ID:
|
|
172
175
|
|
|
@@ -176,7 +179,7 @@ repair datasets list --provider-id 123
|
|
|
176
179
|
|
|
177
180
|
### `repair datasets show <id>`
|
|
178
181
|
|
|
179
|
-
Shows a dataset from the
|
|
182
|
+
Shows a dataset from the Filecoin API.
|
|
180
183
|
|
|
181
184
|
```bash
|
|
182
185
|
repair datasets show 42
|
|
@@ -206,7 +209,7 @@ repair repair create --provider-id 101 --target-provider-id 202
|
|
|
206
209
|
|
|
207
210
|
`--target-provider-id` is the provider that should receive the repaired pieces. It must be different from `--provider-id`.
|
|
208
211
|
|
|
209
|
-
The command snapshots the current chain block number, creates a local repair row, scans active pieces for the source provider, deduplicates them by CID, and creates local `add_piece` operations.
|
|
212
|
+
The command snapshots the current chain block number, creates a local repair row, scans active pieces for the source provider, deduplicates them by CID, and creates local `add_piece` operations. For each piece it probes the approved and endorsed providers (endorsed first) with `HEAD /piece/{cid}`, then the provider being repaired as a last resort, and uses the first one that serves it as the pull source. Pieces that no provider serves are marked `skipped`. The command returns a `repairId`.
|
|
210
213
|
|
|
211
214
|
### `repair repair list`
|
|
212
215
|
|
|
@@ -235,18 +238,18 @@ Runs a pending repair.
|
|
|
235
238
|
repair repair run 1
|
|
236
239
|
```
|
|
237
240
|
|
|
238
|
-
The command first ensures the target repair dataset exists for the configured wallet and target provider. If no matching dataset exists, it creates one with IPFS indexing enabled and CDN disabled, using the configured source label. Then it processes pending `add_piece` operations by pulling pieces from alternate providers into the target provider and committing them on-chain.
|
|
241
|
+
The command first ensures the target repair dataset exists for the configured wallet and target provider. If no matching dataset exists, it creates one with IPFS indexing enabled and CDN disabled, using the configured source label. Then it reads the CIDs already in the target dataset once, marks matching operations completed, and processes the remaining pending `add_piece` operations by pulling pieces from alternate providers into the target provider and committing them on-chain.
|
|
239
242
|
|
|
240
243
|
Options:
|
|
241
244
|
|
|
242
245
|
- `--concurrency <number>` controls how many pull batches run at once. Defaults to `4`.
|
|
243
|
-
- `--batch-size <number>`
|
|
246
|
+
- `--batch-size <number>` caps the number of `add_piece` operations per batch. By default each batch holds as many pieces as fit in one `addPieces` message (about 400 without piece metadata, 80 for legacy datasets).
|
|
244
247
|
- `--payer <address>` overrides the payer address used when creating or finding the target repair dataset. Defaults to the configured wallet address.
|
|
245
248
|
|
|
246
249
|
Example:
|
|
247
250
|
|
|
248
251
|
```bash
|
|
249
|
-
repair repair run 1 --concurrency 8 --batch-size
|
|
252
|
+
repair repair run 1 --concurrency 8 --batch-size 100
|
|
250
253
|
repair repair run 1 --payer 0x1234567890123456789012345678901234567890
|
|
251
254
|
```
|
|
252
255
|
|
|
@@ -304,13 +307,13 @@ The command creates a new target dataset on the target provider, copying CDN and
|
|
|
304
307
|
Options:
|
|
305
308
|
|
|
306
309
|
- `--concurrency <number>` controls how many pull batches run at once. Defaults to `4`.
|
|
307
|
-
- `--batch-size <number>`
|
|
310
|
+
- `--batch-size <number>` caps the number of `add_piece` operations per batch. By default each batch holds as many pieces as fit in one `addPieces` message (about 400 without piece metadata, 80 for legacy datasets).
|
|
308
311
|
- `--payer <address>` overrides the payer address used when creating the target replication dataset. Defaults to the configured wallet address.
|
|
309
312
|
|
|
310
313
|
Example:
|
|
311
314
|
|
|
312
315
|
```bash
|
|
313
|
-
repair replicate run 1 --concurrency 8 --batch-size
|
|
316
|
+
repair replicate run 1 --concurrency 8 --batch-size 100
|
|
314
317
|
repair replicate run 1 --payer 0x1234567890123456789012345678901234567890
|
|
315
318
|
```
|
|
316
319
|
|
|
@@ -337,7 +340,7 @@ repair second-copy create \
|
|
|
337
340
|
|
|
338
341
|
`--pieces-file` is a JSON file with an `aggregates` array. Each aggregate must include a `piece_cid` string. Only aggregate `piece_cid` values are used; `sub_pieces` are ignored.
|
|
339
342
|
|
|
340
|
-
`--source-provider-url` is the provider URL pieces are pulled from. It is not looked up in the
|
|
343
|
+
`--source-provider-url` is the provider URL pieces are pulled from. It is not looked up in the Filecoin API, so it can point at a provider on another network (for example Mainnet while the CLI is configured for Calibration).
|
|
341
344
|
|
|
342
345
|
`--target-provider-id` is the provider on the configured chain that should receive the copied pieces. Its service URL must differ from `--source-provider-url`.
|
|
343
346
|
|
|
@@ -373,13 +376,13 @@ The command first ensures the target dataset exists for the configured wallet an
|
|
|
373
376
|
Options:
|
|
374
377
|
|
|
375
378
|
- `--concurrency <number>` controls how many pull batches run at once. Defaults to `4`.
|
|
376
|
-
- `--batch-size <number>`
|
|
379
|
+
- `--batch-size <number>` caps the number of `add_piece` operations per batch. By default each batch holds as many pieces as fit in one `addPieces` message (about 400 without piece metadata, 80 for legacy datasets).
|
|
377
380
|
- `--payer <address>` overrides the payer address used when creating or finding the target dataset. Defaults to the configured wallet address.
|
|
378
381
|
|
|
379
382
|
Example:
|
|
380
383
|
|
|
381
384
|
```bash
|
|
382
|
-
repair second-copy run 1 --concurrency 8 --batch-size
|
|
385
|
+
repair second-copy run 1 --concurrency 8 --batch-size 100
|
|
383
386
|
repair second-copy run 1 --payer 0x1234567890123456789012345678901234567890
|
|
384
387
|
```
|
|
385
388
|
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiError,
|
|
3
|
+
createApiClient,
|
|
4
|
+
type DataSet,
|
|
5
|
+
DEFAULT_API_URL,
|
|
6
|
+
type Piece,
|
|
7
|
+
type PieceWithDataSet,
|
|
8
|
+
type Provider,
|
|
9
|
+
unwrap,
|
|
10
|
+
} from '@filecoin-project/api-client'
|
|
11
|
+
import { pipeline, withRateLimit, withRetry, withTimeout } from 'fetch-extras'
|
|
12
|
+
import type { IndexerApi } from './types.ts'
|
|
13
|
+
|
|
14
|
+
export { ApiError, DEFAULT_API_URL } from '@filecoin-project/api-client'
|
|
15
|
+
export type { DataSet, Piece, PieceWithDataSet, Provider }
|
|
16
|
+
|
|
17
|
+
/** filecoin-api network path segment. */
|
|
18
|
+
export type Network = 'mainnet' | 'calibration'
|
|
19
|
+
|
|
20
|
+
/** Largest page size filecoin-api accepts. */
|
|
21
|
+
export const MAX_PAGE_SIZE = 200
|
|
22
|
+
|
|
23
|
+
/** Retries for network errors and `408`, `429` and `5xx` responses. */
|
|
24
|
+
const MAX_RETRIES = 5
|
|
25
|
+
|
|
26
|
+
/** Per-attempt request timeout; a timed-out request fails instead of retrying. */
|
|
27
|
+
const REQUEST_TIMEOUT_MS = 30_000
|
|
28
|
+
|
|
29
|
+
/** filecoin-api allows 120 requests per minute per client IP. */
|
|
30
|
+
const RATE_LIMIT = { requestsPerInterval: 120, interval: 60_000 }
|
|
31
|
+
|
|
32
|
+
/** Map a chain ID to its filecoin-api network. */
|
|
33
|
+
export function networkFromChainId(chainId: number): Network {
|
|
34
|
+
return chainId === 314 ? 'mainnet' : 'calibration'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a filecoin-api client for a chain.
|
|
39
|
+
*
|
|
40
|
+
* @param options.baseUrl - API origin; defaults to the client's production URL.
|
|
41
|
+
* @param options.chainId - Chain ID used to pick the network.
|
|
42
|
+
*/
|
|
43
|
+
export function createIndexerApi({ baseUrl, chainId }: { baseUrl?: string; chainId: number }): IndexerApi {
|
|
44
|
+
// Pace requests under the API's limit, and still retry 429s (other clients may share the IP),
|
|
45
|
+
// honouring Retry-After.
|
|
46
|
+
const fetchWithRetry = pipeline(
|
|
47
|
+
fetch,
|
|
48
|
+
withTimeout(REQUEST_TIMEOUT_MS),
|
|
49
|
+
withRateLimit(RATE_LIMIT),
|
|
50
|
+
withRetry({ retries: MAX_RETRIES })
|
|
51
|
+
)
|
|
52
|
+
return {
|
|
53
|
+
client: createApiClient({ baseUrl: baseUrl ?? DEFAULT_API_URL, fetch: fetchWithRetry }),
|
|
54
|
+
network: networkFromChainId(chainId),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Return `null` instead of throwing for a filecoin-api 404. */
|
|
59
|
+
async function orNull<T>(promise: Promise<T>): Promise<T | null> {
|
|
60
|
+
try {
|
|
61
|
+
return await promise
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (ApiError.isNotFound(error)) return null
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Collect every item from a cursor-paginated walk. */
|
|
69
|
+
async function collect<T>(pages: AsyncIterable<T[]>): Promise<T[]> {
|
|
70
|
+
const items: T[] = []
|
|
71
|
+
for await (const page of pages) items.push(...page)
|
|
72
|
+
return items
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Walk a cursor-paginated list, yielding one page of items at a time.
|
|
77
|
+
*
|
|
78
|
+
* @param fetchPage - Fetches the page after `cursor` (`undefined` for the first page).
|
|
79
|
+
*/
|
|
80
|
+
async function* paginate<T>(
|
|
81
|
+
fetchPage: (cursor: string | undefined) => Promise<{ data: T[]; nextCursor: string | null }>
|
|
82
|
+
): AsyncGenerator<T[]> {
|
|
83
|
+
let cursor: string | undefined
|
|
84
|
+
do {
|
|
85
|
+
const page = await fetchPage(cursor)
|
|
86
|
+
yield page.data
|
|
87
|
+
cursor = page.nextCursor ?? undefined
|
|
88
|
+
} while (cursor)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Get a provider by ID, or `null` when it does not exist. */
|
|
92
|
+
export function getProvider({ client, network }: IndexerApi, providerId: bigint): Promise<Provider | null> {
|
|
93
|
+
return orNull(
|
|
94
|
+
client
|
|
95
|
+
.GET('/{network}/providers/{providerId}', {
|
|
96
|
+
params: { path: { network, providerId: providerId.toString() } },
|
|
97
|
+
})
|
|
98
|
+
.then((result) => unwrap(result).data)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** List every provider matching the filters. */
|
|
103
|
+
export function listProviders(
|
|
104
|
+
{ client, network }: IndexerApi,
|
|
105
|
+
query: { active?: boolean; approved?: boolean; endorsed?: boolean } = {}
|
|
106
|
+
): Promise<Provider[]> {
|
|
107
|
+
return collect(
|
|
108
|
+
paginate((cursor) =>
|
|
109
|
+
client
|
|
110
|
+
.GET('/{network}/providers', {
|
|
111
|
+
params: {
|
|
112
|
+
path: { network },
|
|
113
|
+
query: {
|
|
114
|
+
limit: MAX_PAGE_SIZE,
|
|
115
|
+
cursor,
|
|
116
|
+
active: bool(query.active),
|
|
117
|
+
approved: bool(query.approved),
|
|
118
|
+
endorsed: bool(query.endorsed),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
.then(unwrap)
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Get a data set by ID, or `null` when it does not exist. */
|
|
128
|
+
export function getDataSet({ client, network }: IndexerApi, dataSetId: bigint): Promise<DataSet | null> {
|
|
129
|
+
return orNull(
|
|
130
|
+
client
|
|
131
|
+
.GET('/{network}/data-sets/{dataSetId}', {
|
|
132
|
+
params: { path: { network, dataSetId: dataSetId.toString() } },
|
|
133
|
+
})
|
|
134
|
+
.then((result) => unwrap(result).data)
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** List every data set matching the filters, newest ID first. */
|
|
139
|
+
export function listDataSets(
|
|
140
|
+
{ client, network }: IndexerApi,
|
|
141
|
+
query: { owner?: string; providerId?: bigint; deleted?: boolean; withCdn?: boolean }
|
|
142
|
+
): Promise<DataSet[]> {
|
|
143
|
+
return collect(
|
|
144
|
+
paginate((cursor) =>
|
|
145
|
+
client
|
|
146
|
+
.GET('/{network}/data-sets', {
|
|
147
|
+
params: {
|
|
148
|
+
path: { network },
|
|
149
|
+
query: {
|
|
150
|
+
limit: MAX_PAGE_SIZE,
|
|
151
|
+
cursor,
|
|
152
|
+
owner: query.owner,
|
|
153
|
+
provider_id: query.providerId?.toString(),
|
|
154
|
+
deleted: bool(query.deleted),
|
|
155
|
+
with_cdn: bool(query.withCdn),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
.then(unwrap)
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Walk the pieces of one data set page by page, newest piece ID first. */
|
|
165
|
+
export function dataSetPiecePages(
|
|
166
|
+
{ client, network }: IndexerApi,
|
|
167
|
+
dataSetId: bigint,
|
|
168
|
+
query: { removed?: boolean } = {}
|
|
169
|
+
): AsyncGenerator<Piece[]> {
|
|
170
|
+
return paginate((cursor) =>
|
|
171
|
+
client
|
|
172
|
+
.GET('/{network}/data-sets/{dataSetId}/pieces', {
|
|
173
|
+
params: {
|
|
174
|
+
path: { network, dataSetId: dataSetId.toString() },
|
|
175
|
+
query: { limit: MAX_PAGE_SIZE, cursor, removed: bool(query.removed) },
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
.then(unwrap)
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** List every piece in one data set, newest piece ID first. */
|
|
183
|
+
export function listDataSetPieces(
|
|
184
|
+
api: IndexerApi,
|
|
185
|
+
dataSetId: bigint,
|
|
186
|
+
query: { removed?: boolean } = {}
|
|
187
|
+
): Promise<Piece[]> {
|
|
188
|
+
return collect(dataSetPiecePages(api, dataSetId, query))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Walk the pieces of every data set a provider stores, page by page, newest data set and piece
|
|
193
|
+
* first. One walk covers all of the provider's data sets.
|
|
194
|
+
*/
|
|
195
|
+
export function providerPiecePages(
|
|
196
|
+
{ client, network }: IndexerApi,
|
|
197
|
+
providerId: bigint,
|
|
198
|
+
query: { removed?: boolean } = {}
|
|
199
|
+
): AsyncGenerator<PieceWithDataSet[]> {
|
|
200
|
+
return paginate((cursor) =>
|
|
201
|
+
client
|
|
202
|
+
.GET('/{network}/pieces', {
|
|
203
|
+
params: {
|
|
204
|
+
path: { network },
|
|
205
|
+
query: { limit: MAX_PAGE_SIZE, cursor, provider_id: providerId.toString(), removed: bool(query.removed) },
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
.then(unwrap)
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Encode an optional boolean filter as a query string value. */
|
|
213
|
+
function bool(value: boolean | undefined): 'true' | 'false' | undefined {
|
|
214
|
+
return value === undefined ? undefined : value ? 'true' : 'false'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Convert API piece metadata to the string record stored on local operations. */
|
|
218
|
+
export function toStringMetadata(metadata: Record<string, unknown> | null): Record<string, string> {
|
|
219
|
+
return Object.fromEntries(Object.entries(metadata ?? {}).map(([key, value]) => [key, String(value)]))
|
|
220
|
+
}
|
package/src/commands/datasets.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as p from '@clack/prompts'
|
|
2
2
|
import * as SP from '@filoz/synapse-core/sp'
|
|
3
3
|
import { getPdpDataSet } from '@filoz/synapse-core/warm-storage'
|
|
4
|
-
import { and, asc, eq } from 'drizzle-orm'
|
|
5
4
|
import { Cli, z } from 'incur'
|
|
6
5
|
import { isAddress } from 'viem'
|
|
6
|
+
import { getDataSet, getProvider, listDataSetPieces, listDataSets, listProviders } from '../api.ts'
|
|
7
7
|
import { contextMiddleware, contextSchema } from '../middleware.ts'
|
|
8
8
|
import { globalOptions, hashLink } from '../utils.ts'
|
|
9
9
|
|
|
@@ -18,42 +18,33 @@ datasets.command('list', {
|
|
|
18
18
|
options: globalOptions.extend({
|
|
19
19
|
providerId: z.coerce.bigint().optional().describe('Filter datasets by provider ID'),
|
|
20
20
|
payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Filter datasets by payer address'),
|
|
21
|
+
stats: z.boolean().optional().default(false).describe('Include piece counts (slow: walks every piece)'),
|
|
21
22
|
}),
|
|
22
23
|
middleware: [contextMiddleware],
|
|
23
24
|
run: async (c) => {
|
|
24
25
|
try {
|
|
25
|
-
const
|
|
26
|
+
const { api } = c.var
|
|
26
27
|
const payer = c.options.payer?.toLowerCase() ?? c.var.client.account.address.toLowerCase()
|
|
27
|
-
const
|
|
28
|
+
const [rows, providers] = await Promise.all([
|
|
29
|
+
listDataSets(api, { owner: payer, providerId: c.options.providerId, deleted: false }),
|
|
30
|
+
listProviders(api),
|
|
31
|
+
])
|
|
32
|
+
const serviceUrls = new Map(providers.map((provider) => [provider.providerId, provider.serviceUrl]))
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const datasets = await c.var.indexerDb.query.dataSets.findMany({
|
|
34
|
-
where: and(...conditions),
|
|
35
|
-
with: {
|
|
36
|
-
provider: true,
|
|
37
|
-
pieces: {
|
|
38
|
-
where: eq(c.var.indexerDb._.fullSchema.pieces.removed, false),
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
orderBy: [asc(schema.dataSets.dataSetId)],
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const datasetsFlattened = datasets.map((dataset) => {
|
|
45
|
-
const { provider, pieces } = dataset
|
|
46
|
-
return {
|
|
34
|
+
const datasetsFlattened = []
|
|
35
|
+
for (const dataset of rows.reverse()) {
|
|
36
|
+
datasetsFlattened.push({
|
|
47
37
|
id: dataset.dataSetId,
|
|
48
38
|
withCdn: dataset.withCdn,
|
|
49
39
|
withIpfsIndexing: dataset.withIpfsIndexing,
|
|
50
40
|
source: dataset.source,
|
|
51
|
-
provider:
|
|
41
|
+
provider: dataset.providerId == null ? null : serviceUrls.get(dataset.providerId),
|
|
52
42
|
pdpEndEpoch: dataset.pdpEndEpoch,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
...(c.options.stats && {
|
|
44
|
+
pieces: (await listDataSetPieces(api, BigInt(dataset.dataSetId), { removed: false })).length,
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
57
48
|
|
|
58
49
|
return c.ok({
|
|
59
50
|
datasets: datasetsFlattened,
|
|
@@ -92,12 +83,19 @@ datasets.command('terminate', {
|
|
|
92
83
|
retryable: false,
|
|
93
84
|
})
|
|
94
85
|
}
|
|
86
|
+
if (!dataset.provider) {
|
|
87
|
+
return c.error({
|
|
88
|
+
code: 'PROVIDER_NOT_FOUND',
|
|
89
|
+
message: `Provider ${dataset.providerId} for dataset ${dataset.dataSetId} is not available`,
|
|
90
|
+
retryable: false,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
95
93
|
if (isInteractive) {
|
|
96
|
-
spinner.message(`Terminating dataset ${dataset
|
|
94
|
+
spinner.message(`Terminating dataset ${dataset.dataSetId} at ${dataset.provider.pdp.serviceURL}...`)
|
|
97
95
|
}
|
|
98
96
|
const result = await SP.terminateService(c.var.client, {
|
|
99
97
|
dataSetId: c.args.id,
|
|
100
|
-
serviceURL: dataset
|
|
98
|
+
serviceURL: dataset.provider.pdp.serviceURL,
|
|
101
99
|
})
|
|
102
100
|
|
|
103
101
|
const waitForResult = await SP.waitForTerminateService({
|
|
@@ -138,15 +136,8 @@ datasets.command('show', {
|
|
|
138
136
|
middleware: [contextMiddleware],
|
|
139
137
|
run: async (c) => {
|
|
140
138
|
try {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
with: {
|
|
144
|
-
provider: true,
|
|
145
|
-
pieces: {
|
|
146
|
-
where: eq(c.var.indexerDb._.fullSchema.pieces.removed, false),
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
})
|
|
139
|
+
const { api } = c.var
|
|
140
|
+
const dataset = await getDataSet(api, c.args.id)
|
|
150
141
|
|
|
151
142
|
if (!dataset) {
|
|
152
143
|
return c.error({
|
|
@@ -155,7 +146,11 @@ datasets.command('show', {
|
|
|
155
146
|
retryable: false,
|
|
156
147
|
})
|
|
157
148
|
}
|
|
158
|
-
|
|
149
|
+
const [provider, pieces] = await Promise.all([
|
|
150
|
+
dataset.providerId == null ? null : getProvider(api, BigInt(dataset.providerId)),
|
|
151
|
+
listDataSetPieces(api, c.args.id, { removed: false }),
|
|
152
|
+
])
|
|
153
|
+
return c.ok({ ...dataset, provider, pieces: pieces.reverse() })
|
|
159
154
|
} catch (error) {
|
|
160
155
|
if (c.options.debug) {
|
|
161
156
|
console.error(error)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { and, asc, count, eq, inArray, isNull, lte, or, type SQLWrapper, sum } from 'drizzle-orm'
|
|
2
1
|
import { Cli, z } from 'incur'
|
|
3
2
|
import { getBlockNumber } from 'viem/actions'
|
|
3
|
+
import { listProviders, providerPiecePages } from '../api.ts'
|
|
4
|
+
import { getActiveDataSetIds } from '../db/get-active-data-set-ids.ts'
|
|
4
5
|
import { contextMiddleware, contextSchema } from '../middleware.ts'
|
|
5
6
|
import { globalOptions } from '../utils.ts'
|
|
6
7
|
|
|
@@ -12,6 +13,13 @@ function formatBytesAsGb(bytes: bigint): string {
|
|
|
12
13
|
return `${whole}.${fraction.toString().padStart(2, '0')} GB`
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** Compare decimal ID strings numerically. */
|
|
17
|
+
function compareIds(a: string, b: string): number {
|
|
18
|
+
const x = BigInt(a)
|
|
19
|
+
const y = BigInt(b)
|
|
20
|
+
return x < y ? -1 : x > y ? 1 : 0
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export const providers = Cli.create('providers', {
|
|
16
24
|
description: 'Provider commands',
|
|
17
25
|
options: globalOptions,
|
|
@@ -22,51 +30,36 @@ providers.command('list', {
|
|
|
22
30
|
description: 'List all providers from the indexer',
|
|
23
31
|
options: globalOptions.extend({
|
|
24
32
|
all: z.boolean().optional().default(false).describe('Include all providers'),
|
|
33
|
+
stats: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.default(false)
|
|
37
|
+
.describe('Include piece count and total size (slow: walks every piece of each provider)'),
|
|
25
38
|
}),
|
|
26
39
|
middleware: [contextMiddleware],
|
|
27
40
|
run: async (c) => {
|
|
28
41
|
try {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
]
|
|
34
|
-
if (!c.options.all) {
|
|
35
|
-
filters.push(or(eq(schema.providers.approved, true), eq(schema.providers.endorsed, true)))
|
|
36
|
-
}
|
|
37
|
-
const blockNumber = await getBlockNumber(c.var.client)
|
|
38
|
-
const rows = await c.var.indexerDb.query.providers.findMany({
|
|
39
|
-
orderBy: [asc(schema.providers.providerId)],
|
|
40
|
-
where: and(...filters),
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
const providerIds = rows.map((provider) => provider.providerId)
|
|
44
|
-
const statsByProviderId = new Map<bigint, { pieceCount: number; totalSize: bigint }>()
|
|
42
|
+
const { api } = c.var
|
|
43
|
+
const rows = (await listProviders(api, { active: true }))
|
|
44
|
+
.filter((provider) => provider.pdpProductActive && (c.options.all || provider.approved || provider.endorsed))
|
|
45
|
+
.sort((a, b) => compareIds(a.providerId, b.providerId))
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
const stats = await c.var.indexerDb
|
|
48
|
-
.select({
|
|
49
|
-
providerId: schema.dataSets.providerId,
|
|
50
|
-
pieceCount: count(schema.pieces.pieceId),
|
|
51
|
-
totalSize: sum(schema.pieces.rawSize),
|
|
52
|
-
})
|
|
53
|
-
.from(schema.pieces)
|
|
54
|
-
.innerJoin(schema.dataSets, eq(schema.pieces.dataSetId, schema.dataSets.dataSetId))
|
|
55
|
-
.where(
|
|
56
|
-
and(
|
|
57
|
-
inArray(schema.dataSets.providerId, providerIds),
|
|
58
|
-
eq(schema.dataSets.deleted, false),
|
|
59
|
-
or(isNull(schema.dataSets.pdpEndEpoch), lte(schema.dataSets.pdpEndEpoch, blockNumber)),
|
|
60
|
-
eq(schema.pieces.removed, false)
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
.groupBy(schema.dataSets.providerId)
|
|
47
|
+
const statsByProviderId = new Map<string, { pieceCount: number; totalSize: bigint }>()
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
})
|
|
49
|
+
if (c.options.stats && rows.length > 0) {
|
|
50
|
+
const blockNumber = await getBlockNumber(c.var.client)
|
|
51
|
+
for (const provider of rows) {
|
|
52
|
+
const providerId = BigInt(provider.providerId)
|
|
53
|
+
const activeDataSetIds = await getActiveDataSetIds({ api, providerId, blockNumber })
|
|
54
|
+
const stats = { pieceCount: 0, totalSize: 0n }
|
|
55
|
+
for await (const pieces of providerPiecePages(api, providerId, { removed: false })) {
|
|
56
|
+
for (const piece of pieces) {
|
|
57
|
+
if (!activeDataSetIds.has(piece.dataSetId)) continue
|
|
58
|
+
stats.pieceCount++
|
|
59
|
+
stats.totalSize += BigInt(piece.rawSize ?? 0)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
statsByProviderId.set(provider.providerId, stats)
|
|
70
63
|
}
|
|
71
64
|
}
|
|
72
65
|
|
|
@@ -78,8 +71,7 @@ providers.command('list', {
|
|
|
78
71
|
serviceUrl: provider.serviceUrl,
|
|
79
72
|
approved: provider.approved,
|
|
80
73
|
endorsed: provider.endorsed,
|
|
81
|
-
pieceCount: stats
|
|
82
|
-
totalSize: formatBytesAsGb(stats?.totalSize ?? 0n),
|
|
74
|
+
...(stats && { pieceCount: stats.pieceCount, totalSize: formatBytesAsGb(stats.totalSize) }),
|
|
83
75
|
}
|
|
84
76
|
})
|
|
85
77
|
|
package/src/commands/repair.ts
CHANGED
|
@@ -135,7 +135,11 @@ repair.command('run', {
|
|
|
135
135
|
}),
|
|
136
136
|
options: globalOptions.extend({
|
|
137
137
|
concurrency: z.coerce.number().min(1).max(10).default(4).describe('Concurrency level'),
|
|
138
|
-
batchSize: z.coerce
|
|
138
|
+
batchSize: z.coerce
|
|
139
|
+
.number()
|
|
140
|
+
.min(1)
|
|
141
|
+
.optional()
|
|
142
|
+
.describe('Max pieces per batch. Defaults to as many as fit in one addPieces message'),
|
|
139
143
|
payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'),
|
|
140
144
|
}),
|
|
141
145
|
middleware: [contextMiddleware],
|
|
@@ -136,7 +136,11 @@ replicate.command('run', {
|
|
|
136
136
|
}),
|
|
137
137
|
options: globalOptions.extend({
|
|
138
138
|
concurrency: z.coerce.number().min(1).max(10).default(4).describe('Concurrency level'),
|
|
139
|
-
batchSize: z.coerce
|
|
139
|
+
batchSize: z.coerce
|
|
140
|
+
.number()
|
|
141
|
+
.min(1)
|
|
142
|
+
.optional()
|
|
143
|
+
.describe('Max pieces per batch. Defaults to as many as fit in one addPieces message'),
|
|
140
144
|
payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'),
|
|
141
145
|
}),
|
|
142
146
|
middleware: [contextMiddleware],
|
|
@@ -137,7 +137,11 @@ secondCopy.command('run', {
|
|
|
137
137
|
}),
|
|
138
138
|
options: globalOptions.extend({
|
|
139
139
|
concurrency: z.coerce.number().min(1).max(10).default(4).describe('Concurrency level'),
|
|
140
|
-
batchSize: z.coerce
|
|
140
|
+
batchSize: z.coerce
|
|
141
|
+
.number()
|
|
142
|
+
.min(1)
|
|
143
|
+
.optional()
|
|
144
|
+
.describe('Max pieces per batch. Defaults to as many as fit in one addPieces message'),
|
|
141
145
|
payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'),
|
|
142
146
|
}),
|
|
143
147
|
middleware: [contextMiddleware],
|