UNPKG

60.8 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 if (!blockchain_id) {
540 blockchain_id = getSessionBlockchainID();
541 }
542
543 assert(blockchain_id);
544 assert(app_name);
545
546 // TODO: look up the datastore information via Core
547 // TODO: blocked by Core's lack of support for token files
548 // TODO: set device_id, blockchain_id, app_public_keys
549 }
550
551 if (!device_id) {
552 device_id = session.device_id;
553 assert(device_id);
554 }
555
556 if (!api_endpoint) {
557 api_endpoint = session.api_endpoint;
558 assert(api_endpoint);
559 }
560
561 if (!blockchain_id) {
562 blockchain_id = getBlockchainIDFromSessionOrDefault(session);
563 }
564
565 if (!app_public_keys) {
566 app_public_keys = session.app_public_keys;
567 assert(app_public_keys);
568 }
569
570 var blockstack_hostport = api_endpoint.split('://').reverse()[0];
571 var hostinfo = splitHostPort(blockstack_hostport);
572
573 var ctx = {
574 'host': hostinfo.host,
575 'port': hostinfo.port,
576 'blockchain_id': blockchain_id,
577 'device_id': device_id,
578 'datastore_id': datastore_id,
579 'session_token': sessionToken,
580 'app_public_keys': app_public_keys,
581 'session': session,
582 'datastore': null
583 };
584
585 if (data_privkey_hex) {
586 ctx.privkey_hex = data_privkey_hex;
587 }
588
589 var options = {
590 'method': 'GET',
591 'host': hostinfo.host,
592 'port': hostinfo.port,
593 'path': '/v1/stores/' + datastore_id + '?device_ids=' + device_id + '&blockchain_id=' + blockchain_id
594 };
595
596 options['headers'] = { 'Authorization': 'bearer ' + sessionToken };
597
598 return httpRequest(options, _schemas.DATASTORE_RESPONSE_SCHEMA).then(function (ds) {
599 if (!ds || ds.error) {
600 // ENOENT?
601 if (!ds || ds.errno === ENOENT) {
602 return null;
603 } else {
604 var errorMsg = ds.error || 'No response given';
605 throw new Error('Failed to get datastore: ' + errorMsg);
606 }
607 } else {
608 ctx['datastore'] = ds.datastore;
609
610 // save
611 setCachedMountContext(blockchain_id, ctx);
612
613 // this is required for testing purposes, since the core session token will not have been set
614 var _userData = getUserData();
615 if (!_userData.coreSessionToken) {
616 console.log("In test framework; saving session token");
617 _userData.coreSessionToken = sessionToken;
618 setUserData(_userData);
619 }
620
621 return ctx;
622 }
623 });
624}
625
626/*
627 * Get local storage object for Blockstack
628 * Throws on error
629 */
630function getUserData() {
631 var userData = localStorage.getItem(LOCAL_STORAGE_ID);
632 if (userData === null) {
633 userData = '{}';
634 }
635
636 userData = JSON.parse(userData);
637 return userData;
638}
639
640/*
641 * Save local storage
642 */
643function setUserData(userData) {
644 localStorage.setItem(LOCAL_STORAGE_ID, JSON.stringify(userData));
645}
646
647/*
648 * Get a cached app-specific datastore mount context for a given blockchain ID and application
649 * Return null if not found
650 * Throws on error
651 */
652function getCachedMountContext(blockchain_id) {
653
654 var userData = getUserData();
655 if (!userData.datastore_contexts) {
656 console.log("No datastore contexts defined");
657 return null;
658 }
659
660 if (!userData.datastore_contexts[blockchain_id]) {
661 console.log('No datastore contexts for ' + blockchain_id);
662 return null;
663 }
664
665 var ctx = userData.datastore_contexts[blockchain_id];
666 if (!ctx) {
667 console.log('Null datastore context for ' + blockchain_id);
668 return null;
669 }
670
671 return ctx;
672}
673
674/*
675 * Cache a mount context for a blockchain ID
676 */
677function setCachedMountContext(blockchain_id, datastore_context) {
678
679 var userData = getUserData();
680 if (!userData.datastore_contexts) {
681 userData.datastore_contexts = {};
682 }
683
684 userData.datastore_contexts[blockchain_id] = datastore_context;
685 setUserData(userData);
686}
687
688function getBlockchainIDFromSessionOrDefault(session) {
689 if (!session.blockchain_id) {
690 return (0, _inode.hashRawData)(Buffer.from(session.app_user_id).toString('base64'));
691 } else {
692 return session.blockchain_id;
693 }
694}
695
696/*
697 * Get the current session's blockchain ID
698 * Throw if not defined or not present.
699 */
700function getSessionBlockchainID() {
701
702 var userData = getUserData();
703 assert(userData);
704 assert(userData.coreSessionToken);
705
706 var session = jsontokens.decodeToken(userData.coreSessionToken).payload;
707
708 return getBlockchainIDFromSessionOrDefault(session);
709}
710
711/*
712 * Fulfill a replication strategy using the drivers available to us.
713 *
714 * replication_strategy (object): a dict that maps strategies (i.e. 'local', 'public', 'private') to integer counts
715 * classes (object): this is session.storage.classes (i.e. the driver classification; maps a driver name to its list of classes)
716 *
717 * Returns the list of drivers to use.
718 * Throws on error.
719 */
720function selectDrivers(replication_strategy, classes) {
721
722 // select defaults from classification and replication strategy
723 var driver_sets = []; // driver_sets[i] is the set of drivers that support SUPPORTED_STORAGE_CLASSES[i]
724 var driver_classes = {}; // map driver name to set of classes
725 var all_drivers = new Set([]); // set of all drivers available to us
726 var available_drivers = []; // drivers available to us
727 var selected_drivers = []; // drivers compatible with our replication strategy (return value)
728 var have_drivers = false; // whether or not we selected drivers that fulfill our replication strategy
729
730 for (var i = 0; i < SUPPORTED_STORAGE_CLASSES.length; i++) {
731 var driver_set = new Set(classes[SUPPORTED_STORAGE_CLASSES[i]]);
732 driver_sets.push(driver_set);
733
734 var _iteratorNormalCompletion3 = true;
735 var _didIteratorError3 = false;
736 var _iteratorError3 = undefined;
737
738 try {
739 for (var _iterator3 = driver_set[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
740 var d = _step3.value;
741
742 all_drivers.add(d);
743 }
744 } catch (err) {
745 _didIteratorError3 = true;
746 _iteratorError3 = err;
747 } finally {
748 try {
749 if (!_iteratorNormalCompletion3 && _iterator3.return) {
750 _iterator3.return();
751 }
752 } finally {
753 if (_didIteratorError3) {
754 throw _iteratorError3;
755 }
756 }
757 }
758
759 var _iteratorNormalCompletion4 = true;
760 var _didIteratorError4 = false;
761 var _iteratorError4 = undefined;
762
763 try {
764 for (var _iterator4 = driver_set[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
765 var _d = _step4.value;
766
767 console.log('Driver ' + _d + ' implementes ' + SUPPORTED_STORAGE_CLASSES[i]);
768 if (driver_classes[_d]) {
769 driver_classes[_d].push(SUPPORTED_STORAGE_CLASSES[i]);
770 } else {
771 driver_classes[_d] = [SUPPORTED_STORAGE_CLASSES[i]];
772 }
773 }
774 } catch (err) {
775 _didIteratorError4 = true;
776 _iteratorError4 = err;
777 } finally {
778 try {
779 if (!_iteratorNormalCompletion4 && _iterator4.return) {
780 _iterator4.return();
781 }
782 } finally {
783 if (_didIteratorError4) {
784 throw _iteratorError4;
785 }
786 }
787 }
788 }
789
790 var concern_fulfillment = {};
791
792 var _iteratorNormalCompletion5 = true;
793 var _didIteratorError5 = false;
794 var _iteratorError5 = undefined;
795
796 try {
797 for (var _iterator5 = all_drivers[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
798 var _d2 = _step5.value;
799
800 var _classes = driver_classes[_d2];
801
802 // a driver fits the replication strategy if all of its
803 // classes matches at least one concern (i.e. 'local', 'public')
804 var _iteratorNormalCompletion6 = true;
805 var _didIteratorError6 = false;
806 var _iteratorError6 = undefined;
807
808 try {
809 for (var _iterator6 = Object.keys(replication_strategy)[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
810 var concern = _step6.value;
811
812
813 var matches = false;
814 var _iteratorNormalCompletion7 = true;
815 var _didIteratorError7 = false;
816 var _iteratorError7 = undefined;
817
818 try {
819 for (var _iterator7 = _classes[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
820 var dclass = _step7.value;
821
822 if (REPLICATION_STRATEGY_CLASSES[concern].has(dclass)) {
823 matches = true;
824 break;
825 }
826 }
827 } catch (err) {
828 _didIteratorError7 = true;
829 _iteratorError7 = err;
830 } finally {
831 try {
832 if (!_iteratorNormalCompletion7 && _iterator7.return) {
833 _iterator7.return();
834 }
835 } finally {
836 if (_didIteratorError7) {
837 throw _iteratorError7;
838 }
839 }
840 }
841
842 if (matches) {
843 console.log('Driver ' + _d2 + ' fulfills replication concern ' + concern);
844
845 if (concern_fulfillment[concern]) {
846 concern_fulfillment[concern] += 1;
847 } else {
848 concern_fulfillment[concern] = 1;
849 }
850
851 if (concern_fulfillment[concern] <= replication_strategy[concern]) {
852 console.log('Select driver ' + _d2);
853 selected_drivers.push(_d2);
854 }
855 }
856
857 // strategy fulfilled?
858 var fulfilled = true;
859 var _iteratorNormalCompletion8 = true;
860 var _didIteratorError8 = false;
861 var _iteratorError8 = undefined;
862
863 try {
864 for (var _iterator8 = Object.keys(replication_strategy)[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
865 var _concern = _step8.value;
866
867 var count = 0;
868 if (concern_fulfillment[_concern]) {
869 count = concern_fulfillment[_concern];
870 }
871
872 if (count < replication_strategy[_concern]) {
873 fulfilled = false;
874 break;
875 }
876 }
877 } catch (err) {
878 _didIteratorError8 = true;
879 _iteratorError8 = err;
880 } finally {
881 try {
882 if (!_iteratorNormalCompletion8 && _iterator8.return) {
883 _iterator8.return();
884 }
885 } finally {
886 if (_didIteratorError8) {
887 throw _iteratorError8;
888 }
889 }
890 }
891
892 if (fulfilled) {
893 have_drivers = true;
894 break;
895 }
896 }
897 } catch (err) {
898 _didIteratorError6 = true;
899 _iteratorError6 = err;
900 } finally {
901 try {
902 if (!_iteratorNormalCompletion6 && _iterator6.return) {
903 _iterator6.return();
904 }
905 } finally {
906 if (_didIteratorError6) {
907 throw _iteratorError6;
908 }
909 }
910 }
911
912 if (have_drivers) {
913 break;
914 }
915 }
916 } catch (err) {
917 _didIteratorError5 = true;
918 _iteratorError5 = err;
919 } finally {
920 try {
921 if (!_iteratorNormalCompletion5 && _iterator5.return) {
922 _iterator5.return();
923 }
924 } finally {
925 if (_didIteratorError5) {
926 throw _iteratorError5;
927 }
928 }
929 }
930
931 if (!have_drivers) {
932 throw new Error("Unsatisfiable replication strategy");
933 }
934
935 return selected_drivers;
936}
937
938/*
939 * Connect to or create a datastore.
940 * Asynchronous, returns a Promise
941 *
942 * Returns a Promise that yields a datastore connection.
943 * Throws on error.
944 *
945 */
946function datastoreMountOrCreate() {
947 var replication_strategy = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { 'public': 1, 'local': 1 };
948 var sessionToken = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
949 var appPrivateKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
950
951
952 if (!sessionToken) {
953 var userData = getUserData();
954
955 sessionToken = userData.coreSessionToken;
956 assert(sessionToken);
957 }
958
959 // decode
960 var session = jsontokens.decodeToken(sessionToken).payload;
961 var blockchain_id = getBlockchainIDFromSessionOrDefault(session);
962
963 var ds = getCachedMountContext(blockchain_id);
964 if (ds) {
965 return new Promise(function (resolve, reject) {
966 resolve(ds);
967 });
968 }
969
970 // no cached datastore context.
971 // go ahead and create one (need appPrivateKey)
972 if (!appPrivateKey) {
973 var _userData2 = getUserData();
974
975 appPrivateKey = _userData2.appPrivateKey;
976 assert(appPrivateKey);
977 }
978
979 // sanity check
980 var _iteratorNormalCompletion9 = true;
981 var _didIteratorError9 = false;
982 var _iteratorError9 = undefined;
983
984 try {
985 for (var _iterator9 = Object.keys(replication_strategy)[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) {
986 var strategy = _step9.value;
987
988 var supported = false;
989 var _iteratorNormalCompletion10 = true;
990 var _didIteratorError10 = false;
991 var _iteratorError10 = undefined;
992
993 try {
994 for (var _iterator10 = Object.keys(REPLICATION_STRATEGY_CLASSES)[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) {
995 var supported_strategy = _step10.value;
996
997 if (supported_strategy === strategy) {
998 supported = true;
999 break;
1000 }
1001 }
1002 } catch (err) {
1003 _didIteratorError10 = true;
1004 _iteratorError10 = err;
1005 } finally {
1006 try {
1007 if (!_iteratorNormalCompletion10 && _iterator10.return) {
1008 _iterator10.return();
1009 }
1010 } finally {
1011 if (_didIteratorError10) {
1012 throw _iteratorError10;
1013 }
1014 }
1015 }
1016
1017 if (!supported) {
1018 throw new Error('Unsupported replication strategy ' + strategy);
1019 }
1020 }
1021 } catch (err) {
1022 _didIteratorError9 = true;
1023 _iteratorError9 = err;
1024 } finally {
1025 try {
1026 if (!_iteratorNormalCompletion9 && _iterator9.return) {
1027 _iterator9.return();
1028 }
1029 } finally {
1030 if (_didIteratorError9) {
1031 throw _iteratorError9;
1032 }
1033 }
1034 }
1035
1036 var drivers = null;
1037
1038 // find satisfactory storage drivers
1039 if (Object.keys(session.storage.preferences).includes(session.app_domain)) {
1040
1041 // app-specific preference
1042 drivers = session.storage.preferences[app_domain];
1043 } else {
1044
1045 // select defaults given the replication strategy
1046 drivers = selectDrivers(replication_strategy, session.storage.classes);
1047 }
1048
1049 var hostport = session.api_endpoint.split('://').reverse()[0];
1050 var appPublicKeys = session.app_public_keys;
1051 var deviceID = session.device_id;
1052 var allDeviceIDs = [];
1053
1054 for (var i = 0; i < appPublicKeys.length; i++) {
1055 allDeviceIDs.push(appPublicKeys[i].device_id);
1056 }
1057
1058 console.log('Will use drivers ' + drivers.join(','));
1059 console.log('Datastore will span devices ' + allDeviceIDs.join(','));
1060
1061 var datastoreOpts = {
1062 'appPrivateKey': appPrivateKey,
1063 'sessionToken': sessionToken
1064 };
1065
1066 return datastoreMount(datastoreOpts).then(function (datastore_ctx) {
1067 if (!datastore_ctx) {
1068 // does not exist
1069 console.log("Datastore does not exist; creating...");
1070
1071 var info = datastoreCreateRequest('datastore', appPrivateKey, drivers, deviceID, allDeviceIDs);
1072
1073 // go create it
1074 return datastoreCreate(hostport, sessionToken, info).then(function (res) {
1075 if (res.error) {
1076 console.log(error);
1077 var errorNo = res.errno || 'UNKNOWN';
1078 var errorMsg = res.error || 'UNKNOWN';
1079 throw new Error('Failed to create datastore (errno ' + errorNo + '): ' + errorMsg);
1080 }
1081
1082 // connect to it now
1083 return datastoreMount(datastoreOpts);
1084 });
1085 } else if (datastore_ctx.error) {
1086 // some other error
1087 var errorMsg = datastore_ctx.error || 'UNKNOWN';
1088 var errorNo = datastore_ctx.errno || 'UNKNOWN';
1089 throw new Error('Failed to access datastore (errno ' + errorNo + '): ' + errorMsg);
1090 } else {
1091 // exists
1092 return datastore_ctx;
1093 }
1094 });
1095}
1096
1097/*
1098 * Path lookup
1099 *
1100 * @param ds (Object) a datastore context
1101 * @param path (String) the path to the inode
1102 * @param opts (Object) optional arguments:
1103 * .extended (Bool) whether or not to include the entire path's inode information
1104 * .force (Bool) if True, then ignore stale inode errors.
1105 * .idata (Bool) if True, then get the inode payload as well
1106 * .blockchain_id (String) this is the blockchain ID of the datastore owner, if different from the session token
1107 * .ds (datastore context) if given, then use this datastore mount context instead of one from localstorage
1108 *
1109 * Returns a promise that resolves to a lookup response schema (or an extended lookup response schema, if opts.extended is set)
1110 */
1111function lookup(path) {
1112 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1113
1114
1115 var blockchain_id = opts.blockchain_id;
1116
1117 if (!opts.blockchain_id) {
1118 blockchain_id = getSessionBlockchainID();
1119 }
1120
1121 return datastoreMountOrCreate().then(function (ds) {
1122 assert(ds);
1123
1124 var datastore_id = ds.datastore_id;
1125 var device_list = getDeviceList(ds);
1126 var device_pubkeys = getPublicKeyList(ds);
1127 var options = {
1128 'method': 'GET',
1129 'host': ds.host,
1130 'port': ds.port,
1131 'path': '/v1/stores/' + datastore_id + '/inodes?path=' + escape(sanitizePath(path)) + '&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id
1132 };
1133
1134 if (!opts) {
1135 opts = {};
1136 }
1137
1138 var schema = _schemas.DATASTORE_LOOKUP_RESPONSE_SCHEMA;
1139
1140 if (opts.extended) {
1141 options['path'] += '&extended=1';
1142 schema = _schemas.DATASTORE_LOOKUP_EXTENDED_RESPONSE_SCHEMA;
1143 }
1144
1145 if (opts.force) {
1146 options['path'] += '&force=1';
1147 }
1148
1149 if (opts.idata) {
1150 options['idata'] += '&idata=1';
1151 }
1152
1153 return httpRequest(options, schema).then(function (lookup_response) {
1154 if (lookup_response.error || lookup_response.errno) {
1155 var errorMsg = lookup_response.error || 'UNKNOWN';
1156 var errorNo = lookup_response.errno || 'UNKNOWN';
1157 throw new Error('Failed to look up ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1158 } else {
1159 return lookup_response;
1160 }
1161 });
1162 });
1163}
1164
1165/*
1166 * List a directory.
1167 *
1168 * @param ds (Object) a datastore context
1169 * @param path (String) the path to the directory to list
1170 * @param opts (Object) optional arguments:
1171 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1172 * .force (Bool) if True, then ignore stale inode errors.
1173 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1174 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1175 *
1176 * Asynchronous; returns a Promise that resolves to either directory idata, or an extended mutable datum response (if opts.extended is set)
1177 */
1178function listdir(path) {
1179 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1180
1181
1182 var blockchain_id = opts.blockchain_id;
1183
1184 if (!opts.blockchain_id) {
1185 blockchain_id = getSessionBlockchainID();
1186 }
1187
1188 return datastoreMountOrCreate().then(function (ds) {
1189
1190 assert(ds);
1191
1192 var datastore_id = ds.datastore_id;
1193 var device_list = getDeviceList(ds);
1194 var device_pubkeys = getPublicKeyList(ds);
1195 var options = {
1196 'method': 'GET',
1197 'host': ds.host,
1198 'port': ds.port,
1199 '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
1200 };
1201
1202 var schema = _schemas.MUTABLE_DATUM_DIR_IDATA_SCHEMA;
1203
1204 if (!opts) {
1205 opts = {};
1206 }
1207
1208 if (opts.extended) {
1209 options['path'] += '&extended=1';
1210 schema = MUTABLE_DATUM_EXTENDED_RESPONSE_SCHEMA;
1211 }
1212
1213 if (opts.force) {
1214 optsion['path'] += '&force=1';
1215 }
1216
1217 if (ds.session_token) {
1218 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1219 }
1220
1221 return httpRequest(options, schema).then(function (response) {
1222 if (response.error || response.errno) {
1223 var errorMsg = response.error || 'UNKNOWN';
1224 var errorNo = response.errno || 'UNKNOWN';
1225 throw new Error('Failed to listdir ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1226 } else {
1227 return response;
1228 }
1229 });
1230 });
1231}
1232
1233/*
1234 * Stat a file or directory (i.e. get the inode header)
1235 *
1236 * @param ds (Object) a datastore context
1237 * @param path (String) the path to the directory to list
1238 * @param opts (Object) optional arguments:
1239 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1240 * .force (Bool) if True, then ignore stale inode errors.
1241 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1242 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1243 *
1244 * Asynchronous; returns a Promise that resolves to either an inode schema, or a mutable datum extended response schema (if opts.extended is set)
1245 */
1246function stat(path) {
1247 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1248
1249
1250 var ds = opts.ds;
1251 var blockchain_id = opts.blockchain_id;
1252
1253 if (!opts.blockchain_id) {
1254 blockchain_id = getSessionBlockchainID();
1255 }
1256
1257 return datastoreMountOrCreate().then(function (ds) {
1258
1259 assert(ds);
1260
1261 var datastore_id = ds.datastore_id;
1262 var device_list = getDeviceList(ds);
1263 var device_pubkeys = getPublicKeyList(ds);
1264 var options = {
1265 'method': 'GET',
1266 'host': ds.host,
1267 'port': ds.port,
1268 'path': '/v1/stores/' + datastore_id + '/inodes?path=' + escape(sanitizePath(path)) + '&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id
1269 };
1270
1271 var schema = _schemas.MUTABLE_DATUM_INODE_SCHEMA;
1272
1273 if (!opts) {
1274 opts = {};
1275 }
1276
1277 if (opts.extended) {
1278 options['path'] += '&extended=1';
1279 schema = MUTABLE_DATUM_EXTENDED_RESPONSE_SCHEMA;
1280 }
1281
1282 if (opts.force) {
1283 optsion['path'] += '&force=1';
1284 }
1285
1286 if (ds.session_token) {
1287 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1288 }
1289
1290 return httpRequest(options, schema).then(function (response) {
1291 if (response.error || response.errno) {
1292 var errorMsg = response.error || 'UNKNOWN';
1293 var errorNo = response.errno || 'UNKNOWN';
1294 throw new Error('Failed to stat ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1295 } else {
1296 return response;
1297 }
1298 });
1299 });
1300}
1301
1302/*
1303 * Get an undifferentiated file or directory and its data.
1304 * Low-level method, not meant for external consumption.
1305 *
1306 * @param ds (Object) a datastore context
1307 * @param path (String) the path to the directory to list
1308 * @param opts (Object) optional arguments:
1309 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1310 * .force (Bool) if True, then ignore stale inode errors.
1311 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1312 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1313 *
1314 * Asynchronous; returns a Promise that resolves to an inode and its data, or an extended mutable datum response (if opts.extended is set)
1315 */
1316function getInode(path) {
1317 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1318
1319
1320 var blockchain_id = opts.blockchain_id;
1321
1322 if (!opts.blockchain_id) {
1323 blockchain_id = getSessionBlockchainID();
1324 }
1325
1326 return datastoreMountOrCreate().then(function (ds) {
1327
1328 assert(ds);
1329
1330 var datastore_id = ds.datastore_id;
1331 var device_list = getDeviceList(ds);
1332 var device_pubkeys = getPublicKeyList(ds);
1333 var options = {
1334 'method': 'GET',
1335 'host': ds.host,
1336 'port': ds.port,
1337 '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
1338 };
1339
1340 var schema = _schemas.MUTABLE_DATUM_INODE_SCHEMA;
1341
1342 if (!opts) {
1343 opts = {};
1344 }
1345
1346 if (opts.extended) {
1347 options['path'] += '&extended=1';
1348 schema = MUTABLE_DATUM_EXTENDED_RESPONSE_SCHEMA;
1349 }
1350
1351 if (opts.force) {
1352 options['path'] += '&force=1';
1353 }
1354
1355 if (ds.session_token) {
1356 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1357 }
1358
1359 return httpRequest(options, schema).then(function (response) {
1360 if (response.error || response.errno) {
1361 var errorMsg = response.error || 'UNKNOWN';
1362 var errorNo = response.errno || 'UNKNOWN';
1363 throw new Error('Failed to getInode ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1364 } else {
1365 return response;
1366 }
1367 });
1368 });
1369}
1370
1371/*
1372 * Get a file.
1373 *
1374 * @param ds (Object) a datastore context
1375 * @param path (String) the path to the file to read
1376 * @param opts (Object) optional arguments:
1377 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1378 * .force (Bool) if True, then ignore stale inode errors.
1379 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1380 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1381 *
1382 * Asynchronous; returns a Promise that resolves to either raw data, or an extended mutable data response schema (if opts.extended is set).
1383 * If the file does not exist, then the Promise resolves to null. Any other errors result in an Error being thrown.
1384 */
1385function getFile(path) {
1386 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1387
1388
1389 var blockchain_id = opts.blockchain_id;
1390
1391 if (!opts.blockchain_id) {
1392 blockchain_id = getSessionBlockchainID();
1393 }
1394
1395 return datastoreMountOrCreate().then(function (ds) {
1396 assert(ds);
1397
1398 var datastore_id = ds.datastore_id;
1399 var device_list = getDeviceList(ds);
1400 var device_pubkeys = getPublicKeyList(ds);
1401 var options = {
1402 'method': 'GET',
1403 'host': ds.host,
1404 'port': ds.port,
1405 '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
1406 };
1407
1408 var schema = 'bytes';
1409
1410 if (!opts) {
1411 opts = {};
1412 }
1413
1414 if (opts.extended) {
1415 options['path'] += '&extended=1';
1416 schema = MUTABLE_DATUM_EXTENDED_RESPONSE_SCHEMA;
1417 }
1418
1419 if (opts.force) {
1420 options['path'] += '&force=1';
1421 }
1422
1423 if (ds.session_token) {
1424 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1425 }
1426
1427 return httpRequest(options, schema).then(function (response) {
1428 if (response.error || response.errno) {
1429 // ENOENT?
1430 if (response.errno === ENOENT) {
1431 return null;
1432 }
1433
1434 // some other error
1435 var errorMsg = response.error || 'UNKNOWN';
1436 var errorNo = response.errno || 'UNKNOWN';
1437 throw new Error('Failed to getFile ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1438 } else {
1439 return response;
1440 }
1441 });
1442 });
1443}
1444
1445/*
1446 * Execute a datastore operation
1447 *
1448 * @param ds (Object) a datastore context
1449 * @param operation (String) the specific operation being carried out.
1450 * @param path (String) the path of the operation
1451 * @param inodes (Array) the list of inode headers to replicate
1452 * @param payloads (Array) the list of inode payloads in 1-to-1 correspondence to the headers
1453 * @param signatures (Array) the list of signatures over each inode header (also 1-to-1 correspondence)
1454 * @param tombstones (Array) the list of signed inode tombstones
1455 *
1456 * Asynchronous; returns a Promise that resolves to True if the operation succeeded
1457 */
1458function datastoreOperation(ds, operation, path, inodes, payloads, signatures, tombstones) {
1459
1460 var request_path = null;
1461 var http_operation = null;
1462 var datastore_id = ds.datastore_id;
1463 var datastore_privkey = ds.privkey_hex;
1464 var device_list = getDeviceList(ds);
1465 var device_pubkeys = getPublicKeyList(ds);
1466
1467 assert(inodes.length === payloads.length);
1468 assert(payloads.length === signatures.length);
1469
1470 if (operation === 'mkdir') {
1471 request_path = '/v1/stores/' + datastore_id + '/directories?path=' + escape(sanitizePath(path)) + '&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id;
1472 http_operation = 'POST';
1473
1474 assert(inodes.length === 2);
1475 } else if (operation === 'putFile') {
1476 request_path = '/v1/stores/' + datastore_id + '/files?path=' + escape(sanitizePath(path)) + '&device_ids=' + device_list + '&device_pubkeys=' + device_pubkeys + '&blockchain_id=' + ds.blockchain_id;
1477 http_operation = 'PUT';
1478
1479 assert(inodes.length === 1 || inodes.length === 2);
1480 } else if (operation === 'rmdir') {
1481 request_path = '/v1/stores/' + datastore_id + '/directories?path=' + escape(sanitizePath(path)) + '&device_pubkeys=' + device_pubkeys + '&device_ids=' + device_list + '&blockchain_id=' + ds.blockchain_id;
1482 http_operation = 'DELETE';
1483
1484 assert(inodes.length === 1);
1485 assert(tombstones.length >= 1);
1486 } else if (operation === 'deleteFile') {
1487 request_path = '/v1/stores/' + datastore_id + '/files?path=' + escape(sanitizePath(path)) + '&device_pubkeys=' + device_pubkeys + '&device_ids=' + device_list + '&blockchain_id=' + ds.blockchain_id;
1488 http_operation = 'DELETE';
1489
1490 assert(inodes.length === 1);
1491 assert(tombstones.length >= 1);
1492 } else {
1493 console.log('invalid operation ' + operation);
1494 throw new Error('Invalid operation ' + operation);
1495 }
1496
1497 var options = {
1498 'method': http_operation,
1499 'host': ds.host,
1500 'port': ds.port,
1501 'path': request_path
1502 };
1503
1504 if (ds.session_token) {
1505 options['headers'] = { 'Authorization': 'bearer ' + ds.session_token };
1506 }
1507
1508 var datastore_str = JSON.stringify(ds.datastore);
1509 var datastore_sig = (0, _inode.signRawData)(datastore_str, datastore_privkey);
1510
1511 var body_struct = {
1512 'inodes': inodes,
1513 'payloads': payloads,
1514 'signatures': signatures,
1515 'tombstones': tombstones,
1516 'datastore_str': datastore_str,
1517 'datastore_sig': datastore_sig
1518 };
1519
1520 var body = JSON.stringify(body_struct);
1521 options['headers']['Content-Type'] = 'application/json';
1522 options['headers']['Content-Length'] = body.length;
1523
1524 return httpRequest(options, _schemas.SUCCESS_FAIL_SCHEMA, body).then(function (response) {
1525 if (response.error || response.errno) {
1526 var errorMsg = response.error || 'UNKNOWN';
1527 var errorNo = response.errno || 'UNKNOWN';
1528 throw new Error('Failed to ' + operation + ' ' + path + ' (errno: ' + errorNo + '): ' + errorMsg);
1529 } else {
1530 return true;
1531 }
1532 });
1533}
1534
1535/*
1536 * Given a path, get its parent directory
1537 * Make sure it's a directory.
1538 *
1539 * @param ds (Object) a datastore context
1540 * @param path (String) the path to the inode in question
1541 * @param opts (Object) lookup options
1542 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1543 * .force (Bool) if True, then ignore stale inode errors.
1544 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1545 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1546 *
1547 * Asynchronous; returns a Promise
1548 */
1549function getParent(path) {
1550 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1551
1552 var dirpath = dirname(path);
1553 return getInode(dirpath, opts).then(function (inode) {
1554 if (!inode) {
1555 return { 'error': 'Failed to get parent', 'errno': EREMOTEIO };
1556 }
1557 if (inode.type !== _schemas.MUTABLE_DATUM_DIR_TYPE) {
1558 return { 'error': 'Not a directory', 'errno': ENOTDIR };
1559 } else {
1560 return inode;
1561 }
1562 }, function (error_resp) {
1563 return { 'error': 'Failed to get inode', 'errno': EREMOTEIO };
1564 });
1565}
1566
1567/*
1568 * Create or update a file
1569 *
1570 * @param ds (Object) a datastore context
1571 * @param path (String) the path to the file to create (must not exist)
1572 * @param file_buffer (Buffer or String) the file contents
1573 * @param opts (Object) lookup options
1574 * .extended (Bool) whether or not to include the entire path's inode inforamtion
1575 * .force (Bool) if True, then ignore stale inode errors.
1576 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1577 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1578 *
1579 * Asynchronous; returns a Promise
1580 */
1581function putFile(path, file_buffer) {
1582 var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1583
1584
1585 var blockchain_id = opts.blockchain_id;
1586
1587 if (!opts.blockchain_id) {
1588 blockchain_id = getSessionBlockchainID();
1589 }
1590
1591 return datastoreMountOrCreate().then(function (ds) {
1592
1593 assert(ds);
1594
1595 var datastore_id = ds.datastore_id;
1596 var device_id = ds.device_id;
1597 var privkey_hex = ds.privkey_hex;
1598
1599 path = sanitizePath(path);
1600 var child_name = basename(path);
1601
1602 assert(typeof file_buffer === 'string' || file_buffer instanceof Buffer);
1603
1604 // get parent dir
1605 return getParent(path, opts).then(function (parent_dir) {
1606 if (parent_dir.error) {
1607 return parent_dir;
1608 }
1609
1610 // make the file inode information
1611 var file_payload = file_buffer;
1612 var file_hash = null;
1613 if (typeof file_payload !== 'string') {
1614 // buffer
1615 file_payload = file_buffer.toString('base64');
1616 file_hash = (0, _inode.hashDataPayload)(file_buffer.toString());
1617 } else {
1618 // string
1619 file_payload = Buffer.from(file_buffer).toString('base64');
1620 file_hash = (0, _inode.hashDataPayload)(file_buffer);
1621 }
1622
1623 assert(file_hash);
1624
1625 var inode_uuid = null;
1626 var new_parent_dir_inode = null;
1627 var child_version = null;
1628
1629 // new or existing?
1630 if (Object.keys(parent_dir['idata']['children']).includes(child_name)) {
1631
1632 // existing; no directory change
1633 inode_uuid = parent_dir['idata']['children'][child_name]['uuid'];
1634 new_parent_dir_inode = (0, _inode.inodeDirLink)(parent_dir, _schemas.MUTABLE_DATUM_FILE_TYPE, child_name, inode_uuid, true);
1635 } else {
1636
1637 // new
1638 inode_uuid = uuid4();
1639 new_parent_dir_inode = (0, _inode.inodeDirLink)(parent_dir, _schemas.MUTABLE_DATUM_FILE_TYPE, child_name, inode_uuid, false);
1640 }
1641
1642 var version = (0, _inode.getChildVersion)(parent_dir, child_name);
1643 var inode_info = (0, _inode.makeFileInodeBlob)(datastore_id, datastore_id, inode_uuid, file_hash, device_id, version);
1644 var inode_sig = (0, _inode.signDataPayload)(inode_info['header'], privkey_hex);
1645
1646 // make the directory inode information
1647 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);
1648 var new_parent_sig = (0, _inode.signDataPayload)(new_parent_info['header'], privkey_hex);
1649
1650 // post them
1651 var new_parent_info_b64 = new Buffer(new_parent_info['idata']).toString('base64');
1652 return datastoreOperation(ds, 'putFile', path, [inode_info['header'], new_parent_info['header']], [file_payload, new_parent_info_b64], [inode_sig, new_parent_sig], []);
1653 });
1654 });
1655}
1656
1657/*
1658 * Create a directory.
1659 *
1660 * @param ds (Object) datastore context
1661 * @param path (String) path to the directory
1662 * @param opts (object) optional arguments
1663 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1664 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1665 *
1666 * Asynchronous; returns a Promise
1667 */
1668function mkdir(path) {
1669 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1670
1671
1672 var blockchain_id = opts.blockchain_id;
1673
1674 if (!opts.blockchain_id) {
1675 blockchain_id = getSessionBlockchainID();
1676 }
1677
1678 return datastoreMountOrCreate().then(function (ds) {
1679
1680 assert(ds);
1681
1682 var datastore_id = ds.datastore_id;
1683 var device_id = ds.device_id;
1684 var privkey_hex = ds.privkey_hex;
1685
1686 path = sanitizePath(path);
1687 var child_name = basename(path);
1688
1689 return getParent(path, opts).then(function (parent_dir) {
1690 if (parent_dir.error) {
1691 return parent_dir;
1692 }
1693
1694 // must not exist
1695 if (Object.keys(parent_dir['idata']['children']).includes(child_name)) {
1696 return { 'error': 'File or directory exists', 'errno': EEXIST };
1697 }
1698
1699 // make the directory inode information
1700 var inode_uuid = uuid4();
1701 var inode_info = (0, _inode.makeDirInodeBlob)(datastore_id, datastore_id, inode_uuid, {}, device_id);
1702 var inode_sig = (0, _inode.signDataPayload)(inode_info['header'], privkey_hex);
1703
1704 // make the new parent directory information
1705 var new_parent_dir_inode = (0, _inode.inodeDirLink)(parent_dir, _schemas.MUTABLE_DATUM_DIR_TYPE, child_name, inode_uuid);
1706 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);
1707 var new_parent_sig = (0, _inode.signDataPayload)(new_parent_info['header'], privkey_hex);
1708
1709 // post them
1710 return datastoreOperation(ds, 'mkdir', path, [inode_info['header'], new_parent_info['header']], [inode_info['idata'], new_parent_info['idata']], [inode_sig, new_parent_sig], []);
1711 });
1712 });
1713}
1714
1715/*
1716 * Delete a file
1717 *
1718 * @param ds (Object) datastore context
1719 * @param path (String) path to the directory
1720 * @param opts (Object) options for this call
1721 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1722 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1723 *
1724 * Asynchronous; returns a Promise
1725 */
1726function deleteFile(path) {
1727 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1728
1729
1730 var blockchain_id = opts.blockchain_id;
1731
1732 if (!opts.blockchain_id) {
1733 blockchain_id = getSessionBlockchainID();
1734 }
1735
1736 return datastoreMountOrCreate().then(function (ds) {
1737
1738 assert(ds);
1739
1740 var datastore_id = ds.datastore_id;
1741 var device_id = ds.device_id;
1742 var privkey_hex = ds.privkey_hex;
1743 var all_device_ids = ds.datastore.device_ids;
1744
1745 path = sanitizePath(path);
1746 var child_name = basename(path);
1747
1748 return getParent(path, opts).then(function (parent_dir) {
1749 if (parent_dir.error) {
1750 return parent_dir;
1751 }
1752
1753 // no longer exists?
1754 if (!Object.keys(parent_dir['idata']['children']).includes(child_name)) {
1755 return { 'error': 'No such file or directory', 'errno': ENOENT };
1756 }
1757
1758 var inode_uuid = parent_dir['idata']['children'][child_name]['uuid'];
1759
1760 // unlink
1761 var new_parent_dir_inode = (0, _inode.inodeDirUnlink)(parent_dir, child_name);
1762 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);
1763 var new_parent_sig = (0, _inode.signDataPayload)(new_parent_info['header'], privkey_hex);
1764
1765 // make tombstones
1766 var tombstones = (0, _inode.makeInodeTombstones)(datastore_id, inode_uuid, all_device_ids);
1767 var signed_tombstones = (0, _inode.signMutableDataTombstones)(tombstones, privkey_hex);
1768
1769 // post them
1770 return datastoreOperation(ds, 'deleteFile', path, [new_parent_info['header']], [new_parent_info['idata']], [new_parent_sig], signed_tombstones);
1771 });
1772 });
1773}
1774
1775/*
1776 * Remove a directory
1777 *
1778 * @param ds (Object) datastore context
1779 * @param path (String) path to the directory
1780 * @param opts (Object) options for this call
1781 * .blockchain_id (string) this is the blockchain ID of the datastore owner (if different from the session)
1782 * .ds (datastore context) this is the mount context for the datastore, if different from one that we have cached
1783 *
1784 * Asynchronous; returns a Promise
1785 */
1786function rmdir(path) {
1787 var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1788
1789
1790 var blockchain_id = opts.blockchain_id;
1791
1792 if (!opts.blockchain_id) {
1793 blockchain_id = getSessionBlockchainID();
1794 }
1795
1796 return datastoreMountOrCreate().then(function (ds) {
1797
1798 assert(ds);
1799
1800 var datastore_id = ds.datastore_id;
1801 var device_id = ds.device_id;
1802 var privkey_hex = ds.privkey_hex;
1803 var all_device_ids = ds.datastore.device_ids;
1804
1805 path = sanitizePath(path);
1806 var child_name = basename(path);
1807
1808 return getParent(path, opts).then(function (parent_dir) {
1809 if (parent_dir.error) {
1810 return parent_dir;
1811 }
1812
1813 // no longer exists?
1814 if (!Object.keys(parent_dir['idata']['children']).includes(child_name)) {
1815 return { 'error': 'No such file or directory', 'errno': ENOENT };
1816 }
1817
1818 var inode_uuid = parent_dir['idata']['children'][child_name]['uuid'];
1819
1820 // unlink
1821 var new_parent_dir_inode = (0, _inode.inodeDirUnlink)(parent_dir, child_name);
1822 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);
1823 var new_parent_sig = (0, _inode.signDataPayload)(new_parent_info['header'], privkey_hex);
1824
1825 // make tombstones
1826 var tombstones = (0, _inode.makeInodeTombstones)(datastore_id, inode_uuid, all_device_ids);
1827 var signed_tombstones = (0, _inode.signMutableDataTombstones)(tombstones, privkey_hex);
1828
1829 // post them
1830 return datastoreOperation(ds, 'rmdir', path, [new_parent_info['header']], [new_parent_info['idata']], [new_parent_sig], signed_tombstones);
1831 });
1832 });
1833}
\No newline at end of file