blockstack-storage
Version:
The Blockstack Javascript library for storage.
987 lines (820 loc) • 37.8 kB
JavaScript
'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'];
});
}