|
18 | 18 | from typing import Callable, List, NamedTuple |
19 | 19 | from urllib.parse import ParseResult |
20 | 20 |
|
21 | | -import procrunner |
22 | | - |
23 | 21 | from murfey.client.tui.status_bar import StatusBar |
24 | 22 | from murfey.util import Observer |
25 | 23 |
|
@@ -380,16 +378,19 @@ def parse_stdout(line: str): |
380 | 378 | return |
381 | 379 |
|
382 | 380 | def parse_stderr(line: str): |
383 | | - logger.warning(f"rsync stderr: {line!r}") |
| 381 | + if line.strip(): |
| 382 | + logger.warning(f"rsync stderr: {line!r}") |
384 | 383 |
|
385 | 384 | # Generate list of relative filenames for this batch of transferred files |
| 385 | + # Relative filenames will be safe to use on both Windows and Unix |
386 | 386 | relative_filenames: List[Path] = [] |
387 | 387 | for f in files: |
388 | 388 | try: |
389 | 389 | relative_filenames.append(f.relative_to(self._basepath)) |
390 | 390 | except ValueError: |
391 | 391 | raise ValueError(f"File '{f}' is outside of {self._basepath}") from None |
392 | 392 |
|
| 393 | + # Encode files to rsync as bytestring |
393 | 394 | if self._remove_files: |
394 | 395 | if self._required_substrings_for_removal: |
395 | 396 | rsync_stdin_remove = b"\n".join( |
@@ -418,52 +419,88 @@ def parse_stderr(line: str): |
418 | 419 | rsync_stdin = b"\n".join(os.fsencode(f) for f in relative_filenames) |
419 | 420 |
|
420 | 421 | # Create and run rsync subprocesses |
421 | | - # rsync commands to pass to subprocess |
| 422 | + # rsync default settings |
422 | 423 | rsync_cmd = [ |
423 | 424 | "rsync", |
424 | 425 | "-iiv", |
425 | 426 | "--times", |
426 | 427 | "--progress", |
427 | 428 | "--outbuf=line", |
428 | | - "--files-from=-", |
429 | | - "-p", # preserve permissions |
430 | | - "--chmod=D0750,F0750", # 4: Read, 2: Write, 1: Execute | User, Group, Others |
| 429 | + "--files-from=-", # '-' indicates reading from standard input |
| 430 | + # Needed as a pair to trigger permission modifications |
| 431 | + # Ref: https://serverfault.com/a/796341 |
| 432 | + "-p", |
| 433 | + "--chmod=D0750,F0750", # Use extended chmod format |
431 | 434 | ] |
| 435 | + # Add file locations |
432 | 436 | rsync_cmd.extend([".", self._remote]) |
433 | | - result: subprocess.CompletedProcess | None = None |
| 437 | + |
| 438 | + # Transfer files to destination |
| 439 | + result: subprocess.CompletedProcess[bytes] | None = None |
434 | 440 | success = True |
435 | 441 | if rsync_stdin: |
436 | | - result = procrunner.run( |
437 | | - rsync_cmd, |
438 | | - callback_stdout=parse_stdout, |
439 | | - callback_stderr=parse_stderr, |
440 | | - working_directory=str(self._basepath), |
441 | | - stdin=rsync_stdin, |
442 | | - print_stdout=False, |
443 | | - print_stderr=False, |
| 442 | + # Wrap rsync command in a bash command |
| 443 | + cmd = [ |
| 444 | + "bash", |
| 445 | + "-c", |
| 446 | + # rsync command passed in as a single string |
| 447 | + " ".join(rsync_cmd), |
| 448 | + ] |
| 449 | + result = subprocess.run( |
| 450 | + cmd, |
| 451 | + cwd=self._basepath, # As-is Path is fine |
| 452 | + capture_output=True, |
| 453 | + input=rsync_stdin, |
444 | 454 | ) |
445 | | - success = result.returncode == 0 if result else False |
446 | | - |
| 455 | + # Parse outputs |
| 456 | + for line in result.stdout.decode("utf-8", "replace").split("\n"): |
| 457 | + parse_stdout(line) |
| 458 | + for line in result.stderr.decode("utf-8", "replace").split("\n"): |
| 459 | + parse_stderr(line) |
| 460 | + success = result.returncode == 0 |
| 461 | + |
| 462 | + # Remove files from source |
447 | 463 | if rsync_stdin_remove: |
| 464 | + # Insert file removal flag before locations |
448 | 465 | rsync_cmd.insert(-2, "--remove-source-files") |
449 | | - result = procrunner.run( |
450 | | - rsync_cmd, |
451 | | - callback_stdout=parse_stdout, |
452 | | - callback_stderr=parse_stderr, |
453 | | - working_directory=str(self._basepath), |
454 | | - stdin=rsync_stdin_remove, |
455 | | - print_stdout=False, |
456 | | - print_stderr=False, |
| 466 | + # Wrap rsync command in a bash command |
| 467 | + cmd = [ |
| 468 | + "bash", |
| 469 | + "-c", |
| 470 | + # Pass rsync command as single string |
| 471 | + " ".join(rsync_cmd), |
| 472 | + ] |
| 473 | + result = subprocess.run( |
| 474 | + cmd, |
| 475 | + cwd=self._basepath, |
| 476 | + capture_output=True, |
| 477 | + input=rsync_stdin_remove, |
457 | 478 | ) |
458 | | - |
| 479 | + # Parse outputs |
| 480 | + for line in result.stdout.decode("utf-8", "replace").split("\n"): |
| 481 | + parse_stdout(line) |
| 482 | + for line in result.stderr.decode("utf-8", "replace").split("\n"): |
| 483 | + parse_stderr(line) |
| 484 | + # Leave it as a failure if the previous rsync subprocess failed |
459 | 485 | if success: |
460 | | - success = result.returncode == 0 if result else False |
| 486 | + success = result.returncode == 0 |
461 | 487 |
|
462 | 488 | self.notify(successful_updates, secondary=True) |
463 | 489 |
|
| 490 | + # Print out a summary message for each file transfer batch instead of individual messages |
| 491 | + # List out file paths as stored in memory to see if issue is due to file path mismatch |
| 492 | + if len(set(relative_filenames) - transfer_success) != 0: |
| 493 | + logger.debug( |
| 494 | + f"Files identified for transfer ({len(relative_filenames)}): {relative_filenames!r}" |
| 495 | + ) |
| 496 | + logger.debug( |
| 497 | + f"Files successfully transferred ({len(transfer_success)}): {list(transfer_success)!r}" |
| 498 | + ) |
| 499 | + |
464 | 500 | # Compare files from rsync stdout to original list to verify transfer |
465 | 501 | for f in set(relative_filenames) - transfer_success: |
466 | | - logger.warning(f"Transfer of file {f.name!r} considered a failure") |
| 502 | + # Mute individual file warnings; replace with summarised one above |
| 503 | + # logger.warning(f"Transfer of file {f.name!r} considered a failure") |
467 | 504 | self._files_transferred += 1 |
468 | 505 | current_outstanding = self.queue.unfinished_tasks - ( |
469 | 506 | self._files_transferred - previously_transferred |
|
0 commit comments