diff --git a/ProBackup.spec b/ProBackup.spec index a2f79cf..331c070 100644 --- a/ProBackup.spec +++ b/ProBackup.spec @@ -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', ) diff --git a/backup_manager.py b/backup_manager.py index fcd6d79..9cd7788 100644 --- a/backup_manager.py +++ b/backup_manager.py @@ -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)) diff --git a/dist-winXP/ProBackup.exe b/dist-winXP/ProBackup.exe index 1950821..f2f7b3b 100644 Binary files a/dist-winXP/ProBackup.exe and b/dist-winXP/ProBackup.exe differ diff --git a/main.py b/main.py index 3b94878..c16c2ce 100644 --- a/main.py +++ b/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")