UNPKG

61.1 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.datastoreGetId = datastoreGetId;
7exports.sanitizePath = sanitizePath;
8exports.dirname = dirname;
9exports.basename = basename;
10exports.datastoreCreateRequest = datastoreCreateRequest;
11exports.datastoreCreate = datastoreCreate;
12exports.datastoreDeleteRequest = datastoreDeleteRequest;
13exports.datastoreDelete = datastoreDelete;
14exports.datastoreMount = datastoreMount;
15exports.datastoreMountOrCreate = datastoreMountOrCreate;
16exports.lookup = lookup;
17exports.listdir = listdir;
18exports.stat = stat;
19exports.getFile = getFile;
20exports.putFile = putFile;
21exports.mkdir = mkdir;
22exports.deleteFile = deleteFile;
23exports.rmdir = rmdir;
24
25var _schemas = require('./schemas');
26
27var _inode = require('./inode');
28
29var _util = require('./util');
30
31var http = require('http');
32var uuid4 = require('uuid/v4');
33var bitcoinjs = require('bitcoinjs-lib');
34var BigInteger = require('bigi');
35var Promise = require('promise');
36var assert = require('assert');
37var Ajv = require('ajv');
38var jsontokens = require('jsontokens');
39
40var EPERM = 1;
41var ENOENT = 2;
42var EACCES = 13;
43var EEXIST = 17;
44var ENOTDIR = 20;
45var EINVAL = 22;
46var EREMOTEIO = 121;
47
48var LOCAL_STORAGE_ID = "blockstack";
49var SUPPORTED_STORAGE_CLASSES = ["read_public", "write_public", "read_private", "write_private", "read_local", "write_local"];
50var REPLICATION_STRATEGY_CLASSES = {
51 'local': new Set(['read_local', 'write_local']),
52 'publish': new Set(['read_public', 'write_private']),
53 'public': new Set(['read_public', 'write_public']),
54 'private': new Set(['read_private', 'write_private'])
55};
56
57/*
58 * Helper method to validate a JSON response
59 * against a schema. Returns the validated object
60 * on success, and throw an exception on error.
61 */
62function validateJSONResponse(resp, result_schema) {
63
64 var ajv = new Ajv();
65 if (result_schema) {
66 try {
67 var valid = ajv.validate(result_schema, resp);
68 assert(valid);
69 return resp;
70 } catch (e) {
71 try {
72 // error message
73 var _valid = ajv.validate(_schemas.CORE_ERROR_SCHEMA, resp);
74 assert(_valid);
75 return resp;
76 } catch (e2) {
77 console.log("Failed to validate with desired schema");
78 console.log(e.stack);
79 console.log("Failed to validate with error schema");
80 console.log(e2.stack);
81 console.log("Desired schema:");
82 console.log(result_schema);
83 console.log("Parsed message:");
84 console.log(resp);
85 throw new Error("Invalid core message");
86 }
87 }
88 } else {
89 return resp;
90 }
91}
92
93/*
94 * Helper method to issue an HTTP request.
95 * @param options (Object) set of HTTP request options
96 * @param result_schema (Object) JSON schema of the expected result
97 *
98 * Returns a structured JSON response on success, conformant to the result_schema.
99 * Returns plaintext on success if the content-type is application/octet-stream
100 * Returns a structured {'error': ...} object on client-side error
101 * Throws on server-side error
102 */
103function httpRequest(options, result_schema, body) {
104
105 if (body) {
106 options['body'] = body;
107 }
108
109 var url = 'http://' + options.host + ':' + options.port + options.path;
110 return fetch(url, options).then(function (response) {
111
112 if (response.status >= 500) {
113 throw new Error(response.statusText);
114 }
115
116 if (response.status === 404) {
117 return { 'error': 'No such file or directory', 'errno': ENOENT };
118 }
119
120 if (response.status === 403) {
121 return { 'error': 'Access denied', 'errno': EACCES };
122 }
123
124 if (response.status === 401) {
125 return { 'error': 'Invalid request', 'errno': EINVAL };
126 }
127
128 if (response.status === 400) {
129 return { 'error': 'Operation not permitted', 'errno': EPERM };
130 }
131
132 var resp = null;
133 if (response.headers.get('content-type') === 'application/json') {
134 return response.json().then(function (resp) {
135 return validateJSONResponse(resp, result_schema);
136 });
137 } else {
138 return response.text();
139 }
140 });
141}
142
143/*
144 * Convert a datastore public key to its ID.
145 * @param ds_public_key (String) hex-encoded ECDSA public key
146 */
147function datastoreGetId(ds_public_key_hex) {
148 var ec = bitcoinjs.ECPair.fromPublicKeyBuffer(Buffer.from(ds_public_key_hex, 'hex'));
149 return ec.getAddress();
150}
151
152/*
153 * Get a *uncompressed* public key (hex) from private key
154 */
155function getPubkeyHex(privkey_hex) {
156 var privkey = BigInteger.fromBuffer((0, _inode.decodePrivateKey)(privkey_hex));
157 var public_key = new bitcoinjs.ECPair(privkey);
158
159 public_key.compressed = false;
160 var public_key_str = public_key.getPublicKeyBuffer().toString('hex');
161 return public_key_str;
162}
163
164/*
165 * Get query string device list from datastore context
166 */
167function getDeviceList(datastore_ctx) {
168 var escaped_device_ids = [];
169 var _iteratorNormalCompletion = true;
170 var _didIteratorError = false;
171 var _iteratorError = undefined;
172
173 try {
174 for (var _iterator = datastore_ctx.app_public_keys[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
175 var dk = _step.value;
176
177 escaped_device_ids.push(escape(dk.device_id));
178 }
179 } catch (err) {
180 _didIteratorError = true;
181 _iteratorError = err;
182 } finally {
183 try {
184 if (!_iteratorNormalCompletion && _iterator.return) {
185 _iterator.return();
186 }
187 } finally {
188 if (_didIteratorError) {
189 throw _iteratorError;
190 }
191 }
192 }
193
194 var res = escaped_device_ids.join(',');
195 return res;
196}
197
198/*
199 * Get query string public key list from datastore context
200 */
201function getPublicKeyList(datastore_ctx) {
202 var escaped_public_keys = [];
203 var _iteratorNormalCompletion2 = true;
204 var _didIteratorError2 = false;
205 var _iteratorError2 = undefined;
206
207 try {
208 for (var _iterator2 = datastore_ctx.app_public_keys[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
209 var dk = _step2.value;
210
211 escaped_public_keys.push(escape(dk.public_key));
212 }
213 } catch (err) {
214 _didIteratorError2 = true;
215 _iteratorError2 = err;
216 } finally {
217 try {
218 if (!_iteratorNormalCompletion2 && _iterator2.return) {
219 _iterator2.return();
220 }
221 } finally {
222 if (_didIteratorError2) {
223 throw _iteratorError2;
224 }
225 }
226 }
227
228 var res = escaped_public_keys.join(',');
229 return res;
230}
231
232/*
233 * Sanitize a path. Consolidate // to /, and resolve foo/../bar to bar
234 * @param path (String) the path
235 *
236 * Returns the sanitized path.
237 */
238function sanitizePath(path) {
239
240 var parts = path.split('/').filter(function (x) {
241 return x.length > 0;
242 });
243 var retparts = [];
244
245 for (var i = 0; i < parts.length; i++) {
246 if (parts[i] === '..') {
247 retparts.pop();
248 } else {
249 retparts.push(parts[i]);
250 }
251 }
252
253 return '/' + retparts.join('/');
254}
255
256/*
257 * Given a path, get the parent directory.
258 *
259 * @param path (String) the path. Must be sanitized
260 */
261function dirname(path) {
262 return '/' + path.split('/').slice(0, -1).join('/');
263}
264
265/*
266 * Given a path, get the base name
267 *
268 * @param path (String) the path. Must be sanitized
269 */
270function basename(path) {
271 return path.split('/').slice(-1)[0];
272}
273
274/*
275 * Given a host:port string, split it into
276 * a host and port
277 *
278 * @param hostport (String) the host:port
279 *
280 * Returns an object with:
281 * .host
282 * .port
283 */
284function splitHostPort(hostport) {
285
286 var host = hostport;
287 var port = 80;
288 var parts = hostport.split(':');
289 if (parts.length > 1) {
290 host = parts[0];
291 port = parts[1];
292 }
293
294 return { 'host': host, 'port': port };
295}
296
297/*
298 * Create the signed request to create a datastore.
299 * This information can be fed into datastoreCreate()
300 * Returns an object with:
301 * .datastore_info: datastore information
302 * .datastore_sigs: signatures over the above.
303 */
304function datastoreCreateRequest(ds_type, ds_private_key_hex, drivers, device_id, all_device_ids) {
305
306 assert(ds_type === 'datastore' || ds_type === 'collection');
307 var root_uuid = uuid4();
308
309 var ds_public_key = getPubkeyHex(ds_private_key_hex);
310 var datastore_id = datastoreGetId(ds_public_key);
311 var root_blob_info = (0, _inode.makeDirInodeBlob)(datastore_id, datastore_id, root_uuid, {}, device_id, 1);
312
313 // actual datastore payload
314 var datastore_info = {
315 'type': ds_type,
316 'pubkey': ds_public_key,
317 'drivers': drivers,
318 'device_ids': all_device_ids,
319 'root_uuid': root_uuid
320 };
321
322 var data_id = datastore_id + '.datastore';
323 var datastore_blob = (0, _inode.makeMutableDataInfo)(data_id, (0, _util.jsonStableSerialize)(datastore_info), device_id, 1);
324
325 var datastore_str = (0, _util.jsonStableSerialize)(datastore_blob);
326
327 // sign them all
328 var root_sig = (0, _inode.signDataPayload)(root_blob_info.header, ds_private_key_hex);
329 var datastore_sig = (0, _inode.signDataPayload)(datastore_str, ds_private_key_hex);
330
331 // make and sign tombstones for the root
332 var root_tombstones = (0, _inode.makeInodeTombstones)(datastore_id, root_uuid, all_device_ids);
333 var signed_tombstones = (0, _inode.signMutableDataTombstones)(root_tombstones, ds_private_key_hex);
334
335 var info = {
336 'datastore_info': {
337 'datastore_id': datastore_id,
338 'datastore_blob': datastore_str,
339 'root_blob_header': root_blob_info.header,
340 'root_blob_idata': root_blob_info.idata
341 },
342 'datastore_sigs': {
343 'datastore_sig': datastore_sig,
344 'root_sig': root_sig
345 },
346 'root_tombstones': signed_tombstones
347 };
348
349 return info;
350}
351
352/*
353 * Create a datastore
354 * Asynchronous; returns a Promise that resolves to either {'status': true} (on success)
355 * or {'error': ...} (on error)
356 */
357function datastoreCreate(blockstack_hostport, blockstack_session_token, datastore_request) {
358
359 var payload = {
360 'datastore_info': {
361 'datastore_blob': datastore_request.datastore_info.datastore_blob,
362 'root_blob_header': datastore_request.datastore_info.root_blob_header,
363 'root_blob_idata': datastore_request.datastore_info.root_blob_idata
364 },
365 'datastore_sigs': {
366 'datastore_sig': datastore_request.datastore_sigs.datastore_sig,
367 'root_sig': datastore_request.datastore_sigs.root_sig
368 },
369 'root_tombstones': datastore_request.root_tombstones
370 };
371
372 var hostinfo = splitHostPort(blockstack_hostport);
373
374 var options = {
375 'method': 'POST',
376 'host': hostinfo.host,
377 'port': hostinfo.port,
378 'path': '/v1/stores'
379 };
380
381 if (blockstack_session_token) {
382 options['headers'] = { 'Authorization': 'bearer ' + blockstack_session_token };
383 }
384
385 var body = JSON.stringify(payload);
386 options['headers']['Content-Type'] = 'application/json';
387 options['headers']['Content-Length'] = body.length;
388
389 return httpRequest(options, _schemas.SUCCESS_FAIL_SCHEMA, body);
390}
391
392/*
393 * Generate the data needed to delete a datastore.
394 *
395 * @param ds (Object) a datastore context (will be loaded from localstorage if not given)
396 *
397 * Returns an object to be given to datastoreDelete()
398 */
399function datastoreDeleteRequest() {
400 var ds = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
401
402
403 if (!ds) {
404 var blockchain_id = getSessionBlockchainID();
405 assert(blockchain_id);
406
407 ds = getCachedMountContext(blockchain_id);
408 assert(ds);
409 }
410
411 var datastore_id = ds.datastore_id;
412 var device_ids = ds.datastore.device_ids;
413 var root_uuid = ds.datastore.root_uuid;
414 var data_id = datastore_id + '.datastore';
415
416 var tombstones = (0, _inode.makeMutableDataTombstones)(device_ids, data_id);
417 var signed_tombstones = (0, _inode.signMutableDataTombstones)(tombstones, ds.privkey_hex);
418
419 var root_tombstones = (0, _inode.makeInodeTombstones)(datastore_id, root_uuid, device_ids);
420 var signed_root_tombstones = (0, _inode.signMutableDataTombstones)(root_tombstones, ds.privkey_hex);
421
422 var ret = {
423 'datastore_tombstones': signed_tombstones,
424 'root_tombstones': signed_root_tombstones
425 };
426
427 return ret;
428}
429
430/*
431 * Delete a datastore
432 *
433 * @param ds (Object) OPTINOAL: the datastore context (will be loaded from localStorage if not given)
434 * @param ds_tombstones (Object) OPTINOAL: signed information from datastoreDeleteRequest()
435 * @param root_tombstones (Object) OPTINAL: signed information from datastoreDeleteRequest()
436 *
437 * Asynchronous; returns a Promise that resolves to either {'status': true} on success
438 * or {'error': ...} on error
439 */
440function datastoreDelete() {
441 var ds = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
442 var ds_tombstones = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
443 var root_tombstones = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
444
445
446 if (!ds) {
447 var blockchain_id = getSessionBlockchainID();
448 assert(blockchain_id);
449
450 ds = getCachedMountContext(blockchain_id);
451 assert(ds);
452 }
453
454 if (!ds_tombstones || !root_tombstones) {
455 var delete_info = datastoreDeleteRequest(ds);
456 ds_tombstones = delete_info['datastore_tombstones'];
457 root_tombstones = delete_info['root_tombstones'];
458 }
459
460 var device_list = getDeviceList(ds);
461 var payload = {
462 'datastore_tombstones': ds_tombstones,
463 'root_tombstones': root_tombstones
464 };
465
466 var options = {
467 'method': 'DELETE',
468 'host': ds.host,
469 'port': ds.port,
470 'path': '/v1/stores?device_ids=' + device_list
471 };
472
473 if (ds.session_token) {
474 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
475 }
476
477 var body = JSON.stringify(payload);
478 options['headers']['Content-Type'] = 'application/json';
479 options['headers']['Content-Length'] = body.length;
480
481 return httpRequest(options, _schemas.SUCCESS_FAIL_SCHEMA, body);
482}
483
484/*
485 * Look up a datastore and establish enough contextual information to do subsequent storage operations.
486 * Asynchronous; returns a Promise
487 *
488 * opts is an object that must contain either:
489 * * appPrivateKey (string) the application private key
490 * * (optional) sessionToken (string) the Core session token, OR
491 * * (optional) device_id (string) the device ID
492 *
493 * OR:
494 *
495 * * blockchainID (string) the blockchain ID of the user whose datastore we're going to access
496 * * appName (string) the name of the application
497 *
498 * TODO: support accessing datastores from other users
499 *
500 * Returns a Promise that resolves to a datastore connection,
501 * with the following properties:
502 * .host: blockstack host
503 * .datastore: datastore object
504 *
505 * Returns a Promise that resolves to null, if the datastore does not exist.
506 *
507 * Throws an error on all other errors
508 */
509function datastoreMount(opts) {
510
511 var data_privkey_hex = opts.appPrivateKey;
512 var sessionToken = opts.sessionToken;
513
514 // TODO: only support single-user datastore access
515 assert(data_privkey_hex);
516
517 var datastore_id = null;
518 var device_id = null;
519 var blockchain_id = null;
520 var api_endpoint = null;
521 var app_public_keys = null;
522
523 if (!sessionToken) {
524 // load from user data
525 var userData = getUserData();
526
527 sessionToken = userData.coreSessionToken;
528 assert(sessionToken);
529 }
530
531 var session = jsontokens.decodeToken(sessionToken).payload;
532
533 if (data_privkey_hex) {
534 datastore_id = datastoreGetId(getPubkeyHex(data_privkey_hex));
535 } else {
536 blockchain_id = opts.blockchainID;
537 var app_name = opts.appName;
538
539 assert(blockchain_id);
540 assert(app_name);
541
542 // TODO: look up the datastore information via Core
543 // TODO: blocked by Core's lack of support for token files
544 // TODO: set device_id, blockchain_id, app_public_keys
545 }
546
547 if (!device_id) {
548 device_id = session.device_id;
549 assert(device_id);
550 }
551
552 if (!api_endpoint) {
553 api_endpoint = session.api_endpoint;
554 assert(api_endpoint);
555 }
556
557 if (!blockchain_id) {
558 blockchain_id = session.blockchain_id;
559 assert(blockchain_id);
560 }
561
562 if (!app_public_keys) {
563 app_public_keys = session.app_public_keys;
564 assert(app_public_keys);
565 }
566
567 var blockstack_hostport = api_endpoint.split('://').reverse()[0];
568 var hostinfo = splitHostPort(blockstack_hostport);
569
570 var ctx = {
571 'host': hostinfo.host,
572 'port': hostinfo.port,
573 'blockchain_id': blockchain_id,
574 'device_id': device_id,
575 'datastore_id': datastore_id,
576 'session_token': sessionToken,
577 'app_public_keys': app_public_keys,
578 'session': session,
579 'datastore': null
580 };
581
582 if (data_privkey_hex) {
583 ctx.privkey_hex = data_privkey_hex;
584 }
585
586 var options = {
587 'method': 'GET',
588 'host': hostinfo.host,
589 'port': hostinfo.port,
590 'path': '/v1/stores/' + datastore_id + '?device_ids=' + device_id + '&blockchain_id=' + blockchain_id
591 };
592
593 options['headers'] = { 'Authorization': 'bearer ' + sessionToken };
594
595 return httpRequest(options, _schemas.DATASTORE_RESPONSE_SCHEMA).then(function (ds) {
596 if (!ds || ds.error) {
597 // ENOENT?
598 if (!ds || ds.errno === ENOENT) {
599 return null;
600 } else {
601 var errorMsg = ds.error || 'No response given';
602 throw new Error('Failed to get datastore: ' + errorMsg);
603 }
604 } else {
605 ctx['datastore'] = ds.datastore;
606
607 // save
608 setCachedMountContext(blockchain_id, ctx);
609
610 // this is required for testing purposes, since the core session token will not have been set
611 var _userData = getUserData();
612 if (!_userData.coreSessionToken) {
613 console.log("In test framework; saving session token");
614 _userData.coreSessionToken = sessionToken;
615 setUserData(_userData);
616 }
617
618 return ctx;
619 }
620 });
621}
622
623/*
624 * Get local storage object for Blockstack
625 * Throws on error
626 */
627function getUserData() {
628 var localStorage = null;
629
630 if (typeof window === 'undefined' || window === null) {
631 var LocalStorage = require('node-localstorage').LocalStorage;
632 localStorage = new LocalStorage('./scratch');
633 } else {
634 localStorage = window.localStorage;
635 }
636
637 var userData = localStorage.getItem(LOCAL_STORAGE_ID);
638 if (userData === null) {
639 userData = '{}';
640 }
641
642 userData = JSON.parse(userData);
643 return userData;
644}
645
646/*
647 * Save local storage
648 */
649function setUserData(userData) {
650 var localStorage = null;
651
652 if (typeof window === 'undefined' || window === null) {
653 var LocalStorage = require('node-localstorage').LocalStorage;
654 localStorage = new LocalStorage('./scratch');
655 } else {
656 localStorage = window.localStorage;
657 }
658
659 localStorage.setItem(LOCAL_STORAGE_ID, JSON.stringify(userData));
660}
661
662/*
663 * Get a cached app-specific datastore mount context for a given blockchain ID and application
664 * Return null if not found
665 * Throws on error
666 */
667function getCachedMountContext(blockchain_id) {
668
669 var userData = getUserData();
670 if (!userData.datastore_contexts) {
671 console.log("No datastore contexts defined");
672 return null;
673 }
674
675 if (!userData.datastore_contexts[blockchain_id]) {
676 console.log('No datastore contexts for ' + blockchain_id);
677 return null;
678 }
679
680 var ctx = userData.datastore_contexts[blockchain_id];
681 if (!ctx) {
682 console.log('Null datastore context for ' + blockchain_id);
683 return null;
684 }
685
686 return ctx;
687}
688
689/*
690 * Cache a mount context for a blockchain ID
691 */
692function setCachedMountContext(blockchain_id, datastore_context) {
693
694 var userData = getUserData();
695 if (!userData.datastore_contexts) {
696 userData.datastore_contexts = {};
697 }
698
699 userData.datastore_contexts[blockchain_id] = datastore_context;
700 setUserData(userData);
701}
702
703/*
704 * Get the current session's blockchain ID
705 * Throw if not defined or not present.
706 */
707function getSessionBlockchainID() {
708
709 var userData = getUserData();
710 assert(userData);
711 assert(userData.coreSessionToken);
712
713 var session = jsontokens.decodeToken(userData.coreSessionToken).payload;
714
715 assert(session.blockchain_id);
716 return session.blockchain_id;
717}
718
719/*
720 * Fulfill a replication strategy using the drivers available to us.
721 *
722 * replication_strategy (object): a dict that maps strategies (i.e. 'local', 'public', 'private') to integer counts
723 * classes (object): this is session.storage.classes (i.e. the driver classification; maps a driver name to its list of classes)
724 *
725 * Returns the list of drivers to use.
726 * Throws on error.
727 */
728function selectDrivers(replication_strategy, classes) {
729
730 // select defaults from classification and replication strategy
731 var driver_sets = []; // driver_sets[i] is the set of drivers that support SUPPORTED_STORAGE_CLASSES[i]
732 var driver_classes = {}; // map driver name to set of classes
733 var all_drivers = new Set([]); // set of all drivers available to us
734 var available_drivers = []; // drivers available to us
735 var selected_drivers = []; // drivers compatible with our replication strategy (return value)
736 var have_drivers = false; // whether or not we selected drivers that fulfill our replication strategy
737
738 for (var i = 0; i < SUPPORTED_STORAGE_CLASSES.length; i++) {
739 var driver_set = new Set(classes[SUPPORTED_STORAGE_CLASSES[i]]);
740 driver_sets.push(driver_set);
741
742 var _iteratorNormalCompletion3 = true;
743 var _didIteratorError3 = false;
744 var _iteratorError3 = undefined;
745
746 try {
747 for (var _iterator3 = driver_set[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
748 var d = _step3.value;
749
750 all_drivers.add(d);
751 }
752 } catch (err) {
753 _didIteratorError3 = true;
754 _iteratorError3 = err;
755 } finally {
756 try {
757 if (!_iteratorNormalCompletion3 && _iterator3.return) {
758 _iterator3.return();
759 }
760 } finally {
761 if (_didIteratorError3) {
762 throw _iteratorError3;
763 }
764 }
765 }
766
767 var _iteratorNormalCompletion4 = true;
768 var _didIteratorError4 = false;
769 var _iteratorError4 = undefined;
770
771 try {
772 for (var _iterator4 = driver_set[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
773 var _d = _step4.value;
774
775 console.log('Driver ' + _d + ' implementes ' + SUPPORTED_STORAGE_CLASSES[i]);
776 if (driver_classes[_d]) {
777 driver_classes[_d].push(SUPPORTED_STORAGE_CLASSES[i]);
778 } else {
779 driver_classes[_d] = [SUPPORTED_STORAGE_CLASSES[i]];
780 }
781 }
782 } catch (err) {
783 _didIteratorError4 = true;
784 _iteratorError4 = err;
785 } finally {
786 try {
787 if (!_iteratorNormalCompletion4 && _iterator4.return) {
788 _iterator4.return();
789 }
790 } finally {
791 if (_didIteratorError4) {
792 throw _iteratorError4;
793 }
794 }
795 }
796 }
797
798 var concern_fulfillment = {};
799
800 var _iteratorNormalCompletion5 = true;
801 var _didIteratorError5 = false;
802 var _iteratorError5 = undefined;
803
804 try {
805 for (var _iterator5 = all_drivers[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
806 var _d2 = _step5.value;
807
808 var _classes = driver_classes[_d2];
809
810 // a driver fits the replication strategy if all of its
811 // classes matches at least one concern (i.e. 'local', 'public')
812 var _iteratorNormalCompletion6 = true;
813 var _didIteratorError6 = false;
814 var _iteratorError6 = undefined;
815
816 try {
817 for (var _iterator6 = Object.keys(replication_strategy)[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
818 var concern = _step6.value;
819
820
821 var matches = false;
822 var _iteratorNormalCompletion7 = true;
823 var _didIteratorError7 = false;
824 var _iteratorError7 = undefined;
825
826 try {
827 for (var _iterator7 = _classes[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
828 var dclass = _step7.value;
829
830 if (REPLICATION_STRATEGY_CLASSES[concern].has(dclass)) {
831 matches = true;
832 break;
833 }
834 }
835 } catch (err) {
836 _didIteratorError7 = true;
837 _iteratorError7 = err;
838 } finally {
839 try {
840 if (!_iteratorNormalCompletion7 && _iterator7.return) {
841 _iterator7.return();
842 }
843 } finally {
844 if (_didIteratorError7) {
845 throw _iteratorError7;
846 }
847 }
848 }
849
850 if (matches) {
851 console.log('Driver ' + _d2 + ' fulfills replication concern ' + concern);
852
853 if (concern_fulfillment[concern]) {
854 concern_fulfillment[concern] += 1;
855 } else {
856 concern_fulfillment[concern] = 1;
857 }
858
859 if (concern_fulfillment[concern] <= replication_strategy[concern]) {
860 console.log('Select driver ' + _d2);
861 selected_drivers.push(_d2);
862 }
863 }
864
865 // strategy fulfilled?
866 var fulfilled = true;
867 var _iteratorNormalCompletion8 = true;
868 var _didIteratorError8 = false;
869 var _iteratorError8 = undefined;
870
871 try {
872 for (var _iterator8 = Object.keys(replication_strategy)[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
873 var _concern = _step8.value;
874
875 var count = 0;
876 if (concern_fulfillment[_concern]) {
877 count = concern_fulfillment[_concern];
878 }
879
880 if (count < replication_strategy[_concern]) {
881 fulfilled = false;
882 break;
883 }
884 }
885 } catch (err) {
886 _didIteratorError8 = true;
887 _iteratorError8 = err;
888 } finally {
889 try {
890 if (!_iteratorNormalCompletion8 && _iterator8.return) {
891 _iterator8.return();
892 }
893 } finally {
894 if (_didIteratorError8) {
895 throw _iteratorError8;
896 }
897 }
898 }
899
900 if (fulfilled) {
901 have_drivers = true;
902 break;
903 }
904 }
905 } catch (err) {
906 _didIteratorError6 = true;
907 _iteratorError6 = err;
908 } finally {
909 try {
910 if (!_iteratorNormalCompletion6 && _iterator6.return) {
911 _iterator6.return();
912 }
913 } finally {
914 if (_didIteratorError6) {
915 throw _iteratorError6;
916 }
917 }
918 }
919
920 if (have_drivers) {
921 break;
922 }
923 }
924 } catch (err) {
925 _didIteratorError5 = true;
926 _iteratorError5 = err;
927 } finally {
928 try {
929 if (!_iteratorNormalCompletion5 && _iterator5.return) {
930 _iterator5.return();
931 }
932 } finally {
933 if (_didIteratorError5) {
934 throw _iteratorError5;
935 }
936 }
937 }
938
939 if (!have_drivers) {
940 throw new Error("Unsatisfiable replication strategy");
941 }
942
943 return selected_drivers;
944}
945
946/*
947 * Connect to or create a datastore.
948 * Asynchronous, returns a Promise
949 *
950 * Returns a Promise that yields a datastore connection.
951 * Throws on error.
952 *
953 */
954function datastoreMountOrCreate() {
955 var replication_strategy = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { 'public': 1, 'local': 1 };
956 var sessionToken = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
957 var appPrivateKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
958
959
960 if (!sessionToken) {
961 var userData = getUserData();
962
963 sessionToken = userData.coreSessionToken;
964 assert(sessionToken);
965 }
966
967 // decode
968 var session = jsontokens.decodeToken(sessionToken).payload;
969 assert(session.blockchain_id);
970
971 var ds = getCachedMountContext(session.blockchain_id);
972 if (ds) {
973 return new Promise(function (resolve, reject) {
974 resolve(ds);
975 });
976 }
977
978 // no cached datastore context.
979 // go ahead and create one (need appPrivateKey)
980 if (!appPrivateKey) {
981 var _userData2 = getUserData();
982
983 appPrivateKey = _userData2.appPrivateKey;
984 assert(appPrivateKey);
985 }
986
987 // sanity check
988 var _iteratorNormalCompletion9 = true;
989 var _didIteratorError9 = false;
990 var _iteratorError9 = undefined;
991
992 try {
993 for (var _iterator9 = Object.keys(replication_strategy)[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) {
994 var strategy = _step9.value;
995
996 var supported = false;
997 var _iteratorNormalCompletion10 = true;
998 var _didIteratorError10 = false;
999 var _iteratorError10 = undefined;
1000
1001 try {
1002 for (var _iterator10 = Object.keys(REPLICATION_STRATEGY_CLASSES)[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) {
1003 var supported_strategy = _step10.value;
1004
1005 if (supported_strategy === strategy) {
1006 supported = true;
1007 break;
1008 }
1009 }
1010 } catch (err) {
1011 _didIteratorError10 = true;
1012 _iteratorError10 = err;
1013 } finally {
1014 try {
1015 if (!_iteratorNormalCompletion10 && _iterator10.return) {
1016 _iterator10.return();
1017 }
1018 } finally {
1019 if (_didIteratorError10) {
1020 throw _iteratorError10;
1021 }
1022 }
1023 }
1024
1025 if (!supported) {
1026 throw new Error('Unsupported replication strategy ' + strategy);
1027 }
1028 }
1029 } catch (err) {
1030 _didIteratorError9 = true;
1031 _iteratorError9 = err;
1032 } finally {
1033 try {
1034 if (!_iteratorNormalCompletion9 && _iterator9.return) {
1035 _iterator9.return();
1036 }
1037 } finally {
1038 if (_didIteratorError9) {
1039 throw _iteratorError9;
1040 }
1041 }
1042 }
1043
1044 var drivers = null;
1045
1046 // find satisfactory storage drivers
1047 if (Object.keys(session.storage.preferences).includes(session.app_domain)) {
1048
1049 // app-specific preference
1050 drivers = session.storage.preferences[app_domain];
1051 } else {
1052
1053 // select defaults given the replication strategy
1054 drivers = selectDrivers(replication_strategy, session.storage.classes);
1055 }
1056
1057 var hostport = session.api_endpoint.split('://').reverse()[0];
1058 var appPublicKeys = session.app_public_keys;
1059 var deviceID = session.device_id;
1060 var allDeviceIDs = [];
1061
1062 for (var i = 0; i < appPublicKeys.length; i++) {
1063 allDeviceIDs.push(appPublicKeys[i].device_id);
1064 }
1065
1066 console.log('Will use drivers ' + drivers.join(','));
1067 console.log('Datastore will span devices ' + allDeviceIDs.join(','));
1068
1069 var datastoreOpts = {
1070 'appPrivateKey': appPrivateKey,
1071 'sessionToken': sessionToken
1072 };
1073
1074 return datastoreMount(datastoreOpts).then(function (datastore_ctx) {
1075 if (!datastore_ctx) {
1076 // does not exist
1077 console.log("Datastore does not exist; creating...");
1078
1079 var info = datastoreCreateRequest('datastore', appPrivateKey, drivers, deviceID, allDeviceIDs);
1080
1081 // go create it
1082 return datastoreCreate(hostport, sessionToken, info).then(function (res) {
1083 if (res.error) {
1084 console.log(error);
1085 var errorNo = res.errno || 'UNKNOWN';
1086 var errorMsg = res.error || 'UNKNOWN';
1087 throw new Error('Failed to create datastore (errno ' + errorNo + '): ' + errorMsg);
1088 }
1089
1090 // connect to it now
1091 return datastoreMount(datastoreOpts);
1092 });
1093 } else if (datastore_ctx.error) {
1094 // some other error
1095 var errorMsg = datastore_ctx.error || 'UNKNOWN';
1096 var errorNo = datastore_ctx.errno || 'UNKNOWN';
1097 throw new Error('Failed to access datastore (errno ' + errorNo + '): ' + errorMsg);
1098 } else {
1099 // exists
1100 return datastore_ctx;
1101 }
1102 });
1103}
1104
1105/*
1106 * Path lookup
1107 *
1108 * @param ds (Object) a datastore context
1109 * @param path (String) the path to the inode
1110 * @param opts (Object) optional arguments:
1111 * .extended (Bool) whether or not to include the entire path's inode information
1112 * .force (Bool) if True, then ignore stale inode errors.
1113 * .idata (Bool) if True, then get the inode payload as well
1114 * .blockchain_id (String) this is the blockchain ID of the datastore owner, if different from the session token
1115 * .ds (datastore context) if given, then use this datastore mount context instead of one from localstorage
1116 *
1117 * Returns a promise that resolves to a lookup response schema (or an extended lookup response schema, if opts.extended is set)
1118 */
1119function lookup(path) {
1120 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1121
1122
1123 var blockchain_id = opts.blockchain_id;
1124
1125 if (!opts.blockchain_id) {
1126 blockchain_id = getSessionBlockchainID();
1127 }
1128
1129 return datastoreMountOrCreate().then(function (ds) {
1130 assert(ds);
1131
1132 var datastore_id = ds.datastore_id;
1133 var device_list = getDeviceList(ds);
1134 var device_pubkeys = getPublicKeyList(ds);
1135 var options = {
1136 'method': 'GET',
1137 'host': ds.host,
1138 'port': ds.port,
1139 'path': '/v1/stores/' + datastore_id + '/inodes?path=' + escape(sanitizePath(path)) + '&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id
1140 };
1141
1142 if (!opts) {
1143 opts = {};
1144 }
1145
1146 var schema = _schemas.DATASTORE_LOOKUP_RESPONSE_SCHEMA;
1147
1148 if (opts.extended) {
1149 options['path'] += '&extended=1';
1150 schema = _schemas.DATASTORE_LOOKUP_EXTENDED_RESPONSE_SCHEMA;
1151 }
1152
1153 if (opts.force) {
1154 options['path'] += '&force=1';
1155 }
1156
1157 if (opts.idata) {
1158 options['idata'] += '&idata=1';
1159 }
1160
1161 return httpRequest(options, schema).then(function (lookup_response) {
1162 if (lookup_response.error || lookup_response.errno) {
1163 var errorMsg = lookup_response.error || 'UNKNOWN';
1164 var errorNo = lookup_response.errno || 'UNKNOWN';
1165 throw new Error('Failed to look up ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1166 } else {
1167 return lookup_response;
1168 }
1169 });
1170 });
1171}
1172
1173/*
1174 * List a directory.
1175 *
1176 * @param ds (Object) a datastore context
1177 * @param path (String) the path to the directory to list
1178 * @param opts (Object) optional arguments:
1179 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1180 * .force (Bool) if True, then ignore stale inode errors.
1181 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1182 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1183 *
1184 * Asynchronous; returns a Promise that resolves to either directory idata, or an extended mutable datum response (if opts.extended is set)
1185 */
1186function listdir(path) {
1187 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1188
1189
1190 var blockchain_id = opts.blockchain_id;
1191
1192 if (!opts.blockchain_id) {
1193 blockchain_id = getSessionBlockchainID();
1194 }
1195
1196 return datastoreMountOrCreate().then(function (ds) {
1197
1198 assert(ds);
1199
1200 var datastore_id = ds.datastore_id;
1201 var device_list = getDeviceList(ds);
1202 var device_pubkeys = getPublicKeyList(ds);
1203 var options = {
1204 'method': 'GET',
1205 'host': ds.host,
1206 'port': ds.port,
1207 'path': '/v1/stores/' + datastore_id + '/directories?path=' + escape(sanitizePath(path)) + '&idata=1&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id
1208 };
1209
1210 var schema = _schemas.MUTABLE_DATUM_DIR_IDATA_SCHEMA;
1211
1212 if (!opts) {
1213 opts = {};
1214 }
1215
1216 if (opts.extended) {
1217 options['path'] += '&extended=1';
1218 schema = MUTABLE_DATUM_EXTENDED_RESPONSE_SCHEMA;
1219 }
1220
1221 if (opts.force) {
1222 optsion['path'] += '&force=1';
1223 }
1224
1225 if (ds.session_token) {
1226 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1227 }
1228
1229 return httpRequest(options, schema).then(function (response) {
1230 if (response.error || response.errno) {
1231 var errorMsg = response.error || 'UNKNOWN';
1232 var errorNo = response.errno || 'UNKNOWN';
1233 throw new Error('Failed to listdir ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1234 } else {
1235 return response;
1236 }
1237 });
1238 });
1239}
1240
1241/*
1242 * Stat a file or directory (i.e. get the inode header)
1243 *
1244 * @param ds (Object) a datastore context
1245 * @param path (String) the path to the directory to list
1246 * @param opts (Object) optional arguments:
1247 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1248 * .force (Bool) if True, then ignore stale inode errors.
1249 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1250 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1251 *
1252 * Asynchronous; returns a Promise that resolves to either an inode schema, or a mutable datum extended response schema (if opts.extended is set)
1253 */
1254function stat(path) {
1255 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1256
1257
1258 var ds = opts.ds;
1259 var blockchain_id = opts.blockchain_id;
1260
1261 if (!opts.blockchain_id) {
1262 blockchain_id = getSessionBlockchainID();
1263 }
1264
1265 return datastoreMountOrCreate().then(function (ds) {
1266
1267 assert(ds);
1268
1269 var datastore_id = ds.datastore_id;
1270 var device_list = getDeviceList(ds);
1271 var device_pubkeys = getPublicKeyList(ds);
1272 var options = {
1273 'method': 'GET',
1274 'host': ds.host,
1275 'port': ds.port,
1276 'path': '/v1/stores/' + datastore_id + '/inodes?path=' + escape(sanitizePath(path)) + '&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id
1277 };
1278
1279 var schema = _schemas.MUTABLE_DATUM_INODE_SCHEMA;
1280
1281 if (!opts) {
1282 opts = {};
1283 }
1284
1285 if (opts.extended) {
1286 options['path'] += '&extended=1';
1287 schema = MUTABLE_DATUM_EXTENDED_RESPONSE_SCHEMA;
1288 }
1289
1290 if (opts.force) {
1291 optsion['path'] += '&force=1';
1292 }
1293
1294 if (ds.session_token) {
1295 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1296 }
1297
1298 return httpRequest(options, schema).then(function (response) {
1299 if (response.error || response.errno) {
1300 var errorMsg = response.error || 'UNKNOWN';
1301 var errorNo = response.errno || 'UNKNOWN';
1302 throw new Error('Failed to stat ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1303 } else {
1304 return response;
1305 }
1306 });
1307 });
1308}
1309
1310/*
1311 * Get an undifferentiated file or directory and its data.
1312 * Low-level method, not meant for external consumption.
1313 *
1314 * @param ds (Object) a datastore context
1315 * @param path (String) the path to the directory to list
1316 * @param opts (Object) optional arguments:
1317 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1318 * .force (Bool) if True, then ignore stale inode errors.
1319 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1320 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1321 *
1322 * Asynchronous; returns a Promise that resolves to an inode and its data, or an extended mutable datum response (if opts.extended is set)
1323 */
1324function getInode(path) {
1325 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1326
1327
1328 var blockchain_id = opts.blockchain_id;
1329
1330 if (!opts.blockchain_id) {
1331 blockchain_id = getSessionBlockchainID();
1332 }
1333
1334 return datastoreMountOrCreate().then(function (ds) {
1335
1336 assert(ds);
1337
1338 var datastore_id = ds.datastore_id;
1339 var device_list = getDeviceList(ds);
1340 var device_pubkeys = getPublicKeyList(ds);
1341 var options = {
1342 'method': 'GET',
1343 'host': ds.host,
1344 'port': ds.port,
1345 'path': '/v1/stores/' + datastore_id + '/inodes?path=' + escape(sanitizePath(path)) + '&idata=1&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id
1346 };
1347
1348 var schema = _schemas.MUTABLE_DATUM_INODE_SCHEMA;
1349
1350 if (!opts) {
1351 opts = {};
1352 }
1353
1354 if (opts.extended) {
1355 options['path'] += '&extended=1';
1356 schema = MUTABLE_DATUM_EXTENDED_RESPONSE_SCHEMA;
1357 }
1358
1359 if (opts.force) {
1360 options['path'] += '&force=1';
1361 }
1362
1363 if (ds.session_token) {
1364 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1365 }
1366
1367 return httpRequest(options, schema).then(function (response) {
1368 if (response.error || response.errno) {
1369 var errorMsg = response.error || 'UNKNOWN';
1370 var errorNo = response.errno || 'UNKNOWN';
1371 throw new Error('Failed to getInode ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1372 } else {
1373 return response;
1374 }
1375 });
1376 });
1377}
1378
1379/*
1380 * Get a file.
1381 *
1382 * @param ds (Object) a datastore context
1383 * @param path (String) the path to the file to read
1384 * @param opts (Object) optional arguments:
1385 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1386 * .force (Bool) if True, then ignore stale inode errors.
1387 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1388 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1389 *
1390 * Asynchronous; returns a Promise that resolves to either raw data, or an extended mutable data response schema (if opts.extended is set).
1391 * If the file does not exist, then the Promise resolves to null. Any other errors result in an Error being thrown.
1392 */
1393function getFile(path) {
1394 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1395
1396
1397 var blockchain_id = opts.blockchain_id;
1398
1399 if (!opts.blockchain_id) {
1400 blockchain_id = getSessionBlockchainID();
1401 }
1402
1403 return datastoreMountOrCreate().then(function (ds) {
1404 assert(ds);
1405
1406 var datastore_id = ds.datastore_id;
1407 var device_list = getDeviceList(ds);
1408 var device_pubkeys = getPublicKeyList(ds);
1409 var options = {
1410 'method': 'GET',
1411 'host': ds.host,
1412 'port': ds.port,
1413 'path': '/v1/stores/' + datastore_id + '/files?path=' + escape(sanitizePath(path)) + '&idata=1&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id
1414 };
1415
1416 var schema = 'bytes';
1417
1418 if (!opts) {
1419 opts = {};
1420 }
1421
1422 if (opts.extended) {
1423 options['path'] += '&extended=1';
1424 schema = MUTABLE_DATUM_EXTENDED_RESPONSE_SCHEMA;
1425 }
1426
1427 if (opts.force) {
1428 options['path'] += '&force=1';
1429 }
1430
1431 if (ds.session_token) {
1432 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1433 }
1434
1435 return httpRequest(options, schema).then(function (response) {
1436 if (response.error || response.errno) {
1437 // ENOENT?
1438 if (response.errno === ENOENT) {
1439 return null;
1440 }
1441
1442 // some other error
1443 var errorMsg = response.error || 'UNKNOWN';
1444 var errorNo = response.errno || 'UNKNOWN';
1445 throw new Error('Failed to getFile ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1446 } else {
1447 return response;
1448 }
1449 });
1450 });
1451}
1452
1453/*
1454 * Execute a datastore operation
1455 *
1456 * @param ds (Object) a datastore context
1457 * @param operation (String) the specific operation being carried out.
1458 * @param path (String) the path of the operation
1459 * @param inodes (Array) the list of inode headers to replicate
1460 * @param payloads (Array) the list of inode payloads in 1-to-1 correspondence to the headers
1461 * @param signatures (Array) the list of signatures over each inode header (also 1-to-1 correspondence)
1462 * @param tombstones (Array) the list of signed inode tombstones
1463 *
1464 * Asynchronous; returns a Promise that resolves to True if the operation succeeded
1465 */
1466function datastoreOperation(ds, operation, path, inodes, payloads, signatures, tombstones) {
1467
1468 var request_path = null;
1469 var http_operation = null;
1470 var datastore_id = ds.datastore_id;
1471 var datastore_privkey = ds.privkey_hex;
1472 var device_list = getDeviceList(ds);
1473 var device_pubkeys = getPublicKeyList(ds);
1474
1475 assert(inodes.length === payloads.length);
1476 assert(payloads.length === signatures.length);
1477
1478 if (operation === 'mkdir') {
1479 request_path = '/v1/stores/' + datastore_id + '/directories?path=' + escape(sanitizePath(path)) + '&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id;
1480 http_operation = 'POST';
1481
1482 assert(inodes.length === 2);
1483 } else if (operation === 'putFile') {
1484 request_path = '/v1/stores/' + datastore_id + '/files?path=' + escape(sanitizePath(path)) + '&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id;
1485 http_operation = 'PUT';
1486
1487 assert(inodes.length === 1 || inodes.length === 2);
1488 } else if (operation === 'rmdir') {
1489 request_path = '/v1/stores/' + datastore_id + '/directories?path=' + escape(sanitizePath(path)) + '&device_pubkeys=' + device_pubkeys + '&device_ids=' + device_list + '&blockchain_id=' + ds.blockchain_id;
1490 http_operation = 'DELETE';
1491
1492 assert(inodes.length === 1);
1493 assert(tombstones.length >= 1);
1494 } else if (operation === 'deleteFile') {
1495 request_path = '/v1/stores/' + datastore_id + '/files?path=' + escape(sanitizePath(path)) + '&device_pubkeys=' + device_pubkeys + '&device_ids=' + device_list + '&blockchain_id=' + ds.blockchain_id;
1496 http_operation = 'DELETE';
1497
1498 assert(inodes.length === 1);
1499 assert(tombstones.length >= 1);
1500 } else {
1501 console.log('invalid operation ' + operation);
1502 throw new Error('Invalid operation ' + operation);
1503 }
1504
1505 var options = {
1506 'method': http_operation,
1507 'host': ds.host,
1508 'port': ds.port,
1509 'path': request_path
1510 };
1511
1512 if (ds.session_token) {
1513 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1514 }
1515
1516 var datastore_str = JSON.stringify(ds.datastore);
1517 var datastore_sig = (0, _inode.signRawData)(datastore_str, datastore_privkey);
1518
1519 var body_struct = {
1520 'inodes': inodes,
1521 'payloads': payloads,
1522 'signatures': signatures,
1523 'tombstones': tombstones,
1524 'datastore_str': datastore_str,
1525 'datastore_sig': datastore_sig
1526 };
1527
1528 var body = JSON.stringify(body_struct);
1529 options['headers']['Content-Type'] = 'application/json';
1530 options['headers']['Content-Length'] = body.length;
1531
1532 return httpRequest(options, _schemas.SUCCESS_FAIL_SCHEMA, body).then(function (response) {
1533 if (response.error || response.errno) {
1534 var errorMsg = response.error || 'UNKNOWN';
1535 var errorNo = response.errno || 'UNKNOWN';
1536 throw new Error('Failed to ' + operation + ' ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1537 } else {
1538 return true;
1539 }
1540 });
1541}
1542
1543/*
1544 * Given a path, get its parent directory
1545 * Make sure it's a directory.
1546 *
1547 * @param ds (Object) a datastore context
1548 * @param path (String) the path to the inode in question
1549 * @param opts (Object) lookup options
1550 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1551 * .force (Bool) if True, then ignore stale inode errors.
1552 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1553 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1554 *
1555 * Asynchronous; returns a Promise
1556 */
1557function getParent(path) {
1558 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1559
1560 var dirpath = dirname(path);
1561 return getInode(dirpath, opts).then(function (inode) {
1562 if (!inode) {
1563 return { 'error': 'Failed to get parent', 'errno': EREMOTEIO };
1564 }
1565 if (inode.type !== _schemas.MUTABLE_DATUM_DIR_TYPE) {
1566 return { 'error': 'Not a directory', 'errno': ENOTDIR };
1567 } else {
1568 return inode;
1569 }
1570 }, function (error_resp) {
1571 return { 'error': 'Failed to get inode', 'errno': EREMOTEIO };
1572 });
1573}
1574
1575/*
1576 * Create or update a file
1577 *
1578 * @param ds (Object) a datastore context
1579 * @param path (String) the path to the file to create (must not exist)
1580 * @param file_buffer (Buffer or String) the file contents
1581 * @param opts (Object) lookup options
1582 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1583 * .force (Bool) if True, then ignore stale inode errors.
1584 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1585 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1586 *
1587 * Asynchronous; returns a Promise
1588 */
1589function putFile(path, file_buffer) {
1590 var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1591
1592
1593 var blockchain_id = opts.blockchain_id;
1594
1595 if (!opts.blockchain_id) {
1596 blockchain_id = getSessionBlockchainID();
1597 }
1598
1599 return datastoreMountOrCreate().then(function (ds) {
1600
1601 assert(ds);
1602
1603 var datastore_id = ds.datastore_id;
1604 var device_id = ds.device_id;
1605 var privkey_hex = ds.privkey_hex;
1606
1607 path = sanitizePath(path);
1608 var child_name = basename(path);
1609
1610 assert(typeof file_buffer === 'string' || file_buffer instanceof Buffer);
1611
1612 // get parent dir
1613 return getParent(path, opts).then(function (parent_dir) {
1614 if (parent_dir.error) {
1615 return parent_dir;
1616 }
1617
1618 // make the file inode information
1619 var file_payload = file_buffer;
1620 var file_hash = null;
1621 if (typeof file_payload !== 'string') {
1622 // buffer
1623 file_payload = file_buffer.toString('base64');
1624 file_hash = (0, _inode.hashDataPayload)(file_buffer.toString());
1625 } else {
1626 // string
1627 file_payload = Buffer.from(file_buffer).toString('base64');
1628 file_hash = (0, _inode.hashDataPayload)(file_buffer);
1629 }
1630
1631 assert(file_hash);
1632
1633 var inode_uuid = null;
1634 var new_parent_dir_inode = null;
1635 var child_version = null;
1636
1637 // new or existing?
1638 if (Object.keys(parent_dir['idata']['children']).includes(child_name)) {
1639
1640 // existing; no directory change
1641 inode_uuid = parent_dir['idata']['children'][child_name]['uuid'];
1642 new_parent_dir_inode = (0, _inode.inodeDirLink)(parent_dir, _schemas.MUTABLE_DATUM_FILE_TYPE, child_name, inode_uuid, true);
1643 } else {
1644
1645 // new
1646 inode_uuid = uuid4();
1647 new_parent_dir_inode = (0, _inode.inodeDirLink)(parent_dir, _schemas.MUTABLE_DATUM_FILE_TYPE, child_name, inode_uuid, false);
1648 }
1649
1650 var version = (0, _inode.getChildVersion)(parent_dir, child_name);
1651 var inode_info = (0, _inode.makeFileInodeBlob)(datastore_id, datastore_id, inode_uuid, file_hash, device_id, version);
1652 var inode_sig = (0, _inode.signDataPayload)(inode_info['header'], privkey_hex);
1653
1654 // make the directory inode information
1655 var new_parent_info = (0, _inode.makeDirInodeBlob)(datastore_id, new_parent_dir_inode['owner'], new_parent_dir_inode['uuid'], new_parent_dir_inode['idata']['children'], device_id, new_parent_dir_inode['version'] + 1);
1656 var new_parent_sig = (0, _inode.signDataPayload)(new_parent_info['header'], privkey_hex);
1657
1658 // post them
1659 var new_parent_info_b64 = new Buffer(new_parent_info['idata']).toString('base64');
1660 return datastoreOperation(ds, 'putFile', path, [inode_info['header'], new_parent_info['header']], [file_payload, new_parent_info_b64], [inode_sig, new_parent_sig], []);
1661 });
1662 });
1663}
1664
1665/*
1666 * Create a directory.
1667 *
1668 * @param ds (Object) datastore context
1669 * @param path (String) path to the directory
1670 * @param opts (object) optional arguments
1671 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1672 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1673 *
1674 * Asynchronous; returns a Promise
1675 */
1676function mkdir(path) {
1677 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1678
1679
1680 var blockchain_id = opts.blockchain_id;
1681
1682 if (!opts.blockchain_id) {
1683 blockchain_id = getSessionBlockchainID();
1684 }
1685
1686 return datastoreMountOrCreate().then(function (ds) {
1687
1688 assert(ds);
1689
1690 var datastore_id = ds.datastore_id;
1691 var device_id = ds.device_id;
1692 var privkey_hex = ds.privkey_hex;
1693
1694 path = sanitizePath(path);
1695 var child_name = basename(path);
1696
1697 return getParent(path, opts).then(function (parent_dir) {
1698 if (parent_dir.error) {
1699 return parent_dir;
1700 }
1701
1702 // must not exist
1703 if (Object.keys(parent_dir['idata']['children']).includes(child_name)) {
1704 return { 'error': 'File or directory exists', 'errno': EEXIST };
1705 }
1706
1707 // make the directory inode information
1708 var inode_uuid = uuid4();
1709 var inode_info = (0, _inode.makeDirInodeBlob)(datastore_id, datastore_id, inode_uuid, {}, device_id);
1710 var inode_sig = (0, _inode.signDataPayload)(inode_info['header'], privkey_hex);
1711
1712 // make the new parent directory information
1713 var new_parent_dir_inode = (0, _inode.inodeDirLink)(parent_dir, _schemas.MUTABLE_DATUM_DIR_TYPE, child_name, inode_uuid);
1714 var new_parent_info = (0, _inode.makeDirInodeBlob)(datastore_id, new_parent_dir_inode['owner'], new_parent_dir_inode['uuid'], new_parent_dir_inode['idata']['children'], device_id, new_parent_dir_inode['version'] + 1);
1715 var new_parent_sig = (0, _inode.signDataPayload)(new_parent_info['header'], privkey_hex);
1716
1717 // post them
1718 return datastoreOperation(ds, 'mkdir', path, [inode_info['header'], new_parent_info['header']], [inode_info['idata'], new_parent_info['idata']], [inode_sig, new_parent_sig], []);
1719 });
1720 });
1721}
1722
1723/*
1724 * Delete a file
1725 *
1726 * @param ds (Object) datastore context
1727 * @param path (String) path to the directory
1728 * @param opts (Object) options for this call
1729 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1730 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1731 *
1732 * Asynchronous; returns a Promise
1733 */
1734function deleteFile(path) {
1735 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1736
1737
1738 var blockchain_id = opts.blockchain_id;
1739
1740 if (!opts.blockchain_id) {
1741 blockchain_id = getSessionBlockchainID();
1742 }
1743
1744 return datastoreMountOrCreate().then(function (ds) {
1745
1746 assert(ds);
1747
1748 var datastore_id = ds.datastore_id;
1749 var device_id = ds.device_id;
1750 var privkey_hex = ds.privkey_hex;
1751 var all_device_ids = ds.datastore.device_ids;
1752
1753 path = sanitizePath(path);
1754 var child_name = basename(path);
1755
1756 return getParent(path, opts).then(function (parent_dir) {
1757 if (parent_dir.error) {
1758 return parent_dir;
1759 }
1760
1761 // no longer exists?
1762 if (!Object.keys(parent_dir['idata']['children']).includes(child_name)) {
1763 return { 'error': 'No such file or directory', 'errno': ENOENT };
1764 }
1765
1766 var inode_uuid = parent_dir['idata']['children'][child_name]['uuid'];
1767
1768 // unlink
1769 var new_parent_dir_inode = (0, _inode.inodeDirUnlink)(parent_dir, child_name);
1770 var new_parent_info = (0, _inode.makeDirInodeBlob)(datastore_id, new_parent_dir_inode['owner'], new_parent_dir_inode['uuid'], new_parent_dir_inode['idata']['children'], device_id, new_parent_dir_inode['version'] + 1);
1771 var new_parent_sig = (0, _inode.signDataPayload)(new_parent_info['header'], privkey_hex);
1772
1773 // make tombstones
1774 var tombstones = (0, _inode.makeInodeTombstones)(datastore_id, inode_uuid, all_device_ids);
1775 var signed_tombstones = (0, _inode.signMutableDataTombstones)(tombstones, privkey_hex);
1776
1777 // post them
1778 return datastoreOperation(ds, 'deleteFile', path, [new_parent_info['header']], [new_parent_info['idata']], [new_parent_sig], signed_tombstones);
1779 });
1780 });
1781}
1782
1783/*
1784 * Remove a directory
1785 *
1786 * @param ds (Object) datastore context
1787 * @param path (String) path to the directory
1788 * @param opts (Object) options for this call
1789 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1790 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1791 *
1792 * Asynchronous; returns a Promise
1793 */
1794function rmdir(path) {
1795 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1796
1797
1798 var blockchain_id = opts.blockchain_id;
1799
1800 if (!opts.blockchain_id) {
1801 blockchain_id = getSessionBlockchainID();
1802 }
1803
1804 return datastoreMountOrCreate().then(function (ds) {
1805
1806 assert(ds);
1807
1808 var datastore_id = ds.datastore_id;
1809 var device_id = ds.device_id;
1810 var privkey_hex = ds.privkey_hex;
1811 var all_device_ids = ds.datastore.device_ids;
1812
1813 path = sanitizePath(path);
1814 var child_name = basename(path);
1815
1816 return getParent(path, opts).then(function (parent_dir) {
1817 if (parent_dir.error) {
1818 return parent_dir;
1819 }
1820
1821 // no longer exists?
1822 if (!Object.keys(parent_dir['idata']['children']).includes(child_name)) {
1823 return { 'error': 'No such file or directory', 'errno': ENOENT };
1824 }
1825
1826 var inode_uuid = parent_dir['idata']['children'][child_name]['uuid'];
1827
1828 // unlink
1829 var new_parent_dir_inode = (0, _inode.inodeDirUnlink)(parent_dir, child_name);
1830 var new_parent_info = (0, _inode.makeDirInodeBlob)(datastore_id, new_parent_dir_inode['owner'], new_parent_dir_inode['uuid'], new_parent_dir_inode['idata']['children'], device_id, new_parent_dir_inode['version'] + 1);
1831 var new_parent_sig = (0, _inode.signDataPayload)(new_parent_info['header'], privkey_hex);
1832
1833 // make tombstones
1834 var tombstones = (0, _inode.makeInodeTombstones)(datastore_id, inode_uuid, all_device_ids);
1835 var signed_tombstones = (0, _inode.signMutableDataTombstones)(tombstones, privkey_hex);
1836
1837 // post them
1838 return datastoreOperation(ds, 'rmdir', path, [new_parent_info['header']], [new_parent_info['idata']], [new_parent_sig], signed_tombstones);
1839 });
1840 });
1841}
\No newline at end of file