@uprtcl/ipfs-provider
Version:
_Prtcl provider wrappers around ipfs-http-client
263 lines (256 loc) • 8.86 kB
JavaScript
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