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:
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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.
4
main.py
4
main.py
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user