@filoz/repair-cli 0.3.0 → 0.4.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 +1 -1
- package/dist/src/cli.js +2 -0
- package/dist/src/cli.js.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.js +3 -3
- package/dist/src/commands/replicate.js.map +1 -1
- package/dist/src/commands/second-copy.d.ts +25 -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/db/repair-create.d.ts.map +1 -1
- package/dist/src/db/repair-create.js +2 -0
- 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 +2 -0
- 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 +40 -0
- 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/utils.d.ts.map +1 -1
- package/dist/src/utils.js +20 -0
- package/dist/src/utils.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +83 -1
- package/src/cli.ts +2 -0
- package/src/commands/repair.ts +3 -3
- package/src/commands/replicate.ts +3 -3
- package/src/commands/second-copy.ts +187 -0
- package/src/db/repair-create.ts +2 -0
- package/src/db/replicate-create.ts +2 -0
- package/src/db/second-copy-create.ts +147 -0
- package/src/local-schema.ts +3 -0
- package/src/pipeline/add-pieces.ts +1 -1
- package/src/utils.ts +20 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { taskLog } from '@clack/prompts'
|
|
3
|
+
import { getBlockNumber } from 'viem/actions'
|
|
4
|
+
import { NoAlternateProviderError, RepairCreationError } from '../error.ts'
|
|
5
|
+
import type { OperationInsert } from '../local-schema.ts'
|
|
6
|
+
import type { Context } from '../types.ts'
|
|
7
|
+
import { getRepairProvider } from './get-repair-provider.ts'
|
|
8
|
+
|
|
9
|
+
const OPERATIONS_INSERT_BATCH_SIZE = 500
|
|
10
|
+
|
|
11
|
+
export interface SecondCopyCreateOptions extends Context {
|
|
12
|
+
piecesFile: string
|
|
13
|
+
sourceProviderUrl: string
|
|
14
|
+
targetProviderId: bigint
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type PiecesFile = {
|
|
18
|
+
aggregates: Array<{ piece_cid?: unknown }>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a provider service URL for storage and equality checks.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeProviderUrl(url: string): string {
|
|
25
|
+
let parsed: URL
|
|
26
|
+
try {
|
|
27
|
+
parsed = new URL(url)
|
|
28
|
+
} catch {
|
|
29
|
+
throw new RepairCreationError(`Invalid source provider URL: ${url}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
33
|
+
throw new RepairCreationError(`Source provider URL must be http(s): ${url}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Drop trailing slash so endpoint comparisons are stable.
|
|
37
|
+
return parsed.toString().replace(/\/$/, '')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a second-copy pieces file and return unique aggregate piece CIDs.
|
|
42
|
+
*/
|
|
43
|
+
export async function parsePieceCidsFromFile(piecesFile: string): Promise<string[]> {
|
|
44
|
+
let raw: unknown
|
|
45
|
+
try {
|
|
46
|
+
raw = JSON.parse(await readFile(piecesFile, 'utf8'))
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new RepairCreationError(
|
|
49
|
+
error instanceof Error ? `Failed to read pieces file: ${error.message}` : 'Failed to read pieces file'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (raw == null || typeof raw !== 'object' || !Array.isArray((raw as PiecesFile).aggregates)) {
|
|
54
|
+
throw new RepairCreationError('Pieces file must contain an "aggregates" array')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cids = new Set<string>()
|
|
58
|
+
for (const aggregate of (raw as PiecesFile).aggregates) {
|
|
59
|
+
if (typeof aggregate?.piece_cid !== 'string' || aggregate.piece_cid.length === 0) {
|
|
60
|
+
throw new RepairCreationError('Each aggregate must include a non-empty "piece_cid" string')
|
|
61
|
+
}
|
|
62
|
+
cids.add(aggregate.piece_cid)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (cids.size === 0) {
|
|
66
|
+
throw new RepairCreationError('Pieces file contains no piece CIDs')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [...cids]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Prepare a second-copy job from a pieces file and an explicit source provider URL.
|
|
74
|
+
*
|
|
75
|
+
* @param options - The options for creating a second-copy job.
|
|
76
|
+
* @returns The ID of the created local job.
|
|
77
|
+
*/
|
|
78
|
+
export async function secondCopyCreate(options: SecondCopyCreateOptions): Promise<number> {
|
|
79
|
+
const { indexerDb, localDb, piecesFile, sourceProviderUrl, targetProviderId, client } = options
|
|
80
|
+
const localSchema = localDb._.fullSchema
|
|
81
|
+
const now = Date.now()
|
|
82
|
+
const blockNumber = await getBlockNumber(client)
|
|
83
|
+
const normalizedSourceUrl = normalizeProviderUrl(sourceProviderUrl)
|
|
84
|
+
|
|
85
|
+
const log = taskLog({
|
|
86
|
+
title: 'Creating second-copy',
|
|
87
|
+
limit: 10,
|
|
88
|
+
retainLog: true,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const targetProvider = await getRepairProvider({
|
|
92
|
+
indexerDb,
|
|
93
|
+
providerId: targetProviderId,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (!targetProvider) {
|
|
97
|
+
throw new NoAlternateProviderError(targetProviderId)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const normalizedTargetUrl = normalizeProviderUrl(targetProvider.serviceUrl)
|
|
101
|
+
if (normalizedSourceUrl === normalizedTargetUrl) {
|
|
102
|
+
throw new RepairCreationError('Source provider URL must differ from the target provider URL')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pieceCids = await parsePieceCidsFromFile(piecesFile)
|
|
106
|
+
log.message(`Loaded ${pieceCids.length} unique piece CIDs from ${piecesFile}`)
|
|
107
|
+
|
|
108
|
+
const [repair] = await localDb
|
|
109
|
+
.insert(localSchema.repairs)
|
|
110
|
+
.values({
|
|
111
|
+
kind: 'second_copy',
|
|
112
|
+
repairProviderId: 0n,
|
|
113
|
+
repairDataSetId: null,
|
|
114
|
+
sourceProviderUrl: normalizedSourceUrl,
|
|
115
|
+
targetProviderId: targetProvider.providerId,
|
|
116
|
+
targetProviderUrl: targetProvider.serviceUrl,
|
|
117
|
+
targetDataSetId: null,
|
|
118
|
+
blockNumber,
|
|
119
|
+
createdAt: now,
|
|
120
|
+
updatedAt: now,
|
|
121
|
+
})
|
|
122
|
+
.returning({ id: localSchema.repairs.id })
|
|
123
|
+
|
|
124
|
+
if (!repair) throw new RepairCreationError('Failed to create second-copy row')
|
|
125
|
+
|
|
126
|
+
let totalOperations = 0
|
|
127
|
+
for (let offset = 0; offset < pieceCids.length; offset += OPERATIONS_INSERT_BATCH_SIZE) {
|
|
128
|
+
const batchCids = pieceCids.slice(offset, offset + OPERATIONS_INSERT_BATCH_SIZE)
|
|
129
|
+
const operations: OperationInsert[] = batchCids.map((cid) => ({
|
|
130
|
+
repairId: repair.id,
|
|
131
|
+
type: 'add_piece',
|
|
132
|
+
status: 'pending',
|
|
133
|
+
cid,
|
|
134
|
+
metadata: {},
|
|
135
|
+
alternateProvider: normalizedSourceUrl,
|
|
136
|
+
createdAt: now,
|
|
137
|
+
updatedAt: now,
|
|
138
|
+
}))
|
|
139
|
+
|
|
140
|
+
await localDb.insert(localSchema.operations).values(operations)
|
|
141
|
+
totalOperations += operations.length
|
|
142
|
+
log.message(`Inserted ${operations.length} operations (${totalOperations}/${pieceCids.length})`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
log.success(`Created second-copy ${repair.id} with ${totalOperations} operations`, { showLog: true })
|
|
146
|
+
return repair.id
|
|
147
|
+
}
|
package/src/local-schema.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { customType, sqliteTable as table } from 'drizzle-orm/sqlite-core'
|
|
|
6
6
|
import * as Json from 'iso-base/json'
|
|
7
7
|
|
|
8
8
|
export type RepairStatus = 'pending' | 'completed' | 'failed'
|
|
9
|
+
export type RepairKind = 'repair' | 'replicate' | 'second_copy'
|
|
9
10
|
export type OperationStatus = 'pending' | 'completed' | 'failed' | 'skipped'
|
|
10
11
|
export type OperationType = 'create_dataset' | 'add_piece'
|
|
11
12
|
|
|
@@ -44,8 +45,10 @@ export type RepairUpdate = Partial<RepairInsert>
|
|
|
44
45
|
export const repairs = table('repairs', {
|
|
45
46
|
id: t.int().primaryKey({ autoIncrement: true }),
|
|
46
47
|
status: t.text().$type<RepairStatus>().notNull().default('pending'),
|
|
48
|
+
kind: t.text().$type<RepairKind>().notNull().default('repair'),
|
|
47
49
|
repairProviderId: bigintType('repair_provider_id').notNull(),
|
|
48
50
|
repairDataSetId: bigintType('repair_data_set_id'),
|
|
51
|
+
sourceProviderUrl: t.text('source_provider_url'),
|
|
49
52
|
targetProviderId: bigintType('target_provider_id').notNull(),
|
|
50
53
|
targetProviderUrl: t.text('target_provider_url').notNull(),
|
|
51
54
|
targetDataSetId: bigintType('target_data_set_id'),
|
|
@@ -75,7 +75,7 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
|
|
|
75
75
|
dataSetId: dataset.dataSetId,
|
|
76
76
|
clientDataSetId: dataset.clientDataSetId,
|
|
77
77
|
pieces: operationsToPullPieces(operations),
|
|
78
|
-
timeout: 1000 * 60 * 30,
|
|
78
|
+
timeout: 1000 * 60 * 30, // 30 minutes
|
|
79
79
|
onStatus: (status) => {
|
|
80
80
|
const completed = status.pieces.filter((piece) => piece.status === 'complete').length
|
|
81
81
|
const failed = status.pieces.filter((piece) => piece.status === 'failed').length
|
package/src/utils.ts
CHANGED
|
@@ -81,6 +81,7 @@ export async function createLocalDatabase(dbPath: string): Promise<LocalDatabase
|
|
|
81
81
|
}) as LocalDatabase
|
|
82
82
|
|
|
83
83
|
await localDb.$client.execute('PRAGMA journal_mode = WAL')
|
|
84
|
+
await migrateLocalDatabase(localDb)
|
|
84
85
|
|
|
85
86
|
return localDb
|
|
86
87
|
}
|
|
@@ -109,6 +110,25 @@ export async function migrateLocalDatabase(db: LocalDatabase) {
|
|
|
109
110
|
} catch {
|
|
110
111
|
// Column already exists on databases created with the updated schema.
|
|
111
112
|
}
|
|
113
|
+
try {
|
|
114
|
+
await db.$client.execute(`
|
|
115
|
+
ALTER TABLE repairs ADD COLUMN kind text NOT NULL DEFAULT 'repair'
|
|
116
|
+
`)
|
|
117
|
+
} catch {
|
|
118
|
+
// Column already exists on databases created with the updated schema.
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await db.$client.execute(`
|
|
122
|
+
ALTER TABLE repairs ADD COLUMN source_provider_url text
|
|
123
|
+
`)
|
|
124
|
+
} catch {
|
|
125
|
+
// Column already exists on databases created with the updated schema.
|
|
126
|
+
}
|
|
127
|
+
await db.$client.execute(`
|
|
128
|
+
UPDATE repairs
|
|
129
|
+
SET kind = 'replicate'
|
|
130
|
+
WHERE repair_data_set_id IS NOT NULL AND (kind IS NULL OR kind = 'repair')
|
|
131
|
+
`)
|
|
112
132
|
await db.$client.execute(`
|
|
113
133
|
CREATE TABLE IF NOT EXISTS operations (
|
|
114
134
|
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|