Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/pin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createLs } from './ls.js'
import { createRemote, type PinRemoteAPI } from './remote/index.js'
import { createRmAll } from './rm-all.js'
import { createRm } from './rm.js'
import { createUpdate } from './update.js'
import type { AwaitIterable, HTTPRPCOptions } from '../index.js'
import type { HTTPRPCClient } from '../lib/core.js'
import type { CID } from 'multiformats/cid'
Expand Down Expand Up @@ -93,6 +94,13 @@ export interface PinRmAllInput {
recursive?: boolean
}

export interface PinUpdateOptions extends HTTPRPCOptions {
/**
* Remove the old pin. Default: true
*/
unpin?: boolean
}

export interface PinAPI {
/**
* Adds an IPFS block to the pinset and also stores it to the IPFS
Expand Down Expand Up @@ -183,6 +191,20 @@ export interface PinAPI {
*/
rmAll(source: AwaitIterable<PinRmAllInput>, options?: HTTPRPCOptions): AsyncIterable<CID>

/**
* Update a recursive pin
*
* @example
* ```js
* const oldCid = CID.parse('bafyreigdnpedjym3lvesqlllbry7zxjcp6fdvsusrh2mesqsdhd4idmzoq')
* const newCid = CID.parse('bafyreibt45jsjrdasabryzkzn7muhvigwyn2mmuw4hk26zr23fzyitmmy4')
* const result = await ipfs.pin.update(oldCid, newCid)
* console.log(result)
* // [CID('bafyreigdnpedjym3lvesqlllbry7zxjcp6fdvsusrh2mesqsdhd4idmzoq'), CID('bafyreibt45jsjrdasabryzkzn7muhvigwyn2mmuw4hk26zr23fzyitmmy4')]
* ```
*/
update(from: string | CID, to: string | CID, options?: PinUpdateOptions): Promise<CID[]>

remote: PinRemoteAPI
}

Expand All @@ -193,6 +215,7 @@ export function createPin (client: HTTPRPCClient): PinAPI {
ls: createLs(client),
rmAll: createRmAll(client),
rm: createRm(client),
update: createUpdate(client),
remote: createRemote(client)
}
}
20 changes: 20 additions & 0 deletions src/pin/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CID } from 'multiformats/cid'
import { toUrlSearchParams } from '../lib/to-url-search-params.js'
import type { PinAPI } from './index.js'
import type { HTTPRPCClient } from '../lib/core.js'

export function createUpdate (client: HTTPRPCClient): PinAPI['update'] {
return async function update (from, to, options = {}) {
const res = await client.post('pin/update', {
signal: options.signal,
searchParams: toUrlSearchParams({
...options,
arg: [from.toString(), to.toString()]
}),
headers: options.headers
})

const { Pins } = await res.json()
return Pins.map((cid: string) => CID.parse(cid))
}
}
4 changes: 3 additions & 1 deletion test/interface-tests/src/pin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { testLs } from './ls.js'
import testRemote from './remote/index.js'
import { testRmAll } from './rm-all.js'
import { testRm } from './rm.js'
import { testUpdate } from './update.js'

const tests = {
add: testAdd,
addAll: testAddAll,
ls: testLs,
rm: testRm,
rmAll: testRmAll,
remote: testRemote
remote: testRemote,
update: testUpdate
}

export default createSuite(tests)
197 changes: 197 additions & 0 deletions test/interface-tests/src/pin/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/* eslint-env mocha */

import { expect } from 'aegir/chai'
import all from 'it-all'
import drain from 'it-drain'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { getDescribe, getIt, type MochaConfig } from '../utils/mocha.js'
import { fixtures, clearPins, expectPinned, expectNotPinned, pinTypes } from './utils.js'
import type { KuboRPCClient } from '../../../../src/index.js'
import type { Factory, KuboNode } from 'ipfsd-ctl'

export function testUpdate (factory: Factory<KuboNode>, options: MochaConfig): void {
const describe = getDescribe(options)
const it = getIt(options)

describe('.pin.update', function () {
this.timeout(50 * 1000)

let ipfs: KuboRPCClient

before(async function () {
ipfs = (await factory.spawn()).api

await drain(
ipfs.addAll(
fixtures.files.map(file => ({ content: file.data })), {
pin: false
}
)
)

await drain(
ipfs.addAll(fixtures.directory.files.map(
file => ({
path: file.path,
content: file.data
})
), {
pin: false
})
)
})

after(async function () {
await factory.clean()
})

beforeEach(async function () {
return clearPins(ipfs)
})

it('should update a recursive pin', async () => {
// First pin the old object
await ipfs.pin.add(fixtures.directory.cid)

// Create a new object to update to that links to one of the existing files
const newObject = await ipfs.dag.put({
Data: uint8ArrayFromString('new object'),
Links: [{
Name: 'ipfs-add.js',
Hash: fixtures.directory.files[0].cid,
Tsize: 0
}]
}, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256'
})

// Update the pin
const result = await ipfs.pin.update(fixtures.directory.cid, newObject)

// Check the result
expect(result).to.deep.include(newObject)

// Verify old pin is removed
await expectNotPinned(ipfs, fixtures.directory.cid)

// Verify new pin is added
await expectPinned(ipfs, newObject, pinTypes.recursive)

// Verify the linked file is still pinned indirectly
await expectPinned(ipfs, fixtures.directory.files[0].cid, pinTypes.indirect)

// Verify the old recursive object is not pinned
await expectNotPinned(ipfs, fixtures.directory.files[1].cid)
})

it('should update a recursive pin without removing the old pin', async () => {
// First pin the old object
await ipfs.pin.add(fixtures.directory.cid)

// Create a new object to update to that links to one of the existing files
const newObject = await ipfs.dag.put({
Data: uint8ArrayFromString('new object'),
Links: [{
Name: 'ipfs-add.js',
Hash: fixtures.directory.files[0].cid,
Tsize: 0
}]
}, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256'
})

// Update the pin without removing the old one
const result = await ipfs.pin.update(fixtures.directory.cid, newObject, {
unpin: false
})

// Check the result
expect(result).to.deep.include(newObject)

// Verify old pin is still there
await expectPinned(ipfs, fixtures.directory.cid, pinTypes.recursive)

// Verify new pin is added
await expectPinned(ipfs, newObject, pinTypes.recursive)

// Verify the linked file is pinned indirectly through both objects
await expectPinned(ipfs, fixtures.directory.files[0].cid, pinTypes.indirect)

// Verify the unlinked file is still pinned indirectly through the old object
await expectPinned(ipfs, fixtures.directory.files[1].cid, pinTypes.indirect)
})

it('should update a recursive pin with a name label', async () => {
// First pin the old object with a name
const pinName = 'my-label'
await ipfs.pin.add(fixtures.directory.cid, { name: pinName })

// Create a new object to update to that links to one of the existing files
const newObject = await ipfs.dag.put({
Data: uint8ArrayFromString('new object'),
Links: [{
Name: 'ipfs-add.js',
Hash: fixtures.directory.files[0].cid,
Tsize: 0
}]
}, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256'
})

// Update the pin
const result = await ipfs.pin.update(fixtures.directory.cid, newObject)

// Check the result
expect(result).to.deep.include(newObject)

// Verify old pin is removed
await expectNotPinned(ipfs, fixtures.directory.cid)
// Verify the old recursive object is not pinned
await expectNotPinned(ipfs, fixtures.directory.files[1].cid)

// Verify new pin is added and has the name label
const pinset = await all(ipfs.pin.ls({ name: pinName }))
expect(pinset).to.have.lengthOf(1)
expect(pinset[0].cid.toString()).to.equal(newObject.toString())
expect(pinset[0].name).to.equal(pinName)
})

it('should fail to update a non-recursive pin', async () => {
// First pin the old object directly
await ipfs.pin.add(fixtures.directory.cid, {
recursive: false
})

// Create a new object to update to
const newObject = await ipfs.dag.put({
Data: uint8ArrayFromString('new object'),
Links: []
}, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256'
})

// Try to update the pin
await expect(ipfs.pin.update(fixtures.directory.cid, newObject))
.to.eventually.be.rejectedWith(/not recursively pinned/)
})

it('should fail to update a non-existent pin', async () => {
// Create a new object to update to
const newObject = await ipfs.dag.put({
Data: uint8ArrayFromString('new object'),
Links: []
}, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256'
})

// Try to update a non-existent pin
await expect(ipfs.pin.update(fixtures.directory.cid, newObject))
.to.eventually.be.rejectedWith(/not recursively pinned/)
})
})
}
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{
"extends": "aegir/src/config/tsconfig.aegir.json",
"compilerOptions": {
"outDir": "dist"
"outDir": "dist",
"baseUrl": ".",
"paths": {
"kubo-rpc-client": ["./src/index.ts"]
}
},
"include": [
"src",
Expand Down