Skip to content

Commit e872003

Browse files
authored
Merge pull request #36 from Cyber-Syntax:fix/broken-symlink
refactor: symlink handling in SizeCalculator
2 parents 85b7883 + cbcdbfd commit e872003

4 files changed

Lines changed: 67 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22
All notable changes to this project will be documented in this file. Commits automatically generated by github actions.
33

4+
## v0.6.1-beta
45
## v0.6.0-beta
56
### BREAKING CHANGES
67
- The configuration file format has been changed from JSON to INI. Now located at `~/.config/autotarcompress/config.conf`. Please migrate your existing configuration accordingly.

autotarcompress/utils.py

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import os
99
from pathlib import Path
1010

11+
BYTES_IN_KB = 1024.0
1112

1213
class SizeCalculator:
1314
"""Calculate and display total size of backup directories."""
@@ -30,13 +31,13 @@ def calculate_total_size(self) -> int:
3031
int: Total size in bytes.
3132
3233
"""
33-
print("\n\U0001F4C2 **Backup Size Summary**")
34+
print("\n\U0001f4c2 **Backup Size Summary**")
3435
print("=" * 40)
3536
total: int = 0
3637
for directory in self.directories:
3738
dir_size: int = self._calculate_directory_size(directory)
3839
total += dir_size
39-
print(f"\U0001F4C1 {directory}: {self._format_size(dir_size)}")
40+
print(f"\U0001f4c1 {directory}: {self._format_size(dir_size)}")
4041
print("=" * 40)
4142
print(f"\u2705 Total Backup Size: {self._format_size(total)}\n")
4243
return total
@@ -62,30 +63,49 @@ def _calculate_directory_size(self, directory: Path) -> int:
6263
file_path = root_path / file
6364
if self._should_ignore(file_path):
6465
continue
65-
try:
66-
total += file_path.stat().st_size
67-
except OSError as e:
68-
logging.warning(
69-
"\u26A0\uFE0F Error accessing file %s: %s", file_path, e
70-
)
71-
except Exception as e:
72-
logging.warning(
73-
"\u26A0\uFE0F Error accessing directory %s: %s", directory, e
74-
)
66+
67+
# Handle symlinks properly
68+
if file_path.is_symlink():
69+
try:
70+
# Check if symlink target exists
71+
if file_path.exists():
72+
# Valid symlink, get size of target
73+
total += file_path.stat().st_size
74+
else:
75+
# Broken symlink, skip silently
76+
logging.debug(
77+
"Skipping broken symlink: %s -> %s",
78+
file_path,
79+
file_path.readlink(),
80+
)
81+
except OSError as e:
82+
logging.debug("Error handling symlink %s: %s", file_path, e)
83+
else:
84+
# Regular file
85+
try:
86+
total += file_path.stat().st_size
87+
except OSError as e:
88+
logging.warning(
89+
"\u26a0\ufe0f Error accessing file %s: %s", file_path, e
90+
)
91+
except OSError as e:
92+
logging.warning("\u26a0\ufe0f Error accessing directory %s: %s", directory, e)
7593
return total
7694

7795
def _should_ignore(self, path: Path | str) -> bool:
7896
"""Return True if path should be ignored based on ignore list.
7997
8098
Args:
8199
path (Path | str): File or directory path to check.
82-
The check is performed using the normalized path to avoid mismatches due to path formatting.
100+
The check is performed using the normalized path to avoid mismatches
101+
due to path formatting.
83102
84103
Args:
85104
path: The file or directory path to check.
86105
87106
Returns:
88-
True if the path starts with any of the ignore paths, False otherwise.
107+
True if the path starts with any of the ignore paths,
108+
False otherwise.
89109
90110
"""
91111
if isinstance(path, str):
@@ -108,8 +128,10 @@ def _format_size(self, size_in_bytes: int) -> str:
108128
The formatted size string.
109129
110130
"""
131+
size = float(size_in_bytes)
132+
111133
for unit in ["B", "KB", "MB", "GB", "TB"]:
112-
if size_in_bytes < 1024:
113-
return f"{size_in_bytes:.2f} {unit}"
114-
size_in_bytes /= 1024
115-
return f"{size_in_bytes:.2f} PB"
134+
if size < BYTES_IN_KB:
135+
return f"{size:.2f} {unit}"
136+
size /= BYTES_IN_KB
137+
return f"{size:.2f} PB"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = 'AutoTarCompress'
3-
version = '0.6.0-beta'
3+
version = '0.6.1-beta'
44
description = 'It downloads/updates appimages via GitHub API. It also validates the appimage with SHA256 and SHA512.'
55
requires-python = ">= 3.8"
66
dependencies = [

tests/test_utils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,11 @@ class MockStat:
100100
def __init__(self, size: int):
101101
self.st_size = size
102102

103-
with patch("pathlib.Path.stat") as mock_path_stat:
103+
with patch("pathlib.Path.stat") as mock_path_stat, patch(
104+
"pathlib.Path.is_symlink"
105+
) as mock_is_symlink:
104106
mock_path_stat.return_value = MockStat(FILE_SIZE)
107+
mock_is_symlink.return_value = False # Treat all as regular files
105108

106109
dirs = ["/test/dir"]
107110
ignore_list: list[str] = []
@@ -111,3 +114,24 @@ def __init__(self, size: int):
111114

112115
# Should have some size from mocked files
113116
assert total_size >= 0
117+
118+
def test_symlink_handling(self, tmp_path) -> None:
119+
"""Test that broken symlinks are handled gracefully."""
120+
# Create test files and symlinks
121+
regular_file = tmp_path / "regular.txt"
122+
regular_file.write_text("test content")
123+
124+
# Create a valid symlink
125+
valid_symlink = tmp_path / "valid_symlink"
126+
valid_symlink.symlink_to(regular_file)
127+
128+
# Create a broken symlink
129+
broken_symlink = tmp_path / "broken_symlink"
130+
broken_symlink.symlink_to("nonexistent_target")
131+
132+
calculator = SizeCalculator([str(tmp_path)], [])
133+
134+
# Should not raise an exception and should return size > 0
135+
# (from regular file and valid symlink)
136+
total_size = calculator.calculate_total_size()
137+
assert total_size > 0

0 commit comments

Comments
 (0)