magnetk 2.1.1 → 2.2.0
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 +52 -63
- package/client.js +223 -65
- package/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,62 +12,58 @@ npm install magnetk
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
###
|
|
15
|
+
### One-Line Seeding (Zero Config)
|
|
16
|
+
The SDK automatically hashes your file, discovers the relay, and spawns the high-performance Go seeder in the background.
|
|
16
17
|
|
|
17
18
|
```javascript
|
|
18
|
-
import {
|
|
19
|
+
import { MagnetkClient } from 'magnetk';
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
-
|
|
21
|
+
const client = new MagnetkClient({
|
|
22
|
+
relayUrl: '69.169.109.243', // Optional: defaults to public relay
|
|
23
|
+
relayPort: 4003
|
|
24
|
+
});
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
// Seed a file and get a shareable link
|
|
27
|
+
const link = await client.seed('./my-app.zip');
|
|
28
|
+
console.log(`Share this link: ${link}`);
|
|
29
|
+
|
|
30
|
+
// Keep the process alive to serve peers (Ctrl+C to stop)
|
|
31
|
+
await client.keepSeeding();
|
|
25
32
|
```
|
|
26
33
|
|
|
27
|
-
###
|
|
34
|
+
### One-Line Downloading
|
|
35
|
+
Download files with automatic connection optimization (Direct > Local > Relay).
|
|
28
36
|
|
|
29
37
|
```javascript
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
4, // File Size (bytes)
|
|
36
|
-
'12D3KooWDmX2...', // Seed Peer ID
|
|
37
|
-
'/ip4/1.2.3.4/tcp/4001' // Relay Multiaddr
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
console.log(link.toString());
|
|
41
|
-
// Output: magnetk:?dn=app.txt&relay=%2Fip4%2F1.2.3.4...
|
|
38
|
+
const magnetURI = 'magnetk:?xt=urn:sha256:abc...&relay=...';
|
|
39
|
+
|
|
40
|
+
console.log('Downloading...');
|
|
41
|
+
await client.download(magnetURI, './my-app-downloaded.zip');
|
|
42
|
+
console.log('Done!');
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
###
|
|
45
|
+
### Event-Driven Progress tracking
|
|
46
|
+
Track every stage of the transfer with the new Event API.
|
|
45
47
|
|
|
46
48
|
```javascript
|
|
47
|
-
import { MagnetkClient, CONNECTION_TYPE } from 'magnetk';
|
|
48
|
-
|
|
49
49
|
const client = new MagnetkClient();
|
|
50
|
-
const uri = 'magnetk:?xt=urn:sha256:98df9a...&dn=app.txt&relay=/ip4/1.2.3.4/tcp/4001';
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
console.log('Starting P2P download...');
|
|
54
|
-
await client.download(uri, './downloads/app.txt');
|
|
55
|
-
|
|
56
|
-
// Access connection diagnostics
|
|
57
|
-
const stats = client.lastDownloadStats;
|
|
58
|
-
console.log(`Success! Connection Type: ${stats.connectionType}`);
|
|
59
|
-
console.log(`Remote Address: ${stats.remoteAddr}`);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
console.error('Download failed:', err.message);
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
50
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
51
|
+
// Seeding Events
|
|
52
|
+
client.on('hashing', (p) => console.log(`Hashing file: ${p.percent}%`));
|
|
53
|
+
client.on('seeding', (info) => console.log(`Seeding ${info.fileName} (${info.fileSize} bytes)`));
|
|
54
|
+
client.on('relay-connected', (r) => console.log(`Live on Relay: ${r.host}`));
|
|
55
|
+
|
|
56
|
+
// Download Events
|
|
57
|
+
client.on('download-start', (info) => console.log(`Starting: ${info.fileName}`));
|
|
58
|
+
client.on('progress', (p) => {
|
|
59
|
+
process.stdout.write(`\rDownload: ${p.percent.toFixed(1)}% | Speed: ${p.speed} KB/s`);
|
|
60
|
+
});
|
|
61
|
+
client.on('connection-type', (c) => console.log(`\nConnection Mode: ${c.type}`)); // DIRECT, RELAYED, LOCAL
|
|
62
|
+
client.on('complete', (info) => console.log(`\nSaved to: ${info.filePath}`));
|
|
63
|
+
|
|
64
|
+
// Error Handling
|
|
65
|
+
client.on('error', (err) => console.error(`Error (${err.code}): ${err.message}`));
|
|
66
|
+
```
|
|
71
67
|
|
|
72
68
|
## Advanced Features
|
|
73
69
|
|
|
@@ -79,37 +75,30 @@ const identity = await client.getPublicIdentity();
|
|
|
79
75
|
console.log(`Public Identity: ${identity.ip}:${identity.port}`);
|
|
80
76
|
```
|
|
81
77
|
|
|
82
|
-
###
|
|
83
|
-
To share a file on the Magnetk network, use the high-performance **Go Seeder** included in the toolkit:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
# Seed a file via the relay
|
|
87
|
-
./bin/seed.exe -relay "/ip4/relay-ip/tcp/4001" -file "path/to/file.zip"
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## CLI Interface
|
|
91
|
-
|
|
78
|
+
### Manual CLI
|
|
92
79
|
For quick operations, use the global `magnetk` command:
|
|
93
80
|
|
|
94
81
|
```bash
|
|
95
|
-
# Lookup seeds for a hash
|
|
96
|
-
magnetk lookup 98df9a59... --relay 1.2.3.4
|
|
97
|
-
|
|
98
82
|
# Download a file
|
|
99
83
|
magnetk download "magnetk:?..." --output ./file.zip
|
|
100
84
|
```
|
|
101
85
|
|
|
102
86
|
## API Reference
|
|
103
87
|
|
|
104
|
-
### `MagnetkLink`
|
|
105
|
-
- `static parse(uri)`: Create a link object from string.
|
|
106
|
-
- `toString()`: Serialize back to string.
|
|
107
|
-
|
|
108
88
|
### `MagnetkClient`
|
|
109
|
-
- `
|
|
110
|
-
- `
|
|
111
|
-
- `
|
|
112
|
-
- `
|
|
89
|
+
- `constructor(config)`: `{ relayUrl, relayPort, seedPort, enableSTUN }`
|
|
90
|
+
- `seed(filePath)`: Returns `Promise<magnetLink>`. Spawns background seeder.
|
|
91
|
+
- `download(uri, outputPath)`: Downloads file. Emits progress events.
|
|
92
|
+
- `stop()`: Kills all background seeder processes.
|
|
93
|
+
- `getRelayIdentity()`: Fetches Relay Peer ID and Multiaddrs.
|
|
94
|
+
|
|
95
|
+
### Events
|
|
96
|
+
- `hashing`: `{ percent }`
|
|
97
|
+
- `seeding`: `{ fileName, fileSize, hash }`
|
|
98
|
+
- `progress`: `{ percent, bytesDownloaded, totalBytes, speed }`
|
|
99
|
+
- `connection-type`: `{ type }` (DIRECT, RELAYED, LOCAL)
|
|
100
|
+
- `error`: `{ code, message }`
|
|
113
101
|
|
|
114
102
|
## License
|
|
115
103
|
MIT
|
|
104
|
+
|
package/client.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import net from 'net';
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import dgram from 'dgram';
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
11
15
|
import { MagnetkLink } from './magnetk.js';
|
|
12
16
|
|
|
13
17
|
const MSG_TYPE = {
|
|
@@ -17,6 +21,8 @@ const MSG_TYPE = {
|
|
|
17
21
|
MSG_MANIFEST_RES: 0x11,
|
|
18
22
|
MSG_CHUNK_REQ: 0x20,
|
|
19
23
|
MSG_CHUNK_RES: 0x21,
|
|
24
|
+
MSG_GET_IDENTITY: 0x32,
|
|
25
|
+
MSG_IDENTITY_RES: 0x33,
|
|
20
26
|
MSG_ERROR: 0xFF
|
|
21
27
|
};
|
|
22
28
|
|
|
@@ -27,10 +33,17 @@ export const CONNECTION_TYPE = {
|
|
|
27
33
|
UNKNOWN: 'UNKNOWN'
|
|
28
34
|
};
|
|
29
35
|
|
|
30
|
-
export class MagnetkClient {
|
|
36
|
+
export class MagnetkClient extends EventEmitter {
|
|
31
37
|
constructor(config = {}) {
|
|
32
|
-
|
|
33
|
-
this.
|
|
38
|
+
super();
|
|
39
|
+
this.config = {
|
|
40
|
+
relayUrl: '69.169.109.243',
|
|
41
|
+
relayPort: 4003,
|
|
42
|
+
seedPort: 4002,
|
|
43
|
+
enableSTUN: true,
|
|
44
|
+
...config
|
|
45
|
+
};
|
|
46
|
+
this.processes = [];
|
|
34
47
|
this.lastDownloadStats = {
|
|
35
48
|
connectionType: CONNECTION_TYPE.UNKNOWN,
|
|
36
49
|
remoteAddr: null,
|
|
@@ -104,6 +117,33 @@ export class MagnetkClient {
|
|
|
104
117
|
});
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Retrieves the relay's Peer ID and multiaddrs via TCP.
|
|
122
|
+
*/
|
|
123
|
+
async getRelayIdentity() {
|
|
124
|
+
console.log(`[Magnetk-JS] Fetching relay identity from ${this.config.relayUrl}:${this.config.relayPort}...`);
|
|
125
|
+
const socket = await this.connect(this.config.relayUrl, this.config.relayPort);
|
|
126
|
+
const writer = new FrameWriter(socket);
|
|
127
|
+
const reader = new FrameReader(socket);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await writer.writeFrame(MSG_TYPE.MSG_GET_IDENTITY, Buffer.alloc(0)); // Empty payload
|
|
131
|
+
const frame = await reader.readFrame();
|
|
132
|
+
|
|
133
|
+
if (frame.type === MSG_TYPE.MSG_IDENTITY_RES) {
|
|
134
|
+
const result = JSON.parse(frame.payload.toString());
|
|
135
|
+
return result;
|
|
136
|
+
} else if (frame.type === MSG_TYPE.MSG_ERROR) {
|
|
137
|
+
const err = JSON.parse(frame.payload.toString());
|
|
138
|
+
throw new Error(`Relay error: ${err.message}`);
|
|
139
|
+
} else {
|
|
140
|
+
throw new Error(`Unexpected response type: 0x${frame.type.toString(16)}`);
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
socket.end();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
/**
|
|
108
148
|
* Look up seeds for a file hash from the relay.
|
|
109
149
|
* @param {string} relayHost
|
|
@@ -135,39 +175,137 @@ export class MagnetkClient {
|
|
|
135
175
|
}
|
|
136
176
|
}
|
|
137
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Seeds a file and returns a Magnetk link.
|
|
180
|
+
* @param {string} filePath
|
|
181
|
+
* @returns {Promise<string>}
|
|
182
|
+
*/
|
|
183
|
+
async seed(filePath) {
|
|
184
|
+
const absolutePath = path.resolve(filePath);
|
|
185
|
+
if (!fs.existsSync(absolutePath)) {
|
|
186
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.emit('validating-relay');
|
|
190
|
+
// Simple reachability check (optional but good for UX)
|
|
191
|
+
|
|
192
|
+
const fileStats = fs.statSync(absolutePath);
|
|
193
|
+
this.emit('hashing', { percent: 0 });
|
|
194
|
+
|
|
195
|
+
// Calculate hash
|
|
196
|
+
const hash = await this._calculateHash(absolutePath);
|
|
197
|
+
this.emit('hashing', { percent: 100 });
|
|
198
|
+
|
|
199
|
+
// Fetch Relay Identity to get Peer ID
|
|
200
|
+
let relayPeerId;
|
|
201
|
+
try {
|
|
202
|
+
const identity = await this.getRelayIdentity();
|
|
203
|
+
relayPeerId = identity.peer_id;
|
|
204
|
+
console.log(`[Magnetk-JS] Relay Peer ID: ${relayPeerId}`);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.warn(`[Magnetk-JS] Failed to fetch relay identity: ${e.message}. Using default/configured address might fail if Peer ID is missing.`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const binPath = this._resolveBinPath('seed.exe');
|
|
210
|
+
const configPath = this._resolveConfigPath();
|
|
211
|
+
|
|
212
|
+
// Construct full multiaddr if we have the Peer ID
|
|
213
|
+
let relayMultiaddr = `/ip4/${this.config.relayUrl}/tcp/4001`;
|
|
214
|
+
if (relayPeerId) {
|
|
215
|
+
relayMultiaddr += `/p2p/${relayPeerId}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const args = [
|
|
219
|
+
'-relay', relayMultiaddr,
|
|
220
|
+
'-file', absolutePath,
|
|
221
|
+
'-port', this.config.seedPort.toString(),
|
|
222
|
+
'-config', configPath
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
console.log(`[Magnetk-JS] Spawning seeder: ${binPath} ${args.join(' ')}`);
|
|
226
|
+
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
const seeder = spawn(binPath, args);
|
|
229
|
+
this.processes.push(seeder);
|
|
230
|
+
|
|
231
|
+
let linkFound = false;
|
|
232
|
+
|
|
233
|
+
seeder.stdout.on('data', (data) => {
|
|
234
|
+
const output = data.toString();
|
|
235
|
+
if (output.includes('Magnetk Link:')) {
|
|
236
|
+
const link = output.split('Magnetk Link:')[1].trim().split('\n')[0].trim();
|
|
237
|
+
linkFound = true;
|
|
238
|
+
this.emit('seeding', {
|
|
239
|
+
fileName: path.basename(absolutePath),
|
|
240
|
+
fileSize: fileStats.size,
|
|
241
|
+
hash: hash
|
|
242
|
+
});
|
|
243
|
+
resolve(link);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
seeder.stderr.on('data', (data) => {
|
|
248
|
+
const msg = data.toString();
|
|
249
|
+
console.log(`[Seeder Stderr] ${msg}`); // Uncommented for debug
|
|
250
|
+
if (msg.includes('Connected to relay')) {
|
|
251
|
+
this.emit('relay-connected', { host: this.config.relayUrl, port: 4001 });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
seeder.on('error', (err) => {
|
|
256
|
+
if (!linkFound) reject(err);
|
|
257
|
+
this.emit('error', { code: 'SEEDER_ERROR', message: err.message });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
seeder.on('close', (code) => {
|
|
261
|
+
if (!linkFound && code !== 0) {
|
|
262
|
+
reject(new Error(`Seeder exited with code ${code}`));
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Keeps seeding active processes.
|
|
270
|
+
*/
|
|
271
|
+
async keepSeeding() {
|
|
272
|
+
console.log('Press Ctrl+C to stop seeding');
|
|
273
|
+
return new Promise(() => { }); // Wait forever
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Stops all active seeding processes.
|
|
278
|
+
*/
|
|
279
|
+
stop() {
|
|
280
|
+
this.processes.forEach(p => p.kill());
|
|
281
|
+
this.processes = [];
|
|
282
|
+
}
|
|
283
|
+
|
|
138
284
|
/**
|
|
139
285
|
* Downloads a file from a magnetk link.
|
|
140
286
|
* @param {string} magnetURI
|
|
141
287
|
* @param {string} outputPath
|
|
142
288
|
*/
|
|
143
289
|
async download(magnetURI, outputPath) {
|
|
144
|
-
const ml = MagnetkLink.parse(magnetURI);
|
|
145
|
-
|
|
290
|
+
const ml = typeof magnetURI === 'string' ? MagnetkLink.parse(magnetURI) : magnetURI;
|
|
291
|
+
this.emit('download-start', { fileName: ml.fileName });
|
|
146
292
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const discoveryPort = 4003;
|
|
293
|
+
// Resolve relay info from link or config
|
|
294
|
+
const relayHost = ml.relayAddr ? ml.relayAddr.split('/')[2] : this.config.relayUrl;
|
|
295
|
+
const discoveryPort = this.config.relayPort;
|
|
151
296
|
|
|
152
|
-
console.log(`[Magnetk-JS] Querying relay ${relayHost}:${discoveryPort} for seeds...`);
|
|
153
297
|
let lookupResult;
|
|
154
298
|
try {
|
|
155
299
|
lookupResult = await this.lookupPeer(relayHost, discoveryPort, ml.fileHash);
|
|
156
300
|
} catch (e) {
|
|
157
|
-
|
|
301
|
+
this.emit('error', { code: 'LOOKUP_FAILED', message: e.message });
|
|
302
|
+
throw e;
|
|
158
303
|
}
|
|
159
304
|
|
|
160
305
|
const addressesToTry = [];
|
|
161
306
|
if (lookupResult && lookupResult.found && lookupResult.seeds.length > 0) {
|
|
162
307
|
const seed = lookupResult.seeds[0];
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// 1. Add transport_addr if present
|
|
166
|
-
if (seed.transport_addr) {
|
|
167
|
-
addressesToTry.push(seed.transport_addr);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// 2. Add from multiaddrs (look for /ip4/x.x.x.x/tcp/yyyy)
|
|
308
|
+
if (seed.transport_addr) addressesToTry.push(seed.transport_addr);
|
|
171
309
|
if (seed.addresses) {
|
|
172
310
|
seed.addresses.forEach(addr => {
|
|
173
311
|
const parts = addr.split('/');
|
|
@@ -177,14 +315,9 @@ export class MagnetkClient {
|
|
|
177
315
|
});
|
|
178
316
|
}
|
|
179
317
|
}
|
|
180
|
-
|
|
181
|
-
// Add localhost fallback as last resort for dev
|
|
182
318
|
addressesToTry.push('127.0.0.1:4002');
|
|
183
319
|
|
|
184
|
-
// Unique addresses only
|
|
185
320
|
const uniqueAddrs = [...new Set(addressesToTry)];
|
|
186
|
-
console.log(`[Magnetk-JS] Candidate seeds: [${uniqueAddrs.join(', ')}]`);
|
|
187
|
-
|
|
188
321
|
let socket;
|
|
189
322
|
let connectionType = CONNECTION_TYPE.UNKNOWN;
|
|
190
323
|
let finalAddr = null;
|
|
@@ -196,81 +329,106 @@ export class MagnetkClient {
|
|
|
196
329
|
try {
|
|
197
330
|
socket = await this.connect(host, port);
|
|
198
331
|
finalAddr = addr;
|
|
332
|
+
if (host === '127.0.0.1' || host === 'localhost') connectionType = CONNECTION_TYPE.LOCAL;
|
|
333
|
+
else if (host === relayHost) connectionType = CONNECTION_TYPE.RELAYED;
|
|
334
|
+
else connectionType = CONNECTION_TYPE.DIRECT;
|
|
199
335
|
|
|
200
|
-
|
|
201
|
-
if (host === '127.0.0.1' || host === 'localhost') {
|
|
202
|
-
connectionType = CONNECTION_TYPE.LOCAL;
|
|
203
|
-
} else if (host === relayHost) {
|
|
204
|
-
connectionType = CONNECTION_TYPE.RELAYED;
|
|
205
|
-
} else {
|
|
206
|
-
connectionType = CONNECTION_TYPE.DIRECT;
|
|
207
|
-
}
|
|
336
|
+
this.emit('connection-type', { type: connectionType });
|
|
208
337
|
break;
|
|
209
|
-
} catch (e) {
|
|
210
|
-
console.log(`[Magnetk-JS] Failed to connect to ${addr}.`);
|
|
211
|
-
}
|
|
338
|
+
} catch (e) { }
|
|
212
339
|
}
|
|
213
340
|
|
|
214
|
-
if (!socket)
|
|
215
|
-
throw new Error('All seed connection attempts failed. Possible NAT blocking.');
|
|
216
|
-
}
|
|
341
|
+
if (!socket) throw new Error('All seed connection attempts failed.');
|
|
217
342
|
|
|
218
343
|
this.lastDownloadStats.connectionType = connectionType;
|
|
219
344
|
this.lastDownloadStats.remoteAddr = finalAddr;
|
|
220
|
-
console.log(`[Magnetk-JS] » Final Connection: ${finalAddr} (${connectionType})`);
|
|
221
345
|
|
|
222
346
|
const writer = new FrameWriter(socket);
|
|
223
347
|
const reader = new FrameReader(socket);
|
|
224
348
|
|
|
225
349
|
try {
|
|
226
|
-
// STEP 1: Request manifest
|
|
227
|
-
console.log(`[Magnetk-JS] Requesting manifest for ${ml.fileHash.substring(0, 10)}...`);
|
|
228
350
|
await writer.writeJSON(MSG_TYPE.MSG_MANIFEST_REQ, { file_hash: ml.fileHash });
|
|
229
|
-
|
|
230
351
|
const manifestFrame = await reader.readFrame();
|
|
231
|
-
if (manifestFrame.type !== MSG_TYPE.MSG_MANIFEST_RES) {
|
|
232
|
-
throw new Error(`Unexpected manifest response type: 0x${manifestFrame.type.toString(16)}`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
352
|
const manifest = JSON.parse(manifestFrame.payload.toString());
|
|
236
|
-
console.log(`[Magnetk-JS] Manifest received: ${manifest.file_name}, ${manifest.total_chunks} chunks, total size: ${manifest.file_size} bytes`);
|
|
237
353
|
|
|
238
|
-
// STEP 2: Prepare output file
|
|
239
354
|
const fd = fs.openSync(outputPath, 'w');
|
|
355
|
+
let bytesDownloaded = 0;
|
|
356
|
+
const startTime = Date.now();
|
|
240
357
|
|
|
241
|
-
// STEP 3: Request and write chunks
|
|
242
358
|
for (let i = 0; i < manifest.total_chunks; i++) {
|
|
243
|
-
process.stdout.write(`\r[Magnetk-JS] Downloading chunk ${i + 1}/${manifest.total_chunks}...`);
|
|
244
|
-
|
|
245
359
|
await writer.writeJSON(MSG_TYPE.MSG_CHUNK_REQ, {
|
|
246
360
|
file_hash: ml.fileHash,
|
|
247
361
|
chunk_indices: [i]
|
|
248
362
|
});
|
|
249
363
|
|
|
250
364
|
const chunkFrame = await reader.readFrame();
|
|
251
|
-
if (chunkFrame.type !== MSG_TYPE.MSG_CHUNK_RES) {
|
|
252
|
-
throw new Error(`Unexpected chunk response type: 0x${chunkFrame.type.toString(16)}`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Chunk payload format: [4 bytes: index][raw data]
|
|
256
|
-
const index = chunkFrame.payload.readUInt32BE(0);
|
|
257
365
|
const data = chunkFrame.payload.slice(4);
|
|
258
366
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
367
|
+
fs.writeSync(fd, data, 0, data.length, i * manifest.chunk_size);
|
|
368
|
+
|
|
369
|
+
bytesDownloaded += data.length;
|
|
370
|
+
const percent = (bytesDownloaded / manifest.file_size) * 100;
|
|
371
|
+
const elapsedS = (Date.now() - startTime) / 1000;
|
|
372
|
+
const speed = elapsedS > 0 ? (bytesDownloaded / 1024 / elapsedS).toFixed(1) : 0;
|
|
373
|
+
|
|
374
|
+
this.emit('progress', {
|
|
375
|
+
percent,
|
|
376
|
+
bytesDownloaded,
|
|
377
|
+
totalBytes: manifest.file_size,
|
|
378
|
+
speed
|
|
379
|
+
});
|
|
262
380
|
}
|
|
263
381
|
|
|
264
382
|
fs.closeSync(fd);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
} catch (err) {
|
|
268
|
-
console.error(`[Magnetk-JS] Download failed: ${err.message}`);
|
|
269
|
-
throw err;
|
|
383
|
+
this.emit('complete', { filePath: outputPath });
|
|
270
384
|
} finally {
|
|
271
385
|
socket.end();
|
|
272
386
|
}
|
|
273
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Finds available seeders for a hash.
|
|
391
|
+
*/
|
|
392
|
+
async lookupSeeds(fileHash) {
|
|
393
|
+
const result = await this.lookupPeer(this.config.relayUrl, this.config.relayPort, fileHash);
|
|
394
|
+
return result.found ? result.seeds : [];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_calculateHash(filePath) {
|
|
398
|
+
return new Promise((resolve, reject) => {
|
|
399
|
+
const hash = crypto.createHash('sha256');
|
|
400
|
+
const stream = fs.createReadStream(filePath);
|
|
401
|
+
stream.on('data', data => hash.update(data));
|
|
402
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
403
|
+
stream.on('error', reject);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_resolveBinPath(binName) {
|
|
408
|
+
// Try to find bin in likely locations
|
|
409
|
+
const paths = [
|
|
410
|
+
path.join(process.cwd(), 'bin', binName),
|
|
411
|
+
path.join(process.cwd(), '..', '..', '..', 'bin', binName),
|
|
412
|
+
path.join(process.cwd(), '..', '..', 'bin', binName),
|
|
413
|
+
path.join(process.cwd(), '..', 'bin', binName)
|
|
414
|
+
];
|
|
415
|
+
for (const p of paths) {
|
|
416
|
+
if (fs.existsSync(p)) return p;
|
|
417
|
+
}
|
|
418
|
+
return binName; // Fallback to PATH
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_resolveConfigPath() {
|
|
422
|
+
const paths = [
|
|
423
|
+
path.join(process.cwd(), 'config', 'settings.conf'),
|
|
424
|
+
path.join(process.cwd(), '..', '..', '..', 'config', 'settings.conf'),
|
|
425
|
+
path.join(process.cwd(), '..', '..', 'config', 'settings.conf')
|
|
426
|
+
];
|
|
427
|
+
for (const p of paths) {
|
|
428
|
+
if (fs.existsSync(p)) return p;
|
|
429
|
+
}
|
|
430
|
+
return ''; // Let seeder use default
|
|
431
|
+
}
|
|
274
432
|
}
|
|
275
433
|
|
|
276
434
|
class FrameWriter {
|
package/index.js
CHANGED