diff --git a/examples/transfer-files/.gitignore b/examples/transfer-files/.gitignore new file mode 100644 index 0000000000..74e9f3ccf0 --- /dev/null +++ b/examples/transfer-files/.gitignore @@ -0,0 +1 @@ +public/js/ipfs.js diff --git a/examples/transfer-files/README.md b/examples/transfer-files/README.md new file mode 100644 index 0000000000..7e94500edc --- /dev/null +++ b/examples/transfer-files/README.md @@ -0,0 +1,162 @@ +# Tutorial - Transfer files between the browser and other IPFS nodes + +> Welcome! This tutorial will help you exchange files between browser nodes and go-ipfs nodes. + +There are a couple of caveats: + +- js-ipfs currently doesn't support DHT peer discovery, the peer from which you are fetching data should be within the reach (local or in public IP) of the browser node. +- We need to use a signalling server to establish the WebRTC connections, this won't be necessary as soon as libp2p-relay gets developed + +That being said, we will explain throughout this tutorial to circunvent the caveats and once they are fixed, we will update the tutorial as well. + +## Application diagram + +The goal of this tutorial is to create a application with a IPFS node that dials to other instances of it using WebRTC, and at the same time dial and transfer files from a Desktop IPFS node using WebSockets as the transport. + +``` +┌──────────────┐ ┌──────────────┐ +│ Browser │ │ Browser │ +│ │ WebRTC │ │ +│ │◀─────────────────▶│ │ +└──────────────┘ └──────────────┘ + ▲ ▲ + │ │ + │WebSockets WebSockets│ + │ ┌──────────────┐ │ + │ │ Desktop │ │ + └───────▶│ │◀─────────┘ + └──────────────┘ +``` + +## Check out the final state + +In the end, you should get an app running, something like this: + +![](https://ipfs.io/ipfs/Qmbti2nBZWxQLhpggB7tC3HvcxTMivmMo3MVwQveAsHBAE) + +## Step-by-step instructions + +Here's what we are going to be doing, today: + +- 1. Set up, install a go-ipfs node in your machine +- 2. Make your daemons listen on WebSockets +- 3. Start the WebApp +- 4. Dial to a node using WebSockets (your Desktop ones) +- 5. Transfer files between all of your nodes, have fun! + +Let's go. + +### 1. Set up + +You'll need to have an implementation of IPFS running on your machine. Currently, this means either go-ipfs or js-ipfs. + +Installing go-ipfs can be done by installing the binary [here](https://ipfs.io/ipns/dist.ipfs.io/#go-ipfs). Alternatively, you could follow the instructions in the README at [ipfs/go-ipfs](https://github.com/ipfs/go-ipfs). + +Installing js-ipfs requires you to have node and [npm](https://www.npmjs.com). Then, you simply run: + +```sh +> npm install --global ipfs +... +> jsipfs --help +Commands: +... +``` + +This will alias `jsipfs` on your machine; this is to avoid issues with `go-ipfs` being called `ipfs`. + +At this point, you have either js-ipfs or go-ipfs running. Now, initialize it: + +```sh +> ipfs init +# or +> jsipfs init +``` + +This will set up your IPFS repo in your home directory. + +### 2. Make your daemons listen on WebSockets + +At this point, you need to edit your `config` file, the one you just set up with `{js}ipfs init`. It should be in either `~/.jsipfs/config` or `~/.ipfs/config`, depending on whether you're using JS or Go. + +Since websockets support is currently not on by default, you'll need to add a WebSockets address manually. Look into your config file and find the `Addresses` section: + +```json + "Addresses": { + "Swarm": [ + "/ip4/0.0.0.0/tcp/4002" + ], + "API": "/ip4/127.0.0.1/tcp/5002", + "Gateway": "/ip4/127.0.0.1/tcp/9090" + } +``` + +Add the following entry to your `Swarm` array: `/ip4/127.0.0.1/tcp/9999/ws`. Now, it should look like this: + +```json + "Addresses": { + "Swarm": [ + "/ip4/0.0.0.0/tcp/4002", + "/ip4/127.0.0.1/tcp/9999/ws" + ], + "API": "/ip4/127.0.0.1/tcp/5002", + "Gateway": "/ip4/127.0.0.1/tcp/9090" + } +``` + +Now it should listen on Websockets. We're ready to start the daemon. + +```sh +> ipfs daemon +``` + +(Again, either `jsipfs` or `ipfs` works. I'll stop repeting this from here on out.) + +You should see the Websocket address in the output: + +```sh +Initializing daemon... +Swarm listening on /ip4/127.0.0.1/tcp/4001 +Swarm listening on /ip4/127.0.0.1/tcp/9999/ws +Swarm listening on /ip4/192.168.10.38/tcp/4001 +Swarm listening on /ip4/192.168.10.38/tcp/9999/ws +API server listening on /ip4/127.0.0.1/tcp/5001 +Gateway (readonly) server listening on /ip4/0.0.0.0/tcp/8080 +Daemon is ready +``` + +It's there in line 5 - see the `/ws`? Good. that means it is listening. + +### 3. Start the WebApp project + +Now, you'll need to make sure you are in `js-ipfs/examples/transfer-files`. You'll see a `package.json`: this manifest holds the information for which packages you'll need to install to run the webapp. Let's install them, and then start the project: + +```sh +> npm install +> npm start +``` + +You should see this text: + +```sh +Starting up http-server, serving public +Available on: + http://127.0.0.1:12345 + http://192.168.1.24:12345 +Hit CTRL-C to stop the server +``` + +Go to http://127.0.0.1:12345 in your browser; you're now in the webapp, if all went well. + +### 4. Dial to a node using WebSockets (your Desktop ones) + +On your local node, run `ipfs id` to find the WebSockets address that it is listening on. Should look like this: `"/ip4/127.0.0.1/tcp/4003/ws/ipfs/". + +![](https://ipfs.io/ipfs/Qme9RM3SSyb57PGA7n5bEhwhMwS8fDrMZ8zzKkrwncRcfm) +![](https://ipfs.io/ipfs/QmdFX4wJkKpryisjGQGt88Yr8zaQM9zMPL3xzK2YGTUMNM) + +### 5. Transfer files between all of your nodes, have fun! + +Now you can drag an drop files on the browser or add them through the CLI with `ipfs add ` and with the fetch file box, you can retrieve the file to the browser or other browser tabs! + +![](https://ipfs.io/ipfs/QmcVNbhmMFzz9x2mY33GPGetibFGXXD7dYd3kDa7eKEUyw) +![](https://ipfs.io/ipfs/QmZcRvGQtM7mnSWKqFwptCYoBitBJaGBKLLjvzENfzXFMi) diff --git a/examples/transfer-files/img/connect-1.png b/examples/transfer-files/img/connect-1.png new file mode 100644 index 0000000000..cb89afbfed Binary files /dev/null and b/examples/transfer-files/img/connect-1.png differ diff --git a/examples/transfer-files/img/connect-2.png b/examples/transfer-files/img/connect-2.png new file mode 100644 index 0000000000..3f2c27320e Binary files /dev/null and b/examples/transfer-files/img/connect-2.png differ diff --git a/examples/transfer-files/img/diagram.monopic b/examples/transfer-files/img/diagram.monopic new file mode 100644 index 0000000000..35f88ab30f Binary files /dev/null and b/examples/transfer-files/img/diagram.monopic differ diff --git a/examples/transfer-files/img/diagram.txt b/examples/transfer-files/img/diagram.txt new file mode 100644 index 0000000000..05c9ff91cb --- /dev/null +++ b/examples/transfer-files/img/diagram.txt @@ -0,0 +1,19 @@ +┌──────────────┐ ┌──────────────┐ +│ Browser │ │ Browser │ +│ │ WebRTC │ │ +│ │◀─────────────────▶│ │ +│ │ │ │ +└──────────────┘ └──────────────┘ + ▲ ▲ + │ │ + │ │ + │ │ + │WebSockets WebSockets│ + │ │ + │ │ + │ ┌──────────────┐ │ + │ │ Desktop │ │ + │ │ │ │ + └───────▶│ │◀─────────┘ + │ │ + └──────────────┘ \ No newline at end of file diff --git a/examples/transfer-files/img/fetch-files-1.png b/examples/transfer-files/img/fetch-files-1.png new file mode 100644 index 0000000000..c702b64972 Binary files /dev/null and b/examples/transfer-files/img/fetch-files-1.png differ diff --git a/examples/transfer-files/img/fetch-files-2.png b/examples/transfer-files/img/fetch-files-2.png new file mode 100644 index 0000000000..02d17c5be5 Binary files /dev/null and b/examples/transfer-files/img/fetch-files-2.png differ diff --git a/examples/transfer-files/img/final.png b/examples/transfer-files/img/final.png new file mode 100644 index 0000000000..f6f61e98fa Binary files /dev/null and b/examples/transfer-files/img/final.png differ diff --git a/examples/transfer-files/package.json b/examples/transfer-files/package.json new file mode 100644 index 0000000000..12c217ceeb --- /dev/null +++ b/examples/transfer-files/package.json @@ -0,0 +1,14 @@ +{ + "name": "transfer-files", + "version": "1.0.0", + "scripts": { + "check-bundle": "test -f ../../dist/index.js && npm run copy-bundle || (echo \"js-ipfs dist file not found, go up two dirs and run 'npm run build' first\" && exit 1)", + "copy-bundle": "cp ../../dist/index.js ./public/js/ipfs.js", + "serve": "http-server -c-1 -p 12345 public", + "start": "npm run check-bundle && npm run serve" + }, + "license": "MIT", + "devDependencies": { + "http-server": "^0.9.0" + } +} diff --git a/examples/transfer-files/public/css/app.css b/examples/transfer-files/public/css/app.css new file mode 100644 index 0000000000..59cfeb2d1a --- /dev/null +++ b/examples/transfer-files/public/css/app.css @@ -0,0 +1,249 @@ +body { + height: 100vh; + font-family: sans-serif; + color: white; + background: linear-gradient(to bottom,#041727 0%,#062b3f 100%); + pointer-events: auto; +} + +.dragover-popup { + position: absolute; + top: 10px; right: 10px; bottom: 10px; left: 10px; + background-color: rgba(0, 0, 0, 0.5); + text-align: center; + padding-top: 30%; + display: none; + pointer-events: none; +} + +.wrapper { + width: 900px; + margin: 0 auto; + /* filter: blur(5px); */ +} + +.header { + text-align: center; + /* filter: blur(5px); */ +} + +#filesStatus { + padding: 10px; +} + +h1, h2, h3 { + margin: 0px; +} + +h1 { + font-size: 2em; + font-weight: 300; +} + +h2 { + font-size: 1.25em; + font-weight: 700; +} + +h3 { + font-size: 1.0em; + font-weight: 700; +} + + +.header h1 { + margin-top: 20px; + margin-bottom: 20px; +} + +.hidden { + display: none; +} + +.visible { + display: inherit; + font-size: 0.8em; +} + +.error { + font-style: italic; + color: red; +} + +.ipfs { + padding-bottom: 50px; + border-radius: 1px; + box-sizing: border-box; +} + +#details { + padding: 10px; + width: 100%; + box-sizing: border-box; +} + +ul { + margin: 0px; padding: 0px; + list-style: none; + font-size: 11px; +} + +ul li { + margin-top: 10px; + margin-bottom: 10px; + font-size: 9px; + word-wrap: break-word; +} + +button { + background-color: rgba(0,0,0,0.2); + color: #6acad1; + border: 2px solid #6acad1; + font-size: 15px; + padding: 10px 25px 10px 25px; + border-radius: 2px; + margin: 5px; +} + +.multihash-wrapper input { + width: 80%; + float: left; + height: 40px; + margin-left: 1%; + font-size: 16px; + box-sizing: border-box; +} + +.multihash-wrapper button { + width: 17%; + float: left; + height: 40px; + margin: 0px; + margin-left: 1%; +} + +.file-list { + display: block; + margin: 10px; +} + +.file-list a { + font-size: 16px; + color: white; + display: block; +} + +button.connect-peer { + margin: 0px; + margin-top: 2px; + width: 100%; +} + +button:hover { + color: white; + border: 2px solid white; + cursor: pointer; +} + +.address { + font-family: monospace +} + +button:disabled { + opacity: 0.2; +} +input:disabled { + opacity: 0.2; +} +.disabled { + opacity: 0.2; +} + +input { + width: 100%; + border: 2px solid #444; + color: black; + padding: 7px; + border-radius: 2px; + font-size: 9px; + box-sizing: border-box; +} + +textarea, input, button { + outline: none; +} + +button:focus, input:focus { + outline: 3px solid #6acad1; +} + +.picture { + margin-top: 1em; + width: 100%; + background-color: rgba(196, 196, 196, 0.1); + /*padding: 0.25em;*/ + /*font-size: 1.2em;*/ +} + +.settings { + padding: 15px; +} + +.left { + box-sizing: border-box; + /* background-color: red; */ +} + +.right { + /* background-color: green; */ +} + +.setting-item { + margin-top: 20px; +} + +.left, .right { + width: 48%; + float: left; + background-color: rgba(255, 255, 255, 0.05); + box-sizing: border-box; + margin: 1%; + padding: 10px; +} + + + +#files { + padding-top: 10px; + background-color: rgba(255, 255, 255, 0.05); + margin: 1%; +} + +.left.centered { + float: none; + margin: 0px auto; + text-align: center; +} + +.clear { + clear: both; +} + +.note { + position: absolute; + top: 10px; + right: 10px; + font-size: 10px; + font-size: 10px; +} + +#peers i { + display: block; + margin-top: 5px; + margin-bottom: 5px; + font-size: 14px; +} + +.error { + font-size: 18px; +} diff --git a/examples/transfer-files/public/favicon.ico b/examples/transfer-files/public/favicon.ico new file mode 100644 index 0000000000..801f728c08 Binary files /dev/null and b/examples/transfer-files/public/favicon.ico differ diff --git a/examples/transfer-files/public/index.html b/examples/transfer-files/public/index.html new file mode 100644 index 0000000000..c1c3dd49ac --- /dev/null +++ b/examples/transfer-files/public/index.html @@ -0,0 +1,68 @@ + + + + + + IPFS - Transfer Files + + + +
+

Drop file to upload

+
+ +
+ + + +

Transfer Files

+
+ P.S. drop files anywhere to upload them! +
+
+ +
+
+
+
+ + +
+
+

Your daemon

+

ID

+
N/A
+

Addresses

+
    +
  • Not yet online
  • +
+
+
+
+
+

Peers

+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ +
+
+ + + + + + diff --git a/examples/transfer-files/public/ipfs-logo.svg b/examples/transfer-files/public/ipfs-logo.svg new file mode 100644 index 0000000000..5091cce61e --- /dev/null +++ b/examples/transfer-files/public/ipfs-logo.svg @@ -0,0 +1 @@ +IPFS logo (new) \ No newline at end of file diff --git a/examples/transfer-files/public/js/app.js b/examples/transfer-files/public/js/app.js new file mode 100644 index 0000000000..38d3e832bd --- /dev/null +++ b/examples/transfer-files/public/js/app.js @@ -0,0 +1,272 @@ +/* global self */ + +const $startButton = document.querySelector('#start') +const $stopButton = document.querySelector('#stop') +const $peers = document.querySelector('#peers') +const $errors = document.querySelector('#errors') +const $filesStatus = document.querySelector('#filesStatus') +const $multihashInput = document.querySelector('#multihash') +const $catButton = document.querySelector('#cat') +const $connectPeer = document.querySelector('input.connect-peer') +const $connectPeerButton = document.querySelector('button.connect-peer') +const $dragoverPopup = document.querySelector('.dragover-popup') +const $wrapper = document.querySelector('.wrapper') +const $header = document.querySelector('.header') +const $body = document.querySelector('body') +const $idContainer = document.querySelector('.id-container') +const $addressesContainer = document.querySelector('.addresses-container') +const $details = document.querySelector('#details') +const $allDisabledButtons = document.querySelectorAll('button:disabled') +const $allDisabledInputs = document.querySelectorAll('input:disabled') +const $filesList = document.querySelector('.file-list') + +let node +let peerInfo + +/* + * Start and stop the IPFS node + */ + +function start () { + if (!node) { + updateView('starting', node) + + const repoPath = 'ipfs-' + Math.random() + + node = new self.Ipfs({ + repo: repoPath, + config: { + Addresses: { + Swarm: [ + '/libp2p-webrtc-star/dns4/star-signal.cloud.ipfs.team/wss' + ] + } + } + }) + + node.on('start', () => { + node.id().then((id) => { + peerInfo = id + updateView('ready', node) + setInterval(refreshPeerList, 1000) + $peers.innerHTML = '

peers

waiting for peers...' + }) + }) + } +} + +function stop () { + window.location.href = window.location.href // refresh page +} + +/* + * Fetch files and display them to the user + */ + +function createFileBlob (data, multihash) { + const file = new window.Blob(data, {type: 'application/octet-binary'}) + const fileUrl = window.URL.createObjectURL(file) + + const listItem = document.createElement('div') + const link = document.createElement('a') + link.setAttribute('href', fileUrl) + link.setAttribute('download', multihash) + const date = (new Date()).toLocaleTimeString() + + link.innerText = date + ' - ' + multihash + ' - Size: ' + file.size + listItem.appendChild(link) + return listItem +} + +function getFile () { + const multihash = $multihashInput.value + + $multihashInput.value = '' + + $errors.className = 'hidden' + + if (!multihash) { + return console.log('no multihash was inserted') + } + + // files.get documentation + // https://github.com/ipfs/interface-ipfs-core/tree/master/API/files#get + node.files.get(multihash, (err, filesStream) => { + if (err) { + return onError(err) + } + + filesStream.on('data', (file) => { + if (file.content) { + const buf = [] + // buffer up all the data in the file + file.content.on('data', (data) => buf.push(data)) + + file.content.once('end', () => { + const listItem = createFileBlob(buf, multihash) + + $filesList.insertBefore(listItem, $filesList.firstChild) + }) + + file.content.resume() + } + }) + filesStream.resume() + + filesStream.on('end', () => console.log('Every file was fetched for', multihash)) + }) +} + +/* + * Drag and drop + */ +function onDrop (event) { + onDragExit() + $errors.className = 'hidden' + event.preventDefault() + const dt = event.dataTransfer + const files = dt.files + const readFileContents = (file) => { + return new Promise((resolve) => { + const reader = new window.FileReader() + reader.onload = (event) => resolve(event.target.result) + reader.readAsArrayBuffer(file) + }) + } + + for (var i = 0; i < files.length; i++) { + const file = files[i] + readFileContents(file) + .then((buffer) => { + return node.files.add([{ + path: file.name, + content: new node.types.Buffer(buffer) + }]) + }) + .then((files) => { + $multihashInput.value = files[0].hash + $filesStatus.innerHTML = files + .map((e) => `Added ${e.path} as ${e.hash}`) + .join('
') + }) + .catch(onError) + } +} + +/* + * Network related functions + */ + +// Get peers from IPFS and display them + +function connectToPeer (event) { + event.target.disabled = true + node.swarm.connect($connectPeer.value, (err) => { + if (err) { + return onError(err) + } + + $connectPeer.value = '' + + setTimeout(() => { + event.target.disabled = false + }, 500) + }) +} + +function refreshPeerList () { + node.swarm.peers((err, peers) => { + if (err) { + return onError(err) + } + + const peersAsHtml = peers + .map((peer) => peer.addr.toString()) + .map((addr) => { + return '
  • ' + addr + '
  • ' + }).join('') + + $peers.innerHTML = peers.length > 0 + ? '

    Remote Peers

    ' + : '

    Remote Peers

    Waiting for peers...' + }) +} + +/* + * UI functions + */ + +function onError (err) { + let msg = 'An error occured, check the dev console' + + if (err.stack !== undefined) { + msg = err.stack + } else if (typeof err === 'string') { + msg = err + } + + $errors.innerHTML = '' + msg + '' + $errors.className = 'error visible' +} + +window.onerror = onError + +function onDragEnter () { + $dragoverPopup.style.display = 'block' + $wrapper.style.filter = 'blur(5px)' + $header.style.filter = 'blur(5px)' +} + +function onDragExit () { + $dragoverPopup.style.display = 'none' + $wrapper.style.filter = '' + $header.style.filter = '' +} + +/* + * App states + */ +const states = { + ready: () => { + const addressesHtml = peerInfo.addresses.map((address) => { + return '
  • ' + address + '
  • ' + }).join('') + $idContainer.innerText = peerInfo.id + $addressesContainer.innerHTML = addressesHtml + $allDisabledButtons.forEach(b => { b.disabled = false }) + $allDisabledInputs.forEach(b => { b.disabled = false }) + $peers.className = '' + $details.className = '' + $stopButton.disabled = false + $startButton.disabled = true + }, + starting: () => { + $startButton.disabled = true + } +} + +function updateView (state, ipfs) { + if (states[state] !== undefined) { + states[state]() + } else { + throw new Error('Could not find state "' + state + '"') + } +} + +/* + * Boot this application! + */ +const startApplication = () => { + // Setup event listeners + $body.addEventListener('dragenter', onDragEnter) + $body.addEventListener('drop', onDrop) + // TODO should work to hide the dragover-popup but doesn't... + $body.addEventListener('dragleave', onDragExit) + + $startButton.addEventListener('click', start) + $stopButton.addEventListener('click', stop) + $catButton.addEventListener('click', getFile) + $connectPeerButton.addEventListener('click', connectToPeer) +} + +startApplication()