diff --git a/examples/transfer-files/.gitignore b/examples/transfer-files/.gitignore new file mode 100644 index 0000000000..f96c726227 --- /dev/null +++ b/examples/transfer-files/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +public/ipfs.js diff --git a/examples/transfer-files/README.md b/examples/transfer-files/README.md new file mode 100644 index 0000000000..84ccd01ae9 --- /dev/null +++ b/examples/transfer-files/README.md @@ -0,0 +1,220 @@ +# Tutorial - Transfer files between the browser and other IPFS nodes + +> Welcome! This tutorial will help you build a tiny web application where you can fetch and add files to IPFS and transfer these between a go-ipfs node and a js-ipfs node. + +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 +- [full go-ipfs interop is not complete yet, blocked by an interop stream multiplexer](https://github.com/ipfs/js-ipfs/issues/721) + +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 + +If you just want to check out what is the final state of how this application will look like, go to the complete folder, install the dependencies and run it. + +```sh +> cd complete +> npm install +> npm start +# open your browser (Chrome or Firefox) in http://localhost:12345 +``` + +You should get something like this: + +TODO: Insert final screenshot here + +## 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. Initialize the project +- 4. Create the frame for your IPFS enabled app +- 5. Add and cat a file +- 6. Use WebRTC to dial between browser nodes +- 7. Dial to a node using WebSockets (your Desktop ones) +- 8. 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: + +``` +> ipfs init +``` + +or + +``` +> jsipfs init +``` + +This will set up an `init` file 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. You can run `cat ~/.jsipfs/config` to see the contents of the JSON file. + +Since websockets are currently not stable and are experimental, you'll need to add the ability for your daemon to listen on Websocket addresses. Look into your init file (using `cat`) and find the `Addresses` block: + +```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" + } +``` + +To make Websockets work, open up the `config` file and add the following entry to your `Swarm` array: `/ip4/0.0.0.0/tcp/9999/ws`. Now, it should look like this: + + +```json + "Addresses": { + "Swarm": [ + "/ip4/0.0.0.0/tcp/4002", + "/ip4/0.0.0.0/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 explaining that 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/complete`. 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. Create the frame for your IPFS enabled app + +TODO: Not sure what this means. + +### 5. Add and cat a file + +### 6. Use WebRTC to dial between browser nodes +### 7. Dial to a node using WebSockets (your Desktop ones) +### 8. Transfer files between all of your nodes, have fun! + +-------- + +## Start the example + +**NOTE!** Before running the examples, you need to build `js-ipfs`. You can do this by following the instructions in https://github.com/ipfs/js-ipfs#clone-and-install-dependnecies and then building it as per https://github.com/ipfs/js-ipfs#build-a-dist-version. + +``` +npm install +npm start +``` + +Open http://127.0.0.1:8080 in a browser. + +**TODO: add instructions how to cat a hash in the UI.** + +## Tutorial + +Steps +1. create IPFS instance + +TODO. See `./start-ipfs.js` + +3. add a file in go-ipfs +4. copy file's hash +5. ipfs.files.cat + +TODO. add ipfs.files.cat code examples from index.html + +6. output the buffer to + +``` +... +stream.on('end', () => { + const blob = new Blob(buf) + picture.src = URL.createObjectURL(blob) +}) +``` diff --git a/examples/transfer-files/complete/.gitignore b/examples/transfer-files/complete/.gitignore new file mode 100644 index 0000000000..257a410ec0 --- /dev/null +++ b/examples/transfer-files/complete/.gitignore @@ -0,0 +1 @@ +js/ipfs.js diff --git a/examples/transfer-files/complete/css/app.css b/examples/transfer-files/complete/css/app.css new file mode 100644 index 0000000000..59cfeb2d1a --- /dev/null +++ b/examples/transfer-files/complete/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/complete/favicon.ico b/examples/transfer-files/complete/favicon.ico new file mode 100644 index 0000000000..801f728c08 Binary files /dev/null and b/examples/transfer-files/complete/favicon.ico differ diff --git a/examples/transfer-files/complete/index.html b/examples/transfer-files/complete/index.html new file mode 100644 index 0000000000..1be2122b8c --- /dev/null +++ b/examples/transfer-files/complete/index.html @@ -0,0 +1,72 @@ + + + + + + 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/complete/ipfs-logo.svg b/examples/transfer-files/complete/ipfs-logo.svg new file mode 100644 index 0000000000..5091cce61e --- /dev/null +++ b/examples/transfer-files/complete/ipfs-logo.svg @@ -0,0 +1 @@ +IPFS logo (new) \ No newline at end of file diff --git a/examples/transfer-files/complete/js/app.js b/examples/transfer-files/complete/js/app.js new file mode 100644 index 0000000000..4e9e60b94c --- /dev/null +++ b/examples/transfer-files/complete/js/app.js @@ -0,0 +1,253 @@ +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') + +let ipfs +let peerInfo + +// TODO groups to refactor into +// ipfs stuff +// start node +// get details +// get peers +// connect to peer +// get contents of hash +// add files to ipfs +// creating html stuff +// list of peers / peers state +// add file to download list +// error handling +// show ipfs id +// show ipfs addressess +// drag-and-drop +// event listeners +// states + +function start () { + if (!ipfs) { + updateView('starting', ipfs) + window.createNode((err, node) => { + if (err) { + return onError(err) + } + ipfs = node + ipfs.id().then((id) => { + peerInfo = id + updateView('ready', ipfs) + setInterval(updatePeers, 1000) + $peers.innerHTML = '

Peers

Waiting for peers...' + }) + }) + } +} + +const stop = () => { + window.location.href = window.location.href +} + +const connectPeer = (e) => { + e.target.disabled = true + // Connect directly to a peer via it's multiaddr + ipfs.swarm.connect($connectPeer.value, (err) => { + console.log(err) + if (err) return onError(err) + $connectPeer.value = '' + setTimeout(() => { + e.target.disabled = false + }, 500) + }) +} + +const catFile = () => { + const multihash = $multihashInput.value + $multihashInput.value = '' + $errors.className = 'hidden' + if (multihash) { + // Get a file or many files + ipfs.files.get(multihash, (err, stream) => { + if (err) { + onError(err) + } + console.log(stream) + + // .get gives us a stream of files + stream.on('data', (file) => { + console.log('got file', file) + + const buf = [] + + if (file.content) { + // once we get a file, we also want to read the data for that file + file.content.on('data', (data) => buf.push(data)) + + file.content.on('end', () => { + console.log('The buf', buf) + + const content = new window.Blob(buf, {type: 'application/octet-binary'}) + const contentUrl = window.URL.createObjectURL(content) + + const listItem = document.createElement('div') + const link = document.createElement('a') + link.setAttribute('href', contentUrl) + link.setAttribute('download', multihash) + const date = (new Date()).toLocaleTimeString() + + link.innerText = date + ' - ' + multihash + ' - Size: ' + file.size + const fileList = document.querySelector('.file-list') + + listItem.appendChild(link) + fileList.insertBefore(listItem, fileList.firstChild) + }) + + file.content.resume() + } + }) + + stream.on('end', () => console.log('no more files')) + }) + } +} + +const onError = (e) => { + console.error(e) + let msg = 'An error occured, check the dev console' + if (e.stack !== undefined) { + msg = e.stack + } else if (typeof e === 'string') { + msg = e + } + $errors.innerHTML = '' + msg + '' + $errors.className = 'error visible' +} +window.onerror = onError + +const onDragEnter = () => { + $dragoverPopup.style.display = 'block' + $wrapper.style.filter = 'blur(5px)' + $header.style.filter = 'blur(5px)' +} + +const onDragExit = () => { + console.log('drag left') + $dragoverPopup.style.display = 'none' + $wrapper.style.filter = '' + $header.style.filter = '' +} + +// Handle file drop +const onDrop = (event) => { + onDragExit() + $errors.className = 'hidden' + event.preventDefault() + var dt = event.dataTransfer + var 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) + }) + } + + // TODO: Promise reduce? + for (var i = 0; i < files.length; i++) { + const file = files[i] + console.log('Add file', file.name, file.size) + readFileContents(file) + .then((buffer) => { + return ipfs.files.add([{ + path: file.name, + content: new ipfs.types.Buffer(buffer) + }]) + }) + .then((files) => { + console.log('Files added', files) + $multihashInput.value = files[0].hash + $filesStatus.innerHTML = files + .map((e) => `Added ${e.path} as ${e.hash}`) + .join('
') + }) + .catch(onError) + } +} + +// Get peers from IPFS and display them +let numberOfPeersLastTime = 0 +const updatePeers = () => { + // Once in a while, we need to refresh our list of peers in the UI + // .swarm.peers returns an array with all our currently connected peer + ipfs.swarm.peers((err, res) => { + if (err) onError(err) + if (numberOfPeersLastTime !== res.length) { + const peersAsHtml = res.map((p) => p.addr.toString()) + .map((p) => { + return '
  • ' + p + '
  • ' + }).join() + + $peers.innerHTML = res.length > 0 + ? '

    Remote Peers

    ' + : '

    Remote Peers

    Waiting for peers...' + } + numberOfPeersLastTime = res.length + }) +} + +function setupEventListeners () { + $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', catFile) + $connectPeerButton.addEventListener('click', connectPeer) +} + +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 + '"') + } +} + +const startApplication = () => { + setupEventListeners() +} + +startApplication() diff --git a/examples/transfer-files/complete/js/create-node.js b/examples/transfer-files/complete/js/create-node.js new file mode 100644 index 0000000000..efee064fe4 --- /dev/null +++ b/examples/transfer-files/complete/js/create-node.js @@ -0,0 +1,42 @@ +'use strict' + +window.createNode = (callback) => { + // Create a new repository for IPFS in a random path always + const repoPath = '/tmp/ipfs' + Math.random() + const node = new window.Ipfs({repo: repoPath}) + + // Initialize our repository with no extra files + node.init({ emptyRepo: true }, updateConfig) + + function updateConfig (err) { + if (err) return callback(err) + + // Retrieve the initialized default configuration + // node.config.get((err, config) => { + // if (err) return callback(err) + + // // Add our webrtc-star address so we can find other peers easily + // const signalDomain = 'star-signal.cloud.ipfs.team' + // const wstarMultiaddr = `/libp2p-webrtc-star/dns/${signalDomain}/wss/ipfs/${config.Identity.PeerID}` + // config.Addresses.Swarm = config.Addresses.Swarm.concat([ wstarMultiaddr ]) + + // // Set the new configuration + // node.config.replace(config, bootNode) + // }) + bootNode() + } + + function bootNode (err) { + if (err) return callback(err) + + node.load((err) => { + if (err) return callback(err) + + // Actually start all the services and start ipfs + node.goOnline((err) => { + if (err) return callback(err) + callback(null, node) + }) + }) + } +} diff --git a/examples/transfer-files/complete/package.json b/examples/transfer-files/complete/package.json new file mode 100644 index 0000000000..828b227cc1 --- /dev/null +++ b/examples/transfer-files/complete/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 ./js/ipfs.js", + "serve": "http-server -c-1 -p 12345", + "start": "npm run check-bundle && npm run serve" + }, + "license": "MIT", + "devDependencies": { + "http-server": "^0.9.0" + } +} diff --git a/examples/transfer-files/complete/yarn.lock b/examples/transfer-files/complete/yarn.lock new file mode 100644 index 0000000000..5d0c98e5f5 --- /dev/null +++ b/examples/transfer-files/complete/yarn.lock @@ -0,0 +1,114 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +async@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.0.tgz#ac3613b1da9bed1b47510bb4651b8931e47146c7" + +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +corser@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" + +ecstatic@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-1.4.1.tgz#32cb7b6fa2e290d58668674d115e8f0c3d567d6a" + dependencies: + he "^0.5.0" + mime "^1.2.11" + minimist "^1.1.0" + url-join "^1.0.0" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +he@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" + +http-proxy@^1.8.1: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-server@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.9.0.tgz#8f1b06bdc733618d4dc42831c7ba1aff4e06001a" + dependencies: + colors "1.0.3" + corser "~2.0.0" + ecstatic "^1.4.0" + http-proxy "^1.8.1" + opener "~1.4.0" + optimist "0.6.x" + portfinder "0.4.x" + union "~0.4.3" + +mime@^1.2.11: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +mkdirp@0.5.x: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +opener@~1.4.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" + +optimist@0.6.x: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +portfinder@0.4.x: + version "0.4.0" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-0.4.0.tgz#a3ffadffafe4fb98e0601a85eda27c27ce84ca1e" + dependencies: + async "0.9.0" + mkdirp "0.5.x" + +qs@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404" + +requires-port@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +union@~0.4.3: + version "0.4.6" + resolved "https://registry.yarnpkg.com/union/-/union-0.4.6.tgz#198fbdaeba254e788b0efcb630bc11f24a2959e0" + dependencies: + qs "~2.3.3" + +url-join@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" 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