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