UNPKG

create-torrent

Version:
402 lines (342 loc) 11.6 kB
/*! create-torrent. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */ import bencode from 'bencode' import blockIterator from 'block-iterator' import calcPieceLength from 'piece-length' import corePath from 'path' import isFile from 'is-file' import { isJunk } from 'junk' import joinIterator from 'join-async-iterator' import parallel from 'run-parallel' import queueMicrotask from 'queue-microtask' import { hash, hex2arr } from 'uint8-util' import 'fast-readable-async-iterator' import getFiles from './get-files.js' // browser exclude const announceList = [ ['udp://tracker.leechers-paradise.org:6969'], ['udp://tracker.coppersurfer.tk:6969'], ['udp://tracker.opentrackr.org:1337'], ['udp://explodie.org:6969'], ['udp://tracker.empire-js.us:1337'], ['wss://tracker.btorrent.xyz'], ['wss://tracker.openwebtorrent.com'], ['wss://tracker.webtorrent.dev'] ] /** * Create a torrent. * @param {string|File|FileList|Buffer|Stream|Array.<string|File|Buffer|Stream>} input * @param {Object} opts * @param {string=} opts.name * @param {Date=} opts.creationDate * @param {string=} opts.comment * @param {string=} opts.createdBy * @param {boolean|number=} opts.private * @param {number=} opts.pieceLength * @param {number=} opts.maxPieceLength * @param {Array.<Array.<string>>=} opts.announceList * @param {Array.<string>=} opts.urlList * @param {Object=} opts.info * @param {Function} opts.onProgress * @param {function} cb * @return {Buffer} buffer of .torrent file data */ function createTorrent (input, opts, cb) { if (typeof opts === 'function') [opts, cb] = [cb, opts] opts = opts ? Object.assign({}, opts) : {} _parseInput(input, opts, (err, files, singleFileTorrent) => { if (err) return cb(err) opts.singleFileTorrent = singleFileTorrent onFiles(files, opts, cb) }) } function parseInput (input, opts, cb) { if (typeof opts === 'function') [opts, cb] = [cb, opts] opts = opts ? Object.assign({}, opts) : {} _parseInput(input, opts, cb) } const pathSymbol = Symbol('itemPath') /** * Parse input file and return file information. */ function _parseInput (input, opts, cb) { if (isFileList(input)) input = Array.from(input) if (!Array.isArray(input)) input = [input] if (input.length === 0) throw new Error('invalid input type') input.forEach(item => { if (item == null) throw new Error(`invalid input type: ${item}`) }) // In Electron, use the true file path input = input.map(item => { if (isBlob(item) && typeof item.path === 'string' && typeof getFiles === 'function') return item.path return item }) // If there's just one file, allow the name to be set by `opts.name` if (input.length === 1 && typeof input[0] !== 'string' && !input[0].name) input[0].name = opts.name let commonPrefix = null input.forEach((item, i) => { if (typeof item === 'string') { return } let path = item.fullPath || item.name if (!path) { path = `Unknown File ${i + 1}` item.unknownName = true } item[pathSymbol] = path.split('/') // Remove initial slash if (!item[pathSymbol][0]) { item[pathSymbol].shift() } if (item[pathSymbol].length < 2) { // No real prefix commonPrefix = null } else if (i === 0 && input.length > 1) { // The first file has a prefix commonPrefix = item[pathSymbol][0] } else if (item[pathSymbol][0] !== commonPrefix) { // The prefix doesn't match commonPrefix = null } }) const filterJunkFiles = opts.filterJunkFiles === undefined ? true : opts.filterJunkFiles if (filterJunkFiles) { // Remove junk files input = input.filter(item => { if (typeof item === 'string') { return true } return !isJunkPath(item[pathSymbol]) }) } if (commonPrefix) { input.forEach(item => { const pathless = (ArrayBuffer.isView(item) || isReadable(item)) && !item[pathSymbol] if (typeof item === 'string' || pathless) return item[pathSymbol].shift() }) } if (!opts.name && commonPrefix) { opts.name = commonPrefix } if (!opts.name) { // use first user-set file name input.some(item => { if (typeof item === 'string') { opts.name = corePath.basename(item) return true } else if (!item.unknownName) { opts.name = item[pathSymbol][item[pathSymbol].length - 1] return true } return false }) } if (!opts.name) { opts.name = `Unnamed Torrent ${Date.now()}` } if (!opts.maxPieceLength) { opts.maxPieceLength = 4 * 1024 * 1024 } const numPaths = input.reduce((sum, item) => sum + Number(typeof item === 'string'), 0) let isSingleFileTorrent = (input.length === 1) if (input.length === 1 && typeof input[0] === 'string') { if (typeof getFiles !== 'function') { throw new Error('filesystem paths do not work in the browser') } // If there's a single path, verify it's a file before deciding this is a single // file torrent isFile(input[0], (err, pathIsFile) => { if (err) return cb(err) isSingleFileTorrent = pathIsFile processInput() }) } else { queueMicrotask(processInput) } function processInput () { parallel(input.map(item => cb => { const file = {} if (isBlob(item)) { file.getStream = item.stream() file.length = item.size } else if (ArrayBuffer.isView(item)) { file.getStream = [item] // wrap in iterable to write entire buffer at once instead of unwrapping all bytes file.length = item.length } else if (isReadable(item)) { file.getStream = getStreamStream(item, file) file.length = 0 } else if (typeof item === 'string') { if (typeof getFiles !== 'function') { throw new Error('filesystem paths do not work in the browser') } const keepRoot = numPaths > 1 || isSingleFileTorrent getFiles(item, keepRoot, cb) return // early return! } else { throw new Error('invalid input type') } file.path = item[pathSymbol] cb(null, file) }), (err, files) => { if (err) return cb(err) files = files.flat() cb(null, files, isSingleFileTorrent) }) } } const MAX_OUTSTANDING_HASHES = 5 async function getPieceList (files, pieceLength, estimatedTorrentLength, opts, cb) { const pieces = [] let length = 0 let hashedLength = 0 const streams = files.map(file => file.getStream) const onProgress = opts.onProgress let remainingHashes = 0 let pieceNum = 0 let ended = false const iterator = blockIterator(joinIterator(streams), pieceLength, { zeroPadding: false }) try { for await (const chunk of iterator) { await new Promise(resolve => { length += chunk.length const i = pieceNum ++pieceNum if (++remainingHashes < MAX_OUTSTANDING_HASHES) resolve() hash(chunk, 'hex').then(hash => { pieces[i] = hash --remainingHashes hashedLength += chunk.length if (onProgress) onProgress(hashedLength, estimatedTorrentLength) resolve() if (ended && remainingHashes === 0) cb(null, hex2arr(pieces.join('')), length) }) }) } if (remainingHashes === 0) return cb(null, hex2arr(pieces.join('')), length) ended = true } catch (err) { cb(err) } } function onFiles (files, opts, cb) { let _announceList = opts.announceList if (!_announceList) { if (typeof opts.announce === 'string') _announceList = [[opts.announce]] else if (Array.isArray(opts.announce)) { _announceList = opts.announce.map(u => [u]) } } if (!_announceList) _announceList = [] if (globalThis.WEBTORRENT_ANNOUNCE) { if (typeof globalThis.WEBTORRENT_ANNOUNCE === 'string') { _announceList.push([[globalThis.WEBTORRENT_ANNOUNCE]]) } else if (Array.isArray(globalThis.WEBTORRENT_ANNOUNCE)) { _announceList = _announceList.concat(globalThis.WEBTORRENT_ANNOUNCE.map(u => [u])) } } // When no trackers specified, use some reasonable defaults if (opts.announce === undefined && opts.announceList === undefined) { _announceList = _announceList.concat(announceList) } if (typeof opts.urlList === 'string') opts.urlList = [opts.urlList] const torrent = { info: { name: opts.name }, 'creation date': Math.ceil((Number(opts.creationDate) || Date.now()) / 1000), encoding: 'UTF-8' } if (_announceList.length !== 0) { torrent.announce = _announceList[0][0] torrent['announce-list'] = _announceList } if (opts.comment !== undefined) torrent.comment = opts.comment if (opts.createdBy !== undefined) torrent['created by'] = opts.createdBy if (opts.private !== undefined) torrent.info.private = Number(opts.private) if (opts.info !== undefined) Object.assign(torrent.info, opts.info) // "ssl-cert" key is for SSL torrents, see: // - http://blog.libtorrent.org/2012/01/bittorrent-over-ssl/ // - http://www.libtorrent.org/manual-ref.html#ssl-torrents // - http://www.libtorrent.org/reference-Create_Torrents.html if (opts.sslCert !== undefined) torrent.info['ssl-cert'] = opts.sslCert if (opts.urlList !== undefined) torrent['url-list'] = opts.urlList const estimatedTorrentLength = files.reduce(sumLength, 0) const pieceLength = opts.pieceLength || Math.min(calcPieceLength(estimatedTorrentLength), opts.maxPieceLength) torrent.info['piece length'] = pieceLength getPieceList( files, pieceLength, estimatedTorrentLength, opts, (err, pieces, torrentLength) => { if (err) return cb(err) torrent.info.pieces = pieces files.forEach(file => { delete file.getStream }) if (opts.singleFileTorrent) { torrent.info.length = torrentLength } else { torrent.info.files = files } cb(null, bencode.encode(torrent)) } ) } /** * Determine if a a file is junk based on its path * (defined as hidden OR recognized by the `junk` package) * * @param {string} path * @return {boolean} */ function isJunkPath (path) { const filename = path[path.length - 1] return filename[0] === '.' && isJunk(filename) } /** * Accumulator to sum file lengths * @param {number} sum * @param {Object} file * @return {number} */ function sumLength (sum, file) { return sum + file.length } /** * Check if `obj` is a W3C `Blob` object (which `File` inherits from) * @param {*} obj * @return {boolean} */ function isBlob (obj) { return typeof Blob !== 'undefined' && obj instanceof Blob } /** * Check if `obj` is a W3C `FileList` object * @param {*} obj * @return {boolean} */ function isFileList (obj) { return typeof FileList !== 'undefined' && obj instanceof FileList } /** * Check if `obj` is a node Readable stream * @param {*} obj * @return {boolean} */ function isReadable (obj) { return typeof obj === 'object' && obj != null && typeof obj.pipe === 'function' } /** * Convert a readable stream to a lazy async iterator. Adds instrumentation to track * the number of bytes in the stream and set `file.length`. * * @generator * @param {Stream} readable * @param {Object} file * @return {Uint8Array} stream data/chunk */ async function * getStreamStream (readable, file) { for await (const chunk of readable) { file.length += chunk.length yield chunk } } export default createTorrent export { parseInput, announceList, isJunkPath }