filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
311 lines (276 loc) • 8.9 kB
text/typescript
import fastify, { type FastifyInstance, type FastifyRequest } from 'fastify'
import { CID } from 'multiformats/cid'
import type { Logger } from 'pino'
import type { Config } from './config.js'
import { FilecoinPinStore, type PinOptions } from './filecoin-pin-store.js'
import type { ServiceInfo } from './server.js'
import { setupSynapse } from './synapse/service.js'
declare module 'fastify' {
interface FastifyRequest {
user?: {
id: string
name: string
}
}
}
const DEFAULT_USER_INFO = {
id: 'default-user',
name: 'Default User',
}
export async function createFilecoinPinningServer(
config: Config,
logger: Logger,
serviceInfo: ServiceInfo
): Promise<{ server: FastifyInstance; pinStore: FilecoinPinStore }> {
// Set up Synapse service
const synapseService = await setupSynapse(config, logger)
// Create our custom Filecoin pin store with Synapse service
const filecoinPinStore = new FilecoinPinStore({
config,
logger,
synapseService,
})
// Set up event handlers for monitoring
filecoinPinStore.on('pin:block:stored', (data) => {
logger.debug(
{
pinId: data.pinId,
userId: data.userId,
cid: data.cid.toString(),
size: data.size,
},
'Block stored for pin'
)
})
filecoinPinStore.on('pin:block:missing', (data) => {
logger.warn(
{
pinId: data.pinId,
userId: data.userId,
cid: data.cid.toString(),
},
'Block missing for pin'
)
})
filecoinPinStore.on('pin:car:completed', (data) => {
logger.info(
{
pinId: data.pinId,
userId: data.userId,
cid: data.cid.toString(),
blocksWritten: data.stats.blocksWritten,
totalSize: data.stats.totalSize,
missingBlocks: data.stats.missingBlocks.size,
carFilePath: data.carFilePath,
},
'CAR file completed for pin'
)
})
filecoinPinStore.on('pin:failed', (data) => {
logger.error(
{
pinId: data.pinId,
userId: data.userId,
cid: data.cid.toString(),
error: data.error,
},
'Pin operation failed'
)
})
// Create a custom Fastify server
const server = fastify({
logger: false, // We'll use our own logger
})
// Add root route for health check (no auth required)
server.get('/', async (_request, reply) => {
await reply.send({
service: serviceInfo.service,
version: serviceInfo.version,
status: 'ok',
})
})
// Add authentication hook
server.addHook('preHandler', async (request, reply) => {
// Skip auth for root health check
if (request.url === '/') {
return
}
const authHeader = request.headers.authorization
if (authHeader?.startsWith('Bearer ') !== true) {
await reply.code(401).send({ error: 'Missing or invalid authorization header' })
return
}
const token = authHeader.slice(7) // Remove 'Bearer ' prefix
if (token.trim().length === 0) {
await reply.code(401).send({ error: 'Invalid access token' })
return
}
// Add user to request context
request.user = DEFAULT_USER_INFO
})
// Add our custom pin store to the Fastify context
server.decorate('pinStore', filecoinPinStore)
// Register custom routes that use our pin store
await server.register(async (fastify) => {
// Override the default routes with our custom implementations
await registerCustomPinRoutes(fastify, filecoinPinStore, logger)
})
await filecoinPinStore.start()
// Start listening
await server.listen({
port: config.port ?? 0, // Use random port for testing
host: config.host,
})
logger.info('Filecoin pinning service API server started')
return {
server,
pinStore: filecoinPinStore,
}
}
async function registerCustomPinRoutes(
fastify: FastifyInstance,
pinStore: FilecoinPinStore,
logger: Logger
): Promise<void> {
// POST /pins - Create a new pin
fastify.post(
'/pins',
async (
request: FastifyRequest<{
Body: { cid?: string; name?: string; origins?: string[]; meta?: Record<string, string> }
}>,
reply
) => {
try {
const { cid, name, origins, meta } = request.body
if (cid == null) {
await reply.code(400).send({ error: 'Missing required field: cid' })
return
}
// Parse the CID string to CID object
let cidObject: CID
try {
cidObject = CID.parse(cid)
} catch (_error) {
await reply.code(400).send({ error: `Invalid CID format: ${cid}` })
return
}
const pinOptions: PinOptions = {}
if (name != null) pinOptions.name = name
if (origins != null) pinOptions.origins = origins
if (meta != null) pinOptions.meta = meta
if (request.user == null) {
await reply.code(401).send({ error: 'Unauthorized' })
return
}
const result = await pinStore.pin(request.user, cidObject, pinOptions)
await reply.code(202).send({
requestid: result.id,
status: result.status,
created: new Date(result.created).toISOString(),
pin: result.pin,
delegates: [],
info: result.info,
})
} catch (error) {
logger.error({ error }, 'Failed to create pin')
await reply.code(500).send({ error: 'Internal server error' })
}
}
)
// GET /pins/:requestId - Get pin status
fastify.get('/pins/:requestId', async (request: FastifyRequest<{ Params: { requestId: string } }>, reply) => {
try {
if (request.user == null) {
await reply.code(401).send({ error: 'Unauthorized' })
return
}
const result = await pinStore.get(request.user, request.params.requestId)
if (result == null) {
await reply.code(404).send({ error: 'Pin not found' })
return
}
await reply.send({
requestid: result.id,
status: result.status,
created: new Date(result.created).toISOString(),
pin: result.pin,
delegates: [],
info: result.info,
})
} catch (error) {
logger.error({ error }, 'Failed to get pin status')
await reply.code(500).send({ error: 'Internal server error' })
}
})
// GET /pins - List pins
fastify.get(
'/pins',
async (
request: FastifyRequest<{ Querystring: { cid?: string; name?: string; status?: string; limit?: string } }>,
reply
) => {
try {
const { cid, name, status, limit } = request.query
const limitNum = limit != null ? parseInt(limit, 10) : undefined
const listQuery: Parameters<typeof pinStore.list>[1] = {}
if (cid != null) listQuery.cid = cid
if (name != null) listQuery.name = name
if (status != null) listQuery.status = status
if (limitNum != null && !Number.isNaN(limitNum)) listQuery.limit = limitNum
if (request.user == null) {
await reply.code(401).send({ error: 'Unauthorized' })
return
}
const result = await pinStore.list(request.user, listQuery)
const results = result.results.map((pin) => ({
requestid: pin.id,
status: pin.status,
created: new Date(pin.created).toISOString(),
pin: pin.pin,
delegates: [],
info: pin.info,
}))
await reply.send({
count: result.count,
results,
})
} catch (error) {
logger.error({ error }, 'Failed to list pins')
await reply.code(500).send({ error: 'Internal server error' })
}
}
)
// POST /pins/:requestId - Update pin (not commonly used)
fastify.post('/pins/:requestId', async (request: any, reply: any) => {
try {
const { name, origins, meta } = request.body
const result = await pinStore.update(request.user, request.params.requestId, { name, origins, meta })
if (result == null) {
await reply.code(404).send({ error: 'Pin not found' })
return
}
await reply.send({
requestid: result.id,
status: result.status,
created: new Date(result.created).toISOString(),
pin: result.pin,
delegates: [],
info: result.info,
})
} catch (error) {
logger.error({ error }, 'Failed to update pin')
await reply.code(500).send({ error: 'Internal server error' })
}
})
// DELETE /pins/:requestId - Cancel/delete pin and clean up CAR file
fastify.delete('/pins/:requestId', async (request: any, reply: any) => {
try {
await pinStore.cancel(request.user, request.params.requestId)
await reply.code(202).send()
} catch (error) {
logger.error({ error }, 'Failed to cancel pin')
await reply.code(500).send({ error: 'Internal server error' })
}
})
}