Skip to content

Commit c66360a

Browse files
Joel ThorstenssonSgtPooki
andauthored
feat: support pin.update (#337)
* feat: support pin.update * Update src/pin/index.ts Co-authored-by: Russell Dempsey <[email protected]> --------- Co-authored-by: Russell Dempsey <[email protected]>
1 parent 22a1dab commit c66360a

File tree

5 files changed

+250
-2
lines changed

5 files changed

+250
-2
lines changed

src/pin/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLs } from './ls.js'
44
import { createRemote, type PinRemoteAPI } from './remote/index.js'
55
import { createRmAll } from './rm-all.js'
66
import { createRm } from './rm.js'
7+
import { createUpdate } from './update.js'
78
import type { AwaitIterable, HTTPRPCOptions } from '../index.js'
89
import type { HTTPRPCClient } from '../lib/core.js'
910
import type { CID } from 'multiformats/cid'
@@ -93,6 +94,15 @@ export interface PinRmAllInput {
9394
recursive?: boolean
9495
}
9596

97+
export interface PinUpdateOptions extends HTTPRPCOptions {
98+
/**
99+
* Remove the old pin
100+
*
101+
* @default true
102+
*/
103+
unpin?: boolean
104+
}
105+
96106
export interface PinAPI {
97107
/**
98108
* Adds an IPFS block to the pinset and also stores it to the IPFS
@@ -183,6 +193,20 @@ export interface PinAPI {
183193
*/
184194
rmAll(source: AwaitIterable<PinRmAllInput>, options?: HTTPRPCOptions): AsyncIterable<CID>
185195

196+
/**
197+
* Update a recursive pin
198+
*
199+
* @example
200+
* ```js
201+
* const oldCid = CID.parse('bafyreigdnpedjym3lvesqlllbry7zxjcp6fdvsusrh2mesqsdhd4idmzoq')
202+
* const newCid = CID.parse('bafyreibt45jsjrdasabryzkzn7muhvigwyn2mmuw4hk26zr23fzyitmmy4')
203+
* const result = await ipfs.pin.update(oldCid, newCid)
204+
* console.log(result)
205+
* // [CID('bafyreigdnpedjym3lvesqlllbry7zxjcp6fdvsusrh2mesqsdhd4idmzoq'), CID('bafyreibt45jsjrdasabryzkzn7muhvigwyn2mmuw4hk26zr23fzyitmmy4')]
206+
* ```
207+
*/
208+
update(from: string | CID, to: string | CID, options?: PinUpdateOptions): Promise<CID[]>
209+
186210
remote: PinRemoteAPI
187211
}
188212

@@ -193,6 +217,7 @@ export function createPin (client: HTTPRPCClient): PinAPI {
193217
ls: createLs(client),
194218
rmAll: createRmAll(client),
195219
rm: createRm(client),
220+
update: createUpdate(client),
196221
remote: createRemote(client)
197222
}
198223
}

src/pin/update.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { CID } from 'multiformats/cid'
2+
import { toUrlSearchParams } from '../lib/to-url-search-params.js'
3+
import type { PinAPI } from './index.js'
4+
import type { HTTPRPCClient } from '../lib/core.js'
5+
6+
export function createUpdate (client: HTTPRPCClient): PinAPI['update'] {
7+
return async function update (from, to, options = {}) {
8+
const res = await client.post('pin/update', {
9+
signal: options.signal,
10+
searchParams: toUrlSearchParams({
11+
...options,
12+
arg: [from.toString(), to.toString()]
13+
}),
14+
headers: options.headers
15+
})
16+
17+
const { Pins } = await res.json()
18+
return Pins.map((cid: string) => CID.parse(cid))
19+
}
20+
}

test/interface-tests/src/pin/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import { testLs } from './ls.js'
55
import testRemote from './remote/index.js'
66
import { testRmAll } from './rm-all.js'
77
import { testRm } from './rm.js'
8+
import { testUpdate } from './update.js'
89

910
const tests = {
1011
add: testAdd,
1112
addAll: testAddAll,
1213
ls: testLs,
1314
rm: testRm,
1415
rmAll: testRmAll,
15-
remote: testRemote
16+
remote: testRemote,
17+
update: testUpdate
1618
}
1719

1820
export default createSuite(tests)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/* eslint-env mocha */
2+
3+
import { expect } from 'aegir/chai'
4+
import all from 'it-all'
5+
import drain from 'it-drain'
6+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
7+
import { getDescribe, getIt, type MochaConfig } from '../utils/mocha.js'
8+
import { fixtures, clearPins, expectPinned, expectNotPinned, pinTypes } from './utils.js'
9+
import type { KuboRPCClient } from '../../../../src/index.js'
10+
import type { Factory, KuboNode } from 'ipfsd-ctl'
11+
12+
export function testUpdate (factory: Factory<KuboNode>, options: MochaConfig): void {
13+
const describe = getDescribe(options)
14+
const it = getIt(options)
15+
16+
describe('.pin.update', function () {
17+
this.timeout(50 * 1000)
18+
19+
let ipfs: KuboRPCClient
20+
21+
before(async function () {
22+
ipfs = (await factory.spawn()).api
23+
24+
await drain(
25+
ipfs.addAll(
26+
fixtures.files.map(file => ({ content: file.data })), {
27+
pin: false
28+
}
29+
)
30+
)
31+
32+
await drain(
33+
ipfs.addAll(fixtures.directory.files.map(
34+
file => ({
35+
path: file.path,
36+
content: file.data
37+
})
38+
), {
39+
pin: false
40+
})
41+
)
42+
})
43+
44+
after(async function () {
45+
await factory.clean()
46+
})
47+
48+
beforeEach(async function () {
49+
return clearPins(ipfs)
50+
})
51+
52+
it('should update a recursive pin', async () => {
53+
// First pin the old object
54+
await ipfs.pin.add(fixtures.directory.cid)
55+
56+
// Create a new object to update to that links to one of the existing files
57+
const newObject = await ipfs.dag.put({
58+
Data: uint8ArrayFromString('new object'),
59+
Links: [{
60+
Name: 'ipfs-add.js',
61+
Hash: fixtures.directory.files[0].cid,
62+
Tsize: 0
63+
}]
64+
}, {
65+
storeCodec: 'dag-pb',
66+
hashAlg: 'sha2-256'
67+
})
68+
69+
// Update the pin
70+
const result = await ipfs.pin.update(fixtures.directory.cid, newObject)
71+
72+
// Check the result
73+
expect(result).to.deep.include(newObject)
74+
75+
// Verify old pin is removed
76+
await expectNotPinned(ipfs, fixtures.directory.cid)
77+
78+
// Verify new pin is added
79+
await expectPinned(ipfs, newObject, pinTypes.recursive)
80+
81+
// Verify the linked file is still pinned indirectly
82+
await expectPinned(ipfs, fixtures.directory.files[0].cid, pinTypes.indirect)
83+
84+
// Verify the old recursive object is not pinned
85+
await expectNotPinned(ipfs, fixtures.directory.files[1].cid)
86+
})
87+
88+
it('should update a recursive pin without removing the old pin', async () => {
89+
// First pin the old object
90+
await ipfs.pin.add(fixtures.directory.cid)
91+
92+
// Create a new object to update to that links to one of the existing files
93+
const newObject = await ipfs.dag.put({
94+
Data: uint8ArrayFromString('new object'),
95+
Links: [{
96+
Name: 'ipfs-add.js',
97+
Hash: fixtures.directory.files[0].cid,
98+
Tsize: 0
99+
}]
100+
}, {
101+
storeCodec: 'dag-pb',
102+
hashAlg: 'sha2-256'
103+
})
104+
105+
// Update the pin without removing the old one
106+
const result = await ipfs.pin.update(fixtures.directory.cid, newObject, {
107+
unpin: false
108+
})
109+
110+
// Check the result
111+
expect(result).to.deep.include(newObject)
112+
113+
// Verify old pin is still there
114+
await expectPinned(ipfs, fixtures.directory.cid, pinTypes.recursive)
115+
116+
// Verify new pin is added
117+
await expectPinned(ipfs, newObject, pinTypes.recursive)
118+
119+
// Verify the linked file is pinned indirectly through both objects
120+
await expectPinned(ipfs, fixtures.directory.files[0].cid, pinTypes.indirect)
121+
122+
// Verify the unlinked file is still pinned indirectly through the old object
123+
await expectPinned(ipfs, fixtures.directory.files[1].cid, pinTypes.indirect)
124+
})
125+
126+
it('should update a recursive pin with a name label', async () => {
127+
// First pin the old object with a name
128+
const pinName = 'my-label'
129+
await ipfs.pin.add(fixtures.directory.cid, { name: pinName })
130+
131+
// Create a new object to update to that links to one of the existing files
132+
const newObject = await ipfs.dag.put({
133+
Data: uint8ArrayFromString('new object'),
134+
Links: [{
135+
Name: 'ipfs-add.js',
136+
Hash: fixtures.directory.files[0].cid,
137+
Tsize: 0
138+
}]
139+
}, {
140+
storeCodec: 'dag-pb',
141+
hashAlg: 'sha2-256'
142+
})
143+
144+
// Update the pin
145+
const result = await ipfs.pin.update(fixtures.directory.cid, newObject)
146+
147+
// Check the result
148+
expect(result).to.deep.include(newObject)
149+
150+
// Verify old pin is removed
151+
await expectNotPinned(ipfs, fixtures.directory.cid)
152+
// Verify the old recursive object is not pinned
153+
await expectNotPinned(ipfs, fixtures.directory.files[1].cid)
154+
155+
// Verify new pin is added and has the name label
156+
const pinset = await all(ipfs.pin.ls({ name: pinName }))
157+
expect(pinset).to.have.lengthOf(1)
158+
expect(pinset[0].cid.toString()).to.equal(newObject.toString())
159+
expect(pinset[0].name).to.equal(pinName)
160+
})
161+
162+
it('should fail to update a non-recursive pin', async () => {
163+
// First pin the old object directly
164+
await ipfs.pin.add(fixtures.directory.cid, {
165+
recursive: false
166+
})
167+
168+
// Create a new object to update to
169+
const newObject = await ipfs.dag.put({
170+
Data: uint8ArrayFromString('new object'),
171+
Links: []
172+
}, {
173+
storeCodec: 'dag-pb',
174+
hashAlg: 'sha2-256'
175+
})
176+
177+
// Try to update the pin
178+
await expect(ipfs.pin.update(fixtures.directory.cid, newObject))
179+
.to.eventually.be.rejectedWith(/not recursively pinned/)
180+
})
181+
182+
it('should fail to update a non-existent pin', async () => {
183+
// Create a new object to update to
184+
const newObject = await ipfs.dag.put({
185+
Data: uint8ArrayFromString('new object'),
186+
Links: []
187+
}, {
188+
storeCodec: 'dag-pb',
189+
hashAlg: 'sha2-256'
190+
})
191+
192+
// Try to update a non-existent pin
193+
await expect(ipfs.pin.update(fixtures.directory.cid, newObject))
194+
.to.eventually.be.rejectedWith(/not recursively pinned/)
195+
})
196+
})
197+
}

tsconfig.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"extends": "aegir/src/config/tsconfig.aegir.json",
33
"compilerOptions": {
4-
"outDir": "dist"
4+
"outDir": "dist",
5+
"baseUrl": ".",
6+
"paths": {
7+
"kubo-rpc-client": ["./src/index.ts"]
8+
}
59
},
610
"include": [
711
"src",

0 commit comments

Comments
 (0)