UNPKG

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