filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
247 lines (227 loc) • 8.35 kB
text/typescript
/**
* Get Data Set Pieces
*
* Functions for retrieving pieces from a dataset with optional metadata enrichment.
*
* @module core/data-set/get-data-set-pieces
*/
import { getSizeFromPieceCID } from '@filoz/synapse-core/piece'
import {
type DataSetPieceData,
METADATA_KEYS,
PDPServer,
PDPVerifier,
type StorageContext,
type Synapse,
WarmStorageService,
} from '@filoz/synapse-sdk'
import { reconcilePieceStatus } from '../piece/piece-status.js'
import type { Warning } from '../utils/types.js'
import { isStorageContextWithDataSetId } from './type-guards.js'
import type { DataSetPiecesResult, GetDataSetPiecesOptions, PieceInfo, StorageContextWithDataSetId } from './types.js'
import { PieceStatus } from './types.js'
/**
* Get all pieces for a dataset from a StorageContext
*
* This function uses the StorageContext.getPieces() async generator to retrieve
* all pieces in a dataset. Optionally fetches metadata for each piece from WarmStorage.
*
* Example usage:
* ```typescript
* const result = await getDataSetPieces(storageContext, {
* includeMetadata: true,
* batchSize: 100
* })
*
* console.log(`Found ${result.pieces.length} pieces`)
* for (const piece of result.pieces) {
* console.log(` ${piece.pieceCid}`)
* if (piece.rootIpfsCid) {
* console.log(` IPFS: ${piece.rootIpfsCid}`)
* }
* }
* ```
*
* @param storageContext - Storage context from upload or dataset resolution
* @param options - Optional configuration
* @returns Pieces and warnings
*/
export async function getDataSetPieces(
synapse: Synapse,
storageContext: StorageContext,
options?: GetDataSetPiecesOptions
): Promise<DataSetPiecesResult> {
const logger = options?.logger
const includeMetadata = options?.includeMetadata ?? false
const signal = options?.signal
if (!isStorageContextWithDataSetId(storageContext)) {
throw new Error('Storage context does not have a dataset ID')
}
const pieces: PieceInfo[] = []
const warnings: Warning[] = []
// call PDPVerifier.getScheduledRemovals to get the list of pieces that are scheduled for removal
let scheduledRemovals: number[] = []
let pdpServerPieces: DataSetPieceData[] | null = null
try {
const warmStorage = await WarmStorageService.create(synapse.getProvider(), synapse.getWarmStorageAddress())
const pdpVerifier = new PDPVerifier(synapse.getProvider(), warmStorage.getPDPVerifierAddress())
scheduledRemovals = await pdpVerifier.getScheduledRemovals(storageContext.dataSetId)
try {
const providerInfo = await synapse.getProviderInfo(storageContext.provider.serviceProvider)
const pdpServer = new PDPServer(null, providerInfo.products?.PDP?.data?.serviceURL ?? '')
const dataSet = await pdpServer.getDataSet(storageContext.dataSetId)
pdpServerPieces = dataSet.pieces
} catch (error) {
logger?.warn({ error }, 'Failed to fetch provider data for scheduled removals and orphan detection')
warnings.push({
code: 'PROVIDER_DATA_UNAVAILABLE',
message: 'Failed to fetch provider data; orphan detection disabled',
context: { dataSetId: storageContext.dataSetId, error: String(error) },
})
}
} catch (error) {
logger?.warn({ error }, 'Failed to get scheduled removals')
warnings.push({
code: 'SCHEDULED_REMOVALS_UNAVAILABLE',
message: 'Failed to get scheduled removals',
context: { dataSetId: storageContext.dataSetId, error: String(error) },
})
}
// Use the async generator to fetch all pieces
try {
const getPiecesOptions = { ...(signal && { signal }) }
const providerPiecesById = pdpServerPieces ? new Map(pdpServerPieces.map((piece) => [piece.pieceId, piece])) : null
for await (const piece of storageContext.getPieces(getPiecesOptions)) {
const pieceId = piece.pieceId
const pieceCid = piece.pieceCid
const { status, warning } = reconcilePieceStatus({
pieceId,
pieceCid,
scheduledRemovals,
providerPiecesById,
})
const pieceInfo: PieceInfo = {
pieceId,
pieceCid: pieceCid.toString(),
status,
}
if (warning) {
warnings.push(warning)
}
// Calculate piece size from CID
try {
pieceInfo.size = getSizeFromPieceCID(pieceCid)
} catch (error) {
logger?.warn(
{ pieceId: piece.pieceId, pieceCid: piece.pieceCid.toString(), error },
'Failed to calculate piece size from CID'
)
}
pieces.push(pieceInfo)
}
if (providerPiecesById !== null) {
// reconcilePieceStatus removes provider matches as we stream on-chain pieces.
// Remaining entries are only reported by the provider, which are off-chain orphans.
for (const piece of providerPiecesById.values()) {
// add the rest of the pieces to the pieces list
pieces.push({
pieceId: piece.pieceId,
pieceCid: piece.pieceCid.toString(),
status: PieceStatus.OFFCHAIN_ORPHANED,
})
warnings.push({
code: 'OFFCHAIN_ORPHANED',
message: 'Piece is reported by provider but not on-chain',
context: { pieceId: piece.pieceId, pieceCid: piece.pieceCid.toString() },
})
}
}
pieces.sort((a, b) => a.pieceId - b.pieceId)
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw error
}
// If getPieces fails completely, throw - this is a critical error
logger?.error({ dataSetId: storageContext.dataSetId, error }, 'Failed to retrieve pieces from dataset')
throw new Error(`Failed to retrieve pieces for dataset ${storageContext.dataSetId}: ${String(error)}`)
}
// Optionally enrich with metadata
if (includeMetadata && pieces.length > 0) {
await enrichPiecesWithMetadata(synapse, storageContext, pieces, warnings, logger)
}
// Calculate total size from pieces that have sizes
const piecesWithSizes = pieces.filter((p): p is PieceInfo & { size: number } => p.size != null)
const result: DataSetPiecesResult = {
pieces,
dataSetId: storageContext.dataSetId,
warnings,
}
if (piecesWithSizes.length > 0) {
result.totalSizeBytes = piecesWithSizes.reduce((sum, piece) => sum + BigInt(piece.size), 0n)
}
return result
}
/**
* Internal helper: Enrich pieces with metadata from WarmStorage
*
* This function fetches metadata for each piece and extracts:
* - rootIpfsCid (from METADATA_KEYS.IPFS_ROOT_CID)
* - Full metadata object
*
* Non-fatal errors are added to the warnings array.
*/
async function enrichPiecesWithMetadata(
synapse: Synapse,
storageContext: StorageContextWithDataSetId,
pieces: PieceInfo[],
warnings: Warning[],
logger?: GetDataSetPiecesOptions['logger']
): Promise<void> {
const dataSetId = storageContext.dataSetId
// Create WarmStorage service instance
let warmStorage: WarmStorageService
try {
warmStorage = await WarmStorageService.create(synapse.getProvider(), synapse.getWarmStorageAddress())
} catch (error) {
// If we can't create the service, warn and return
logger?.warn({ error }, 'Failed to create WarmStorageService for metadata enrichment')
warnings.push({
code: 'WARM_STORAGE_INIT_FAILED',
message: 'Failed to initialize WarmStorageService for metadata enrichment',
context: { error: String(error) },
})
return
}
// Fetch metadata for each piece
for (const piece of pieces) {
try {
const metadata = await warmStorage.getPieceMetadata(dataSetId, piece.pieceId)
// Extract root IPFS CID if available
const rootIpfsCid = metadata[METADATA_KEYS.IPFS_ROOT_CID]
if (rootIpfsCid) {
piece.rootIpfsCid = rootIpfsCid
}
// Store full metadata
piece.metadata = metadata
} catch (error) {
// Non-fatal: piece exists but metadata fetch failed
logger?.warn(
{
dataSetId,
pieceId: piece.pieceId,
error,
},
'Failed to fetch metadata for piece'
)
warnings.push({
code: 'METADATA_FETCH_FAILED',
message: `Failed to fetch metadata for piece ${piece.pieceId}`,
context: {
pieceId: piece.pieceId,
dataSetId,
error: String(error),
},
})
}
}
}