@rsksmart/rif-storage
Version:
Library integrating distributed storage projects
281 lines (280 loc) • 9.94 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const async_iterator_to_stream_1 = __importDefault(require("async-iterator-to-stream"));
const cids_1 = __importDefault(require("cids"));
const debug_1 = __importDefault(require("debug"));
const stream_1 = require("stream");
const ipfs_http_client_1 = __importDefault(require("ipfs-http-client"));
const definitions_1 = require("../definitions");
const errors_1 = require("../errors");
const utils_1 = require("../utils");
const log = debug_1.default('rds:ipfs');
function isIpfs(client) {
client = client;
return typeof client.get === 'function' && typeof client.add === 'function';
}
/**
* Validates if an address is valid CID representative.
*
* @private
* @throws ValueError if address is not valid
* @param address
*/
function validateAddress(address) {
const isAddress = typeof address === 'string' || cids_1.default.isCID(address) || Buffer.isBuffer(address);
if (!isAddress) {
throw new errors_1.ValueError(`Address ${address} is not valid IPFS's CID`);
}
return true;
}
function contentToBuffer(iter) {
return __awaiter(this, void 0, void 0, function* () {
const arrs = yield utils_1.arrayFromAsyncIter(iter);
return Buffer.concat(arrs);
});
}
/**
* Converts IPFS style of returned data into Directory object
*
* @private
* @example
* const ipfs = [{
* path: '/tmp/myfile.txt',
* content: <data as T>
* }]
* mapDataFromIpfs(ipfs)
* // returns:
* // {
* // '/tmp/myfile.txt': {
* // data: <data as T>
* // size: <data as T>.length
* // }
* // }
*
* @param data - IPFS data returned from ipfs.get()
* @param originalAddress - Original CID address that is supposed to be removed from path
*/
function mapDataFromIpfs(data, originalAddress) {
return __awaiter(this, void 0, void 0, function* () {
const result = {};
for (const entry of data) {
// TODO: [Q] What about directories? Currently ignored
if (entry.type === 'dir')
continue;
if (!entry.content) {
throw new Error('File did not have any content returned from IPFS Client!');
}
const content = yield contentToBuffer(entry.content);
result[entry.path.replace(originalAddress.toString() + '/', '')] = {
data: content,
size: content.length
};
}
return result;
});
}
/**
* Converts and validate Directory object to IPFS style of data
*
* @private
* @example
* const directory = {
* '/tmp/myfile.txt': {
* data: <data as a Buffer>
* }
* }
* mapDataFromIpfs(ipfs)
* // returns:
* // const ipfs = [{
* // path: '/tmp/myfile.txt',
* // content: <data as a Buffer >
* // }]
*
* @param data - Directory data
* @return Array of objects that is consumable using ipfs.add()
*/
function mapDataToIpfs(data) {
return Object.entries(data).map(([path, entry]) => {
if (path === '') {
throw new errors_1.ValueError('Empty path (name of property) is not allowed!');
}
return {
path,
content: entry.data
};
});
}
/**
* Add data to IPFS
*
* @see Storage#put
* @param data
* @param options
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function put(data, options) {
return __awaiter(this, void 0, void 0, function* () {
options = options || {};
if (typeof data === 'string') {
data = Buffer.from(data);
}
// Convert single element DirectoryArray
if (Array.isArray(data) && data.length === 1) {
const el = data[0];
data = el.data;
if (!options.fileName && el.path) {
options.fileName = el.path;
}
}
if (Buffer.isBuffer(data) || utils_1.isReadable(data)) {
log('uploading single file');
let dataToAdd;
if (options.fileName) {
dataToAdd = {
content: data,
path: options.fileName || ''
};
delete options.fileName;
options.wrapWithDirectory = true;
}
else {
dataToAdd = data;
}
const result = yield this.ipfs.add(dataToAdd, options);
return result.cid.toString();
}
log('uploading directory');
options.wrapWithDirectory = options.wrapWithDirectory !== false;
if ((typeof data !== 'object' && !Array.isArray(data)) || data === null) {
throw new TypeError('data have to be string, Readable, Buffer, DirectoryArray or Directory object!');
}
if (options.fileName) {
throw new errors_1.ValueError('You are uploading directory, yet you specified fileName that is not applicable here!');
}
if ((Array.isArray(data) && data.length === 0) || Object.keys(data).length === 0) {
// TODO: [Q] Empty object should throw error? If not then what to return? https://github.com/rsksmart/rif-storage-js/issues/4
throw new errors_1.ValueError('You passed empty Directory');
}
let mappedData;
if (utils_1.isTSDirectory(data, utils_1.isReadableOrBuffer)) {
mappedData = mapDataToIpfs(data);
}
else if (utils_1.isTSDirectoryArray(data, utils_1.isReadableOrBuffer)) {
mappedData = data.map(entry => {
return {
content: entry.data,
path: entry.path
};
});
}
else {
throw new errors_1.ValueError('data have to be string, Readable, Buffer, DirectoryArray<Buffer | Readable> or Directory<Buffer | Readable> object!');
}
const last = yield utils_1.lastAsyncIterItem(this.ipfs.addAll(mappedData, options));
if (!last) {
throw new Error('No data were returned from IPFS client.');
}
return last.cid.toString();
});
}
/**
* Retrieves data from IPFS
*
* @see Storage#get
* @param address - CID compatible address
* @param options
*/
function get(address, options) {
return __awaiter(this, void 0, void 0, function* () {
validateAddress(address);
const result = yield utils_1.arrayFromAsyncIter(this.ipfs.get(address, options));
// Generally process directory when there is more then one
// entry, but the first and only entry can be empty directory.
if (result.length >= 2 || result[0].type === 'dir') {
log(`fetching directory from ${address}`);
return utils_1.markDirectory(yield mapDataFromIpfs(result, address));
}
const file = result[0];
log(`fetching single file from ${address}`);
if (!file.content) {
throw new Error('File did not have any content returned from IPFS Client!');
}
return utils_1.markFile(yield contentToBuffer(file.content));
});
}
/**
* Fetch data from IPFS network and returns it as Readable stream in object mode
* that yield objects in format {data: <Readable>, path: 'string'}
*
* @param address
* @param options
* @see Storage#getReadable
*/
// eslint-disable-next-line require-await
function getReadable(address, options) {
return __awaiter(this, void 0, void 0, function* () {
validateAddress(address);
const trans = new stream_1.Transform({
objectMode: true,
transform(entry, encoding, callback) {
if (entry.type === 'dir') {
callback(null, null);
}
else {
let pathWithoutRootHash;
if (entry.path.includes('/')) {
const splittedPath = entry.path.split('/');
pathWithoutRootHash = splittedPath.slice(1).join('/');
}
else { // Should be root
pathWithoutRootHash = '/';
}
callback(null, {
path: pathWithoutRootHash,
data: async_iterator_to_stream_1.default(entry.content)
});
}
}
});
async_iterator_to_stream_1.default.obj(this.ipfs.get(address, options)).pipe(trans);
return trans;
});
}
/**
* Factory for supporting IPFS
*
* @param options
* @constructor
*/
function IpfsFactory(options) {
let ipfsClient;
if (isIpfs(options)) {
ipfsClient = options;
log('ipfs client using an embedded node');
}
else {
ipfsClient = ipfs_http_client_1.default(options);
const addr = typeof options === 'string' ? options : options.host;
log('ipfs client using http api to ', addr);
}
return Object.freeze({
ipfs: ipfsClient,
type: definitions_1.Provider.IPFS,
put,
get,
getReadable
});
}
exports.default = IpfsFactory;