magnetk 2.1.1 → 2.2.3
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 +78 -62
- package/client.js +237 -65
- package/index.js +1 -0
- package/package.json +4 -2
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
|
-
|
|
65
|
-
### Checking Connection Types
|
|
66
50
|
|
|
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,57 @@ const identity = await client.getPublicIdentity();
|
|
|
79
75
|
console.log(`Public Identity: ${identity.ip}:${identity.port}`);
|
|
80
76
|
```
|
|
81
77
|
|
|
82
|
-
###
|
|
83
|
-
|
|
78
|
+
### Manual CLI
|
|
79
|
+
For quick operations, use the global `magnetk` command:
|
|
84
80
|
|
|
85
81
|
```bash
|
|
86
|
-
#
|
|
87
|
-
|
|
82
|
+
# Download a file
|
|
83
|
+
magnetk download "magnetk:?..." --output ./file.zip
|
|
88
84
|
```
|
|
89
85
|
|
|
90
|
-
##
|
|
86
|
+
## API Reference
|
|
91
87
|
|
|
92
|
-
|
|
88
|
+
### `MagnetkClient`
|
|
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.
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 }`
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
## Prerequisites
|
|
103
|
+
|
|
104
|
+
The JavaScript SDK is a lightweight wrapper around the high-performance **Magnetk Go Binaries**. You must have these binaries on your system to use seeding or relay features.
|
|
105
|
+
|
|
106
|
+
1. **Download** the latest binaries for your platform (Windows/Linux/macOS).
|
|
107
|
+
2. **Ensure** they are in your system `PATH` OR provide the path manually in the SDK config.
|
|
108
|
+
|
|
109
|
+
### Manual Configuration
|
|
110
|
+
```javascript
|
|
111
|
+
const client = new MagnetkClient({
|
|
112
|
+
seederPath: 'C:\\path\\to\\seed.exe', // Explicit path
|
|
113
|
+
relayUrl: '69.169.109.243'
|
|
114
|
+
});
|
|
100
115
|
```
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
### Environment Variables
|
|
118
|
+
You can also set the binary path globally:
|
|
119
|
+
- `MAGNETK_SEEDER_PATH`: Path to the `seed.exe` binary.
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
- `static parse(uri)`: Create a link object from string.
|
|
106
|
-
- `toString()`: Serialize back to string.
|
|
121
|
+
## Troubleshooting
|
|
107
122
|
|
|
108
|
-
###
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
### "spawn seed.exe ENOENT" Error
|
|
124
|
+
This means the SDK cannot find the Go seeder binary.
|
|
125
|
+
1. Check if `seed.exe` is in your system `PATH`.
|
|
126
|
+
2. Or provide `seederPath` in the `MagnetkClient` constructor.
|
|
127
|
+
3. Or set the `MAGNETK_SEEDER_PATH` environment variable.
|
|
113
128
|
|
|
114
129
|
## License
|
|
115
130
|
MIT
|
|
131
|
+
|
package/client.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
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';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
11
16
|
import { MagnetkLink } from './magnetk.js';
|
|
12
17
|
|
|
13
18
|
const MSG_TYPE = {
|
|
@@ -17,6 +22,8 @@ const MSG_TYPE = {
|
|
|
17
22
|
MSG_MANIFEST_RES: 0x11,
|
|
18
23
|
MSG_CHUNK_REQ: 0x20,
|
|
19
24
|
MSG_CHUNK_RES: 0x21,
|
|
25
|
+
MSG_GET_IDENTITY: 0x32,
|
|
26
|
+
MSG_IDENTITY_RES: 0x33,
|
|
20
27
|
MSG_ERROR: 0xFF
|
|
21
28
|
};
|
|
22
29
|
|
|
@@ -27,10 +34,19 @@ export const CONNECTION_TYPE = {
|
|
|
27
34
|
UNKNOWN: 'UNKNOWN'
|
|
28
35
|
};
|
|
29
36
|
|
|
30
|
-
export class MagnetkClient {
|
|
37
|
+
export class MagnetkClient extends EventEmitter {
|
|
31
38
|
constructor(config = {}) {
|
|
32
|
-
|
|
33
|
-
this.
|
|
39
|
+
super();
|
|
40
|
+
this.config = {
|
|
41
|
+
relayUrl: '69.169.109.243',
|
|
42
|
+
relayPort: 4003,
|
|
43
|
+
seedPort: 4002,
|
|
44
|
+
enableSTUN: true,
|
|
45
|
+
seederPath: null,
|
|
46
|
+
configPath: null,
|
|
47
|
+
...config
|
|
48
|
+
};
|
|
49
|
+
this.processes = [];
|
|
34
50
|
this.lastDownloadStats = {
|
|
35
51
|
connectionType: CONNECTION_TYPE.UNKNOWN,
|
|
36
52
|
remoteAddr: null,
|
|
@@ -104,6 +120,33 @@ export class MagnetkClient {
|
|
|
104
120
|
});
|
|
105
121
|
}
|
|
106
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Retrieves the relay's Peer ID and multiaddrs via TCP.
|
|
125
|
+
*/
|
|
126
|
+
async getRelayIdentity() {
|
|
127
|
+
console.log(`[Magnetk-JS] Fetching relay identity from ${this.config.relayUrl}:${this.config.relayPort}...`);
|
|
128
|
+
const socket = await this.connect(this.config.relayUrl, this.config.relayPort);
|
|
129
|
+
const writer = new FrameWriter(socket);
|
|
130
|
+
const reader = new FrameReader(socket);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await writer.writeFrame(MSG_TYPE.MSG_GET_IDENTITY, Buffer.alloc(0)); // Empty payload
|
|
134
|
+
const frame = await reader.readFrame();
|
|
135
|
+
|
|
136
|
+
if (frame.type === MSG_TYPE.MSG_IDENTITY_RES) {
|
|
137
|
+
const result = JSON.parse(frame.payload.toString());
|
|
138
|
+
return result;
|
|
139
|
+
} else if (frame.type === MSG_TYPE.MSG_ERROR) {
|
|
140
|
+
const err = JSON.parse(frame.payload.toString());
|
|
141
|
+
throw new Error(`Relay error: ${err.message}`);
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error(`Unexpected response type: 0x${frame.type.toString(16)}`);
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
socket.end();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
107
150
|
/**
|
|
108
151
|
* Look up seeds for a file hash from the relay.
|
|
109
152
|
* @param {string} relayHost
|
|
@@ -135,39 +178,137 @@ export class MagnetkClient {
|
|
|
135
178
|
}
|
|
136
179
|
}
|
|
137
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Seeds a file and returns a Magnetk link.
|
|
183
|
+
* @param {string} filePath
|
|
184
|
+
* @returns {Promise<string>}
|
|
185
|
+
*/
|
|
186
|
+
async seed(filePath) {
|
|
187
|
+
const absolutePath = path.resolve(filePath);
|
|
188
|
+
if (!fs.existsSync(absolutePath)) {
|
|
189
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.emit('validating-relay');
|
|
193
|
+
// Simple reachability check (optional but good for UX)
|
|
194
|
+
|
|
195
|
+
const fileStats = fs.statSync(absolutePath);
|
|
196
|
+
this.emit('hashing', { percent: 0 });
|
|
197
|
+
|
|
198
|
+
// Calculate hash
|
|
199
|
+
const hash = await this._calculateHash(absolutePath);
|
|
200
|
+
this.emit('hashing', { percent: 100 });
|
|
201
|
+
|
|
202
|
+
// Fetch Relay Identity to get Peer ID
|
|
203
|
+
let relayPeerId;
|
|
204
|
+
try {
|
|
205
|
+
const identity = await this.getRelayIdentity();
|
|
206
|
+
relayPeerId = identity.peer_id;
|
|
207
|
+
console.log(`[Magnetk-JS] Relay Peer ID: ${relayPeerId}`);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.warn(`[Magnetk-JS] Failed to fetch relay identity: ${e.message}. Using default/configured address might fail if Peer ID is missing.`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const binPath = this._resolveBinPath('seed.exe');
|
|
213
|
+
const configPath = this._resolveConfigPath();
|
|
214
|
+
|
|
215
|
+
// Construct full multiaddr if we have the Peer ID
|
|
216
|
+
let relayMultiaddr = `/ip4/${this.config.relayUrl}/tcp/4001`;
|
|
217
|
+
if (relayPeerId) {
|
|
218
|
+
relayMultiaddr += `/p2p/${relayPeerId}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const args = [
|
|
222
|
+
'-relay', relayMultiaddr,
|
|
223
|
+
'-file', absolutePath,
|
|
224
|
+
'-port', this.config.seedPort.toString(),
|
|
225
|
+
'-config', configPath
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
console.log(`[Magnetk-JS] Spawning seeder: ${binPath} ${args.join(' ')}`);
|
|
229
|
+
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
const seeder = spawn(binPath, args);
|
|
232
|
+
this.processes.push(seeder);
|
|
233
|
+
|
|
234
|
+
let linkFound = false;
|
|
235
|
+
|
|
236
|
+
seeder.stdout.on('data', (data) => {
|
|
237
|
+
const output = data.toString();
|
|
238
|
+
if (output.includes('Magnetk Link:')) {
|
|
239
|
+
const link = output.split('Magnetk Link:')[1].trim().split('\n')[0].trim();
|
|
240
|
+
linkFound = true;
|
|
241
|
+
this.emit('seeding', {
|
|
242
|
+
fileName: path.basename(absolutePath),
|
|
243
|
+
fileSize: fileStats.size,
|
|
244
|
+
hash: hash
|
|
245
|
+
});
|
|
246
|
+
resolve(link);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
seeder.stderr.on('data', (data) => {
|
|
251
|
+
const msg = data.toString();
|
|
252
|
+
console.log(`[Seeder Stderr] ${msg}`); // Uncommented for debug
|
|
253
|
+
if (msg.includes('Connected to relay')) {
|
|
254
|
+
this.emit('relay-connected', { host: this.config.relayUrl, port: 4001 });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
seeder.on('error', (err) => {
|
|
259
|
+
if (!linkFound) reject(err);
|
|
260
|
+
this.emit('error', { code: 'SEEDER_ERROR', message: err.message });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
seeder.on('close', (code) => {
|
|
264
|
+
if (!linkFound && code !== 0) {
|
|
265
|
+
reject(new Error(`Seeder exited with code ${code}`));
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Keeps seeding active processes.
|
|
273
|
+
*/
|
|
274
|
+
async keepSeeding() {
|
|
275
|
+
console.log('Press Ctrl+C to stop seeding');
|
|
276
|
+
return new Promise(() => { }); // Wait forever
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Stops all active seeding processes.
|
|
281
|
+
*/
|
|
282
|
+
stop() {
|
|
283
|
+
this.processes.forEach(p => p.kill());
|
|
284
|
+
this.processes = [];
|
|
285
|
+
}
|
|
286
|
+
|
|
138
287
|
/**
|
|
139
288
|
* Downloads a file from a magnetk link.
|
|
140
289
|
* @param {string} magnetURI
|
|
141
290
|
* @param {string} outputPath
|
|
142
291
|
*/
|
|
143
292
|
async download(magnetURI, outputPath) {
|
|
144
|
-
const ml = MagnetkLink.parse(magnetURI);
|
|
145
|
-
|
|
293
|
+
const ml = typeof magnetURI === 'string' ? MagnetkLink.parse(magnetURI) : magnetURI;
|
|
294
|
+
this.emit('download-start', { fileName: ml.fileName });
|
|
146
295
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const discoveryPort = 4003;
|
|
296
|
+
// Resolve relay info from link or config
|
|
297
|
+
const relayHost = ml.relayAddr ? ml.relayAddr.split('/')[2] : this.config.relayUrl;
|
|
298
|
+
const discoveryPort = this.config.relayPort;
|
|
151
299
|
|
|
152
|
-
console.log(`[Magnetk-JS] Querying relay ${relayHost}:${discoveryPort} for seeds...`);
|
|
153
300
|
let lookupResult;
|
|
154
301
|
try {
|
|
155
302
|
lookupResult = await this.lookupPeer(relayHost, discoveryPort, ml.fileHash);
|
|
156
303
|
} catch (e) {
|
|
157
|
-
|
|
304
|
+
this.emit('error', { code: 'LOOKUP_FAILED', message: e.message });
|
|
305
|
+
throw e;
|
|
158
306
|
}
|
|
159
307
|
|
|
160
308
|
const addressesToTry = [];
|
|
161
309
|
if (lookupResult && lookupResult.found && lookupResult.seeds.length > 0) {
|
|
162
310
|
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)
|
|
311
|
+
if (seed.transport_addr) addressesToTry.push(seed.transport_addr);
|
|
171
312
|
if (seed.addresses) {
|
|
172
313
|
seed.addresses.forEach(addr => {
|
|
173
314
|
const parts = addr.split('/');
|
|
@@ -177,14 +318,9 @@ export class MagnetkClient {
|
|
|
177
318
|
});
|
|
178
319
|
}
|
|
179
320
|
}
|
|
180
|
-
|
|
181
|
-
// Add localhost fallback as last resort for dev
|
|
182
321
|
addressesToTry.push('127.0.0.1:4002');
|
|
183
322
|
|
|
184
|
-
// Unique addresses only
|
|
185
323
|
const uniqueAddrs = [...new Set(addressesToTry)];
|
|
186
|
-
console.log(`[Magnetk-JS] Candidate seeds: [${uniqueAddrs.join(', ')}]`);
|
|
187
|
-
|
|
188
324
|
let socket;
|
|
189
325
|
let connectionType = CONNECTION_TYPE.UNKNOWN;
|
|
190
326
|
let finalAddr = null;
|
|
@@ -196,81 +332,117 @@ export class MagnetkClient {
|
|
|
196
332
|
try {
|
|
197
333
|
socket = await this.connect(host, port);
|
|
198
334
|
finalAddr = addr;
|
|
335
|
+
if (host === '127.0.0.1' || host === 'localhost') connectionType = CONNECTION_TYPE.LOCAL;
|
|
336
|
+
else if (host === relayHost) connectionType = CONNECTION_TYPE.RELAYED;
|
|
337
|
+
else connectionType = CONNECTION_TYPE.DIRECT;
|
|
199
338
|
|
|
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
|
-
}
|
|
339
|
+
this.emit('connection-type', { type: connectionType });
|
|
208
340
|
break;
|
|
209
|
-
} catch (e) {
|
|
210
|
-
console.log(`[Magnetk-JS] Failed to connect to ${addr}.`);
|
|
211
|
-
}
|
|
341
|
+
} catch (e) { }
|
|
212
342
|
}
|
|
213
343
|
|
|
214
|
-
if (!socket)
|
|
215
|
-
throw new Error('All seed connection attempts failed. Possible NAT blocking.');
|
|
216
|
-
}
|
|
344
|
+
if (!socket) throw new Error('All seed connection attempts failed.');
|
|
217
345
|
|
|
218
346
|
this.lastDownloadStats.connectionType = connectionType;
|
|
219
347
|
this.lastDownloadStats.remoteAddr = finalAddr;
|
|
220
|
-
console.log(`[Magnetk-JS] » Final Connection: ${finalAddr} (${connectionType})`);
|
|
221
348
|
|
|
222
349
|
const writer = new FrameWriter(socket);
|
|
223
350
|
const reader = new FrameReader(socket);
|
|
224
351
|
|
|
225
352
|
try {
|
|
226
|
-
// STEP 1: Request manifest
|
|
227
|
-
console.log(`[Magnetk-JS] Requesting manifest for ${ml.fileHash.substring(0, 10)}...`);
|
|
228
353
|
await writer.writeJSON(MSG_TYPE.MSG_MANIFEST_REQ, { file_hash: ml.fileHash });
|
|
229
|
-
|
|
230
354
|
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
355
|
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
356
|
|
|
238
|
-
// STEP 2: Prepare output file
|
|
239
357
|
const fd = fs.openSync(outputPath, 'w');
|
|
358
|
+
let bytesDownloaded = 0;
|
|
359
|
+
const startTime = Date.now();
|
|
240
360
|
|
|
241
|
-
// STEP 3: Request and write chunks
|
|
242
361
|
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
362
|
await writer.writeJSON(MSG_TYPE.MSG_CHUNK_REQ, {
|
|
246
363
|
file_hash: ml.fileHash,
|
|
247
364
|
chunk_indices: [i]
|
|
248
365
|
});
|
|
249
366
|
|
|
250
367
|
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
368
|
const data = chunkFrame.payload.slice(4);
|
|
258
369
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
370
|
+
fs.writeSync(fd, data, 0, data.length, i * manifest.chunk_size);
|
|
371
|
+
|
|
372
|
+
bytesDownloaded += data.length;
|
|
373
|
+
const percent = (bytesDownloaded / manifest.file_size) * 100;
|
|
374
|
+
const elapsedS = (Date.now() - startTime) / 1000;
|
|
375
|
+
const speed = elapsedS > 0 ? (bytesDownloaded / 1024 / elapsedS).toFixed(1) : 0;
|
|
376
|
+
|
|
377
|
+
this.emit('progress', {
|
|
378
|
+
percent,
|
|
379
|
+
bytesDownloaded,
|
|
380
|
+
totalBytes: manifest.file_size,
|
|
381
|
+
speed
|
|
382
|
+
});
|
|
262
383
|
}
|
|
263
384
|
|
|
264
385
|
fs.closeSync(fd);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
} catch (err) {
|
|
268
|
-
console.error(`[Magnetk-JS] Download failed: ${err.message}`);
|
|
269
|
-
throw err;
|
|
386
|
+
this.emit('complete', { filePath: outputPath });
|
|
270
387
|
} finally {
|
|
271
388
|
socket.end();
|
|
272
389
|
}
|
|
273
390
|
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Finds available seeders for a hash.
|
|
394
|
+
*/
|
|
395
|
+
async lookupSeeds(fileHash) {
|
|
396
|
+
const result = await this.lookupPeer(this.config.relayUrl, this.config.relayPort, fileHash);
|
|
397
|
+
return result.found ? result.seeds : [];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_calculateHash(filePath) {
|
|
401
|
+
return new Promise((resolve, reject) => {
|
|
402
|
+
const hash = crypto.createHash('sha256');
|
|
403
|
+
const stream = fs.createReadStream(filePath);
|
|
404
|
+
stream.on('data', data => hash.update(data));
|
|
405
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
406
|
+
stream.on('error', reject);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_resolveBinPath(binName) {
|
|
411
|
+
// 1. Check explicit config
|
|
412
|
+
if (this.config.seederPath && binName === 'seed.exe') return this.config.seederPath;
|
|
413
|
+
|
|
414
|
+
// 2. Check environment variable
|
|
415
|
+
const envPath = binName === 'seed.exe' ? process.env.MAGNETK_SEEDER_PATH : null;
|
|
416
|
+
if (envPath && fs.existsSync(envPath)) return envPath;
|
|
417
|
+
|
|
418
|
+
// 3. Check local dev environment paths
|
|
419
|
+
const paths = [
|
|
420
|
+
path.join(process.cwd(), 'bin', binName),
|
|
421
|
+
path.join(process.cwd(), '..', '..', '..', 'bin', binName),
|
|
422
|
+
path.join(process.cwd(), '..', '..', 'bin', binName),
|
|
423
|
+
path.join(process.cwd(), '..', 'bin', binName)
|
|
424
|
+
];
|
|
425
|
+
for (const p of paths) {
|
|
426
|
+
if (fs.existsSync(p)) return p;
|
|
427
|
+
}
|
|
428
|
+
return binName; // Fallback to PATH
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_resolveConfigPath() {
|
|
432
|
+
// 1. Check explicit config
|
|
433
|
+
if (this.config.configPath) return this.config.configPath;
|
|
434
|
+
|
|
435
|
+
// 2. Check local dev environment
|
|
436
|
+
const paths = [
|
|
437
|
+
path.join(process.cwd(), 'config', 'settings.conf'),
|
|
438
|
+
path.join(process.cwd(), '..', '..', '..', 'config', 'settings.conf'),
|
|
439
|
+
path.join(process.cwd(), '..', '..', 'config', 'settings.conf')
|
|
440
|
+
];
|
|
441
|
+
for (const p of paths) {
|
|
442
|
+
if (fs.existsSync(p)) return p;
|
|
443
|
+
}
|
|
444
|
+
return ''; // Let seeder use default
|
|
445
|
+
}
|
|
274
446
|
}
|
|
275
447
|
|
|
276
448
|
class FrameWriter {
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magnetk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "JavaScript SDK for Magnetk P2P File Transfer System",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"utils.js",
|
|
15
15
|
"README.md",
|
|
16
16
|
"LICENSE",
|
|
17
|
-
"package.json"
|
|
17
|
+
"package.json",
|
|
18
|
+
"share.js",
|
|
19
|
+
"download.js"
|
|
18
20
|
],
|
|
19
21
|
"scripts": {
|
|
20
22
|
"test": "node test.js"
|