bun-torrent 0.0.1-beta → 0.0.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/package.json +12 -1
- package/src/client.test.ts +6 -2
- package/src/torrent/storage/index.ts +15 -1
- package/src/torrent/storage/storage.test.ts +2 -2
package/README.md
CHANGED
|
@@ -1 +1,235 @@
|
|
|
1
1
|
# bun-torrent
|
|
2
|
+
|
|
3
|
+
A minimal Bun-native BitTorrent download-only client written in TypeScript.
|
|
4
|
+
|
|
5
|
+
`bun-torrent` can parse `.torrent` files, announce to HTTP and UDP trackers, connect to peers, download pieces, validate piece hashes, and write the downloaded files to disk. The public API is intentionally small: create a `Client`, inspect a torrent when you need metadata, then call `download()`.
|
|
6
|
+
|
|
7
|
+
It has no runtime dependencies.
|
|
8
|
+
|
|
9
|
+
> This package is currently beta software. The API can still change before a stable release.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Bun `>= 1.3.0`
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add bun-torrent@beta
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
or:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install bun-torrent@beta
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { Client } from 'bun-torrent';
|
|
31
|
+
|
|
32
|
+
const client = new Client({
|
|
33
|
+
outputDirectory: './downloads',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const torrent = await client.download({
|
|
37
|
+
torrentFile: './example.torrent',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
torrent.on('progress', (progress) => {
|
|
41
|
+
console.log({
|
|
42
|
+
percent: `${(progress.percent * 100).toFixed(2)}%`,
|
|
43
|
+
downloaded: progress.downloadedBytes,
|
|
44
|
+
received: progress.receivedBytes,
|
|
45
|
+
speed: progress.speed,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
torrent.on('done', () => {
|
|
50
|
+
console.log('Download complete');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
torrent.on('error', (error) => {
|
|
54
|
+
console.error('Download failed', error);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await torrent.done;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`download()` returns a `Torrent` instance and starts the download immediately.
|
|
61
|
+
|
|
62
|
+
## Client Setup
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { Client } from 'bun-torrent';
|
|
66
|
+
|
|
67
|
+
const client = new Client({
|
|
68
|
+
outputDirectory: './downloads',
|
|
69
|
+
maxInFlightRequestsPerPeer: 20,
|
|
70
|
+
requestTimeoutMs: 15_000,
|
|
71
|
+
progressEvents: 'piece',
|
|
72
|
+
speedSampleIntervalMs: 500,
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Client options:
|
|
77
|
+
|
|
78
|
+
- `outputDirectory`: directory where downloaded files are written. Defaults to `process.cwd()`.
|
|
79
|
+
- `files`: optional default file selection for downloads.
|
|
80
|
+
- `maxInFlightRequestsPerPeer`: maximum active block requests per peer. Defaults to `20`.
|
|
81
|
+
- `requestTimeoutMs`: timeout for an individual block request. Defaults to `15000`.
|
|
82
|
+
- `progressEvents`: `'piece'` emits progress when a piece completes, `'block'` emits for every received block. Defaults to `'piece'`.
|
|
83
|
+
- `speedSampleIntervalMs`: minimum interval used to refresh speed calculations. Defaults to `500`.
|
|
84
|
+
|
|
85
|
+
Options passed to `download()` override the client defaults for that download.
|
|
86
|
+
|
|
87
|
+
## Inspecting a Torrent
|
|
88
|
+
|
|
89
|
+
Use `inspect()` when you want metadata before starting a download, for example to show the file list or choose only some files.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const metadata = await client.inspect({
|
|
93
|
+
torrentFile: './example.torrent',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(metadata.name);
|
|
97
|
+
console.log(metadata.length);
|
|
98
|
+
console.log(metadata.files);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Torrent file input can be a file path, `Uint8Array`, or `ArrayBuffer`.
|
|
102
|
+
|
|
103
|
+
## Download Options
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const torrent = await client.download(
|
|
107
|
+
{
|
|
108
|
+
torrentFile: './example.torrent',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
outputDirectory: './downloads',
|
|
112
|
+
minConnections: 5,
|
|
113
|
+
announcePort: 6881,
|
|
114
|
+
progressEvents: 'block',
|
|
115
|
+
onChangeState: (state) => {
|
|
116
|
+
console.log('client state:', state);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Download options:
|
|
123
|
+
|
|
124
|
+
- `outputDirectory`: override the output directory for this download.
|
|
125
|
+
- `files`: download only selected files.
|
|
126
|
+
- `minConnections`: minimum connectable peer count requested before downloading starts.
|
|
127
|
+
- `announcePort`: port sent to trackers in announce requests.
|
|
128
|
+
- `maxInFlightRequestsPerPeer`: override request concurrency per peer.
|
|
129
|
+
- `requestTimeoutMs`: override block request timeout.
|
|
130
|
+
- `progressEvents`: `'piece'` or `'block'`.
|
|
131
|
+
- `speedSampleIntervalMs`: override speed sample interval.
|
|
132
|
+
- `onChangeState`: receives client setup states: `parsing`, `tracking`, `connecting`, `downloading`.
|
|
133
|
+
|
|
134
|
+
Tracker announce failures are treated as non-fatal. If no tracker responds, the client can still continue with an empty peer list instead of throwing during tracking.
|
|
135
|
+
|
|
136
|
+
## Selecting Files
|
|
137
|
+
|
|
138
|
+
For multi-file torrents, pass `files` to download only specific files. A file can be selected by its slash-joined path string or by its torrent path array.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const metadata = await client.inspect({
|
|
142
|
+
torrentFile: './big-buck-bunny.torrent',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log(metadata.files);
|
|
146
|
+
|
|
147
|
+
const torrent = await client.download(
|
|
148
|
+
{
|
|
149
|
+
torrentFile: './big-buck-bunny.torrent',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
outputDirectory: './downloads',
|
|
153
|
+
files: ['Big Buck Bunny.mp4'],
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
console.log(torrent.files);
|
|
158
|
+
await torrent.done;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`torrent.files` separates the selected files from the skipped files:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
{
|
|
165
|
+
included: [
|
|
166
|
+
{ path: ['Big Buck Bunny.mp4'], length: 276134947, offset: 140 },
|
|
167
|
+
],
|
|
168
|
+
excluded: [
|
|
169
|
+
{ path: ['Big Buck Bunny.en.srt'], length: 140, offset: 0 },
|
|
170
|
+
{ path: ['poster.jpg'], length: 310380, offset: 276135087 },
|
|
171
|
+
],
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Passing `files: null` or omitting `files` downloads everything.
|
|
176
|
+
|
|
177
|
+
## Torrent Events
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
torrent.on('state', ({ previous, state }) => {
|
|
181
|
+
console.log(previous, '->', state);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
torrent.on('progress', (progress) => {
|
|
185
|
+
console.log(progress.percent, progress.speed);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
torrent.on('peer', (peer) => {
|
|
189
|
+
console.log('peer connected', peer);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
torrent.on('done', () => {
|
|
193
|
+
console.log('done');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
torrent.on('error', (error) => {
|
|
197
|
+
console.error(error);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
torrent.on('close', () => {
|
|
201
|
+
console.log('closed');
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The current torrent state is also available through `torrent.state`. Possible states are:
|
|
206
|
+
|
|
207
|
+
- `downloading`
|
|
208
|
+
- `completed`
|
|
209
|
+
- `failed`
|
|
210
|
+
- `closed`
|
|
211
|
+
|
|
212
|
+
Call `torrent.close()` to stop the download and close peer connections.
|
|
213
|
+
|
|
214
|
+
## Progress Shape
|
|
215
|
+
|
|
216
|
+
Progress events and `torrent.progress` expose:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
{
|
|
220
|
+
totalBytes: number;
|
|
221
|
+
receivedBytes: number;
|
|
222
|
+
downloadedBytes: number;
|
|
223
|
+
totalPieces: number;
|
|
224
|
+
completedPieces: number;
|
|
225
|
+
percent: number;
|
|
226
|
+
speedBytesPerSecond: number;
|
|
227
|
+
speed: string;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`receivedBytes` counts received piece data, while `downloadedBytes` counts completed and hash-validated pieces.
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-torrent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"description": "A minimal Bun-native BitTorrent download-only client.",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/nikola04/bun-torrent.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/nikola04/bun-torrent/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/nikola04/bun-torrent#readme",
|
|
8
19
|
"keywords": [
|
|
9
20
|
"bun",
|
|
10
21
|
"bittorrent",
|
package/src/client.test.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
2
5
|
|
|
3
6
|
import { encodeBencode, toBValue } from '@torrent/index';
|
|
4
7
|
import { Client, DEFAULT_CLIENT_CONFIG } from './client';
|
|
@@ -53,8 +56,9 @@ describe('Client.inspect', () => {
|
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
test('inspects torrent metadata from file path input', async () => {
|
|
56
|
-
const
|
|
57
|
-
|
|
59
|
+
const outputDirectory = await mkdtemp(join(tmpdir(), 'bun-torrent-client-'));
|
|
60
|
+
const path = join(outputDirectory, 'inspect.torrent');
|
|
61
|
+
await writeFile(path, makeTorrent({ announce: 'https://tracker.test/announce' }));
|
|
58
62
|
|
|
59
63
|
const client = new Client();
|
|
60
64
|
const metadata = await client.inspect({ torrentFile: path });
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { TorrentMetadata } from '@torrent/types';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
2
3
|
import { mkdir, open } from 'node:fs/promises';
|
|
4
|
+
import type { FileHandle } from 'node:fs/promises';
|
|
3
5
|
import { dirname, join } from 'node:path';
|
|
4
6
|
import {
|
|
5
7
|
getPieceLength,
|
|
@@ -90,7 +92,7 @@ export const writePiece = async (
|
|
|
90
92
|
const path = resolveTorrentFilePath(options.outputDirectory, write.path);
|
|
91
93
|
await mkdir(dirname(path), { recursive: true });
|
|
92
94
|
|
|
93
|
-
const file = await
|
|
95
|
+
const file = await openWritableFile(path);
|
|
94
96
|
try {
|
|
95
97
|
await file.write(
|
|
96
98
|
data.subarray(write.dataOffset, write.dataOffset + write.length),
|
|
@@ -157,5 +159,17 @@ const resolveTorrentFilePath = (outputDirectory: string, parts: string[]): strin
|
|
|
157
159
|
return join(outputDirectory, ...parts);
|
|
158
160
|
};
|
|
159
161
|
|
|
162
|
+
const openWritableFile = async (path: string): Promise<FileHandle> => {
|
|
163
|
+
try {
|
|
164
|
+
return await open(path, constants.O_RDWR);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (!isNotFoundError(error)) throw error;
|
|
167
|
+
return await open(path, constants.O_RDWR | constants.O_CREAT);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const isNotFoundError = (error: unknown): error is NodeJS.ErrnoException =>
|
|
172
|
+
error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
173
|
+
|
|
160
174
|
export { TorrentStorageError, TorrentStorageErrorCode } from './storage.error';
|
|
161
175
|
export type { FileWrite, WritePieceOptions } from './types';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import type { TorrentMetadata } from '@torrent/types';
|
|
4
|
-
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { sha1 } from '@utils/sha1';
|
|
@@ -27,7 +27,7 @@ const makeMetadata = ({
|
|
|
27
27
|
files,
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
const readBytes = async (path: string): Promise<number[]> => [...(await
|
|
30
|
+
const readBytes = async (path: string): Promise<number[]> => [...(await readFile(path))];
|
|
31
31
|
|
|
32
32
|
describe('planPieceWrites', () => {
|
|
33
33
|
test('maps a single-file piece to one write', () => {
|