Skip to content

Commit 1e874c2

Browse files
authored
feat: support remote pinning services in ipfs-http-client (#3293)
Implement [remote pinning service API](https://github.com/ipfs/pinning-services-api-spec) in ipfs-http-client.
1 parent fdefd11 commit 1e874c2

File tree

4 files changed

+375
-2
lines changed

4 files changed

+375
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"devDependencies": {
8383
"aegir": "^29.2.2",
8484
"delay": "^4.4.0",
85-
"go-ipfs": "^0.7.0",
85+
"go-ipfs": "0.8.0-rc2",
8686
"ipfs-core": "^0.4.2",
8787
"ipfsd-ctl": "^7.2.0",
8888
"it-all": "^1.0.4",

src/pin/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
'use strict'
22

3+
const Remote = require('./remote')
4+
35
module.exports = config => ({
46
add: require('./add')(config),
57
addAll: require('./add-all')(config),
68
ls: require('./ls')(config),
79
rm: require('./rm')(config),
8-
rmAll: require('./rm-all')(config)
10+
rmAll: require('./rm-all')(config),
11+
remote: new Remote(config)
912
})

src/pin/remote/index.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
'use strict'
2+
3+
const CID = require('cids')
4+
const Client = require('../../lib/core')
5+
const Service = require('./service')
6+
const toUrlSearchParams = require('../../lib/to-url-search-params')
7+
8+
/**
9+
* @typedef {import('../..').HttpOptions} HttpOptions
10+
* @typedef {import('../../lib/core').ClientOptions} ClientOptions
11+
* @typedef {import('ipfs-core-types/src/basic').AbortOptions} AbortOptions
12+
* @typedef {import('ipfs-core-types/src/pin/remote').API} API
13+
* @typedef {import('ipfs-core-types/src/pin/remote').Pin} Pin
14+
* @typedef {import('ipfs-core-types/src/pin/remote').AddOptions} AddOptions
15+
* @typedef {import('ipfs-core-types/src/pin/remote').Query} Query
16+
* @typedef {import('ipfs-core-types/src/pin/remote').Status} Status
17+
*
18+
* @implements {API}
19+
*/
20+
class Remote {
21+
/**
22+
* @param {ClientOptions} options
23+
*/
24+
constructor (options) {
25+
/** @private */
26+
this.client = new Client(options)
27+
/** @readonly */
28+
this.service = new Service(options)
29+
}
30+
31+
/**
32+
* Stores an IPFS object(s) from a given path to a remote pinning service.
33+
*
34+
* @param {CID} cid
35+
* @param {AddOptions & AbortOptions & HttpOptions} options
36+
* @returns {Promise<Pin>}
37+
*/
38+
add (cid, options) {
39+
return Remote.add(this.client, cid, options)
40+
}
41+
42+
/**
43+
* @param {Client} client
44+
* @param {CID} cid
45+
* @param {AddOptions & AbortOptions & HttpOptions} options
46+
*/
47+
static async add (client, cid, { timeout, signal, headers, ...options }) {
48+
const response = await client.post('pin/remote/add', {
49+
timeout,
50+
signal,
51+
headers,
52+
searchParams: encodeAddParams({ cid, ...options })
53+
})
54+
55+
return Remote.decodePin(await response.json())
56+
}
57+
58+
/**
59+
* @param {Object} json
60+
* @param {string} json.Name
61+
* @param {string} json.Cid
62+
* @param {Status} json.Status
63+
* @returns {Pin}
64+
*/
65+
static decodePin ({ Name: name, Status: status, Cid: cid }) {
66+
return {
67+
cid: new CID(cid),
68+
name,
69+
status
70+
}
71+
}
72+
73+
/**
74+
* Returns a list of matching pins on the remote pinning service.
75+
*
76+
* @param {Query & AbortOptions & HttpOptions} query
77+
*/
78+
ls (query) {
79+
return Remote.ls(this.client, query)
80+
}
81+
82+
/**
83+
*
84+
* @param {Client} client
85+
* @param {Query & AbortOptions & HttpOptions} options
86+
* @returns {AsyncIterable<Pin>}
87+
*/
88+
static async * ls (client, { timeout, signal, headers, ...query }) {
89+
const response = await client.post('pin/remote/ls', {
90+
signal,
91+
timeout,
92+
headers,
93+
searchParams: encodeQuery(query)
94+
})
95+
96+
for await (const pin of response.ndjson()) {
97+
yield Remote.decodePin(pin)
98+
}
99+
}
100+
101+
/**
102+
* Removes a single pin object matching query allowing it to be garbage
103+
* collected (if needed). Will error if multiple pins mtach provided
104+
* query. To remove all matches use `rmAll` instead.
105+
*
106+
* @param {Query & AbortOptions & HttpOptions} query
107+
*/
108+
rm (query) {
109+
return Remote.rm(this.client, { ...query, all: false })
110+
}
111+
112+
/**
113+
* Removes all pin object that match given query allowing them to be garbage
114+
* collected if needed.
115+
*
116+
* @param {Query & AbortOptions & HttpOptions} query
117+
*/
118+
rmAll (query) {
119+
return Remote.rm(this.client, { ...query, all: true })
120+
}
121+
122+
/**
123+
*
124+
* @param {Client} client
125+
* @param {{all: boolean} & Query & AbortOptions & HttpOptions} options
126+
*/
127+
static async rm (client, { timeout, signal, headers, ...query }) {
128+
await client.post('pin/remote/rm', {
129+
timeout,
130+
signal,
131+
headers,
132+
searchParams: encodeQuery(query)
133+
})
134+
}
135+
}
136+
137+
/**
138+
* @param {any} service
139+
* @returns {string}
140+
*/
141+
const encodeService = (service) => {
142+
if (typeof service === 'string' && service !== '') {
143+
return service
144+
} else {
145+
throw new TypeError('service name must be passed')
146+
}
147+
}
148+
149+
/**
150+
* @param {any} cid
151+
* @returns {string}
152+
*/
153+
const encodeCID = (cid) => {
154+
if (CID.isCID(cid)) {
155+
return cid.toString()
156+
} else {
157+
throw new TypeError(`CID instance expected instead of ${cid}`)
158+
}
159+
}
160+
161+
/**
162+
* @param {Query & { all?: boolean }} query
163+
* @returns {URLSearchParams}
164+
*/
165+
const encodeQuery = ({ service, cid, name, status, all }) => {
166+
const query = toUrlSearchParams({
167+
service: encodeService(service),
168+
name,
169+
force: all ? true : undefined
170+
})
171+
172+
if (cid) {
173+
for (const value of cid) {
174+
query.append('cid', encodeCID(value))
175+
}
176+
}
177+
178+
if (status) {
179+
for (const value of status) {
180+
query.append('status', value)
181+
}
182+
}
183+
184+
return query
185+
}
186+
187+
/**
188+
* @param {AddOptions & {cid:CID}} options
189+
* @returns {URLSearchParams}
190+
*/
191+
const encodeAddParams = ({ cid, service, background, name, origins }) => {
192+
const params = toUrlSearchParams({
193+
arg: encodeCID(cid),
194+
service: encodeService(service),
195+
name,
196+
background: background ? true : undefined
197+
})
198+
199+
if (origins) {
200+
for (const origin of origins) {
201+
params.append('origin', origin.toString())
202+
}
203+
}
204+
205+
return params
206+
}
207+
208+
module.exports = Remote

src/pin/remote/service.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
'use strict'
2+
3+
const Client = require('../../lib/core')
4+
const toUrlSearchParams = require('../../lib/to-url-search-params')
5+
6+
/**
7+
* @typedef {import('../../lib/core').ClientOptions} ClientOptions
8+
* @typedef {import('../..').HttpOptions} HttpOptions
9+
* @typedef {import('ipfs-core-types/src/basic').AbortOptions} AbortOptions
10+
* @typedef {import('ipfs-core-types/src/pin/remote/service').API} API
11+
* @typedef {import('ipfs-core-types/src/pin/remote/service').Credentials} Credentials
12+
* @typedef {import('ipfs-core-types/src/pin/remote/service').RemotePinService} RemotePinService
13+
* @typedef {import('ipfs-core-types/src/pin/remote/service').RemotePinServiceWithStat} RemotePinServiceWithStat
14+
* @implements {API}
15+
*/
16+
class Service {
17+
/**
18+
* @param {ClientOptions} options
19+
*/
20+
constructor (options) {
21+
/** @private */
22+
this.client = new Client(options)
23+
}
24+
25+
/**
26+
* @param {Client} client
27+
* @param {string} name
28+
* @param {Credentials & AbortOptions & HttpOptions} options
29+
*/
30+
static async add (client, name, options) {
31+
const { endpoint, key, headers, timeout, signal } = options
32+
await client.post('pin/remote/service/add', {
33+
timeout,
34+
signal,
35+
searchParams: toUrlSearchParams({
36+
arg: [name, Service.encodeEndpoint(endpoint), key]
37+
}),
38+
headers
39+
})
40+
}
41+
42+
/**
43+
* @param {URL} url
44+
*/
45+
static encodeEndpoint (url) {
46+
const href = String(url)
47+
if (href === 'undefined') {
48+
throw Error('endpoint is required')
49+
}
50+
// Workaround trailing `/` issue in go-ipfs
51+
// @see https://github.com/ipfs/go-ipfs/issues/7826
52+
return href[href.length - 1] === '/' ? href.slice(0, -1) : href
53+
}
54+
55+
/**
56+
* @param {Client} client
57+
* @param {string} name
58+
* @param {AbortOptions & HttpOptions} [options]
59+
*/
60+
static async rm (client, name, { timeout, signal, headers } = {}) {
61+
await client.post('pin/remote/service/rm', {
62+
timeout,
63+
signal,
64+
headers,
65+
searchParams: toUrlSearchParams({
66+
arg: name
67+
})
68+
})
69+
}
70+
71+
/**
72+
* @template {true} Stat
73+
* @param {Client} client
74+
* @param {{ stat?: Stat } & AbortOptions & HttpOptions} [options]
75+
*/
76+
static async ls (client, { stat, timeout, signal, headers } = {}) {
77+
const response = await client.post('pin/remote/service/ls', {
78+
searchParams: stat === true ? toUrlSearchParams({ stat }) : undefined,
79+
timeout,
80+
signal,
81+
headers
82+
})
83+
84+
/** @type {{RemoteServices: Object[]}} */
85+
const { RemoteServices } = await response.json()
86+
87+
/** @type {Stat extends true ? RemotePinServiceWithStat[] : RemotePinService []} */
88+
return (RemoteServices.map(Service.decodeRemoteService))
89+
}
90+
91+
/**
92+
* @param {Object} json
93+
* @returns {RemotePinServiceWithStat}
94+
*/
95+
static decodeRemoteService (json) {
96+
return {
97+
service: json.Service,
98+
endpoint: new URL(json.ApiEndpoint),
99+
...(json.Stat && { stat: Service.decodeStat(json.Stat) })
100+
}
101+
}
102+
103+
/**
104+
* @param {Object} json
105+
* @returns {import('ipfs-core-types/src/pin/remote/service').Stat}
106+
*/
107+
static decodeStat (json) {
108+
switch (json.Status) {
109+
case 'valid': {
110+
const { Pinning, Pinned, Queued, Failed } = json.PinCount
111+
return {
112+
status: 'valid',
113+
pinCount: {
114+
queued: Queued,
115+
pinning: Pinning,
116+
pinned: Pinned,
117+
failed: Failed
118+
}
119+
}
120+
}
121+
case 'invalid': {
122+
return { status: 'invalid' }
123+
}
124+
default: {
125+
return { status: json.Status }
126+
}
127+
}
128+
}
129+
130+
/**
131+
* Registers remote pinning service with a given name. Errors if service
132+
* with the given name is already registered.
133+
*
134+
* @param {string} name
135+
* @param {Credentials & AbortOptions & HttpOptions} options
136+
*/
137+
add (name, options) {
138+
return Service.add(this.client, name, options)
139+
}
140+
141+
/**
142+
* Unregisteres remote pinning service with a given name. If service with such
143+
* name isn't registerede this is a noop.
144+
*
145+
* @param {string} name
146+
* @param {AbortOptions & HttpOptions} [options]
147+
*/
148+
rm (name, options) {
149+
return Service.rm(this.client, name, options)
150+
}
151+
152+
/**
153+
* List registered remote pinning services.
154+
*
155+
* @param {{ stat?: true } & AbortOptions & HttpOptions} [options]
156+
*/
157+
ls (options) {
158+
return Service.ls(this.client, options)
159+
}
160+
}
161+
162+
module.exports = Service

0 commit comments

Comments
 (0)