2018-04-06 19:28:04 +10:00
/ *
This Transport layers builds on WebTorrent
Y Lists have listeners and generate events - see docs at ...
* /
// WebTorrent components
const WebTorrent = require ( 'webtorrent' ) ;
const stream = require ( 'readable-stream' ) ;
// Other Dweb modules
const errors = require ( './Errors' ) ; // Standard Dweb Errors
const Transport = require ( './Transport.js' ) ; // Base class for TransportXyz
const Transports = require ( './Transports' ) ; // Manage all Transports that are loaded
let defaultoptions = {
webtorrent : { }
} ;
class TransportWEBTORRENT extends Transport {
/ *
WebTorrent specific transport
Fields :
webtorrent : object returned when starting webtorrent
* /
constructor ( options , verbose ) {
super ( options , verbose ) ;
this . webtorrent = undefined ; // Undefined till start WebTorrent
this . options = options ; // Dictionary of options
this . name = "WEBTORRENT" ; // For console log etc
this . supportURLs = [ 'magnet' ] ;
this . supportFunctions = [ 'fetch' , 'createReadStream' ] ;
this . status = Transport . STATUS _LOADED ;
}
p _webtorrentstart ( verbose ) {
/ *
Start WebTorrent and wait until for ready .
* /
let self = this ;
return new Promise ( ( resolve , reject ) => {
this . webtorrent = new WebTorrent ( this . options . webtorrent ) ;
this . webtorrent . once ( "ready" , ( ) => {
console . log ( "WEBTORRENT READY" ) ;
resolve ( ) ;
} ) ;
this . webtorrent . once ( "error" , ( err ) => reject ( err ) ) ;
this . webtorrent . on ( "warning" , ( err ) => {
console . warn ( "WebTorrent Torrent WARNING: " + err . message ) ;
} ) ;
} )
}
static setup0 ( options , verbose ) {
/ *
First part of setup , create obj , add to Transports but dont attempt to connect , typically called instead of p _setup if want to parallelize connections .
* /
let combinedoptions = Transport . mergeoptions ( defaultoptions , options ) ;
console . log ( "WebTorrent options %o" , combinedoptions ) ;
let t = new TransportWEBTORRENT ( combinedoptions , verbose ) ;
Transports . addtransport ( t ) ;
return t ;
}
2018-04-08 14:53:19 +10:00
async p _setup1 ( verbose , cb ) {
2018-04-06 19:28:04 +10:00
try {
this . status = Transport . STATUS _STARTING ;
2018-04-08 14:53:19 +10:00
if ( cb ) cb ( this ) ;
2018-04-06 19:28:04 +10:00
await this . p _webtorrentstart ( verbose ) ;
2018-04-08 14:53:19 +10:00
await this . p _status ( verbose ) ;
2018-04-06 19:28:04 +10:00
} catch ( err ) {
console . error ( "WebTorrent failed to connect" , err ) ;
this . status = Transport . STATUS _FAILED ;
}
2018-04-08 14:53:19 +10:00
if ( cb ) cb ( this ) ;
2018-04-06 19:28:04 +10:00
return this ;
}
async p _status ( verbose ) {
/ *
Return a string for the status of a transport . No particular format , but keep it short as it will probably be in a small area of the screen .
* /
if ( this . webtorrent && this . webtorrent . ready ) {
this . status = Transport . STATUS _CONNECTED ;
} else if ( this . webtorrent ) {
this . status = Transport . STATUS _STARTING ;
} else {
this . status = Transport . STATUS _FAILED ;
}
return this . status ;
}
webtorrentparseurl ( url ) {
/ * P a r s e a U R L
2018-04-16 08:56:13 +10:00
url : URL as string or already parsed into Url - should start magnet : or in future might support dweb : /magnet/ ; some other formats might be supported
2018-04-06 19:28:04 +10:00
returns : torrentid , path
* /
if ( ! url ) {
throw new errors . CodingError ( "TransportWEBTORRENT.p_rawfetch: requires url" ) ;
}
2018-04-16 08:56:13 +10:00
const urlstring = ( typeof url === "string" ? url : url . href )
2018-04-06 19:28:04 +10:00
const index = urlstring . indexOf ( '/' ) ;
if ( index === - 1 ) {
throw new errors . CodingError ( "TransportWEBTORRENT.p_rawfetch: invalid url - missing path component. Should look like magnet:xyzabc/path/to/file" ) ;
}
const torrentId = urlstring . slice ( 0 , index ) ;
const path = urlstring . slice ( index + 1 ) ;
return { torrentId , path }
}
async p _webtorrentadd ( torrentId ) {
return new Promise ( ( resolve , reject ) => {
// Check if this torrentId is already added to the webtorrent client
let torrent = this . webtorrent . get ( torrentId ) ;
// If not, then add the torrentId to the torrent client
if ( ! torrent ) {
torrent = this . webtorrent . add ( torrentId ) ;
torrent . once ( "error" , ( err ) => {
reject ( new errors . TransportError ( "Torrent encountered a fatal error " + err . message ) ) ;
} ) ;
torrent . on ( "warning" , ( err ) => {
console . warn ( "WebTorrent Torrent WARNING: " + err . message + " (" + torrent . name + ")" ) ;
} ) ;
}
if ( torrent . ready ) {
resolve ( torrent ) ;
} else {
torrent . once ( "ready" , ( ) => {
resolve ( torrent ) ;
} ) ;
}
if ( typeof window !== "undefined" ) { // Check running in browser
window . WEBTORRENT _TORRENT = torrent ;
torrent . once ( 'close' , ( ) => {
window . WEBTORRENT _TORRENT = null
} )
}
} ) ;
}
webtorrentfindfile ( torrent , path ) {
/ *
Given a torrent object and a path to a file within the torrent , find the given file .
* /
const filePath = torrent . name + '/' + path ;
const file = torrent . files . find ( file => {
return file . path === filePath ;
} ) ;
if ( ! file ) {
//debugger;
throw new errors . TransportError ( "Requested file (" + path + ") not found within torrent " ) ;
}
return file ;
}
p _rawfetch ( url , { verbose = false } = { } ) {
/ *
Fetch some bytes based on a url of the form :
magnet : xyzabc / path / to / file
( Where xyzabc is the typical magnet uri contents )
No assumption is made about the data in terms of size or structure . Returns a new Promise that resolves to a buffer .
: param string url : URL of object being retrieved
: param boolean verbose : true for debugging output
: resolve buffer : Return the object being fetched .
: throws : TransportError if url invalid - note this happens immediately , not as a catch in the promise
* /
return new Promise ( ( resolve , reject ) => {
if ( verbose ) console . log ( "WebTorrent p_rawfetch" , url ) ;
const { torrentId , path } = this . webtorrentparseurl ( url ) ;
this . p _webtorrentadd ( torrentId )
. then ( ( torrent ) => {
2018-04-16 08:56:13 +10:00
torrent . deselect ( 0 , torrent . pieces . length - 1 , false ) ; // Dont download entire torrent as will pull just the file we want (warning - may give problems if multiple reads from same webtorrent)
2018-04-06 19:28:04 +10:00
const file = this . webtorrentfindfile ( torrent , path ) ;
file . getBuffer ( ( err , buffer ) => {
if ( err ) {
return reject ( new errors . TransportError ( "Torrent encountered a fatal error " + err . message + " (" + torrent . name + ")" ) ) ;
}
resolve ( buffer ) ;
} ) ;
} )
. catch ( ( err ) => reject ( err ) ) ;
} ) ;
}
2018-04-16 08:56:13 +10:00
async _p _fileTorrentFromUrl ( url ) {
/ *
Then open a webtorrent for the file specified in the path part of the url
url : of form magnet : ... or magnet / : ...
return : Web Torrent file
* /
2018-04-06 19:28:04 +10:00
try {
const { torrentId , path } = this . webtorrentparseurl ( url ) ;
let torrent = await this . p _webtorrentadd ( torrentId ) ;
2018-04-16 08:56:13 +10:00
torrent . deselect ( 0 , torrent . pieces . length - 1 , false ) ; // Dont download entire torrent as will pull just the file we want (warning - may give problems if multiple reads from same webtorrent)
return this . webtorrentfindfile ( torrent , path ) ;
2018-04-06 19:28:04 +10:00
} catch ( err ) {
2018-04-16 08:56:13 +10:00
console . log ( ` p_fileFrom failed on ${ url } ${ err . message } ` ) ;
2018-04-06 19:28:04 +10:00
throw ( err ) ;
} ;
2018-04-16 08:56:13 +10:00
}
2018-04-16 18:58:18 +10:00
2018-04-16 08:56:13 +10:00
async p _f _createReadStream ( url , { verbose = false , wanturl = false } = { } ) { //TODO-API
2018-04-06 19:28:04 +10:00
/ *
Fetch bytes progressively , using a node . js readable stream , based on a url of the form :
2018-04-16 08:56:13 +10:00
No assumption is made about the data in terms of size or structure .
2018-04-06 19:28:04 +10:00
2018-04-16 18:58:18 +10:00
This is the initialisation step , which returns a function suitable for < VIDEO >
2018-04-06 19:28:04 +10:00
2018-04-16 08:56:13 +10:00
Returns a new Promise that resolves to function for a node . js readable stream .
2018-04-06 19:28:04 +10:00
2018-04-16 08:56:13 +10:00
Node . js readable stream docs : https : //nodejs.org/api/stream.html#stream_readable_streams
: param string url : URL of object being retrieved of form magnet : xyzabc / path / to / file ( Where xyzabc is the typical magnet uri contents )
2018-04-06 19:28:04 +10:00
: param boolean verbose : true for debugging output
2018-04-16 08:56:13 +10:00
: resolves to : f ( { start , end } ) => stream ( The readable stream . )
2018-04-06 19:28:04 +10:00
: throws : TransportError if url invalid - note this happens immediately , not as a catch in the promise
* /
2018-04-16 08:56:13 +10:00
if ( verbose ) console . log ( "TransportWEBTORRENT p_f_createreadstream %o" , url ) ;
try {
let filet = this . _p _fileTorrentFromUrl ( url ) ;
let self = this ;
if ( wanturl ) {
2018-04-16 18:52:36 +10:00
console . log ( "XXX@WT:242 returning" , url )
2018-04-16 08:56:13 +10:00
return url ;
} else {
return function ( opts ) { return self . createReadStream ( filet , opts , verbose ) ; } ;
}
} catch ( err ) {
console . log ( ` p_f_createReadStream failed on ${ url } ${ err . message } ` ) ;
throw ( err ) ;
} ;
}
2018-04-06 19:28:04 +10:00
2018-04-16 08:56:13 +10:00
createReadStream ( file , opts , verbose ) {
/ *
The function , encapsulated and inside another function by p _f _createReadStream ( see docs )
: param file : Webtorrent "file" as returned by webtorrentfindfile
: param opts : { start : byte to start from ; end : optional end byte }
: param boolean verbose : true for debugging output
: returns stream : The readable stream .
* /
2018-04-16 18:52:36 +10:00
verbose = true ; //TODO-SW just for debugging
2018-04-16 08:56:13 +10:00
if ( verbose ) console . log ( "TransportWEBTORRENT createreadstream %o %o" , file . name , opts ) ;
2018-04-06 19:28:04 +10:00
try {
const through = new stream . PassThrough ( ) ;
const fileStream = file . createReadStream ( opts ) ;
fileStream . pipe ( through ) ;
return through ;
2018-04-16 18:52:36 +10:00
//return fileStream; //TODO-SW-WT this was "return through" and will need to be for non-SW cases
2018-04-06 19:28:04 +10:00
} catch ( err ) {
2018-04-16 08:56:13 +10:00
console . log ( "TransportWEBTORRENT caught error" , err )
2018-04-06 19:28:04 +10:00
if ( typeof through . destroy === 'function' ) through . destroy ( err )
else through . emit ( 'error' , err )
} ;
}
2018-04-16 08:56:13 +10:00
async p _createReadStream ( url , opts , verbose ) {
let filet = await this . _p _fileTorrentFromUrl ( url ) ;
return this . createReadStream ( filet , opts , verbose ) ;
}
2018-04-06 19:28:04 +10:00
static async p _test ( opts , verbose ) {
try {
let transport = await this . p _setup ( opts , verbose ) ; // Assumes IPFS already setup
if ( verbose ) console . log ( transport . name , "setup" ) ;
let res = await transport . p _status ( verbose ) ;
console . assert ( res === Transport . STATUS _CONNECTED )
// Creative commons torrent, copied from https://webtorrent.io/free-torrents
let bigBuckBunny = 'magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c&dn=Big+Buck+Bunny&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fbig-buck-bunny.torrent/Big Buck Bunny.en.srt' ;
let data1 = await transport . p _rawfetch ( bigBuckBunny , { verbose } ) ;
data1 = data1 . toString ( ) ;
assertData ( data1 ) ;
const stream = await transport . createReadStream ( bigBuckBunny , verbose ) ;
const chunks = [ ] ;
stream . on ( "data" , ( chunk ) => {
chunks . push ( chunk ) ;
} ) ;
stream . on ( "end" , ( ) => {
const data2 = Buffer . concat ( chunks ) . toString ( ) ;
assertData ( data2 ) ;
} ) ;
function assertData ( data ) {
// Test for a string that is contained within the file
let expectedWithinData = "00:00:02,000 --> 00:00:05,000" ;
console . assert ( data . indexOf ( expectedWithinData ) !== - 1 , "Should fetch 'Big Buck Bunny.en.srt' from the torrent" ) ;
// Test that the length is what we expect
console . assert ( data . length , 129 , "'Big Buck Bunny.en.srt' was " + data . length ) ;
}
} catch ( err ) {
console . log ( "Exception thrown in TransportWEBTORRENT.p_test:" , err . message ) ;
throw err ;
}
}
}
Transports . _transportclasses [ "WEBTORRENT" ] = TransportWEBTORRENT ;
exports = module . exports = TransportWEBTORRENT ;