UNPKG

25.5 kBJavaScriptView Raw
1/*
2This is a shim to the IPFS library, (Lists are handled in YJS or OrbitDB)
3See https://github.com/ipfs/js-ipfs but note its often out of date relative to the generic API doc.
4*/
5
6//TODO-IPFS Note API changes in https://github.com/ipfs/js-ipfs/issues/1721 probably all ipfs.files -> ipfs.
7
8const httptools = require('./httptools'); // Expose some of the httptools so that IPFS can use it as a backup
9const debug = require('debug')('dweb-transports:ipfs');
10
11// IPFS components
12let IPFS; //TODO-SPLIT move this line lower when fix structure
13//TODO-SPLIT remove this import depend on archive.html or node to pre-load
14//IPFS = require('ipfs');
15//TODO-SPLIT remove this import depend on archive.html or node to pre-load
16//const ipfsAPI = require('ipfs-http-client');
17//We now get this from IPFS.CID
18//const CID = require('cids');
19
20// Library packages other than IPFS
21const Url = require('url');
22const stream = require('readable-stream'); // Needed for the pullthrough - this is NOT Ipfs streams
23
24// Other Dweb modules
25const errors = require('./Errors'); // Standard Dweb Errors
26const Transport = require('./Transport.js'); // Base class for TransportXyz
27const Transports = require('./Transports'); // Manage all Transports that are loaded
28const utils = require('./utils'); // Utility functions
29
30const defaultoptions = {
31 repo: '/tmp/dweb_ipfsv3107', //TODO-IPFS restarted 2018-10-06 because was caching connection ws-star
32 //init: false,
33 //start: false,
34 //TODO-IPFS-Q how is this decentralized - can it run offline? Does it depend on star-signal.cloud.ipfs.team
35 config: {
36 // Addresses: { Swarm: [ '/dns4/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star']}, // For Y - same as defaults
37 // Addresses: { Swarm: [ ] }, // Disable WebRTC to test browser crash, note disables Y so doesnt work.
38 //Addresses: {Swarm: ['/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star']}, // from https://github.com/ipfs/js-ipfs#faq 2017-12-05 as alternative to webrtc works sort-of
39 //Bootstrap: ['/dns4/dweb.me/tcp/4245/wss/ipfs/QmPNgKEjC7wkpu3aHUzKKhZmbEfiGzL5TP1L8zZoHJyXZW'], // Connect via WSS to IPFS instance at IA on FnF
40 Bootstrap: ['/dns4/dweb.me/tcp/4245/wss/ipfs/QmQz3p44VVQDeAieaW28DMjcTVzLbpxqaQB9bkXnyd7HY5'], // Connect via WSS to IPFS instance in Kube at IA
41 // Previously was: QmQ921MRjsbP12fHSEDcdFeuHFg6qKDFurm2rXgA5K3RQD on kube
42 },
43 //init: true, // Comment out for Y
44 EXPERIMENTAL: { pubsub: true },
45 preload: { enabled: false },
46 //Off by default, it never seems to have the content (routing issues) pass as an argument if want to use
47 //httpIPFSgateway: "https://ipfs.io",
48};
49
50class TransportIPFS extends Transport {
51 /*
52 IPFS specific transport
53
54 Fields:
55 ipfs: object returned when starting IPFS
56 */
57
58 constructor(options) {
59 super(options);
60 [ "urlUrlstore", "httpIPFSgateway"].forEach(k => {
61 this[k] = options[k];
62 delete options[k];
63 });
64 this.ipfs = undefined; // Undefined till start IPFS
65 this.options = options; // Dictionary of options
66 this.name = "IPFS"; // For console log etc
67 this.supportURLs = ['ipfs'];
68 this.supportFunctions = ['fetch', 'store', 'seed', 'createReadStream']; // Does not support reverse
69 this.supportFeatures = ['noCache']; // Note doesnt actually support noCache, but immutable is same
70 this.status = Transport.STATUS_LOADED;
71 }
72
73 _ipfsversion(ipfs, s, cb) {
74 ipfs.version((err, data) => {
75 if (err) {
76 debug("IPFS via %s present but unresponsive: %o", s, data);
77 this.ipfstype = "FAILED";
78 cb(err);
79 } else {
80 debug("IPFS available via %s: %o", s, data);
81 this.ipfstype = s;
82 cb(null, ipfs);
83 }
84 });
85 }
86 IPFSAutoConnect(cb) {
87 IPFS = global.Ipfs || window.Ipfs; //Loaded by <script etc but still need a create
88 const ipfsAPI = global.IpfsHttpClient || window.IpfsHttpClient;
89 //TODO-SPLIT I think next few lines are wrong, dont think I've seen global.ipfs or window.ipfs but
90 //TODO-SPLIT https://github.com/ipfs/js-ipfs implies global.Ipfs but needs a "create" or "new"
91 if (global.ipfs) {
92 this._ipfsversion(global.ipfs, "global.ipfs", cb );
93 } else if (typeof window !== "undefined" && window.ipfs) {
94 this._ipfsversion(window.ipfs, "window.ipfs", cb);
95 } else {
96 // noinspection ES6ConvertVarToLetConst
97 var ipfs = ipfsAPI('localhost', '5001', {protocol: 'http'}); // leaving out the arguments will default to these values
98 ipfs.version((err, data) => {
99 if (err) {
100 debug("IPFS via API failed %s, trying running own IPFS client", err.message);
101 ipfs = new IPFS(this.options);
102 ipfs.on('ready', () => {
103 this._ipfsversion(ipfs, "client", cb);
104 }); // This only works in the client version, not on API
105 ipfs.on('error', (err) => {
106 debug("IPFS via client error %s", err.message); // Calls error, note this could be a problem if it gets errors after "ready"
107 cb(err);
108 }) // This only works in the client version, not on API
109 } else {
110
111 this._ipfsversion(ipfs, "API", cb); // Note wastes an extra ipfs.version call but that's cheap
112 }
113 });
114 }
115 }
116
117 /*OBS
118 p_ipfsstart() {
119 /-*
120 Just start IPFS - not Y (note used with "yarrays" and will be used for non-IPFS list management)
121 Note - can't figure out how to use async with this, as we resolve the promise based on the event callback
122 *-/
123 const self = this;
124 return new Promise((resolve, reject) => {
125 this.ipfs = new IPFS(this.options);
126 this.ipfs.on('ready', () => {
127 //this._makepromises();
128 resolve();
129 });
130 this.ipfs.on('error', (err) => reject(err));
131 })
132 .then(() => self.ipfs.version())
133 .then((version) => debug('ready %o',version))
134 .catch((err) => {
135 console.warn("IPFS p_ipfsstart failed", err.message);
136 throw(err);
137 });
138 }
139 */
140
141 static setup0(options) {
142 /*
143 First part of setup, create obj, add to Transports but dont attempt to connect, typically called instead of p_setup if want to parallelize connections.
144 */
145 const combinedoptions = Transport.mergeoptions(defaultoptions, options.ipfs);
146 debug("setup options=%o", combinedoptions);
147 const t = new TransportIPFS(combinedoptions); // Note doesnt start IPFS
148 Transports.addtransport(t);
149 return t;
150 }
151
152 p_setup1(cbstatus, cb) {
153 /* Start IPFS connection
154 cbstatus function(this), for updating status, it must be ale to be called multiple times.
155 returns this via cb(err,res) or promise
156 errors This should never "fail" as it will break the Promise.all, it should return "this" but set this.status = Transport.STATUS_FAILED
157 */
158
159 if (cb) { try { f.call(this, cb) } catch(err) { cb(err)}} else { return new Promise((resolve, reject) => { try { f.call(this, (err, res) => { if (err) {reject(err)} else {resolve(res)} })} catch(err) {reject(err)}})} // Promisify pattern v2
160 function f(cb) {
161 // Logged by Transports
162 this.status = Transport.STATUS_STARTING; // Should display, but probably not refreshed in most case
163 if (cbstatus) cbstatus(this);
164 this.IPFSAutoConnect((err, ipfs) => { // Various errors possible inc websocket
165 if (err) {
166 debug("Failed to connect %s", err.message);
167 this.status = Transport.STATUS_FAILED;
168 } else {
169 this.ipfs = ipfs;
170 this.status = Transport.STATUS_CONNECTED;
171 }
172 if (cbstatus) cbstatus(this);
173 cb(null, this); // Don't fail, report the error and set statust to Transport.STATUS_FAILED
174 })
175 }
176 }
177
178 p_setup2(refreshstatus) {
179 if (this.status === Transport.STATUS_FAILED) {
180 debug("Stage 1 failed, skipping");
181 }
182 return this;
183 }
184
185 stop(refreshstatus, cb) { //TODO-API p_stop > stop
186 if (this.ipfstype === "client") {
187 this.ipfs.stop((err, res) => {
188 this.status = Transport.STATUS_FAILED;
189 if (refreshstatus) refreshstatus(this);
190 cb(err, res);
191 });
192 } else {
193 // We didn't start it, don't try and stop it
194 this.status = Transport.STATUS_FAILED;
195 if (refreshstatus) refreshstatus(this);
196 cb(miull, this);
197 }
198 }
199 async p_status() {
200 /*
201 Return a numeric code for the status of a transport.
202 TODO - this no longer works if using the http api
203 */
204 this.status = (await this.ipfs.isOnline()) ? Transport.STATUS_CONNECTED : Transport.STATUS_FAILED;
205 return super.p_status();
206 }
207
208 // Everything else - unless documented here - should be opaque to the actual structure of a CID
209 // or a url. This code may change as its not clear (from IPFS docs) if this is the right mapping.
210 static urlFrom(unknown) {
211 /*
212 Convert a CID into a standardised URL e.g. ipfs:/ipfs/abc123
213 */
214 if (unknown instanceof IPFS.CID) //TODO-SPLIT - I think there is a way to get this from a types array
215 return "ipfs:/ipfs/"+unknown.toBaseEncodedString();
216 if (typeof unknown === "object" && unknown.hash) // e.g. from files.add
217 return "ipfs:/ipfs/"+unknown.hash;
218 if (typeof unknown === "string") // Not used currently
219 return "ipfs:/ipfs/"+unknown;
220 throw new errors.CodingError("TransportIPFS.urlFrom: Cant convert to url from",unknown);
221 }
222
223 static cidFrom(url) {
224 /*
225 Convert a URL e.g. ipfs:/ipfs/abc123 into a CID structure suitable for retrieval
226 url: String of form "ipfs://ipfs/<hash>" or parsed URL or CID
227 returns: CID
228 throws: TransportError if cant convert
229 */
230 if (url instanceof IPFS.CID) return url;
231 if (typeof(url) === "string") url = Url.parse(url);
232 if (url && url["pathname"]) { // On browser "instanceof Url" isn't valid)
233 const patharr = url.pathname.split('/');
234 if ((!["ipfs:","dweb:"].includes(url.protocol)) || (patharr[1] !== 'ipfs') || (patharr.length < 3))
235 throw new errors.TransportError("TransportIPFS.cidFrom bad format for url should be dweb: or ipfs:/ipfs/...: " + url.href);
236 if (patharr.length > 3)
237 throw new errors.TransportError("TransportIPFS.cidFrom not supporting paths in url yet, should be dweb: or ipfs:/ipfs/...: " + url.href);
238 return new IPFS.CID(patharr[2]);
239 } else {
240 throw new errors.CodingError("TransportIPFS.cidFrom: Cant convert url", url);
241 }
242 }
243
244 static _stringFrom(url) {
245 // Tool for ipfsFrom and ipfsGatewayFrom
246 if (url instanceof IPFS.CID)
247 return "/ipfs/"+url.toBaseEncodedString();
248 if (typeof url === 'object' && url.path) { // It better be URL which unfortunately is hard to test
249 return url.path;
250 }
251 }
252 static ipfsFrom(url) {
253 /*
254 Convert to a ipfspath i.e. /ipfs/Qm....
255 Required because of strange differences in APIs between files.cat and dag.get see https://github.com/ipfs/js-ipfs/issues/1229
256 */
257 url = this._stringFrom(url); // Convert CID or Url to a string hopefully containing /ipfs/
258 if (url.indexOf('/ipfs/') > -1) {
259 return url.slice(url.indexOf('/ipfs/'));
260 }
261 throw new errors.CodingError(`TransportIPFS.ipfsFrom: Cant convert url ${url} into a path starting /ipfs/`);
262 }
263
264 ipfsGatewayFrom(url) {
265 /*
266 url: CID, Url, or a string
267 returns: https://ipfs.io/ipfs/<cid>
268 */
269 url = this._stringFrom(url); // Convert CID or Url to a string hopefully containing /ipfs/
270 if (url.indexOf('/ipfs/') > -1) {
271 return this.httpIPFSgateway + url.slice(url.indexOf('/ipfs/'));
272 }
273 throw new errors.CodingError(`TransportIPFS.ipfsGatewayFrom: Cant convert url ${url} into a path starting /ipfs/`);
274 }
275
276 static multihashFrom(url) {
277 if (url instanceof IPFS.CID)
278 return url.toBaseEncodedString();
279 if (typeof url === 'object' && url.path)
280 url = url.path; // /ipfs/Q...
281 if (typeof(url) === "string") {
282 const idx = url.indexOf("/ipfs/");
283 if (idx > -1) {
284 return url.slice(idx+6);
285 }
286 }
287 throw new errors.CodingError(`Cant turn ${url} into a multihash`);
288 }
289
290 // noinspection JSCheckFunctionSignatures
291 async p_rawfetch(url, {timeoutMS=60000, relay=false}={}) {
292 /*
293 Fetch some bytes based on a url of the form ipfs:/ipfs/Qm..... or ipfs:/ipfs/z.... .
294 No assumption is made about the data in terms of size or structure, nor can we know whether it was created with dag.put or ipfs add or http /api/v0/add/
295
296 Where required by the underlying transport it should retrieve a number if its "blocks" and concatenate them.
297 Returns a new Promise that resolves currently to a string.
298 There may also be need for a streaming version of this call, at this point undefined since we havent (currently) got a use case..
299
300 :param string url: URL of object being retrieved {ipfs|dweb}:/ipfs/<cid> or /
301 :resolve buffer: Return the object being fetched. (may in the future return a stream and buffer externally)
302 :throws: TransportError if url invalid - note this happens immediately, not as a catch in the promise
303 */
304 // Attempt logged by Transports
305 if (!url) throw new errors.CodingError("TransportIPFS.p_rawfetch: requires url");
306 const cid = TransportIPFS.cidFrom(url); // Throws TransportError if url bad
307 const ipfspath = TransportIPFS.ipfsFrom(url); // Need because dag.get has different requirement than file.cat
308
309 try {
310 const res = await utils.p_timeout(this.ipfs.dag.get(cid), timeoutMS, "Timed out IPFS fetch of "+TransportIPFS._stringFrom(cid)); // Will reject and throw TimeoutError if times out
311 // noinspection Annotator
312 if (res.remainderPath.length)
313 { // noinspection ExceptionCaughtLocallyJS
314 throw new errors.TransportError("Not yet supporting paths in p_rawfetch");
315 }
316 let buff;
317 if (res.value.constructor.name === "DAGNode") { // Kludge to replace above, as its not matching the type against the "require" above.
318 // We retrieved a DAGNode, call files.cat (the node will come from the cache quickly)
319 buff = await this.ipfs.cat(ipfspath); //See js-ipfs v0.27 version and https://github.com/ipfs/js-ipfs/issues/1229 and https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#cat
320 } else { //c: not a file
321 debug("Found a raw IPFS block (unusual) - not a DAGNode - handling as such");
322 buff = res.value;
323 }
324 // Success logged by Transports
325 return buff;
326 } catch (err) { // TimeoutError or could be some other error from IPFS etc
327 debug("Caught error '%s' fetching via IPFS", err.message);
328 if (!this.httpIPFSgateway) {
329 throw(err);
330 } else {
331 try {
332 debug("Trying IPFS HTTP gateway");
333 let ipfsurl = this.ipfsGatewayFrom(url);
334 return await utils.p_timeout(
335 httptools.p_GET(ipfsurl), // Returns a buffer
336 timeoutMS, "Timed out IPFS fetch of "+ipfsurl)
337 } catch (err) {
338 // Failure logged by Transports:
339 //debug("Failed to retrieve from gateway: %s", err.message);
340 throw err;
341 }
342 }
343 }
344 }
345
346 async p_rawstore(data) {
347 /*
348 Store a blob of data onto the decentralised transport.
349 Returns a promise that resolves to the url of the data
350
351 :param string|Buffer data: Data to store - no assumptions made to size or content
352 :resolve string: url of data stored
353 */
354 console.assert(data, "TransportIPFS.p_rawstore: requires data");
355 const buf = (data instanceof Buffer) ? data : new Buffer(data);
356 const res = (await this.ipfs.add(buf,{ "cid-version": 1, hashAlg: 'sha2-256'}))[0];
357 return TransportIPFS.urlFrom(res);
358 }
359
360 seed({directoryPath=undefined, fileRelativePath=undefined, ipfsHash=undefined, urlToFile=undefined}, cb) {
361 /* Note always passed a cb by Transports.seed - no need to support Promise here
362 ipfsHash: IPFS hash if known (usually not known)
363 urlToFile: Where the IPFS server can get the file - must be live before this called as will fetch and hash
364 TODO support directoryPath/fileRelativePath, but to working around IPFS limitation in https://github.com/ipfs/go-ipfs/issues/4224 will need to check relative to IPFS home, and if not symlink it and add symlink
365 TODO maybe support adding raw data (using add)
366
367 Note neither js-ipfs-http-client nor js-ipfs appear to support urlstore yet, see https://github.com/ipfs/js-ipfs-http-client/issues/969
368 */
369 // This is the URL that the IPFS server uses to get the file from the local mirrorHttp
370 if (!(this.urlUrlstore && urlToFile)) { // Not doing IPFS
371 debug("IPFS.seed support requires urlUrlstore and urlToFile"); // Report, though Transports.seed currently ignores this
372 cb(new Error("IPFS.seed support requires urlUrlstore and urlToFile")); // Report, though Transports.seed currently ignores this
373 } else {
374 // Building by hand becase of lack of support in js-ipfs-http-client
375 const url = `${this.urlUrlstore}?arg=${encodeURIComponent(urlToFile)}`;
376 // Have to be careful to avoid loops, the call to addIPFS should only be after file is retrieved and cached, and then addIPFS shouldnt be called if already cached
377 httptools.p_GET(url, {retries:0}, (err, res) => {
378 if (err) {
379 debug("IPFS.seed for %s failed in http: %s", urlToFile, err.message);
380 cb(err); // Note error currently ignored in Transports
381 } else {
382 debug("Added %s to IPFS key=", urlToFile, res.Key);
383 // Check for mismatch - this isn't an error, for example it could be an updated file, old IPFS hash will now fail, but is out of date and shouldnt be shared
384 if (ipfsHash && ipfsHash !== res.Key) { debug("ipfs hash doesnt match expected metadata has %s daemon returned %s", ipfsHash, res.Key); }
385 cb(null, res)
386 }
387 })
388 }
389 }
390 async p_f_createReadStream(url, {wanturl=false}={}) {
391 /*
392 Fetch bytes progressively, using a node.js readable stream, based on a url of the form:
393 No assumption is made about the data in terms of size or structure.
394
395 This is the initialisation step, which returns a function suitable for <VIDEO>
396
397 Returns a new Promise that resolves to function for a node.js readable stream.
398
399 Node.js readable stream docs: https://nodejs.org/api/stream.html#stream_readable_streams
400
401 :param string url: URL of object being retrieved of form:
402 magnet:xyzabc/path/to/file (Where xyzabc is the typical magnet uri contents)
403 ipfs:/ipfs/Q123
404 :param boolean wanturl True if want the URL of the stream (for service workers)
405 :resolves to: f({start, end}) => stream (The readable stream.)
406 :throws: TransportError if url invalid - note this happens immediately, not as a catch in the promise
407 */
408 // Logged by Transports;
409 //debug("p_f_createreadstream %o", url);
410 let stream;
411 try {
412 let multihash = url.pathname.split('/ipfs/')[1];
413 if (multihash.includes('/'))
414 { // noinspection ExceptionCaughtLocallyJS
415 throw new CodingError("Should not be seeing URLS with a path here:"+url);
416 }
417 let self = this;
418 if (wanturl) { // In ServiceWorker
419 return url;
420 } else {
421 return function createReadStream(opts) {
422 /*
423 The function, encapsulated and inside another function by p_f_createReadStream (see docs)
424 :param opts: { start: byte to start from; end: optional end byte }
425 :returns stream: The readable stream.
426 FOR IPFS this is copied and adapted from git repo js-ipfs/examples/browser-readablestream/index.js
427 */
428 debug("reading from stream %o %o", multihash, opts || "" );
429
430 const start = opts ? opts.start : 0;
431 // The videostream library does not always pass an end byte but when
432 // it does, it wants bytes between start & end inclusive.
433 // catReadableStream returns the bytes exclusive so increment the end
434 // byte if it's been requested
435 const end = (opts && opts.end) ? start + opts.end + 1 : undefined;
436 // If we've streamed before, clean up the existing stream
437 if (stream && stream.destroy) {
438 stream.destroy()
439 }
440
441 // This stream will contain the requested bytes
442
443 // For debugging used a known good IPFS video
444 //let fakehash="QmedXJYwvNSJFRMVFuJt7BfCMcJwPoqJgqN3U2MYxHET5a"
445 //console.log("XXX@IPFS.p_f_createReadStream faking call to",multihash, "with", fakehash)
446 //multihash=fakehash;
447 stream = self.ipfs.catReadableStream(multihash, {
448 offset: start,
449 length: end && end - start
450 });
451 // Log error messages
452
453 stream.on('error', (err) => console.error(err));
454
455 /* Gimmick from example :-)
456 if (start === 0) {
457 // Show the user some messages while we wait for the data stream to start
458 statusMessages(stream, log)
459 }
460 */
461 return stream
462 };
463 }
464 } catch(err) {
465 if (stream && stream.destroy) {
466 stream.destroy()
467 }
468 // Error logged by Transports
469 //console.log(`p_f_createReadStream failed on ${url} ${err.message}`);
470 throw(err);
471 }
472 }
473
474 static async p_test(opts) {
475 {console.log("TransportIPFS.test")}
476 try {
477 const transport = await this.p_setup(opts); // Assumes IPFS already setup
478 console.log(transport.name,"setup");
479 const res = await transport.p_status();
480 console.assert(res === Transport.STATUS_CONNECTED);
481
482 let urlqbf;
483 const qbf = "The quick brown fox";
484 const qbf_url = "ipfs:/ipfs/zdpuAscRnisRkYnEyJAp1LydQ3po25rCEDPPEDMymYRfN1yPK"; // Expected url
485 const testurl = "1114"; // Just a predictable number can work with
486 const url = await transport.p_rawstore(qbf);
487 console.log("rawstore returned", url);
488 const newcid = TransportIPFS.cidFrom(url); // Its a CID which has a buffer in it
489 console.assert(url === qbf_url, "url should match url from rawstore");
490 const cidmultihash = url.split('/')[2]; // Store cid from first block in form of multihash
491 const newurl = TransportIPFS.urlFrom(newcid);
492 console.assert(url === newurl, "Should round trip");
493 urlqbf = url;
494 const data = await transport.p_rawfetch(urlqbf);
495 console.assert(data.toString() === qbf, "Should fetch block stored above");
496 //console.log("TransportIPFS test complete");
497 return transport
498 } catch(err) {
499 console.log("Exception thrown in TransportIPFS.test:", err.message);
500 throw err;
501 }
502 }
503
504}
505TransportIPFS.scripts=['ipfs/dist/index.min.js', // window.Ipfs 2.3Mb
506 'ipfs-http-client/dist/index.min.js']; //window.IpfsHttpClient
507TransportIPFS.requires=['ipfs', 'ipfs-http-client'];
508
509
510Transports._transportclasses["IPFS"] = TransportIPFS;
511// noinspection JSUndefinedPropertyAssignment
512exports = module.exports = TransportIPFS;