Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

feature: implement remote pinning API #3588

Closed
wants to merge 21 commits into from
Closed

feature: implement remote pinning API #3588

wants to merge 21 commits into from

Conversation

yusefnapora
Copy link
Contributor

This is an implementation of the ipfs.pin.remote API as well as I can gather from reading the tests in interface-ipfs-core.

I haven't figured out how to actually run the tests yet, so we'll see 😄

Leaving as a draft for now, since I probably missed some stuff and am out of steam for the day. Also it's depending on an unpublished npm package (the js-ipfs-pinning-service-client I wrote earlier) so we should sort that out.

cc @Gozala :)

@welcome

This comment has been minimized.

@yusefnapora
Copy link
Contributor Author

Hey @Gozala & @alanshaw, I think this is basically ready to review, but there are a couple things that I'd like to sort out before merging.

First is the js-ipfs-pinning-service-client package, which isn't published to NPM and is just living in a github repo under my personal username. Would this belong in js-ipfs as one of the subpackages? I can pull it into this PR if so. Otherwise I guess it should at least get moved to the ipfs github org or similar and published to npm.

Second, I didn't implement saving / loading pinning service configs to the main IPFS config, although the config service instance does get passed through to the right spot, so it should't be too hard to wire up. The cli commands (js-ipfs pin remote add, etc) are also unimplemented, but maybe that could wait for a separate PR.

Anyway, things seem to have reached "works on my machine" status, so it would be great if someone else could check it out and kick the tires a bit.

@yusefnapora yusefnapora marked this pull request as ready for review March 12, 2021 15:55
@BigLep BigLep added the status/in-progress In progress label Mar 15, 2021
Copy link
Member

@lidel lidel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick drive-by comments :)

Perhaps as a part of this PR you could add a simple browser example under examples/browser-remote-pinning with:

  • input for CID (or a file picker, so user can ipfs.add a file and fill CID field that way)
  • inputs for pinning service Endpoint and Key
  • "pin!" button

It would make testing against different services easier + will be a useful artifact for the community that wants to add support for remote pinning to their dapp.

First is the js-ipfs-pinning-service-client package, which isn't published to NPM and is just living in a github repo under my personal username. Would this belong in js-ipfs as one of the subpackages?

I don't think it should be part of js-ipfs, it will be useful for people who don't run IPFS node at all (ipfs/pinning-services-api-spec#43)
We already have go counterpart at https://github.com/ipfs/go-pinning-service-http-client, so perhaps move it to https://github.com/ipfs/ and then publish to NPM, so I can add it to https://github.com/ipfs/pinning-services-api-spec#client-libraries :)

@yusefnapora
Copy link
Contributor Author

@lidel, that's a good idea about the example. I haven't tested this in browsers yet, so that will be good proof that it works :) I'll try to add the example in tomorrow.

I agree that the pinning HTTP client is useful outside of js-ipfs, so it makes sense to put it into the ipfs github org. I'd like to add some tests to the client first, but that shouldn't take long. Then someone could review it and pull it into the ipfs org and do the npm thing. I'll send a flare up tomorrow or Wednesday for review :)

Copy link
Contributor

@Gozala Gozala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yusefnapora again for driving this effort, I am very excited about this making it's way to js-ipfs.

I did a first pass and left some notes. Overall it looks good but there are few things we'd need to figure out before we can move this forward

  1. Separate generated pinning service client. I am not sure if having it in separate package provides enough value to justify added overhead of coordinating changes.
  2. Generated client pulls in different http library that would increase bundle size in browser and probably increase a maintenance burden. I think we would want to reuse existing http code used in rest of the code base instead (and that may imply not generating, but writing client code). However ultimately it's @achingbrain call.
  3. Currently things are not persisted across sessions, which I think we should add before landing this.
  4. We already have interface and type definitions in place (see https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs-core-types/src/pin/remote.ts) and it would be good to utilize them instead of duplicate them. Specifically @implements {API} allows us to ensure that same API is implanted across HTTP client and js-ipfs. If we need to update types that's fine, but we should try to reuse and comply to them unless there's a good reason not to.
  5. I think we would prefer to not introduce another code style. I hate been this person as I've personally been pushing to revisiting current dependency injection style used to plain functions wrapped in class based sugar approach outlined here Incidental Complexity: Dependency injection is getting in the way #3391. For the reference remote pinning service API on IPFS client follows it https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs-http-client/src/pin/remote/index.js

I realize there's a lot here, but I'm available to help with moving certain pieces.

@@ -89,6 +89,7 @@
"it-first": "^1.0.4",
"it-last": "^1.0.4",
"it-pipe": "^1.1.0",
"js-ipfs-pinning-service-client": "github:yusefnapora/js-ipfs-pinning-service-client#874c6ec7875ec2b3039c2361638c54158e7e4874",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would want to migrate it into this monorepo possibly as a separate package, but only if having just pinning service client on it's own provides some value. If it needs to be used in conjunction with js-ipfs anyway it's probably best to just fold it under ipfs-core to reduce maintenance overhead.

I've also skimmed over https://github.com/yusefnapora/js-ipfs-pinning-service-client/blob/master/generated/src/ApiClient.js, it generates code that pulls in separate http library that we would most likely want to avoid as it would further increase bundle size and would be more maintenance burden.

packages/ipfs-core/src/components/pin/remote.js Outdated Show resolved Hide resolved
packages/ipfs-core/src/components/pin/remote.js Outdated Show resolved Hide resolved
packages/ipfs-core/src/components/pin/remote.js Outdated Show resolved Hide resolved
packages/ipfs-core/src/components/pin/remote.js Outdated Show resolved Hide resolved
packages/ipfs-core/src/components/pin/remote.js Outdated Show resolved Hide resolved
@yusefnapora
Copy link
Contributor Author

yusefnapora commented Mar 26, 2021

@Gozala thanks for all the review feedback 👍

I'm fine with refactoring to fit the current style; I was kind of confused by it when I first started hacking, but I think I'm getting it now 😄

I also kind of hate the generated client for the same reasons you're raising... plus I just don't really like the code it generates and would be happy to ditch it. The HTTP API itself is pretty simple, so it's not hard to write a client using any HTTP request library. I'll plan on just doing that, and I can just leave the js client on my github org for now. We can always move it to shipyard or something if people start using it in other projects.

I'll plan to hack on that and implement the config persistence, and I'll ping you next week sometime for another review pass.

Copy link
Contributor

@Gozala Gozala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yusefnapora for all the work that went into it. I think it's mostly there, I made bunch of minor comments to improve things here and there. There is a thing however that I think need to be addressed

  1. Unless I've missed something, it appears to me that the way things are setup ipfs.pin.remote.* calls will fail in a next sessions, because config isn't going to be loaded.

And a before thing that I think would be really nice to have, but doesn't need to block this:

pin/remote/client.js seems unnecessary and only complicates things at this point. I think you end up with it because client used to be a separate lib, now that it's not the case it would make more sense to just have that logic in add/rm/ls/* and just pull endpoint/key instead.

@@ -86,7 +89,8 @@ class IPFS {
})
const resolve = createResolveAPI({ ipld, name })
const pinManager = new PinManagerAPI({ repo, dagReader })
const pin = new PinAPI({ gcLock, pinManager, dagReader })
const pinRemote = new PinRemoteAPI({ swarm, config, peerId })
const pin = new PinAPI({ gcLock, pinManager, dagReader, pinRemote })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since pinRemote is not used here and is only created to be passed to PinAPI I would suggest instead passing config, swarm and peerId and construct PinRemoteAPI in PinAPI instead.

* @typedef {Object} RemotePinningServiceConfig
* @property {Object} API
* @property {string} API.Endpoint
* @property {string} API.Key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go-ipfs settled on omitting endpoint keys when printing config so that they aren't easy to leak (See https://github.com/ipfs/go-ipfs/pull/7661/files#r537641281)

We should at least do the same, although @lidel suggested something smarter is needed in the long term, not sure what exactly he meant but it might be worth doing that here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The smarter thing to do is to move all secrets to separate files outside the JSON config.
The only secret right now is Identity.PrivKey this PR adds second type of secret under Pinning.RemoteServices[*].API.Key.
I may be less work to move secrets outside, than to sanitize the output of ipfs config and make sure ipfs config replace does not forget the secrets.

* @param {Credentials & AbortOptions} credentials
*/
async function add (name, credentials) {
await loadConfig()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT (Feel free to disregard): I would very much prefer if the state was encapsulated by something like a PinningConfig such that this would be more in the lines of:

const services = await pinningConfig.use()
const service = services[name]

throw new Error('parameter "name" is required')
}
await loadConfig()
delete clients[name]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably not big deal here, but concurrency implications of this make me unease. E.g. concurrent add & rm calls may end up in a race condition where one may undo result of the other. However probability seems low enough that adding task queuing is probably not worth it 😳

* List the configured remote pinning services.
*
* @param {{stat: ?boolean} & AbortOptions} opts
* @returns {Promise<Array<RemotePinService> | Array<RemotePinServiceWithStat>>} - a Promise resolving to an array of objects describing the configured remote pinning services. If stat==true, each object will include more detailed status info, including the number of pins for each pin status.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should be able to type it as:

Suggested change
* @returns {Promise<Array<RemotePinService> | Array<RemotePinServiceWithStat>>} - a Promise resolving to an array of objects describing the configured remote pinning services. If stat==true, each object will include more detailed status info, including the number of pins for each pin status.
* @returns {Promise<Array<Options['stat'] extends true ? RemotePinServiceWithStat : RemotePinService>>} - a Promise resolving to an array of objects describing the configured remote pinning services. If stat==true, each object will include more detailed status info, including the number of pins for each pin status.

const total = body.count
let yielded = 0
while (true) {
if (body.results.length < 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems redundunt if it's empty next loop will be noon and next if clause will return and if doesn't it's probably better to fail.

throw new Error('pin failed: ' + JSON.stringify(pinResponse.info))
}

await delay(pollIntervalMs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should make pollInterval part of the optional add options so it could be configured if necessary to longer / shorter interval.

*/
async function rmAll (options) {
const { service } = options
const svc = serviceRegistry.serviceNamed(service)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm missing something this will return null unload config was loaded which may not be the case here.

*/
async function * ls (options) {
const { service } = options
const svc = serviceRegistry.serviceNamed(service)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm missing something this will return null unload config was loaded which may not be the case here.

*/
async function rm (options) {
const { service } = options
const svc = serviceRegistry.serviceNamed(service)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm missing something this will return null unload config was loaded which may not be the case here.

}

async function awaitPinCompletion (pinResponse) {
const pollIntervalMs = 100
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/ipfs/go-ipfs/blob/v0.8.0/core/commands/pin/remotepin.go#L224:

Suggested change
const pollIntervalMs = 100
const pollIntervalMs = 500

@BigLep
Copy link
Contributor

BigLep commented May 3, 2021

@yusefnapora : what's the plan for this PR? Will you be working land it? My goal is to understand the next steps. Thanks!

@lidel
Copy link
Member

lidel commented May 10, 2021

Closing until protocol/web3-dev-team#58 is picked up by one of project teams

@lidel lidel closed this May 10, 2021
@brickpop
Copy link

Any chance that the PR is ever considered? Pinning data on a remote node is very important for us
Thank you

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
No open projects
Archived in project
Development

Successfully merging this pull request may close these issues.

5 participants