Skip to content

Commit da14bd9

Browse files
committed
refactor: Address CodeRabbit review feedback
- manager.py: Harden directory permissions, atomic writes, robust metadata - cleaner.py: Add sudo -n flag, fix freed bytes calculation, use safe param - scheduler.py: Pass safe_mode to systemd/cron setup - cli.py: Add error message when no subcommand specified - test_cleanup_scanner.py: Reduce test file size from 150MB to 2MB - test_cleanup_cleaner.py: Fix test for safe mode behavior - CLEANUP_GUIDE.md: Fix markdownlint issues
1 parent d298e01 commit da14bd9

File tree

7 files changed

+95
-67
lines changed

7 files changed

+95
-67
lines changed

cortex/cleanup/cleaner.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ def clean_package_cache(self) -> int:
3939
if self.dry_run:
4040
return size_freed
4141

42-
# Run apt-get clean
43-
cmd = "sudo apt-get clean"
42+
# Run apt-get clean (use -n for non-interactive mode)
43+
cmd = "sudo -n apt-get clean"
4444
result = run_command(cmd, validate=True)
4545

4646
if result.success:
@@ -65,7 +65,8 @@ def remove_orphaned_packages(self, packages: List[str]) -> int:
6565
if self.dry_run:
6666
return 0 # Size is estimated in scanner
6767

68-
cmd = "sudo apt-get autoremove -y"
68+
# Use -n for non-interactive mode
69+
cmd = "sudo -n apt-get autoremove -y"
6970
result = run_command(cmd, validate=True)
7071

7172
freed_bytes = 0
@@ -122,33 +123,25 @@ def clean_temp_files(self, files: List[str]) -> int:
122123
filepath = Path(filepath_str)
123124
if not filepath.exists():
124125
continue
126+
127+
# Get size before any operation
128+
try:
129+
size = filepath.stat().st_size
130+
except OSError:
131+
size = 0
125132

126133
if self.dry_run:
127-
try:
128-
freed_bytes += filepath.stat().st_size
129-
except OSError:
130-
pass
134+
freed_bytes += size
131135
continue
132136

133137
# Move to quarantine
134138
item_id = self.manager.quarantine_file(str(filepath))
135139
if item_id:
136-
# We assume success means we freed the file size.
137-
# Ideally we should get the size from the manager or check before.
138-
# But for now we don't have the size unless we check again.
139-
# Let's assume the scanner's size is accurate enough for reporting,
140-
# or check size before quarantine if we really need to return exact freed bytes here.
141-
# But manager.quarantine_file moves it, so it's gone from original location.
142-
pass
140+
freed_bytes += size
143141
else:
144-
# Failed to quarantine
145-
pass
142+
logger.warning(f"Failed to quarantine temp file: {filepath}")
146143

147-
# Returning 0 here because calculating exact freed bytes per file during deletion
148-
# without re-statting every file (which might fail if moved) is complex.
149-
# The CLI can use the scan result for total potential, or we can improve this.
150-
# For now, let's return 0 and rely on the scan result or improve manager to return size.
151-
return freed_bytes
144+
return freed_bytes
152145

153146
def compress_logs(self, files: List[str]) -> int:
154147
"""
@@ -216,7 +209,9 @@ def run_cleanup(self, scan_results: List[ScanResult], safe: bool = True) -> Dict
216209
summary["Package Cache"] = self.clean_package_cache()
217210

218211
elif result.category == "Orphaned Packages":
219-
summary["Orphaned Packages"] = self.remove_orphaned_packages(result.items)
212+
# Only remove orphaned packages in non-safe mode
213+
if not safe:
214+
summary["Orphaned Packages"] = self.remove_orphaned_packages(result.items)
220215

221216
elif result.category == "Temporary Files":
222217
summary["Temporary Files"] = self.clean_temp_files(result.items)

cortex/cleanup/manager.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,42 @@ class CleanupManager:
2929
"""
3030
Manages the quarantine (undo) system for cleaned files.
3131
"""
32-
def __init__(self):
32+
def __init__(self) -> None:
33+
"""Initialize quarantine storage and metadata paths."""
3334
self.quarantine_dir = Path.home() / ".cortex" / "trash"
3435
self.metadata_file = self.quarantine_dir / "metadata.json"
3536
self._ensure_dir()
3637

37-
def _ensure_dir(self):
38+
def _ensure_dir(self) -> None:
3839
"""Ensure quarantine directory exists with secure permissions."""
39-
if not self.quarantine_dir.exists():
40-
self.quarantine_dir.mkdir(parents=True, mode=0o700)
40+
self.quarantine_dir.mkdir(parents=True, exist_ok=True)
41+
try:
42+
# Ensure privacy even if pre-existing
43+
self.quarantine_dir.chmod(0o700)
44+
except OSError:
45+
# Best-effort; callers still handle failures later
46+
pass
4147

4248
def _load_metadata(self) -> Dict[str, dict]:
4349
"""Load metadata from JSON file."""
4450
if not self.metadata_file.exists():
4551
return {}
4652
try:
47-
with open(self.metadata_file, 'r') as f:
53+
with self.metadata_file.open("r", encoding="utf-8") as f:
4854
return json.load(f)
49-
except json.JSONDecodeError:
55+
except (json.JSONDecodeError, OSError, ValueError):
5056
return {}
5157

52-
def _save_metadata(self, metadata: Dict[str, dict]):
53-
"""Save metadata to JSON file."""
54-
with open(self.metadata_file, 'w') as f:
58+
def _save_metadata(self, metadata: Dict[str, dict]) -> None:
59+
"""Save metadata to JSON file atomically."""
60+
tmp = self.metadata_file.with_suffix(".json.tmp")
61+
with tmp.open("w", encoding="utf-8") as f:
5562
json.dump(metadata, f, indent=2)
63+
os.replace(tmp, self.metadata_file)
64+
try:
65+
self.metadata_file.chmod(0o600)
66+
except OSError:
67+
pass
5668

5769
def quarantine_file(self, filepath_str: str) -> Optional[str]:
5870
"""
@@ -147,20 +159,27 @@ def list_items(self) -> List[QuarantineItem]:
147159
items.append(QuarantineItem(**v))
148160
return sorted(items, key=lambda x: x.timestamp, reverse=True)
149161

150-
def cleanup_old_items(self, days: int = 30):
162+
def cleanup_old_items(self, days: int = 30) -> None:
151163
"""
152164
Remove quarantine items older than X days.
153165
154166
Args:
155167
days (int): Age in days to expire items.
168+
169+
Raises:
170+
ValueError: If days is negative.
156171
"""
172+
if days < 0:
173+
raise ValueError("days must be >= 0")
174+
157175
metadata = self._load_metadata()
158176
now = time.time()
159177
cutoff = now - (days * 86400)
160178

161179
to_remove = []
162180
for item_id, data in metadata.items():
163-
if data['timestamp'] < cutoff:
181+
ts = data.get("timestamp")
182+
if isinstance(ts, (int, float)) and ts < cutoff:
164183
to_remove.append(item_id)
165184

166185
for item_id in to_remove:
@@ -174,3 +193,4 @@ def cleanup_old_items(self, days: int = 30):
174193

175194
if to_remove:
176195
self._save_metadata(metadata)
196+

cortex/cleanup/scheduler.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def enable_schedule(
137137
)
138138

139139
# Try to set up systemd timer first
140-
systemd_result = self._setup_systemd_timer(interval)
140+
systemd_result = self._setup_systemd_timer(interval, safe_mode)
141141
if systemd_result["success"]:
142142
self.save_config(config)
143143
return {
@@ -147,7 +147,7 @@ def enable_schedule(
147147
}
148148

149149
# Fall back to cron
150-
cron_result = self._setup_cron(interval)
150+
cron_result = self._setup_cron(interval, safe_mode)
151151
if cron_result["success"]:
152152
self.save_config(config)
153153
return {
@@ -237,12 +237,13 @@ def _get_cron_schedule(self, interval: ScheduleInterval) -> str:
237237
else: # monthly
238238
return "0 3 1 * *" # 3 AM 1st of month
239239

240-
def _setup_systemd_timer(self, interval: ScheduleInterval) -> Dict[str, Any]:
240+
def _setup_systemd_timer(self, interval: ScheduleInterval, safe_mode: bool = True) -> Dict[str, Any]:
241241
"""
242242
Set up systemd timer for scheduling.
243243
244244
Args:
245245
interval: Scheduling interval.
246+
safe_mode: If True, run with --safe flag; otherwise --force.
246247
247248
Returns:
248249
dict: Result with success status.
@@ -258,14 +259,17 @@ def _setup_systemd_timer(self, interval: ScheduleInterval) -> Dict[str, Any]:
258259
if result.returncode not in (0, 1): # 1 is "degraded" which is OK
259260
return {"success": False, "error": "systemd not available"}
260261

262+
# Determine cleanup mode flag
263+
mode_flag = "--safe" if safe_mode else "--force"
264+
261265
# Create service file
262266
service_content = f"""[Unit]
263267
Description=Cortex Disk Cleanup Service
264268
After=network.target
265269
266270
[Service]
267271
Type=oneshot
268-
ExecStart=/usr/bin/env cortex cleanup run --safe --yes
272+
ExecStart=/usr/bin/env cortex cleanup run {mode_flag} --yes
269273
"""
270274

271275
# Create timer file
@@ -350,19 +354,21 @@ def _check_systemd_timer(self) -> bool:
350354
except (subprocess.TimeoutExpired, OSError):
351355
return False
352356

353-
def _setup_cron(self, interval: ScheduleInterval) -> Dict[str, Any]:
357+
def _setup_cron(self, interval: ScheduleInterval, safe_mode: bool = True) -> Dict[str, Any]:
354358
"""
355359
Set up cron job for scheduling.
356360
357361
Args:
358362
interval: Scheduling interval.
363+
safe_mode: If True, run with --safe flag; otherwise --force.
359364
360365
Returns:
361366
dict: Result with success status.
362367
"""
363368
try:
364369
cron_schedule = self._get_cron_schedule(interval)
365-
cron_command = f"{cron_schedule} /usr/bin/env cortex cleanup run --safe --yes # cortex-cleanup"
370+
mode_flag = "--safe" if safe_mode else "--force"
371+
cron_command = f"{cron_schedule} /usr/bin/env cortex cleanup run {mode_flag} --yes # cortex-cleanup"
366372

367373
# Get current crontab
368374
result = subprocess.run(

cortex/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,10 @@ def cleanup(self, args):
530530
from rich.prompt import Confirm
531531
from datetime import datetime
532532

533+
if not hasattr(args, 'cleanup_action') or args.cleanup_action is None:
534+
self._print_error("Please specify a subcommand (scan/run/undo/schedule)")
535+
return 1
536+
533537
if args.cleanup_action == 'scan':
534538
scanner = CleanupScanner()
535539
self._print_status("🔍", "Scanning for cleanup opportunities...")

docs/CLEANUP_GUIDE.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ Cortex provides intelligent disk cleanup capabilities that can:
1515

1616
```bash
1717
# Scan for cleanup opportunities
18-
$ cortex cleanup scan
18+
cortex cleanup scan
1919

2020
# Run cleanup (with confirmation)
21-
$ cortex cleanup run
21+
cortex cleanup run
2222

2323
# Run cleanup without confirmation (safe mode)
24-
$ cortex cleanup run --safe --yes
24+
cortex cleanup run --safe --yes
2525
```
2626

2727
## Commands
@@ -31,11 +31,12 @@ $ cortex cleanup run --safe --yes
3131
Identify cleanup opportunities without making any changes:
3232

3333
```bash
34-
$ cortex cleanup scan
34+
cortex cleanup scan
3535
```
3636

3737
**Output example:**
38-
```
38+
39+
```text
3940
💾 Cleanup Opportunities:
4041
4142
Category Items Size
@@ -53,13 +54,13 @@ Execute cleanup operations:
5354

5455
```bash
5556
# Safe mode (default) - with confirmation
56-
$ cortex cleanup run
57+
cortex cleanup run
5758

5859
# Safe mode - skip confirmation
59-
$ cortex cleanup run --safe --yes
60+
cortex cleanup run --safe --yes
6061

6162
# Force mode - clean all items (use with caution)
62-
$ cortex cleanup run --force --yes
63+
cortex cleanup run --force --yes
6364
```
6465

6566
**Options:**
@@ -76,14 +77,15 @@ Restore files that were cleaned:
7677

7778
```bash
7879
# List restorable items
79-
$ cortex cleanup undo
80+
cortex cleanup undo
8081

8182
# Restore a specific item
82-
$ cortex cleanup undo <item-id>
83+
cortex cleanup undo <item-id>
8384
```
8485

8586
**Example:**
86-
```bash
87+
88+
```text
8789
$ cortex cleanup undo
8890
ID File Size Date
8991
abc123 temp_file.txt 1.2 MB 2024-01-15 10:30
@@ -101,19 +103,19 @@ Configure automatic cleanup:
101103

102104
```bash
103105
# Show current schedule status
104-
$ cortex cleanup schedule --show
106+
cortex cleanup schedule --show
105107

106108
# Enable weekly cleanup (default)
107-
$ cortex cleanup schedule --enable
109+
cortex cleanup schedule --enable
108110

109111
# Enable daily cleanup
110-
$ cortex cleanup schedule --enable --interval daily
112+
cortex cleanup schedule --enable --interval daily
111113

112114
# Enable monthly cleanup
113-
$ cortex cleanup schedule --enable --interval monthly
115+
cortex cleanup schedule --enable --interval monthly
114116

115117
# Disable scheduled cleanup
116-
$ cortex cleanup schedule --disable
118+
cortex cleanup schedule --disable
117119
```
118120

119121
**Supported intervals:**
@@ -186,7 +188,7 @@ Some cleanup operations require root privileges:
186188

187189
```bash
188190
# Clean system package cache
189-
$ sudo cortex cleanup run
191+
sudo cortex cleanup run
190192
```
191193

192194
### No Space Reclaimed
@@ -218,4 +220,4 @@ Default settings for cleanup:
218220

219221
---
220222

221-
For more information, visit: https://cortexlinux.com/docs/cleanup
223+
For more information, visit: [Cortex Cleanup Documentation](https://cortexlinux.com/docs/cleanup)

tests/test_cleanup_cleaner.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def test_compress_logs_dry_run(self, dry_run_cleaner, tmp_path):
188188
assert log_file.exists()
189189

190190
def test_run_cleanup_all_categories(self, cleaner):
191-
"""Test run_cleanup with all categories."""
191+
"""Test run_cleanup with all categories (non-safe mode)."""
192192
scan_results = [
193193
ScanResult("Package Cache", 1000, 5, []),
194194
ScanResult("Orphaned Packages", 2000, 3, ["pkg1"]),
@@ -201,7 +201,8 @@ def test_run_cleanup_all_categories(self, cleaner):
201201
patch.object(cleaner, 'clean_temp_files', return_value=500), \
202202
patch.object(cleaner, 'compress_logs', return_value=800):
203203

204-
summary = cleaner.run_cleanup(scan_results)
204+
# Use safe=False to include orphaned packages
205+
summary = cleaner.run_cleanup(scan_results, safe=False)
205206

206207
assert summary["Package Cache"] == 1000
207208
assert summary["Orphaned Packages"] == 2000

0 commit comments

Comments
 (0)