kopia lustrzana https://gitlab.com/rysiekpl/libresilient
gun-ipfs plugin ready for new plugin loading system (ref. #15)
rodzic
05e7997e89
commit
d8b6f64d4a
|
@ -4,17 +4,16 @@ describe("plugin: gun-ipfs", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
Object.assign(global, makeServiceWorkerEnv());
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
self.LibResilientPlugins = new Array()
|
init = {
|
||||||
self.LibResilientConfig = {
|
name: 'gun-ipfs',
|
||||||
plugins: {
|
gunPubkey: 'stub'
|
||||||
'gun-ipfs':{
|
}
|
||||||
'gunPubkey': 'stub'
|
global.LibResilientPluginConstructors = new Map()
|
||||||
}
|
LR = {
|
||||||
}
|
log: jest.fn((component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
self.log = jest.fn((component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
})
|
|
||||||
global.Ipfs = {
|
global.Ipfs = {
|
||||||
create: ()=>{
|
create: ()=>{
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
|
@ -71,18 +70,24 @@ describe("plugin: gun-ipfs", () => {
|
||||||
|
|
||||||
test("it should register in LibResilientPlugins", () => {
|
test("it should register in LibResilientPlugins", () => {
|
||||||
require("../../plugins/gun-ipfs.js");
|
require("../../plugins/gun-ipfs.js");
|
||||||
expect(self.LibResilientPlugins[0].name).toEqual('gun-ipfs');
|
expect(LibResilientPluginConstructors.get('gun-ipfs')(LR, init).name).toEqual('gun-ipfs');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("IPFS setup should be initiated", ()=>{
|
test("IPFS setup should be initiated", async ()=>{
|
||||||
self.importScripts = jest.fn()
|
self.importScripts = jest.fn()
|
||||||
require("../../plugins/gun-ipfs.js");
|
require("../../plugins/gun-ipfs.js");
|
||||||
|
try {
|
||||||
|
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch('/test.json')
|
||||||
|
} catch {}
|
||||||
expect(self.importScripts).toHaveBeenNthCalledWith(1, './lib/ipfs.js')
|
expect(self.importScripts).toHaveBeenNthCalledWith(1, './lib/ipfs.js')
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Gun setup should be initiated", ()=>{
|
test("Gun setup should be initiated", async ()=>{
|
||||||
self.importScripts = jest.fn()
|
self.importScripts = jest.fn()
|
||||||
require("../../plugins/gun-ipfs.js");
|
require("../../plugins/gun-ipfs.js");
|
||||||
|
try {
|
||||||
|
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch('/test.json')
|
||||||
|
} catch {}
|
||||||
expect(self.importScripts).toHaveBeenNthCalledWith(2, "./lib/gun.js", "./lib/sea.js", "./lib/webrtc.js")
|
expect(self.importScripts).toHaveBeenNthCalledWith(2, "./lib/gun.js", "./lib/sea.js", "./lib/webrtc.js")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -91,7 +96,7 @@ describe("plugin: gun-ipfs", () => {
|
||||||
|
|
||||||
expect.assertions(1)
|
expect.assertions(1)
|
||||||
try {
|
try {
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test.json')
|
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch(self.location.origin + '/test.json')
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
expect(e).toEqual(new Error('IPFS address is undefined for: /test.json'))
|
expect(e).toEqual(new Error('IPFS address is undefined for: /test.json'))
|
||||||
}
|
}
|
||||||
|
@ -102,7 +107,7 @@ describe("plugin: gun-ipfs", () => {
|
||||||
|
|
||||||
expect.assertions(1)
|
expect.assertions(1)
|
||||||
try {
|
try {
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test/')
|
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch(self.location.origin + '/test/')
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
expect(e).toEqual(new Error('IPFS address is undefined for: /test/index.html'))
|
expect(e).toEqual(new Error('IPFS address is undefined for: /test/index.html'))
|
||||||
}
|
}
|
||||||
|
@ -110,47 +115,47 @@ describe("plugin: gun-ipfs", () => {
|
||||||
|
|
||||||
test("content types should be guessed correctly when fetching", async ()=>{
|
test("content types should be guessed correctly when fetching", async ()=>{
|
||||||
require("../../plugins/gun-ipfs.js");
|
require("../../plugins/gun-ipfs.js");
|
||||||
|
var gunipfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
try {
|
||||||
|
await gunipfsPlugin.fetch(self.location.origin + '/test/')
|
||||||
|
} catch(e) {}
|
||||||
|
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/html")
|
||||||
|
LR.log.mockClear()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test/')
|
await gunipfsPlugin.fetch(self.location.origin + '/test.htm')
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
expect(self.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/html")
|
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/html")
|
||||||
self.log.mockClear()
|
LR.log.mockClear()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test.htm')
|
await gunipfsPlugin.fetch(self.location.origin + '/test.css')
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
expect(self.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/html")
|
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/css")
|
||||||
self.log.mockClear()
|
LR.log.mockClear()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test.css')
|
await gunipfsPlugin.fetch(self.location.origin + '/test.js')
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
expect(self.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/css")
|
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/javascript")
|
||||||
self.log.mockClear()
|
LR.log.mockClear()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test.js')
|
await gunipfsPlugin.fetch(self.location.origin + '/test.json')
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
expect(self.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/javascript")
|
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : application/json")
|
||||||
self.log.mockClear()
|
LR.log.mockClear()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test.json')
|
await gunipfsPlugin.fetch(self.location.origin + '/test.svg')
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
expect(self.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : application/json")
|
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : image/svg+xml")
|
||||||
self.log.mockClear()
|
LR.log.mockClear()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test.svg')
|
await gunipfsPlugin.fetch(self.location.origin + '/test.ico')
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
expect(self.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : image/svg+xml")
|
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : image/x-icon")
|
||||||
self.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await self.LibResilientPlugins[0].fetch(self.location.origin + '/test.ico')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(self.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : image/x-icon")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("fetching should work (stub!)", async ()=>{
|
test("fetching should work (stub!)", async ()=>{
|
||||||
|
@ -169,24 +174,25 @@ describe("plugin: gun-ipfs", () => {
|
||||||
})
|
})
|
||||||
require("../../plugins/gun-ipfs.js");
|
require("../../plugins/gun-ipfs.js");
|
||||||
|
|
||||||
await self.Ipfs.create()
|
//await self.Ipfs.create()
|
||||||
let response = await self.LibResilientPlugins[0].fetch(self.location.origin + '/test.json')
|
let response = await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch(self.location.origin + '/test.json')
|
||||||
expect(response.body.type).toEqual('application/json')
|
expect(response.body.type).toEqual('application/json')
|
||||||
expect(String.fromCharCode.apply(null, response.body.parts[0])).toEqual('{test: "success"}')
|
expect(String.fromCharCode.apply(null, response.body.parts[0])).toEqual('{test: "success"}')
|
||||||
})
|
})
|
||||||
|
|
||||||
test("publishContent should error out if passed anything else than string or array of string", async ()=>{
|
test("publishContent should error out if passed anything else than string or array of string", async ()=>{
|
||||||
require("../../plugins/gun-ipfs.js");
|
require("../../plugins/gun-ipfs.js");
|
||||||
|
var gunipfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
expect(()=>{
|
expect(()=>{
|
||||||
self.LibResilientPlugins[0].publish({
|
gunipfsPlugin.publish({
|
||||||
url: self.location.origin + '/test.json'
|
url: self.location.origin + '/test.json'
|
||||||
})
|
})
|
||||||
}).toThrow('Handling a Response: not implemented yet')
|
}).toThrow('Handling a Response: not implemented yet')
|
||||||
expect(()=>{
|
expect(()=>{
|
||||||
self.LibResilientPlugins[0].publish(true)
|
gunipfsPlugin.publish(true)
|
||||||
}).toThrow('Only accepts: string, Array of string, Response.')
|
}).toThrow('Only accepts: string, Array of string, Response.')
|
||||||
expect(()=>{
|
expect(()=>{
|
||||||
self.LibResilientPlugins[0].publish([true, 5])
|
gunipfsPlugin.publish([true, 5])
|
||||||
}).toThrow('Only accepts: string, Array of string, Response.')
|
}).toThrow('Only accepts: string, Array of string, Response.')
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,480 +21,483 @@ if (typeof window === 'undefined') {
|
||||||
\* ========================================================================= */
|
\* ========================================================================= */
|
||||||
|
|
||||||
// no polluting of the global namespace please
|
// no polluting of the global namespace please
|
||||||
(function () {
|
(function(LRPC){
|
||||||
|
// this never changes
|
||||||
|
const pluginName = "gun-ipfs"
|
||||||
|
LRPC.set(pluginName, (LR, init={})=>{
|
||||||
|
|
||||||
var ipfs;
|
var ipfs;
|
||||||
var gun;
|
var gun;
|
||||||
var gunUser;
|
var gunUser;
|
||||||
|
|
||||||
// sane defaults
|
// sane defaults
|
||||||
let defaultConfig = {
|
let defaultConfig = {
|
||||||
// name of this plugin
|
// Gun nodes to use
|
||||||
// should not be changed
|
gunNodes: ['https://gunjs.herokuapp.com/gun'],
|
||||||
name: "gun-ipfs",
|
// the pubkey of the preconfigured Gun user; always needs to be set in config.js
|
||||||
// Gun nodes to use
|
gunPubkey: null,
|
||||||
gunNodes: ['https://gunjs.herokuapp.com/gun'],
|
// the IPFS gateway we're using for verification when publishing; default is usually ok
|
||||||
// the pubkey of the preconfigured Gun user; always needs to be set in config.js
|
ipfsGateway: 'https://gateway.ipfs.io'
|
||||||
gunPubkey: null,
|
|
||||||
// the IPFS gateway we're using for verification when publishing; default is usually ok
|
|
||||||
ipfsGateway: 'https://gateway.ipfs.io'
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge the defaults with settings from LibResilientConfig
|
|
||||||
let config = {...defaultConfig, ...self.LibResilientConfig.plugins[defaultConfig.name]}
|
|
||||||
|
|
||||||
// reality check: Gun pubkey needs to be set to a non-empty string
|
|
||||||
if (typeof(config.gunPubkey) !== "string" || config.gunPubkey === "") {
|
|
||||||
let err = new Error("gunPubkey not confgured")
|
|
||||||
console.error(err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* importing stuff works differently between a browser window context
|
|
||||||
* and a ServiceWorker context, because things just can't be easy and sane
|
|
||||||
*/
|
|
||||||
function doImport() {
|
|
||||||
var args = Array.prototype.slice.call(arguments);
|
|
||||||
self.log(config.name, `doImport()\n+-- ${args.join('\n+-- ')}`)
|
|
||||||
if (typeof self.importScripts !== 'undefined') {
|
|
||||||
self.log(config.name, `+-- self.importScripts.apply()`)
|
|
||||||
self.importScripts.apply(self, args)
|
|
||||||
} else {
|
|
||||||
self.log(
|
|
||||||
config.name,
|
|
||||||
'assuming these scripts are already included:\n',
|
|
||||||
args.join('\n+-- ')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function setup_ipfs() {
|
// merge the defaults with settings from init
|
||||||
if (ipfs === undefined) {
|
let config = {...defaultConfig, ...init}
|
||||||
ipfs = false // we don't want to start a few times over
|
|
||||||
self.log(config.name, 'importing IPFS-related libraries...');
|
// reality check: Gun pubkey needs to be set to a non-empty string
|
||||||
doImport(
|
if (typeof(config.gunPubkey) !== "string" || config.gunPubkey === "") {
|
||||||
"./lib/ipfs.js");
|
let err = new Error("gunPubkey not confgured")
|
||||||
self.log(config.name, 'setting up IPFS...')
|
console.error(err)
|
||||||
try {
|
throw err
|
||||||
ipfs = await self.Ipfs.create();
|
|
||||||
self.log(config.name, '+-- IPFS loaded :: ipfs is : ' + typeof ipfs)
|
|
||||||
} catch(e) {
|
|
||||||
console.error('+-- Error loading IPFS: ' + e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function setup_gun() {
|
/**
|
||||||
self.log(config.name, 'setup_gun()')
|
* importing stuff works differently between a browser window context
|
||||||
if (gun === undefined) {
|
* and a ServiceWorker context, because things just can't be easy and sane
|
||||||
gun = false // we don't want to start a few times over
|
|
||||||
self.log(config.name, 'importing Gun-related libraries...');
|
|
||||||
try {
|
|
||||||
doImport(
|
|
||||||
"./lib/gun.js",
|
|
||||||
"./lib/sea.js",
|
|
||||||
"./lib/webrtc.js");
|
|
||||||
} catch(e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
self.log(config.name, 'setting up Gun...')
|
|
||||||
try {
|
|
||||||
gun = Gun(config.gunNodes);
|
|
||||||
self.log(config.name, '+-- gun loaded :: gun is : ' + typeof gun);
|
|
||||||
} catch(e) {
|
|
||||||
self.log(config.name, 'Error setting up Gun: ' + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( (gun !== false) && (gun !== undefined) ) {
|
|
||||||
if (gunUser === undefined) {
|
|
||||||
gunUser = false // we don't want to start a few times over
|
|
||||||
self.log(config.name, 'setting up gunUser...')
|
|
||||||
gunUser = gun.user(config.gunPubkey)
|
|
||||||
self.log(config.name, '+-- gun init complete :: gunUser is: ' + typeof gunUser);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("at this point Gun should have been set up already, but isn't!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setup_gun_ipfs() {
|
|
||||||
self.log(config.name, 'setup_gun_ipfs()')
|
|
||||||
if (ipfs === undefined || gun === undefined) {
|
|
||||||
self.log(config.name, 'setting up...')
|
|
||||||
setup_ipfs();
|
|
||||||
setup_gun();
|
|
||||||
} else {
|
|
||||||
self.log(config.name, 'setup already underway (ipfs: ' + ( (ipfs) ? 'done' : 'loading' ) + ', gun: ' + ( (gun) ? 'done' : 'loading' ) + ')')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ========================================================================= *\
|
|
||||||
|* === Main functionality === *|
|
|
||||||
\* ========================================================================= */
|
|
||||||
|
|
||||||
let getGunData = (gunaddr) => {
|
|
||||||
return new Promise(
|
|
||||||
(resolve, reject) => {
|
|
||||||
self.log(
|
|
||||||
config.name,
|
|
||||||
'getGunData()\n',
|
|
||||||
`+-- gunUser : ${typeof gunUser}\n`,
|
|
||||||
`+-- gunaddr[] : ${gunaddr}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// get the data
|
|
||||||
gunUser
|
|
||||||
.get(gunaddr[0])
|
|
||||||
.get(gunaddr[1])
|
|
||||||
.once(function(addr){
|
|
||||||
if (typeof addr !== 'undefined') {
|
|
||||||
self.log(config.name, `IPFS address: "${addr}"`);
|
|
||||||
resolve(addr);
|
|
||||||
} else {
|
|
||||||
// looks like we didn't get anything
|
|
||||||
reject(new Error('IPFS address is undefined for: ' + gunaddr[1]))
|
|
||||||
}
|
|
||||||
// ToDo: what happens when we hit the timeout here?
|
|
||||||
}, {wait: 5000});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the workhorse of this plugin
|
|
||||||
*/
|
|
||||||
async function getContentFromGunAndIPFS(url) {
|
|
||||||
var urlArray = url.replace(/https?:\/\//, '').split('/')
|
|
||||||
var gunaddr = [urlArray[0], '/' + urlArray.slice(1).join('/')]
|
|
||||||
|
|
||||||
/*
|
|
||||||
* if the gunaddr[1] ends in '/', append 'index.html' to it
|
|
||||||
*/
|
*/
|
||||||
if (gunaddr[1].charAt(gunaddr[1].length - 1) === '/') {
|
function doImport() {
|
||||||
self.log(config.name, "path ends in '/', assuming 'index.html' should be appended.");
|
var args = Array.prototype.slice.call(arguments);
|
||||||
gunaddr[1] += 'index.html';
|
LR.log(pluginName, `doImport()\n+-- ${args.join('\n+-- ')}`)
|
||||||
}
|
if (typeof self.importScripts !== 'undefined') {
|
||||||
|
LR.log(pluginName, `+-- self.importScripts.apply()`)
|
||||||
// inform
|
self.importScripts.apply(self, args)
|
||||||
self.log(
|
|
||||||
config.name,
|
|
||||||
`starting Gun lookup of: ${gunaddr.join(', ')}\n`,
|
|
||||||
`+-- gun : ${typeof gun}\n`,
|
|
||||||
`+-- gunUser : ${typeof gunUser}`
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* naïvely assume content type based on file extension
|
|
||||||
* TODO: this needs a fix
|
|
||||||
*/
|
|
||||||
var contentType = '';
|
|
||||||
switch (gunaddr.slice(-1)[0].split('.', -1)[1].toLowerCase()) {
|
|
||||||
case 'html':
|
|
||||||
case 'htm':
|
|
||||||
contentType = 'text/html';
|
|
||||||
break;
|
|
||||||
case 'css':
|
|
||||||
contentType = 'text/css';
|
|
||||||
break;
|
|
||||||
case 'js':
|
|
||||||
contentType = 'text/javascript';
|
|
||||||
break;
|
|
||||||
case 'json':
|
|
||||||
contentType = 'application/json';
|
|
||||||
break;
|
|
||||||
case 'svg':
|
|
||||||
contentType = 'image/svg+xml';
|
|
||||||
break;
|
|
||||||
case 'ico':
|
|
||||||
contentType = 'image/x-icon';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.log(config.name, " +-- guessed contentType : " + contentType);
|
|
||||||
|
|
||||||
return getGunData(gunaddr).then(ipfsaddr => {
|
|
||||||
self.log(config.name, `starting IPFS lookup of: '${ipfsaddr}'`);
|
|
||||||
return ipfs.get(ipfsaddr).next();
|
|
||||||
}).then(file => {
|
|
||||||
// we only need one
|
|
||||||
if (file.value.content) {
|
|
||||||
async function getContent(source) {
|
|
||||||
var content = new Uint8Array()
|
|
||||||
var data = await source.next()
|
|
||||||
while (! data.done) {
|
|
||||||
var newContent = new Uint8Array(content.length + data.value.length);
|
|
||||||
newContent.set(content)
|
|
||||||
newContent.set(data.value, content.length)
|
|
||||||
content = newContent
|
|
||||||
data = await source.next()
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
return getContent(file.value.content).then((content)=>{
|
|
||||||
self.log(config.name, `got a Gun-addressed IPFS-stored file: ${file.value.path}\n+-- content is: ${typeof content}`);
|
|
||||||
// creating and populating the blob
|
|
||||||
var blob = new Blob(
|
|
||||||
[content],
|
|
||||||
{'type': contentType}
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
blob,
|
|
||||||
{
|
|
||||||
'status': 200,
|
|
||||||
'statusText': 'OK',
|
|
||||||
'headers': {
|
|
||||||
'Content-Type': contentType,
|
|
||||||
'ETag': file.value.path,
|
|
||||||
'X-LibResilient-Method': config.name,
|
|
||||||
'X-LibResilient-ETag': file.value.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ========================================================================= *\
|
|
||||||
|* === Publishing stuff === *|
|
|
||||||
\* ========================================================================= */
|
|
||||||
|
|
||||||
/*
|
|
||||||
* these are used for adding content to IPFS and Gun
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* adding stuff to IPFS
|
|
||||||
* accepts an array of URLs
|
|
||||||
*
|
|
||||||
* returns a Promise that resolves to an object mapping URLs to IPFS hashes
|
|
||||||
*/
|
|
||||||
let addToIPFS = (resources) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
self.log(config.name, "adding to IPFS...")
|
|
||||||
self.log(config.name, "+-- number of resources:", resources.length)
|
|
||||||
var ipfs_addresses = new Map();
|
|
||||||
|
|
||||||
resources.forEach(function(res){
|
|
||||||
self.log(config.name, " +-- handling internal resource:", res)
|
|
||||||
|
|
||||||
ipfs.add(Ipfs.urlSource(res))
|
|
||||||
.then((result) => {
|
|
||||||
// add to the list -- this is needed to add stuff to Gun
|
|
||||||
// result.path is just the filename stored in IPFS, not the actual path!
|
|
||||||
// res holds the full URL
|
|
||||||
// what we need in ipfs_addresses is in fact the absolute path (no domain, no scheme)
|
|
||||||
var abs_path = res.replace(window.location.origin, '')
|
|
||||||
ipfs_addresses.set(abs_path, '/ipfs/' + result.cid.string)
|
|
||||||
self.log(config.name, "added to IPFS: " + abs_path + ' as ' + ipfs_addresses.get(abs_path))
|
|
||||||
// if we seem to have all we need, resolve!
|
|
||||||
if (ipfs_addresses.size === resources.length) resolve(ipfs_addresses);
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* verification that content pushed to IPFS
|
|
||||||
* is, in fact, available in IPFS
|
|
||||||
*
|
|
||||||
* a nice side-effect is that this will pre-load the content on
|
|
||||||
* a gateway, which tends to be a large (and fast) IPFS node
|
|
||||||
*
|
|
||||||
* this is prety naïve, in that it pulls the content from an ipfs gateway
|
|
||||||
* and assumes all is well if it get a HTTP 200 and any content
|
|
||||||
*
|
|
||||||
* that is, it does *not* check that the content matches what was pushed
|
|
||||||
* we trust IPFS here, I guess
|
|
||||||
*
|
|
||||||
* finally, we're using a regular fetch() instead of just going through our
|
|
||||||
* ipfs object because our IPFS object might have things cached and we want
|
|
||||||
* to test a completey independent route
|
|
||||||
*
|
|
||||||
* takes a object mapping paths to IPFS addresses
|
|
||||||
* and returns a Promise that resolves to true
|
|
||||||
*/
|
|
||||||
let verifyInIPFS = (ipfs_addresses) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
self.log(config.name, 'checking IPFS content against a gateway...')
|
|
||||||
self.log(config.name, '+-- gateway in use: ' + config.ipfsGateway)
|
|
||||||
// get the list of IPFS addresses
|
|
||||||
var updatedPaths = Array.from(ipfs_addresses.values())
|
|
||||||
for (const path of ipfs_addresses.keys()) {
|
|
||||||
// start the fetch
|
|
||||||
fetch(config.ipfsGateway + ipfs_addresses.get(path))
|
|
||||||
.then((response) => {
|
|
||||||
ipfsaddr = response.url.replace(config.ipfsGateway, '')
|
|
||||||
if (response.ok) {
|
|
||||||
self.log(config.name, '+-- verified: ' + ipfsaddr)
|
|
||||||
var pathIndex = updatedPaths.indexOf(ipfsaddr)
|
|
||||||
if (pathIndex > -1) {
|
|
||||||
updatedPaths.splice(pathIndex, 1)
|
|
||||||
}
|
|
||||||
if (updatedPaths.length === 0) {
|
|
||||||
self.log(config.name, 'all updates confirmed successful!')
|
|
||||||
resolve(ipfs_addresses);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject(new Error('HTTP error (' + response.status + ' ' + response.statusText + ' for: ' + ipfsaddr))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// it would be nice to have the failed path here somehow
|
|
||||||
// alternatively, updating updatedPaths with info on failed
|
|
||||||
// requests might work
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* auth a Gun admin user
|
|
||||||
* (and verify it's the correct one with regards to the configured config.gunPubkey)
|
|
||||||
*/
|
|
||||||
let authGunAdmin = (user, pass) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// we need a separate Gun instance, otherwise gu will get merged with gunUser
|
|
||||||
// and we want these to be separate
|
|
||||||
var g = Gun(config.gunNodes)
|
|
||||||
var gu = g.user()
|
|
||||||
gu.auth(user, pass, (userReference) => {
|
|
||||||
if (userReference.err) {
|
|
||||||
reject(new Error(userReference.err))
|
|
||||||
// reality check -- does it match our preconfigured pubkey?
|
|
||||||
} else if (gu._.soul.slice(1) === config.gunPubkey) {
|
|
||||||
self.log(config.name, 'Gun admin user authenticated using password.');
|
|
||||||
// we need to keep the reference to g, otherwise gu becomes unusable
|
|
||||||
var gApi = {
|
|
||||||
user: gu,
|
|
||||||
gun: g
|
|
||||||
}
|
|
||||||
resolve(gApi)
|
|
||||||
} else {
|
|
||||||
reject(new Error('Password-authenticated user does not match preconfigured pubkey!'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add IPFS addresses to Gun
|
|
||||||
*/
|
|
||||||
let addToGun = (user, pass, ipfs_addresses) => {
|
|
||||||
// we need an authenticated Gun user
|
|
||||||
return authGunAdmin(user, pass)
|
|
||||||
.then((gunAPI) => {
|
|
||||||
self.log(config.name, '+-- adding new IPFS addresses to Gun...')
|
|
||||||
// TODO BUG: this will probably fail, or cause failures down the line
|
|
||||||
// TODO BUG: we're setting a Map() object here; it's unclear how GunDB
|
|
||||||
// TODO BUG: will handle this
|
|
||||||
gunAPI.user.get(window.location.host).put(ipfs_addresses /*, function(ack) {...}*/);
|
|
||||||
return gunAPI;
|
|
||||||
})
|
|
||||||
/**
|
|
||||||
* regular confirmations don't seem to work
|
|
||||||
*
|
|
||||||
* so instead we're using the regular read-only Gun user
|
|
||||||
* to .get() the data that we've .put() just a minute ago
|
|
||||||
*
|
|
||||||
* we then subscribe to the .on() events and once we notice the correct
|
|
||||||
* addresseswe consider our job done and quit.
|
|
||||||
*/
|
|
||||||
.then((gunAPI) => {
|
|
||||||
// get the paths
|
|
||||||
self.log(config.name, '+-- starting verification of updated Gun data...')
|
|
||||||
var updatedPaths = Array.from(ipfs_addresses.values())
|
|
||||||
for (const path of ipfs_addresses) {
|
|
||||||
self.log(config.name, ' +-- watching: ' + path)
|
|
||||||
//debuglog('watching path for updates:', path)
|
|
||||||
// using the global gunUser to check if updates propagated
|
|
||||||
gunUser.get(window.location.host).get(path).on(function(updaddr, updpath){
|
|
||||||
/*debuglog('+--', updpath)
|
|
||||||
debuglog(' updated :', ipfs_addresses[updpath])
|
|
||||||
debuglog(' received :', updaddr)*/
|
|
||||||
if (ipfs_addresses.get(updpath) == updaddr) {
|
|
||||||
// update worked!
|
|
||||||
gunUser.get(window.location.host).get(updpath).off()
|
|
||||||
self.log(config.name, '+-- update confirmed for:', updpath, '[' + updaddr + ']')
|
|
||||||
var pathIndex = updatedPaths.indexOf(updpath)
|
|
||||||
if (pathIndex > -1) {
|
|
||||||
updatedPaths.splice(pathIndex, 1)
|
|
||||||
}
|
|
||||||
if (updatedPaths.length === 0) {
|
|
||||||
self.log(config.name, 'all updates confirmed successful!')
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* example code for of adding content to IPFS, verifying it was successfully added,
|
|
||||||
* and adding the new addresses to Gun (and verifying changes propagated)
|
|
||||||
*
|
|
||||||
* TODO: this should accept a URL, a Response, or a list of URLs,
|
|
||||||
* and handle stuff appropriately
|
|
||||||
*/
|
|
||||||
let publishContent = (resource, user, password) => {
|
|
||||||
|
|
||||||
if (typeof resource === 'string') {
|
|
||||||
// we need this as an array of strings
|
|
||||||
resource = [resource]
|
|
||||||
} else if (typeof resource === 'object') {
|
|
||||||
if (!Array.isArray(resource)) {
|
|
||||||
// TODO: this needs to be implemented such that the Response is used directly
|
|
||||||
// but that would require all called functions to also accept a Response
|
|
||||||
// and act accordingly; #ThisIsComplicated
|
|
||||||
throw new Error("Handling a Response: not implemented yet")
|
|
||||||
} else {
|
} else {
|
||||||
resource.forEach((res)=>{
|
LR.log(
|
||||||
if (typeof res !== 'string') {
|
pluginName,
|
||||||
throw new TypeError("Only accepts: string, Array of string, Response.")
|
'assuming these scripts are already included:\n',
|
||||||
|
args.join('\n+-- ')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup_ipfs() {
|
||||||
|
if (ipfs === undefined) {
|
||||||
|
ipfs = false // we don't want to start a few times over
|
||||||
|
LR.log(pluginName, 'importing IPFS-related libraries...');
|
||||||
|
doImport(
|
||||||
|
"./lib/ipfs.js");
|
||||||
|
LR.log(pluginName, 'setting up IPFS...')
|
||||||
|
try {
|
||||||
|
ipfs = await self.Ipfs.create();
|
||||||
|
LR.log(pluginName, '+-- IPFS loaded :: ipfs is : ' + typeof ipfs)
|
||||||
|
} catch(e) {
|
||||||
|
console.error('+-- Error loading IPFS: ' + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup_gun() {
|
||||||
|
LR.log(pluginName, 'setup_gun()')
|
||||||
|
if (gun === undefined) {
|
||||||
|
gun = false // we don't want to start a few times over
|
||||||
|
LR.log(pluginName, 'importing Gun-related libraries...');
|
||||||
|
try {
|
||||||
|
doImport(
|
||||||
|
"./lib/gun.js",
|
||||||
|
"./lib/sea.js",
|
||||||
|
"./lib/webrtc.js");
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
LR.log(pluginName, 'setting up Gun...')
|
||||||
|
try {
|
||||||
|
gun = Gun(config.gunNodes);
|
||||||
|
LR.log(pluginName, '+-- gun loaded :: gun is : ' + typeof gun);
|
||||||
|
} catch(e) {
|
||||||
|
LR.log(pluginName, 'Error setting up Gun: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( (gun !== false) && (gun !== undefined) ) {
|
||||||
|
if (gunUser === undefined) {
|
||||||
|
gunUser = false // we don't want to start a few times over
|
||||||
|
LR.log(pluginName, 'setting up gunUser...')
|
||||||
|
gunUser = gun.user(config.gunPubkey)
|
||||||
|
LR.log(pluginName, '+-- gun init complete :: gunUser is: ' + typeof gunUser);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("at this point Gun should have been set up already, but isn't!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup_gun_ipfs() {
|
||||||
|
LR.log(pluginName, 'setup_gun_ipfs()')
|
||||||
|
if (ipfs === undefined || gun === undefined) {
|
||||||
|
LR.log(pluginName, 'setting up...')
|
||||||
|
await setup_ipfs();
|
||||||
|
await setup_gun();
|
||||||
|
} else {
|
||||||
|
LR.log(pluginName, 'setup already underway (ipfs: ' + ( (ipfs) ? 'done' : 'loading' ) + ', gun: ' + ( (gun) ? 'done' : 'loading' ) + ')')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ========================================================================= *\
|
||||||
|
|* === Main functionality === *|
|
||||||
|
\* ========================================================================= */
|
||||||
|
|
||||||
|
let getGunData = (gunaddr) => {
|
||||||
|
return new Promise(
|
||||||
|
(resolve, reject) => {
|
||||||
|
LR.log(
|
||||||
|
pluginName,
|
||||||
|
'getGunData()\n',
|
||||||
|
`+-- gunUser : ${typeof gunUser}\n`,
|
||||||
|
`+-- gunaddr[] : ${gunaddr}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// get the data
|
||||||
|
gunUser
|
||||||
|
.get(gunaddr[0])
|
||||||
|
.get(gunaddr[1])
|
||||||
|
.once(function(addr){
|
||||||
|
if (typeof addr !== 'undefined') {
|
||||||
|
LR.log(pluginName, `IPFS address: "${addr}"`);
|
||||||
|
resolve(addr);
|
||||||
|
} else {
|
||||||
|
// looks like we didn't get anything
|
||||||
|
reject(new Error('IPFS address is undefined for: ' + gunaddr[1]))
|
||||||
|
}
|
||||||
|
// ToDo: what happens when we hit the timeout here?
|
||||||
|
}, {wait: 5000});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the workhorse of this plugin
|
||||||
|
*/
|
||||||
|
async function getContentFromGunAndIPFS(url) {
|
||||||
|
|
||||||
|
await setup_gun_ipfs();
|
||||||
|
|
||||||
|
var urlArray = url.replace(/https?:\/\//, '').split('/')
|
||||||
|
var gunaddr = [urlArray[0], '/' + urlArray.slice(1).join('/')]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* if the gunaddr[1] ends in '/', append 'index.html' to it
|
||||||
|
*/
|
||||||
|
if (gunaddr[1].charAt(gunaddr[1].length - 1) === '/') {
|
||||||
|
LR.log(pluginName, "path ends in '/', assuming 'index.html' should be appended.");
|
||||||
|
gunaddr[1] += 'index.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
// inform
|
||||||
|
LR.log(
|
||||||
|
pluginName,
|
||||||
|
`starting Gun lookup of: ${gunaddr.join(', ')}\n`,
|
||||||
|
`+-- gun : ${typeof gun}\n`,
|
||||||
|
`+-- gunUser : ${typeof gunUser}`
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* naïvely assume content type based on file extension
|
||||||
|
* TODO: this needs a fix
|
||||||
|
*/
|
||||||
|
var contentType = '';
|
||||||
|
switch (gunaddr.slice(-1)[0].split('.', -1)[1].toLowerCase()) {
|
||||||
|
case 'html':
|
||||||
|
case 'htm':
|
||||||
|
contentType = 'text/html';
|
||||||
|
break;
|
||||||
|
case 'css':
|
||||||
|
contentType = 'text/css';
|
||||||
|
break;
|
||||||
|
case 'js':
|
||||||
|
contentType = 'text/javascript';
|
||||||
|
break;
|
||||||
|
case 'json':
|
||||||
|
contentType = 'application/json';
|
||||||
|
break;
|
||||||
|
case 'svg':
|
||||||
|
contentType = 'image/svg+xml';
|
||||||
|
break;
|
||||||
|
case 'ico':
|
||||||
|
contentType = 'image/x-icon';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
LR.log(pluginName, " +-- guessed contentType : " + contentType);
|
||||||
|
|
||||||
|
return getGunData(gunaddr).then(ipfsaddr => {
|
||||||
|
LR.log(pluginName, `starting IPFS lookup of: '${ipfsaddr}'`);
|
||||||
|
LR.log(pluginName, `ipfs is: '${ipfs}'`);
|
||||||
|
return ipfs.get(ipfsaddr).next();
|
||||||
|
}).then(file => {
|
||||||
|
// we only need one
|
||||||
|
if (file.value.content) {
|
||||||
|
async function getContent(source) {
|
||||||
|
var content = new Uint8Array()
|
||||||
|
var data = await source.next()
|
||||||
|
while (! data.done) {
|
||||||
|
var newContent = new Uint8Array(content.length + data.value.length);
|
||||||
|
newContent.set(content)
|
||||||
|
newContent.set(data.value, content.length)
|
||||||
|
content = newContent
|
||||||
|
data = await source.next()
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
return getContent(file.value.content).then((content)=>{
|
||||||
|
LR.log(pluginName, `got a Gun-addressed IPFS-stored file: ${file.value.path}\n+-- content is: ${typeof content}`);
|
||||||
|
// creating and populating the blob
|
||||||
|
var blob = new Blob(
|
||||||
|
[content],
|
||||||
|
{'type': contentType}
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
blob,
|
||||||
|
{
|
||||||
|
'status': 200,
|
||||||
|
'statusText': 'OK',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'ETag': file.value.path,
|
||||||
|
'X-LibResilient-Method': pluginName,
|
||||||
|
'X-LibResilient-ETag': file.value.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ========================================================================= *\
|
||||||
|
|* === Publishing stuff === *|
|
||||||
|
\* ========================================================================= */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* these are used for adding content to IPFS and Gun
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adding stuff to IPFS
|
||||||
|
* accepts an array of URLs
|
||||||
|
*
|
||||||
|
* returns a Promise that resolves to an object mapping URLs to IPFS hashes
|
||||||
|
*/
|
||||||
|
let addToIPFS = (resources) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
LR.log(pluginName, "adding to IPFS...")
|
||||||
|
LR.log(pluginName, "+-- number of resources:", resources.length)
|
||||||
|
var ipfs_addresses = new Map();
|
||||||
|
|
||||||
|
resources.forEach(function(res){
|
||||||
|
LR.log(pluginName, " +-- handling internal resource:", res)
|
||||||
|
|
||||||
|
ipfs.add(Ipfs.urlSource(res))
|
||||||
|
.then((result) => {
|
||||||
|
// add to the list -- this is needed to add stuff to Gun
|
||||||
|
// result.path is just the filename stored in IPFS, not the actual path!
|
||||||
|
// res holds the full URL
|
||||||
|
// what we need in ipfs_addresses is in fact the absolute path (no domain, no scheme)
|
||||||
|
var abs_path = res.replace(window.location.origin, '')
|
||||||
|
ipfs_addresses.set(abs_path, '/ipfs/' + result.cid.string)
|
||||||
|
LR.log(pluginName, "added to IPFS: " + abs_path + ' as ' + ipfs_addresses.get(abs_path))
|
||||||
|
// if we seem to have all we need, resolve!
|
||||||
|
if (ipfs_addresses.size === resources.length) resolve(ipfs_addresses);
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* verification that content pushed to IPFS
|
||||||
|
* is, in fact, available in IPFS
|
||||||
|
*
|
||||||
|
* a nice side-effect is that this will pre-load the content on
|
||||||
|
* a gateway, which tends to be a large (and fast) IPFS node
|
||||||
|
*
|
||||||
|
* this is prety naïve, in that it pulls the content from an ipfs gateway
|
||||||
|
* and assumes all is well if it get a HTTP 200 and any content
|
||||||
|
*
|
||||||
|
* that is, it does *not* check that the content matches what was pushed
|
||||||
|
* we trust IPFS here, I guess
|
||||||
|
*
|
||||||
|
* finally, we're using a regular fetch() instead of just going through our
|
||||||
|
* ipfs object because our IPFS object might have things cached and we want
|
||||||
|
* to test a completey independent route
|
||||||
|
*
|
||||||
|
* takes a object mapping paths to IPFS addresses
|
||||||
|
* and returns a Promise that resolves to true
|
||||||
|
*/
|
||||||
|
let verifyInIPFS = (ipfs_addresses) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
LR.log(pluginName, 'checking IPFS content against a gateway...')
|
||||||
|
LR.log(pluginName, '+-- gateway in use: ' + config.ipfsGateway)
|
||||||
|
// get the list of IPFS addresses
|
||||||
|
var updatedPaths = Array.from(ipfs_addresses.values())
|
||||||
|
for (const path of ipfs_addresses.keys()) {
|
||||||
|
// start the fetch
|
||||||
|
fetch(config.ipfsGateway + ipfs_addresses.get(path))
|
||||||
|
.then((response) => {
|
||||||
|
ipfsaddr = response.url.replace(config.ipfsGateway, '')
|
||||||
|
if (response.ok) {
|
||||||
|
LR.log(pluginName, '+-- verified: ' + ipfsaddr)
|
||||||
|
var pathIndex = updatedPaths.indexOf(ipfsaddr)
|
||||||
|
if (pathIndex > -1) {
|
||||||
|
updatedPaths.splice(pathIndex, 1)
|
||||||
|
}
|
||||||
|
if (updatedPaths.length === 0) {
|
||||||
|
LR.log(pluginName, 'all updates confirmed successful!')
|
||||||
|
resolve(ipfs_addresses);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error('HTTP error (' + response.status + ' ' + response.statusText + ' for: ' + ipfsaddr))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// it would be nice to have the failed path here somehow
|
||||||
|
// alternatively, updating updatedPaths with info on failed
|
||||||
|
// requests might work
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* auth a Gun admin user
|
||||||
|
* (and verify it's the correct one with regards to the configured config.gunPubkey)
|
||||||
|
*/
|
||||||
|
let authGunAdmin = (user, pass) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// we need a separate Gun instance, otherwise gu will get merged with gunUser
|
||||||
|
// and we want these to be separate
|
||||||
|
var g = Gun(config.gunNodes)
|
||||||
|
var gu = g.user()
|
||||||
|
gu.auth(user, pass, (userReference) => {
|
||||||
|
if (userReference.err) {
|
||||||
|
reject(new Error(userReference.err))
|
||||||
|
// reality check -- does it match our preconfigured pubkey?
|
||||||
|
} else if (gu._.soul.slice(1) === config.gunPubkey) {
|
||||||
|
LR.log(pluginName, 'Gun admin user authenticated using password.');
|
||||||
|
// we need to keep the reference to g, otherwise gu becomes unusable
|
||||||
|
var gApi = {
|
||||||
|
user: gu,
|
||||||
|
gun: g
|
||||||
|
}
|
||||||
|
resolve(gApi)
|
||||||
|
} else {
|
||||||
|
reject(new Error('Password-authenticated user does not match preconfigured pubkey!'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// everything else -- that's a paddlin'!
|
|
||||||
throw new TypeError("Only accepts: string, Array of string, Response.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// add to IPFS
|
|
||||||
var ipfsPromise = addToIPFS(resource)
|
|
||||||
return Promise.all([
|
|
||||||
// verify stuff ended up in IPFS
|
|
||||||
ipfsPromise.then(verifyInIPFS),
|
|
||||||
// add to Gun and verify Gun updates propagation
|
|
||||||
ipfsPromise.then((hashes) => {
|
|
||||||
addToGun(user, password, hashes)
|
|
||||||
})
|
})
|
||||||
])
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* ========================================================================= *\
|
/**
|
||||||
|* === Initialization === *|
|
* add IPFS addresses to Gun
|
||||||
\* ========================================================================= */
|
*/
|
||||||
|
let addToGun = (user, pass, ipfs_addresses) => {
|
||||||
|
// we need an authenticated Gun user
|
||||||
|
return authGunAdmin(user, pass)
|
||||||
|
.then((gunAPI) => {
|
||||||
|
LR.log(pluginName, '+-- adding new IPFS addresses to Gun...')
|
||||||
|
// TODO BUG: this will probably fail, or cause failures down the line
|
||||||
|
// TODO BUG: we're setting a Map() object here; it's unclear how GunDB
|
||||||
|
// TODO BUG: will handle this
|
||||||
|
gunAPI.user.get(window.location.host).put(ipfs_addresses /*, function(ack) {...}*/);
|
||||||
|
return gunAPI;
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* regular confirmations don't seem to work
|
||||||
|
*
|
||||||
|
* so instead we're using the regular read-only Gun user
|
||||||
|
* to .get() the data that we've .put() just a minute ago
|
||||||
|
*
|
||||||
|
* we then subscribe to the .on() events and once we notice the correct
|
||||||
|
* addresseswe consider our job done and quit.
|
||||||
|
*/
|
||||||
|
.then((gunAPI) => {
|
||||||
|
// get the paths
|
||||||
|
LR.log(pluginName, '+-- starting verification of updated Gun data...')
|
||||||
|
var updatedPaths = Array.from(ipfs_addresses.values())
|
||||||
|
for (const path of ipfs_addresses) {
|
||||||
|
LR.log(pluginName, ' +-- watching: ' + path)
|
||||||
|
//debuglog('watching path for updates:', path)
|
||||||
|
// using the global gunUser to check if updates propagated
|
||||||
|
gunUser.get(window.location.host).get(path).on(function(updaddr, updpath){
|
||||||
|
/*debuglog('+--', updpath)
|
||||||
|
debuglog(' updated :', ipfs_addresses[updpath])
|
||||||
|
debuglog(' received :', updaddr)*/
|
||||||
|
if (ipfs_addresses.get(updpath) == updaddr) {
|
||||||
|
// update worked!
|
||||||
|
gunUser.get(window.location.host).get(updpath).off()
|
||||||
|
LR.log(pluginName, '+-- update confirmed for:', updpath, '[' + updaddr + ']')
|
||||||
|
var pathIndex = updatedPaths.indexOf(updpath)
|
||||||
|
if (pathIndex > -1) {
|
||||||
|
updatedPaths.splice(pathIndex, 1)
|
||||||
|
}
|
||||||
|
if (updatedPaths.length === 0) {
|
||||||
|
LR.log(pluginName, 'all updates confirmed successful!')
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// we probably need to handle this better
|
/**
|
||||||
setup_gun_ipfs();
|
* example code for of adding content to IPFS, verifying it was successfully added,
|
||||||
|
* and adding the new addresses to Gun (and verifying changes propagated)
|
||||||
|
*
|
||||||
|
* TODO: this should accept a URL, a Response, or a list of URLs,
|
||||||
|
* and handle stuff appropriately
|
||||||
|
*/
|
||||||
|
let publishContent = (resource, user, password) => {
|
||||||
|
|
||||||
|
if (typeof resource === 'string') {
|
||||||
|
// we need this as an array of strings
|
||||||
|
resource = [resource]
|
||||||
|
} else if (typeof resource === 'object') {
|
||||||
|
if (!Array.isArray(resource)) {
|
||||||
|
// TODO: this needs to be implemented such that the Response is used directly
|
||||||
|
// but that would require all called functions to also accept a Response
|
||||||
|
// and act accordingly; #ThisIsComplicated
|
||||||
|
throw new Error("Handling a Response: not implemented yet")
|
||||||
|
} else {
|
||||||
|
resource.forEach((res)=>{
|
||||||
|
if (typeof res !== 'string') {
|
||||||
|
throw new TypeError("Only accepts: string, Array of string, Response.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// everything else -- that's a paddlin'!
|
||||||
|
throw new TypeError("Only accepts: string, Array of string, Response.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to IPFS
|
||||||
|
var ipfsPromise = addToIPFS(resource)
|
||||||
|
return Promise.all([
|
||||||
|
// verify stuff ended up in IPFS
|
||||||
|
ipfsPromise.then(verifyInIPFS),
|
||||||
|
// add to Gun and verify Gun updates propagation
|
||||||
|
ipfsPromise.then((hashes) => {
|
||||||
|
addToGun(user, password, hashes)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
// and add ourselves to it
|
/* ========================================================================= *\
|
||||||
// with some additional metadata
|
|* === Initialization === *|
|
||||||
self.LibResilientPlugins.push({
|
\* ========================================================================= */
|
||||||
name: config.name,
|
|
||||||
description: 'Decentralized resource fetching using Gun for address resolution and IPFS for content delivery.',
|
// and add ourselves to it
|
||||||
version: 'COMMIT_UNKNOWN',
|
// with some additional metadata
|
||||||
fetch: getContentFromGunAndIPFS,
|
return {
|
||||||
publish: publishContent
|
name: pluginName,
|
||||||
})
|
description: 'Decentralized resource fetching using Gun for address resolution and IPFS for content delivery.',
|
||||||
|
version: 'COMMIT_UNKNOWN',
|
||||||
|
fetch: getContentFromGunAndIPFS,
|
||||||
|
publish: publishContent
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
// done with not polluting the global namespace
|
// done with not polluting the global namespace
|
||||||
})()
|
})(LibResilientPluginConstructors)
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue