@iksdev/shard-cli 0.1.9 → 0.1.11
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 +4 -4
- package/bin/shard.js +104 -116
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ 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
30
|
shard share ./MonFichier.mp4 --server https://shard-0ow4.onrender.com --limits 0 --temps 0
|
|
@@ -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,6 +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
|
-
- Par défaut `shard share`
|
|
55
|
-
-
|
|
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
56
|
- Utilise `--upload` pour revenir au mode historique (upload serveur).
|
package/bin/shard.js
CHANGED
|
@@ -4,9 +4,8 @@ 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
|
-
const
|
|
8
|
+
const { WebSocket } = require('ws');
|
|
10
9
|
const { Readable } = require('stream');
|
|
11
10
|
const { pipeline } = require('stream/promises');
|
|
12
11
|
|
|
@@ -23,7 +22,7 @@ Usage:
|
|
|
23
22
|
shard login --username <name> --password <pass> [--server <url>]
|
|
24
23
|
shard whoami [--server <url>]
|
|
25
24
|
shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]
|
|
26
|
-
shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--
|
|
25
|
+
shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]
|
|
27
26
|
shard logout
|
|
28
27
|
shard config show
|
|
29
28
|
shard config set-server <url>
|
|
@@ -312,79 +311,19 @@ function parseOptionalPositiveInt(raw, flagName) {
|
|
|
312
311
|
return n;
|
|
313
312
|
}
|
|
314
313
|
|
|
315
|
-
function
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
function toWebSocketUrl(serverUrl, token) {
|
|
315
|
+
const url = new URL(serverUrl);
|
|
316
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
317
|
+
url.pathname = '/api/relay/ws';
|
|
318
|
+
url.search = '';
|
|
319
|
+
url.searchParams.set('token', token);
|
|
320
|
+
return url.toString();
|
|
319
321
|
}
|
|
320
322
|
|
|
321
|
-
async function startManagedTunnel(port) {
|
|
322
|
-
const tunnel = await localtunnel({ port });
|
|
323
|
-
const publicUrl = normalizePublicUrl(tunnel?.url || '');
|
|
324
|
-
if (!publicUrl) {
|
|
325
|
-
throw new Error('Tunnel demarre mais URL publique manquante');
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
publicUrl,
|
|
329
|
-
close: async () => {
|
|
330
|
-
try {
|
|
331
|
-
await tunnel.close();
|
|
332
|
-
} catch (_) {
|
|
333
|
-
// ignore close errors
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function startLocalSingleFileServer({ filePath, fileName, mimeType, port, accessKey }) {
|
|
340
|
-
const safeName = encodeURIComponent(fileName);
|
|
341
|
-
const routePath = `/download/${accessKey}`;
|
|
342
|
-
|
|
343
|
-
const server = http.createServer((req, res) => {
|
|
344
|
-
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
|
345
|
-
|
|
346
|
-
if (requestUrl.pathname === '/health') {
|
|
347
|
-
res.statusCode = 200;
|
|
348
|
-
res.setHeader('Content-Type', 'application/json');
|
|
349
|
-
res.end(JSON.stringify({ ok: true, mode: 'local-share', file: fileName }));
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (requestUrl.pathname !== routePath) {
|
|
354
|
-
res.statusCode = 404;
|
|
355
|
-
res.setHeader('Content-Type', 'application/json');
|
|
356
|
-
res.end(JSON.stringify({ error: 'Not found' }));
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const stream = fs.createReadStream(filePath);
|
|
361
|
-
res.statusCode = 200;
|
|
362
|
-
res.setHeader('Content-Type', mimeType || 'application/octet-stream');
|
|
363
|
-
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${safeName}`);
|
|
364
|
-
stream.on('error', () => {
|
|
365
|
-
if (!res.headersSent) {
|
|
366
|
-
res.statusCode = 500;
|
|
367
|
-
res.end('Read error');
|
|
368
|
-
} else {
|
|
369
|
-
res.destroy();
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
stream.pipe(res);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
return new Promise((resolve, reject) => {
|
|
376
|
-
server.once('error', reject);
|
|
377
|
-
server.listen(port, '0.0.0.0', () => {
|
|
378
|
-
server.removeListener('error', reject);
|
|
379
|
-
resolve({ server, routePath });
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
323
|
async function shareFile(positionals, flags) {
|
|
385
324
|
const target = positionals[0];
|
|
386
325
|
if (!target) {
|
|
387
|
-
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--
|
|
326
|
+
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]');
|
|
388
327
|
}
|
|
389
328
|
|
|
390
329
|
const absPath = path.resolve(process.cwd(), target);
|
|
@@ -414,42 +353,99 @@ async function shareFile(positionals, flags) {
|
|
|
414
353
|
|
|
415
354
|
const fileName = path.basename(absPath);
|
|
416
355
|
if (localMode) {
|
|
417
|
-
const port = Math.max(parseInt(flags.port || process.env.SHARD_LOCAL_PORT || '8787', 10) || 8787, 1);
|
|
418
|
-
const accessKey = crypto.randomBytes(18).toString('base64url');
|
|
419
356
|
const mimeType = guessMime(absPath);
|
|
420
|
-
|
|
421
|
-
|
|
357
|
+
const relayFileId = crypto.randomBytes(12).toString('base64url');
|
|
358
|
+
const wsUrl = toWebSocketUrl(server, token);
|
|
359
|
+
const relaySocket = new WebSocket(wsUrl);
|
|
360
|
+
|
|
361
|
+
let relayClientId = '';
|
|
362
|
+
let closed = false;
|
|
363
|
+
const closeRelay = () => {
|
|
364
|
+
if (closed) return;
|
|
365
|
+
closed = true;
|
|
366
|
+
try { relaySocket.close(); } catch (_) {}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const stopSignals = ['SIGINT', 'SIGTERM'];
|
|
370
|
+
for (const sig of stopSignals) {
|
|
371
|
+
process.on(sig, closeRelay);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const openPromise = new Promise((resolve, reject) => {
|
|
375
|
+
const timer = setTimeout(() => {
|
|
376
|
+
reject(new Error('Timeout connexion relay'));
|
|
377
|
+
}, 15000);
|
|
378
|
+
|
|
379
|
+
relaySocket.on('open', () => {
|
|
380
|
+
clearTimeout(timer);
|
|
381
|
+
resolve();
|
|
382
|
+
});
|
|
383
|
+
relaySocket.on('error', (event) => {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
reject(new Error(`Echec connexion relay: ${event?.message || 'ws error'}`));
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
422
389
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
390
|
+
await openPromise;
|
|
391
|
+
|
|
392
|
+
const helloPromise = new Promise((resolve, reject) => {
|
|
393
|
+
const timer = setTimeout(() => reject(new Error('Handshake relay timeout')), 12000);
|
|
394
|
+
relaySocket.on('message', (event) => {
|
|
395
|
+
let msg = null;
|
|
396
|
+
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
397
|
+
if (!msg || typeof msg !== 'object') return;
|
|
398
|
+
if (msg.type === 'hello_ack' && msg.relayClientId) {
|
|
399
|
+
clearTimeout(timer);
|
|
400
|
+
relayClientId = String(msg.relayClientId);
|
|
401
|
+
resolve();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
429
404
|
});
|
|
430
|
-
|
|
431
|
-
if (!publicUrl) {
|
|
432
|
-
console.log(`Aucune URL publique fournie, creation automatique du tunnel (port ${port})...`);
|
|
433
|
-
tunnel = await startManagedTunnel(port);
|
|
434
|
-
publicUrl = tunnel.publicUrl;
|
|
435
|
-
}
|
|
405
|
+
await helloPromise;
|
|
436
406
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
407
|
+
relaySocket.on('message', async (event) => {
|
|
408
|
+
let msg = null;
|
|
409
|
+
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
410
|
+
if (!msg || typeof msg !== 'object') return;
|
|
411
|
+
if (msg.type === 'heartbeat_ack') return;
|
|
412
|
+
if (msg.type !== 'stream_request') return;
|
|
413
|
+
if (String(msg.relayFileId || '') !== relayFileId) return;
|
|
414
|
+
const uploadUrl = String(msg.uploadUrl || '');
|
|
415
|
+
if (!uploadUrl) return;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const stream = fs.createReadStream(absPath);
|
|
419
|
+
const response = await fetch(uploadUrl, {
|
|
420
|
+
method: 'POST',
|
|
421
|
+
headers: {
|
|
422
|
+
'Content-Type': mimeType,
|
|
423
|
+
'Content-Length': String(st.size)
|
|
424
|
+
},
|
|
425
|
+
body: stream,
|
|
426
|
+
duplex: 'half'
|
|
427
|
+
});
|
|
428
|
+
if (!response.ok) {
|
|
429
|
+
const reason = await response.text().catch(() => '');
|
|
430
|
+
console.error(`Relay upload failed: HTTP ${response.status} ${reason}`.trim());
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
console.error(`Relay upload error: ${err.message}`);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const heartbeat = setInterval(() => {
|
|
438
|
+
if (relaySocket.readyState === WebSocket.OPEN) {
|
|
439
|
+
relaySocket.send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));
|
|
440
|
+
}
|
|
441
|
+
}, 15000);
|
|
446
442
|
|
|
447
|
-
const directDownloadUrl = `${publicUrl}${localServer.routePath}`;
|
|
448
443
|
const payload = {
|
|
449
444
|
fileName,
|
|
450
445
|
fileSize: st.size,
|
|
451
446
|
mimeType,
|
|
452
|
-
|
|
447
|
+
relayClientId,
|
|
448
|
+
relayFileId
|
|
453
449
|
};
|
|
454
450
|
if (limits !== undefined && limits > 0) payload.maxDownloads = limits;
|
|
455
451
|
if (temps !== undefined && temps > 0) payload.expiresInDays = temps;
|
|
@@ -464,35 +460,27 @@ async function shareFile(positionals, flags) {
|
|
|
464
460
|
});
|
|
465
461
|
|
|
466
462
|
const share = created?.share || {};
|
|
467
|
-
console.log(`Partage
|
|
463
|
+
console.log(`Partage relay cree pour: ${fileName}`);
|
|
468
464
|
if (share.url) console.log(`URL Shard: ${share.url}`);
|
|
469
|
-
console.log(`URL publique: ${publicUrl}`);
|
|
470
|
-
console.log(`URL agent locale: ${directDownloadUrl}`);
|
|
471
465
|
if (share.token) console.log(`Token: ${share.token}`);
|
|
472
|
-
console.log(`
|
|
466
|
+
console.log(`Relay client id: ${relayClientId}`);
|
|
473
467
|
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
474
468
|
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
475
469
|
console.log('Laisse cette commande ouverte tant que le partage doit fonctionner.');
|
|
476
470
|
|
|
477
|
-
const stopSignals = ['SIGINT', 'SIGTERM'];
|
|
478
471
|
await new Promise((resolve) => {
|
|
479
|
-
|
|
480
|
-
for (const sig of stopSignals) process.off(sig, stop);
|
|
481
|
-
if (tunnel && tunnel.close) {
|
|
482
|
-
void tunnel.close();
|
|
483
|
-
}
|
|
484
|
-
localServer.server.close(() => resolve());
|
|
485
|
-
};
|
|
486
|
-
for (const sig of stopSignals) process.on(sig, stop);
|
|
472
|
+
relaySocket.on('close', () => resolve());
|
|
487
473
|
});
|
|
474
|
+
clearInterval(heartbeat);
|
|
475
|
+
for (const sig of stopSignals) {
|
|
476
|
+
process.off(sig, closeRelay);
|
|
477
|
+
}
|
|
488
478
|
return;
|
|
489
479
|
} catch (error) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
if (localServer && localServer.server) {
|
|
494
|
-
await new Promise((resolve) => localServer.server.close(() => resolve()));
|
|
480
|
+
for (const sig of stopSignals) {
|
|
481
|
+
process.off(sig, closeRelay);
|
|
495
482
|
}
|
|
483
|
+
closeRelay();
|
|
496
484
|
throw error;
|
|
497
485
|
}
|
|
498
486
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iksdev/shard-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "CLI pour synchroniser un dossier local avec Shard",
|
|
5
5
|
"bin": {
|
|
6
6
|
"shard": "bin/shard.js"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"check": "node --check bin/shard.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"
|
|
13
|
+
"ws": "^8.18.3"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=20.0.0"
|