@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.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/bin/shard.js +105 -89
  3. 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 local (sans stockage serveur)
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 --local --public-url https://xxxx.trycloudflare.com --limits 0 --temps 0
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>] [--local --public-url <url> --port <n>]`
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
- - En mode `--local`, le fichier reste sur ton PC: le serveur stocke seulement metadata + token.
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>] [--local --public-url <url> --port <n>]
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 --local --public-url https://xxxx.trycloudflare.com --limits 0 --temps 0
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 normalizePublicUrl(input) {
314
- const raw = String(input || '').trim();
315
- if (!raw) return '';
316
- return raw.replace(/\/+$/, '');
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>] [--local --public-url <url> --port <n>]');
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.local);
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 publicUrl = normalizePublicUrl(flags['public-url'] || process.env.SHARD_PUBLIC_URL);
398
- if (!publicUrl) {
399
- throw new Error('Mode local: --public-url est requis (ou SHARD_PUBLIC_URL)');
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 publicUrlParsed;
403
- try {
404
- publicUrlParsed = new URL(publicUrl);
405
- } catch {
406
- throw new Error(`--public-url invalide: ${publicUrl}`);
407
- }
408
- if (!['http:', 'https:'].includes(publicUrlParsed.protocol)) {
409
- throw new Error('--public-url doit etre en http(s)');
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 port = Math.max(parseInt(flags.port || process.env.SHARD_LOCAL_PORT || '8787', 10) || 8787, 1);
413
- const accessKey = crypto.randomBytes(18).toString('base64url');
414
- const mimeType = guessMime(absPath);
415
- let localServer = null;
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
- localServer = await startLocalSingleFileServer({
418
- filePath: absPath,
419
- fileName,
420
- mimeType,
421
- port,
422
- accessKey
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 directDownloadUrl = `${publicUrl}${localServer.routePath}`;
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
- downloadUrl: directDownloadUrl
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 local cree pour: ${fileName}`);
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(`Agent local en ecoute sur: http://0.0.0.0:${port}${localServer.routePath}`);
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
- const stop = () => {
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
- if (localServer && localServer.server) {
465
- await new Promise((resolve) => localServer.server.close(() => resolve()));
479
+ for (const sig of stopSignals) {
480
+ process.off(sig, closeRelay);
466
481
  }
482
+ closeRelay();
467
483
  throw error;
468
484
  }
469
485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iksdev/shard-cli",
3
- "version": "0.1.8",
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"