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, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
upx=False, upx=True,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=False, console=False,
disable_windowed_traceback=False,
argv_emulation=False,
icon='icon.ico', icon='icon.ico',
) )

View File

@@ -219,37 +219,31 @@ 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_limit, job_name=None): def cleanup_old_backups(self, target_folder, retention_limit):
""" """
Count-Based Retention: Keep 'retention_limit' newest files/archives. Count-Based Retention (Scoped):
Delete the rest. 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: if retention_limit <= 0:
return return
# SAFETY FIX: Scope cleanup to the specific Job Name folder if not os.path.exists(target_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
return return
if not os.path.exists(scan_root): self.log("Menjalankan pembersihan di {} (Batasan: {} item terbaru)...".format(os.path.basename(target_folder), retention_limit))
return
self.log("Menjalankan pembersihan di {} (Batasan: {} file terbaru)...".format(scan_root, retention_limit))
all_backups = [] all_backups = []
try: try:
# 1. Collect all files recursively # 1. Collect all items in this SPECIFIC folder only (Non-recursive)
for root, dirs, files in os.walk(scan_root): # Python 3.4 compatible (os.scandir came in 3.5)
for filename in files: for entry in os.listdir(target_folder):
filepath = os.path.join(root, filename)
try: try:
mtime = os.path.getmtime(filepath) full_path = os.path.join(target_folder, entry)
all_backups.append((filepath, mtime)) # Get modified time
mtime = os.path.getmtime(full_path)
all_backups.append((full_path, mtime))
except: except:
pass pass
@@ -258,30 +252,24 @@ class BackupStrategy(ABC):
# 3. Check if we have excess # 3. Check if we have excess
if len(all_backups) > retention_limit: if len(all_backups) > retention_limit:
files_to_delete = all_backups[retention_limit:] items_to_delete = all_backups[retention_limit:]
count = 0 count = 0
for filepath, _ in files_to_delete: for path, _ in items_to_delete:
try: try:
os.remove(filepath) if os.path.isdir(path):
self.log("Menghapus backup lama (Pruning): {}".format(os.path.basename(filepath))) shutil.rmtree(path)
else:
os.remove(path)
self.log("Pruning item lama: {}".format(os.path.basename(path)))
count += 1 count += 1
except Exception as e: 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: if count > 0:
self.log("Pembersihan selesai. Dihapus: {} file.".format(count)) self.log("Pembersihan selesai. Dihapus: {} item.".format(count))
else: else:
self.log("Jumlah file ({}) masih dibawah batas ({}). Tidak ada penghapusan.".format(len(all_backups), retention_limit)) self.log("Jumlah item ({}) aman (Batas: {}).".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
except Exception as e: except Exception as e:
self.log("Error saat pembersihan: {}".format(e)) self.log("Error saat pembersihan: {}".format(e))
@@ -399,11 +387,10 @@ class FileBackup(BackupStrategy):
dst_file = os.path.join(current_dest_dir, file) dst_file = os.path.join(current_dest_dir, file)
shutil.copy2(src_file, dst_file) shutil.copy2(src_file, dst_file)
# Retention # Cleanup
retention_days = config.get('retention_days', 0) retention_days = config.get('retention_days', 0)
if retention_days > 0: if retention_days > 0:
job_name = config.get('name') self.cleanup_old_backups(target_dest, retention_days)
self.cleanup_old_backups(dest, retention_days, job_name)
self.progress(100) self.progress(100)
@@ -566,7 +553,7 @@ class MySQLBackup(BackupStrategy):
self.log("Backup MySQL berhasil diselesaikan.") self.log("Backup MySQL berhasil diselesaikan.")
self.progress(100) 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: except Exception as e:
self.log("Gagal melakukan backup: {}".format(e)) self.log("Gagal melakukan backup: {}".format(e))
@@ -727,7 +714,7 @@ class PostgresBackup(BackupStrategy):
self.log("Backup PostgreSQL berhasil diselesaikan.") self.log("Backup PostgreSQL berhasil diselesaikan.")
self.progress(100) 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: except Exception as e:
self.log("Gagal melakukan backup: {}".format(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, QProgressBar, QFileDialog, QMessageBox, QGroupBox,
QSystemTrayIcon, QMenu, QSpinBox, QTabWidget, QSystemTrayIcon, QMenu, QSpinBox, QTabWidget,
QListWidget, QSplitter, QListWidgetItem, QInputDialog, QDialog, 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.QtCore import Qt, QThread, pyqtSignal as Signal, QTimer, QTime
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
@@ -490,7 +490,6 @@ class BackupApp(QMainWindow):
pd_layout = QHBoxLayout(page_daily) pd_layout = QHBoxLayout(page_daily)
pd_layout.setContentsMargins(0,0,0,0) pd_layout.setContentsMargins(0,0,0,0)
pd_layout.addWidget(QLabel("Pukul:")) pd_layout.addWidget(QLabel("Pukul:"))
from PyQt5.QtWidgets import QTimeEdit
self.job_time_edit = QTimeEdit() self.job_time_edit = QTimeEdit()
self.job_time_edit.setDisplayFormat("HH:mm") self.job_time_edit.setDisplayFormat("HH:mm")
self.create_spinbox_with_buttons(self.job_time_edit, pd_layout) self.create_spinbox_with_buttons(self.job_time_edit, pd_layout)
@@ -501,6 +500,7 @@ 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.setSuffix(" File") self.retention_spin.setSuffix(" File")