├── .npmignore ├── .gitignore ├── img.jpg ├── test ├── browser │ ├── package.json │ ├── directory.js │ └── basic.js ├── error.js ├── ssl.js ├── buffer.js ├── mixed.js ├── source.js ├── stream.js ├── basic.js └── fs.js ├── .github └── workflows │ ├── ci.yml │ ├── stale.yml │ └── release.yml ├── LICENSE ├── get-files.js ├── bin └── cmd.js ├── package.json ├── README.md ├── CHANGELOG.md └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | img.jpg 2 | test/ 3 | .github/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/create-torrent/master/img.jpg -------------------------------------------------------------------------------- /test/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.0.0", 4 | "browserify": { 5 | "transform": ["brfs"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/error.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import createTorrent from '../index.js' 3 | 4 | test('error handling', t => { 5 | t.plan(5) 6 | 7 | t.throws(() => createTorrent(null, () => {})) 8 | t.throws(() => createTorrent(undefined, () => {})) 9 | t.throws(() => createTorrent([null], () => {})) 10 | t.throws(() => createTorrent([undefined], () => {})) 11 | t.throws(() => createTorrent([null, undefined], () => {})) 12 | }) 13 | -------------------------------------------------------------------------------- /test/ssl.js: -------------------------------------------------------------------------------- 1 | import parseTorrent from 'parse-torrent' 2 | import test from 'tape' 3 | import createTorrent from '../index.js' 4 | 5 | test('create ssl cert torrent', t => { 6 | t.plan(2) 7 | 8 | const sslCert = new Uint8Array(Buffer.from('content cert X.509')) 9 | 10 | createTorrent(Buffer.from('abc'), { 11 | name: 'abc.txt', 12 | sslCert 13 | }, async (err, torrent) => { 14 | t.error(err) 15 | const parsedTorrent = await parseTorrent(torrent) 16 | t.deepEqual(parsedTorrent.info['ssl-cert'], sslCert) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node ${{ matrix.node }} / ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | node: 15 | - '18' 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node }} 21 | - run: npm install 22 | - run: npm run build --if-present 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: '0 12 * * *' 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | 14 | steps: 15 | - uses: actions/stale@v8 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | stale-issue-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?' 19 | stale-pr-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?' 20 | exempt-issue-labels: accepted,blocked,bug,security,meta 21 | exempt-pr-labels: accepted,blocked,bug,'help wanted',security,meta 22 | stale-issue-label: 'stale' 23 | stale-pr-label: 'stale' 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | persist-credentials: false 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | - name: Cache 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-npm-${{ hashFiles('**/package.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-npm- 28 | - name: Install dependencies 29 | run: npm i 30 | env: 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | - name: Release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | run: npx semantic-release 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh and WebTorrent, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/buffer.js: -------------------------------------------------------------------------------- 1 | import createTorrent from '../index.js' 2 | import parseTorrent from 'parse-torrent' 3 | import path from 'path' 4 | import { hash } from 'uint8-util' 5 | import test from 'tape' 6 | 7 | test('create nested torrent with array of buffers', t => { 8 | t.plan(14) 9 | 10 | const buf1 = Buffer.from('bl') 11 | buf1.name = 'dir1/buf1.txt' 12 | 13 | const buf2 = Buffer.from('ah') 14 | buf2.name = 'dir2/buf2.txt' 15 | 16 | const startTime = Date.now() 17 | createTorrent([buf1, buf2], { 18 | name: 'multi' 19 | }, async (err, torrent) => { 20 | t.error(err) 21 | 22 | const parsedTorrent = await parseTorrent(torrent) 23 | 24 | t.equals(parsedTorrent.name, 'multi') 25 | 26 | t.notOk(parsedTorrent.private) 27 | 28 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 29 | 30 | t.ok(Array.isArray(parsedTorrent.announce)) 31 | 32 | t.deepEquals(path.normalize(parsedTorrent.files[0].path), path.normalize('multi/dir1/buf1.txt')) 33 | t.deepEquals(parsedTorrent.files[0].length, 2) 34 | 35 | t.deepEquals(path.normalize(parsedTorrent.files[1].path), path.normalize('multi/dir2/buf2.txt')) 36 | t.deepEquals(parsedTorrent.files[1].length, 2) 37 | 38 | t.equal(parsedTorrent.length, 4) 39 | t.equal(parsedTorrent.info.pieces.length, 20) 40 | t.equal(parsedTorrent.pieceLength, 16384) 41 | 42 | t.deepEquals(parsedTorrent.pieces, [ 43 | '5bf1fd927dfb8679496a2e6cf00cbe50c1c87145' 44 | ]) 45 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 46 | t.equals(hash, '8fa3c08e640db9576156b21f31353402456a0208') 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /get-files.js: -------------------------------------------------------------------------------- 1 | import corePath from 'path' 2 | import fs from 'fs' 3 | import { isNotJunk } from 'junk' 4 | import once from 'once' 5 | import parallel from 'run-parallel' 6 | 7 | function notHidden (file) { 8 | return file[0] !== '.' 9 | } 10 | 11 | function traversePath (path, fn, cb) { 12 | fs.stat(path, (err, stats) => { 13 | if (err) return cb(err) 14 | if (stats.isDirectory()) { 15 | fs.readdir(path, (err, entries) => { 16 | if (err) return cb(err) 17 | parallel(entries.filter(notHidden).filter(isNotJunk).map(entry => cb => { 18 | traversePath(corePath.join(path, entry), fn, cb) 19 | }), cb) 20 | }) 21 | } else if (stats.isFile()) { 22 | fn(path, cb) 23 | } 24 | // Ignore other types (not a file or directory) 25 | }) 26 | } 27 | 28 | /** 29 | * Convert a file path to a lazy readable stream. 30 | * @param {string} path 31 | * @return {function} 32 | */ 33 | function getFilePathStream (path) { 34 | return () => fs.createReadStream(path) 35 | } 36 | 37 | export default function getFiles (path, keepRoot, cb) { 38 | traversePath(path, getFileInfo, (err, files) => { 39 | if (err) return cb(err) 40 | 41 | if (Array.isArray(files)) files = files.flat(Infinity) 42 | else files = [files] 43 | 44 | path = corePath.normalize(path) 45 | if (keepRoot) { 46 | path = path.slice(0, path.lastIndexOf(corePath.sep) + 1) 47 | } 48 | if (path[path.length - 1] !== corePath.sep) path += corePath.sep 49 | 50 | files.forEach(file => { 51 | file.getStream = getFilePathStream(file.path) 52 | file.path = file.path.replace(path, '').split(corePath.sep) 53 | }) 54 | 55 | cb(null, files) 56 | }) 57 | } 58 | 59 | function getFileInfo (path, cb) { 60 | cb = once(cb) 61 | fs.stat(path, (err, stat) => { 62 | if (err) return cb(err) 63 | const info = { 64 | length: stat.size, 65 | path 66 | } 67 | cb(null, info) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs' 3 | import minimist from 'minimist' 4 | import createTorrent from '../index.js' 5 | 6 | const argv = minimist(process.argv.slice(2), { 7 | alias: { 8 | o: 'outfile', 9 | n: 'name', 10 | h: 'help', 11 | v: 'version' 12 | }, 13 | boolean: [ 14 | 'help', 15 | 'version', 16 | 'private' 17 | ], 18 | string: [ 19 | 'outfile', 20 | 'name', 21 | 'creationDate', 22 | 'comment', 23 | 'createdBy', 24 | 'announce', 25 | 'urlList' 26 | ], 27 | default: { 28 | createdBy: 'WebTorrent ' 29 | } 30 | }) 31 | 32 | const infile = argv._[0] 33 | const outfile = argv.outfile 34 | const help = `usage: create-torrent [OPTIONS] 35 | 36 | Create a torrent file from a directory or file. 37 | 38 | If an output file isn't specified with '-o', the torrent file will be 39 | written to stdout. 40 | 41 | -o, --outfile Output file. If not specified, stdout is used [string] 42 | -n, --name Torrent name [string] 43 | --creationDate Creation date [Date] 44 | --comment Torrent comment [string] 45 | --createdBy Created by client [string] 46 | --private Private torrent? [boolean] [default: false] 47 | --pieceLength Piece length [number] [default: reasonable length] 48 | --announce Tracker url [string] [default: reasonable trackers] 49 | --urlList Web seed url [string]` 50 | 51 | if (argv.version) { 52 | console.log(require('../package.json').version) 53 | process.exit(0) 54 | } 55 | 56 | if (!infile || argv.help) { 57 | console.log(help) 58 | process.exit(0) 59 | } 60 | 61 | createTorrent(infile, argv, (err, torrent) => { 62 | if (err) { 63 | console.error(err.stack) 64 | process.exit(1) 65 | } else if (outfile) { 66 | fs.writeFile(outfile, torrent, err => { 67 | if (err) { 68 | console.error(err.stack) 69 | process.exit(1) 70 | } 71 | }) 72 | } else { 73 | process.stdout.write(torrent) 74 | } 75 | }) 76 | -------------------------------------------------------------------------------- /test/browser/directory.js: -------------------------------------------------------------------------------- 1 | /* global Blob */ 2 | 3 | import fs from 'fs' 4 | import parseTorrent from 'parse-torrent' 5 | import path from 'path' 6 | import { sha1 } from 'uint8-util' 7 | import test from 'tape' 8 | import createTorrent from '../../index.js' 9 | 10 | function makeFileShim (buf, name, fullPath) { 11 | const file = new Blob([buf]) 12 | file.fullPath = fullPath 13 | file.name = name 14 | return file 15 | } 16 | 17 | const numbers1 = makeFileShim(fs.readFileSync(path.join(__dirname, '../../node_modules/webtorrent-fixtures/fixtures/numbers/1.txt'), 'utf8'), '1.txt', 'numbers/1.txt') 18 | const numbers2 = makeFileShim(fs.readFileSync(path.join(__dirname, '../../node_modules/webtorrent-fixtures/fixtures/numbers/2.txt'), 'utf8'), '2.txt', 'numbers/2.txt') 19 | const numbers3 = makeFileShim(fs.readFileSync(path.join(__dirname, '../../node_modules/webtorrent-fixtures/fixtures/numbers/3.txt'), 'utf8'), '3.txt', 'numbers/3.txt') 20 | 21 | const DSStore = makeFileShim('blah', '.DS_Store', '/numbers/.DS_Store') // this should be ignored 22 | 23 | test('create multi file torrent with directory at root', t => { 24 | t.plan(15) 25 | 26 | const startTime = Date.now() 27 | createTorrent([numbers1, numbers2, numbers3, DSStore], (err, torrent) => { 28 | t.error(err) 29 | 30 | const parsedTorrent = parseTorrent(torrent) 31 | 32 | t.equals(parsedTorrent.name, 'numbers') 33 | 34 | t.notOk(parsedTorrent.private) 35 | 36 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 37 | 38 | t.ok(Array.isArray(parsedTorrent.announce)) 39 | 40 | t.deepEquals(parsedTorrent.files[0].path, 'numbers/1.txt') 41 | t.deepEquals(parsedTorrent.files[0].length, 1) 42 | 43 | t.deepEquals(parsedTorrent.files[1].path, 'numbers/2.txt') 44 | t.deepEquals(parsedTorrent.files[1].length, 2) 45 | 46 | t.deepEquals(parsedTorrent.files[2].path, 'numbers/3.txt') 47 | t.deepEquals(parsedTorrent.files[2].length, 3) 48 | 49 | t.equal(parsedTorrent.length, 6) 50 | t.equal(parsedTorrent.info.pieces.length, 20) 51 | 52 | t.deepEquals(parsedTorrent.pieces, [ 53 | '1f74648e50a6a6708ec54ab327a163d5536b7ced' 54 | ]) 55 | 56 | t.equals(sha1.sync(parsedTorrent.infoBuffer), '89d97c2261a21b040cf11caa661a3ba7233bb7e6') 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-torrent", 3 | "description": "Create .torrent files", 4 | "version": "6.0.17", 5 | "author": { 6 | "name": "WebTorrent LLC", 7 | "email": "feross@webtorrent.io", 8 | "url": "https://webtorrent.io" 9 | }, 10 | "type": "module", 11 | "bin": { 12 | "create-torrent": "./bin/cmd.js" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/webtorrent/create-torrent/issues" 16 | }, 17 | "browser": { 18 | "./get-files.js": false, 19 | "is-file": false 20 | }, 21 | "dependencies": { 22 | "bencode": "^4.0.0", 23 | "block-iterator": "^1.1.1", 24 | "fast-readable-async-iterator": "^2.0.0", 25 | "is-file": "^1.0.0", 26 | "join-async-iterator": "^1.1.1", 27 | "junk": "^4.0.1", 28 | "minimist": "^1.2.8", 29 | "once": "^1.4.0", 30 | "piece-length": "^2.0.1", 31 | "queue-microtask": "^1.2.3", 32 | "run-parallel": "^1.2.0", 33 | "uint8-util": "^2.2.4" 34 | }, 35 | "devDependencies": { 36 | "@webtorrent/semantic-release-config": "1.0.10", 37 | "brfs": "2.0.2", 38 | "parse-torrent": "11.0.16", 39 | "semantic-release": "21.1.2", 40 | "standard": "*", 41 | "tape": "5.7.5", 42 | "webtorrent-fixtures": "2.0.2" 43 | }, 44 | "engines": { 45 | "node": ">=12" 46 | }, 47 | "exports": { 48 | "import": "./index.js" 49 | }, 50 | "keywords": [ 51 | ".torrent", 52 | "bittorrent", 53 | "create", 54 | "create torrent", 55 | "make", 56 | "new", 57 | "peer-to-peer", 58 | "torrent", 59 | "torrent file", 60 | "torrent files", 61 | "webtorrent" 62 | ], 63 | "license": "MIT", 64 | "repository": { 65 | "type": "git", 66 | "url": "git://github.com/webtorrent/create-torrent.git" 67 | }, 68 | "scripts": { 69 | "test": "standard && tape test/*.js" 70 | }, 71 | "standard": { 72 | "globals": [ 73 | "Blob", 74 | "FileList" 75 | ] 76 | }, 77 | "funding": [ 78 | { 79 | "type": "github", 80 | "url": "https://github.com/sponsors/feross" 81 | }, 82 | { 83 | "type": "patreon", 84 | "url": "https://www.patreon.com/feross" 85 | }, 86 | { 87 | "type": "consulting", 88 | "url": "https://feross.org/support" 89 | } 90 | ], 91 | "renovate": { 92 | "extends": [ 93 | "github>webtorrent/renovate-config" 94 | ], 95 | "rangeStrategy": "bump" 96 | }, 97 | "release": { 98 | "extends": "@webtorrent/semantic-release-config" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/mixed.js: -------------------------------------------------------------------------------- 1 | import fixtures from 'webtorrent-fixtures' 2 | import fs from 'fs' 3 | import parseTorrent from 'parse-torrent' 4 | import path from 'path' 5 | import { hash } from 'uint8-util' 6 | import test from 'tape' 7 | import createTorrent from '../index.js' 8 | 9 | test('create multi file torrent with array of mixed types', t => { 10 | t.plan(20) 11 | 12 | const number11Path = path.join(fixtures.lotsOfNumbers.contentPath, 'big numbers', '11.txt') 13 | const number10Path = path.join(fixtures.lotsOfNumbers.contentPath, 'big numbers', '10.txt') 14 | const numbersPath = fixtures.numbers.contentPath 15 | 16 | const stream = fs.createReadStream(number10Path) 17 | stream.name = '10.txt' 18 | 19 | // Note: Order should be preserved 20 | const input = [number11Path, stream, numbersPath] 21 | 22 | const startTime = Date.now() 23 | createTorrent(input, { 24 | name: 'multi', 25 | 26 | // force piece length to 32KB so info-hash will 27 | // match what transmission generated, since we use 28 | // a different algo for picking piece length 29 | pieceLength: 32768, 30 | 31 | private: false // also force `private: false` to match transmission 32 | 33 | }, async (err, torrent) => { 34 | t.error(err) 35 | 36 | const parsedTorrent = await parseTorrent(torrent) 37 | 38 | t.equals(parsedTorrent.name, 'multi') 39 | 40 | t.notOk(parsedTorrent.private) 41 | 42 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 43 | 44 | t.ok(Array.isArray(parsedTorrent.announce)) 45 | 46 | t.deepEquals(path.normalize(parsedTorrent.files[0].path), path.normalize('multi/11.txt')) 47 | t.deepEquals(parsedTorrent.files[0].length, 2) 48 | 49 | t.deepEquals(path.normalize(parsedTorrent.files[1].path), path.normalize('multi/10.txt')) 50 | t.deepEquals(parsedTorrent.files[1].length, 2) 51 | 52 | t.deepEquals(path.normalize(parsedTorrent.files[2].path), path.normalize('multi/numbers/1.txt')) 53 | t.deepEquals(parsedTorrent.files[2].length, 1) 54 | 55 | t.deepEquals(path.normalize(parsedTorrent.files[3].path), path.normalize('multi/numbers/2.txt')) 56 | t.deepEquals(parsedTorrent.files[3].length, 2) 57 | 58 | t.deepEquals(path.normalize(parsedTorrent.files[4].path), path.normalize('multi/numbers/3.txt')) 59 | t.deepEquals(parsedTorrent.files[4].length, 3) 60 | 61 | t.equal(parsedTorrent.length, 10) 62 | t.equal(parsedTorrent.info.pieces.length, 20) 63 | t.equal(parsedTorrent.pieceLength, 32768) 64 | 65 | t.deepEquals(parsedTorrent.pieces, [ 66 | '9ad893bb9aeca601a0fab4ba85bd4a4c18b630e3' 67 | ]) 68 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 69 | t.equals(hash, 'bad3f8ea0d1d8a55c18a039dd464f1078f83dba2') 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/source.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import fixtures from 'webtorrent-fixtures' 3 | import parseTorrent from 'parse-torrent' 4 | import { hash } from 'uint8-util' 5 | import test from 'tape' 6 | 7 | import createTorrent from '../index.js' 8 | 9 | const { contentPath: folderPath } = fixtures.folder 10 | 11 | test('verify info-hash without no source set (default)', t => { 12 | t.plan(12) 13 | 14 | createTorrent(folderPath, { 15 | pieceLength: 262144, // matching mktorrent 16 | announce: 'http://private.tracker.org/', 17 | private: true 18 | }, async (err, torrent) => { 19 | t.error(err) 20 | 21 | const parsedTorrent = await parseTorrent(torrent) 22 | 23 | t.equals(parsedTorrent.name, 'folder') 24 | 25 | t.equal(parsedTorrent.info.source, undefined, 'info.source not defined') 26 | 27 | t.ok(parsedTorrent.private, 'private=true') 28 | 29 | t.deepEqual(parsedTorrent.announce, ['http://private.tracker.org/'], 'single private announce url') 30 | 31 | t.deepEquals(parsedTorrent.files[0].path, join('folder', 'file.txt'), 'check one and only file') 32 | t.deepEquals(parsedTorrent.files[0].length, 15, 'file length') 33 | 34 | t.equal(parsedTorrent.length, 15, 'total length = file length') 35 | t.equal(parsedTorrent.info.pieces.length, 20) 36 | t.equal(parsedTorrent.pieceLength, 262144) 37 | 38 | t.deepEquals(parsedTorrent.pieces, ['799c11e348d39f1704022b8354502e2f81f3c037']) 39 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 40 | t.equals(hash, 'b4dfce1f956f720c928535ded617d07696a819ef', 'mktorrent hash with no source') 41 | }) 42 | }) 43 | }) 44 | 45 | test('verify info-hash an additional source attribute set on the info dict (a way to allow private cross seeding of the same content)', t => { 46 | t.plan(12) 47 | 48 | createTorrent(folderPath, { 49 | pieceLength: 262144, // matching mktorrent 50 | announce: 'http://private.tracker.org/', 51 | private: true, 52 | info: { source: 'SOURCE' } // Set custom 'info source' attribute, this should result in a different info-hash 53 | }, async (err, torrent) => { 54 | t.error(err) 55 | 56 | const parsedTorrent = await parseTorrent(torrent) 57 | 58 | t.equals(parsedTorrent.name, 'folder') 59 | 60 | t.ok(parsedTorrent.private, 'private=true') 61 | 62 | // Source is now being read as a Buffer, 63 | // if 'parse-torrent' is updated this test will still pass 64 | t.equal(Buffer.from(parsedTorrent.info.source).toString(), 'SOURCE', 'info.source=\'SOURCE\'') 65 | 66 | t.deepEqual(parsedTorrent.announce, ['http://private.tracker.org/'], 'single private announce url') 67 | 68 | t.deepEquals(parsedTorrent.files[0].path, join('folder', 'file.txt'), 'check one and only file') 69 | t.deepEquals(parsedTorrent.files[0].length, 15, 'file length') 70 | 71 | t.equal(parsedTorrent.length, 15, 'total length = file length') 72 | t.equal(parsedTorrent.info.pieces.length, 20) 73 | t.equal(parsedTorrent.pieceLength, 262144) 74 | 75 | t.deepEquals(parsedTorrent.pieces, ['799c11e348d39f1704022b8354502e2f81f3c037']) 76 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 77 | t.equals(hash, 'a9499b56289356a3d5b8636387deb83709b8fa42', 'mktorrent run with -s SOURCE') 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/browser/basic.js: -------------------------------------------------------------------------------- 1 | /* global Blob */ 2 | 3 | import fixtures from 'webtorrent-fixtures' 4 | import fs from 'fs' 5 | import parseTorrent from 'parse-torrent' 6 | import path from 'path' 7 | import { sha1 } from 'uint8-util' 8 | import test from 'tape' 9 | import createTorrent from '../../index.js' 10 | 11 | function makeFileShim (buf, name) { 12 | const file = new Blob([buf]) 13 | file.fullPath = `/${name}` 14 | file.name = name 15 | return file 16 | } 17 | 18 | const leaves = makeFileShim(fixtures.leaves.content, 'Leaves of Grass by Walt Whitman.epub') 19 | 20 | const numbers1 = makeFileShim(fs.readFileSync(path.join(__dirname, '../../node_modules/webtorrent-fixtures/fixtures/numbers/1.txt'), 'utf8'), '1.txt') 21 | const numbers2 = makeFileShim(fs.readFileSync(path.join(__dirname, '../../node_modules/webtorrent-fixtures/fixtures/numbers/2.txt'), 'utf8'), '2.txt') 22 | const numbers3 = makeFileShim(fs.readFileSync(path.join(__dirname, '../../node_modules/webtorrent-fixtures/fixtures/numbers/3.txt'), 'utf8'), '3.txt') 23 | 24 | test('create single file torrent', t => { 25 | t.plan(11) 26 | 27 | const startTime = Date.now() 28 | createTorrent(leaves, (err, torrent) => { 29 | t.error(err) 30 | 31 | const parsedTorrent = parseTorrent(torrent) 32 | 33 | t.equals(parsedTorrent.name, 'Leaves of Grass by Walt Whitman.epub') 34 | 35 | t.notOk(parsedTorrent.private) 36 | 37 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 38 | 39 | t.ok(Array.isArray(parsedTorrent.announce)) 40 | 41 | t.equals(parsedTorrent.files[0].path, 'Leaves of Grass by Walt Whitman.epub') 42 | t.equals(parsedTorrent.files[0].length, 362017) 43 | 44 | t.equal(parsedTorrent.length, 362017) 45 | t.equal(parsedTorrent.pieceLength, 16384) 46 | 47 | t.deepEquals(parsedTorrent.pieces, [ 48 | '1f9c3f59beec079715ec53324bde8569e4a0b4eb', 49 | 'ec42307d4ce5557b5d3964c5ef55d354cf4a6ecc', 50 | '7bf1bcaf79d11fa5e0be06593c8faafc0c2ba2cf', 51 | '76d71c5b01526b23007f9e9929beafc5151e6511', 52 | '0931a1b44c21bf1e68b9138f90495e690dbc55f5', 53 | '72e4c2944cbacf26e6b3ae8a7229d88aafa05f61', 54 | 'eaae6abf3f07cb6db9677cc6aded4dd3985e4586', 55 | '27567fa7639f065f71b18954304aca6366729e0b', 56 | '4773d77ae80caa96a524804dfe4b9bd3deaef999', 57 | 'c9dd51027467519d5eb2561ae2cc01467de5f643', 58 | '0a60bcba24797692efa8770d23df0a830d91cb35', 59 | 'b3407a88baa0590dc8c9aa6a120f274367dcd867', 60 | 'e88e8338c572a06e3c801b29f519df532b3e76f6', 61 | '70cf6aee53107f3d39378483f69cf80fa568b1ea', 62 | 'c53b506159e988d8bc16922d125d77d803d652c3', 63 | 'ca3070c16eed9172ab506d20e522ea3f1ab674b3', 64 | 'f923d76fe8f44ff32e372c3b376564c6fb5f0dbe', 65 | '52164f03629fd1322636babb2c014b7dae582da4', 66 | '1363965261e6ce12b43701f0a8c9ed1520a70eba', 67 | '004400a267765f6d3dd5c7beb5bd3c75f3df2a54', 68 | '560a61801147fa4ec7cf568e703acb04e5610a4d', 69 | '56dcc242d03293e9446cf5e457d8eb3d9588fd90', 70 | 'c698de9b0dad92980906c026d8c1408fa08fe4ec' 71 | ]) 72 | 73 | window.parsedTorrent = parsedTorrent 74 | sha1(parsedTorrent.infoBuffer, hash => { 75 | t.equals(hash, 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36') 76 | }) 77 | }) 78 | }) 79 | 80 | test('create multi file torrent', t => { 81 | t.plan(16) 82 | 83 | const startTime = Date.now() 84 | createTorrent([numbers1, numbers2, numbers3], { 85 | // force piece length to 32KB so info-hash will 86 | // match what transmission generated, since we use 87 | // a different algo for picking piece length 88 | pieceLength: 32768, 89 | 90 | private: false, // also force `private: false` to match transmission 91 | name: 'numbers' 92 | 93 | }, (err, torrent) => { 94 | t.error(err) 95 | 96 | const parsedTorrent = parseTorrent(torrent) 97 | 98 | t.equals(parsedTorrent.name, 'numbers') 99 | 100 | t.notOk(parsedTorrent.private) 101 | 102 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 103 | 104 | t.ok(Array.isArray(parsedTorrent.announce)) 105 | 106 | t.deepEquals(parsedTorrent.files[0].path, 'numbers/1.txt') 107 | t.deepEquals(parsedTorrent.files[0].length, 1) 108 | 109 | t.deepEquals(parsedTorrent.files[1].path, 'numbers/2.txt') 110 | t.deepEquals(parsedTorrent.files[1].length, 2) 111 | 112 | t.deepEquals(parsedTorrent.files[2].path, 'numbers/3.txt') 113 | t.deepEquals(parsedTorrent.files[2].length, 3) 114 | 115 | t.equal(parsedTorrent.length, 6) 116 | t.equal(parsedTorrent.info.pieces.length, 20) 117 | t.equal(parsedTorrent.pieceLength, 32768) 118 | 119 | t.deepEquals(parsedTorrent.pieces, [ 120 | '1f74648e50a6a6708ec54ab327a163d5536b7ced' 121 | ]) 122 | sha1(parsedTorrent.infoBuffer, hash => { 123 | t.equals(hash, '80562f38656b385ea78959010e51a2cc9db41ea0') 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /test/stream.js: -------------------------------------------------------------------------------- 1 | import fixtures from 'webtorrent-fixtures' 2 | import fs from 'fs' 3 | import parseTorrent from 'parse-torrent' 4 | import path from 'path' 5 | import { hash } from 'uint8-util' 6 | import test from 'tape' 7 | import createTorrent from '../index.js' 8 | 9 | test('create single file torrent from a stream', t => { 10 | t.plan(11) 11 | 12 | const stream = fs.createReadStream(fixtures.leaves.contentPath) 13 | stream.name = 'Leaves of Grass by Walt Whitman.epub' 14 | 15 | const startTime = Date.now() 16 | createTorrent(stream, { pieceLength: 16384 }, async (err, torrent) => { 17 | t.error(err) 18 | 19 | const parsedTorrent = await parseTorrent(torrent) 20 | 21 | t.equals(parsedTorrent.name, 'Leaves of Grass by Walt Whitman.epub') 22 | 23 | t.notOk(parsedTorrent.private) 24 | 25 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 26 | 27 | t.ok(Array.isArray(parsedTorrent.announce)) 28 | 29 | t.equals(path.normalize(parsedTorrent.files[0].path), path.normalize('Leaves of Grass by Walt Whitman.epub')) 30 | t.equals(parsedTorrent.files[0].length, 362017) 31 | 32 | t.equal(parsedTorrent.length, 362017) 33 | t.equal(parsedTorrent.pieceLength, 16384) 34 | 35 | t.deepEquals(parsedTorrent.pieces, [ 36 | '1f9c3f59beec079715ec53324bde8569e4a0b4eb', 37 | 'ec42307d4ce5557b5d3964c5ef55d354cf4a6ecc', 38 | '7bf1bcaf79d11fa5e0be06593c8faafc0c2ba2cf', 39 | '76d71c5b01526b23007f9e9929beafc5151e6511', 40 | '0931a1b44c21bf1e68b9138f90495e690dbc55f5', 41 | '72e4c2944cbacf26e6b3ae8a7229d88aafa05f61', 42 | 'eaae6abf3f07cb6db9677cc6aded4dd3985e4586', 43 | '27567fa7639f065f71b18954304aca6366729e0b', 44 | '4773d77ae80caa96a524804dfe4b9bd3deaef999', 45 | 'c9dd51027467519d5eb2561ae2cc01467de5f643', 46 | '0a60bcba24797692efa8770d23df0a830d91cb35', 47 | 'b3407a88baa0590dc8c9aa6a120f274367dcd867', 48 | 'e88e8338c572a06e3c801b29f519df532b3e76f6', 49 | '70cf6aee53107f3d39378483f69cf80fa568b1ea', 50 | 'c53b506159e988d8bc16922d125d77d803d652c3', 51 | 'ca3070c16eed9172ab506d20e522ea3f1ab674b3', 52 | 'f923d76fe8f44ff32e372c3b376564c6fb5f0dbe', 53 | '52164f03629fd1322636babb2c014b7dae582da4', 54 | '1363965261e6ce12b43701f0a8c9ed1520a70eba', 55 | '004400a267765f6d3dd5c7beb5bd3c75f3df2a54', 56 | '560a61801147fa4ec7cf568e703acb04e5610a4d', 57 | '56dcc242d03293e9446cf5e457d8eb3d9588fd90', 58 | 'c698de9b0dad92980906c026d8c1408fa08fe4ec' 59 | ]) 60 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 61 | t.equals(hash, 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36') 62 | }) 63 | }) 64 | }) 65 | 66 | test('create multi file torrent with streams', t => { 67 | t.plan(16) 68 | 69 | const files = fs.readdirSync(fixtures.numbers.contentPath).map(file => { 70 | const stream = fs.createReadStream(`${fixtures.numbers.contentPath}/${file}`) 71 | stream.name = file 72 | return stream 73 | }) 74 | 75 | const startTime = Date.now() 76 | createTorrent(files, { 77 | // force piece length to 32KB so info-hash will 78 | // match what transmission generated, since we use 79 | // a different algo for picking piece length 80 | pieceLength: 32768, 81 | 82 | private: false, // also force `private: false` to match transmission 83 | 84 | name: 'numbers' 85 | 86 | }, async (err, torrent) => { 87 | t.error(err) 88 | 89 | const parsedTorrent = await parseTorrent(torrent) 90 | 91 | t.equals(parsedTorrent.name, 'numbers') 92 | 93 | t.notOk(parsedTorrent.private) 94 | 95 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 96 | 97 | t.ok(Array.isArray(parsedTorrent.announce)) 98 | 99 | t.deepEquals(path.normalize(parsedTorrent.files[0].path), path.normalize('numbers/1.txt')) 100 | t.deepEquals(parsedTorrent.files[0].length, 1) 101 | 102 | t.deepEquals(path.normalize(parsedTorrent.files[1].path), path.normalize('numbers/2.txt')) 103 | t.deepEquals(parsedTorrent.files[1].length, 2) 104 | 105 | t.deepEquals(path.normalize(parsedTorrent.files[2].path), path.normalize('numbers/3.txt')) 106 | t.deepEquals(parsedTorrent.files[2].length, 3) 107 | 108 | t.equal(parsedTorrent.length, 6) 109 | t.equal(parsedTorrent.info.pieces.length, 20) 110 | t.equal(parsedTorrent.pieceLength, 32768) 111 | 112 | t.deepEquals(parsedTorrent.pieces, [ 113 | '1f74648e50a6a6708ec54ab327a163d5536b7ced' 114 | ]) 115 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 116 | t.equals(hash, '80562f38656b385ea78959010e51a2cc9db41ea0') 117 | }) 118 | }) 119 | }) 120 | 121 | test('implicit name and pieceLength for stream', t => { 122 | t.plan(6) 123 | 124 | const stream = fs.createReadStream(fixtures.leaves.contentPath) 125 | 126 | createTorrent(stream, async (err, torrent) => { 127 | t.error(err) 128 | const parsedTorrent = await parseTorrent(torrent) 129 | 130 | t.ok(parsedTorrent.name.includes('Unnamed Torrent')) 131 | t.equal(parsedTorrent.pieceLength, 16384) 132 | 133 | t.equal(parsedTorrent.files.length, 1) 134 | t.ok(parsedTorrent.files[0].name.includes('Unnamed Torrent')) 135 | t.ok(parsedTorrent.files[0].path.includes('Unnamed Torrent')) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-torrent [![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [ci-image]: https://github.com/webtorrent/create-torrent/actions/workflows/ci.yml/badge.svg 4 | [ci-url]: https://github.com/webtorrent/create-torrent/actions/workflows/ci.yml 5 | [npm-image]: https://img.shields.io/npm/v/create-torrent.svg 6 | [npm-url]: https://npmjs.org/package/create-torrent 7 | [downloads-image]: https://img.shields.io/npm/dm/create-torrent.svg 8 | [downloads-url]: https://npmjs.org/package/create-torrent 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | #### Create .torrent files 13 | 14 | ![creation](https://raw.githubusercontent.com/webtorrent/create-torrent/master/img.jpg) 15 | 16 | This module is used by [WebTorrent](http://webtorrent.io)! This module works in node.js and the browser (with [browserify](http://browserify.org/)). 17 | 18 | ### install 19 | 20 | ``` 21 | npm install create-torrent 22 | ``` 23 | 24 | ### usage 25 | 26 | The simplest way to use `create-torrent` is like this: 27 | 28 | ```js 29 | import createTorrent from 'create-torrent' 30 | import fs from 'fs' 31 | 32 | createTorrent('/path/to/folder', (err, torrent) => { 33 | if (!err) { 34 | // `torrent` is a Buffer with the contents of the new .torrent file 35 | fs.writeFile('my.torrent', torrent) 36 | } 37 | }) 38 | ``` 39 | 40 | A reasonable piece length (approx. 1024 pieces) will automatically be selected 41 | for the .torrent file, or you can override it if you want a different size (See 42 | API docs below). 43 | 44 | ### api 45 | 46 | #### `createTorrent(input, [opts], function callback (err, torrent) {})` 47 | 48 | Create a new `.torrent` file. 49 | 50 | `input` can be any of the following: 51 | 52 | - path to the file or folder on filesystem (string) 53 | - W3C [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object (from an `` or drag and drop) 54 | - W3C [FileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList) object (basically an array of `File` objects) 55 | - Node [Buffer](http://nodejs.org/api/buffer.html) object 56 | - Node [stream.Readable](http://nodejs.org/api/stream.html) object 57 | 58 | Or, an **array of `string`, `File`, `Buffer`, or `stream.Readable` objects**. 59 | 60 | `opts` is optional and allows you to set special settings on the produced .torrent file. 61 | 62 | ``` js 63 | { 64 | name: String, // name of the torrent (default = basename of `path`, or 1st file's name) 65 | comment: String, // free-form textual comments of the author 66 | createdBy: String, // name and version of program used to create torrent 67 | creationDate: Date // creation time in UNIX epoch format (default = now) 68 | filterJunkFiles: Boolean, // remove hidden and other junk files? (default = true) 69 | private: Boolean, // is this a private .torrent? (default = false) 70 | pieceLength: Number, // force a custom piece length (number of bytes) 71 | announceList: [[String]], // custom trackers (array of arrays of strings) (see [bep12](http://www.bittorrent.org/beps/bep_0012.html)) 72 | urlList: [String], // web seed urls (see [bep19](http://www.bittorrent.org/beps/bep_0019.html)) 73 | info: Object, // add non-standard info dict entries, e.g. info.source, a convention for cross-seeding 74 | onProgress: Function // called with the number of bytes hashed and estimated total size after every piece 75 | } 76 | ``` 77 | 78 | If `announceList` is omitted, the following trackers will be included automatically: 79 | 80 | - udp://tracker.leechers-paradise.org:6969 81 | - udp://tracker.coppersurfer.tk:6969 82 | - udp://tracker.opentrackr.org:1337 83 | - udp://explodie.org:6969 84 | - udp://tracker.empire-js.us:1337 85 | - wss://tracker.btorrent.xyz 86 | - wss://tracker.openwebtorrent.com 87 | - wss://tracker.webtorrent.dev 88 | 89 | Trackers that start with `wss://` are for WebRTC peers. See 90 | [WebTorrent](https://webtorrent.io) to learn more. 91 | 92 | `callback` is called with an error and a Buffer of the torrent data. It is up to you to 93 | save it to a file if that's what you want to do. 94 | 95 | **Note:** Every torrent is required to have a name. If one is not explicitly provided 96 | through `opts.name`, one will be determined automatically using the following logic: 97 | 98 | - If all files share a common path prefix, that will be used. For example, if all file 99 | paths start with `/imgs/` the torrent name will be `imgs`. 100 | - Otherwise, the first file that has a name will determine the torrent name. For example, 101 | if the first file is `/foo/bar/baz.txt`, the torrent name will be `baz.txt`. 102 | - If no files have names (say that all files are Buffer or Stream objects), then a name 103 | like "Unnamed Torrent " will be generated. 104 | 105 | **Note:** Every file is required to have a name. For filesystem paths or W3C File objects, 106 | the name is included in the object. For Buffer or Readable stream types, a `name` property 107 | can be set on the object, like this: 108 | 109 | ```js 110 | const buf = Buffer.from('Some file content') 111 | buf.name = 'Some file name' 112 | ``` 113 | 114 | ### command line 115 | 116 | ``` 117 | usage: create-torrent {-o outfile.torrent} 118 | 119 | Create a torrent file from a directory or file. 120 | 121 | If an output file isn\'t specified with `-o`, the torrent file will be 122 | written to stdout. 123 | ``` 124 | 125 | ### license 126 | 127 | MIT. Copyright (c) [Feross Aboukhadijeh](https://feross.org) and [WebTorrent, LLC](https://webtorrent.io). 128 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | import parseTorrent from 'parse-torrent' 2 | import path from 'path' 3 | import test from 'tape' 4 | import createTorrent from '../index.js' 5 | 6 | test('implicit torrent name and file name', t => { 7 | t.plan(5) 8 | 9 | const buf1 = Buffer.from('buf1') 10 | 11 | createTorrent(buf1, async (err, torrent) => { 12 | t.error(err) 13 | const parsedTorrent = await parseTorrent(torrent) 14 | 15 | t.ok(parsedTorrent.name.includes('Unnamed Torrent')) 16 | 17 | t.equal(parsedTorrent.files.length, 1) 18 | t.ok(parsedTorrent.files[0].name.includes('Unnamed Torrent')) 19 | t.ok(parsedTorrent.files[0].path.includes('Unnamed Torrent')) 20 | }) 21 | }) 22 | 23 | test('implicit file name from torrent name', t => { 24 | t.plan(5) 25 | 26 | const buf1 = Buffer.from('buf1') 27 | 28 | createTorrent(buf1, { name: 'My Cool File' }, async (err, torrent) => { 29 | t.error(err) 30 | const parsedTorrent = await parseTorrent(torrent) 31 | 32 | t.equal(parsedTorrent.name, 'My Cool File') 33 | 34 | t.equal(parsedTorrent.files.length, 1) 35 | t.equal(parsedTorrent.files[0].name, 'My Cool File') 36 | t.equal(parsedTorrent.files[0].path, 'My Cool File') 37 | }) 38 | }) 39 | 40 | test('implicit torrent name from file name', t => { 41 | t.plan(5) 42 | 43 | const buf1 = Buffer.from('buf1') 44 | buf1.name = 'My Cool File' 45 | 46 | createTorrent(buf1, async (err, torrent) => { 47 | t.error(err) 48 | const parsedTorrent = await parseTorrent(torrent) 49 | 50 | t.equal(parsedTorrent.name, 'My Cool File') 51 | 52 | t.equal(parsedTorrent.files.length, 1) 53 | t.equal(parsedTorrent.files[0].name, 'My Cool File') 54 | t.equal(parsedTorrent.files[0].path, 'My Cool File') 55 | }) 56 | }) 57 | 58 | test('implicit file names from torrent name', t => { 59 | t.plan(7) 60 | 61 | const buf1 = Buffer.from('buf1') 62 | const buf2 = Buffer.from('buf2') 63 | 64 | createTorrent([buf1, buf2], { name: 'My Cool File' }, async (err, torrent) => { 65 | t.error(err) 66 | const parsedTorrent = await parseTorrent(torrent) 67 | 68 | t.equal(parsedTorrent.name, 'My Cool File') 69 | 70 | t.equal(parsedTorrent.files.length, 2) 71 | 72 | t.ok(parsedTorrent.files[0].name.includes('Unknown File')) 73 | t.ok(parsedTorrent.files[0].path.includes('Unknown File')) 74 | 75 | t.ok(parsedTorrent.files[1].name.includes('Unknown File')) 76 | t.ok(parsedTorrent.files[1].path.includes('Unknown File')) 77 | }) 78 | }) 79 | 80 | test('set file name with `name` property', t => { 81 | t.plan(5) 82 | 83 | const buf1 = Buffer.from('buf1') 84 | buf1.name = 'My Cool File' 85 | 86 | createTorrent(buf1, async (err, torrent) => { 87 | t.error(err) 88 | const parsedTorrent = await parseTorrent(torrent) 89 | 90 | t.equal(parsedTorrent.name, 'My Cool File') 91 | 92 | t.equal(parsedTorrent.files.length, 1) 93 | t.equal(parsedTorrent.files[0].name, 'My Cool File') 94 | t.equal(parsedTorrent.files[0].path, 'My Cool File') 95 | }) 96 | }) 97 | 98 | test('set file names with `name` property', t => { 99 | t.plan(7) 100 | 101 | const buf1 = Buffer.from('buf1') 102 | buf1.name = 'My Cool File 1' 103 | 104 | const buf2 = Buffer.from('buf2') 105 | buf2.name = 'My Cool File 2' 106 | 107 | createTorrent([buf1, buf2], { name: 'My Cool Torrent' }, async (err, torrent) => { 108 | t.error(err) 109 | const parsedTorrent = await parseTorrent(torrent) 110 | 111 | t.equal(parsedTorrent.name, 'My Cool Torrent') 112 | 113 | t.equal(parsedTorrent.files.length, 2) 114 | 115 | t.equal(parsedTorrent.files[0].name, 'My Cool File 1') 116 | t.equal(parsedTorrent.files[0].path, path.join('My Cool Torrent', 'My Cool File 1')) 117 | 118 | t.equal(parsedTorrent.files[1].name, 'My Cool File 2') 119 | t.equal(parsedTorrent.files[1].path, path.join('My Cool Torrent', 'My Cool File 2')) 120 | }) 121 | }) 122 | 123 | test('set file name with `fullPath` property', t => { 124 | t.plan(5) 125 | 126 | const buf1 = Buffer.from('buf1') 127 | buf1.fullPath = 'My Cool File' 128 | 129 | createTorrent(buf1, async (err, torrent) => { 130 | t.error(err) 131 | const parsedTorrent = await parseTorrent(torrent) 132 | 133 | t.equal(parsedTorrent.name, 'My Cool File') 134 | 135 | t.equal(parsedTorrent.files.length, 1) 136 | t.equal(parsedTorrent.files[0].name, 'My Cool File') 137 | t.equal(parsedTorrent.files[0].path, 'My Cool File') 138 | }) 139 | }) 140 | 141 | test('set file names with `fullPath` property', t => { 142 | t.plan(7) 143 | 144 | const buf1 = Buffer.from('buf1') 145 | buf1.fullPath = 'My Cool File 1' 146 | 147 | const buf2 = Buffer.from('buf2') 148 | buf2.fullPath = 'My Cool File 2' 149 | 150 | createTorrent([buf1, buf2], { name: 'My Cool Torrent' }, async (err, torrent) => { 151 | t.error(err) 152 | const parsedTorrent = await parseTorrent(torrent) 153 | 154 | t.equal(parsedTorrent.name, 'My Cool Torrent') 155 | 156 | t.equal(parsedTorrent.files.length, 2) 157 | 158 | t.equal(parsedTorrent.files[0].name, 'My Cool File 1') 159 | t.equal(parsedTorrent.files[0].path, path.join('My Cool Torrent', 'My Cool File 1')) 160 | 161 | t.equal(parsedTorrent.files[1].name, 'My Cool File 2') 162 | t.equal(parsedTorrent.files[1].path, path.join('My Cool Torrent', 'My Cool File 2')) 163 | }) 164 | }) 165 | 166 | test('implicit torrent name from file name with slashes in it', t => { 167 | t.plan(5) 168 | 169 | const buf1 = Buffer.from('buf1') 170 | buf1.name = 'My Cool Folder/My Cool File' 171 | 172 | createTorrent(buf1, async (err, torrent) => { 173 | t.error(err) 174 | const parsedTorrent = await parseTorrent(torrent) 175 | 176 | t.equal(parsedTorrent.name, 'My Cool File') 177 | 178 | t.equal(parsedTorrent.files.length, 1) 179 | t.equal(parsedTorrent.files[0].name, 'My Cool File') 180 | t.equal(parsedTorrent.files[0].path, 'My Cool File') 181 | }) 182 | }) 183 | 184 | test('implicit torrent name from file names with slashes in them', t => { 185 | t.plan(7) 186 | 187 | const buf1 = Buffer.from('buf1') 188 | buf1.name = 'My Cool Folder/My Cool File 1' 189 | 190 | const buf2 = Buffer.from('buf2') 191 | buf2.name = 'My Cool Folder/My Cool File 2' 192 | 193 | createTorrent([buf1, buf2], async (err, torrent) => { 194 | t.error(err) 195 | const parsedTorrent = await parseTorrent(torrent) 196 | 197 | t.equal(parsedTorrent.name, 'My Cool Folder') 198 | 199 | t.equal(parsedTorrent.files.length, 2) 200 | 201 | t.equal(parsedTorrent.files[0].name, 'My Cool File 1') 202 | t.equal(parsedTorrent.files[0].path, path.join('My Cool Folder', 'My Cool File 1')) 203 | 204 | t.equal(parsedTorrent.files[1].name, 'My Cool File 2') 205 | t.equal(parsedTorrent.files[1].path, path.join('My Cool Folder', 'My Cool File 2')) 206 | }) 207 | }) 208 | -------------------------------------------------------------------------------- /test/fs.js: -------------------------------------------------------------------------------- 1 | import fixtures from 'webtorrent-fixtures' 2 | import parseTorrent from 'parse-torrent' 3 | import path from 'path' 4 | import { hash } from 'uint8-util' 5 | import test from 'tape' 6 | import createTorrent from '../index.js' 7 | 8 | test('create single file torrent', t => { 9 | t.plan(11) 10 | 11 | const startTime = Date.now() 12 | createTorrent(fixtures.leaves.contentPath, async (err, torrent) => { 13 | t.error(err) 14 | 15 | const parsedTorrent = await parseTorrent(torrent) 16 | 17 | t.equals(parsedTorrent.name, 'Leaves of Grass by Walt Whitman.epub') 18 | 19 | t.notOk(parsedTorrent.private) 20 | 21 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 22 | 23 | t.ok(Array.isArray(parsedTorrent.announce)) 24 | 25 | t.equals(path.normalize(parsedTorrent.files[0].path), path.normalize('Leaves of Grass by Walt Whitman.epub')) 26 | t.equals(parsedTorrent.files[0].length, 362017) 27 | 28 | t.equal(parsedTorrent.length, 362017) 29 | t.equal(parsedTorrent.pieceLength, 16384) 30 | 31 | t.deepEquals(parsedTorrent.pieces, [ 32 | '1f9c3f59beec079715ec53324bde8569e4a0b4eb', 33 | 'ec42307d4ce5557b5d3964c5ef55d354cf4a6ecc', 34 | '7bf1bcaf79d11fa5e0be06593c8faafc0c2ba2cf', 35 | '76d71c5b01526b23007f9e9929beafc5151e6511', 36 | '0931a1b44c21bf1e68b9138f90495e690dbc55f5', 37 | '72e4c2944cbacf26e6b3ae8a7229d88aafa05f61', 38 | 'eaae6abf3f07cb6db9677cc6aded4dd3985e4586', 39 | '27567fa7639f065f71b18954304aca6366729e0b', 40 | '4773d77ae80caa96a524804dfe4b9bd3deaef999', 41 | 'c9dd51027467519d5eb2561ae2cc01467de5f643', 42 | '0a60bcba24797692efa8770d23df0a830d91cb35', 43 | 'b3407a88baa0590dc8c9aa6a120f274367dcd867', 44 | 'e88e8338c572a06e3c801b29f519df532b3e76f6', 45 | '70cf6aee53107f3d39378483f69cf80fa568b1ea', 46 | 'c53b506159e988d8bc16922d125d77d803d652c3', 47 | 'ca3070c16eed9172ab506d20e522ea3f1ab674b3', 48 | 'f923d76fe8f44ff32e372c3b376564c6fb5f0dbe', 49 | '52164f03629fd1322636babb2c014b7dae582da4', 50 | '1363965261e6ce12b43701f0a8c9ed1520a70eba', 51 | '004400a267765f6d3dd5c7beb5bd3c75f3df2a54', 52 | '560a61801147fa4ec7cf568e703acb04e5610a4d', 53 | '56dcc242d03293e9446cf5e457d8eb3d9588fd90', 54 | 'c698de9b0dad92980906c026d8c1408fa08fe4ec' 55 | ]) 56 | 57 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 58 | t.equals(hash, 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36') 59 | }) 60 | }) 61 | }) 62 | 63 | test('create single file torrent from buffer', t => { 64 | t.plan(1) 65 | 66 | createTorrent(Buffer.from('blah'), { name: 'blah.txt' }, async (err, torrent) => { 67 | t.error(err) 68 | try { 69 | await parseTorrent(torrent) 70 | } catch (err) { 71 | t.fail(`failed to parse created torrent: ${err.message}`) 72 | } 73 | }) 74 | }) 75 | 76 | test('create multi file torrent', t => { 77 | t.plan(16) 78 | 79 | const startTime = Date.now() 80 | createTorrent(fixtures.numbers.contentPath, { 81 | // force piece length to 32KB so info-hash will 82 | // match what transmission generated, since we use 83 | // a different algo for picking piece length 84 | pieceLength: 32768, 85 | 86 | private: false // also force `private: false` to match transmission 87 | 88 | }, async (err, torrent) => { 89 | t.error(err) 90 | 91 | const parsedTorrent = await parseTorrent(torrent) 92 | 93 | t.equals(parsedTorrent.name, 'numbers') 94 | 95 | t.notOk(parsedTorrent.private) 96 | 97 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 98 | 99 | t.ok(Array.isArray(parsedTorrent.announce)) 100 | 101 | t.deepEquals(path.normalize(parsedTorrent.files[0].path), path.normalize('numbers/1.txt')) 102 | t.deepEquals(parsedTorrent.files[0].length, 1) 103 | 104 | t.deepEquals(path.normalize(parsedTorrent.files[1].path), path.normalize('numbers/2.txt')) 105 | t.deepEquals(parsedTorrent.files[1].length, 2) 106 | 107 | t.deepEquals(path.normalize(parsedTorrent.files[2].path), path.normalize('numbers/3.txt')) 108 | t.deepEquals(parsedTorrent.files[2].length, 3) 109 | 110 | t.equal(parsedTorrent.length, 6) 111 | t.equal(parsedTorrent.info.pieces.length, 20) 112 | t.equal(parsedTorrent.pieceLength, 32768) 113 | 114 | t.deepEquals(parsedTorrent.pieces, [ 115 | '1f74648e50a6a6708ec54ab327a163d5536b7ced' 116 | ]) 117 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 118 | t.equals(hash, '80562f38656b385ea78959010e51a2cc9db41ea0') 119 | }) 120 | }) 121 | }) 122 | 123 | test('create multi file torrent with nested directories', t => { 124 | t.plan(21) 125 | 126 | const startTime = Date.now() 127 | createTorrent(fixtures.lotsOfNumbers.contentPath, { 128 | // force piece length to 32KB so info-hash will 129 | // match what transmission generated, since we use 130 | // a different algo for picking piece length 131 | pieceLength: 32768, 132 | 133 | private: false // also force `private: false` to match transmission 134 | 135 | }, async (err, torrent) => { 136 | t.error(err) 137 | 138 | const parsedTorrent = await parseTorrent(torrent) 139 | 140 | t.equals(parsedTorrent.name, 'lots-of-numbers') 141 | 142 | t.notOk(parsedTorrent.private) 143 | 144 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 145 | 146 | t.ok(Array.isArray(parsedTorrent.announce)) 147 | 148 | t.deepEquals(path.normalize(parsedTorrent.files[0].path), path.normalize('lots-of-numbers/big numbers/10.txt')) 149 | t.deepEquals(parsedTorrent.files[0].length, 2) 150 | 151 | t.deepEquals(path.normalize(parsedTorrent.files[1].path), path.normalize('lots-of-numbers/big numbers/11.txt')) 152 | t.deepEquals(parsedTorrent.files[1].length, 2) 153 | 154 | t.deepEquals(path.normalize(parsedTorrent.files[2].path), path.normalize('lots-of-numbers/big numbers/12.txt')) 155 | t.deepEquals(parsedTorrent.files[2].length, 2) 156 | 157 | t.deepEquals(path.normalize(parsedTorrent.files[3].path), path.normalize('lots-of-numbers/small numbers/1.txt')) 158 | t.deepEquals(parsedTorrent.files[3].length, 1) 159 | 160 | t.deepEquals(path.normalize(parsedTorrent.files[4].path), path.normalize('lots-of-numbers/small numbers/2.txt')) 161 | t.deepEquals(parsedTorrent.files[4].length, 2) 162 | 163 | t.deepEquals(path.normalize(parsedTorrent.files[5].path), path.normalize('lots-of-numbers/small numbers/3.txt')) 164 | t.deepEquals(parsedTorrent.files[5].length, 3) 165 | 166 | t.equal(parsedTorrent.length, 12) 167 | t.equal(parsedTorrent.pieceLength, 32768) 168 | 169 | t.deepEquals(parsedTorrent.pieces, [ 170 | '47972f2befaee58b6f3860cd39bd56cb33a488f0' 171 | ]) 172 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 173 | t.equals(hash, '427887e9c03e123f9c8458b1947090edf1c75baa') 174 | }) 175 | }) 176 | }) 177 | 178 | test('create multi file torrent with array of paths', t => { 179 | t.plan(20) 180 | 181 | const number10Path = path.join(fixtures.lotsOfNumbers.contentPath, 'big numbers', '10.txt') 182 | const number11Path = path.join(fixtures.lotsOfNumbers.contentPath, 'big numbers', '11.txt') 183 | const numbersPath = fixtures.numbers.contentPath 184 | 185 | const input = [number10Path, number11Path, numbersPath] 186 | 187 | const startTime = Date.now() 188 | createTorrent(input, { 189 | name: 'multi', 190 | 191 | // force piece length to 32KB so info-hash will 192 | // match what transmission generated, since we use 193 | // a different algo for picking piece length 194 | pieceLength: 32768, 195 | 196 | private: false // also force `private: false` to match transmission 197 | 198 | }, async (err, torrent) => { 199 | t.error(err) 200 | 201 | const parsedTorrent = await parseTorrent(torrent) 202 | 203 | t.equals(parsedTorrent.name, 'multi') 204 | 205 | t.notOk(parsedTorrent.private) 206 | 207 | t.ok(parsedTorrent.created.getTime() >= startTime, 'created time is after start time') 208 | 209 | t.ok(Array.isArray(parsedTorrent.announce)) 210 | 211 | t.deepEquals(path.normalize(parsedTorrent.files[0].path), path.normalize('multi/10.txt')) 212 | t.deepEquals(parsedTorrent.files[0].length, 2) 213 | 214 | t.deepEquals(path.normalize(parsedTorrent.files[1].path), path.normalize('multi/11.txt')) 215 | t.deepEquals(parsedTorrent.files[1].length, 2) 216 | 217 | t.deepEquals(path.normalize(parsedTorrent.files[2].path), path.normalize('multi/numbers/1.txt')) 218 | t.deepEquals(parsedTorrent.files[2].length, 1) 219 | 220 | t.deepEquals(path.normalize(parsedTorrent.files[3].path), path.normalize('multi/numbers/2.txt')) 221 | t.deepEquals(parsedTorrent.files[3].length, 2) 222 | 223 | t.deepEquals(path.normalize(parsedTorrent.files[4].path), path.normalize('multi/numbers/3.txt')) 224 | t.deepEquals(parsedTorrent.files[4].length, 3) 225 | 226 | t.equal(parsedTorrent.length, 10) 227 | t.equal(parsedTorrent.info.pieces.length, 20) 228 | t.equal(parsedTorrent.pieceLength, 32768) 229 | 230 | t.deepEquals(parsedTorrent.pieces, [ 231 | '1c4e1ba6da4d771ff82025d7cf76e8c368c6c3dd' 232 | ]) 233 | hash(parsedTorrent.infoBuffer, 'hex').then(hash => { 234 | t.equals(hash, 'df25a2959378892f6ddaf4a2d7e75174e09878bb') 235 | }) 236 | }) 237 | }) 238 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [6.0.17](https://github.com/webtorrent/create-torrent/compare/v6.0.16...v6.0.17) (2024-02-09) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update dependency uint8-util to ^2.2.4 ([#257](https://github.com/webtorrent/create-torrent/issues/257)) ([87c3a33](https://github.com/webtorrent/create-torrent/commit/87c3a337d7d31e51c99c27d3d1bcf22558f24f82)) 7 | 8 | ## [6.0.16](https://github.com/webtorrent/create-torrent/compare/v6.0.15...v6.0.16) (2023-12-12) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency fast-readable-async-iterator to v2 ([#251](https://github.com/webtorrent/create-torrent/issues/251)) ([a566cfb](https://github.com/webtorrent/create-torrent/commit/a566cfb8cf3222d5204ec033c9dbf581aa0e96e4)) 14 | 15 | ## [6.0.15](https://github.com/webtorrent/create-torrent/compare/v6.0.14...v6.0.15) (2023-08-11) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **deps:** update dependency uint8-util to ^2.2.2 ([#233](https://github.com/webtorrent/create-torrent/issues/233)) ([1c15ac5](https://github.com/webtorrent/create-torrent/commit/1c15ac5da79ba368616259c54ee27e82419b7b61)) 21 | 22 | ## [6.0.14](https://github.com/webtorrent/create-torrent/compare/v6.0.13...v6.0.14) (2023-08-10) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update dependency bencode to v4 ([#236](https://github.com/webtorrent/create-torrent/issues/236)) ([edef6b9](https://github.com/webtorrent/create-torrent/commit/edef6b9c39afbb56dc5f7905af59514d70e7087c)) 28 | 29 | ## [6.0.13](https://github.com/webtorrent/create-torrent/compare/v6.0.12...v6.0.13) (2023-07-31) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **deps:** update dependency bencode to ^3.1.1 ([b7b1bab](https://github.com/webtorrent/create-torrent/commit/b7b1bab47b0b550a5c320ac2f2374bb85f215aee)) 35 | 36 | ## [6.0.12](https://github.com/webtorrent/create-torrent/compare/v6.0.11...v6.0.12) (2023-07-23) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **deps:** update dependency junk to v4 ([#231](https://github.com/webtorrent/create-torrent/issues/231)) ([6a89a3a](https://github.com/webtorrent/create-torrent/commit/6a89a3ad68374c865772dbc22ea4b8c5557ba94e)) 42 | 43 | ## [6.0.11](https://github.com/webtorrent/create-torrent/compare/v6.0.10...v6.0.11) (2023-04-02) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * **deps:** update dependency minimist to ^1.2.8 ([#212](https://github.com/webtorrent/create-torrent/issues/212)) ([7721d1e](https://github.com/webtorrent/create-torrent/commit/7721d1e466818cfed75b4d9e155a71fa9f33bb11)) 49 | * **deps:** update dependency uint8-util to ^2.1.9 ([#211](https://github.com/webtorrent/create-torrent/issues/211)) ([9bf6749](https://github.com/webtorrent/create-torrent/commit/9bf674978b01000287cd5206bf633b2700461bae)) 50 | 51 | ## [6.0.10](https://github.com/webtorrent/create-torrent/compare/v6.0.9...v6.0.10) (2023-02-22) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * add once dependency ([#214](https://github.com/webtorrent/create-torrent/issues/214)) ([75822e9](https://github.com/webtorrent/create-torrent/commit/75822e9a94a0977acca4699b4ff71545d959e26f)) 57 | 58 | ## [6.0.9](https://github.com/webtorrent/create-torrent/compare/v6.0.8...v6.0.9) (2023-01-31) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * **deps:** update webtorrent ([#208](https://github.com/webtorrent/create-torrent/issues/208)) ([4cf1295](https://github.com/webtorrent/create-torrent/commit/4cf1295e0465234a0117a4a3bc78b75c3f36c5ec)), closes [#209](https://github.com/webtorrent/create-torrent/issues/209) 64 | 65 | ## [6.0.8](https://github.com/webtorrent/create-torrent/compare/v6.0.7...v6.0.8) (2023-01-31) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * **deps:** update dependency uint8-util to ^2.1.7 ([#205](https://github.com/webtorrent/create-torrent/issues/205)) ([29996f4](https://github.com/webtorrent/create-torrent/commit/29996f4acb10a92f0c91d5fefdf873c9f1aa3bc3)) 71 | 72 | ## [6.0.7](https://github.com/webtorrent/create-torrent/compare/v6.0.6...v6.0.7) (2023-01-31) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * **deps:** update webtorrent ([db3e86f](https://github.com/webtorrent/create-torrent/commit/db3e86f29fe6cbc2ce18c3d7e14e974c35a2826c)) 78 | 79 | ## [6.0.6](https://github.com/webtorrent/create-torrent/compare/v6.0.5...v6.0.6) (2023-01-26) 80 | 81 | 82 | ### Performance Improvements 83 | 84 | * drop buffer ([#201](https://github.com/webtorrent/create-torrent/issues/201)) ([2e030c7](https://github.com/webtorrent/create-torrent/commit/2e030c7358d17fd679d427f39b55152a2c7f826f)) 85 | 86 | ## [6.0.5](https://github.com/webtorrent/create-torrent/compare/v6.0.4...v6.0.5) (2023-01-26) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * **deps:** update dependency uint8-util to ^2.1.5 ([#198](https://github.com/webtorrent/create-torrent/issues/198)) ([e710d84](https://github.com/webtorrent/create-torrent/commit/e710d84a1fc887ff50e4be949f0ca528c7bf01c2)) 92 | 93 | ## [6.0.4](https://github.com/webtorrent/create-torrent/compare/v6.0.3...v6.0.4) (2023-01-25) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * **deps:** update dependency uint8-util to v2 ([#192](https://github.com/webtorrent/create-torrent/issues/192)) ([a88e7d1](https://github.com/webtorrent/create-torrent/commit/a88e7d131c8fbbf43b04420594022b0055fd275e)), closes [#197](https://github.com/webtorrent/create-torrent/issues/197) 99 | 100 | ## [6.0.3](https://github.com/webtorrent/create-torrent/compare/v6.0.2...v6.0.3) (2023-01-25) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * **deps:** update dependency bencode to v3 ([#189](https://github.com/webtorrent/create-torrent/issues/189)) ([cdddf06](https://github.com/webtorrent/create-torrent/commit/cdddf0648afb12ad6ace11b16924c3b6400e21fc)) 106 | 107 | ## [6.0.2](https://github.com/webtorrent/create-torrent/compare/v6.0.1...v6.0.2) (2022-12-03) 108 | 109 | 110 | ### Bug Fixes 111 | 112 | * drop rusha ([#184](https://github.com/webtorrent/create-torrent/issues/184)) ([9fcc2e9](https://github.com/webtorrent/create-torrent/commit/9fcc2e97c99bcd5f23e857f855630a759802bcf6)) 113 | 114 | ## [6.0.1](https://github.com/webtorrent/create-torrent/compare/v6.0.0...v6.0.1) (2022-11-28) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * **deps:** update dependency block-iterator to ^1.1.1 ([#188](https://github.com/webtorrent/create-torrent/issues/188)) ([a51b07c](https://github.com/webtorrent/create-torrent/commit/a51b07cc10b2bac96b985a0d5aee7bceae6505d4)) 120 | 121 | # [6.0.0](https://github.com/webtorrent/create-torrent/compare/v5.0.9...v6.0.0) (2022-11-25) 122 | 123 | 124 | ### Features 125 | 126 | * esm ([#187](https://github.com/webtorrent/create-torrent/issues/187)) ([0a3cb58](https://github.com/webtorrent/create-torrent/commit/0a3cb5886ff4403e8b1c27ede6932a2b21b8ac36)) 127 | 128 | 129 | ### BREAKING CHANGES 130 | 131 | * ESM only 132 | 133 | * feat: esm 134 | 135 | * fix: regex artifacts 136 | 137 | ## [5.0.9](https://github.com/webtorrent/create-torrent/compare/v5.0.8...v5.0.9) (2022-11-10) 138 | 139 | 140 | ### Bug Fixes 141 | 142 | * hash faster than callback ([#186](https://github.com/webtorrent/create-torrent/issues/186)) ([298f356](https://github.com/webtorrent/create-torrent/commit/298f356c9ba7832c22eee954025ab50bf29dc922)) 143 | 144 | ## [5.0.8](https://github.com/webtorrent/create-torrent/compare/v5.0.7...v5.0.8) (2022-11-10) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * **deps:** update dependency minimist to ^1.2.7 ([#162](https://github.com/webtorrent/create-torrent/issues/162)) ([b81eb98](https://github.com/webtorrent/create-torrent/commit/b81eb98b3a0b0358c40cac387a3a324ae0b4616a)) 150 | 151 | ## [5.0.7](https://github.com/webtorrent/create-torrent/compare/v5.0.6...v5.0.7) (2022-11-10) 152 | 153 | 154 | ### Bug Fixes 155 | 156 | * drop block-stream, drop streamx ([#185](https://github.com/webtorrent/create-torrent/issues/185)) ([4e0669c](https://github.com/webtorrent/create-torrent/commit/4e0669c3b2f92a4c0d115bcb6b01c26a21f9dbd6)) 157 | 158 | ## [5.0.6](https://github.com/webtorrent/create-torrent/compare/v5.0.5...v5.0.6) (2022-09-02) 159 | 160 | 161 | ### Bug Fixes 162 | 163 | * drop multi-stream ([#174](https://github.com/webtorrent/create-torrent/issues/174)) ([284f260](https://github.com/webtorrent/create-torrent/commit/284f2601e26f35c910e33de7c666bf5010b8dae3)) 164 | 165 | ## [5.0.5](https://github.com/webtorrent/create-torrent/compare/v5.0.4...v5.0.5) (2022-09-02) 166 | 167 | 168 | ### Bug Fixes 169 | 170 | * **deps:** update dependency fast-blob-stream to ^1.1.1 ([#175](https://github.com/webtorrent/create-torrent/issues/175)) ([d5cb6f3](https://github.com/webtorrent/create-torrent/commit/d5cb6f3cda1ef94f29583ad1a44280339d7fb15f)) 171 | * migrate to streamx ([#173](https://github.com/webtorrent/create-torrent/issues/173)) ([40a0f50](https://github.com/webtorrent/create-torrent/commit/40a0f50ec4829a7d047b36f79c79ccf3885b511e)) 172 | 173 | ## [5.0.4](https://github.com/webtorrent/create-torrent/compare/v5.0.3...v5.0.4) (2022-07-03) 174 | 175 | 176 | ### Bug Fixes 177 | 178 | * replace filestream with fast-blob-stream ([#171](https://github.com/webtorrent/create-torrent/issues/171)) ([d93a718](https://github.com/webtorrent/create-torrent/commit/d93a7181add5a8ac3fbd4b6bec92ad61f6b235cc)) 179 | 180 | ## [5.0.3](https://github.com/webtorrent/create-torrent/compare/v5.0.2...v5.0.3) (2022-07-03) 181 | 182 | 183 | ### Bug Fixes 184 | 185 | * **deps:** update webtorrent ([#163](https://github.com/webtorrent/create-torrent/issues/163)) ([7f1fb98](https://github.com/webtorrent/create-torrent/commit/7f1fb980feddc9005bd5983ae893f47ebc12ede8)) 186 | 187 | ## [5.0.2](https://github.com/webtorrent/create-torrent/compare/v5.0.1...v5.0.2) (2022-03-10) 188 | 189 | 190 | ### Bug Fixes 191 | 192 | * exception on folder depth > 1 ([#160](https://github.com/webtorrent/create-torrent/issues/160)) ([4afcea2](https://github.com/webtorrent/create-torrent/commit/4afcea2360284ce9d0762ed66507ae22b1b32b04)) 193 | 194 | ## [5.0.1](https://github.com/webtorrent/create-torrent/compare/v5.0.0...v5.0.1) (2021-08-06) 195 | 196 | 197 | ### Bug Fixes 198 | 199 | * remove flat pollyfill in get-files ([2dbd091](https://github.com/webtorrent/create-torrent/commit/2dbd09164d6df6170edcd0afb2f1921f29d5536f)) 200 | 201 | # [5.0.0](https://github.com/webtorrent/create-torrent/compare/v4.7.2...v5.0.0) (2021-08-05) 202 | 203 | 204 | ### Bug Fixes 205 | 206 | * Remove flat util fn ([#136](https://github.com/webtorrent/create-torrent/issues/136)) ([de7e8a9](https://github.com/webtorrent/create-torrent/commit/de7e8a9d69d367444d815b7c9aae3491e7a1392e)) 207 | 208 | 209 | ### BREAKING CHANGES 210 | 211 | * Node 12+ supported 212 | 213 | ## [4.7.2](https://github.com/webtorrent/create-torrent/compare/v4.7.1...v4.7.2) (2021-08-04) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * **deps:** update dependency bencode to ^2.0.2 ([#143](https://github.com/webtorrent/create-torrent/issues/143)) ([654a814](https://github.com/webtorrent/create-torrent/commit/654a8145a0ff31e200d6f6cb04d3c620faaacfc8)) 219 | 220 | ## [4.7.1](https://github.com/webtorrent/create-torrent/compare/v4.7.0...v4.7.1) (2021-07-22) 221 | 222 | 223 | ### Bug Fixes 224 | 225 | * **deps:** update dependency multistream to ^4.1.0 ([cbdc633](https://github.com/webtorrent/create-torrent/commit/cbdc633cd4cfc8a2389ccea884765fc3e219ad72)) 226 | * **deps:** update dependency run-parallel to ^1.2.0 ([ad58e7f](https://github.com/webtorrent/create-torrent/commit/ad58e7f67803fbeafdd433eac9be40fc31920347)) 227 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! create-torrent. MIT License. WebTorrent LLC */ 2 | import bencode from 'bencode' 3 | import blockIterator from 'block-iterator' 4 | import calcPieceLength from 'piece-length' 5 | import corePath from 'path' 6 | import isFile from 'is-file' 7 | import { isJunk } from 'junk' 8 | import joinIterator from 'join-async-iterator' 9 | import parallel from 'run-parallel' 10 | import queueMicrotask from 'queue-microtask' 11 | import { hash, hex2arr } from 'uint8-util' 12 | import 'fast-readable-async-iterator' 13 | 14 | import getFiles from './get-files.js' // browser exclude 15 | 16 | const announceList = [ 17 | ['udp://tracker.leechers-paradise.org:6969'], 18 | ['udp://tracker.coppersurfer.tk:6969'], 19 | ['udp://tracker.opentrackr.org:1337'], 20 | ['udp://explodie.org:6969'], 21 | ['udp://tracker.empire-js.us:1337'], 22 | ['wss://tracker.btorrent.xyz'], 23 | ['wss://tracker.openwebtorrent.com'], 24 | ['wss://tracker.webtorrent.dev'] 25 | ] 26 | 27 | /** 28 | * Create a torrent. 29 | * @param {string|File|FileList|Buffer|Stream|Array.} input 30 | * @param {Object} opts 31 | * @param {string=} opts.name 32 | * @param {Date=} opts.creationDate 33 | * @param {string=} opts.comment 34 | * @param {string=} opts.createdBy 35 | * @param {boolean|number=} opts.private 36 | * @param {number=} opts.pieceLength 37 | * @param {Array.>=} opts.announceList 38 | * @param {Array.=} opts.urlList 39 | * @param {Object=} opts.info 40 | * @param {Function} opts.onProgress 41 | * @param {function} cb 42 | * @return {Buffer} buffer of .torrent file data 43 | */ 44 | function createTorrent (input, opts, cb) { 45 | if (typeof opts === 'function') [opts, cb] = [cb, opts] 46 | opts = opts ? Object.assign({}, opts) : {} 47 | 48 | _parseInput(input, opts, (err, files, singleFileTorrent) => { 49 | if (err) return cb(err) 50 | opts.singleFileTorrent = singleFileTorrent 51 | onFiles(files, opts, cb) 52 | }) 53 | } 54 | 55 | function parseInput (input, opts, cb) { 56 | if (typeof opts === 'function') [opts, cb] = [cb, opts] 57 | opts = opts ? Object.assign({}, opts) : {} 58 | _parseInput(input, opts, cb) 59 | } 60 | 61 | const pathSymbol = Symbol('itemPath') 62 | 63 | /** 64 | * Parse input file and return file information. 65 | */ 66 | function _parseInput (input, opts, cb) { 67 | if (isFileList(input)) input = Array.from(input) 68 | if (!Array.isArray(input)) input = [input] 69 | 70 | if (input.length === 0) throw new Error('invalid input type') 71 | 72 | input.forEach(item => { 73 | if (item == null) throw new Error(`invalid input type: ${item}`) 74 | }) 75 | 76 | // In Electron, use the true file path 77 | input = input.map(item => { 78 | if (isBlob(item) && typeof item.path === 'string' && typeof getFiles === 'function') return item.path 79 | return item 80 | }) 81 | 82 | // If there's just one file, allow the name to be set by `opts.name` 83 | if (input.length === 1 && typeof input[0] !== 'string' && !input[0].name) input[0].name = opts.name 84 | 85 | let commonPrefix = null 86 | input.forEach((item, i) => { 87 | if (typeof item === 'string') { 88 | return 89 | } 90 | 91 | let path = item.fullPath || item.name 92 | if (!path) { 93 | path = `Unknown File ${i + 1}` 94 | item.unknownName = true 95 | } 96 | 97 | item[pathSymbol] = path.split('/') 98 | 99 | // Remove initial slash 100 | if (!item[pathSymbol][0]) { 101 | item[pathSymbol].shift() 102 | } 103 | 104 | if (item[pathSymbol].length < 2) { // No real prefix 105 | commonPrefix = null 106 | } else if (i === 0 && input.length > 1) { // The first file has a prefix 107 | commonPrefix = item[pathSymbol][0] 108 | } else if (item[pathSymbol][0] !== commonPrefix) { // The prefix doesn't match 109 | commonPrefix = null 110 | } 111 | }) 112 | 113 | const filterJunkFiles = opts.filterJunkFiles === undefined ? true : opts.filterJunkFiles 114 | if (filterJunkFiles) { 115 | // Remove junk files 116 | input = input.filter(item => { 117 | if (typeof item === 'string') { 118 | return true 119 | } 120 | return !isJunkPath(item[pathSymbol]) 121 | }) 122 | } 123 | 124 | if (commonPrefix) { 125 | input.forEach(item => { 126 | const pathless = (ArrayBuffer.isView(item) || isReadable(item)) && !item[pathSymbol] 127 | if (typeof item === 'string' || pathless) return 128 | item[pathSymbol].shift() 129 | }) 130 | } 131 | 132 | if (!opts.name && commonPrefix) { 133 | opts.name = commonPrefix 134 | } 135 | 136 | if (!opts.name) { 137 | // use first user-set file name 138 | input.some(item => { 139 | if (typeof item === 'string') { 140 | opts.name = corePath.basename(item) 141 | return true 142 | } else if (!item.unknownName) { 143 | opts.name = item[pathSymbol][item[pathSymbol].length - 1] 144 | return true 145 | } 146 | return false 147 | }) 148 | } 149 | 150 | if (!opts.name) { 151 | opts.name = `Unnamed Torrent ${Date.now()}` 152 | } 153 | 154 | const numPaths = input.reduce((sum, item) => sum + Number(typeof item === 'string'), 0) 155 | 156 | let isSingleFileTorrent = (input.length === 1) 157 | 158 | if (input.length === 1 && typeof input[0] === 'string') { 159 | if (typeof getFiles !== 'function') { 160 | throw new Error('filesystem paths do not work in the browser') 161 | } 162 | // If there's a single path, verify it's a file before deciding this is a single 163 | // file torrent 164 | isFile(input[0], (err, pathIsFile) => { 165 | if (err) return cb(err) 166 | isSingleFileTorrent = pathIsFile 167 | processInput() 168 | }) 169 | } else { 170 | queueMicrotask(processInput) 171 | } 172 | 173 | function processInput () { 174 | parallel(input.map(item => cb => { 175 | const file = {} 176 | 177 | if (isBlob(item)) { 178 | file.getStream = item.stream() 179 | file.length = item.size 180 | } else if (ArrayBuffer.isView(item)) { 181 | file.getStream = [item] // wrap in iterable to write entire buffer at once instead of unwrapping all bytes 182 | file.length = item.length 183 | } else if (isReadable(item)) { 184 | file.getStream = getStreamStream(item, file) 185 | file.length = 0 186 | } else if (typeof item === 'string') { 187 | if (typeof getFiles !== 'function') { 188 | throw new Error('filesystem paths do not work in the browser') 189 | } 190 | const keepRoot = numPaths > 1 || isSingleFileTorrent 191 | getFiles(item, keepRoot, cb) 192 | return // early return! 193 | } else { 194 | throw new Error('invalid input type') 195 | } 196 | file.path = item[pathSymbol] 197 | cb(null, file) 198 | }), (err, files) => { 199 | if (err) return cb(err) 200 | files = files.flat() 201 | cb(null, files, isSingleFileTorrent) 202 | }) 203 | } 204 | } 205 | 206 | const MAX_OUTSTANDING_HASHES = 5 207 | 208 | async function getPieceList (files, pieceLength, estimatedTorrentLength, opts, cb) { 209 | const pieces = [] 210 | let length = 0 211 | let hashedLength = 0 212 | 213 | const streams = files.map(file => file.getStream) 214 | 215 | const onProgress = opts.onProgress 216 | 217 | let remainingHashes = 0 218 | let pieceNum = 0 219 | let ended = false 220 | 221 | const iterator = blockIterator(joinIterator(streams), pieceLength, { zeroPadding: false }) 222 | try { 223 | for await (const chunk of iterator) { 224 | await new Promise(resolve => { 225 | length += chunk.length 226 | const i = pieceNum 227 | ++pieceNum 228 | if (++remainingHashes < MAX_OUTSTANDING_HASHES) resolve() 229 | hash(chunk, 'hex').then(hash => { 230 | pieces[i] = hash 231 | --remainingHashes 232 | hashedLength += chunk.length 233 | if (onProgress) onProgress(hashedLength, estimatedTorrentLength) 234 | resolve() 235 | if (ended && remainingHashes === 0) cb(null, hex2arr(pieces.join('')), length) 236 | }) 237 | }) 238 | } 239 | if (remainingHashes === 0) return cb(null, hex2arr(pieces.join('')), length) 240 | ended = true 241 | } catch (err) { 242 | cb(err) 243 | } 244 | } 245 | 246 | function onFiles (files, opts, cb) { 247 | let _announceList = opts.announceList 248 | 249 | if (!_announceList) { 250 | if (typeof opts.announce === 'string') _announceList = [[opts.announce]] 251 | else if (Array.isArray(opts.announce)) { 252 | _announceList = opts.announce.map(u => [u]) 253 | } 254 | } 255 | 256 | if (!_announceList) _announceList = [] 257 | 258 | if (globalThis.WEBTORRENT_ANNOUNCE) { 259 | if (typeof globalThis.WEBTORRENT_ANNOUNCE === 'string') { 260 | _announceList.push([[globalThis.WEBTORRENT_ANNOUNCE]]) 261 | } else if (Array.isArray(globalThis.WEBTORRENT_ANNOUNCE)) { 262 | _announceList = _announceList.concat(globalThis.WEBTORRENT_ANNOUNCE.map(u => [u])) 263 | } 264 | } 265 | 266 | // When no trackers specified, use some reasonable defaults 267 | if (opts.announce === undefined && opts.announceList === undefined) { 268 | _announceList = _announceList.concat(announceList) 269 | } 270 | 271 | if (typeof opts.urlList === 'string') opts.urlList = [opts.urlList] 272 | 273 | const torrent = { 274 | info: { 275 | name: opts.name 276 | }, 277 | 'creation date': Math.ceil((Number(opts.creationDate) || Date.now()) / 1000), 278 | encoding: 'UTF-8' 279 | } 280 | 281 | if (_announceList.length !== 0) { 282 | torrent.announce = _announceList[0][0] 283 | torrent['announce-list'] = _announceList 284 | } 285 | 286 | if (opts.comment !== undefined) torrent.comment = opts.comment 287 | 288 | if (opts.createdBy !== undefined) torrent['created by'] = opts.createdBy 289 | 290 | if (opts.private !== undefined) torrent.info.private = Number(opts.private) 291 | 292 | if (opts.info !== undefined) Object.assign(torrent.info, opts.info) 293 | 294 | // "ssl-cert" key is for SSL torrents, see: 295 | // - http://blog.libtorrent.org/2012/01/bittorrent-over-ssl/ 296 | // - http://www.libtorrent.org/manual-ref.html#ssl-torrents 297 | // - http://www.libtorrent.org/reference-Create_Torrents.html 298 | if (opts.sslCert !== undefined) torrent.info['ssl-cert'] = opts.sslCert 299 | 300 | if (opts.urlList !== undefined) torrent['url-list'] = opts.urlList 301 | 302 | const estimatedTorrentLength = files.reduce(sumLength, 0) 303 | const pieceLength = opts.pieceLength || calcPieceLength(estimatedTorrentLength) 304 | torrent.info['piece length'] = pieceLength 305 | 306 | getPieceList( 307 | files, 308 | pieceLength, 309 | estimatedTorrentLength, 310 | opts, 311 | (err, pieces, torrentLength) => { 312 | if (err) return cb(err) 313 | torrent.info.pieces = pieces 314 | 315 | files.forEach(file => { 316 | delete file.getStream 317 | }) 318 | 319 | if (opts.singleFileTorrent) { 320 | torrent.info.length = torrentLength 321 | } else { 322 | torrent.info.files = files 323 | } 324 | 325 | cb(null, bencode.encode(torrent)) 326 | } 327 | ) 328 | } 329 | 330 | /** 331 | * Determine if a a file is junk based on its path 332 | * (defined as hidden OR recognized by the `junk` package) 333 | * 334 | * @param {string} path 335 | * @return {boolean} 336 | */ 337 | function isJunkPath (path) { 338 | const filename = path[path.length - 1] 339 | return filename[0] === '.' && isJunk(filename) 340 | } 341 | 342 | /** 343 | * Accumulator to sum file lengths 344 | * @param {number} sum 345 | * @param {Object} file 346 | * @return {number} 347 | */ 348 | function sumLength (sum, file) { 349 | return sum + file.length 350 | } 351 | 352 | /** 353 | * Check if `obj` is a W3C `Blob` object (which `File` inherits from) 354 | * @param {*} obj 355 | * @return {boolean} 356 | */ 357 | function isBlob (obj) { 358 | return typeof Blob !== 'undefined' && obj instanceof Blob 359 | } 360 | 361 | /** 362 | * Check if `obj` is a W3C `FileList` object 363 | * @param {*} obj 364 | * @return {boolean} 365 | */ 366 | function isFileList (obj) { 367 | return typeof FileList !== 'undefined' && obj instanceof FileList 368 | } 369 | 370 | /** 371 | * Check if `obj` is a node Readable stream 372 | * @param {*} obj 373 | * @return {boolean} 374 | */ 375 | function isReadable (obj) { 376 | return typeof obj === 'object' && obj != null && typeof obj.pipe === 'function' 377 | } 378 | 379 | /** 380 | * Convert a readable stream to a lazy async iterator. Adds instrumentation to track 381 | * the number of bytes in the stream and set `file.length`. 382 | * 383 | * @generator 384 | * @param {Stream} readable 385 | * @param {Object} file 386 | * @return {Uint8Array} stream data/chunk 387 | */ 388 | async function * getStreamStream (readable, file) { 389 | for await (const chunk of readable) { 390 | file.length += chunk.length 391 | yield chunk 392 | } 393 | } 394 | 395 | export default createTorrent 396 | export { parseInput, announceList, isJunkPath } 397 | --------------------------------------------------------------------------------