UNPKG

@rsksmart/rif-storage

Version:

Library integrating distributed storage projects

281 lines (280 loc) 9.94 kB
"use strict"; 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;