@livedesk/client 0.1.90 → 0.1.92

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.
@@ -466,6 +466,76 @@ async function handleFileTransferCommand(options, payload = {}) {
466
466
  };
467
467
  }
468
468
 
469
+ async function handleFileTransferChunkCommand(options, payload = {}) {
470
+ const baseDir = normalizeDirectoryPath(payload.remoteDirectory || options.filesDir);
471
+ const name = sanitizePathSegment(payload.name, 'file');
472
+ const relativePath = sanitizeRelativeFilePath(payload.relativePath || name, name);
473
+ const targetPath = path.resolve(baseDir, relativePath);
474
+ const relativeFromBase = path.relative(baseDir, targetPath);
475
+ if (relativeFromBase.startsWith('..') || path.isAbsolute(relativeFromBase)) {
476
+ throw new Error(`Unsafe file path: ${relativePath}`);
477
+ }
478
+
479
+ const transferId = String(payload.transferId || '').replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
480
+ const offset = Math.max(0, Math.floor(Number(payload.offset) || 0));
481
+ const totalBytes = Math.max(0, Math.floor(Number(payload.totalBytes) || 0));
482
+ const final = payload.final === true;
483
+ const buffer = payload.dataBase64 ? Buffer.from(String(payload.dataBase64), 'base64') : Buffer.alloc(0);
484
+ if (!transferId || offset > totalBytes || offset + buffer.length > totalBytes || buffer.length > 512 * 1024) {
485
+ throw new Error('Invalid file transfer chunk.');
486
+ }
487
+ if (buffer.length === 0 && !(final && totalBytes === 0 && offset === 0)) {
488
+ throw new Error('Empty file chunks are not allowed.');
489
+ }
490
+
491
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
492
+ const tempPath = `${targetPath}.livedesk-${transferId}.part`;
493
+ const handle = await fs.open(tempPath, offset === 0 ? 'w+' : 'r+');
494
+ let completedSize = 0;
495
+ try {
496
+ const before = await handle.stat();
497
+ if (offset > before.size) {
498
+ throw new Error(`File chunk gap detected at ${offset}; current length is ${before.size}.`);
499
+ }
500
+ if (buffer.length > 0) {
501
+ await handle.write(buffer, 0, buffer.length, offset);
502
+ }
503
+ const after = await handle.stat();
504
+ completedSize = after.size;
505
+ if (final) {
506
+ await handle.sync();
507
+ if (after.size !== totalBytes) {
508
+ throw new Error(`File transfer is incomplete (${after.size}/${totalBytes}).`);
509
+ }
510
+ }
511
+ } finally {
512
+ await handle.close();
513
+ }
514
+
515
+ if (final) {
516
+ await fs.rm(targetPath, { force: true });
517
+ await fs.rename(tempPath, targetPath);
518
+ const lastModified = Number(payload.lastModified || 0);
519
+ if (lastModified > 0) {
520
+ const modifiedAt = new Date(lastModified);
521
+ await fs.utimes(targetPath, modifiedAt, modifiedAt).catch(() => undefined);
522
+ }
523
+ }
524
+
525
+ return {
526
+ kind: 'file.transfer.chunk',
527
+ transferId,
528
+ status: final ? 'completed' : 'receiving',
529
+ relativePath: relativePath.split(path.sep).join('/'),
530
+ offset,
531
+ bytes: buffer.length,
532
+ totalBytes,
533
+ completedSize,
534
+ final,
535
+ completedAt: final ? new Date().toISOString() : null
536
+ };
537
+ }
538
+
469
539
  function clampNumber(value, min, max, fallback) {
470
540
  const number = Number(value);
471
541
  if (!Number.isFinite(number)) {
@@ -996,6 +1066,24 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
996
1066
  return;
997
1067
  }
998
1068
 
1069
+ if (command === 'file.transfer.chunk') {
1070
+ try {
1071
+ const result = await handleFileTransferChunkCommand(options, message.payload || {});
1072
+ writeJsonLine(socket, {
1073
+ type: 'command.result',
1074
+ commandId: message.commandId,
1075
+ result
1076
+ });
1077
+ } catch (error) {
1078
+ writeJsonLine(socket, {
1079
+ type: 'command.result',
1080
+ commandId: message.commandId,
1081
+ error: error?.message || String(error)
1082
+ });
1083
+ }
1084
+ return;
1085
+ }
1086
+
999
1087
  if (command === 'audio.start' || command === 'audio.stop') {
1000
1088
  writeJsonLine(socket, {
1001
1089
  type: 'command.result',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.90",
3
+ "version": "0.1.92",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {