Skip to content

Commit 32efafb

Browse files
authored
feat: kubo 0.38 interop and pin name support (#343)
* feat: add pin name support - add name parameter to pin.add and pin.addAll - add names flag to pin.ls (auto-enabled with name filter) - filter undefined/null values in URL parameters - handle mixed arrays in normalise-input - add validation for conflicting options - add comprehensive tests * test: verify metadata is stored in UnixFS update test to check metadata (mode/mtime) is properly sent to API and stored in UnixFS structure, retrievable via DAG API * fix: resolve kubo import warnings - change kubo imports from named to default imports (CommonJS module) - fix import order for ipfs-unixfs in test file * fix: handle missing ProvideBufLen in bitswap stat response kubo API doesn't return ProvideBufLen field in JSON response, default to 0 when missing to fix test failure * fix: update config.set tests to use valid config keys - replace arbitrary 'Fruit' key with 'Gateway.RootRedirect' - replace removed 'Discovery.MDNS.Interval' with 'Gateway.MaxConcurrentRequests' - add test for unknown config key rejection - kubo no longer allows arbitrary config keys * test: use IPFS_GO_EXEC for custom kubo binary - respect IPFS_GO_EXEC env var in interface tests - document IPFS_GO_EXEC usage in README - add version logging to track which binary is used allows testing with development versions of kubo * chore: update kubo to 0.38.0-rc1 see https://github.com/ipfs/kubo/releases/tag/v0.38.0-rc1 * fix: remove console.log from test to fix lint error removed debug console.log statement that was causing eslint no-console rule violation * chore: update kubo to 0.38.0 * refactor: simplify pin input normalization with toPinnable helper
1 parent d20a7ff commit 32efafb

File tree

17 files changed

+1839
-509
lines changed

17 files changed

+1839
-509
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,20 @@ const ipfs = create({ timeout: '2m' })
389389

390390
We run tests by executing `npm test` in a terminal window. This will run both Node.js and Browser tests, both in Chrome and PhantomJS. To ensure that the module conforms with the [`interface-ipfs-core`](https://github.com/ipfs/js-ipfs/tree/master/packages/interface-ipfs-core) spec, we run the batch of tests provided by the interface module, which can be found [here](https://github.com/ipfs/js-ipfs/tree/master/packages/interface-ipfs-core/src).
391391

392+
#### Testing with Custom Kubo Binary
393+
394+
By default, tests use the kubo binary from `node_modules`. To test with a custom kubo binary (e.g., a development version), use the `IPFS_GO_EXEC` environment variable:
395+
396+
```bash
397+
# Test with a custom kubo binary
398+
IPFS_GO_EXEC=/path/to/custom/kubo npm test
399+
400+
# Example: testing with locally built kubo
401+
IPFS_GO_EXEC=/home/user/kubo/cmd/ipfs/ipfs npm test
402+
```
403+
404+
This is particularly useful when developing features that require changes to both kubo and this client.
405+
392406
## Historical context
393407

394408
This module started as a direct mapping from the go-ipfs cli to a JavaScript implementation, although this was useful and familiar to a lot of developers that were coming to IPFS for the first time, it also created some confusion on how to operate the core of IPFS and have access to the full capacity of the protocol. After much consideration, we decided to create `interface-ipfs-core` with the goal of standardizing the interface of a core implementation of IPFS, and keep the utility functions the IPFS community learned to use and love, such as reading files from disk and storing them directly to IPFS.

package-lock.json

Lines changed: 1483 additions & 433 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@
205205
"it-pipe": "^3.0.1",
206206
"it-tar": "^6.0.0",
207207
"it-to-buffer": "^4.0.5",
208-
"kubo": "^0.29.0",
208+
"kubo": "^0.38.0",
209209
"mock-ipfs-pinning-service": "^0.4.2",
210210
"nock": "^14.0.1",
211211
"p-defer": "^4.0.0",

src/bitswap/stat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function createStat (client: HTTPRPCClient): BitswapAPI['stat'] {
1818

1919
function toCoreInterface (res: any): BitswapStats {
2020
return {
21-
provideBufLen: res.ProvideBufLen,
21+
provideBufLen: res.ProvideBufLen ?? 0,
2222
wantlist: (res.Wantlist ?? []).map((k: any) => CID.parse(k['/'])),
2323
peers: (res.Peers ?? []).map((str: any) => peerIdFromString(str)),
2424
blocksReceived: BigInt(res.BlocksReceived),

src/lib/pins/normalise-input.ts

Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Pinnable {
66
cid?: CID
77
recursive?: boolean
88
metadata?: any
9+
name?: string
910
}
1011

1112
export type ToPin = CID | string | Pinnable
@@ -15,6 +16,7 @@ export interface Pin {
1516
path: string | CID
1617
recursive: boolean
1718
metadata?: any
19+
name?: string
1820
}
1921

2022
function isIterable (thing: any): thing is IterableIterator<any> & Iterator<any> {
@@ -88,34 +90,12 @@ export async function * normaliseInput (input: Source): AsyncGenerator<Pin> {
8890
return iterator
8991
}
9092

91-
// Iterable<CID>
92-
if (isCID(first.value)) {
93-
yield toPin({ cid: first.value })
94-
for (const cid of iterator) {
95-
yield toPin({ cid })
96-
}
97-
return
93+
// Iterable<ToPin>
94+
yield toPin(toPinnable(first.value))
95+
for (const obj of iterator) {
96+
yield toPin(toPinnable(obj))
9897
}
99-
100-
// Iterable<String>
101-
if (typeof first.value === 'string') {
102-
yield toPin({ path: first.value })
103-
for (const path of iterator) {
104-
yield toPin({ path })
105-
}
106-
return
107-
}
108-
109-
// Iterable<Pinnable>
110-
if (first.value.cid != null || first.value.path != null) {
111-
yield toPin(first.value)
112-
for (const obj of iterator) {
113-
yield toPin(obj)
114-
}
115-
return
116-
}
117-
118-
throw new InvalidParametersError(`Unexpected input: ${typeof input}`)
98+
return
11999
}
120100

121101
// AsyncIterable<?>
@@ -124,44 +104,35 @@ export async function * normaliseInput (input: Source): AsyncGenerator<Pin> {
124104
const first = await iterator.next()
125105
if (first.done === true) return iterator
126106

127-
// AsyncIterable<CID>
128-
if (isCID(first.value)) {
129-
yield toPin({ cid: first.value })
130-
for await (const cid of iterator) {
131-
yield toPin({ cid })
132-
}
133-
return
134-
}
135-
136-
// AsyncIterable<String>
137-
if (typeof first.value === 'string') {
138-
yield toPin({ path: first.value })
139-
for await (const path of iterator) {
140-
yield toPin({ path })
141-
}
142-
return
107+
// AsyncIterable<ToPin>
108+
yield toPin(toPinnable(first.value))
109+
for await (const obj of iterator) {
110+
yield toPin(toPinnable(obj))
143111
}
112+
return
113+
}
144114

145-
// AsyncIterable<{ cid: CID|String recursive, metadata }>
146-
if (first.value.cid != null || first.value.path != null) {
147-
yield toPin(first.value)
148-
for await (const obj of iterator) {
149-
yield toPin(obj)
150-
}
151-
return
152-
}
115+
throw new InvalidParametersError(`Unexpected input: ${typeof input}`)
116+
}
153117

154-
throw new InvalidParametersError(`Unexpected input: ${typeof input}`)
118+
function toPinnable (input: ToPin): Pinnable {
119+
if (isCID(input)) {
120+
return { cid: input }
121+
}
122+
if (typeof input === 'string') {
123+
return { path: input }
124+
}
125+
if (typeof input === 'object' && (input.cid != null || input.path != null)) {
126+
return input
155127
}
156-
157128
throw new InvalidParametersError(`Unexpected input: ${typeof input}`)
158129
}
159130

160131
function toPin (input: Pinnable): Pin {
161-
const path = input.cid ?? `${input.path}`
132+
const path = input.cid ?? (input.path != null ? `${input.path}` : undefined)
162133

163134
if (path == null) {
164-
throw new InvalidParametersError('Unexpected input: Please path either a CID or an IPFS path')
135+
throw new InvalidParametersError('Unexpected input: Please pass either a CID or an IPFS path')
165136
}
166137

167138
const pin: Pin = {
@@ -173,5 +144,9 @@ function toPin (input: Pinnable): Pin {
173144
pin.metadata = input.metadata
174145
}
175146

147+
if (input.name != null) {
148+
pin.name = input.name
149+
}
150+
176151
return pin
177152
}

src/lib/to-url-search-params.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,15 @@ export function toUrlSearchParams ({ arg, searchParams, hashAlg, mtime, mode, ..
3535
arg = [arg]
3636
}
3737

38-
const urlSearchParams = new URLSearchParams(options)
38+
// Filter out undefined and null values to avoid sending "undefined" or "null" as strings
39+
const filteredOptions: Record<string, any> = {}
40+
for (const [key, value] of Object.entries(options)) {
41+
if (value !== undefined && value !== null) {
42+
filteredOptions[key] = value
43+
}
44+
}
45+
46+
const urlSearchParams = new URLSearchParams(filteredOptions)
3947

4048
arg.forEach((arg: any) => {
4149
urlSearchParams.append('arg', arg)

src/pin/add-all.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import type { HTTPRPCClient } from '../lib/core.js'
66

77
export function createAddAll (client: HTTPRPCClient): PinAPI['addAll'] {
88
return async function * addAll (source, options = {}) {
9-
for await (const { path, recursive, metadata } of normaliseInput(source)) {
9+
for await (const { path, recursive, metadata, name } of normaliseInput(source)) {
1010
const res = await client.post('pin/add', {
1111
signal: options.signal,
1212
searchParams: toUrlSearchParams({
1313
...options,
14-
arg: path,
14+
arg: path.toString(),
1515
recursive,
1616
metadata: metadata != null ? JSON.stringify(metadata) : undefined,
17+
name: name ?? options.name,
1718
stream: true
1819
}),
1920
headers: options.headers

src/pin/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ export interface PinAddAllOptions extends HTTPRPCOptions {
4141
* Internal option used to control whether to create a repo write lock during a pinning operation
4242
*/
4343
lock?: boolean
44+
45+
/**
46+
* An optional name for created pin(s)
47+
*/
48+
name?: string
4449
}
4550

4651
export type PinAddInput = CID | PinAddInputWithOptions
@@ -65,16 +70,42 @@ export interface PinAddInputWithOptions {
6570
* A human readable string to store with this pin
6671
*/
6772
comments?: string
73+
74+
/**
75+
* An optional name for the created pin
76+
*/
77+
name?: string
6878
}
6979

7080
export type PinType = 'recursive' | 'direct' | 'indirect' | 'all'
7181

7282
export type PinQueryType = 'recursive' | 'direct' | 'indirect' | 'all'
7383

7484
export interface PinLsOptions extends HTTPRPCOptions {
85+
/**
86+
* Path(s) to specific object(s) to be listed
87+
*/
7588
paths?: CID | CID[] | string | string[]
89+
90+
/**
91+
* The type of pinned keys to list. Can be "direct", "indirect", "recursive", or "all".
92+
*
93+
* @default "all"
94+
*/
7695
type?: PinQueryType
96+
97+
/**
98+
* Limit returned pins to ones with names that contain the value provided (case-sensitive, partial match).
99+
* Implies names=true.
100+
*/
77101
name?: string
102+
103+
/**
104+
* Include pin names in the output (slower, disabled by default).
105+
*
106+
* @default false
107+
*/
108+
names?: boolean
78109
}
79110

80111
export interface PinLsResult {

src/pin/ls.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,25 @@ export function createLs (client: HTTPRPCClient): PinAPI['ls'] {
2727
paths = Array.isArray(options.paths) ? options.paths : [options.paths]
2828
}
2929

30+
// Check for conflicting options
31+
if (options.name != null && options.names === false) {
32+
throw new Error('Cannot use name filter when names is explicitly set to false')
33+
}
34+
35+
// Check for empty name filter
36+
if (options.name === '') {
37+
throw new Error('Name filter cannot be empty string')
38+
}
39+
40+
// If name filter is provided, automatically enable names flag
41+
const names = options.names ?? (options.name != null)
42+
3043
const res = await client.post('pin/ls', {
3144
signal: options.signal,
3245
searchParams: toUrlSearchParams({
3346
...options,
3447
arg: paths.map(path => `${path}`),
48+
names,
3549
stream: true
3650
}),
3751
headers: options.headers

test/files.spec.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import * as dagPB from '@ipld/dag-pb'
44
import { expect } from 'aegir/chai'
5+
import { UnixFS } from 'ipfs-unixfs'
56
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
67
import { factory } from './utils/factory.js'
78
import type { KuboRPCClient } from '../src/index.js'
@@ -19,23 +20,35 @@ describe('.add', function () {
1920

2021
after(async function () { return f.clean() })
2122

22-
it('should ignore metadata until https://github.com/ipfs/go-ipfs/issues/6920 is implemented', async function () {
23+
it('should send metadata when provided (affects CID) but not return it until issue #6920 is resolved', async function () {
2324
const data = uint8ArrayFromString('some data')
2425
const result = await ipfs.add(data, {
2526
// @ts-expect-error missing field
2627
mode: 0o600,
2728
mtime: {
2829
secs: 1000,
29-
nsecs: 0
30+
nsecs: 500
3031
}
3132
})
3233

34+
// Metadata is not returned in response yet (issue #6920)
3335
expect(result).to.not.have.property('mode')
3436
expect(result).to.not.have.property('mtime')
3537
expect(result).to.have.property('cid')
3638

3739
const { cid } = result
3840
expect(cid).to.have.property('code', dagPB.code)
39-
expect(cid.toString()).to.equal('QmVv4Wz46JaZJeH5PMV4LGbRiiMKEmszPYY3g6fjGnVXBS')
41+
// CID is different because metadata is now properly sent to the API
42+
expect(cid.toString()).to.equal('QmPDB1sHH2FNqwJm2A6747uf6JUZyEB2cnNFRPz2uzjoDZ')
43+
44+
// Verify metadata is stored in UnixFS structure via DAG API
45+
const dagNode = await ipfs.dag.get(cid)
46+
const pbData = dagNode.value.Data
47+
const unixfs = UnixFS.unmarshal(pbData)
48+
49+
// Verify mode and mtime are stored
50+
expect(unixfs.mode).to.equal(0o600)
51+
expect(unixfs.mtime?.secs).to.equal(1000n)
52+
expect(unixfs.mtime?.nsecs).to.equal(500)
4053
})
4154
})

0 commit comments

Comments
 (0)