Migrate to WebTorrent.

pull/18/head
Alex Kern 11 years ago
parent 9a6b1b8f8e
commit 75e646c4b9

@ -1,25 +0,0 @@
const rankSize = 16
function blobLength(b) {
if (typeof b.byteLength !== 'undefined') return b.byteLength
if (typeof b.size !== 'undefined') return b.size
return b.length
}
export default class ChunkedBlob {
constructor() {
this.size = 0
this.ranks = []
}
add(b) {
this.size += blobLength(b)
this.ranks.push(b)
}
toBlob() {
return new Blob(this.ranks)
}
}

@ -1,54 +0,0 @@
import ChunkedBlob from './ChunkedBlob'
export default class DownloadFile {
constructor(name, size, type) {
this.name = name
this.size = size
this.type = type
this.chunks = new ChunkedBlob()
}
addChunk(b) {
this.chunks.add(b)
}
clearChunks() {
this.chunks = new ChunkedBlob()
}
isComplete() {
return this.getProgress() === 1
}
getProgress() {
return this.chunks.size / this.size
}
download() {
let blob = this.chunks.toBlob()
let url = URL.createObjectURL(blob)
let a = document.createElement('a')
document.body.appendChild(a)
a.download = this.name
a.href = url
a.click()
setTimeout(() => {
URL.revokeObjectURL(url)
document.body.removeChild(a)
}, 0)
}
toJSON() {
return {
name: this.name,
size: this.size,
type: this.type
}
}
}

@ -1,33 +0,0 @@
const chunkSize = 256 * 1024
export default class UploadFile {
constructor(file) {
this.name = file.name
this.size = file.size
this.type = file.type
this.blob = file
}
countChunks() {
return Math.ceil(this.size / chunkSize)
}
getChunk(i) {
if (i < 0 || i >= this.countChunks())
throw new Error('Chunk out of bounds')
let start = i * chunkSize
let end = Math.min(start + chunkSize, this.size)
return this.blob.slice(start, end)
}
toJSON() {
return {
name: this.name,
size: this.size,
type: this.type
}
}
}

@ -3,8 +3,7 @@ import alt from '../alt'
export default alt.createActions(class DownloadActions { export default alt.createActions(class DownloadActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'requestDownload', 'requestDownload'
'beginDownload'
) )
} }
}) })

@ -3,8 +3,6 @@ import alt from '../alt'
export default alt.createActions(class UploadActions { export default alt.createActions(class UploadActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'sendToDownloader',
'setUploadToken',
'uploadFile' 'uploadFile'
) )
} }

@ -4,7 +4,7 @@ import SupportStore from '../stores/SupportStore'
function getState() { function getState() {
return { return {
active: SupportStore.getState().isChrome && DownloadStore.getState().file.size >= 500000000 active: SupportStore.getState().isChrome && DownloadStore.getState().fileSize >= 500000000
} }
} }

@ -2,10 +2,10 @@ import ChromeNotice from './ChromeNotice'
import DownloadActions from '../actions/DownloadActions' import DownloadActions from '../actions/DownloadActions'
import DownloadButton from './DownloadButton' import DownloadButton from './DownloadButton'
import DownloadStore from '../stores/DownloadStore' import DownloadStore from '../stores/DownloadStore'
import ErrorPage from './ErrorPage'
import ProgressBar from './ProgressBar' import ProgressBar from './ProgressBar'
import React from 'react' import React from 'react'
import Spinner from './Spinner' import Spinner from './Spinner'
import peer from 'filepizza-peerjs'
export default class DownloadPage extends React.Component { export default class DownloadPage extends React.Component {
@ -16,20 +16,14 @@ export default class DownloadPage extends React.Component {
this._onChange = () => { this._onChange = () => {
this.setState(DownloadStore.getState()) this.setState(DownloadStore.getState())
} }
this._onConnection = (conn) => {
DownloadActions.beginDownload(conn)
}
} }
componentDidMount() { componentDidMount() {
DownloadStore.listen(this._onChange) DownloadStore.listen(this._onChange)
peer.on('connection', this._onConnection)
} }
componentDidUnmount() { componentDidUnmount() {
DownloadStore.unlisten(this._onChange) DownloadStore.unlisten(this._onChange)
peer.removeListener('connection', this._onConnection)
} }
downloadFile() { downloadFile() {
@ -43,8 +37,8 @@ export default class DownloadPage extends React.Component {
<h1>FilePizza</h1> <h1>FilePizza</h1>
<Spinner dir="down" <Spinner dir="down"
name={this.state.file.name} name={this.state.fileName}
size={this.state.file.size} /> size={this.state.fileSize} />
<ChromeNotice /> <ChromeNotice />
<DownloadButton onClick={this.downloadFile.bind(this)} /> <DownloadButton onClick={this.downloadFile.bind(this)} />
@ -57,39 +51,29 @@ export default class DownloadPage extends React.Component {
<h1>FilePizza</h1> <h1>FilePizza</h1>
<Spinner dir="down" animated <Spinner dir="down" animated
name={this.state.file.name} name={this.state.fileName}
size={this.state.file.size} /> size={this.state.fileSize} />
<ChromeNotice /> <ChromeNotice />
<ProgressBar value={this.state.progress} /> <ProgressBar value={this.state.progress} />
</div> </div>
case 'cancelled':
return <div className="page">
<h1>FilePizza</h1>
<Spinner dir="down"
name={this.state.file.name}
size={this.state.file.size} />
<ChromeNotice />
<ProgressBar value={-1} />
</div>
case 'done': case 'done':
return <div className="page"> return <div className="page">
<h1>FilePizza</h1> <h1>FilePizza</h1>
<Spinner dir="down" <Spinner dir="down"
name={this.state.file.name} name={this.state.fileName}
size={this.state.file.size} /> size={this.state.fileSize} />
<ChromeNotice /> <ChromeNotice />
<ProgressBar value={1} /> <ProgressBar value={1} />
</div> </div>
default:
return <ErrorPage />
} }
} }

@ -1,5 +1,4 @@
import DropZone from './DropZone' import DropZone from './DropZone'
import ProgressBar from './ProgressBar'
import React from 'react' import React from 'react'
import Spinner from './Spinner' import Spinner from './Spinner'
import Tempalink from './Tempalink' import Tempalink from './Tempalink'
@ -16,20 +15,14 @@ export default class UploadPage extends React.Component {
this._onChange = () => { this._onChange = () => {
this.setState(UploadStore.getState()) this.setState(UploadStore.getState())
} }
this._onDownload = (peerID) => {
UploadActions.sendToDownloader(peerID)
}
} }
componentDidMount() { componentDidMount() {
UploadStore.listen(this._onChange) UploadStore.listen(this._onChange)
socket.on('download', this._onDownload)
} }
componentDidUnmount() { componentDidUnmount() {
UploadStore.unlisten(this._onChange) UploadStore.unlisten(this._onChange)
socket.removeListener('download', this._onDownload)
} }
uploadFile(file) { uploadFile(file) {
@ -37,9 +30,9 @@ export default class UploadPage extends React.Component {
} }
handleSelectedFile(event) { handleSelectedFile(event) {
let files = event.target.files; let files = event.target.files
if (files.length > 0) { if (files.length > 0) {
UploadActions.uploadFile(files[0]); UploadActions.uploadFile(files[0])
} }
} }
@ -75,24 +68,17 @@ export default class UploadPage extends React.Component {
</div> </div>
case 'uploading': case 'uploading':
var keys = Object.keys(this.state.peerProgress)
keys.reverse()
return <div className="page"> return <div className="page">
<h1>FilePizza</h1> <h1>FilePizza</h1>
<Spinner dir="up" animated {...this.state.file} /> <Spinner dir="up" animated
name={this.state.fileName}
size={this.state.fileSize} />
<p>Send someone this link to download.</p> <p>Send someone this link to download.</p>
<p>This link will work as long as this page is open.</p> <p>This link will work as long as this page is open.</p>
<Tempalink token={this.state.token} /> <Tempalink token={this.state.token} />
{keys.length > 0 ? <p>Download Progress</p> : null}
<div className="data">
{keys.map((key) => {
return <ProgressBar small key={key} value={this.state.peerProgress[key]} />
})}
</div>
</div> </div>
} }
} }

@ -35,5 +35,6 @@ export function find(token) {
} }
export function remove(client) { export function remove(client) {
if (client == null) return
delete tokens[client.token] delete tokens[client.token]
} }

@ -11,7 +11,10 @@ routes.get(/^\/([a-z]+-[a-z]+-[a-z]+-[a-z]+)$/, function (req, res, next) {
DownloadStore: { DownloadStore: {
status: 'ready', status: 'ready',
token: uploader.token, token: uploader.token,
file: uploader.metadata fileSize: uploader.fileSize,
fileName: uploader.fileName,
fileType: uploader.fileType,
infoHash: uploader.infoHash
} }
} }

@ -14,3 +14,4 @@ export default (
<NotFoundRoute handler={ErrorPage} /> <NotFoundRoute handler={ErrorPage} />
</Route> </Route>
) )

@ -3,7 +3,6 @@ var express = require('express')
var fs = require('fs') var fs = require('fs')
var http = require('http') var http = require('http')
var path = require('path') var path = require('path')
var peer = require('peer')
var socketIO = require('socket.io') var socketIO = require('socket.io')
var winston = require('winston') var winston = require('winston')
var expressWinston = require('express-winston') var expressWinston = require('express-winston')
@ -32,8 +31,6 @@ server.listen(process.env.PORT || 3000, function () {
console.log('FilePizza listening on %s:%s', host, port) console.log('FilePizza listening on %s:%s', host, port)
}) })
app.use('/peer', peer.ExpressPeerServer(server))
app.use(expressWinston.logger({ app.use(expressWinston.logger({
winstonInstance: winston, winstonInstance: winston,
expressFormat: true expressFormat: true
@ -55,22 +52,18 @@ io.on('connection', function (socket) {
socket.on('upload', function (metadata, res) { socket.on('upload', function (metadata, res) {
if (upload) return if (upload) return
upload = true
db.create(socket).then((u) => { db.create(socket).then((u) => {
upload = u upload = u
upload.metadata = metadata upload.fileName = metadata.fileName
upload.fileSize = metadata.fileSize
upload.fileType = metadata.fileType
upload.infoHash = metadata.infoHash
res(upload.token) res(upload.token)
}) })
}) })
socket.on('download', function (data) {
var uploader = db.find(data.token)
if (!uploader) return
uploader.socket.emit('download', data.peerID)
})
socket.on('disconnect', function () { socket.on('disconnect', function () {
if (upload) db.remove(upload) db.remove(upload)
}) })
}) })

@ -1,62 +1,57 @@
import DownloadActions from '../actions/DownloadActions' import DownloadActions from '../actions/DownloadActions'
import DownloadFile from '../DownloadFile' import WebTorrent from 'webtorrent'
import peer from 'filepizza-peerjs'
import alt from '../alt' import alt from '../alt'
import socket from 'filepizza-socket' import socket from 'filepizza-socket'
function downloadBlobURL(name, blobURL) {
let a = document.createElement('a')
document.body.appendChild(a)
a.download = name
a.href = blobURL
a.click()
}
export default alt.createStore(class DownloadStore { export default alt.createStore(class DownloadStore {
constructor() { constructor() {
this.bindActions(DownloadActions) this.bindActions(DownloadActions)
this.status = 'ready' this.fileName = ''
this.token = null this.fileSize = 0
this.file = null this.fileType = ''
this.progress = 0 this.progress = 0
this.status = 'uninitialized'
this.on('bootstrap', () => { this.token = null
if (this.file && !(this.file instanceof DownloadFile)) { this.infoHash = null
this.file = new DownloadFile(this.file.name,
this.file.size,
this.file.type)
}
})
} }
onRequestDownload() { onRequestDownload() {
if (this.status !== 'ready') return if (this.status !== 'ready') return
this.status = 'requesting' this.status = 'requesting'
socket.emit('download', { const client = new WebTorrent()
peerID: peer.id, client.download(this.infoHash, (torrent) => {
token: this.token this.setState({ status: 'downloading' })
})
} let downloaded = 0
const file = torrent.files[0]
onBeginDownload(conn) { const stream = file.createReadStream()
if (this.status !== 'requesting') return stream.on('data', (chunk) => {
this.status = 'downloading' if (this.status !== 'downloading') return
conn.on('data', (chunk) => { downloaded += chunk.length
if (this.status !== 'downloading') return
if (downloaded === file.length) {
this.file.addChunk(chunk) this.setState({ status: 'done', progress: 1 })
file.getBlobURL((err, blobURL) => {
if (this.file.isComplete()) { if (err) throw err
this.setState({ status: 'done', progress: 1 }) downloadBlobURL(this.fileName, blobURL)
this.file.download() })
conn.close() } else {
} else { this.setState({ progress: downloaded / file.length })
this.setState({ progress: this.file.getProgress() }) }
conn.send('more')
} })
})
conn.on('close', () => {
if (this.status !== 'downloading') return
this.setState({ status: 'cancelled', progress: 0 })
this.file.clearChunks()
}) })
} }

@ -1,71 +1,43 @@
import UploadActions from '../actions/UploadActions' import UploadActions from '../actions/UploadActions'
import UploadFile from '../UploadFile'
import alt from '../alt' import alt from '../alt'
import peer from 'filepizza-peerjs'
import socket from 'filepizza-socket' import socket from 'filepizza-socket'
import WebTorrent from 'webtorrent'
export default alt.createStore(class UploadStore { export default alt.createStore(class UploadStore {
constructor() { constructor() {
this.bindActions(UploadActions) this.bindActions(UploadActions)
this.fileName = ''
this.fileSize = 0
this.fileType = ''
this.status = 'ready' this.status = 'ready'
this.token = null this.token = null
this.file = null this.infoHash = null
this.peerProgress = {}
} }
onUploadFile(file) { onUploadFile(file) {
if (this.status !== 'ready') return if (this.status !== 'ready') return
this.status = 'processing' this.status = 'processing'
this.file = new UploadFile(file)
socket.emit('upload', { const client = new WebTorrent()
name: this.file.name, client.seed(file, (torrent) => {
size: this.file.size, socket.emit('upload', {
type: this.file.type fileName: file.name,
}, (token) => { fileSize: file.size,
this.setState({ fileType: file.type,
status: 'uploading', infoHash: torrent.infoHash
token: token }, (token) => {
this.setState({
status: 'uploading',
token: token,
fileName: file.name,
fileSize: file.size,
fileType: file.type,
infoHash: torrent.infoHash
})
}) })
}) })
} }
onSendToDownloader(peerID) {
if (this.status !== 'uploading') return
let conn = peer.connect(peerID, {
reliable: true
})
let totalChunks = this.file.countChunks()
let i = 0
let sendNextChunk = () => {
if (i === totalChunks) return
let packet = this.file.getChunk(i)
conn.send(packet)
i++
this.peerProgress[peerID] = i/totalChunks
}
conn.on('open', () => {
sendNextChunk()
this.setState({ peerProgress: this.peerProgress })
})
conn.on('data', (data) => {
if (data === 'more') sendNextChunk()
this.setState({ peerProgress: this.peerProgress })
})
conn.on('close', () => {
if (this.peerProgress[peerID] < 1) {
this.peerProgress[peerID] = -1
}
this.setState({ peerProgress: this.peerProgress })
})
}
}, 'UploadStore') }, 'UploadStore')

@ -1,6 +1,6 @@
{ {
"name": "filepizza", "name": "filepizza",
"version": "0.1.2", "version": "0.2.0",
"description": "Free peer-to-peer file transfers in your browser.", "description": "Free peer-to-peer file transfers in your browser.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -25,13 +25,10 @@
"classnames": "^1.2.0", "classnames": "^1.2.0",
"express": "^4.12.0", "express": "^4.12.0",
"express-winston": "^0.3.1", "express-winston": "^0.3.1",
"filepizza-peerjs": "^1.0.0",
"filepizza-socket": "^1.0.0", "filepizza-socket": "^1.0.0",
"newrelic": "^1.21.1", "newrelic": "^1.21.1",
"nib": "^1.1.0", "nib": "^1.1.0",
"node-uuid": "^1.4.3", "node-uuid": "^1.4.3",
"peer": "^0.2.8",
"peerjs": "^0.3.14",
"react": "^0.13.0", "react": "^0.13.0",
"react-frozenhead": "^0.3.0", "react-frozenhead": "^0.3.0",
"react-google-analytics": "^0.2.0", "react-google-analytics": "^0.2.0",
@ -40,6 +37,7 @@
"socket.io-client": "^1.3.5", "socket.io-client": "^1.3.5",
"stylus": "^0.50.0", "stylus": "^0.50.0",
"webrtcsupport": "^2.1.2", "webrtcsupport": "^2.1.2",
"webtorrent": "^0.56.0",
"winston": "^1.0.1", "winston": "^1.0.1",
"xkcd-password": "^1.2.0" "xkcd-password": "^1.2.0"
}, },

Loading…
Cancel
Save