-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
4 changed files
with
375 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,12 @@ | ||
'use strict' | ||
|
||
const Remote = require('./remote') | ||
|
||
module.exports = config => ({ | ||
add: require('./add')(config), | ||
addAll: require('./add-all')(config), | ||
ls: require('./ls')(config), | ||
rm: require('./rm')(config), | ||
rmAll: require('./rm-all')(config) | ||
rmAll: require('./rm-all')(config), | ||
remote: new Remote(config) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
'use strict' | ||
|
||
const CID = require('cids') | ||
const Client = require('../../lib/core') | ||
const Service = require('./service') | ||
const toUrlSearchParams = require('../../lib/to-url-search-params') | ||
|
||
/** | ||
* @typedef {import('../..').HttpOptions} HttpOptions | ||
* @typedef {import('../../lib/core').ClientOptions} ClientOptions | ||
* @typedef {import('ipfs-core-types/src/basic').AbortOptions} AbortOptions | ||
* @typedef {import('ipfs-core-types/src/pin/remote').API} API | ||
* @typedef {import('ipfs-core-types/src/pin/remote').Pin} Pin | ||
* @typedef {import('ipfs-core-types/src/pin/remote').AddOptions} AddOptions | ||
* @typedef {import('ipfs-core-types/src/pin/remote').Query} Query | ||
* @typedef {import('ipfs-core-types/src/pin/remote').Status} Status | ||
* | ||
* @implements {API} | ||
*/ | ||
class Remote { | ||
/** | ||
* @param {ClientOptions} options | ||
*/ | ||
constructor (options) { | ||
/** @private */ | ||
this.client = new Client(options) | ||
/** @readonly */ | ||
this.service = new Service(options) | ||
} | ||
|
||
/** | ||
* Stores an IPFS object(s) from a given path to a remote pinning service. | ||
* | ||
* @param {CID} cid | ||
* @param {AddOptions & AbortOptions & HttpOptions} options | ||
* @returns {Promise<Pin>} | ||
*/ | ||
add (cid, options) { | ||
return Remote.add(this.client, cid, options) | ||
} | ||
|
||
/** | ||
* @param {Client} client | ||
* @param {CID} cid | ||
* @param {AddOptions & AbortOptions & HttpOptions} options | ||
*/ | ||
static async add (client, cid, { timeout, signal, headers, ...options }) { | ||
const response = await client.post('pin/remote/add', { | ||
timeout, | ||
signal, | ||
headers, | ||
searchParams: encodeAddParams({ cid, ...options }) | ||
}) | ||
|
||
return Remote.decodePin(await response.json()) | ||
} | ||
|
||
/** | ||
* @param {Object} json | ||
* @param {string} json.Name | ||
* @param {string} json.Cid | ||
* @param {Status} json.Status | ||
* @returns {Pin} | ||
*/ | ||
static decodePin ({ Name: name, Status: status, Cid: cid }) { | ||
return { | ||
cid: new CID(cid), | ||
name, | ||
status | ||
} | ||
} | ||
|
||
/** | ||
* Returns a list of matching pins on the remote pinning service. | ||
* | ||
* @param {Query & AbortOptions & HttpOptions} query | ||
*/ | ||
ls (query) { | ||
return Remote.ls(this.client, query) | ||
} | ||
|
||
/** | ||
* | ||
* @param {Client} client | ||
* @param {Query & AbortOptions & HttpOptions} options | ||
* @returns {AsyncIterable<Pin>} | ||
*/ | ||
static async * ls (client, { timeout, signal, headers, ...query }) { | ||
const response = await client.post('pin/remote/ls', { | ||
signal, | ||
timeout, | ||
headers, | ||
searchParams: encodeQuery(query) | ||
}) | ||
|
||
for await (const pin of response.ndjson()) { | ||
yield Remote.decodePin(pin) | ||
} | ||
} | ||
|
||
/** | ||
* Removes a single pin object matching query allowing it to be garbage | ||
* collected (if needed). Will error if multiple pins mtach provided | ||
* query. To remove all matches use `rmAll` instead. | ||
* | ||
* @param {Query & AbortOptions & HttpOptions} query | ||
*/ | ||
rm (query) { | ||
return Remote.rm(this.client, { ...query, all: false }) | ||
} | ||
|
||
/** | ||
* Removes all pin object that match given query allowing them to be garbage | ||
* collected if needed. | ||
* | ||
* @param {Query & AbortOptions & HttpOptions} query | ||
*/ | ||
rmAll (query) { | ||
return Remote.rm(this.client, { ...query, all: true }) | ||
} | ||
|
||
/** | ||
* | ||
* @param {Client} client | ||
* @param {{all: boolean} & Query & AbortOptions & HttpOptions} options | ||
*/ | ||
static async rm (client, { timeout, signal, headers, ...query }) { | ||
await client.post('pin/remote/rm', { | ||
timeout, | ||
signal, | ||
headers, | ||
searchParams: encodeQuery(query) | ||
}) | ||
} | ||
} | ||
|
||
/** | ||
* @param {any} service | ||
* @returns {string} | ||
*/ | ||
const encodeService = (service) => { | ||
if (typeof service === 'string' && service !== '') { | ||
return service | ||
} else { | ||
throw new TypeError('service name must be passed') | ||
} | ||
} | ||
|
||
/** | ||
* @param {any} cid | ||
* @returns {string} | ||
*/ | ||
const encodeCID = (cid) => { | ||
if (CID.isCID(cid)) { | ||
return cid.toString() | ||
} else { | ||
throw new TypeError(`CID instance expected instead of ${cid}`) | ||
} | ||
} | ||
|
||
/** | ||
* @param {Query & { all?: boolean }} query | ||
* @returns {URLSearchParams} | ||
*/ | ||
const encodeQuery = ({ service, cid, name, status, all }) => { | ||
const query = toUrlSearchParams({ | ||
service: encodeService(service), | ||
name, | ||
force: all ? true : undefined | ||
}) | ||
|
||
if (cid) { | ||
for (const value of cid) { | ||
query.append('cid', encodeCID(value)) | ||
} | ||
} | ||
|
||
if (status) { | ||
for (const value of status) { | ||
query.append('status', value) | ||
} | ||
} | ||
|
||
return query | ||
} | ||
|
||
/** | ||
* @param {AddOptions & {cid:CID}} options | ||
* @returns {URLSearchParams} | ||
*/ | ||
const encodeAddParams = ({ cid, service, background, name, origins }) => { | ||
const params = toUrlSearchParams({ | ||
arg: encodeCID(cid), | ||
service: encodeService(service), | ||
name, | ||
background: background ? true : undefined | ||
}) | ||
|
||
if (origins) { | ||
for (const origin of origins) { | ||
params.append('origin', origin.toString()) | ||
} | ||
} | ||
|
||
return params | ||
} | ||
|
||
module.exports = Remote |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
'use strict' | ||
|
||
const Client = require('../../lib/core') | ||
const toUrlSearchParams = require('../../lib/to-url-search-params') | ||
|
||
/** | ||
* @typedef {import('../../lib/core').ClientOptions} ClientOptions | ||
* @typedef {import('../..').HttpOptions} HttpOptions | ||
* @typedef {import('ipfs-core-types/src/basic').AbortOptions} AbortOptions | ||
* @typedef {import('ipfs-core-types/src/pin/remote/service').API} API | ||
* @typedef {import('ipfs-core-types/src/pin/remote/service').Credentials} Credentials | ||
* @typedef {import('ipfs-core-types/src/pin/remote/service').RemotePinService} RemotePinService | ||
* @typedef {import('ipfs-core-types/src/pin/remote/service').RemotePinServiceWithStat} RemotePinServiceWithStat | ||
* @implements {API} | ||
*/ | ||
class Service { | ||
/** | ||
* @param {ClientOptions} options | ||
*/ | ||
constructor (options) { | ||
/** @private */ | ||
this.client = new Client(options) | ||
} | ||
|
||
/** | ||
* @param {Client} client | ||
* @param {string} name | ||
* @param {Credentials & AbortOptions & HttpOptions} options | ||
*/ | ||
static async add (client, name, options) { | ||
const { endpoint, key, headers, timeout, signal } = options | ||
await client.post('pin/remote/service/add', { | ||
timeout, | ||
signal, | ||
searchParams: toUrlSearchParams({ | ||
arg: [name, Service.encodeEndpoint(endpoint), key] | ||
}), | ||
headers | ||
}) | ||
} | ||
|
||
/** | ||
* @param {URL} url | ||
*/ | ||
static encodeEndpoint (url) { | ||
const href = String(url) | ||
if (href === 'undefined') { | ||
throw Error('endpoint is required') | ||
} | ||
// Workaround trailing `/` issue in go-ipfs | ||
// @see https://github.com/ipfs/go-ipfs/issues/7826 | ||
return href[href.length - 1] === '/' ? href.slice(0, -1) : href | ||
} | ||
|
||
/** | ||
* @param {Client} client | ||
* @param {string} name | ||
* @param {AbortOptions & HttpOptions} [options] | ||
*/ | ||
static async rm (client, name, { timeout, signal, headers } = {}) { | ||
await client.post('pin/remote/service/rm', { | ||
timeout, | ||
signal, | ||
headers, | ||
searchParams: toUrlSearchParams({ | ||
arg: name | ||
}) | ||
}) | ||
} | ||
|
||
/** | ||
* @template {true} Stat | ||
* @param {Client} client | ||
* @param {{ stat?: Stat } & AbortOptions & HttpOptions} [options] | ||
*/ | ||
static async ls (client, { stat, timeout, signal, headers } = {}) { | ||
const response = await client.post('pin/remote/service/ls', { | ||
searchParams: stat === true ? toUrlSearchParams({ stat }) : undefined, | ||
timeout, | ||
signal, | ||
headers | ||
}) | ||
|
||
/** @type {{RemoteServices: Object[]}} */ | ||
const { RemoteServices } = await response.json() | ||
|
||
/** @type {Stat extends true ? RemotePinServiceWithStat[] : RemotePinService []} */ | ||
return (RemoteServices.map(Service.decodeRemoteService)) | ||
} | ||
|
||
/** | ||
* @param {Object} json | ||
* @returns {RemotePinServiceWithStat} | ||
*/ | ||
static decodeRemoteService (json) { | ||
return { | ||
service: json.Service, | ||
endpoint: new URL(json.ApiEndpoint), | ||
...(json.Stat && { stat: Service.decodeStat(json.Stat) }) | ||
} | ||
} | ||
|
||
/** | ||
* @param {Object} json | ||
* @returns {import('ipfs-core-types/src/pin/remote/service').Stat} | ||
*/ | ||
static decodeStat (json) { | ||
switch (json.Status) { | ||
case 'valid': { | ||
const { Pinning, Pinned, Queued, Failed } = json.PinCount | ||
return { | ||
status: 'valid', | ||
pinCount: { | ||
queued: Queued, | ||
pinning: Pinning, | ||
pinned: Pinned, | ||
failed: Failed | ||
} | ||
} | ||
} | ||
case 'invalid': { | ||
return { status: 'invalid' } | ||
} | ||
default: { | ||
return { status: json.Status } | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Registers remote pinning service with a given name. Errors if service | ||
* with the given name is already registered. | ||
* | ||
* @param {string} name | ||
* @param {Credentials & AbortOptions & HttpOptions} options | ||
*/ | ||
add (name, options) { | ||
return Service.add(this.client, name, options) | ||
} | ||
|
||
/** | ||
* Unregisteres remote pinning service with a given name. If service with such | ||
* name isn't registerede this is a noop. | ||
* | ||
* @param {string} name | ||
* @param {AbortOptions & HttpOptions} [options] | ||
*/ | ||
rm (name, options) { | ||
return Service.rm(this.client, name, options) | ||
} | ||
|
||
/** | ||
* List registered remote pinning services. | ||
* | ||
* @param {{ stat?: true } & AbortOptions & HttpOptions} [options] | ||
*/ | ||
ls (options) { | ||
return Service.ls(this.client, options) | ||
} | ||
} | ||
|
||
module.exports = Service |