Open rjcorwin opened 6 years ago
@chrisekelley @RangerMauve
Here's a video walking through my fork of dat-archive-web-test (https://github.com/rjsteinert/dat-archive-web-test) which I think can serve as a hello world example at least for now of this feature working. The trick is creating an archive from one archive's context, navigating to another archive and still having write permission on that created archive.
dat://dat-archive-web-test-video-rjsteinert.hashbase.io/dat-archive-web-test.mov
@rjsteinert The video and demo code illustrate the issue very well: without a common place to store the private keys when a dat archive is forked, the user cannot gain access to write to it. Where would we store those keys?
I'm moving towards the react-native option at the moment - I've already seen another dat demo in react-mobile (the dat installer) so there may be some code that could guide us. Going this route would make baby Jesus cry a little because we'd be straying from our "view source" ethos. But the current solution is a bit hacky - stuffing the whole dat into poor indexdb - what happens when it's a really big dat? I would expect that going the react-native route would take bunsen out of the proof-of-concept/test lab/must-wear-your-shop-glasses phase into a something more stable. But since I know almost nothing about react-native, and have only exchanged furtive glances with react, I'd defer to others on the real-world benefits of using react-native.
Nice demo! Can't wait for it to be functional. :D
With regards to key storage, the keys are being persisted in the random-access-storage
by Hyperdrive when a new one gets created, so any storage would work. It might be a good idea to add access control restrictions by only giving read access to origins that have created the dat, or have been given access via selectArchive
. This can be done by wrapping random-access-storage
and redirecting to an invalid file name when we detect the current origin doesn't have write access to that dat. I think for now we can cheat and worry about that later.
I think that the current approach of using an iFrame gives us a lot of benefits for rapidly innovating that we would lose going to RN. If the iframe IndexedDB issue is sorted out, I think going with this will be good for a proof of concept. I don't think most dats are going to be storing huge amounts of data, and if that becomes a problem we can start caching most dats (ie not the ones users created themselves) in memory and clearing the cache periodically.
I think IndexedDB is being used by progressive web apps to do caching, so if it's good enough for those use cases, it should be good enough for many of the sites that will be built on dat. I think the only real worry will be with huge files like high res images and videos.
However, with RN we could store the data right on the filesystem much like how Beaker does it at the moment which I think would be necessary for a finished product. Luckily, a lot of the logic that's being developed in the web version could be ported over to RN. The main thing that will change is the UI, how Node.js gets started, and the random-access-storage. I think the most difficult part of this will be the UI transition. I've been doing React at work for just over a year now, and if you use plain old react with out redux or any of that other fancy crap people use, it's not hard to get started with.
Oh, with regards to the window.location
issue, I was thinking that we could modify dat-archive-web to detect redirected URLs and automatically do the decoding. So even if you pass http://rcxwhny359q90p85xzdz47qyn7zj7c0d3gzekbn8029enekc95bg.gateway.mauve.moe:3000
, it'll know to convert it to dat://c33bc8d7c32a6e905905efdbf21efea9ff23b00d1c3ee9aea80092eaba6c4957
https://github.com/RangerMauve/dat-archive-web/issues/3
Moving forward on the iframe demo, what is our next step - to use RJ's fork of dat-archive-web-test (https://github.com/rjsteinert/dat-archive-web-test) and apply Mauve's demo for dat-polyfill to it? I can spend some time on this over the weekend.
So, what you'll need to do is implement this line in your iframe, and include a compiled version of dat-polyfill
in your child frame.
Basically, you pass in the parent window and child iframe
as an arguments, and an object with the random-access-storage
instance (I suggest random-access-idb), and a function for addArchive
to save the info about newly created archives somewhere locally, and selectArchive
which will show some sort of UI for the user to choose from the list of archives and invoke the callback with the URL.
You can pass the gateway URL to the child by appending ?DAT_GATEWAY=http%3A//gateway.mauve.moe%3A3000
to the child URL. If no gateway is supplied, it will attempt to auto-detect it from window.location
or fallback to http:localhost:3000
The rest, like providing the global DatArchive API and proxying the storage to the parent, is handled by the polyfill.
Thanks for the tips - I forked dat-polyfill and am trying to understand the flow. I added a function to get the dat url from the input box and fork the archive, but I now think I must do the forking from the child frame - is that correct? Here is the relevant code:
https://github.com/chrisekelley/dat-polyfill/blob/master/demo/server/index.js
document.addEventListener('DOMContentLoaded', (event) => {
console.log('DOMContentLoaded')
const searchBox = document.getElementById('search')
const goButton = document.getElementById('go')
const frame = document.getElementById('client-frame')
searchBox.value = wikiDat
goButton.addEventListener('click', addDat)
const server = new RPC.Server(window, frame.contentWindow, {
storage,
addArchive,
selectArchive
})
window.gatewayServer = server
})
async function addDat () {
const searchBox = document.getElementById('search')
const datAddress = searchBox.value
console.log('added ' + datAddress)
let forkedArchive = await DatArchive.fork(datAddress)
}
I'm thinking this is the wrong approach because DatArchive is not exposed in the parent (aka server) bundle.js. - my last line throws an error. So - should I postMessage to the child iframe the dat url and DAT_GATEWAY? I'd also need to add an index.js to the client and add an event listener for this message.
I just changed it so that forking is happening on the child iframe: in server/index.js:
async function addDat () {
const searchBox = document.getElementById('search')
const datAddress = searchBox.value
console.log('added ' + datAddress)
// let forkedArchive = await DatArchive.fork(datAddress)
frame.contentWindow.postMessage(datAddress, '*');
}
and in client/index.js:
async function receiveMessage (event) {
// Do we trust the sender of this message?
// if (event.origin !== "http://example.com:8080")
// return;
console.log(event.data)
let data = event.data
if (data.startsWith('dat:')) {
let datAddress = data
let forkedArchive = await DatArchive.fork(datAddress)
console.log('we forked!')
}
}
window.addEventListener('message', receiveMessage, false)
I now need to check if the message is a string; startsWith is blowing up; After DatArchive.fork is run, event.data is probably returning the forkedArchive :-)
I did a little more work on it, sorted the blowing up, here is what I see in the Application panel:
The app never makes it to the console.log('we forked!')
part - I was hoping to be able to inspect the forkedArchive - but I do think that the polyfill/dat-archive-web are copying the dat and forking it to indexdb.
It might be a good idea to add access control restrictions by only giving read access to origins that have created the dat, or have been given access via selectArchive
@RangerMauve I was thinking the same at first but have realized we might not have to do anything as long as an app can't query for "what private keys do we have?" It's the same security model of Dat itself, if you don't know the public key, you can't guess it.
I think that the current approach of using an iFrame gives us a lot of benefits for rapidly innovating that we would lose going to RN. ... I think IndexedDB is being used by progressive web apps to do caching, so if it's good enough for those use cases, it should be good enough for many of the sites that will be built on dat. ... However, with RN we could store the data right on the filesystem much like how Beaker does it at the moment which I think would be necessary for a finished product. Luckily, a lot of the logic that's being developed in the web version could be ported over to RN.
@RangerMauve Well put, I agree. RN would make a great Bunsen 2.0 (or 1.0) initiative.
I've been doing React at work for just over a year now, and if you use plain old react with out redux or any of that other fancy crap people use, it's not hard to get started with.
Haha I believe it. Luckily Chris and I have been using Redux with Web Components for the past year on the Tangerine project so we have some familiarity with it if that is something we want to go with. Doing Redux with Web Components has made me really want to switch to React. Our code is much more complicated than if we had written with React because you can't just write one easy to understand render function for performance reasons. Luckily there is light at the end of the tunnel for Web Components using Lit HTML!
In regards to goals for next step, I'm hoping to see get the following scenario working.
dat://dat-archive-web-test-rjsteinert.hashbase.io/
into the navigation bar in Mauve's dat-polyfill demo server app and hit go
.Click to fork this archive
button in the app itself.go
.At the moment I'm getting stuck trying to navigate to a Dat Archive using Mauve's dat-polyfill demo server app. I enter an address in the address bar, hit go, and then I get a new tab with the same content.
Hey, sorry. I didn't actually implement the navigation yet. All it does is load the client.html
in an iframe.
To test it I open the dev tools and load the iframe context and test the DatArchive API from there.
Ideally if you have something (like the dat gateway) that can load a site that's included the bundled polyfill, you could load that into the iframe instead (my adjusting the source manually)
@chrisekelley Don'y use postframe directly, have the library set up the server instead. (it handles the RPC over postmessage, and adding other messages might mess it up)
Ideally you should do all your dat creation logic within the child frame, and the child frame should have a copy of dat-polyfill loaded up.
@RangerMauve doh! copy/paste error from a different version of this demo. I just cleaned it up.
Dat creation happens in the child iframe. Here is the code form child/index.js:
async function receiveMessage (event) {
// console.log(event.data)
let data = event.data
if (typeof data === 'string' && data.startsWith('dat:')) {
let datAddress = data
let forkedArchive = await DatArchive.fork(datAddress)
console.log('we forked!')
}
}
window.addEventListener('message', receiveMessage, false)
It waits for a message from the parent, which has the dat archive url to fork. It's just a forkin' machine. But once it is forked, how can we access this forked archive?
@chrisekelley Check out the indexeddb storage in the parent. You should see a single DB that has the contents of all the dats.
This means that when you do the fork, the forked data is saved to indexeddb. That means that a different frame can take the forked URL and create a dat instance from it.
let forkedArchive = await DatArchive.fork(datAddress)
console.log('we forked to', forkedArchive.url)
// Later, copy the URL
const forkedURL = ""; // Paste here
const theArchive = new DatArchive(forkedURL); // Is the same archive
BTW, if you use the master
branch of dat-archive-web from github, you will be able to use new DatArchive(window.location)
or DatArchive.fork(window.location)
for dats served from a gateway. :D
Yeah, cool, I do see the indexdb in the parent. I'm debugging why let forkedArchive = await DatArchive.fork(datAddress)
is not returning the forkedArchive, and I turned up something cool in your code:
archive._archive.key.toString('hex'),
archive._archive.metadata.secretKey.toString('hex'),
Well that sure is handy!
This is from
await DatArchive._manager.onAddArchive(
archive._archive.key.toString('hex'),
archive._archive.metadata.secretKey.toString('hex'),
{title, description, type, author}
)
@chrisekelley Yeah, you can set the addArchive handler when you create the server for the polyfill. :D
I wouldn't rely on the archive._archive
property if you can. It'll break in regular Beaker apps. :P
What is the return value of .fork()
? Are you getting any errors? I haven't tested forking all that much to be honest.
@RangerMauve ahhh yeah, I was just looking at addArchive, and setArchives pops the keys into localstorage - very nice!
It's not making it out of fork - trying to track it down, that's how I found the rpc call to addArchive.
In regards to fork(), I think it is failing somewhere during const destDat = await DatArchive.create(opts)
- but it does get as far as hitting addArchive.
Have you tried wrapping everything in a try catch to see if an error is being thrown and ignored because of promises?
Sadly I think I'm going to be busy this evening but I'll see if I can reproduce it later tonight or tomorrow.
Good idea on the try/catch, but so far nothing is getting thrown. I think an error is happening in this part of DatArchive.js create():
await DatArchive._manager.onAddArchive(
archive._archive.key.toString('hex'),
archive._archive.metadata.secretKey.toString('hex'),
{title, description, type, author}
)
It could be happening in addArchives's callback in rpc.js Client:
addArchive (key, secretKey, options, callback) {
this._rpc.call('addArchive', key, secretKey, options, callback)
}
The code for the callback is here:
RPC.prototype._handle = function (msg) {
-- snip --
var args = msg.arguments.concat(function () {
self._dstPostMessage({
protocol: 'frame-rpc',
version: VERSION,
response: msg.sequence,
arguments: [].slice.call(arguments)
});
-- snip --
Does your server's addArchive implementation invoke the callback? If you don't invoke the callback it will stall.
No, it's the same code as the one in your master:
function addArchive (key, secretKey, options, callback) {
const archiveList = getArchives()
archiveList.push({
key,
secretKey,
details: options
})
setArchives(archiveList)
}
So, should I callback()
after setArchives(archiveList)
?
Answered the question myself - YES! Things are working :-)
Whoops! I probably should have tested it more. 😅
YES - here is the console output. Forked dat opens in Beaker browser.
DOMContentLoaded
index.js:34 added dat://wysiwywiki-pfrazee.hashbase.io
index.js:54 setting Archives for key: 4db84001b9bc19692fdfd873b015589207e795bbe660e7ee4272cbc305bfd5fe
DatArchive.js:318 finished writing the archive.
index.js:8 we forked to dat://4db84001b9bc19692fdfd873b015589207e795bbe660e7ee4272cbc305bfd5fe
Next step - mess around with the keys that are in LocalStorage. I want to see if the app can open up the forked archive and write to it.
We're getting there!
Here's the data stored in LocalStorage - includes the secret key for the forked archive:
Also see that in Indexeddb is dat://storage - i'm assuming we would use random-access-idb to browse these.
Yeah, I don't think we'd really be browsing it manually, I think it'd be easier to leave it as a general storage for dats. Maybe we'd want to mess with it in order to clear old data, though.
Ahh yes of course - we'd use a higher-level API to access the dats - something like this?
const contents = `
<title>Gateway Test</title>
<p>Hello World!</p>
`
const archive = new DatArchive('dat://4db84001b9bc19692fdfd873b015589207e795bbe660e7ee4272cbc305bfd5fe')
await archive.writeFile('index.html', contents)
console.log('Wrote index.html')
Yeah!
Eventually you'd want to pair that with DatArchive.selectArchive()
to make it more user-friendly.
Cool - I added DatArchive.selectArchive()
to my client/index.js, and now the checkbox displays - tweaks in this commit - and I can submit and handleSelected() does its thing.
I also see that there is some websockets action going on:
The UI hides the form w/ checkbox but nothing else happens in the UI. I see that some useful stuff happens in handleSelected:
if (currentSelection) {
const input = form.querySelector('input:checked')
const url = `dat://${input.value}`
currentSelection.callback(false, url)
currentSelection = null
}
On currentSelection.callback(false, url)
- I think that loads the archive - but how do I access it?
In the client iframe, should I listen for an event upon the success of currentSelection.callback(false, url)
and access the forkedArchive with new DatArchive(window.location)?
On the client side it should look something like
var theArchive = await DatArchive.selectArchive({})
Basically, the call to selectArchive creates a promise then sends an RPC to the parent with a callback to get the URL of the dat to load.
The parent shows the UI for choosing an archive and when the user chooses one, it invokes the callback to send an RPC back to the child frame with the URL
The child frame can now resolve the promise for the URL, invoke new DatURL(theurl)
and return it.
Relevant code is:
The goal with the polyfill was to make the client-side code look exactly the same as it would in beaker once the polyfill is included. :D
This forking / selecting archives / creating archives should work exactly the same way as it would in beaker.
Ideally the code you write should be able to load in beaker, skip creating the global DatArchive variable (since Beaker already defines it), and work exactly as it does in the demo with the polyfill, just with Beaker's selectArchive
implementation.
By the way, regarding size limits, by default Beaker only allows 100mb of storage for a dat archive.
Now I think I understand much better the promise part - thank . you so much for going through that.
Once I have the result of selectArchive, it is necessary to invoke new DatArchive(url), since selectArchive returns an archive?
I experimented with writing to this archive, but looks like I may need to do some business w/ the secretkey.
Revised code in client/index.js
let archive = await DatArchive.selectArchive({})
let url = archive.url
console.log('primed selectArchive for ' + url)
// necessary to create another archive, when we've just been handed one?
let nuArchive = await new DatArchive(url)
const contents = `
<title>Gateway Test</title>
<p>Hello World!</p>
`
await archive.writeFile('sayHello.html', contents)
console.log('Wrote sayHello.html to ' + nuArchive.url)
returns ArchiveNotWritableError: Cannot write to this archive ; Not the owner
I don't think you need to create a new archive from the URL since it should just work from the existing one.
It's kinda weird that you can't write to the new archive, though. :/
Try to add an await nuArchive._loadPromise
to make sure it's fully initialized.
Make sure that you're selecting the archive that got created as a result of .fork()
or .create()
. Otherwise you won't have write access to it.
Just to clarify, when you `fork() an archive, you're going to be creating a new one with all the contents copied over. It'll have a different URL and should have the private key stored in the indexeddb storage in the parent.
I tightened up the code and added the archive._loadPromise, but still don't have writable permissions on the archive. I did check if I'm trying to write to the forked version - here are the logs:
added dat://wysiwywiki-pfrazee.hashbase.io
VM5192 bundle.js:55 setting Archives for key: e01e76457c5f6a839e53d75191aba7153a963f524dfadcd1e70814a11b5c2889
VM5272 index.js:8 we forked to dat://e01e76457c5f6a839e53d75191aba7153a963f524dfadcd1e70814a11b5c2889
VM5272 index.js:10 hey!
Navigated to http://127.0.0.1:8080/server/
index.js:24 DOMContentLoaded
index.js:27 DOMContentLoaded in client
index.js:19 dat: dat://e01e76457c5f6a839e53d75191aba7153a963f524dfadcd1e70814a11b5c2889
index.js:31 primed selectArchive for dat://e01e76457c5f6a839e53d75191aba7153a963f524dfadcd1e70814a11b5c2889
index.js:43 Error: ArchiveNotWritableError: Cannot write to this archive ; Not the owner
Here is the localStorage:
Here is indexedDB - look at that uuid - shouldn't it be e01e76457c5f6a839e53d75191aba7153a963f524dfadcd1e70814a11b5c2889? It is 46c09171de62c2db83360cb8a81a4d61f4e280c994bf3e2f3a1bafb88eb13979.
The raw dat address of Paul's wysiwyg is dat://46c09171de62c2db83360cb8a81a4d61f4e280c994bf3e2f3a1bafb88eb13979/
Are files actually being copied into the fork? Can you view anything? The indexedDB instance will hold all keys, scroll down to see if the forked data is there.
Try changing dat-archive-web around here so that you await srcDat._loadPromise
to make sure it's all ready.
Maybe it's forking before the original archive is actually loaded. That wouldn't explain the inability to write.
Would you mind linking me to the latest source? I'm gonna be AFK for a bit but I'll take a look in a few hours. Sadly this has been a super busy week for me. 😂
I am not sure - it's a bunch of Uint8Arrays:
I figured this was just the storage format
I'll try out the dat-archive-web change.
I pushed all of my changes to my fork -dat-polyfill/
Hey, thanks a lot for walking through this with me! No rush :-)
You should be able to see keys that start with the forked dat URL. If you're not seeing them then the fork didn't work.
OK - I made one change - pointed DAT_GATEWAY to my instance at localhost instead of to your .moe gateway:
<iframe id="client-frame" src="../client/index.html?DAT_GATEWAY=http%3A//localhost%3A3000" class="client-frame" seamless="seamless"></iframe>
and now it is populating the correct dat:
But I still get this error: index.js:43 Error: ArchiveNotWritableError: Cannot write to this archive ; Not the owner
Update: now that is no longer working, back to 46c09171de62c2db83360cb8a81a4d61f4e280c994bf3e2f3a1bafb88eb13979...
I just checked window.gatewayServer and window.gatewayServer while in the client iframe, and they are uninitialized, whereas they are set in server/index.js. I don't see those being used anywhere, so they may not matter, but it got me thinking....is my fork in client getting created in the correct format or where it should be?
I am doing the forking in client/index.js when I receive a postMessage from the parent providing the url
async function receiveMessage (event) {
// console.log(event.data)
let data = event.data
if (typeof data === 'string' && data.startsWith('dat:')) {
let datAddress = data
try {
let forkedArchive = await DatArchive.fork(datAddress)
console.log('we forked to', forkedArchive.url)
// let theArchive = await DatArchive.selectArchive({})
console.log('hey!')
parent.location.reload()
} catch (e) {
console.log('Error: ' + e)
}
} else {
-- snip --
}
}
I'm not 100% sure what's up to be honest. I think we'll need to read up on how it gets the private key and determines that it's the owner.
I'm going to be gone this weekend so I don't think I'll be able to help too much until Monday.
Try bugging the people in the dat gitter or IRC about the writability. Mention that we're using random-access-idb to store the hyperdrive data, but when we open it up again it thinks that it isn't the owner.
Hey, I see nobody in the gitter seemed to get back to you. :(
It seems the error is happening because the archive doesn't have the 'writable' flag set.
That flag is defined in hyperdrive as being a getter for metadata.writable
metadata
is a hypercore instance for the key and the folder for storing it.
It seems this property gets set either from opts.writable
, which can't be passed into the metadata, or by detecting the secret key when it tries to load the signature when opening.
It will be loading the data from the metadata folder in the storage.
It seems that when the storage is opened it will attempt to load the secret key from the /metadata/secret_key
file
There could be a problem at any one of those steps. I think the first thing you can do to make sure it works is to check that the metadata/secret_key
file is being written to the IDB storage.
Weird, it works fine with just DatArchiveWeb.
Gonna see if I have time to run the demo locally. :D
Reproduced it, it isn't able to load the secret key for some reason.
Getting the following error:
Error: Not opened
at Request._openAndNotClosed (http://localhost:3030/client/bundle.js:41764:34)
at Request._run (http://localhost:3030/client/bundle.js:41792:16)
at Request._unqueue (http://localhost:3030/client/bundle.js:41752:50)
at Request.callback (http://localhost:3030/client/bundle.js:41757:8)
at RPCTransport.NoopTransport._next (http://localhost:3030/client/bundle.js:41571:11)
at _rpc.call (http://localhost:3030/client/bundle.js:53333:12)
at RPC._handle (http://localhost:3030/client/bundle.js:24446:20)
at RPC._onmessage (http://localhost:3030/client/bundle.js:24384:14)
Might be a problem with the random-access-network lib.
Oh man, this is pretty intense. :P
Here's how I reproduced it:
new DatArchive.create()
to make a new archivenew DatArchive()
to create another instance using the first oneI think something similar is happening with the demo
What we need to do:
Not sure if this is the only approach we can take, and why random-access-network doesn't allow multiple opened instances.
@RangerMauve don't hesitate to ping me in github I almost didn't see your message :p. Not sure what you meant by "multiple openend instances", I think it should definitely be able to do that. If you have a small reproduction case I might look into it!
Btw about random-access-idb, I remember you had issues but I tried out my version based on random-access-storage
and it worked. Just note that it has a slightly different signature in which you have to name the database before getting the ras
instance (const rai = require('random-access-idb')('test')
).
@RangerMauve's work on dat-archive-web and dat-polyfill has paved the way for allowing Bunsen to help apps using the polyfill to be backed by Bunsen. In particular the demo for dat-polyfill shows how Bunsen UI can help assist in providing a consistent domain for storage.