UNPKG

13.1 kBJavaScriptView Raw
1const Transport = require('./Transport'); // Base class for TransportXyz
2const Transports = require('./Transports'); // Manage all Transports that are loaded
3const httptools = require('./httptools'); // Expose some of the httptools so that IPFS can use it as a backup
4const Url = require('url');
5const debug = require('debug')('dweb-transports:hash');
6const canonicaljson = require('@stratumn/canonicaljson');
7
8defaultHashOptions = {
9 urlbase: 'https://dweb.me',
10 //heartbeat: { delay: 30000 } // Uncomment to check once a minute, but not needed since piggybacking on HTTP
11};
12
13servercommands = { // What the server wants to see to return each of these
14 rawfetch: "contenthash", // was content/rawfetch which should still work.
15 rawstore: "contenturl/rawstore",
16 rawadd: "void/rawadd",
17 rawlist: "metadata/rawlist",
18 get: "get/table",
19 set: "set/table",
20 delete: "delete/table",
21 keys: "keys/table",
22 getall: "getall/table"
23};
24
25
26class TransportHASH extends Transport {
27 /* Subclass of Transport for handling Hashes - see API.md for docs TODO-HASH write up
28
29 options {
30 urlbase: e.g. https://dweb.me Where to go for URLS like /contenthash
31 heartbeat: {
32 delay // Time in milliseconds between checks - 30000 might be appropriate - if missing it wont do a heartbeat
33 statusCB // Callback cb(transport) when status changes
34 }
35 }
36 */
37
38 constructor(options) {
39 super(options); // These are options.hash
40 this.options = options;
41 this.urlbase = options.urlbase; // e.g. https://dweb.me
42 this.supportURLs = ['contenthash'];
43 this.supportFunctions = ['fetch', 'store', 'add', 'list', 'reverse', 'newlisturls', "get", "set", "keys", "getall", "delete", "newtable", "newdatabase"]; //Does not support: listmonitor - reverse is disabled somewhere not sure if here or caller
44 this.supportFeatures = ['noCache'];
45 if (typeof window === "undefined") {
46 // running in node, can support createReadStream, (browser can't - see createReadStream below)
47 this.supportFunctions.push("createReadStream");
48 }
49 // noinspection JSUnusedGlobalSymbols
50 this.supportFeatures = ['fetch.range', 'noCache'];
51 this.name = "HASH"; // For console log etc
52 this.status = Transport.STATUS_LOADED;
53 }
54
55 static setup0(options) {
56 let combinedoptions = Transport.mergeoptions(defaultHashOptions, options.hash);
57 try {
58 let t = new TransportHASH(combinedoptions);
59 Transports.addtransport(t);
60 return t;
61 } catch (err) {
62 debug("ERROR: HASH unable to setup0", err.message);
63 throw err;
64 }
65 }
66
67 p_setup2(statusCB) { // Has to run after TransportHTTP
68 this.http = Transports.http(); // Find an HTTP transport to use
69 return new Promise((resolve, unusedReject) => {
70 this.status = Transport.STATUS_STARTING;
71 if (statusCB) statusCB(this);
72 this.updateStatus((unusedErr, unusedRes) => {
73 if (statusCB) statusCB(this);
74 this.startHeartbeat(this.options.heartbeat);
75 resolve(this); // Note always resolve even if error from p_status as have set status to failed
76 });
77 })
78 }
79
80 async p_status(cb) { //TODO-API
81 /*
82 Return (via cb or promise) a numeric code for the status of a transport.
83 */
84 if (cb) { try { this.updateStatus(cb) } catch(err) { cb(err)}} else { return new Promise((resolve, reject) => { try { this.updateStatus((err, res) => { if (err) {reject(err)} else {resolve(res)} })} catch(err) {reject(err)}})} // Promisify pattern v2f
85 }
86 updateStatus(cb) { //TODO-API
87 this.updateInfo((err, res) => {
88 if (err) {
89 debug("Error status call to info failed %s", err.message);
90 this.status = Transport.STATUS_FAILED;
91 cb(null, this.status); // DOnt pass error up, the status indicates the error
92 } else {
93 this.info = res; // Save result
94 this.status = Transport.STATUS_CONNECTED;
95 cb(null, this.status);
96 }
97 });
98 }
99
100 startHeartbeat({delay=undefined, statusCB=undefined}) {
101 if (delay) {
102 debug("%s Starting Heartbeat", this.name)
103 this.heartbeatTimer = setInterval(() => {
104 this.updateStatus((err, res)=>{ // Pings server and sets status
105 if (statusCB) statusCB(this); // repeatedly call callback if supplies
106 }, (unusedErr, unusedRes)=>{}); // Dont wait for status to complete
107 }, delay);
108 }
109 }
110 stopHeartbeat() {
111 if (this.heartbeatTimer) {
112 debug("stopping heartbeat");
113 clearInterval(this.heartbeatTimer);}
114 }
115 stop(refreshstatus, cb) {
116 this.stopHeartbeat();
117 this.status = Transport.STATUS_FAILED;
118 if (refreshstatus) { refreshstatus(this); }
119 cb(null, this);
120 }
121
122 _cmdurl(command) {
123 return `${this.urlbase}/${command}`
124 }
125 _url(url, command, parmstr) {
126 if (!url) throw new errors.CodingError(`${command}: requires url`);
127 if (typeof url !== "string") { url = url.href }
128 url = url.replace('contenthash:/contenthash', this._cmdurl(command)) ;
129 url = url.replace('getall/table', command);
130 url = url + (parmstr ? "?"+parmstr : "");
131 return url;
132 }
133
134 // noinspection JSCheckFunctionSignatures
135 async p_rawfetch(url, opts={}) {
136 /*
137 Fetch from underlying transport,
138 Fetch is used both for contenthash requests and table as when passed to SmartDict.p_fetch may not know what we have
139 url: Of resource - which is turned into the HTTP url in p_httpfetch
140 opts: {start, end, retries, noCache} see p_GET for documentation
141 throws: TransportError if fails
142 */
143 if (((typeof url === "string") ? url : url.href).includes('/getall/table')) {
144 throw new Error("Probably dont want to be calling p_rawfetch on a KeyValueTable, especially since dont know if its keyvaluetable or subclass"); //TODO-NAMING
145 } else {
146 return await this.http.p_rawfetch(this._url(url, servercommands.rawfetch), opts);
147 }
148 }
149
150 p_rawlist(url) {
151 // obj being loaded
152 // Locate and return a block, based on its url
153 if (!url) throw new errors.CodingError("TransportHASH.p_rawlist: requires url");
154 return this.http.p_rawfetch(this._url(url, servercommands.rawlist));
155 }
156 rawreverse() { throw new errors.ToBeImplementedError("Undefined function TransportHASH.rawreverse"); }
157
158 async p_rawstore(data) {
159 /*
160 Store data on http server,
161 data: string
162 resolves to: {string}: url
163 throws: TransportError on failure in p_POST > p_httpfetch
164 */
165 //PY: res = self._sendGetPost(True, "rawstore", headers={"Content-Type": "application/octet-stream"}, urlargs=[], data=data)
166 console.assert(data, "TransportHASH.p_rawstore: requires data");
167 const res = await httptools.p_POST(this._cmdurl(servercommands.rawstore), {data, contenttype: "application/octet-stream"}); // resolves to URL
168 let parsedurl = Url.parse(res);
169 let pathparts = parsedurl.pathname.split('/');
170 return `contenthash:/contenthash/${pathparts.slice(-1)}`
171 }
172
173 p_rawadd(url, sig) {
174 // Logged by Transports
175 if (!url || !sig) throw new errors.CodingError("TransportHASH.p_rawadd: invalid parms", url, sig);
176 const data = canonicaljson.stringify(sig.preflight(Object.assign({},sig)))+"\n";
177 return httptools.p_POST(this._url(url, servercommands.rawadd), {data, contenttype: "application/json"}); // Returns immediately
178 }
179
180 p_newlisturls(cl) {
181 let u = cl._publicurls.map(urlstr => Url.parse(urlstr))
182 .find(parsedurl =>
183 ((parsedurl.protocol === "https:" && ["gateway.dweb.me", "dweb.me"].includes(parsedurl.host)
184 && (parsedurl.pathname.includes('/content/rawfetch') || parsedurl.pathname.includes('/contenthash/')))
185 || (parsedurl.protocol === "contenthash:") && (parsedurl.pathname.split('/')[1] === "contenthash")));
186 if (!u) {
187 // noinspection JSUnresolvedVariable
188 u = `contenthash:/contenthash/${ cl.keypair.verifyexportmultihashsha256_58() }`; // Pretty random, but means same test will generate same list and server is expecting base58 of a hash
189 }
190 return [u,u];
191 }
192
193 // ============================== Stream support via Transport HTTP = see documentation there =====
194
195 async p_f_createReadStream(url, {wanturl=false}={}) {
196 return this.http.p_f_createReadStream(this._url(url, servercommands.rawfetch), {wanturl});
197 }
198
199 createReadStream(url, opts) {
200 return this.http.createReadStream(this._url(url, servercommands.rawfetch), opts);
201 }
202
203 async p_createReadStream(url, opts) {
204 return this.http.p_createReadStream(this._url(url, servercommands.rawfetch), opts);
205 }
206
207 // ============================== Key Value support
208
209
210 // Support for Key-Value pairs as per
211 // https://docs.google.com/document/d/1yfmLRqKPxKwB939wIy9sSaa7GKOzM5PrCZ4W1jRGW6M/edit#
212 async p_newdatabase(pubkey) {
213 //if (pubkey instanceof Dweb.PublicPrivate)
214 if (pubkey.hasOwnProperty("keypair"))
215 pubkey = pubkey.keypair.signingexport();
216 // By this point pubkey should be an export of a public key of form xyz:abc where xyz
217 // specifies the type of public key (NACL VERIFY being the only kind we expect currently)
218 let u = `${this.urlbase}/getall/table/${encodeURIComponent(pubkey)}`;
219 return {"publicurl": u, "privateurl": u};
220 }
221
222
223 async p_newtable(pubkey, table) {
224 if (!pubkey) throw new errors.CodingError("p_newtable currently requires a pubkey");
225 let database = await this.p_newdatabase(pubkey);
226 // If have use cases without a database, then call p_newdatabase first
227 return { privateurl: `${database.privateurl}/${table}`, publicurl: `${database.publicurl}/${table}`} // No action required to create it
228 }
229
230 //TODO-KEYVALUE needs signing with private key of list
231 async p_set(url, keyvalues, value) { // url = yjs:/yjs/database/table/key
232 if (!url || !keyvalues) throw new errors.CodingError("TransportHASH.p_set: invalid parms", url, keyvalyes);
233 // Logged by Transports
234 //debug("p_set %o %o %o", url, keyvalues, value);
235 if (typeof keyvalues === "string") {
236 let data = canonicaljson.stringify([{key: keyvalues, value: value}]);
237 await httptools.p_POST(this._url(url, servercommands.set), {data, contenttype: "application/json"}); // Returns immediately
238 } else {
239 let data = canonicaljson.stringify(Object.keys(keyvalues).map((k) => ({"key": k, "value": keyvalues[k]})));
240 await httptools.p_POST(this._url(url, servercommands.set), {data, contenttype: "application/json"}); // Returns immediately
241 }
242 }
243
244 _keyparm(key) {
245 return `key=${encodeURIComponent(key)}`
246 }
247 async p_get(url, keys) {
248 if (!url && keys) throw new errors.CodingError("TransportHASH.p_get: requires url and at least one key");
249 let parmstr =Array.isArray(keys) ? keys.map(k => this._keyparm(k)).join('&') : this._keyparm(keys);
250 const res = await httptools.p_GET(this._url(url, servercommands.get, parmstr));
251 return Array.isArray(keys) ? res : res[keys]
252 }
253
254 async p_delete(url, keys) {
255 if (!url && keys) throw new errors.CodingError("TransportHASH.p_get: requires url and at least one key");
256 let parmstr = keys.map(k => this._keyparm(k)).join('&');
257 await httptools.p_GET(this._url(url, servercommands.delete, parmstr));
258 }
259
260 async p_keys(url) {
261 if (!url && keys) throw new errors.CodingError("TransportHASH.p_get: requires url and at least one key");
262 return await httptools.p_GET(this._url(url, servercommands.keys));
263 }
264 async p_getall(url) {
265 if (!url && keys) throw new errors.CodingError("TransportHASH.p_get: requires url and at least one key");
266 return await httptools.p_GET(this._url(url, servercommands.getall));
267 }
268 /* Make sure doesnt shadow regular p_rawfetch
269 async p_rawfetch(url) {
270 return {
271 table: "keyvaluetable",
272 _map: await this.p_getall(url)
273 }; // Data structure is ok as SmartDict.p_fetch will pass to KVT constructor
274 }
275 */
276
277 async p_info() { //TODO-API
278 /*
279 Return (via cb or promise) a numeric code for the status of a transport.
280 */
281 return new Promise((resolve, reject) => { try { this.updateInfo((err, res) => { if (err) {reject(err)} else {resolve(res)} })} catch(err) {reject(err)}}) // Promisify pattern v2b (no CB)
282 }
283
284 updateInfo(cb) {
285 httptools.p_GET(`${this.urlbase}/info`, {retries: 1}, cb); // Try info, but dont retry (usually heartbeat will reconnect)
286 }
287
288 static async p_test(opts={}) {
289 {console.log("TransportHASH.test")}
290 try {
291 let transport = await this.p_setup(opts);
292 console.log("HASH connected");
293 let res = await transport.p_info();
294 console.log("TransportHASH info=",res);
295 res = await transport.p_status();
296 console.assert(res === Transport.STATUS_CONNECTED);
297 await transport.p_test_kvt("NACL%20VERIFY");
298 } catch(err) {
299 console.log("Exception thrown in TransportHASH.test:", err.message);
300 throw err;
301 }
302 }
303
304 static async test() {
305 return this;
306 }
307
308}
309Transports._transportclasses["HASH"] = TransportHASH;
310TransportHASH.requires = TransportHASH.scripts = []; // Nothing to load
311exports = module.exports = TransportHASH;
312