@filoz/repair-cli 0.3.0 → 1.0.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 +3 -3
- 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/cli.js +2 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/datasets.d.ts +3 -5
- package/dist/src/commands/datasets.d.ts.map +1 -1
- package/dist/src/commands/datasets.js +24 -33
- package/dist/src/commands/datasets.js.map +1 -1
- package/dist/src/commands/providers.d.ts +3 -5
- 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 +3 -5
- package/dist/src/commands/repair.d.ts.map +1 -1
- package/dist/src/commands/repair.js +3 -3
- package/dist/src/commands/repair.js.map +1 -1
- package/dist/src/commands/replicate.d.ts +3 -5
- package/dist/src/commands/replicate.d.ts.map +1 -1
- package/dist/src/commands/replicate.js +3 -3
- package/dist/src/commands/replicate.js.map +1 -1
- package/dist/src/commands/second-copy.d.ts +23 -0
- package/dist/src/commands/second-copy.d.ts.map +1 -0
- package/dist/src/commands/second-copy.js +175 -0
- package/dist/src/commands/second-copy.js.map +1 -0
- package/dist/src/commands/session-key.d.ts +3 -5
- 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 +3 -5
- 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.d.ts.map +1 -1
- package/dist/src/db/repair-create.js +5 -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 +15 -21
- package/dist/src/db/replicate-create.js.map +1 -1
- package/dist/src/db/second-copy-create.d.ts +10 -0
- package/dist/src/db/second-copy-create.d.ts.map +1 -0
- package/dist/src/db/second-copy-create.js +104 -0
- package/dist/src/db/second-copy-create.js.map +1 -0
- package/dist/src/local-schema.d.ts +41 -1
- package/dist/src/local-schema.d.ts.map +1 -1
- package/dist/src/local-schema.js +2 -0
- package/dist/src/local-schema.js.map +1 -1
- package/dist/src/middleware.d.ts +7 -11
- 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 +3 -3
- package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
- package/dist/src/pipeline/add-pieces.js +15 -7
- 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 +6 -7
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +7 -10
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +21 -2
- package/dist/src/utils.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +98 -13
- package/src/api.ts +220 -0
- package/src/cli.ts +2 -0
- package/src/commands/datasets.ts +24 -36
- package/src/commands/providers.ts +34 -42
- package/src/commands/repair.ts +3 -3
- package/src/commands/replicate.ts +3 -3
- package/src/commands/second-copy.ts +187 -0
- 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 +5 -3
- package/src/db/replicate-create.ts +15 -21
- package/src/db/second-copy-create.ts +147 -0
- package/src/local-schema.ts +3 -0
- package/src/middleware.ts +7 -11
- package/src/pipeline/add-pieces.ts +21 -11
- package/src/pipeline/create-datasets.ts +9 -29
- package/src/types.ts +9 -7
- package/src/utils.ts +22 -4
- 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/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { datasets } from './commands/datasets.ts'
|
|
|
4
4
|
import { providers } from './commands/providers.ts'
|
|
5
5
|
import { repair } from './commands/repair.ts'
|
|
6
6
|
import { replicate } from './commands/replicate.ts'
|
|
7
|
+
import { secondCopy } from './commands/second-copy.ts'
|
|
7
8
|
import { sessionKey } from './commands/session-key.ts'
|
|
8
9
|
import { setup } from './commands/setup.ts'
|
|
9
10
|
import { wallet } from './commands/wallet.ts'
|
|
@@ -18,6 +19,7 @@ cli.command(setup)
|
|
|
18
19
|
cli.command(wallet)
|
|
19
20
|
cli.command(repair)
|
|
20
21
|
cli.command(replicate)
|
|
22
|
+
cli.command(secondCopy)
|
|
21
23
|
cli.command(datasets)
|
|
22
24
|
cli.command(providers)
|
|
23
25
|
cli.command(sessionKey)
|
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,
|
|
@@ -138,15 +129,8 @@ datasets.command('show', {
|
|
|
138
129
|
middleware: [contextMiddleware],
|
|
139
130
|
run: async (c) => {
|
|
140
131
|
try {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
with: {
|
|
144
|
-
provider: true,
|
|
145
|
-
pieces: {
|
|
146
|
-
where: eq(c.var.indexerDb._.fullSchema.pieces.removed, false),
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
})
|
|
132
|
+
const { api } = c.var
|
|
133
|
+
const dataset = await getDataSet(api, c.args.id)
|
|
150
134
|
|
|
151
135
|
if (!dataset) {
|
|
152
136
|
return c.error({
|
|
@@ -155,7 +139,11 @@ datasets.command('show', {
|
|
|
155
139
|
retryable: false,
|
|
156
140
|
})
|
|
157
141
|
}
|
|
158
|
-
|
|
142
|
+
const [provider, pieces] = await Promise.all([
|
|
143
|
+
dataset.providerId == null ? null : getProvider(api, BigInt(dataset.providerId)),
|
|
144
|
+
listDataSetPieces(api, c.args.id, { removed: false }),
|
|
145
|
+
])
|
|
146
|
+
return c.ok({ ...dataset, provider, pieces: pieces.reverse() })
|
|
159
147
|
} catch (error) {
|
|
160
148
|
if (c.options.debug) {
|
|
161
149
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, desc, eq, inArray
|
|
1
|
+
import { and, desc, eq, inArray } from 'drizzle-orm'
|
|
2
2
|
import { Cli, z } from 'incur'
|
|
3
3
|
import { isAddress } from 'viem'
|
|
4
4
|
import { repairCreate } from '../db/repair-create.ts'
|
|
@@ -52,7 +52,7 @@ repair.command('list', {
|
|
|
52
52
|
try {
|
|
53
53
|
const localSchema = c.var.localDb._.fullSchema
|
|
54
54
|
const repairs = await c.var.localDb.query.repairs.findMany({
|
|
55
|
-
where:
|
|
55
|
+
where: eq(localSchema.repairs.kind, 'repair'),
|
|
56
56
|
orderBy: [desc(localSchema.repairs.createdAt)],
|
|
57
57
|
with: {
|
|
58
58
|
operations: true,
|
|
@@ -147,7 +147,7 @@ repair.command('run', {
|
|
|
147
147
|
where: and(
|
|
148
148
|
eq(schema.repairs.id, c.args.repairId),
|
|
149
149
|
inArray(schema.repairs.status, ['pending', 'failed']),
|
|
150
|
-
|
|
150
|
+
eq(schema.repairs.kind, 'repair')
|
|
151
151
|
),
|
|
152
152
|
})
|
|
153
153
|
if (!repair) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, desc, eq, inArray
|
|
1
|
+
import { and, desc, eq, inArray } from 'drizzle-orm'
|
|
2
2
|
import { Cli, z } from 'incur'
|
|
3
3
|
import { isAddress } from 'viem'
|
|
4
4
|
import { repairDelete } from '../db/repair-delete.ts'
|
|
@@ -52,7 +52,7 @@ replicate.command('list', {
|
|
|
52
52
|
try {
|
|
53
53
|
const localSchema = c.var.localDb._.fullSchema
|
|
54
54
|
const replications = await c.var.localDb.query.repairs.findMany({
|
|
55
|
-
where:
|
|
55
|
+
where: eq(localSchema.repairs.kind, 'replicate'),
|
|
56
56
|
orderBy: [desc(localSchema.repairs.createdAt)],
|
|
57
57
|
with: {
|
|
58
58
|
operations: true,
|
|
@@ -148,7 +148,7 @@ replicate.command('run', {
|
|
|
148
148
|
where: and(
|
|
149
149
|
eq(schema.repairs.id, c.args.replicateId),
|
|
150
150
|
inArray(schema.repairs.status, ['pending', 'failed']),
|
|
151
|
-
|
|
151
|
+
eq(schema.repairs.kind, 'replicate')
|
|
152
152
|
),
|
|
153
153
|
})
|
|
154
154
|
if (!repair) {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { and, desc, eq, inArray } from 'drizzle-orm'
|
|
2
|
+
import { Cli, z } from 'incur'
|
|
3
|
+
import { isAddress } from 'viem'
|
|
4
|
+
import { repairDelete } from '../db/repair-delete.ts'
|
|
5
|
+
import { secondCopyCreate } from '../db/second-copy-create.ts'
|
|
6
|
+
import { contextMiddleware, contextSchema } from '../middleware.ts'
|
|
7
|
+
import { runAddPieces } from '../pipeline/add-pieces.ts'
|
|
8
|
+
import { ensureRepairDataset } from '../pipeline/create-datasets.ts'
|
|
9
|
+
import { globalOptions } from '../utils.ts'
|
|
10
|
+
|
|
11
|
+
export const secondCopy = Cli.create('second-copy', {
|
|
12
|
+
description: 'Second-copy piece commands',
|
|
13
|
+
vars: contextSchema,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
secondCopy.command('create', {
|
|
17
|
+
description: 'Create a second-copy job from a pieces file',
|
|
18
|
+
options: globalOptions.extend({
|
|
19
|
+
piecesFile: z.string().describe('Path to JSON file with aggregates[].piece_cid'),
|
|
20
|
+
sourceProviderUrl: z.string().url().describe('Source provider URL to pull pieces from'),
|
|
21
|
+
targetProviderId: z.coerce.bigint().describe('Target provider ID for second-copy'),
|
|
22
|
+
}),
|
|
23
|
+
middleware: [contextMiddleware],
|
|
24
|
+
run: async (c) => {
|
|
25
|
+
try {
|
|
26
|
+
const { piecesFile, sourceProviderUrl, targetProviderId } = c.options
|
|
27
|
+
|
|
28
|
+
const secondCopyId = await secondCopyCreate({
|
|
29
|
+
...c.var,
|
|
30
|
+
piecesFile,
|
|
31
|
+
sourceProviderUrl,
|
|
32
|
+
targetProviderId,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return c.ok({
|
|
36
|
+
secondCopyId,
|
|
37
|
+
})
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(error)
|
|
40
|
+
return c.error({
|
|
41
|
+
code: 'SECOND_COPY_FAILED',
|
|
42
|
+
message: error instanceof Error ? error.message : 'Failed to create the second-copy job',
|
|
43
|
+
retryable: true,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
secondCopy.command('list', {
|
|
50
|
+
description: 'List all second-copy jobs',
|
|
51
|
+
options: globalOptions,
|
|
52
|
+
middleware: [contextMiddleware],
|
|
53
|
+
run: async (c) => {
|
|
54
|
+
try {
|
|
55
|
+
const localSchema = c.var.localDb._.fullSchema
|
|
56
|
+
const jobs = await c.var.localDb.query.repairs.findMany({
|
|
57
|
+
where: eq(localSchema.repairs.kind, 'second_copy'),
|
|
58
|
+
orderBy: [desc(localSchema.repairs.createdAt)],
|
|
59
|
+
with: {
|
|
60
|
+
operations: true,
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const secondCopies = jobs.map((repair) => {
|
|
65
|
+
const { operations, ...job } = repair
|
|
66
|
+
return {
|
|
67
|
+
id: job.id,
|
|
68
|
+
status: job.status,
|
|
69
|
+
sourceProviderUrl: job.sourceProviderUrl,
|
|
70
|
+
targetProviderId: job.targetProviderId,
|
|
71
|
+
targetProviderUrl: job.targetProviderUrl,
|
|
72
|
+
targetDataSetId: job.targetDataSetId,
|
|
73
|
+
blockNumber: job.blockNumber,
|
|
74
|
+
operations: operations.length,
|
|
75
|
+
pending: operations.filter((operation) => operation.status === 'pending').length,
|
|
76
|
+
failed: operations.filter((operation) => operation.status === 'failed').length,
|
|
77
|
+
completed: operations.filter((operation) => operation.status === 'completed').length,
|
|
78
|
+
skipped: operations.filter((operation) => operation.status === 'skipped').length,
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return c.ok({
|
|
83
|
+
secondCopies,
|
|
84
|
+
})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(error)
|
|
87
|
+
return c.error({
|
|
88
|
+
code: 'SECOND_COPY_FAILED',
|
|
89
|
+
message: error instanceof Error ? error.message : 'Failed to list second-copy jobs',
|
|
90
|
+
retryable: true,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
secondCopy.command('delete', {
|
|
97
|
+
description: 'Delete a second-copy job',
|
|
98
|
+
args: z.object({
|
|
99
|
+
secondCopyId: z.coerce.number().describe('Second-copy ID to delete'),
|
|
100
|
+
}),
|
|
101
|
+
options: globalOptions,
|
|
102
|
+
middleware: [contextMiddleware],
|
|
103
|
+
run: async (c) => {
|
|
104
|
+
try {
|
|
105
|
+
const { deleted, operationsDeleted } = await repairDelete({
|
|
106
|
+
localDb: c.var.localDb,
|
|
107
|
+
repairId: c.args.secondCopyId,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (!deleted) {
|
|
111
|
+
return c.error({
|
|
112
|
+
code: 'SECOND_COPY_NOT_FOUND',
|
|
113
|
+
message: 'Second-copy job not found',
|
|
114
|
+
retryable: false,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return c.ok({
|
|
119
|
+
secondCopyId: c.args.secondCopyId,
|
|
120
|
+
operationsDeleted,
|
|
121
|
+
})
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(error)
|
|
124
|
+
return c.error({
|
|
125
|
+
code: 'SECOND_COPY_FAILED',
|
|
126
|
+
message: error instanceof Error ? error.message : 'Failed to delete the second-copy job',
|
|
127
|
+
retryable: true,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
secondCopy.command('run', {
|
|
134
|
+
description: 'Run a second-copy job',
|
|
135
|
+
args: z.object({
|
|
136
|
+
secondCopyId: z.coerce.number().describe('Second-copy ID to run'),
|
|
137
|
+
}),
|
|
138
|
+
options: globalOptions.extend({
|
|
139
|
+
concurrency: z.coerce.number().min(1).max(10).default(4).describe('Concurrency level'),
|
|
140
|
+
batchSize: z.coerce.number().min(1).max(40).default(40).describe('Max pieces per batch'),
|
|
141
|
+
payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'),
|
|
142
|
+
}),
|
|
143
|
+
middleware: [contextMiddleware],
|
|
144
|
+
run: async (c) => {
|
|
145
|
+
try {
|
|
146
|
+
const schema = c.var.localDb._.fullSchema
|
|
147
|
+
const payer = c.options.payer ?? c.var.client.account.address
|
|
148
|
+
const repair = await c.var.localDb.query.repairs.findFirst({
|
|
149
|
+
where: and(
|
|
150
|
+
eq(schema.repairs.id, c.args.secondCopyId),
|
|
151
|
+
inArray(schema.repairs.status, ['pending', 'failed']),
|
|
152
|
+
eq(schema.repairs.kind, 'second_copy')
|
|
153
|
+
),
|
|
154
|
+
})
|
|
155
|
+
if (!repair) {
|
|
156
|
+
return c.error({
|
|
157
|
+
code: 'SECOND_COPY_NOT_FOUND',
|
|
158
|
+
message: 'Second-copy job not found, it may have already been run or completed',
|
|
159
|
+
retryable: false,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await ensureRepairDataset({
|
|
164
|
+
...c.var,
|
|
165
|
+
repair,
|
|
166
|
+
payer,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
await runAddPieces({
|
|
170
|
+
...c.var,
|
|
171
|
+
repair,
|
|
172
|
+
concurrency: c.options.concurrency,
|
|
173
|
+
batchSize: c.options.batchSize,
|
|
174
|
+
})
|
|
175
|
+
return c.ok({
|
|
176
|
+
secondCopyId: repair.id,
|
|
177
|
+
})
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(error)
|
|
180
|
+
return c.error({
|
|
181
|
+
code: 'SECOND_COPY_FAILED',
|
|
182
|
+
message: error instanceof Error ? error.message : 'Failed to run the second-copy job',
|
|
183
|
+
retryable: true,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
})
|
package/src/commands/setup.ts
CHANGED
|
@@ -3,21 +3,16 @@ import { Cli, z } from 'incur'
|
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import type { Hash } from 'viem'
|
|
5
5
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
6
|
+
import { DEFAULT_API_URL } from '../api.ts'
|
|
6
7
|
import { config, createLocalDatabase, globalOptions, migrateLocalDatabase } from '../utils.ts'
|
|
7
8
|
|
|
8
|
-
function
|
|
9
|
-
let url: URL
|
|
9
|
+
function validateHttpUrl(value: string) {
|
|
10
10
|
try {
|
|
11
|
-
url = new URL(value)
|
|
11
|
+
const url = new URL(value)
|
|
12
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
12
13
|
} catch {
|
|
13
14
|
return false
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
if (url.protocol !== 'postgresql:' && url.protocol !== 'postgres:') {
|
|
17
|
-
return false
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return Boolean(url.hostname && url.username && url.password && url.pathname.length > 1)
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
export const setup = Cli.create('setup', {
|
|
@@ -44,35 +39,18 @@ export const setup = Cli.create('setup', {
|
|
|
44
39
|
})
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
message: 'Enter
|
|
50
|
-
validate(value) {
|
|
51
|
-
if (!value || !validatePostgresUrl(value)) {
|
|
52
|
-
return `Invalid postgres URL!`
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
initialValue: config.get('indexerMainnetUrl'),
|
|
56
|
-
withGuide: false,
|
|
57
|
-
})
|
|
58
|
-
if (p.isCancel(indexerMainnetUrl)) {
|
|
59
|
-
return c.error({
|
|
60
|
-
code: 'SETUP_CANCELLED',
|
|
61
|
-
message: 'Setup cancelled',
|
|
62
|
-
retryable: false,
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
const indexerCalibrationUrl = await p.text({
|
|
66
|
-
message: 'Enter your Calibration Indexer Postgres URL',
|
|
42
|
+
// API URL
|
|
43
|
+
const apiUrl = await p.text({
|
|
44
|
+
message: 'Enter the Filecoin API URL',
|
|
67
45
|
validate(value) {
|
|
68
|
-
if (!value || !
|
|
69
|
-
return `Invalid
|
|
46
|
+
if (!value || !validateHttpUrl(value)) {
|
|
47
|
+
return `Invalid http(s) URL!`
|
|
70
48
|
}
|
|
71
49
|
},
|
|
72
|
-
initialValue: config.get('
|
|
50
|
+
initialValue: config.get('apiUrl') ?? DEFAULT_API_URL,
|
|
73
51
|
withGuide: false,
|
|
74
52
|
})
|
|
75
|
-
if (p.isCancel(
|
|
53
|
+
if (p.isCancel(apiUrl)) {
|
|
76
54
|
return c.error({
|
|
77
55
|
code: 'SETUP_CANCELLED',
|
|
78
56
|
message: 'Setup cancelled',
|
|
@@ -129,8 +107,7 @@ export const setup = Cli.create('setup', {
|
|
|
129
107
|
|
|
130
108
|
// Set config
|
|
131
109
|
config.set('privateKey', pk)
|
|
132
|
-
config.set('
|
|
133
|
-
config.set('indexerCalibrationUrl', indexerCalibrationUrl)
|
|
110
|
+
config.set('apiUrl', apiUrl)
|
|
134
111
|
config.set('chainId', chainId)
|
|
135
112
|
config.set('dbPath', dbPath)
|
|
136
113
|
config.set('source', source)
|
package/src/db/dedupe-cids.ts
CHANGED
|
@@ -1,37 +1,20 @@
|
|
|
1
|
-
import { and, eq, inArray } from 'drizzle-orm'
|
|
2
1
|
import type { OperationSelect } from '../local-schema.ts'
|
|
3
|
-
import type {
|
|
2
|
+
import type { LocalDatabase } from '../types.ts'
|
|
4
3
|
import { upsertOperations } from './upsert-operations.ts'
|
|
5
4
|
|
|
6
|
-
export type
|
|
7
|
-
indexerDb: IndexerDatabase
|
|
5
|
+
export type DedupeCidsOptions = {
|
|
8
6
|
localDb: LocalDatabase
|
|
9
|
-
|
|
7
|
+
/** CIDs already in the target repair dataset. */
|
|
8
|
+
existingCids: ReadonlySet<string>
|
|
10
9
|
operations: OperationSelect[]
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
13
|
+
* Mark operations whose CID is already in the target repair dataset as completed.
|
|
15
14
|
* Returns operations that are not onchain.
|
|
16
15
|
*/
|
|
17
|
-
export async function dedupeCids({
|
|
18
|
-
const
|
|
19
|
-
const schema = indexerDb._.fullSchema
|
|
20
|
-
|
|
21
|
-
const rows = await indexerDb
|
|
22
|
-
.select({ cid: schema.pieces.cid })
|
|
23
|
-
.from(schema.pieces)
|
|
24
|
-
.where(
|
|
25
|
-
and(eq(schema.pieces.dataSetId, dataSetId), eq(schema.pieces.removed, false), inArray(schema.pieces.cid, cids))
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
const existingCids = new Set<string>()
|
|
29
|
-
const completedOperations: OperationSelect[] = []
|
|
30
|
-
|
|
31
|
-
for (const row of rows) {
|
|
32
|
-
completedOperations.push(...operations.filter((operation) => operation.cid === row.cid))
|
|
33
|
-
existingCids.add(row.cid)
|
|
34
|
-
}
|
|
16
|
+
export async function dedupeCids({ localDb, existingCids, operations }: DedupeCidsOptions) {
|
|
17
|
+
const completedOperations = operations.filter((operation) => existingCids.has(operation.cid))
|
|
35
18
|
|
|
36
19
|
if (completedOperations.length > 0) {
|
|
37
20
|
await upsertOperations({
|
|
@@ -1,49 +1,38 @@
|
|
|
1
|
-
import { and, asc, eq, isNull } from 'drizzle-orm'
|
|
2
1
|
import type { Address } from 'viem'
|
|
3
|
-
import
|
|
2
|
+
import { listDataSets } from '../api.ts'
|
|
3
|
+
import type { IndexerApi } from '../types.ts'
|
|
4
4
|
|
|
5
5
|
export type FindRepairDatasetOptions = {
|
|
6
|
-
|
|
6
|
+
api: IndexerApi
|
|
7
7
|
providerId: bigint
|
|
8
8
|
payer: Address
|
|
9
9
|
source: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Find one IPFS-enabled repair dataset for a provider
|
|
13
|
+
* Find one live IPFS-enabled repair dataset for a provider, if it exists.
|
|
14
14
|
*
|
|
15
15
|
* When multiple datasets match, the lowest `dataSetId` is returned.
|
|
16
16
|
*/
|
|
17
17
|
export async function findRepairDataset({
|
|
18
|
-
|
|
18
|
+
api,
|
|
19
19
|
providerId,
|
|
20
20
|
payer,
|
|
21
21
|
source,
|
|
22
22
|
}: FindRepairDatasetOptions): Promise<bigint | null> {
|
|
23
|
-
const
|
|
23
|
+
const dataSets = await listDataSets(api, {
|
|
24
|
+
owner: payer.toLowerCase(),
|
|
25
|
+
providerId,
|
|
26
|
+
deleted: false,
|
|
27
|
+
withCdn: false,
|
|
28
|
+
})
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.where(
|
|
31
|
-
and(
|
|
32
|
-
eq(schema.dataSets.providerId, providerId),
|
|
33
|
-
eq(schema.dataSets.deleted, false),
|
|
34
|
-
isNull(schema.dataSets.pdpEndEpoch),
|
|
35
|
-
eq(schema.dataSets.payer, payer.toLowerCase()),
|
|
36
|
-
eq(schema.dataSets.source, source),
|
|
37
|
-
eq(schema.dataSets.withCdn, false),
|
|
38
|
-
eq(schema.dataSets.withIpfsIndexing, true)
|
|
39
|
-
)
|
|
40
|
-
)
|
|
41
|
-
.orderBy(asc(schema.dataSets.dataSetId))
|
|
42
|
-
.limit(1)
|
|
43
|
-
|
|
44
|
-
if (result.length === 0) {
|
|
45
|
-
return null
|
|
30
|
+
let dataSetId: bigint | null = null
|
|
31
|
+
for (const dataSet of dataSets) {
|
|
32
|
+
if (dataSet.pdpEndEpoch != null || dataSet.source !== source || !dataSet.withIpfsIndexing) continue
|
|
33
|
+
const id = BigInt(dataSet.dataSetId)
|
|
34
|
+
if (dataSetId == null || id < dataSetId) dataSetId = id
|
|
46
35
|
}
|
|
47
36
|
|
|
48
|
-
return
|
|
37
|
+
return dataSetId
|
|
49
38
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type DataSet, listDataSets } from '../api.ts'
|
|
2
|
+
import type { IndexerApi } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether a data set's pieces count at the repair block: not deleted, and either not terminated
|
|
6
|
+
* (no PDP end epoch) or terminated with an end epoch after `blockNumber`, so the provider still
|
|
7
|
+
* has to prove it.
|
|
8
|
+
*/
|
|
9
|
+
export function isDataSetActiveAt(dataSet: Pick<DataSet, 'deleted' | 'pdpEndEpoch'>, blockNumber: bigint): boolean {
|
|
10
|
+
return !dataSet.deleted && (dataSet.pdpEndEpoch == null || BigInt(dataSet.pdpEndEpoch) > blockNumber)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* IDs of a provider's data sets that are active at the repair block (see {@link isDataSetActiveAt}).
|
|
15
|
+
*/
|
|
16
|
+
export async function getActiveDataSetIds({
|
|
17
|
+
api,
|
|
18
|
+
providerId,
|
|
19
|
+
blockNumber,
|
|
20
|
+
}: {
|
|
21
|
+
api: IndexerApi
|
|
22
|
+
providerId: bigint
|
|
23
|
+
blockNumber: bigint
|
|
24
|
+
}): Promise<Set<string>> {
|
|
25
|
+
const dataSets = await listDataSets(api, { providerId, deleted: false })
|
|
26
|
+
return new Set(
|
|
27
|
+
dataSets.filter((dataSet) => isDataSetActiveAt(dataSet, blockNumber)).map((dataSet) => dataSet.dataSetId)
|
|
28
|
+
)
|
|
29
|
+
}
|