@iksdev/shard-cli 0.1.8 → 0.1.10
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 +6 -4
- package/bin/shard.js +105 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,10 +24,10 @@ shard --help
|
|
|
24
24
|
shard login --server http://localhost:3000 --username admin --password secret
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
2. Partage
|
|
27
|
+
2. Partage relay (sans stockage serveur, commande simple)
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
shard share ./MonFichier.mp4 --server https://shard-0ow4.onrender.com --
|
|
30
|
+
shard share ./MonFichier.mp4 --server https://shard-0ow4.onrender.com --limits 0 --temps 0
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
3. Sync un dossier
|
|
@@ -41,7 +41,7 @@ shard sync ./MonDossier
|
|
|
41
41
|
- `shard login --username <name> --password <pass> [--server <url>]`
|
|
42
42
|
- `shard whoami [--server <url>]`
|
|
43
43
|
- `shard sync <folder> [--server <url>] [--dry-run] [--force]`
|
|
44
|
-
- `shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--
|
|
44
|
+
- `shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]`
|
|
45
45
|
- `shard logout`
|
|
46
46
|
- `shard config show`
|
|
47
47
|
- `shard config set-server <url>`
|
|
@@ -51,4 +51,6 @@ shard sync ./MonDossier
|
|
|
51
51
|
- Le CLI stocke la config dans `~/.shard-cli/config.json`.
|
|
52
52
|
- Le CLI stocke l'etat de sync dans `<ton-dossier>/.shard-sync-state.json`.
|
|
53
53
|
- Les uploads passent par `POST /api/files/upload` avec token `Bearer`.
|
|
54
|
-
-
|
|
54
|
+
- Par défaut `shard share` utilise le mode relay: le fichier reste sur ton PC, le serveur stocke seulement metadata + token.
|
|
55
|
+
- Aucun tunnel externe à installer: le serveur Shard relaie directement le flux via websocket.
|
|
56
|
+
- Utilise `--upload` pour revenir au mode historique (upload serveur).
|
package/bin/shard.js
CHANGED
|
@@ -4,7 +4,6 @@ const fs = require('fs');
|
|
|
4
4
|
const fsp = require('fs/promises');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const http = require('http');
|
|
8
7
|
const crypto = require('crypto');
|
|
9
8
|
const { Readable } = require('stream');
|
|
10
9
|
const { pipeline } = require('stream/promises');
|
|
@@ -22,7 +21,7 @@ Usage:
|
|
|
22
21
|
shard login --username <name> --password <pass> [--server <url>]
|
|
23
22
|
shard whoami [--server <url>]
|
|
24
23
|
shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]
|
|
25
|
-
shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--
|
|
24
|
+
shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]
|
|
26
25
|
shard logout
|
|
27
26
|
shard config show
|
|
28
27
|
shard config set-server <url>
|
|
@@ -32,7 +31,8 @@ Examples:
|
|
|
32
31
|
shard sync ./MonDossier
|
|
33
32
|
shard sync ./MonDossier --once
|
|
34
33
|
shard sync ./MonDossier --dry-run
|
|
35
|
-
shard share ./MonFichier.mp4 --
|
|
34
|
+
shard share ./MonFichier.mp4 --limits 0 --temps 0
|
|
35
|
+
shard share ./MonFichier.mp4 --upload
|
|
36
36
|
`);
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -310,61 +310,19 @@ function parseOptionalPositiveInt(raw, flagName) {
|
|
|
310
310
|
return n;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
function
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
function toWebSocketUrl(serverUrl, token) {
|
|
314
|
+
const url = new URL(serverUrl);
|
|
315
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
316
|
+
url.pathname = '/api/relay/ws';
|
|
317
|
+
url.search = '';
|
|
318
|
+
url.searchParams.set('token', token);
|
|
319
|
+
return url.toString();
|
|
317
320
|
}
|
|
318
321
|
|
|
319
|
-
function startLocalSingleFileServer({ filePath, fileName, mimeType, port, accessKey }) {
|
|
320
|
-
const safeName = encodeURIComponent(fileName);
|
|
321
|
-
const routePath = `/download/${accessKey}`;
|
|
322
|
-
|
|
323
|
-
const server = http.createServer((req, res) => {
|
|
324
|
-
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
|
325
|
-
|
|
326
|
-
if (requestUrl.pathname === '/health') {
|
|
327
|
-
res.statusCode = 200;
|
|
328
|
-
res.setHeader('Content-Type', 'application/json');
|
|
329
|
-
res.end(JSON.stringify({ ok: true, mode: 'local-share', file: fileName }));
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (requestUrl.pathname !== routePath) {
|
|
334
|
-
res.statusCode = 404;
|
|
335
|
-
res.setHeader('Content-Type', 'application/json');
|
|
336
|
-
res.end(JSON.stringify({ error: 'Not found' }));
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const stream = fs.createReadStream(filePath);
|
|
341
|
-
res.statusCode = 200;
|
|
342
|
-
res.setHeader('Content-Type', mimeType || 'application/octet-stream');
|
|
343
|
-
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${safeName}`);
|
|
344
|
-
stream.on('error', () => {
|
|
345
|
-
if (!res.headersSent) {
|
|
346
|
-
res.statusCode = 500;
|
|
347
|
-
res.end('Read error');
|
|
348
|
-
} else {
|
|
349
|
-
res.destroy();
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
stream.pipe(res);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
return new Promise((resolve, reject) => {
|
|
356
|
-
server.once('error', reject);
|
|
357
|
-
server.listen(port, '0.0.0.0', () => {
|
|
358
|
-
server.removeListener('error', reject);
|
|
359
|
-
resolve({ server, routePath });
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
322
|
async function shareFile(positionals, flags) {
|
|
365
323
|
const target = positionals[0];
|
|
366
324
|
if (!target) {
|
|
367
|
-
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--
|
|
325
|
+
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]');
|
|
368
326
|
}
|
|
369
327
|
|
|
370
328
|
const absPath = path.resolve(process.cwd(), target);
|
|
@@ -378,7 +336,7 @@ async function shareFile(positionals, flags) {
|
|
|
378
336
|
|
|
379
337
|
const limits = parseOptionalPositiveInt(flags.limits, '--limits');
|
|
380
338
|
const temps = parseOptionalPositiveInt(flags.temps, '--temps');
|
|
381
|
-
const localMode = Boolean(flags.
|
|
339
|
+
const localMode = !Boolean(flags.upload);
|
|
382
340
|
|
|
383
341
|
const config = await readConfig();
|
|
384
342
|
const server = getServer(flags, config);
|
|
@@ -394,40 +352,99 @@ async function shareFile(positionals, flags) {
|
|
|
394
352
|
|
|
395
353
|
const fileName = path.basename(absPath);
|
|
396
354
|
if (localMode) {
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
355
|
+
const mimeType = guessMime(absPath);
|
|
356
|
+
const relayFileId = crypto.randomBytes(12).toString('base64url');
|
|
357
|
+
const wsUrl = toWebSocketUrl(server, token);
|
|
358
|
+
const relaySocket = new WebSocket(wsUrl);
|
|
401
359
|
|
|
402
|
-
let
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
360
|
+
let relayClientId = '';
|
|
361
|
+
let closed = false;
|
|
362
|
+
const closeRelay = () => {
|
|
363
|
+
if (closed) return;
|
|
364
|
+
closed = true;
|
|
365
|
+
try { relaySocket.close(); } catch (_) {}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const stopSignals = ['SIGINT', 'SIGTERM'];
|
|
369
|
+
for (const sig of stopSignals) {
|
|
370
|
+
process.on(sig, closeRelay);
|
|
410
371
|
}
|
|
411
372
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
373
|
+
const openPromise = new Promise((resolve, reject) => {
|
|
374
|
+
const timer = setTimeout(() => {
|
|
375
|
+
reject(new Error('Timeout connexion relay'));
|
|
376
|
+
}, 15000);
|
|
377
|
+
|
|
378
|
+
relaySocket.addEventListener('open', () => {
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
resolve();
|
|
381
|
+
});
|
|
382
|
+
relaySocket.addEventListener('error', (event) => {
|
|
383
|
+
clearTimeout(timer);
|
|
384
|
+
reject(new Error(`Echec connexion relay: ${event?.message || 'ws error'}`));
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
416
388
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
389
|
+
await openPromise;
|
|
390
|
+
|
|
391
|
+
const helloPromise = new Promise((resolve, reject) => {
|
|
392
|
+
const timer = setTimeout(() => reject(new Error('Handshake relay timeout')), 12000);
|
|
393
|
+
relaySocket.addEventListener('message', (event) => {
|
|
394
|
+
let msg = null;
|
|
395
|
+
try { msg = JSON.parse(String(event.data || '')); } catch (_) { return; }
|
|
396
|
+
if (!msg || typeof msg !== 'object') return;
|
|
397
|
+
if (msg.type === 'hello_ack' && msg.relayClientId) {
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
relayClientId = String(msg.relayClientId);
|
|
400
|
+
resolve();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
await helloPromise;
|
|
405
|
+
|
|
406
|
+
relaySocket.addEventListener('message', async (event) => {
|
|
407
|
+
let msg = null;
|
|
408
|
+
try { msg = JSON.parse(String(event.data || '')); } catch (_) { return; }
|
|
409
|
+
if (!msg || typeof msg !== 'object') return;
|
|
410
|
+
if (msg.type === 'heartbeat_ack') return;
|
|
411
|
+
if (msg.type !== 'stream_request') return;
|
|
412
|
+
if (String(msg.relayFileId || '') !== relayFileId) return;
|
|
413
|
+
const uploadUrl = String(msg.uploadUrl || '');
|
|
414
|
+
if (!uploadUrl) return;
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const stream = fs.createReadStream(absPath);
|
|
418
|
+
const response = await fetch(uploadUrl, {
|
|
419
|
+
method: 'POST',
|
|
420
|
+
headers: {
|
|
421
|
+
'Content-Type': mimeType,
|
|
422
|
+
'Content-Length': String(st.size)
|
|
423
|
+
},
|
|
424
|
+
body: stream,
|
|
425
|
+
duplex: 'half'
|
|
426
|
+
});
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
const reason = await response.text().catch(() => '');
|
|
429
|
+
console.error(`Relay upload failed: HTTP ${response.status} ${reason}`.trim());
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
console.error(`Relay upload error: ${err.message}`);
|
|
433
|
+
}
|
|
423
434
|
});
|
|
424
435
|
|
|
425
|
-
const
|
|
436
|
+
const heartbeat = setInterval(() => {
|
|
437
|
+
if (relaySocket.readyState === WebSocket.OPEN) {
|
|
438
|
+
relaySocket.send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));
|
|
439
|
+
}
|
|
440
|
+
}, 15000);
|
|
441
|
+
|
|
426
442
|
const payload = {
|
|
427
443
|
fileName,
|
|
428
444
|
fileSize: st.size,
|
|
429
445
|
mimeType,
|
|
430
|
-
|
|
446
|
+
relayClientId,
|
|
447
|
+
relayFileId
|
|
431
448
|
};
|
|
432
449
|
if (limits !== undefined && limits > 0) payload.maxDownloads = limits;
|
|
433
450
|
if (temps !== undefined && temps > 0) payload.expiresInDays = temps;
|
|
@@ -442,28 +459,27 @@ async function shareFile(positionals, flags) {
|
|
|
442
459
|
});
|
|
443
460
|
|
|
444
461
|
const share = created?.share || {};
|
|
445
|
-
console.log(`Partage
|
|
462
|
+
console.log(`Partage relay cree pour: ${fileName}`);
|
|
446
463
|
if (share.url) console.log(`URL Shard: ${share.url}`);
|
|
447
|
-
console.log(`URL agent locale: ${directDownloadUrl}`);
|
|
448
464
|
if (share.token) console.log(`Token: ${share.token}`);
|
|
449
|
-
console.log(`
|
|
465
|
+
console.log(`Relay client id: ${relayClientId}`);
|
|
450
466
|
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
451
467
|
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
452
468
|
console.log('Laisse cette commande ouverte tant que le partage doit fonctionner.');
|
|
453
469
|
|
|
454
|
-
const stopSignals = ['SIGINT', 'SIGTERM'];
|
|
455
470
|
await new Promise((resolve) => {
|
|
456
|
-
|
|
457
|
-
for (const sig of stopSignals) process.off(sig, stop);
|
|
458
|
-
localServer.server.close(() => resolve());
|
|
459
|
-
};
|
|
460
|
-
for (const sig of stopSignals) process.on(sig, stop);
|
|
471
|
+
relaySocket.addEventListener('close', () => resolve());
|
|
461
472
|
});
|
|
473
|
+
clearInterval(heartbeat);
|
|
474
|
+
for (const sig of stopSignals) {
|
|
475
|
+
process.off(sig, closeRelay);
|
|
476
|
+
}
|
|
462
477
|
return;
|
|
463
478
|
} catch (error) {
|
|
464
|
-
|
|
465
|
-
|
|
479
|
+
for (const sig of stopSignals) {
|
|
480
|
+
process.off(sig, closeRelay);
|
|
466
481
|
}
|
|
482
|
+
closeRelay();
|
|
467
483
|
throw error;
|
|
468
484
|
}
|
|
469
485
|
}
|