UNPKG

@uprtcl/ipfs-provider

Version:

_Prtcl provider wrappers around ipfs-http-client

263 lines (256 loc) 8.86 kB
import 'reflect-metadata'; import CBOR from 'cbor-js'; import IPFS from 'ipfs'; import { Connection, defaultCidConfig } from '@uprtcl/multiplatform'; import { Logger } from '@uprtcl/micro-orchestrator'; import CID from 'cids'; import Dexie from 'dexie'; function sortObject(object) { if (typeof object !== 'object' || object instanceof Array || object === null) { // Not to sort the array return object; } const keys = Object.keys(object).sort(); const newObject = {}; for (let i = 0; i < keys.length; i++) { newObject[keys[i]] = sortObject(object[keys[i]]); } return newObject; } const ENABLE_LOG = true; const promiseWithTimeout = (promise, timeout) => { let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error('Request timed out')); }, timeout); }); return Promise.race([promise, timeoutPromise]); }; class IpfsStore extends Connection { constructor(cidConfig = defaultCidConfig, client, pinner, connectionOptions = {}) { super(connectionOptions); this.cidConfig = cidConfig; this.client = client; this.pinner = pinner; this.logger = new Logger('IpfsStore'); this.casID = 'ipfs'; } /** * @override */ async connect(ipfsOptions) { if (!this.client) { this.client = new IPFS.create(); } } /** * Adds a raw js object to IPFS with the given cid configuration */ create(object) { return this.putIpfs(object); } async putIpfs(object) { const sorted = sortObject(object); const buffer = CBOR.encode(sorted); { this.logger.log(`Trying to add object:`, { object, sorted, buffer }); } let putConfig = { format: this.cidConfig.codec, hashAlg: this.cidConfig.type, cidVersion: this.cidConfig.version, pin: true, }; /** recursively try */ const result = await this.client.dag.put(Buffer.from(buffer), putConfig); let hashString = result.toString(this.cidConfig.base); { this.logger.log(`Object stored`, { object, sorted, buffer, hashString, }); } if (this.pinner) this.pinner.pin(hashString); return hashString; } /** * Retrieves the object with the given hash from IPFS */ async get(hash) { /** recursively try */ if (!hash) throw new Error('hash undefined or empty'); try { const raw = await promiseWithTimeout(this.client.dag.get(hash), 10000); const forceBuffer = Uint8Array.from(raw.value); let object = CBOR.decode(forceBuffer.buffer); if (ENABLE_LOG) { this.logger.log(`Object retrieved ${hash}`, { raw, object }); } return object; } catch (e) { throw new Error(`Error reading ${hash}`); } } } const constants = [ ['base8', 37], ['base10', 39], ['base16', 66], ['base32', 62], ['base32pad', 63], ['base32hex', 76], ['base32hexpad', 74], ['base32z', 68], ['base58flickr', 90], ['base58btc', 122], ['base64', 109], ['base64pad', 77], ['base64url', 75], ['Ubase64urlpad', 55], ]; const multibaseToUint = (multibaseName) => { return constants.filter((e) => e[0] == multibaseName)[0][1]; }; const uintToMultibase = (number) => { return constants.filter((e) => e[1] == number)[0][0]; }; const cidToHex32 = (cidStr) => { /** store the encoded cids as they are, including the multibase bytes */ const cid = new CID(cidStr); const bytes = cid.bytes; /* push the code of the multibse (UTF8 number of the string) */ const bytesWithMultibase = new Uint8Array(bytes.length + 1); bytesWithMultibase.set(Uint8Array.from([multibaseToUint(cid.multibaseName)])); bytesWithMultibase.set(bytes, 1); /** convert to hex */ let cidEncoded16 = Buffer.from(bytesWithMultibase).toString('hex'); /** pad with zeros */ cidEncoded16 = cidEncoded16.padStart(128, '0'); const cidHex0 = cidEncoded16.slice(-64); /** LSB */ const cidHex1 = cidEncoded16.slice(-128, -64); return ['0x' + cidHex1, '0x' + cidHex0]; }; const bytes32ToCid = (bytes) => { const cidHex1 = bytes[0].substring(2); const cidHex0 = bytes[1].substring(2); /** LSB */ const cidHex = cidHex1.concat(cidHex0).replace(/^0+/, ''); if (cidHex === '') return ''; const cidBufferWithBase = Buffer.from(cidHex, 'hex'); const multibaseCode = cidBufferWithBase[0]; const cidBuffer = cidBufferWithBase.slice(1); const multibaseName = uintToMultibase(multibaseCode); /** Force Buffer class */ const cid = new CID(cidBuffer); return cid.toBaseEncodedString(multibaseName); }; class PinnerCacheDB extends Dexie { constructor(name) { super(name); this.version(1).stores({ entities: '&id,pinned', }); this.entities = this.table('entities'); } } class PinnerCached { constructor(url, flushInterval) { this.url = url; this.logger = new Logger('Pinner Cached'); this.isFlusshing = false; this.cache = new PinnerCacheDB(`pinner-cache-${url}`); setInterval(() => this.flush(), flushInterval); } async pin(hash) { const current = await this.cache.entities.get(hash); if (!current) { await this.cache.entities.put({ id: hash, pinned: 0 }); } } async isPinned(address) { const result = await fetch(`${this.url}/includes?address=${address}`, { method: 'GET', }); const { includes } = await result.json(); return includes; } async getAll(address) { if (this.url) { const addr = address.toString(); const result = await fetch(`${this.url}/getAll?address=${addr}`, { method: 'GET', }); return result.json(); } } async getEntity(hash) { const addr = hash.toString(); const result = await fetch(`${this.url}/getEntity?cid=${addr}`, { method: 'GET', }); return result.json(); } async flush() { if (!this.cache) throw new Error('cache not initialized'); if (this.isFlusshing) return; this.isFlusshing = true; const unpinned = this.cache.entities.where('pinned').equals(0); const n = await unpinned.count(); if (n > 0) { this.logger.log(`${n} objects not pinned pinned`); } const unpinnedHashed = await unpinned .clone() .and((o) => !o.id.startsWith('/orbitdb/')) .toArray(); const unpinnedDB = await unpinned .clone() .and((o) => o.id.startsWith('/orbitdb/')) .toArray(); if (unpinnedHashed.length > 0) { this.logger.log('pinning entities', unpinnedHashed); await fetch(`${this.url}/pin_hash`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ cids: unpinnedHashed.map((e) => e.id) }), }); this.logger.log('pinning entities - done'); } if (unpinnedDB.length > 0) { this.logger.log('pinning addresses', unpinnedDB); await fetch(`${this.url}/pin`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ addresses: unpinnedDB.map((e) => e.id) }), }); this.logger.log('pinning addresses - done'); } const nPinned = await this.cache.entities .where('pinned') .equals(0) .modify({ pinned: 1 }); if (nPinned > 0) { this.logger.log('marked as pinned', nPinned); } if (n !== nPinned) { throw new Error(`Error marked the pinned objects as pinned`); } this.isFlusshing = false; } } export { IpfsStore, PinnerCached, bytes32ToCid, cidToHex32, sortObject }; //# sourceMappingURL=uprtcl-ipfs-provider.es5.js.map