diff --git a/app/index.html b/app/index.html
index 3c45f6e..a380e86 100644
--- a/app/index.html
+++ b/app/index.html
@@ -43,6 +43,7 @@
+
@@ -51,7 +52,9 @@
+
+
diff --git a/app/scripts/app/controllers/index_controller.js b/app/scripts/app/controllers/index_controller.js
index 4e622c1..437d217 100644
--- a/app/scripts/app/controllers/index_controller.js
+++ b/app/scripts/app/controllers/index_controller.js
@@ -155,23 +155,12 @@ FileDrop.IndexController = Ember.ArrayController.extend({
console.log('Peer:\t Received file', data);
var connection = data.connection,
- peer = this.findBy('peer.id', connection.peer),
- blob = data.blob,
- info = peer.get('transfer.info'),
- dataUrl;
+ peer = this.findBy('peer.id', connection.peer);
// Stop listening for 'receiving' progress now that we have a file
peer.get('peer.connection').removeAllListeners('receiving_progress');
peer.set('transfer.receiving_progress', 0);
peer.set('transfer.info', null);
-
- // Save received file
- dataUrl = window.URL.createObjectURL(blob);
- var a = document.createElement('a');
- a.setAttribute('download', info.name);
- a.setAttribute('href', dataUrl);
- document.body.appendChild(a);
- a.click();
},
// Based on http://net.ipcalf.com/
diff --git a/app/scripts/app/lib/file.js b/app/scripts/app/lib/file.js
new file mode 100644
index 0000000..0eb72af
--- /dev/null
+++ b/app/scripts/app/lib/file.js
@@ -0,0 +1,183 @@
+window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
+window.URL = window.URL || window.webkitURL;
+
+FileDrop.File = function (options) {
+ var self = this;
+
+ this.name = options.name;
+ this.size = options.size;
+ this.type = options.type;
+
+ this._reset();
+
+ return new Promise(function (resolve, reject) {
+ window.requestFileSystem(
+ window.TEMPORARY,
+ options.size,
+ function (filesystem) {
+ self.filesystem = filesystem;
+ resolve(self);
+ },
+ function (error) {
+ self.errorHandler(error);
+ reject(error);
+ }
+ );
+ });
+};
+
+FileDrop.File.removeAll = function () {
+ return new Promise(function (resolve, reject) {
+ var filer = new Filer();
+
+ filer.init({persistent: false}, function () {
+ filer.ls('/', function (entries) {
+ function rm(entry) {
+ if (entry) {
+ filer.rm(entry, function () {
+ rm(entries.pop());
+ });
+ } else {
+ resolve();
+ }
+ }
+
+ rm(entries.pop());
+ });
+ }, function (error) {
+ console.log(error);
+ reject(error);
+ });
+ });
+};
+
+FileDrop.File.prototype.append = function (data) {
+ var self = this,
+ options = {
+ create: this.create
+ };
+
+ return new Promise(function (resolve, reject) {
+ self.filesystem.root.getFile(self.name, options, function (fileEntry) {
+ if (self.create) {
+ self.fileEntry = fileEntry;
+ self.create = false;
+ }
+
+ fileEntry.createWriter(function (writer) {
+ var blob = new Blob(data, {type: self.type});
+
+ // console.log('File: Appending ' + blob.size + ' bytes at ' + self.seek);
+
+ writer.onwriteend = function () {
+ self.seek += blob.size;
+ resolve(fileEntry);
+ };
+
+ writer.onerror = function (error) {
+ self.errorHandler(error);
+ reject(error);
+ };
+
+ writer.seek(self.seek);
+ writer.write(blob);
+ }, function (error) {
+ self.errorHandler(error);
+ reject(error);
+ });
+ }, function (error) {
+ self.errorHandler(error);
+ reject(error);
+ });
+ });
+};
+
+FileDrop.File.prototype.save = function () {
+ var self = this;
+
+ console.log('File: Saving file: ', this.fileEntry);
+
+ var a = document.createElement('a');
+ a.download = this.name;
+
+ if (this._isWebKit()) {
+ a.href = this.fileEntry.toURL();
+ finish(a);
+ } else {
+ this.fileEntry.file(function (file) {
+ a.href = window.URL.createObjectURL(file);
+ finish(a);
+ });
+ }
+
+ function finish(a) {
+ document.body.appendChild(a);
+ a.addEventListener('click', function () {
+ // Remove file entry from filesystem.
+ setTimeout(function () {
+ self.remove().then(self._reset);
+ }, 1); // Hack, but otherwise browser doesn't save the file at all.
+
+ a.parentNode.removeChild(a);
+ });
+ a.click();
+ }
+};
+
+FileDrop.File.prototype.errorHandler = function (error) {
+ var msg;
+
+ switch (error.code) {
+ case FileError.QUOTA_EXCEEDED_ERR:
+ msg = 'QUOTA_EXCEEDED_ERR';
+ break;
+ case FileError.NOT_FOUND_ERR:
+ msg = 'NOT_FOUND_ERR';
+ break;
+ case FileError.SECURITY_ERR:
+ msg = 'SECURITY_ERR';
+ break;
+ case FileError.INVALID_MODIFICATION_ERR:
+ msg = 'INVALID_MODIFICATION_ERR';
+ break;
+ case FileError.INVALID_STATE_ERR:
+ msg = 'INVALID_STATE_ERR';
+ break;
+ default:
+ msg = 'Unknown Error';
+ break;
+ }
+
+ console.error('File error: ' + msg);
+};
+
+FileDrop.File.prototype.remove = function () {
+ var self = this;
+
+ return new Promise(function (resolve, reject) {
+ self.filesystem.root.getFile(self.name, {create: false}, function (fileEntry) {
+ fileEntry.remove(function () {
+ console.debug('File: Removed file: ' + self.name);
+ resolve(fileEntry);
+ }, function (error) {
+ self.errorHandler(error);
+ reject(error);
+ });
+ }, function (error) {
+ self.errorHandler(error);
+ reject(error);
+ });
+ });
+};
+
+FileDrop.File.prototype._reset = function () {
+ this.create = true;
+ this.filesystem = null;
+ this.fileEntry = null;
+ this.seek = 0;
+};
+
+FileDrop.File.prototype._isWebKit = function () {
+ return !!window.webkitRequestFileSystem;
+};
+
diff --git a/app/scripts/app/lib/webrtc.js b/app/scripts/app/lib/webrtc.js
index dc9d384..a860295 100644
--- a/app/scripts/app/lib/webrtc.js
+++ b/app/scripts/app/lib/webrtc.js
@@ -1,6 +1,3 @@
-window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
-window.URL = window.URL || window.webkitURL;
-
// TODO: provide TURN server config
// once it's possible to create rooms with custom names.
FileDrop.WebRTC = function (options) {
@@ -35,11 +32,10 @@ FileDrop.WebRTC = function (options) {
// Listen for incoming connections
this.conn.on('connection', this._onConnection.bind(this));
- this.conn.on('close', function (error) {
+ this.conn.on('close', function () {
console.log('Peer:\t Connected to server closed.');
});
-
this.conn.on('error', function (error) {
console.log('Peer:\t Error while connecting to server: ', error);
});
@@ -52,7 +48,7 @@ FileDrop.WebRTC = function (options) {
};
};
-FileDrop.WebRTC.CHUNKS_PER_ACK = 32;
+FileDrop.WebRTC.CHUNKS_PER_ACK = 64;
FileDrop.WebRTC.prototype.connect = function (id) {
var connection = this.conn.connect(id, {
@@ -94,32 +90,38 @@ FileDrop.WebRTC.prototype._onConnection = function (connection) {
};
FileDrop.WebRTC.prototype._onBinaryData = function (data, connection) {
- var incoming = this.files.incoming[connection.peer],
+ var self = this,
+ incoming = this.files.incoming[connection.peer],
+ block = incoming.block,
info = incoming.info,
+ receivedChunkNum = incoming.receivedChunkNum,
chunksPerAck = FileDrop.WebRTC.CHUNKS_PER_ACK,
- cache = incoming.cache,
- receivedChunkNum, nextChunkNum, blob;
-
- cache.push(data);
-
- receivedChunkNum = cache.length - 1;
- nextChunkNum = receivedChunkNum + 1;
+ nextChunkNum, lastChunkInFile, lastChunkInBlock;
connection.emit('receiving_progress', receivedChunkNum / (info.chunksTotal - 1));
- console.log('Got chunk no ' + (receivedChunkNum + 1) + ' out of ' + info.chunksTotal);
+ // console.log('Got chunk no ' + (receivedChunkNum + 1) + ' out of ' + info.chunksTotal);
- if (receivedChunkNum === info.chunksTotal - 1) {
- // If all chunks were transmitted, create a blob
- blob = new Blob(cache, {type : info.type});
+ block.push(data);
- $.publish('file.p2p.peer', {
- blob: blob,
- connection: connection
+ nextChunkNum = incoming.receivedChunkNum = receivedChunkNum + 1;
+ lastChunkInFile = receivedChunkNum === info.chunksTotal - 1;
+ lastChunkInBlock = receivedChunkNum > 0 && ((receivedChunkNum + 1) % chunksPerAck) === 0;
+
+ if (lastChunkInFile || lastChunkInBlock) {
+ this.file.append(block).then(function () {
+ if (lastChunkInFile) {
+ self.file.save();
+
+ $.publish('file.p2p.peer', {
+ blob: self.file,
+ connection: connection
+ });
+ } else {
+ // console.log('Requesting block starting at: ' + (nextChunkNum));
+ incoming.block = [];
+ self._requestFileBlock(connection, nextChunkNum);
+ }
});
- } else if (receivedChunkNum > 0 && (receivedChunkNum + 1) % chunksPerAck === 0) {
- // If all chunks in a block were transmitted, request a new block
- console.log('Requesting block starting at: ' + (receivedChunkNum + 1));
- this._requestFileBlock(connection, receivedChunkNum + 1);
}
};
@@ -161,7 +163,7 @@ FileDrop.WebRTC.prototype._onJSONData = function (data, connection) {
case 'block_request':
var file = this.files.outgoing[connection.peer].file;
- console.log('Peer:\t Block request: ', data.payload);
+ // console.log('Peer:\t Block request: ', data.payload);
this._sendBlock(connection, file, data.payload);
break;
@@ -190,25 +192,31 @@ FileDrop.WebRTC.prototype.sendFileInfo = function (connection, info) {
};
FileDrop.WebRTC.prototype.sendFileResponse = function (connection, response) {
- var message = {
- type: 'response',
- payload: response
- };
+ var self = this,
+ message = {
+ type: 'response',
+ payload: response
+ };
- // If recipient rejected the file, delete stored file info
- if (!response) {
+ if (response) {
+ // If recipient accepted the file, request required space to store the file on HTML5 filesystem
+ var incoming = this.files.incoming[connection.peer],
+ info = incoming.info;
+
+ new FileDrop.File({name: info.name, size: info.size, type: info.type})
+ .then(function (file) {
+ self.file = file;
+
+ incoming.block = [];
+ incoming.receivedChunkNum = 0;
+
+ connection.send(JSON.stringify(message));
+ });
+ } else {
+ // Otherwise, delete stored file info
delete this.files.incoming[connection.peer];
+ connection.send(JSON.stringify(message));
}
-
- connection.send(JSON.stringify(message));
-};
-
-FileDrop.WebRTC.prototype._requestFileBlock = function (connection, chunkNum) {
- var message = {
- type: 'block_request',
- payload: chunkNum
- };
- connection.send(JSON.stringify(message));
};
FileDrop.WebRTC.prototype.sendFile = function (connection, file) {
@@ -222,6 +230,14 @@ FileDrop.WebRTC.prototype.sendFile = function (connection, file) {
this._sendBlock(connection, file, 0);
};
+FileDrop.WebRTC.prototype._requestFileBlock = function (connection, chunkNum) {
+ var message = {
+ type: 'block_request',
+ payload: chunkNum
+ };
+ connection.send(JSON.stringify(message));
+};
+
// FIXME: Figure out why 64th chunk is sent twice
FileDrop.WebRTC.prototype._sendBlock = function (connection, file, beginChunkNum) {
var info = this.files.outgoing[connection.peer].info,
@@ -231,7 +247,7 @@ FileDrop.WebRTC.prototype._sendBlock = function (connection, file, beginChunkNum
endChunkNum = beginChunkNum + chunksToSend - 1,
chunkNum;
- console.log('Send block: start: ' + beginChunkNum + ' end: ' + endChunkNum);
+ // console.log('Send block: start: ' + beginChunkNum + ' end: ' + endChunkNum);
for (chunkNum = beginChunkNum; chunkNum < endChunkNum + 1; chunkNum++) {
this._sendChunk(connection, file, chunkNum);
@@ -255,7 +271,7 @@ FileDrop.WebRTC.prototype._sendChunk = function (connection, file, chunkNum) {
connection.send(event.target.result);
connection.emit('sending_progress', chunkNum / (info.chunksTotal - 1));
- console.log('Sent chunk no ' + (chunkNum + 1) + ' out of ' + info.chunksTotal);
+ // console.log('Sent chunk no ' + (chunkNum + 1) + ' out of ' + info.chunksTotal);
}
};
reader.readAsArrayBuffer(blob);
diff --git a/app/scripts/initializer.js b/app/scripts/initializer.js
new file mode 100644
index 0000000..9fffd48
--- /dev/null
+++ b/app/scripts/initializer.js
@@ -0,0 +1,6 @@
+// Clear HTML5 filesystem on page load
+FileDrop.deferReadiness();
+FileDrop.File.removeAll().then(function () {
+ console.log("Cleared HTML5 filesystem");
+ FileDrop.advanceReadiness();
+});
diff --git a/app/scripts/vendor/filer.min.js b/app/scripts/vendor/filer.min.js
new file mode 100644
index 0000000..ca8b95e
--- /dev/null
+++ b/app/scripts/vendor/filer.min.js
@@ -0,0 +1,16 @@
+var self=this;self.URL=self.URL||self.webkitURL;self.requestFileSystem=self.requestFileSystem||self.webkitRequestFileSystem;self.resolveLocalFileSystemURL=self.resolveLocalFileSystemURL||self.webkitResolveLocalFileSystemURL;navigator.temporaryStorage=navigator.temporaryStorage||navigator.webkitTemporaryStorage;navigator.persistentStorage=navigator.persistentStorage||navigator.webkitPersistentStorage;self.BlobBuilder=self.BlobBuilder||self.MozBlobBuilder||self.WebKitBlobBuilder;
+if(void 0===self.FileError){var FileError=function(){};FileError.prototype.prototype=Error.prototype}
+var Util={toArray:function(a){return Array.prototype.slice.call(a||[],0)},strToDataURL:function(a,b,c){return(void 0!=c?c:1)?"data:"+b+";base64,"+self.btoa(a):"data:"+b+","+a},strToObjectURL:function(a,b){for(var c=new Uint8Array(a.length),e=0;ethis.length)c=this.length;0>c&&(c+=this.length);0>c&&(c=0)};this.truncate=function(a){b=b?af&&(f=0);b=new Blob([e,new Uint8Array(f),d,g],{type:b.type})}else b=new Blob([d],{type:d.type});a.file_.blob_=b;a.file_.lastModifiedDate=d.lastModifiedDate||null;h.put(a,function(){c+=d.size;if(this.onwriteend)this.onwriteend()}.bind(this),this.onerror)}}function y(a){var c=!1;this.readEntries=function(b,d){if(!b)throw Error("Expected successCallback argument.");c?b([]):h.getAllEntries(a.fullPath,function(a){c=!0;b(a)},d)}}function s(a,c){this.modificationTime_=a||null;this.size_=c||
+0}function o(){}function j(a){this.file_=null;Object.defineProperty(this,"isFile",{enumerable:!0,get:function(){return!0}});Object.defineProperty(this,"isDirectory",{enumerable:!0,get:function(){return!1}});if(a)this.file_=a.file_,this.name=a.name,this.fullPath=a.fullPath,this.filesystem=a.filesystem}function i(a){Object.defineProperty(this,"isFile",{enumerable:!0,get:function(){return!1}});Object.defineProperty(this,"isDirectory",{enumerable:!0,get:function(){return!0}});if(a)this.name=a.name,this.fullPath=
+a.fullPath,this.filesystem=a.filesystem}function z(a){t=a==f.TEMPORARY?"Temporary":"Persistent";this.name=(location.protocol+location.host).replace(/:/g,"_")+":"+t;this.root=new i;this.root.fullPath=g;this.root.filesystem=this;this.root.name=""}function k(a){switch(a.target.errorCode){case 12:console.log("Error - Attempt to open db with a lower version than the current one.");break;default:console.log("errorCode: "+a.target.errorCode)}console.log(a,a.code,a.message)}if(!f.requestFileSystem&&!f.webkitRequestFileSystem){var u=
+f.indexedDB||f.mozIndexedDB||f.msIndexedDB;if(u){f.TEMPORARY=0;f.PERSISTENT=1;if(void 0===f.FileError)window.FileError=function(){},FileError.prototype.prototype=Error.prototype;FileError.INVALID_MODIFICATION_ERR=9;FileError.NOT_FOUND_ERR=1;m.prototype=FileError.prototype;m.prototype.toString=Error.prototype.toString;var l=new m({code:FileError.INVALID_MODIFICATION_ERR,name:"INVALID_MODIFICATION_ERR"}),p=new m({code:1E3,name:"Not implemented"}),w=new m({code:FileError.NOT_FOUND_ERR,name:"Not found"}),
+n=null,t="temporary",h={db:null},g="/",v=String.fromCharCode(g.charCodeAt(0)+1);r.prototype.constructor=r;s.prototype={get modificationTime(){return this.modificationTime_},get size(){return this.size_}};o.prototype={name:null,fullPath:null,filesystem:null,copyTo:function(){throw p;},getMetadata:function(a,c){if(!a)throw Error("Expected successCallback argument.");try{this.isFile?a(new s(this.file_.lastModifiedDate,this.file_.size)):c(new m({code:1001,name:"getMetadata() not implemented for DirectoryEntry"}))}catch(b){c&&
+c(b)}},getParent:function(){throw p;},moveTo:function(){throw p;},remove:function(a,c){if(!a)throw Error("Expected successCallback argument.");h["delete"](this.fullPath,function(){a()},c)},toURL:function(){return"filesystem:"+(location.protocol+"//"+location.host)+g+t.toLowerCase()+this.fullPath}};j.prototype=new o;j.prototype.constructor=j;j.prototype.createWriter=function(a){a(new x(this))};j.prototype.file=function(a,c){if(!a)throw Error("Expected successCallback argument.");if(null==this.file_)if(c)c(w);
+else throw w;else{var b=null==this.file_.blob_?this.file_:this.file_.blob_;b.lastModifiedDate=this.file_.lastModifiedDate;a(b)}};i.prototype=new o;i.prototype.constructor=i;i.prototype.createReader=function(){return new y(this)};i.prototype.getDirectory=function(a,c,b,d){a=q(this.fullPath,a);h.get(a,function(e){c||(c={});!0===c.create&&!0===c.exclusive&&e?d&&d(l):!0===c.create&&!e?(e=new i,e.name=a.split(g).pop(),e.fullPath=a,e.filesystem=n,h.put(e,b,d)):!0===c.create&&e?e.isDirectory?b(new i(e)):
+d&&d(l):(!c.create||!1===c.create)&&!e?a==g?(e=new i,e.name="",e.fullPath=g,e.filesystem=n,b(e)):d&&d(l):(!c.create||!1===c.create)&&e&&e.isFile?d&&d(l):b(new i(e))},d)};i.prototype.getFile=function(a,c,b,d){a=q(this.fullPath,a);h.get(a,function(e){c||(c={});!0===c.create&&!0===c.exclusive&&e?d&&d(l):!0===c.create&&!e?(e=new j,e.name=a.split(g).pop(),e.fullPath=a,e.filesystem=n,e.file_=new r({size:0,name:e.name,lastModifiedDate:new Date}),h.put(e,b,d)):!0===c.create&&e?e.isFile?b(new j(e)):d&&d(l):
+(!c.create||!1===c.create)&&!e?d&&d(l):(!c.create||!1===c.create)&&e&&e.isDirectory?d&&d(l):b(new j(e))},d)};i.prototype.removeRecursively=function(a,c){if(!a)throw Error("Expected successCallback argument.");this.remove(a,c)};h.open=function(a,c,b){var d=this,a=u.open(a.replace(":","_"));a.onerror=b||k;a.onupgradeneeded=function(a){d.db=a.target.result;d.db.onerror=k;d.db.objectStoreNames.contains("entries")||d.db.createObjectStore("entries")};a.onsuccess=function(a){d.db=a.target.result;d.db.onerror=
+k;c(a)};a.onblocked=b||k};h.close=function(){this.db.close();this.db=null};h.drop=function(a,c){if(this.db){var b=u.deleteDatabase(this.db.name);b.onsuccess=function(b){a(b)};b.onerror=c||k;h.close()}};h.get=function(a,c,b){if(this.db){var d=this.db.transaction(["entries"],"readonly"),a=IDBKeyRange.bound(a,a+v,!1,!0),e=d.objectStore("entries").get(a);d.onabort=b||k;d.oncomplete=function(){c(e.result)}}};h.getAllEntries=function(a,c,b){if(this.db){var d=[],e=null;a!=g&&(e=IDBKeyRange.bound(a+g,a+v,
+!1,!0));var f=this.db.transaction(["entries"],"readonly");f.onabort=b||k;f.oncomplete=function(){d=d.filter(function(b){var c=b.fullPath.split(g).length,d=a.split(g).length;if(a==g&&c