Use alt for Flux.

pull/1/head
Alex Kern 11 years ago
parent b642fa6588
commit 22ecf9cb12

@ -1,16 +0,0 @@
import alt from './alt';
export default alt.createActions(class Actions {
constructor() {
this.generateActions(
'download',
'receiveData',
'requestDownload',
'sendToDownloader',
'setDownloadInfo',
'setPeerID',
'setUploadToken',
'upload'
)
}
})

@ -1,57 +0,0 @@
import DropZone from './DropZone';
import FileDescription from './FileDescription';
import React from 'react';
import Tempalink from './Tempalink';
import socket from './socket';
import Actions from './Actions';
import Store from './Store';
export default class App extends React.Component {
constructor() {
this.state = Store.getState();
this._onChange = function() {
this.setState(Store.getState());
}.bind(this);
}
componentDidMount() {
Store.listen(this._onChange);
}
componentDidUnmount() {
Store.unlisten(this._onChange);
}
uploadFile(file) {
Actions.upload(file);
}
downloadFile() {
Actions.requestDownload();
}
render() {
if (this.state.readyToUpload) {
return (
<div>
<FileDescription file={this.state.uploadFile} />
<Tempalink token={this.state.uploadToken} />
</div>
);
} else if (this.state.readyToDownload) {
return (
<div>
<FileDescription file={this.state.downloadMetadata} />
<button onClick={this.downloadFile.bind(this)}>Download</button>
</div>
);
} else {
return <DropZone onDrop={this.uploadFile.bind(this)} />;
}
}
}

@ -1,31 +1,35 @@
const chunkSize = 32;
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(n) {
this.n = n;
this.count = n;
constructor() {
this.count = 0;
this.size = 0;
this.chunks = [];
this.lastChunk = [];
}
setChunk(i, b) {
if (i < 0 || i >= this.n)
throw new Error('Chunk out of range');
add(b) {
this.count++;
this.size += blobLength(b);
this.lastChunk.push(b);
if (this.chunks[i])
throw new Error('Chunk already set');
this.count--;
this.chunks[i] = b;
if (this.lastChunk.length === chunkSize) {
let chunk = new Blob(this.lastChunk);
this.chunks.push(chunk);
this.lastChunk = [];
}
ready() {
return this.count === 0;
}
toBlob() {
if (!this.ready())
throw new Error('Incomplete blob');
return new Blob(this.chunks);
let allChunks = this.chunks.concat(this.lastChunk);
return new Blob(allChunks);
}
}

@ -0,0 +1,38 @@
import ChunkedBlob from './ChunkedBlob';
export default class DownloadFile {
constructor(name, size, type) {
this.name = name;
this.size = size;
this.type = type;
this.packets = new ChunkedBlob();
}
addPacket(b) {
this.packets.add(b);
}
clearPackets() {
this.packets = new ChunkedBlob();
}
isComplete() {
return this.packets.size === this.size;
}
getProgress() {
return this.packets.size / this.size;
}
download() {
let blob = this.packets.toBlob();
let url = URL.createObjectURL(blob);
let a = document.createElement('a');
a.download = this.name;
a.href = url;
a.click();
URL.revokeObjectURL(url);
}
}

@ -0,0 +1,33 @@
function statusPredicate(v) {
if (v.length === 1) {
return v.toUpperCase();
} else {
return 'is' + statusPredicate(v.charAt(0)) + v.substring(1);
}
}
export default class Status {
constructor(values) {
this.value = values[0];
this.values = values;
for (let v of values) {
this[statusPredicate(v)] = function () {
return this.value === v;
}
}
}
get() {
return this.value;
}
set(value) {
if (this.values.indexOf(value) === -1)
throw new Error('Unknown value');
this.value = value;
}
}

@ -1,106 +0,0 @@
import Actions from './Actions';
import ChunkedBlob from './ChunkedBlob';
import alt from './alt';
import peer from './peer';
import socket from './socket';
const chunkSize = 1024;
function downloadFile(name, blob) {
let url = URL.createObjectURL(blob);
let a = document.createElement('a');
a.download = name;
a.href = url;
a.click();
URL.revokeObjectURL(url);
}
export default alt.createStore(class Store {
constructor() {
this.bindActions(Actions);
this.peerID = null;
this.uploadToken = null;
this.uploadFile = null;
this.readyToUpload = false;
this.downloadToken = null;
this.downloadMetadata = null;
this.downloadChunks = null;
this.readyToDownload = false;
}
updateReadyStatus() {
this.readyToUpload = !!this.peerID && !!this.uploadToken && !!this.uploadFile;
this.readyToDownload = !!this.peerID && !!this.downloadToken && !!this.downloadMetadata;
}
onSetPeerID(id) {
this.peerID = id;
this.updateReadyStatus();
}
onSetUploadToken(token) {
this.uploadToken = token;
this.updateReadyStatus();
}
onUpload(file) {
this.uploadFile = file;
this.updateReadyStatus();
socket.emit('upload', {
name: file.name,
size: file.size,
type: file.type
});
}
onSetDownloadInfo(info) {
this.downloadToken = info.token;
this.downloadMetadata = info.metadata;
this.updateReadyStatus();
}
onRequestDownload() {
if (!this.readyToDownload) return;
socket.emit('download', {
peerID: this.peerID,
token: this.downloadToken
});
}
onSendToDownloader(peerID) {
if (!this.readyToUpload) return;
let file = this.uploadFile;
let conn = peer.connect(peerID);
conn.on('open', function () {
let chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
let start = i * chunkSize;
let end = start + chunkSize;
let chunk = file.slice(start, end);
conn.send({ n: chunks, i: i, b: chunk });
}
});
}
onReceiveData(data) {
if (!this.readyToDownload) return;
if (!this.downloadChunks)
this.downloadChunks = new ChunkedBlob(data.n);
this.downloadChunks.setChunk(data.i, data.b);
if (this.downloadChunks.ready()) {
let blob = this.downloadChunks.toBlob();
downloadFile(this.downloadMetadata.name, blob);
}
}
})

@ -0,0 +1,25 @@
const packetSize = 16 * 1024;
export default class UploadFile {
constructor(file) {
this.name = file.name;
this.size = file.size;
this.type = file.type;
this.blob = file;
}
countPackets() {
return Math.ceil(this.size / packetSize);
}
getPacket(i) {
if (i < 0 || i >= this.countPackets())
throw new Error('Packet out of bounds');
let start = i * packetSize;
let end = Math.min(start + packetSize, this.size);
return this.blob.slice(start, end);
}
}

@ -0,0 +1,12 @@
import alt from '../alt';
export default alt.createActions(class DownloadActions {
constructor() {
this.generateActions(
'beginDownload',
'requestDownload',
'cancelDownlaod',
'setDownloadInfo'
)
}
})

@ -0,0 +1,9 @@
import alt from '../alt';
export default alt.createActions(class PeerActions {
constructor() {
this.generateActions(
'setPeerID'
)
}
})

@ -0,0 +1,11 @@
import alt from '../alt';
export default alt.createActions(class UploadActions {
constructor() {
this.generateActions(
'sendToDownloader',
'setUploadToken',
'uploadFile'
)
}
})

@ -0,0 +1,74 @@
import DownloadActions from '../actions/DownloadActions';
import DownloadStore from '../stores/DownloadStore';
import DropZone from './DropZone';
import FileDescription from './FileDescription';
import PeerStore from '../stores/PeerStore';
import React from 'react';
import Tempalink from './Tempalink';
import UploadActions from '../actions/UploadActions';
import UploadStore from '../stores/UploadStore';
function getState() {
return {
peerID: PeerStore.getPeerID(),
readyToUpload: UploadStore.getState().status.isUploading(),
uploadFile: UploadStore.getState().file,
uploadToken: UploadStore.getState().token,
downloadFile: DownloadStore.getState().file,
downloadToken: DownloadStore.getState().token,
readyToDownload: DownloadStore.getState().status.isReady()
};
}
export default class App extends React.Component {
constructor() {
this.state = getState();
this._onChange = function() {
this.setState(getState());
}.bind(this);
}
componentDidMount() {
PeerStore.listen(this._onChange);
UploadStore.listen(this._onChange);
DownloadStore.listen(this._onChange);
}
componentDidUnmount() {
PeerStore.unlisten(this._onChange);
UploadStore.unlisten(this._onChange);
DownloadStore.unlisten(this._onChange);
}
uploadFile(file) {
UploadActions.uploadFile(file);
}
downloadFile() {
DownloadActions.requestDownload();
}
render() {
if (this.state.readyToUpload) {
return (
<div>
<FileDescription file={this.state.uploadFile} />
<Tempalink token={this.state.uploadToken} />
</div>
);
} else if (this.state.readyToDownload) {
return (
<div>
<FileDescription file={this.state.downloadFile} />
<button onClick={this.downloadFile.bind(this)}>Download</button>
</div>
);
} else {
return <DropZone onDrop={this.uploadFile.bind(this)} />;
}
}
}

@ -1,6 +1,12 @@
import App from './App';
import App from './components/App';
import React from 'react';
import Actions from './Actions';
import DownloadActions from './actions/DownloadActions';
if (window.WebDrop) DownloadActions.setDownloadInfo({
token: window.WebDrop.token,
name: window.WebDrop.metadata.name,
size: window.WebDrop.metadata.size,
type: window.WebDrop.metadata.type
})
if (window.WebDrop) Actions.setDownloadInfo(window.WebDrop);
React.render(<App />, document.getElementById('app'));

@ -1,14 +1,13 @@
import Actions from './Actions';
import DownloadActions from './actions/DownloadActions';
import Peer from 'peerjs';
import PeerActions from './actions/PeerActions';
var peer = module.exports = new Peer({ key: '8w3x9m637e0o1or' });
peer.on('open', function () {
Actions.setPeerID(peer.id);
PeerActions.setPeerID(peer.id);
});
peer.on('connection', function (conn) {
conn.on('data', function (data) {
Actions.receiveData(data);
});
DownloadActions.beginDownload(conn);
});

@ -1,12 +1,8 @@
import io from 'socket.io-client';
import Actions from './Actions';
import UploadActions from './actions/UploadActions';
var socket = module.exports = io.connect(window.location.origin);
socket.on('token', function (token) {
Actions.setUploadToken(token);
});
var socket = module.exports = io.connect();
socket.on('download', function (peerID) {
Actions.sendToDownloader(peerID);
UploadActions.sendToDownloader(peerID);
});

@ -0,0 +1,97 @@
import DownloadActions from '../actions/DownloadActions';
import DownloadFile from '../DownloadFile';
import PeerActions from '../actions/PeerActions';
import PeerStore from './PeerStore';
import Status from '../Status';
import alt from '../alt';
import socket from '../socket';
class DownloadStatus extends Status {
constructor() {
super([
'offline',
'ready',
'requesting',
'downloading',
'cancelled',
'done'
]);
}
}
export default alt.createStore(class DownloadStore {
constructor() {
this.bindActions(DownloadActions);
this.bindActions(PeerActions);
this.conn = null;
this.token = null;
this.file = null;
this.status = new DownloadStatus();
}
onSetPeerID() {
this.waitFor(PeerStore.dispatchToken);
if (this.status.isOffline() && this.token) this.status.set('ready');
}
onSetDownloadInfo(info) {
this.token = info.token;
this.file = new DownloadFile(info.name, info.size, info.type);
if (this.status.isOffline() && PeerStore.getPeerID()) this.status.set('ready');
}
onRequestDownload() {
if (!this.status.isReady()) return;
this.status.set('requesting');
socket.emit('download', {
peerID: PeerStore.getPeerID(),
token: this.token
});
}
onBeginDownload(conn) {
if (!this.status.isRequesting()) return;
this.status.set('downloading');
this.conn = conn;
let chunkSize = conn.metadata.chunkSize;
let i = 0;
conn.on('data', (data) => {
if (!this.status.isDownloading()) return;
this.file.addPacket(data);
i++;
if (this.file.isComplete()) {
this.status.set('done');
this.file.download();
conn.close();
} else if (i % chunkSize === 0) {
conn.send('more');
}
});
conn.on('close', () => {
if (!this.status.isDownloading()) return;
this._cancel();
});
}
onCancelDownload() {
if (!this.status.isRequesting() && !this.status.isDownloading()) return;
this._cancel();
}
_cancel() {
this.status.set('cancelled');
if (this.conn) this.conn.close();
this.conn = null;
this.file.clearPackets();
}
})

@ -0,0 +1,19 @@
import PeerActions from '../actions/PeerActions';
import alt from '../alt';
export default alt.createStore(class PeerStore {
constructor() {
this.bindActions(PeerActions);
this.peerID = null;
}
onSetPeerID(id) {
this.peerID = id;
}
static getPeerID() {
return this.getState().peerID;
}
})

@ -0,0 +1,87 @@
import PeerActions from '../actions/PeerActions';
import PeerStore from './PeerStore';
import Status from '../Status';
import UploadActions from '../actions/UploadActions';
import UploadFile from '../UploadFile';
import alt from '../alt';
import peer from '../peer';
import socket from '../socket';
const chunkSize = 32;
class UploadStatus extends Status {
constructor() {
super([
'offline',
'ready',
'processing',
'uploading'
]);
}
}
export default alt.createStore(class UploadStore {
constructor() {
this.bindActions(PeerActions);
this.bindActions(UploadActions);
this.token = null;
this.file = null;
this.status = new UploadStatus();
}
onSetPeerID(id) {
this.waitFor(PeerStore.dispatchToken);
if (this.status.isOffline()) this.status.set('ready');
}
onUploadFile(file) {
if (!this.status.isReady()) return;
this.status.set('processing');
this.file = new UploadFile(file);
socket.emit('upload', {
name: this.file.name,
size: this.file.size,
type: this.file.type
}, (token) => {
this.status.set('uploading');
this.token = token;
this.emitChange();
});
}
onSendToDownloader(peerID) {
if (!this.status.isUploading()) return;
let file = this.file;
let conn = peer.connect(peerID, {
reliable: true,
metadata: { chunkSize: chunkSize }
});
conn.on('open', function () {
let packets = file.countPackets();
let packet = 0;
function sendNextChunk() {
for (let i = 0; i < chunkSize; i++) {
if (packet >= packets) break;
let b = file.getPacket(packet);
conn.send(b);
packet++;
}
}
conn.on('data', function (data) {
if (data === 'more') sendNextChunk();
});
sendNextChunk();
});
}
})

@ -23,10 +23,10 @@ io.on('connection', function (socket) {
var upload = null;
socket.on('upload', function (metadata) {
socket.on('upload', function (metadata, res) {
if (!upload) upload = new Upload(socket);
upload.metadata = metadata;
socket.emit('token', upload.token);
res(upload.token);
});
socket.on('download', function (data) {

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save