@iksdev/shard-cli 0.1.9 → 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 +4 -4
- package/bin/shard.js +103 -116
- package/package.json +1 -4
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,7 @@ 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 localtunnel = require('localtunnel');
|
|
10
8
|
const { Readable } = require('stream');
|
|
11
9
|
const { pipeline } = require('stream/promises');
|
|
12
10
|
|
|
@@ -23,7 +21,7 @@ Usage:
|
|
|
23
21
|
shard login --username <name> --password <pass> [--server <url>]
|
|
24
22
|
shard whoami [--server <url>]
|
|
25
23
|
shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]
|
|
26
|
-
shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--
|
|
24
|
+
shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]
|
|
27
25
|
shard logout
|
|
28
26
|
shard config show
|
|
29
27
|
shard config set-server <url>
|
|
@@ -312,79 +310,19 @@ function parseOptionalPositiveInt(raw, flagName) {
|
|
|
312
310
|
return n;
|
|
313
311
|
}
|
|
314
312
|
|
|
315
|
-
function
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
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();
|
|
319
320
|
}
|
|
320
321
|
|
|
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
322
|
async function shareFile(positionals, flags) {
|
|
385
323
|
const target = positionals[0];
|
|
386
324
|
if (!target) {
|
|
387
|
-
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]');
|
|
388
326
|
}
|
|
389
327
|
|
|
390
328
|
const absPath = path.resolve(process.cwd(), target);
|
|
@@ -414,42 +352,99 @@ async function shareFile(positionals, flags) {
|
|
|
414
352
|
|
|
415
353
|
const fileName = path.basename(absPath);
|
|
416
354
|
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
355
|
const mimeType = guessMime(absPath);
|
|
420
|
-
|
|
421
|
-
|
|
356
|
+
const relayFileId = crypto.randomBytes(12).toString('base64url');
|
|
357
|
+
const wsUrl = toWebSocketUrl(server, token);
|
|
358
|
+
const relaySocket = new WebSocket(wsUrl);
|
|
359
|
+
|
|
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);
|
|
371
|
+
}
|
|
372
|
+
|
|
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
|
+
|
|
422
388
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
});
|
|
429
403
|
});
|
|
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
|
-
}
|
|
404
|
+
await helloPromise;
|
|
436
405
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const heartbeat = setInterval(() => {
|
|
437
|
+
if (relaySocket.readyState === WebSocket.OPEN) {
|
|
438
|
+
relaySocket.send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));
|
|
439
|
+
}
|
|
440
|
+
}, 15000);
|
|
446
441
|
|
|
447
|
-
const directDownloadUrl = `${publicUrl}${localServer.routePath}`;
|
|
448
442
|
const payload = {
|
|
449
443
|
fileName,
|
|
450
444
|
fileSize: st.size,
|
|
451
445
|
mimeType,
|
|
452
|
-
|
|
446
|
+
relayClientId,
|
|
447
|
+
relayFileId
|
|
453
448
|
};
|
|
454
449
|
if (limits !== undefined && limits > 0) payload.maxDownloads = limits;
|
|
455
450
|
if (temps !== undefined && temps > 0) payload.expiresInDays = temps;
|
|
@@ -464,35 +459,27 @@ async function shareFile(positionals, flags) {
|
|
|
464
459
|
});
|
|
465
460
|
|
|
466
461
|
const share = created?.share || {};
|
|
467
|
-
console.log(`Partage
|
|
462
|
+
console.log(`Partage relay cree pour: ${fileName}`);
|
|
468
463
|
if (share.url) console.log(`URL Shard: ${share.url}`);
|
|
469
|
-
console.log(`URL publique: ${publicUrl}`);
|
|
470
|
-
console.log(`URL agent locale: ${directDownloadUrl}`);
|
|
471
464
|
if (share.token) console.log(`Token: ${share.token}`);
|
|
472
|
-
console.log(`
|
|
465
|
+
console.log(`Relay client id: ${relayClientId}`);
|
|
473
466
|
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
474
467
|
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
475
468
|
console.log('Laisse cette commande ouverte tant que le partage doit fonctionner.');
|
|
476
469
|
|
|
477
|
-
const stopSignals = ['SIGINT', 'SIGTERM'];
|
|
478
470
|
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);
|
|
471
|
+
relaySocket.addEventListener('close', () => resolve());
|
|
487
472
|
});
|
|
473
|
+
clearInterval(heartbeat);
|
|
474
|
+
for (const sig of stopSignals) {
|
|
475
|
+
process.off(sig, closeRelay);
|
|
476
|
+
}
|
|
488
477
|
return;
|
|
489
478
|
} catch (error) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
if (localServer && localServer.server) {
|
|
494
|
-
await new Promise((resolve) => localServer.server.close(() => resolve()));
|
|
479
|
+
for (const sig of stopSignals) {
|
|
480
|
+
process.off(sig, closeRelay);
|
|
495
481
|
}
|
|
482
|
+
closeRelay();
|
|
496
483
|
throw error;
|
|
497
484
|
}
|
|
498
485
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iksdev/shard-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "CLI pour synchroniser un dossier local avec Shard",
|
|
5
5
|
"bin": {
|
|
6
6
|
"shard": "bin/shard.js"
|
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"check": "node --check bin/shard.js"
|
|
11
11
|
},
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"localtunnel": "^2.0.2"
|
|
14
|
-
},
|
|
15
12
|
"engines": {
|
|
16
13
|
"node": ">=20.0.0"
|
|
17
14
|
},
|