mirror of https://github.com/kern/filepizza
Use alt for Flux.
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');
|
||||
|
||||
if (this.chunks[i])
|
||||
throw new Error('Chunk already set');
|
||||
add(b) {
|
||||
this.count++;
|
||||
this.size += blobLength(b);
|
||||
this.lastChunk.push(b);
|
||||
|
||||
this.count--;
|
||||
this.chunks[i] = b;
|
||||
}
|
||||
|
||||
ready() {
|
||||
return this.count === 0;
|
||||
if (this.lastChunk.length === chunkSize) {
|
||||
let chunk = new Blob(this.lastChunk);
|
||||
this.chunks.push(chunk);
|
||||
this.lastChunk = [];
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue