Skip to content
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
"it-pipe": "^3.0.1",
"it-tar": "^6.0.0",
"it-to-buffer": "^4.0.5",
"kubo": "^0.29.0",
"kubo": "^0.37.0",
"mock-ipfs-pinning-service": "^0.4.2",
"nock": "^14.0.1",
"p-defer": "^4.0.0",
Expand Down
28 changes: 24 additions & 4 deletions src/lib/pins/normalise-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Pinnable {
cid?: CID
recursive?: boolean
metadata?: any
name?: string
}

export type ToPin = CID | string | Pinnable
Expand All @@ -15,6 +16,7 @@ export interface Pin {
path: string | CID
recursive: boolean
metadata?: any
name?: string
}

function isIterable (thing: any): thing is IterableIterator<any> & Iterator<any> {
Expand Down Expand Up @@ -110,7 +112,14 @@ export async function * normaliseInput (input: Source): AsyncGenerator<Pin> {
if (first.value.cid != null || first.value.path != null) {
yield toPin(first.value)
for (const obj of iterator) {
yield toPin(obj)
// Handle mixed iterables - obj could be CID or Pinnable
if (isCID(obj)) {
yield toPin({ cid: obj })
} else if (typeof obj === 'string') {
yield toPin({ path: obj })
} else {
yield toPin(obj)
}
}
return
}
Expand Down Expand Up @@ -146,7 +155,14 @@ export async function * normaliseInput (input: Source): AsyncGenerator<Pin> {
if (first.value.cid != null || first.value.path != null) {
yield toPin(first.value)
for await (const obj of iterator) {
yield toPin(obj)
// Handle mixed async iterables - obj could be CID or Pinnable
if (isCID(obj)) {
yield toPin({ cid: obj })
} else if (typeof obj === 'string') {
yield toPin({ path: obj })
} else {
yield toPin(obj)
}
}
return
}
Expand All @@ -158,10 +174,10 @@ export async function * normaliseInput (input: Source): AsyncGenerator<Pin> {
}

function toPin (input: Pinnable): Pin {
const path = input.cid ?? `${input.path}`
const path = input.cid ?? (input.path != null ? `${input.path}` : undefined)

if (path == null) {
throw new InvalidParametersError('Unexpected input: Please path either a CID or an IPFS path')
throw new InvalidParametersError('Unexpected input: Please pass either a CID or an IPFS path')
}

const pin: Pin = {
Expand All @@ -173,5 +189,9 @@ function toPin (input: Pinnable): Pin {
pin.metadata = input.metadata
}

if (input.name != null) {
pin.name = input.name
}

return pin
}
10 changes: 9 additions & 1 deletion src/lib/to-url-search-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ export function toUrlSearchParams ({ arg, searchParams, hashAlg, mtime, mode, ..
arg = [arg]
}

const urlSearchParams = new URLSearchParams(options)
// Filter out undefined and null values to avoid sending "undefined" or "null" as strings
const filteredOptions: Record<string, any> = {}
for (const [key, value] of Object.entries(options)) {
if (value !== undefined && value !== null) {
filteredOptions[key] = value
}
}

const urlSearchParams = new URLSearchParams(filteredOptions)

arg.forEach((arg: any) => {
urlSearchParams.append('arg', arg)
Expand Down
5 changes: 3 additions & 2 deletions src/pin/add-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type { HTTPRPCClient } from '../lib/core.js'

export function createAddAll (client: HTTPRPCClient): PinAPI['addAll'] {
return async function * addAll (source, options = {}) {
for await (const { path, recursive, metadata } of normaliseInput(source)) {
for await (const { path, recursive, metadata, name } of normaliseInput(source)) {
const res = await client.post('pin/add', {
signal: options.signal,
searchParams: toUrlSearchParams({
...options,
arg: path,
arg: path.toString(),
recursive,
metadata: metadata != null ? JSON.stringify(metadata) : undefined,
name: name ?? options.name,
stream: true
}),
headers: options.headers
Expand Down
31 changes: 31 additions & 0 deletions src/pin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export interface PinAddAllOptions extends HTTPRPCOptions {
* Internal option used to control whether to create a repo write lock during a pinning operation
*/
lock?: boolean

/**
* An optional name for created pin(s)
*/
name?: string
}

export type PinAddInput = CID | PinAddInputWithOptions
Expand All @@ -65,16 +70,42 @@ export interface PinAddInputWithOptions {
* A human readable string to store with this pin
*/
comments?: string

/**
* An optional name for the created pin
*/
name?: string
}

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

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

export interface PinLsOptions extends HTTPRPCOptions {
/**
* Path(s) to specific object(s) to be listed
*/
paths?: CID | CID[] | string | string[]

/**
* The type of pinned keys to list. Can be "direct", "indirect", "recursive", or "all".
*
* @default "all"
*/
type?: PinQueryType

/**
* Limit returned pins to ones with names that contain the value provided (case-sensitive, partial match).
* Implies names=true.
*/
name?: string

/**
* Include pin names in the output (slower, disabled by default).
*
* @default false
*/
names?: boolean
}

export interface PinLsResult {
Expand Down
14 changes: 14 additions & 0 deletions src/pin/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,25 @@ export function createLs (client: HTTPRPCClient): PinAPI['ls'] {
paths = Array.isArray(options.paths) ? options.paths : [options.paths]
}

// Check for conflicting options
if (options.name != null && options.names === false) {
throw new Error('Cannot use name filter when names is explicitly set to false')
}

// Check for empty name filter
if (options.name === '') {
throw new Error('Name filter cannot be empty string')
}

// If name filter is provided, automatically enable names flag
const names = options.names ?? (options.name != null)

const res = await client.post('pin/ls', {
signal: options.signal,
searchParams: toUrlSearchParams({
...options,
arg: paths.map(path => `${path}`),
names,
stream: true
}),
headers: options.headers
Expand Down
97 changes: 97 additions & 0 deletions test/interface-tests/src/pin/add-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,102 @@ export function testAddAll (factory: Factory<KuboNode>, options: MochaConfig): v
}
}())
})

it('should add pins with individual names', async () => {
const pins = await all(ipfs.pin.addAll([
{
cid: fixtures.files[0].cid,
name: 'pin-1'
},
{
cid: fixtures.files[1].cid,
name: 'pin-2'
}
]))

expect(pins).to.have.deep.members([
fixtures.files[0].cid,
fixtures.files[1].cid
])

// Verify names were set (need to use names flag to see them)
const pinList = await all(ipfs.pin.ls({ names: true }))
const pin1 = pinList.find(p => p.cid.equals(fixtures.files[0].cid))
const pin2 = pinList.find(p => p.cid.equals(fixtures.files[1].cid))

expect(pin1?.name).to.equal('pin-1')
expect(pin2?.name).to.equal('pin-2')
})

it('should add pins with a global name option', async () => {
const globalName = 'global-pin-name'
const pins = await all(ipfs.pin.addAll([
fixtures.files[0].cid,
fixtures.files[1].cid
], { name: globalName }))

expect(pins).to.have.deep.members([
fixtures.files[0].cid,
fixtures.files[1].cid
])

// Verify global name was applied (name filter automatically enables names)
const pinList = await all(ipfs.pin.ls({ name: globalName }))
expect(pinList).to.have.lengthOf(2)
expect(pinList.map(p => p.name)).to.deep.equal([globalName, globalName])
})

it('should prioritize individual names over global option', async () => {
const globalName = 'global-name'
const individualName = 'individual-name'

const pins = await all(ipfs.pin.addAll([
{
cid: fixtures.files[0].cid,
name: individualName
},
fixtures.files[1].cid
], { name: globalName }))

expect(pins).to.have.deep.members([
fixtures.files[0].cid,
fixtures.files[1].cid
])

// Verify individual name took precedence (need names flag)
const pinList = await all(ipfs.pin.ls({ names: true }))
const pin1 = pinList.find(p => p.cid.equals(fixtures.files[0].cid))
const pin2 = pinList.find(p => p.cid.equals(fixtures.files[1].cid))

expect(pin1?.name).to.equal(individualName)
expect(pin2?.name).to.equal(globalName)
})

it('should add pins without names', async () => {
const pins = await all(ipfs.pin.addAll([
fixtures.files[0].cid,
fixtures.files[1].cid
]))

expect(pins).to.have.deep.members([
fixtures.files[0].cid,
fixtures.files[1].cid
])

// Verify no names were set
// Without names flag, names should be undefined
const pinList = await all(ipfs.pin.ls())
const pin1 = pinList.find(p => p.cid.equals(fixtures.files[0].cid))
const pin2 = pinList.find(p => p.cid.equals(fixtures.files[1].cid))
expect(pin1?.name).to.be.undefined()
expect(pin2?.name).to.be.undefined()

// Even with names flag, they should still be undefined since no names were set
const pinListWithNames = await all(ipfs.pin.ls({ names: true }))
const pin1WithNames = pinListWithNames.find(p => p.cid.equals(fixtures.files[0].cid))
const pin2WithNames = pinListWithNames.find(p => p.cid.equals(fixtures.files[1].cid))
expect(pin1WithNames?.name).to.be.undefined()
expect(pin2WithNames?.name).to.be.undefined()
})
})
}
58 changes: 58 additions & 0 deletions test/interface-tests/src/pin/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,63 @@ export function testAdd (factory: Factory<KuboNode>, options: MochaConfig): void
type: 'indirect'
})
})

it('should add a pin with a name', async () => {
const pinName = 'test-pin-name'
const cid = await ipfs.pin.add(fixtures.files[0].cid, {
name: pinName
})
expect(cid).to.deep.equal(fixtures.files[0].cid)

// Using name filter automatically enables names flag
const pins = await all(ipfs.pin.ls({ name: pinName }))
expect(pins).to.have.lengthOf(1)
expect(pins[0]).to.deep.include({
cid: fixtures.files[0].cid,
type: 'recursive',
name: pinName
})
})

it('should add a pin without a name', async () => {
const cid = await ipfs.pin.add(fixtures.files[0].cid)
expect(cid).to.deep.equal(fixtures.files[0].cid)

// Check without names flag - name should be undefined
const pins = await all(ipfs.pin.ls({ paths: fixtures.files[0].cid }))
expect(pins).to.have.lengthOf(1)
expect(pins[0].cid).to.deep.equal(fixtures.files[0].cid)
expect(pins[0].name).to.be.undefined()

// Also verify with names flag - should still be undefined since no name was set
const pinsWithNames = await all(ipfs.pin.ls({ paths: fixtures.files[0].cid, names: true }))
expect(pinsWithNames).to.have.lengthOf(1)
expect(pinsWithNames[0].cid).to.deep.equal(fixtures.files[0].cid)
expect(pinsWithNames[0].name).to.be.undefined()
})

it('should update pin name when pinning again with different name', async () => {
const firstName = 'first-name'
const secondName = 'second-name'

// Pin with first name
await ipfs.pin.add(fixtures.files[0].cid, { name: firstName })

// Verify first name (name filter automatically enables names flag)
let pins = await all(ipfs.pin.ls({ name: firstName }))
expect(pins).to.have.lengthOf(1)
expect(pins[0].name).to.equal(firstName)

// Pin again with second name
await ipfs.pin.add(fixtures.files[0].cid, { name: secondName })

// Verify name was updated
pins = await all(ipfs.pin.ls({ name: firstName }))
expect(pins).to.have.lengthOf(0)

pins = await all(ipfs.pin.ls({ name: secondName }))
expect(pins).to.have.lengthOf(1)
expect(pins[0].name).to.equal(secondName)
})
})
}
Loading
Loading