UNPKG

blockstack-storage

Version:

The Blockstack Javascript library for storage.

987 lines (820 loc) 37.8 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.getFile = getFile; exports.putFile = putFile; exports.deleteFile = deleteFile; exports.listFiles = listFiles; exports.getFileURLs = getFileURLs; var _schemas = require('./schemas'); var _blob = require('./blob'); var _policy = require('./policy'); var _inode = require('./inode'); var _util = require('./util'); var _blockstack = require('blockstack'); var _datastore = require('./datastore'); var _requests = require('./requests'); var _metadata = require('./metadata'); var assert = require('assert'); var crypto = require('crypto'); var http = require('http'); var uuid4 = require('uuid/v4'); var bitcoinjs = require('bitcoinjs-lib'); var BigInteger = require('bigi'); var Promise = require('promise'); var jsontokens = require('jsontokens'); var urlparse = require('url'); /* * Get the device-specific root directory page * Need either blockchain_id and full_app_name, or datastore_id and data_pubkeys. * * @blockchain_id (string) the blockchain ID that owns the datastore * @full_app_name (string) the fully-qualified application name * @datastore_id (string) the datastore ID * @data_pubkeys (array) a list of {'device_id': ..., 'public_key': ...} objects * @force (boolean) if true, then tolerate stale data. * * Returns a Promise that resolves to the device root (an object that conforms to GET_DEVICE_ROOT_RESPONSE) */ function getDeviceRoot(device_id, opts) { var blockchain_id = opts.blockchain_id || null; var full_app_name = opts.full_app_name || null; var datastore_id = opts.datastore_id || null; var data_pubkeys = opts.data_pubkeys || null; var force = opts.force || false; assert(datastore_id && data_pubkeys || blockchain_id && full_app_name, 'Need either both datastore_id/data_pubkeys or full_app_name/blockchain_id'); return (0, _datastore.datastoreMount)({ 'blockchainID': blockchain_id, 'appName': full_app_name, 'datastoreID': datastore_id, 'dataPubkeys': data_pubkeys }).then(function (ds) { assert(ds, 'No datastore returned'); var reqinfo = (0, _datastore.datastoreRequestPathInfo)(ds); var options = { 'method': 'GET', 'host': reqinfo.host, 'port': reqinfo.port, 'path': '/v1/stores/' + reqinfo.store_id + '/device_roots?force=' + (force ? '1' : '0') + '&this_device_id=' + device_id + '&' + reqinfo.qs }; console.log('get_device_root: ' + options.path); return (0, _requests.httpRequest)(options, _schemas.GET_DEVICE_ROOT_RESPONSE).then(function (response) { if (response.error || response.errno) { // ENOENT? if (response.errno === 'ENOENT') { return response; } // some other error var errorMsg = response.error || 'UNKNOWN'; var errorNo = response.errno || 'UNKNOWN'; throw new Error('Failed to getFile ' + path + ' (errno: ' + errorNo + '): ' + errorMsg); } else { return response; } }); }); } /* * Get the entire datastore's root directory. * Need either blockchain_id and full_app_name, or datastore_id and data_pubkeys. * * opts: * @blockchain_id (string) the blockchain ID that owns the datastore * @full_app_name (string) the fully-qualified application name * @datastore_id (string) the datastore ID * @data_pubkeys (array) a list of {'device_id': ..., 'public_key': ...} objects * @force (boolean) if true, then tolerate stale data. * * Returns a Promise that resolves to the datastore root (an object that conforms to GET_ROOT_RESPONSE) */ function getRoot(opts) { var blockchain_id = opts.blockchain_id || null; var full_app_name = opts.full_app_name || null; var datastore_id = opts.datastore_id || null; var data_pubkeys = opts.data_pubkeys || null; var force = opts.force || false; assert(datastore_id && data_pubkeys || blockchain_id && full_app_name, 'Need either both datastore_id/data_pubkeys or full_app_name/blockchain_id'); return (0, _datastore.datastoreMount)({ 'blockchainID': blockchain_id, 'appName': full_app_name, 'datastoreID': datastore_id, 'dataPubkeys': data_pubkeys }).then(function (ds) { assert(ds, 'No datastore returned'); var reqinfo = (0, _datastore.datastoreRequestPathInfo)(ds); var options = { 'method': 'GET', 'host': reqinfo.host, 'port': reqinfo.port, 'path': '/v1/stores/' + reqinfo.store_id + '/listing?force=' + (force ? '1' : '0') + '&' + reqinfo.qs }; console.log('get_root: ' + options.path); return (0, _requests.httpRequest)(options, _schemas.GET_ROOT_RESPONSE).then(function (response) { if (response.error || response.errno) { // ENOENT? if (response.errno === 'ENOENT') { return response; } // some other error var errorMsg = response.error || 'UNKNOWN'; var errorNo = response.errno || 'UNKNOWN'; throw new Error('Failed to getFile ' + path + ' (errno: ' + errorNo + '): ' + errorMsg); } else { return response; } }); }); } /* * Get a file's header. * Need either blockchain_id or full_app_name, or datastore_id and data_pubkeys * * @file_name (string) the name of the file to query * @device_id (string) this device ID * * opts: * @blockchain_id (string) the blockchain ID that owns the datastore * @full_app_name (string) the fully-qualified application name * @datastore_id (string) the datastore ID * @data_pubkeys (array) a list of {'device_id': ..., 'public_key': ...} objects * @force (boolean) if true, then tolerate stale data. * * Returns a Promise that resolves to the file header (an object that conforms to FILE_LOOKUP_RESPONSE) */ function getFileHeader(file_name, device_id, opts) { var blockchain_id = opts.blockchain_id || null; var full_app_name = opts.full_app_name || null; var datastore_id = opts.datastore_id || null; var data_pubkeys = opts.data_pubkeys || null; var force = opts.force || false; return (0, _datastore.datastoreMount)({ 'blockchainID': blockchain_id, 'appName': full_app_name, 'datastoreID': datastore_id, 'dataPubkeys': data_pubkeys }).then(function (ds) { assert(ds, 'No datastore returned'); var reqinfo = (0, _datastore.datastoreRequestPathInfo)(ds); var options = { 'method': 'GET', 'host': reqinfo.host, 'port': reqinfo.port, 'path': '/v1/stores/' + reqinfo.store_id + '/headers?path=' + file_name + '&force=' + (force ? '1' : '0') + '&this_device_id=' + device_id + '&' + reqinfo.qs }; console.log('get_file_header: ' + options.path); return (0, _requests.httpRequest)(options, _schemas.FILE_LOOKUP_RESPONSE).then(function (response) { if (response.error || response.errno) { // ENOENT? if (response.errno === 'ENOENT') { return response; } // some other error var errorMsg = response.error || 'UNKNOWN'; var errorNo = response.errno || 'UNKNOWN'; throw new Error('Failed to getFile ' + path + ' (errno: ' + errorNo + '): ' + errorMsg); } else { return response; } }); }); } /* * Get raw file data. * Need either blockchain_id or full_app_name, or datastore_id and data_pubkeys * * @file_name (string) the name of the file to query * * opts: * @blockchain_id (string) the blockchain ID that owns the datastore * @full_app_name (string) the fully-qualified application name * @datastore_id (string) the datastore ID * @data_pubkeys (array) a list of {'device_id': ..., 'public_key': ...} objects * @force (boolean) if true, then tolerate stale data. * * Returns a Promise that resolves to the file data */ function getFileData(file_name, opts) { var blockchain_id = opts.blockchain_id || null; var full_app_name = opts.full_app_name || null; var datastore_id = opts.datastore_id || null; var data_pubkeys = opts.data_pubkeys || null; var force = opts.force || false; return (0, _datastore.datastoreMount)({ 'blockchainID': blockchain_id, 'appName': full_app_name, 'datastoreID': datastore_id, 'dataPubkeys': data_pubkeys }).then(function (ds) { assert(ds, 'No datastore returned'); var reqinfo = (0, _datastore.datastoreRequestPathInfo)(ds); var options = { 'method': 'GET', 'host': reqinfo.host, 'port': reqinfo.port, 'path': '/v1/stores/' + reqinfo.store_id + '/files?path=' + file_name + '&force=' + (force ? '1' : '0') + '&' + reqinfo.qs }; console.log('get_file: ' + options.path); return (0, _requests.httpRequest)(options, 'bytes').then(function (response) { if (response.error || response.errno) { // ENOENT? if (response.errno === 'ENOENT') { return null; } // some other error var errorMsg = response.error || 'UNKNOWN'; var errorNo = response.errno || 'UNKNOWN'; throw new Error('Failed to getFile ' + path + ' (errno: ' + errorNo + '): ' + errorMsg); } else { return response; } }); }); } /* * Create or put a whole file. * * @datastore_str (string) serialized mutable data blob encoding the datastore we're working on * @datastore_sig (string) the signature over datastore_str with this device's private key * @file_name (string) the name of this file * @file_header_blob (string) the serialized header for this file (with no URLs set) * @payload_b64 (string or Buffer) the raw data, base64-encoded * @signature (string) the signature over @file_header_blob * * opts: * @blockchain_id (string) the blockchain ID that owns the datastore * @full_app_name (string) the fully-qualified application name * @force (boolean) if true, then tolerate stale data. * * Returns a Promise that resolves to the list of URLs on success (PUT_DATA_RESPONSE) */ function putFileData(datastore_str, datastore_sig, file_name, file_header_blob, payload_b64, signature, opts) { var blockchain_id = opts.blockchain_id || null; var full_app_name = opts.full_app_name || null; var force = opts.force || null; var datastore = JSON.parse(datastore_str); var datastore_id = (0, _datastore.datastoreGetId)(datastore['pubkey']); return (0, _datastore.datastoreMount)({ 'blockchainID': blockchain_id, 'appName': full_app_name, 'datastoreID': datastore_id }).then(function (ds) { assert(ds, 'No datastore returned'); var reqinfo = (0, _datastore.datastoreRequestPathInfo)(ds); var options = { 'method': 'POST', 'host': reqinfo.host, 'port': reqinfo.port, 'path': '/v1/stores/' + datastore_id + '/files?path=' + file_name + '&' + reqinfo.qs, 'headers': { 'Authorization': 'bearer ' + (0, _metadata.getSessionToken)() } }; var request_body = { 'headers': [file_header_blob], 'payloads': [payload_b64], 'signatures': [signature], 'tombstones': [], 'datastore_str': datastore_str, 'datastore_sig': datastore_sig }; console.log('put_file: ' + options.path); var body = JSON.stringify(request_body); options['headers']['Content-Type'] = 'application/json'; options['headers']['Content-Length'] = body.length; return (0, _requests.httpRequest)(options, _schemas.PUT_DATA_RESPONSE, body).then(function (response) { if (response.error || response.errno) { var errorMsg = response.error || 'UNKNOWN'; var errorNo = response.errno || 'UNKNOWN'; throw new Error('Failed to put file ' + file_name + ' (errno: ' + errorNo + '): ' + errorMsg); } else { return response; } }); }); } /* * Put a new device root * * @datastore_str (string) serialized mutable data blob encoding the datastore we're working on * @datastore_sig (string) the signature over datastore_str with this device's private key * @device_root_page_blob (string) the mutable data blob containing the new device root * @signature (string) the signature over @device_root_page_blob * * opts: * @blockchain_id (string) the blockchain ID that owns the datastore * @full_app_name (string) the fully-qualified application name * @sync (boolean) if true, then write the device root synchronously * * Returns a Promise that resolves to the list of URLs to the replicas (PUT_DATA_RESPONSE) */ function putDeviceRoot(datastore_str, datastore_sig, device_root_page_blob, signature, opts) { var blockchain_id = opts.blockchain_id || (0, _metadata.getSessionBlockchainID)(); var full_app_name = opts.full_app_name || (0, _metadata.getSessionAppName)(); var sync = opts.sync || false; var datastore = JSON.parse(datastore_str); var datastore_id = (0, _datastore.datastoreGetId)(datastore['pubkey']); return (0, _datastore.datastoreMount)({ 'blockchainID': blockchain_id, 'appName': full_app_name, 'datastoreID': datastore_id }).then(function (ds) { assert(ds, 'No datastore returned'); var reqinfo = (0, _datastore.datastoreRequestPathInfo)(ds); var options = { 'method': 'POST', 'host': reqinfo.host, 'port': reqinfo.port, 'path': '/v1/stores/' + datastore_id + '/device_roots?sync=' + (sync ? '1' : '0') + '&' + reqinfo.qs, 'headers': { 'Authorization': 'bearer ' + (0, _metadata.getSessionToken)() } }; var request_body = { 'headers': [], 'payloads': [device_root_page_blob], 'signatures': [signature], 'tombstones': [], 'datastore_str': datastore_str, 'datastore_sig': datastore_sig }; console.log('put_device_root: ' + options.path); var body = JSON.stringify(request_body); options['headers']['Content-Type'] = 'application/json'; options['headers']['Content-Length'] = body.length; return (0, _requests.httpRequest)(options, _schemas.PUT_DATA_RESPONSE, body).then(function (response) { if (response.error || response.errno) { var errorMsg = response.error || 'UNKNOWN'; var errorNo = response.errno || 'UNKNOWN'; throw new Error('Failed to put device root (errno: ' + errorNo + '): ' + errorMsg); } else { return true; } }); }); } /* * Delete a whole file. * * @datastore_str (string) serialized mutable data blob encoding the datastore we're working on * @datastore_sig (string) the signature over datastore_str with this device's private key * @signed_tombstones (array) the list of signed tombstones for this file * * opts: * @blockchain_id (string) the blockchain ID that owns the datastore * @full_app_name (string) the fully-qualified application name * * Returns a Promise that resolves to true */ function deleteFileData(datastore_str, datastore_sig, signed_tombstones, opts) { var blockchain_id = opts.blockchain_id || null; var full_app_name = opts.full_app_name || null; var datastore = JSON.parse(datastore_str); var datastore_id = (0, _datastore.datastoreGetId)(datastore['pubkey']); return (0, _datastore.datastoreMount)({ 'blockchainID': blockchain_id, 'appName': full_app_name, 'datastoreID': datastore_id }).then(function (ds) { assert(ds, 'No datastore returned'); var reqinfo = (0, _datastore.datastoreRequestPathInfo)(ds); var options = { 'method': 'DELETE', 'host': reqinfo.host, 'port': reqinfo.port, 'path': '/v1/stores/' + datastore_id + '/files?' + reqinfo.qs, 'headers': { 'Authorization': 'bearer ' + (0, _metadata.getSessionToken)() } }; var request_body = { 'headers': [], 'payloads': [], 'signatures': [], 'tombstones': signed_tombstones, 'datastore_str': datastore_str, 'datastore_sig': datastore_sig }; console.log('delete_file: ' + options.path); var body = JSON.stringify(request_body); options['headers']['Content-Type'] = 'application/json'; options['headers']['Content-Length'] = body.length; return (0, _requests.httpRequest)(options, _schemas.SUCCESS_FAIL_SCHEMA, body).then(function (response) { if (response.error || response.errno) { var errorMsg = response.error || 'UNKNOWN'; var errorNo = response.errno || 'UNKNOWN'; throw new Error('Failed to put device root (errno: ' + errorNo + '): ' + errorMsg); } else { return true; } }); }); } /* * Get profile, zone file, and name record information * * @blockchain_id (string) the blockchain ID * * Returns a promise that resolves to {'profile': ..., 'name_record': ...,}, with either 'zoenfile' or 'zonefile_b64' defined */ function getProfileData(blockchain_id) { var sessionToken = (0, _metadata.getSessionToken)(); assert(sessionToken); var urlinfo = urlparse.parse(sessionToken.api_endpoint); var host = urlinfo.hostname; var port = urlinfo.port; var options = { 'method': 'GET', 'host': host, 'port': port, 'path': '/v1/names/' + blockchain_id + '/profile' }; console.log('get_profile: ' + options.path); return (0, _requests.httpRequest)(options, _schemas.GET_PROFILE_RESPONSE).then(function (response) { if (response.error || response.errno) { var errorMsg = response.error || 'UNKNOWN'; var errorNo = response.errno || 'UNKNOWN'; throw new Error('Failed to put device root (errno: ' + errorNo + '): ' + errorMsg); } else { return response; } }); } /* * Go look up the device root page * * @this_device_id (string): this device ID * * opts: * @blockchain_id (string): blockchain ID that owns the datastore * @full_app_name (string): name of the application that uses the datastore * @datastore_id (string): datastore ID * @data_pubkeys (array): array of device/datapubkey pairs * @force (boolean): tolerate stale data or not * * Returns a Promise that resolves to either: * {'status': True, 'device_root': ..., 'datastore': ..., 'created': true/false} * {'error': ..., 'errno': ...} */ function findDeviceRootInfo(this_device_id, opts) { var blockchain_id = opts.blockchain_id || null; var full_app_name = opts.full_app_name || null; var datastore_id = opts.datastore_id || null; var data_pubkeys = opts.data_pubkeys || null; var force = opts.force || false; // look up datastore info return findDatastoreInfo(this_device_id, { 'blockchain_id': blockchain_id, 'full_app_name': full_app_name, 'datastore_id': datastore_id, 'data_pubkeys': data_pubkeys, 'force': force }).then(function (dsinfo) { if (dsinfo['error'] || dsinfo['errno']) { return dsinfo; } var datastore = dsinfo['datastore']; var data_pubkey = dsinfo['data_pubkey']; if (!data_pubkeys) { data_pubkeys = dsinfo['data_pubkeys']; } var datastore_id = (0, _datastore.datastoreGetId)(datastore['pubkey']); var root_uuid = datastore['root_uuid']; var drivers = datastore['drivers']; // do we expect this device root to exist already? we might not, if this is the first time we're trying to modify the device root var expect_device_root = false; if ((0, _blockstack.decompressPublicKey)(datastore['pubkey']) === (0, _blockstack.decompressPublicKey)(data_pubkey)) { // we created this console.log('This device ' + this_device_id + ' created datastore ' + datastore_id + ', so we expect its root to exist'); expect_device_root = true; } var root_version = (0, _metadata.getDeviceRootVersion)(datastore_id, root_uuid, [this_device_id]); if (root_version > 0) { // previously seen or written console.log('This device ' + this_device_id + ' has seen version ' + root_version + ' for ' + datastore_id + ', so we expect the root to exist'); expect_device_root = true; } return getDeviceRoot(this_device_id, { blockchain_id: blockchain_id, full_app_name: full_app_name, datastore_id: datastore_id, data_pubkeys: data_pubkeys, force: force }).then(function (res) { var created = false; var device_root = null; if (res['error'] || res['errno']) { console.log('Failed to get device ' + this_device_id + ' root page for ' + datastore_id + '.' + root_uuid + ': ' + res['error']); if (expect_device_root) { return res; } else { console.log('Creating empty device root for ' + this_device_id); device_root = (0, _inode.makeEmptyDeviceRootDirectory)(datastore_id, []); created = true; } } else { device_root = res['device_root_page']; } return { 'status': true, 'device_root': device_root, 'created': created, 'datastore': datastore }; }); }); } /* * Get application public key listing * * @blockchain_id (string): blockchain ID that owns this datastore * @full_app_name (string): full application name */ function getAppKeys(blockchain_id, full_app_name, data_pubkeys) { if (data_pubkeys) { return new Promise(function (resolve, reject) { resolve(data_pubkeys); }); } else { return getProfileData(blockchain_id).then(function (res) { if (res.error || res.errno) { console.log('Failed to get profile data for ' + blockchain_id + ': ' + res.error); return res; } // TODO: actually parse and verify the key file, // but for now, since we asked a trusted node, // just extract the key file data var profile_jwt = res['profile']; if (typeof profile_jwt !== 'string') { console.log('Legacy profile for ' + blockchain_id + ': not a string'); return { 'error': 'Legacy profile for ' + blockchain_id + ': not a string' }; } try { var profile = jsontokens.decodeToken(profile_jwt)['payload']; assert(profile, 'Failed to decode profile JWT: No payload field'); var keyfile_jwt = profile['keyfile']; assert(keyfile_jwt, 'Failed to decode profile JWT: no keyfile field'); var keyfile = jsontokens.decodeToken(keyfile_jwt)['payload']; assert(keyfile, 'Failed to decode keyfile JWT: no payload field'); var keyfile_apps = keyfile['keys']['apps']; assert(keyfile_apps, 'No "apps" field in keyfile'); data_pubkeys = []; var device_ids = Object.keys(keyfile_apps); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = device_ids[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var dev_id = _step.value; var dev_listing = jsontokens.decodeToken(keyfile_apps[dev_id])['payload']; if (!dev_listing[full_app_name]) { continue; } var apk = { 'device_id': dev_id, 'public_key': dev_listing[full_app_name]['public_key'] }; data_pubkeys.push(apk); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return data_pubkeys; } catch (e) { console.log(e); console.log(JSON.stringify(e)); throw e; } }); } } /* * Find datastore info * * opts: * @blockchain_id (string): blockchain ID that owns the datastore * @full_app_name (string): name of the application that uses the datastore * @datastore_id (string): datastore ID * @data_pubkeys (array): array of device/datapubkey pairs * @force (boolean): tolerate stale data or not * */ function findDatastoreInfo(this_device_id, opts) { var blockchain_id = opts.blockchain_id || null; var full_app_name = opts.full_app_name || null; var datastore_id = opts.datastore_id || null; var data_pubkeys = opts.data_pubkeys || null; var force = opts.force || false; assert(full_app_name && blockchain_id || datastore_id && data_pubkeys, 'Need either blockchain_id/full_app_name or datastore_id/data_pubkeys'); return getAppKeys(full_app_name, blockchain_id, data_pubkeys).then(function (data_pubkeys) { var device_ids = Object.keys(data_pubkeys); return (0, _datastore.datastoreMount)({ 'blockchainID': blockchain_id, 'full_app_name': full_app_name, 'dataPubkeys': data_pubkeys, 'datastoreID': datastore_id, 'deviceID': this_device_id }).then(function (ds) { var datastore = ds.datastore; var datastore_id = (0, _datastore.datastoreGetId)(datastore['pubkey']); var root_uuid = datastore['root_uuid']; var drivers = datastore['drivers']; // find this device's public key var data_pubkey = null; var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = data_pubkeys[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var dpk = _step2.value; if (dpk['device_id'] === this_device_id) { data_pubkey = dpk['public_key']; break; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } if (!data_pubkey) { return { 'error': 'Failed to look up public key for this device' }; } var ret = { 'status': true, 'datastore': datastore, 'data_pubkeys': data_pubkeys, 'data_pubkey': data_pubkey }; return ret; }); }); } /* * Get a file. * * @file_name (string) the name of the file * * opts: * @blockchainID (string) the owner of the remote datastore * @force (string) if true, tolerate stale data * * Returns a Promise that resolves to the data, or null if not found. * Throws an exception on network or storage errors */ function getFile(file_name) { var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var blockchain_id = opts.blockchainID || (0, _metadata.getSessionBlockchainID)(); var app_name = (0, _metadata.getSessionAppName)(); var datastore_id = (0, _metadata.getSessionDatastoreID)(); var force = opts.force || false; return getFileData(file_name, { 'blockchain_id': blockchain_id, 'datastore_id': datastore_id, 'full_app_name': app_name, 'force': force }); } /* * Put a file * * @file_name (string) the name of the file * @file_data (buffer) the data to store * * opts: * sync (boolean) synchronously store the new device root directory page (default: false) * * Returns a promise that resolves to file URLs */ function putFile(file_name, file_buffer) { var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var blockchain_id = (0, _metadata.getSessionBlockchainID)(); var app_name = (0, _metadata.getSessionAppName)(); var sync = opts.sync || false; return (0, _datastore.datastoreMountOrCreate)().then(function (ds) { assert(ds, 'No datastore mounted or created'); var datastore = ds.datastore; var datastore_id = ds.datastore_id; var root_uuid = null; var device_id = ds.device_id; var privkey_hex = ds.privkey_hex; var data_pubkeys = ds.app_public_keys; assert(privkey_hex, 'No private key for datastore'); assert(device_id, 'No device ID given'); assert(data_pubkeys, 'No device public keys given'); // look up current device root return findDeviceRootInfo(device_id, { 'blockchain_id': blockchain_id, 'full_app_name': app_name, 'datastore_id': datastore_id, 'data_pubkeys': data_pubkeys }).then(function (root_info) { if (root_info.error) { console.log('Failed to load device root for datastore ' + datastore_id + ', device ' + device_id); return root_info; } datastore = root_info['datastore']; datastore_id = (0, _datastore.datastoreGetId)(datastore['pubkey']); root_uuid = datastore['root_uuid']; var device_root = root_info['device_root']; // serialize var file_payload_b64 = Buffer(file_buffer).toString("base64"); var file_hash = (0, _blob.hashDataPayload)(file_buffer.toString()); // make file header blob var file_header = (0, _inode.makeFileEntry)(file_hash, []); var file_header_str = (0, _util.jsonStableSerialize)(file_header); var file_data_id = datastore_id + '/' + file_name; var file_header_blob = (0, _blob.makeDataInfo)(file_data_id, file_header_str, device_id); // sign header blob var file_header_blob_str = (0, _util.jsonStableSerialize)(file_header_blob); var file_header_sig = (0, _blob.signDataPayload)(file_header_blob_str, privkey_hex); // serialize and sign datastore var datastore_str = JSON.stringify(datastore); var datastore_sig = (0, _blob.signRawData)(datastore_str, privkey_hex); return putFileData(datastore_str, datastore_sig, file_name, file_header_blob_str, file_payload_b64, file_header_sig, { 'blockchain_id': blockchain_id, 'full_app_name': app_name }).then(function (res) { if (res.error || res.errno) { console.log('Failed to store file ' + file_name + ' to datastore ' + datastore_id + ' (owned by ' + blockchain_id + ' in ' + app_name + ')'); return res; } // update root directory entry with new URLs var file_urls = res['urls']; assert(file_urls, 'No URLs given back'); file_header = (0, _inode.makeFileEntry)(file_hash, file_urls); var device_root_info = (0, _inode.deviceRootInsert)(datastore_id, root_uuid, device_root, file_name, file_header, device_id); var device_root_blob = device_root_info['device_root_blob']; var device_root_version = device_root_info['timestamp']; // serialize and sign var device_root_blob_str = (0, _util.jsonStableSerialize)(device_root_blob); var device_root_blob_sig = (0, _blob.signDataPayload)(device_root_blob_str, privkey_hex); // replicate return putDeviceRoot(datastore_str, datastore_sig, device_root_blob_str, device_root_blob_sig, { 'blockchain_id': blockchain_id, 'full_app_name': app_name, 'sync': sync }).then(function (res) { if (res.error || res.errno) { console.log('Failed to replicate new device root for device ' + device_id + ' in datastore ' + datastore_id + ' (owned by ' + blockchain_id + ' in ' + app_name + ')'); return res; } (0, _metadata.putDeviceRootVersion)(datastore_id, root_uuid, device_id, device_root_version); return file_urls; }); }); }); }); } /* * Delete a file. * * @file_name (string) the name of the file to delete * * opts: * @sync (boolean) whether or not so synchronously update the root directory page (default: false) * * Returns a Promise that resolves to true on success */ function deleteFile(file_name, opts) { var blockchain_id = (0, _metadata.getSessionBlockchainID)(); var app_name = (0, _metadata.getSessionAppName)(); var sync = opts.sync || false; return (0, _datastore.datastoreMountOrCreate)().then(function (ds) { assert(ds, 'No datastore mounted or created'); var datastore = ds.datastore; var datastore_id = ds.datastore_id; var root_uuid = null; var device_id = ds.device_id; var privkey_hex = ds.privkey_hex; var data_pubkeys = ds.app_public_keys; var device_ids = datastore['device_ids']; assert(privkey_hex, 'No private key for datastore'); // look up current device root return findDeviceRootInfo(device_id, { 'blockchain_id': blockchain_id, 'full_app_name': app_name, 'datastore_id': datastore_id, 'data_pubkeys': data_pubkeys }).then(function (root_info) { if (root_info.error) { console.log('Failed to load device root for datastore ' + datastore_id + ', device ' + device_id); return root_info; } datastore = root_info['datastore']; datastore_id = (0, _datastore.datastoreGetId)(datastore['pubkey']); root_uuid = datastore['root_uuid']; var device_root = root_info['device_root']; if (!Object.keys(device_root['files']).includes(file_name)) { // doesn't exist return { 'error': 'No such file: ' + file_name, 'errno': 'ENOENT' }; } // make tombstones var file_data_id = datastore_id + '/' + file_name; var file_tombstone = (0, _blob.makeDataTombstones)([device_id], file_data_id)[0]; var file_tombstones = (0, _blob.makeDataTombstones)(device_ids, file_data_id); var signed_file_tombstones = (0, _blob.signDataTombstones)(file_tombstones, privkey_hex); // serialize and sign datastore var datastore_str = JSON.stringify(datastore); var datastore_sig = (0, _blob.signRawData)(datastore_str, privkey_hex); return deleteFileData(datastore_str, datastore_sig, signed_file_tombstones, { 'blockchain_id': blockchain_id, 'full_app_name': app_name, 'datastore_id': datastore_id, 'data_pubkeys': data_pubkeys }).then(function (res) { if (res.error || res.errno) { console.log('Failed to delete file \'' + file_name + '\': ' + res.error); return res; } // update device root directory var device_root_blob = (0, _inode.deviceRootRemove)(datastore_id, root_uuid, device_root, file_name, file_tombstone, device_id); // serialize and sign var device_root_blob_str = (0, _util.jsonStableSerialize)(device_root_blob); var device_root_blob_sig = (0, _blob.signDataPayload)(device_root_blob_str, privkey_hex); // replicate return putDeviceRoot(datastore_str, datastore_sig, device_root_blob_str, device_root_blob_sig, { 'blockchain_id': blockchain_id, 'full_app_name': app_name, 'sync': sync }).then(function (res) { if (res.error || res.errno) { console.log('Failed to replicate new device root for device ' + device_id + ' in datastore ' + datastore_id + ' (owned by ' + blockchain_id + ' in ' + app_name + ')'); return res; } return true; }); }); }); }); } /* * List all files * opts: * @blockchainID (string) the owner of the remote datastore * @force (string) if true, tolerate stale data * * Returns a Promise that resolves to the root directory * Throws an exception on network or storage errors */ function listFiles(opts) { var blockchain_id = opts.blockchainID || (0, _metadata.getSessionBlockchainID)(); var app_name = (0, _metadata.getSessionAppName)(); var datastore_id = (0, _metadata.getSessionDatastoreID)(); var force = opts.force || false; return getRoot({ 'blockchain_id': blockchain_id, 'datastore_id': datastore_id, 'full_app_name': full_app_name, 'force': force }); } /* * Get a file's URL(s) * * @file_name (string) the name of the file * * opts: * @blockchainID (string) the owner of the datastore that contains the file * @force (string) if true, tolerate stale data * * Returns a Promise that resolves to the file's URL or URLs * Returns {'error': ...} on "recoverable" error (i.e. bad input, file doesn't exist) * Throws an exception on network or storage errors */ function getFileURLs(file_name, opts) { var blockchain_id = opts.blockchainID || (0, _metadata.getSessionBlockchainID)(); var app_name = (0, _metadata.getSessionAppName)(); var datastore_id = (0, _metadata.getSessionDatastoreID)(); var device_id = (0, _metadata.getSessionDeviceID)(); var force = opts.force || false; return getFileHeader(file_name, device_id, { 'blockchain_id': blockchain_id, 'datastore_id': datastore_id, 'full_app_name': app_name, 'force': force }).then(function (res) { if (res.error || res.errno) { return res; } return res['file_info']['urls']; }); }