Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
58 changes: 40 additions & 18 deletions autotarcompress/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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
Expand All @@ -62,30 +63,49 @@ 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:
"""Return True if path should be ignored based on ignore list.

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):
Expand All @@ -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"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
26 changes: 25 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand All @@ -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