From cbcdbfd64c3e1674ba7565b88ce4222ca012ba61 Mon Sep 17 00:00:00 2001 From: Cyber-Syntax <115875369+Cyber-Syntax@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:41:30 +0300 Subject: [PATCH] refactor: enhance symlink handling in SizeCalculator --- CHANGELOG.md | 1 + autotarcompress/utils.py | 58 +++++++++++++++++++++++++++------------- pyproject.toml | 2 +- tests/test_utils.py | 26 +++++++++++++++++- 4 files changed, 67 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7813fdf..aa88331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog All notable changes to this project will be documented in this file. Commits automatically generated by github actions. +## v0.6.1-beta ## v0.6.0-beta ### BREAKING CHANGES - The configuration file format has been changed from JSON to INI. Now located at `~/.config/autotarcompress/config.conf`. Please migrate your existing configuration accordingly. diff --git a/autotarcompress/utils.py b/autotarcompress/utils.py index 15a4d07..d87386d 100644 --- a/autotarcompress/utils.py +++ b/autotarcompress/utils.py @@ -8,6 +8,7 @@ import os from pathlib import Path +BYTES_IN_KB = 1024.0 class SizeCalculator: """Calculate and display total size of backup directories.""" @@ -30,13 +31,13 @@ def calculate_total_size(self) -> int: int: Total size in bytes. """ - print("\n\U0001F4C2 **Backup Size Summary**") + print("\n\U0001f4c2 **Backup Size Summary**") print("=" * 40) total: int = 0 for directory in self.directories: dir_size: int = self._calculate_directory_size(directory) total += dir_size - print(f"\U0001F4C1 {directory}: {self._format_size(dir_size)}") + print(f"\U0001f4c1 {directory}: {self._format_size(dir_size)}") print("=" * 40) print(f"\u2705 Total Backup Size: {self._format_size(total)}\n") return total @@ -62,16 +63,33 @@ def _calculate_directory_size(self, directory: Path) -> int: file_path = root_path / file if self._should_ignore(file_path): continue - try: - total += file_path.stat().st_size - except OSError as e: - logging.warning( - "\u26A0\uFE0F Error accessing file %s: %s", file_path, e - ) - except Exception as e: - logging.warning( - "\u26A0\uFE0F Error accessing directory %s: %s", directory, e - ) + + # Handle symlinks properly + if file_path.is_symlink(): + try: + # Check if symlink target exists + if file_path.exists(): + # Valid symlink, get size of target + total += file_path.stat().st_size + else: + # Broken symlink, skip silently + logging.debug( + "Skipping broken symlink: %s -> %s", + file_path, + file_path.readlink(), + ) + except OSError as e: + logging.debug("Error handling symlink %s: %s", file_path, e) + else: + # Regular file + try: + total += file_path.stat().st_size + except OSError as e: + logging.warning( + "\u26a0\ufe0f Error accessing file %s: %s", file_path, e + ) + except OSError as e: + logging.warning("\u26a0\ufe0f Error accessing directory %s: %s", directory, e) return total def _should_ignore(self, path: Path | str) -> bool: @@ -79,13 +97,15 @@ def _should_ignore(self, path: Path | str) -> bool: Args: path (Path | str): File or directory path to check. - The check is performed using the normalized path to avoid mismatches due to path formatting. + The check is performed using the normalized path to avoid mismatches + due to path formatting. Args: path: The file or directory path to check. Returns: - True if the path starts with any of the ignore paths, False otherwise. + True if the path starts with any of the ignore paths, + False otherwise. """ if isinstance(path, str): @@ -108,8 +128,10 @@ def _format_size(self, size_in_bytes: int) -> str: The formatted size string. """ + size = float(size_in_bytes) + for unit in ["B", "KB", "MB", "GB", "TB"]: - if size_in_bytes < 1024: - return f"{size_in_bytes:.2f} {unit}" - size_in_bytes /= 1024 - return f"{size_in_bytes:.2f} PB" + if size < BYTES_IN_KB: + return f"{size:.2f} {unit}" + size /= BYTES_IN_KB + return f"{size:.2f} PB" diff --git a/pyproject.toml b/pyproject.toml index f176acc..0f0baf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = 'AutoTarCompress' -version = '0.6.0-beta' +version = '0.6.1-beta' description = 'It downloads/updates appimages via GitHub API. It also validates the appimage with SHA256 and SHA512.' requires-python = ">= 3.8" dependencies = [ diff --git a/tests/test_utils.py b/tests/test_utils.py index 38678f7..eea38dd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -100,8 +100,11 @@ class MockStat: def __init__(self, size: int): self.st_size = size - with patch("pathlib.Path.stat") as mock_path_stat: + with patch("pathlib.Path.stat") as mock_path_stat, patch( + "pathlib.Path.is_symlink" + ) as mock_is_symlink: mock_path_stat.return_value = MockStat(FILE_SIZE) + mock_is_symlink.return_value = False # Treat all as regular files dirs = ["/test/dir"] ignore_list: list[str] = [] @@ -111,3 +114,24 @@ def __init__(self, size: int): # Should have some size from mocked files assert total_size >= 0 + + def test_symlink_handling(self, tmp_path) -> None: + """Test that broken symlinks are handled gracefully.""" + # Create test files and symlinks + regular_file = tmp_path / "regular.txt" + regular_file.write_text("test content") + + # Create a valid symlink + valid_symlink = tmp_path / "valid_symlink" + valid_symlink.symlink_to(regular_file) + + # Create a broken symlink + broken_symlink = tmp_path / "broken_symlink" + broken_symlink.symlink_to("nonexistent_target") + + calculator = SizeCalculator([str(tmp_path)], []) + + # Should not raise an exception and should return size > 0 + # (from regular file and valid symlink) + total_size = calculator.calculate_total_size() + assert total_size > 0