refactor: Improve backup retention logic to be non-recursive and folder-specific, enabling directory pruning and updating PyInstaller build settings.

This commit is contained in:
2026-01-31 15:58:21 +08:00
parent cecb1809e6
commit 6e67a09621
4 changed files with 35 additions and 46 deletions

View File

@@ -100,9 +100,11 @@ exe = EXE(
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
icon='icon.ico',
)

View File

@@ -219,69 +219,57 @@ class BackupStrategy(ABC):
def is_cancelled(self):
return self._cancel_flag
def cleanup_old_backups(self, dest, retention_limit, job_name=None):
def cleanup_old_backups(self, target_folder, retention_limit):
"""
Count-Based Retention: Keep 'retention_limit' newest files/archives.
Delete the rest.
Count-Based Retention (Scoped):
Keep 'retention_limit' newest items (files/folders) in 'target_folder'.
Do NOT scan recursively. Do NOT look at other date folders.
"""
if retention_limit <= 0:
return
# SAFETY FIX: Scope cleanup to the specific Job Name folder
if job_name:
safe_job_name = "".join(c for c in job_name if c.isalnum() or c in (' ', '.', '_', '-')).strip()
scan_root = os.path.join(dest, safe_job_name)
else:
# If no job name provided, DO NOT scan the root dest to prevent deleting user files
if not os.path.exists(target_folder):
return
if not os.path.exists(scan_root):
return
self.log("Menjalankan pembersihan di {} (Batasan: {} file terbaru)...".format(scan_root, retention_limit))
self.log("Menjalankan pembersihan di {} (Batasan: {} item terbaru)...".format(os.path.basename(target_folder), retention_limit))
all_backups = []
try:
# 1. Collect all files recursively
for root, dirs, files in os.walk(scan_root):
for filename in files:
filepath = os.path.join(root, filename)
try:
mtime = os.path.getmtime(filepath)
all_backups.append((filepath, mtime))
except:
pass
# 1. Collect all items in this SPECIFIC folder only (Non-recursive)
# Python 3.4 compatible (os.scandir came in 3.5)
for entry in os.listdir(target_folder):
try:
full_path = os.path.join(target_folder, entry)
# Get modified time
mtime = os.path.getmtime(full_path)
all_backups.append((full_path, mtime))
except:
pass
# 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:]
items_to_delete = all_backups[retention_limit:]
count = 0
for filepath, _ in files_to_delete:
for path, _ in items_to_delete:
try:
os.remove(filepath)
self.log("Menghapus backup lama (Pruning): {}".format(os.path.basename(filepath)))
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
self.log("Pruning item lama: {}".format(os.path.basename(path)))
count += 1
except Exception as e:
self.log("Gagal menghapus {}: {}".format(os.path.basename(filepath), e))
self.log("Gagal menghapus {}: {}".format(os.path.basename(path), e))
if count > 0:
self.log("Pembersihan selesai. Dihapus: {} file.".format(count))
self.log("Pembersihan selesai. Dihapus: {} item.".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
for root, dirs, files in os.walk(scan_root, topdown=False):
for name in dirs:
d_path = os.path.join(root, name)
try:
if not os.listdir(d_path): # Check if empty
os.rmdir(d_path)
except: pass
self.log("Jumlah item ({}) aman (Batas: {}).".format(len(all_backups), retention_limit))
except Exception as e:
self.log("Error saat pembersihan: {}".format(e))
@@ -399,11 +387,10 @@ class FileBackup(BackupStrategy):
dst_file = os.path.join(current_dest_dir, file)
shutil.copy2(src_file, dst_file)
# Retention
# Cleanup
retention_days = config.get('retention_days', 0)
if retention_days > 0:
job_name = config.get('name')
self.cleanup_old_backups(dest, retention_days, job_name)
self.cleanup_old_backups(target_dest, retention_days)
self.progress(100)
@@ -566,7 +553,7 @@ class MySQLBackup(BackupStrategy):
self.log("Backup MySQL berhasil diselesaikan.")
self.progress(100)
self.cleanup_old_backups(dest, config.get('retention_days', 0))
self.cleanup_old_backups(target_dest, config.get('retention_days', 0))
except Exception as e:
self.log("Gagal melakukan backup: {}".format(e))
@@ -727,7 +714,7 @@ class PostgresBackup(BackupStrategy):
self.log("Backup PostgreSQL berhasil diselesaikan.")
self.progress(100)
self.cleanup_old_backups(dest, config.get('retention_days', 0))
self.cleanup_old_backups(target_dest, config.get('retention_days', 0))
except Exception as e:
self.log("Gagal melakukan backup: {}".format(e))

Binary file not shown.

View File

@@ -8,7 +8,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QProgressBar, QFileDialog, QMessageBox, QGroupBox,
QSystemTrayIcon, QMenu, QSpinBox, QTabWidget,
QListWidget, QSplitter, QListWidgetItem, QInputDialog, QDialog,
QSizePolicy, QAction, QStyle)
QSizePolicy, QAction, QStyle, QTimeEdit)
from PyQt5.QtCore import Qt, QThread, pyqtSignal as Signal, QTimer, QTime
from PyQt5.QtGui import QIcon
@@ -490,7 +490,6 @@ class BackupApp(QMainWindow):
pd_layout = QHBoxLayout(page_daily)
pd_layout.setContentsMargins(0,0,0,0)
pd_layout.addWidget(QLabel("Pukul:"))
from PyQt5.QtWidgets import QTimeEdit
self.job_time_edit = QTimeEdit()
self.job_time_edit.setDisplayFormat("HH:mm")
self.create_spinbox_with_buttons(self.job_time_edit, pd_layout)
@@ -501,6 +500,7 @@ class BackupApp(QMainWindow):
sched_layout.addStretch()
sched_layout.addWidget(QLabel("| Retensi:"))
self.retention_spin = QSpinBox()
self.retention_spin.setRange(0, 3650)
self.retention_spin.setSpecialValueText("Selamanya")
self.retention_spin.setSuffix(" File")