Skip to content

Commit 293b54c

Browse files
authored
Merge pull request #41 from Cyber-Syntax:fix/no-such-dir
fix: no such file or dir error by new validation logic
2 parents 1ee3b0c + d2fdce8 commit 293b54c

5 files changed

Lines changed: 290 additions & 48 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.7.1-beta
45
## v0.7.0-beta
56
### BREAKING CHANGES
67
This release introduces a new command-line interface (CLI) for AutoTarCompress, enabling users to perform all operations via terminal commands. The previous interactive menu mode still exists but may be deprecated in future releases. Users are encouraged to transition to the CLI for better automation and scripting capabilities. For detailed usage instructions, please refer to the updated documentation in [docs/wiki.md](docs/wiki.md).

autotarcompress/commands/backup.py

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717

1818
from autotarcompress.commands.command import Command
1919
from autotarcompress.config import BackupConfig
20-
from autotarcompress.utils import SizeCalculator
20+
from autotarcompress.utils import (
21+
SizeCalculator,
22+
ensure_backup_folder,
23+
validate_and_expand_paths,
24+
)
2125

2226

2327
class BackupCommand(Command):
@@ -40,15 +44,49 @@ def execute(self) -> bool:
4044
bool: True if backup succeeded, False otherwise.
4145
4246
"""
47+
# Validate and ensure backup directories
48+
existing_dirs, missing_dirs = validate_and_expand_paths(
49+
self.config.dirs_to_backup
50+
)
51+
if missing_dirs:
52+
# Log missing directories; no need to print to stdout here.
53+
self.logger.warning(
54+
"Some configured backup directories do not exist: %s",
55+
missing_dirs,
56+
)
57+
58+
# Use equality comparison instead of identity; lists with the same
59+
# contents should be compared by value.
60+
if existing_dirs != self.config.dirs_to_backup:
61+
self.logger.info(
62+
"Proceeding with existing directories only: %s",
63+
existing_dirs,
64+
)
65+
self.config.dirs_to_backup = existing_dirs
66+
67+
# Ensure backup folder exists
68+
try:
69+
backup_folder_path = ensure_backup_folder(
70+
self.config.backup_folder
71+
)
72+
self.config.backup_folder = str(backup_folder_path)
73+
self.logger.info(
74+
"Backup folder ensured at: %s",
75+
self.config.backup_folder,
76+
)
77+
except (OSError, PermissionError) as e:
78+
self.logger.error("Failed to ensure backup folder: %s", e)
79+
return False
80+
4381
if not self.config.dirs_to_backup:
44-
self._print_and_log(
45-
"No directories configured for backup. Skipping backup.", level="error"
82+
self.logger.error(
83+
"No directories configured for backup. Skipping backup."
4684
)
4785
return False
4886
total_size: int = self._calculate_total_size()
4987
if total_size == 0:
50-
self._print_and_log(
51-
"Total backup size is 0 bytes. Nothing to back up.", level="warning"
88+
self.logger.warning(
89+
"Total backup size is 0 bytes. Nothing to back up."
5290
)
5391
return False
5492
success: bool = self._run_backup_process(total_size)
@@ -63,7 +101,10 @@ def _calculate_total_size(self) -> int:
63101
int: Total size in bytes.
64102
65103
"""
66-
calculator = SizeCalculator(self.config.dirs_to_backup, self.config.ignore_list)
104+
calculator = SizeCalculator(
105+
self.config.dirs_to_backup,
106+
self.config.ignore_list,
107+
)
67108
return calculator.calculate_total_size()
68109

69110
def _run_backup_process(self, total_size: int) -> bool:
@@ -77,39 +118,56 @@ def _run_backup_process(self, total_size: int) -> bool:
77118
78119
"""
79120
if os.path.exists(self.config.backup_path):
80-
self._print_and_log(f"File already exists: {self.config.backup_path}", level="warning")
121+
print(f"File already exists: {self.config.backup_path}")
122+
self.logger.warning(
123+
"File already exists: %s",
124+
self.config.backup_path,
125+
)
81126
if not self._prompt_overwrite():
82-
self._print_and_log("Backup aborted by user due to existing file.", level="info")
127+
msg = "Backup aborted by user due to existing file."
128+
self.logger.info("%s", msg)
83129
return False
84130
try:
85131
os.remove(self.config.backup_path)
86-
self.logger.info("Removed existing backup file: %s", self.config.backup_path)
132+
self.logger.info(
133+
"Removed existing backup file: %s",
134+
self.config.backup_path,
135+
)
87136
# NOTE: Broad except is used here to ensure any file removal error
88137
# is caught during backup overwrite prompt. This is a critical IO
89138
# operation.
90139
except (OSError, PermissionError) as e:
91-
self._print_and_log(f"Failed to remove existing backup: {e}", level="error")
140+
self.logger.error("Failed to remove existing backup: %s", e)
92141
return False
93142

94143
cmd = self._build_tar_command()
95144
total_size_gb = total_size / 1024**3
96145

97-
self.logger.info("Starting backup to %s", self.config.backup_path)
98-
self.logger.info("Total size: %.2f GB", total_size_gb)
146+
self.logger.info(
147+
"Starting backup to %s",
148+
self.config.backup_path,
149+
)
150+
self.logger.info(
151+
"Total size: %.2f GB",
152+
total_size_gb,
153+
)
99154

100155
try:
101156
subprocess.run(cmd, shell=True, check=True)
102157
self.logger.info("Backup completed successfully")
103-
self._print_and_log("Backup completed successfully.", level="info")
104158
return True
105159
except subprocess.CalledProcessError as e:
106-
self._print_and_log(f"Backup failed: {e}", level="error")
160+
self.logger.error("Backup failed: %s", e)
107161
return False
108162

109163
def _build_tar_command(self) -> str:
110164
"""Build the tar+xz command string for backup."""
111-
exclude_options = " ".join(f"--exclude={path}" for path in self.config.ignore_list)
112-
dir_paths = [os.path.expanduser(path) for path in self.config.dirs_to_backup]
165+
exclude_options = " ".join(
166+
f"--exclude={path}" for path in self.config.ignore_list
167+
)
168+
dir_paths = [
169+
os.path.expanduser(path) for path in self.config.dirs_to_backup
170+
]
113171
quoted_paths = [shlex.quote(path) for path in dir_paths]
114172
cpu_count = os.cpu_count() or 1
115173
threads = max(1, cpu_count - 1)
@@ -124,18 +182,6 @@ def _prompt_overwrite(self) -> bool:
124182
response = input("Do you want to remove it? (y/n): ").strip().lower()
125183
return response == "y"
126184

127-
def _print_and_log(self, message: str, level: str = "info") -> None:
128-
"""Print and log a message at the specified level."""
129-
print(message)
130-
if level == "info":
131-
self.logger.info(message)
132-
elif level == "warning":
133-
self.logger.warning(message)
134-
elif level == "error":
135-
self.logger.error(message)
136-
else:
137-
self.logger.debug(message)
138-
139185
def _save_backup_info(self, total_size: int) -> None:
140186
"""Save backup information to last-backup-info.json."""
141187
try:
@@ -149,7 +195,9 @@ def _save_backup_info(self, total_size: int) -> None:
149195
}
150196

151197
# Save the info file in the backup folder
152-
info_file_path = Path(self.config.backup_folder) / "last-backup-info.json"
198+
info_file_path = (
199+
Path(self.config.backup_folder) / "last-backup-info.json"
200+
)
153201

154202
with open(info_file_path, "w", encoding="utf-8") as f:
155203
json.dump(backup_info, f, indent=2)

autotarcompress/utils.py

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,107 @@
88
import os
99
from pathlib import Path
1010

11+
logger = logging.getLogger(__name__)
12+
13+
14+
def validate_and_expand_paths(
15+
paths_to_check: list[str],
16+
) -> tuple[list[str], list[str]]:
17+
"""Validate and expand a list of candidate backup paths.
18+
19+
Args:
20+
paths_to_check (list[str]): Candidate paths which may contain
21+
shell user-expansions such as ``~``.
22+
23+
Returns:
24+
tuple[list[str], list[str]]: A tuple ``(existing_paths,
25+
missing_paths)``. ``existing_paths`` contains expanded absolute
26+
paths that exist on disk. ``missing_paths`` contains expanded
27+
absolute paths that do not exist.
28+
29+
Notes:
30+
This function does not raise; the caller decides whether to
31+
abort or proceed. Existing paths are returned as strings to
32+
simplify insertion into shell commands.
33+
34+
"""
35+
existing: list[str] = []
36+
missing: list[str] = []
37+
38+
# Iterate over the provided list, handling a possible ``None`` by
39+
# falling back to an empty sequence.
40+
for candidate in paths_to_check or []:
41+
if not candidate:
42+
continue
43+
candidate_path = Path(candidate).expanduser()
44+
if candidate_path.exists():
45+
existing.append(str(candidate_path))
46+
else:
47+
missing.append(str(candidate_path))
48+
49+
if missing:
50+
print(f"Some configured backup paths do not exist: {missing}")
51+
logger.warning(
52+
"Some configured backup paths do not exist: %s",
53+
missing,
54+
)
55+
56+
print("Proceeding with existing directories only.")
57+
logger.info(
58+
"Proceeding with existing directories only: %s",
59+
existing,
60+
)
61+
return existing, missing
62+
63+
64+
def ensure_backup_folder(folder: str) -> Path:
65+
"""Ensure the backup folder exists, creating it if necessary.
66+
67+
Args:
68+
folder (str): Path to the backup folder. May contain shell
69+
user-expansions such as ``~``.
70+
71+
Returns:
72+
pathlib.Path: Expanded ``Path`` pointing to the ensured folder.
73+
74+
Raises:
75+
OSError: If the folder cannot be created due to permission or
76+
filesystem errors.
77+
78+
"""
79+
path = Path(folder).expanduser()
80+
if not path.exists():
81+
print(f"Creating backup folder at {path}")
82+
logger.info(
83+
"Creating backup folder at %s",
84+
path,
85+
)
86+
path.mkdir(parents=True, exist_ok=True)
87+
return path
88+
89+
1190
BYTES_IN_KB = 1024.0
1291

92+
1393
class SizeCalculator:
1494
"""Calculate and display total size of backup directories."""
1595

1696
def __init__(self, directories: list[str], ignore_list: list[str]) -> None:
1797
"""Initialize SizeCalculator.
1898
1999
Args:
20-
directories (list[str]): Directories to include in size calculation.
21-
ignore_list (list[str]): Paths to ignore during calculation.
100+
directories (list[str]): Directories to include in size
101+
calculation.
102+
ignore_list (list[str]): Paths to ignore during
103+
calculation.
22104
23105
"""
24-
self.directories: list[Path] = [Path(os.path.expanduser(d)) for d in directories]
25-
self.ignore_list: list[Path] = [Path(os.path.expanduser(p)) for p in ignore_list]
106+
self.directories: list[Path] = [
107+
Path(os.path.expanduser(d)) for d in directories
108+
]
109+
self.ignore_list: list[Path] = [
110+
Path(os.path.expanduser(p)) for p in ignore_list
111+
]
26112

27113
def calculate_total_size(self) -> int:
28114
"""Sum the sizes of all directories, printing a summary.
@@ -31,7 +117,7 @@ def calculate_total_size(self) -> int:
31117
int: Total size in bytes.
32118
33119
"""
34-
print("\n\U0001f4c2 **Backup Size Summary**")
120+
print("\n\U0001f4c2 Backup Directories (Exist Only)")
35121
print("=" * 40)
36122
total: int = 0
37123
for directory in self.directories:
@@ -73,23 +159,33 @@ def _calculate_directory_size(self, directory: Path) -> int:
73159
total += file_path.stat().st_size
74160
else:
75161
# Broken symlink, skip silently
76-
logging.debug(
162+
logger.debug(
77163
"Skipping broken symlink: %s -> %s",
78164
file_path,
79165
file_path.readlink(),
80166
)
81167
except OSError as e:
82-
logging.debug("Error handling symlink %s: %s", file_path, e)
168+
logger.debug(
169+
"Error handling symlink %s: %s",
170+
file_path,
171+
e,
172+
)
83173
else:
84174
# Regular file
85175
try:
86176
total += file_path.stat().st_size
87177
except OSError as e:
88-
logging.warning(
89-
"\u26a0\ufe0f Error accessing file %s: %s", file_path, e
178+
logger.warning(
179+
"\u26a0\ufe0f Error accessing file %s: %s",
180+
file_path,
181+
e,
90182
)
91183
except OSError as e:
92-
logging.warning("\u26a0\ufe0f Error accessing directory %s: %s", directory, e)
184+
logger.warning(
185+
"\u26a0\ufe0f Error accessing directory %s: %s",
186+
directory,
187+
e,
188+
)
93189
return total
94190

95191
def _should_ignore(self, path: Path | str) -> bool:
@@ -114,8 +210,11 @@ def _should_ignore(self, path: Path | str) -> bool:
114210
# Convert to absolute path for consistent comparison
115211
absolute_path = path.absolute()
116212

213+
# Return True if the absolute path starts with any of the
214+
# ignored paths. Normalize both sides for reliable comparison.
117215
return any(
118-
str(absolute_path).startswith(str(ignored.absolute())) for ignored in self.ignore_list
216+
str(absolute_path).startswith(str(ignored.absolute()))
217+
for ignored in self.ignore_list
119218
)
120219

121220
def _format_size(self, size_in_bytes: int) -> str:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = 'AutoTarCompress'
3-
version = '0.7.0-beta'
3+
version = '0.7.1-beta'
44
maintainers = [{ name = "Cyber-Syntax" }]
55
description = 'It downloads/updates appimages via GitHub API. It also validates the appimage with SHA256 and SHA512.'
66
keywords = ["tar", "compress", "autotarcompress", "backup"]
@@ -59,7 +59,7 @@ exclude = [
5959
"__init__.py",
6060
]
6161

62-
line-length = 100
62+
line-length = 79
6363
indent-width = 4
6464
# target-version = [
6565
# 'py38',

0 commit comments

Comments
 (0)