ipfs-repo
Version:
IPFS Repo implementation
324 lines (273 loc) • 7.91 kB
JavaScript
/* eslint max-nested-callbacks: ["error", 8] */
import { CID } from 'multiformats/cid'
import errCode from 'err-code'
import debug from 'debug'
import first from 'it-first'
import { createUnsafe } from 'multiformats/block'
import * as cborg from 'cborg'
import * as dagPb from '@ipld/dag-pb'
import {
cidToKey,
keyToMultihash
} from './utils/blockstore.js'
import { walkDag } from './utils/walk-dag.js'
import { PinTypes } from './pin-types.js'
import QuickLRU from 'quick-lru'
/**
* @typedef {import('./types').PinType} PinType
* @typedef {import('./types').PinQueryType} PinQueryType
* @typedef {import('multiformats/codecs/interface').BlockCodec<any, any>} BlockCodec
* @typedef {import('./types').PinOptions} PinOptions
* @typedef {import('./types').AbortOptions} AbortOptions
* @typedef {import('./types').Pins} Pins
*/
/**
* @typedef {object} PinInternal
* @property {number} depth
* @property {import('multiformats/cid').Version} [version]
* @property {number} [codec]
* @property {Record<string, any>} [metadata]
*/
/**
* @typedef {object} FetchCompleteDagOptions
* @property {AbortSignal} [signal]
* @property {number} [cidCacheMaxSize]
*/
const CID_CACHE_MAX_SIZE = 2048
/**
* @param {string} type
*/
function invalidPinTypeErr (type) {
const errMsg = `Invalid type '${type}', must be one of {direct, indirect, recursive, all}`
return errCode(new Error(errMsg), 'ERR_INVALID_PIN_TYPE')
}
/**
* @implements {Pins}
*/
export class PinManager {
/**
* @param {object} config
* @param {import('interface-datastore').Datastore} config.pinstore
* @param {import('interface-blockstore').Blockstore} config.blockstore
* @param {import('./types').loadCodec} config.loadCodec
*/
constructor ({ pinstore, blockstore, loadCodec }) {
this.pinstore = pinstore
this.blockstore = blockstore
this.loadCodec = loadCodec
this.log = debug('ipfs:repo:pin')
this.directPins = new Set()
this.recursivePins = new Set()
}
/**
* @param {CID} cid
* @param {PinOptions & AbortOptions} [options]
*/
async pinDirectly (cid, options = {}) {
await this.blockstore.get(cid, options)
/** @type {PinInternal} */
const pin = {
depth: 0
}
if (cid.version !== 0) {
pin.version = cid.version
}
if (cid.code !== dagPb.code) {
pin.codec = cid.code
}
if (options.metadata) {
pin.metadata = options.metadata
}
return this.pinstore.put(cidToKey(cid), cborg.encode(pin))
}
/**
* @param {CID} cid
* @param {AbortOptions} [options]
*/
unpin (cid, options) {
return this.pinstore.delete(cidToKey(cid), options)
}
/**
* @param {CID} cid
* @param {PinOptions & FetchCompleteDagOptions & AbortOptions} [options]
*/
async pinRecursively (cid, options = {}) {
await this.fetchCompleteDag(cid, options)
/** @type {PinInternal} */
const pin = {
depth: Infinity
}
if (cid.version !== 0) {
pin.version = cid.version
}
if (cid.code !== dagPb.code) {
pin.codec = cid.code
}
if (options.metadata) {
pin.metadata = options.metadata
}
await this.pinstore.put(cidToKey(cid), cborg.encode(pin))
}
/**
* @param {AbortOptions} [options]
*/
async * directKeys (options) {
for await (const entry of this.pinstore.query({
filters: [(entry) => {
const pin = cborg.decode(entry.value)
return pin.depth === 0
}]
})) {
const pin = cborg.decode(entry.value)
const version = pin.version || 0
const codec = pin.codec != null ? pin.codec : dagPb.code
const multihash = keyToMultihash(entry.key)
yield {
cid: CID.create(version, codec, multihash),
metadata: pin.metadata
}
}
}
/**
* @param {AbortOptions} [options]
*/
async * recursiveKeys (options) {
for await (const entry of this.pinstore.query({
filters: [(entry) => {
const pin = cborg.decode(entry.value)
return pin.depth === Infinity
}]
})) {
const pin = cborg.decode(entry.value)
const version = pin.version || 0
const codec = pin.codec != null ? pin.codec : dagPb.code
const multihash = keyToMultihash(entry.key)
yield {
cid: CID.create(version, codec, multihash),
metadata: pin.metadata
}
}
}
/**
* @param {AbortOptions} [options]
*/
async * indirectKeys (options) {
for await (const { cid } of this.recursiveKeys()) {
for await (const childCid of walkDag(cid, this.blockstore, this.loadCodec, options)) {
// recursive pins override indirect pins
const types = [
PinTypes.recursive
]
const result = await this.isPinnedWithType(childCid, types)
if (result.pinned) {
continue
}
yield childCid
}
}
}
/**
* @param {CID} cid
* @param {PinQueryType|PinQueryType[]} types
* @param {AbortOptions} [options]
*/
async isPinnedWithType (cid, types, options) {
if (!Array.isArray(types)) {
types = [types]
}
const all = types.includes(PinTypes.all)
const direct = types.includes(PinTypes.direct)
const recursive = types.includes(PinTypes.recursive)
const indirect = types.includes(PinTypes.indirect)
if (recursive || direct || all) {
const result = await first(this.pinstore.query({
prefix: cidToKey(cid).toString(),
filters: [entry => {
if (all) {
return true
}
const pin = cborg.decode(entry.value)
return types.includes(pin.depth === 0 ? PinTypes.direct : PinTypes.recursive)
}],
limit: 1
}))
if (result) {
const pin = cborg.decode(result.value)
return {
cid,
pinned: true,
reason: pin.depth === 0 ? PinTypes.direct : PinTypes.recursive,
metadata: pin.metadata
}
}
}
const self = this
/**
* @param {CID} key
* @param {AsyncIterable<{ cid: CID, metadata: any }>} source
*/
async function * findChild (key, source) {
for await (const { cid: parentCid } of source) {
for await (const childCid of walkDag(parentCid, self.blockstore, self.loadCodec)) {
if (childCid.equals(key)) {
yield parentCid
return
}
}
}
}
if (all || indirect) {
// indirect (default)
// check each recursive key to see if multihash is under it
const parentCid = await first(findChild(cid, this.recursiveKeys()))
if (parentCid) {
return {
cid,
pinned: true,
reason: PinTypes.indirect,
parent: parentCid
}
}
}
return {
cid,
pinned: false
}
}
/**
* @param {CID} cid
* @param {FetchCompleteDagOptions} [options]
*/
async fetchCompleteDag (cid, options = {}) {
const seen = new QuickLRU({ maxSize: options.cidCacheMaxSize ?? CID_CACHE_MAX_SIZE })
/**
* @param {CID} cid
* @param {AbortOptions} options
*/
const walkDag = async (cid, options) => {
if (seen.has(cid.toString())) {
return
}
seen.set(cid.toString(), true)
const bytes = await this.blockstore.get(cid, options)
const codec = await this.loadCodec(cid.code)
const block = createUnsafe({ bytes, cid, codec })
await Promise.all(
[...block.links()].map(([, childCid]) => walkDag(childCid, options))
)
}
await walkDag(cid, options)
}
/**
* Throws an error if the pin type is invalid
*
* @param {any} type
* @returns {type is PinType}
*/
static checkPinType (type) {
if (typeof type !== 'string' || !Object.keys(PinTypes).includes(type)) {
throw invalidPinTypeErr(type)
}
return true
}
}