Switch to Count-Based Retention and update UI
This commit is contained in:
@@ -219,8 +219,12 @@ class BackupStrategy(ABC):
|
|||||||
def is_cancelled(self):
|
def is_cancelled(self):
|
||||||
return self._cancel_flag
|
return self._cancel_flag
|
||||||
|
|
||||||
def cleanup_old_backups(self, dest, retention_days, job_name=None):
|
def cleanup_old_backups(self, dest, retention_limit, job_name=None):
|
||||||
if retention_days <= 0:
|
"""
|
||||||
|
Count-Based Retention: Keep 'retention_limit' newest files/archives.
|
||||||
|
Delete the rest.
|
||||||
|
"""
|
||||||
|
if retention_limit <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
# SAFETY FIX: Scope cleanup to the specific Job Name folder
|
# SAFETY FIX: Scope cleanup to the specific Job Name folder
|
||||||
@@ -229,31 +233,46 @@ class BackupStrategy(ABC):
|
|||||||
scan_root = os.path.join(dest, safe_job_name)
|
scan_root = os.path.join(dest, safe_job_name)
|
||||||
else:
|
else:
|
||||||
# If no job name provided, DO NOT scan the root dest to prevent deleting user files
|
# If no job name provided, DO NOT scan the root dest to prevent deleting user files
|
||||||
# self.log("Warning: Nama pekerjaan tidak ada. Skip pembersihan demi keamanan.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if not os.path.exists(scan_root):
|
if not os.path.exists(scan_root):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log("Menjalankan pembersihan di {} (Retensi: {} hari)...".format(scan_root, retention_days))
|
self.log("Menjalankan pembersihan di {} (Batasan: {} file terbaru)...".format(scan_root, retention_limit))
|
||||||
now = time.time()
|
|
||||||
cutoff = now - (retention_days * 86400)
|
all_backups = []
|
||||||
|
|
||||||
count = 0
|
|
||||||
try:
|
try:
|
||||||
# Recursive scan for YYYY/MM/DD structure
|
# 1. Collect all files recursively
|
||||||
for root, dirs, files in os.walk(scan_root):
|
for root, dirs, files in os.walk(scan_root):
|
||||||
for filename in files:
|
for filename in files:
|
||||||
filepath = os.path.join(root, filename)
|
filepath = os.path.join(root, filename)
|
||||||
try:
|
try:
|
||||||
# Check modified time
|
mtime = os.path.getmtime(filepath)
|
||||||
file_time = os.path.getmtime(filepath)
|
all_backups.append((filepath, mtime))
|
||||||
if file_time < cutoff:
|
except:
|
||||||
os.remove(filepath)
|
pass
|
||||||
self.log("Menghapus backup lama: {}".format(filename))
|
|
||||||
count += 1
|
# 2. Sort by time DESCENDING (Newest first)
|
||||||
|
all_backups.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
# 3. Check if we have excess
|
||||||
|
if len(all_backups) > retention_limit:
|
||||||
|
files_to_delete = all_backups[retention_limit:]
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for filepath, _ in files_to_delete:
|
||||||
|
try:
|
||||||
|
os.remove(filepath)
|
||||||
|
self.log("Menghapus backup lama (Pruning): {}".format(os.path.basename(filepath)))
|
||||||
|
count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log("Gagal menghapus {}: {}".format(filename, e))
|
self.log("Gagal menghapus {}: {}".format(os.path.basename(filepath), e))
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
self.log("Pembersihan selesai. Dihapus: {} file.".format(count))
|
||||||
|
else:
|
||||||
|
self.log("Jumlah file ({}) masih dibawah batas ({}). Tidak ada penghapusan.".format(len(all_backups), retention_limit))
|
||||||
|
|
||||||
# Optional: Remove empty folders after cleanup
|
# Optional: Remove empty folders after cleanup
|
||||||
for root, dirs, files in os.walk(scan_root, topdown=False):
|
for root, dirs, files in os.walk(scan_root, topdown=False):
|
||||||
@@ -267,11 +286,6 @@ class BackupStrategy(ABC):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log("Error saat pembersihan: {}".format(e))
|
self.log("Error saat pembersihan: {}".format(e))
|
||||||
|
|
||||||
if count > 0:
|
|
||||||
self.log("Pembersihan selesai. Menghapus {} file lama.".format(count))
|
|
||||||
else:
|
|
||||||
self.log("Pembersihan selesai. Tidak ada file lama ditemukan.")
|
|
||||||
|
|
||||||
def _get_dated_dest_dir(self, base_dest, job_name=None):
|
def _get_dated_dest_dir(self, base_dest, job_name=None):
|
||||||
"""Creates and returns JobName/YYYY/MM/DD subfolder path inside base_dest."""
|
"""Creates and returns JobName/YYYY/MM/DD subfolder path inside base_dest."""
|
||||||
|
|
||||||
|
|||||||
4
main.py
4
main.py
@@ -501,10 +501,10 @@ class BackupApp(QMainWindow):
|
|||||||
|
|
||||||
sched_layout.addStretch()
|
sched_layout.addStretch()
|
||||||
sched_layout.addWidget(QLabel("| Retensi:"))
|
sched_layout.addWidget(QLabel("| Retensi:"))
|
||||||
self.retention_spin = QSpinBox()
|
|
||||||
self.retention_spin.setRange(0, 3650)
|
self.retention_spin.setRange(0, 3650)
|
||||||
self.retention_spin.setSpecialValueText("Selamanya")
|
self.retention_spin.setSpecialValueText("Selamanya")
|
||||||
self.retention_spin.setToolTip("Hapus backup lebih lama dari X hari. 0 = Simpan Selamanya.")
|
self.retention_spin.setSuffix(" File")
|
||||||
|
self.retention_spin.setToolTip("Simpan X file backup terbaru. File yang lebih lama akan dihapus.")
|
||||||
self.create_spinbox_with_buttons(self.retention_spin, sched_layout)
|
self.create_spinbox_with_buttons(self.retention_spin, sched_layout)
|
||||||
|
|
||||||
sched_group.setLayout(sched_layout)
|
sched_group.setLayout(sched_layout)
|
||||||
|
|||||||
Reference in New Issue
Block a user