Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,20 @@ const ipfs = create({ timeout: '2m' })

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).

#### Testing with Custom Kubo Binary

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:

```bash
# Test with a custom kubo binary
IPFS_GO_EXEC=/path/to/custom/kubo npm test

# Example: testing with locally built kubo
IPFS_GO_EXEC=/home/user/kubo/cmd/ipfs/ipfs npm test
```

This is particularly useful when developing features that require changes to both kubo and this client.

## Historical context

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.
Expand Down
1,916 changes: 1,483 additions & 433 deletions package-lock.json

Large diffs are not rendered by default.

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.38.0",
"mock-ipfs-pinning-service": "^0.4.2",
"nock": "^14.0.1",
"p-defer": "^4.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/bitswap/stat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function createStat (client: HTTPRPCClient): BitswapAPI['stat'] {

function toCoreInterface (res: any): BitswapStats {
return {
provideBufLen: res.ProvideBufLen,
provideBufLen: res.ProvideBufLen ?? 0,
wantlist: (res.Wantlist ?? []).map((k: any) => CID.parse(k['/'])),
peers: (res.Peers ?? []).map((str: any) => peerIdFromString(str)),
blocksReceived: BigInt(res.BlocksReceived),
Expand Down
85 changes: 30 additions & 55 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 @@ -88,34 +90,12 @@ export async function * normaliseInput (input: Source): AsyncGenerator<Pin> {
return iterator
}

// Iterable<CID>
if (isCID(first.value)) {
yield toPin({ cid: first.value })
for (const cid of iterator) {
yield toPin({ cid })
}
return
// Iterable<ToPin>
yield toPin(toPinnable(first.value))
for (const obj of iterator) {
yield toPin(toPinnable(obj))
}

// Iterable<String>
if (typeof first.value === 'string') {
yield toPin({ path: first.value })
for (const path of iterator) {
yield toPin({ path })
}
return
}

// Iterable<Pinnable>
if (first.value.cid != null || first.value.path != null) {
yield toPin(first.value)
for (const obj of iterator) {
yield toPin(obj)
}
return
}

throw new InvalidParametersError(`Unexpected input: ${typeof input}`)
return
}

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

// AsyncIterable<CID>
if (isCID(first.value)) {
yield toPin({ cid: first.value })
for await (const cid of iterator) {
yield toPin({ cid })
}
return
}

// AsyncIterable<String>
if (typeof first.value === 'string') {
yield toPin({ path: first.value })
for await (const path of iterator) {
yield toPin({ path })
}
return
// AsyncIterable<ToPin>
yield toPin(toPinnable(first.value))
for await (const obj of iterator) {
yield toPin(toPinnable(obj))
}
return
}

// AsyncIterable<{ cid: CID|String recursive, metadata }>
if (first.value.cid != null || first.value.path != null) {
yield toPin(first.value)
for await (const obj of iterator) {
yield toPin(obj)
}
return
}
throw new InvalidParametersError(`Unexpected input: ${typeof input}`)
}

throw new InvalidParametersError(`Unexpected input: ${typeof input}`)
function toPinnable (input: ToPin): Pinnable {
if (isCID(input)) {
return { cid: input }
}
if (typeof input === 'string') {
return { path: input }
}
if (typeof input === 'object' && (input.cid != null || input.path != null)) {
return input
}

throw new InvalidParametersError(`Unexpected input: ${typeof input}`)
}

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 +144,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
19 changes: 16 additions & 3 deletions test/files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

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

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

// Metadata is not returned in response yet (issue #6920)
expect(result).to.not.have.property('mode')
expect(result).to.not.have.property('mtime')
expect(result).to.have.property('cid')

const { cid } = result
expect(cid).to.have.property('code', dagPB.code)
expect(cid.toString()).to.equal('QmVv4Wz46JaZJeH5PMV4LGbRiiMKEmszPYY3g6fjGnVXBS')
// CID is different because metadata is now properly sent to the API
expect(cid.toString()).to.equal('QmPDB1sHH2FNqwJm2A6747uf6JUZyEB2cnNFRPz2uzjoDZ')

// Verify metadata is stored in UnixFS structure via DAG API
const dagNode = await ipfs.dag.get(cid)
const pbData = dagNode.value.Data
const unixfs = UnixFS.unmarshal(pbData)

// Verify mode and mtime are stored
expect(unixfs.mode).to.equal(0o600)
expect(unixfs.mtime?.secs).to.equal(1000n)
expect(unixfs.mtime?.nsecs).to.equal(500)
})
})
4 changes: 2 additions & 2 deletions test/interface-tests.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-env mocha */

import { path } from 'kubo'
import kubo from 'kubo'
import { isWindows, isFirefox, isChrome } from './constants.js'
import * as tests from './interface-tests/src/index.js'
import { factory } from './utils/factory.js'
Expand Down Expand Up @@ -267,7 +267,7 @@ describe('kubo-rpc-client tests against kubo', function () {
*/
const commonFactory = factory({
type: 'kubo',
bin: path?.(),
bin: process.env.IPFS_GO_EXEC ?? kubo?.path(),
test: true
})
describe('kubo RPC client interface tests', function () {
Expand Down
21 changes: 13 additions & 8 deletions test/interface-tests/src/config/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ export function testSet (factory: Factory<KuboNode>, options: MochaConfig): void
})

it('should set a new key', async () => {
await ipfs.config.set('Fruit', 'banana')
await ipfs.config.set('Gateway.RootRedirect', '/test1')

const fruit = await ipfs.config.get('Fruit')
expect(fruit).to.equal('banana')
const redirect = await ipfs.config.get('Gateway.RootRedirect')
expect(redirect).to.equal('/test1')
})

it('should set an already existing key', async () => {
await ipfs.config.set('Fruit', 'morango')
await ipfs.config.set('Gateway.RootRedirect', '/test2')

const fruit = await ipfs.config.get('Fruit')
expect(fruit).to.equal('morango')
const redirect = await ipfs.config.get('Gateway.RootRedirect')
expect(redirect).to.equal('/test2')
})

it('should set a number', async () => {
const key = 'Discovery.MDNS.Interval'
const key = 'Gateway.MaxConcurrentRequests'
const val = 11

await ipfs.config.set(key, val)
Expand Down Expand Up @@ -77,10 +77,15 @@ export function testSet (factory: Factory<KuboNode>, options: MochaConfig): void
return expect(ipfs.config.set(uint8ArrayFromString('heeey'), '')).to.eventually.be.rejected()
})

it('should fail on unknown config key', () => {
return expect(ipfs.config.set('Fruit', 'banana')).to.eventually.be.rejected()
.with.property('message').that.matches(/Fruit not found/)
})

it('should fail on non valid value', () => {
const val: any = {}
val.val = val
return expect(ipfs.config.set('Fruit', val)).to.eventually.be.rejected()
return expect(ipfs.config.set('Gateway.RootRedirect', val)).to.eventually.be.rejected()
})
})
}
Loading