Skip to content

Commit 621e619

Browse files
committed
Automatic folder checking every 5 seconds Tooltips explain disabled buttons Progress bar with file size, total size, speed Cancel option for backups Multithreading for smooth UI USERPROFILE for reliable Windows paths
Automatic folder checking every 5 seconds Tooltips explain disabled buttons Progress bar with file size, total size, speed Cancel option for backups Multithreading for smooth UI USERPROFILE for reliable Windows paths
1 parent 7714b62 commit 621e619

File tree

1 file changed

+193
-71
lines changed

1 file changed

+193
-71
lines changed

Windows Backup/windows_backup.py

Lines changed: 193 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,89 @@
1-
import os, sys, glob, shutil
2-
from PySide6.QtWidgets import QMainWindow, QApplication, QPushButton, QWidget, QFileDialog, QMessageBox, QGridLayout
1+
import os, sys, shutil, time
2+
from PySide6.QtWidgets import (
3+
QMainWindow, QApplication, QPushButton, QWidget, QFileDialog, QMessageBox,
4+
QGridLayout, QProgressDialog, QSpacerItem, QSizePolicy
5+
)
6+
from PySide6.QtCore import Qt, QThread, Signal, QTimer
7+
8+
9+
def get_user_path(subpath=""):
10+
"""Return full path inside user's profile directory."""
11+
user_profile = os.environ.get("USERPROFILE", "")
12+
return os.path.join(user_profile, subpath) if subpath else user_profile
13+
14+
15+
class BackupWorker(QThread):
16+
progress_update = Signal(int, str, int, int, float)
17+
finished = Signal(int, bool, str)
18+
19+
def __init__(self, source_dir, destination_root, patterns):
20+
super().__init__()
21+
self.source_dir = source_dir
22+
self.destination_root = destination_root
23+
self.patterns = patterns
24+
self.cancelled = False
25+
self.files_copied = 0
26+
self.total_size = 0
27+
self.copied_size = 0
28+
self.start_time = 0
29+
30+
def run(self):
31+
try:
32+
total_files, total_size = self.count_files()
33+
self.total_size = total_size
34+
if total_files == 0:
35+
self.finished.emit(0, False, "No files found to backup.")
36+
return
37+
38+
self.start_time = time.time()
39+
files_processed = 0
40+
41+
for root, dirs, files in os.walk(self.source_dir):
42+
if self.cancelled:
43+
break
44+
rel_path = os.path.relpath(root, self.source_dir)
45+
destination_folder = os.path.join(self.destination_root, rel_path)
46+
os.makedirs(destination_folder, exist_ok=True)
47+
48+
for file in files:
49+
if any(file.lower().endswith(p.lstrip("*").lower()) for p in self.patterns):
50+
if self.cancelled:
51+
break
52+
source_file = os.path.join(root, file)
53+
dest_file = os.path.join(destination_folder, file)
54+
try:
55+
file_size = os.path.getsize(source_file)
56+
shutil.copy2(source_file, dest_file)
57+
self.files_copied += 1
58+
self.copied_size += file_size
59+
except Exception as e:
60+
self.finished.emit(self.files_copied, False, f"Failed to copy {file}: {e}")
61+
return
62+
files_processed += 1
63+
elapsed_time = time.time() - self.start_time
64+
speed = self.copied_size / (elapsed_time + 0.1)
65+
self.progress_update.emit(files_processed, file, file_size, self.copied_size, speed)
66+
67+
if self.cancelled:
68+
self.finished.emit(self.files_copied, False, "Backup cancelled by user.")
69+
else:
70+
self.finished.emit(self.files_copied, True, "Backup completed successfully.")
71+
72+
except Exception as e:
73+
self.finished.emit(self.files_copied, False, f"Backup failed: {e}")
74+
75+
def count_files(self):
76+
count = 0
77+
total_size = 0
78+
for root, dirs, files in os.walk(self.source_dir):
79+
for file in files:
80+
if any(file.lower().endswith(p.lstrip("*").lower()) for p in self.patterns):
81+
count += 1
82+
total_size += os.path.getsize(os.path.join(root, file))
83+
return count, total_size
84+
85+
def cancel(self):
86+
self.cancelled = True
387

488

589
class MainWindow(QMainWindow):
@@ -8,39 +92,64 @@ def __init__(self):
892

993
self.setWindowTitle("Windows Backup Application")
1094
self.setFixedSize(800, 600)
11-
12-
# Define Backup Location Variable
13-
self.backup_location = "" # Set by User
95+
self.backup_location = ""
1496

1597
central_widget = QWidget()
1698
self.setCentralWidget(central_widget)
1799
layout = QGridLayout()
18100
central_widget.setLayout(layout)
19-
cols = 2
20101

21-
# Set Backup Location Button
102+
# --- Alignment/spacing tweaks for clean layout ---
103+
cols = 2
104+
layout.setHorizontalSpacing(12)
105+
layout.setVerticalSpacing(12)
106+
layout.setContentsMargins(16, 16, 16, 16)
107+
for c in range(cols):
108+
layout.setColumnStretch(c, 1)
109+
# -------------------------------------------------
110+
111+
# Set Backup Location Button (spans top row)
22112
self.backup_location_button = QPushButton("Set Backup Location", self)
23113
self.backup_location_button.clicked.connect(self.set_backup_location)
24-
layout.addWidget(self.backup_location_button, 0, 0, 1, cols) # Span top row
25-
26-
# Create Backup Buttons
114+
layout.addWidget(self.backup_location_button, 0, 0, 1, cols)
115+
116+
# Map buttons to (source path, patterns, destination name)
117+
self.backup_sources = {
118+
"Backup Contacts": (get_user_path("Contacts"), ["*"], "Contacts"),
119+
"Backup Photos": (get_user_path("Pictures"), ["*"], "Pictures"),
120+
"Backup Documents": (get_user_path("Documents"), ["*"], "Documents"),
121+
"Backup Videos": (get_user_path("Videos"), ["*"], "Videos"),
122+
"Backup Music": (get_user_path("Music"), ["*"], "Music"),
123+
"Backup Desktop": (get_user_path("Desktop"), ["*"], "Desktop"),
124+
"Backup Downloads": (get_user_path("Downloads"), ["*"], "Downloads"),
125+
"Backup Outlook Files": (get_user_path("AppData/Local/Microsoft/Outlook"),
126+
["*.pst", "*.ost", "*.nst"], "Outlook"),
127+
}
128+
129+
# Create buttons in a predictable left-to-right, top-to-bottom order
27130
self.backup_buttons = []
28-
self.backup_buttons.append(self.create_button("Backup Contacts", self.backup_contacts))
29-
self.backup_buttons.append(self.create_button("Backup Photos", self.backup_photos))
30-
self.backup_buttons.append(self.create_button("Backup Documents", self.backup_documents))
31-
self.backup_buttons.append(self.create_button("Backup Videos", self.backup_videos))
32-
self.backup_buttons.append(self.create_button("Backup Music", self.backup_music))
33-
self.backup_buttons.append(self.create_button("Backup Desktop", self.backup_desktop))
34-
self.backup_buttons.append(self.create_button("Backup Downloads", self.backup_downloads))
35-
self.backup_buttons.append(self.create_button("Backup Outlook Files", self.backup_outlook_files))
36-
37-
# Add buttons to grid layout
38-
for idx, btn in enumerate(self.backup_buttons):
39-
btn.setFixedSize(150, 40)
131+
for idx, (name, (src, pats, dest)) in enumerate(self.backup_sources.items()):
132+
btn = self.create_button(name, lambda checked=False, n=name: self.start_backup(*self.backup_sources[n]))
133+
btn.setFixedSize(180, 42)
40134
btn.setEnabled(False)
41-
row = 1 + idx // cols
42-
col = idx % cols
135+
self.backup_buttons.append((btn, name))
136+
137+
# --- Correct grid placement (fixes orientation/alignment) ---
138+
row = 1 + (idx // cols) # first row after the header
139+
col = idx % cols # 0, 1, 0, 1, ...
43140
layout.addWidget(btn, row, col)
141+
# ------------------------------------------------------------
142+
143+
# Add a flexible spacer row below buttons to keep them top-aligned
144+
total_rows = 1 + ((len(self.backup_sources) + cols - 1) // cols) # header + button rows
145+
layout.setRowStretch(total_rows + 1, 1)
146+
layout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding),
147+
total_rows + 1, 0, 1, cols)
148+
149+
# Auto-refresh timer every 5 seconds to re-check sources
150+
self.refresh_timer = QTimer(self)
151+
self.refresh_timer.timeout.connect(self.check_source_folders)
152+
self.refresh_timer.start(5000)
44153

45154
def create_button(self, text, command):
46155
btn = QPushButton(text, self)
@@ -51,62 +160,75 @@ def set_backup_location(self):
51160
folder = QFileDialog.getExistingDirectory(self, "Select Backup Location")
52161
if folder:
53162
self.backup_location = folder
54-
for btn in self.backup_buttons:
55-
btn.setEnabled(True)
56-
57-
def copy_with_glob(self, source_dir, destination_name, patterns=["*"]):
58-
"""Copy files matching patterns from source_dir to backup_location/destination_name"""
163+
self.check_source_folders()
164+
165+
def check_source_folders(self):
166+
"""Enable buttons only if source exists and has files; set tooltips if disabled"""
167+
for btn, name in self.backup_buttons:
168+
src, patterns, dest = self.backup_sources[name]
169+
folder_exists = os.path.exists(src)
170+
has_files = folder_exists and any(
171+
any(f.lower().endswith(p.lstrip("*").lower()) for f in files for p in patterns)
172+
for _, _, files in os.walk(src)
173+
)
174+
175+
if folder_exists and has_files:
176+
if not btn.isEnabled():
177+
btn.setEnabled(True)
178+
btn.setToolTip(f"Backup files from {src}{dest}")
179+
else:
180+
if btn.isEnabled():
181+
btn.setEnabled(False)
182+
if not folder_exists:
183+
btn.setToolTip(f"Source folder not found: {src}")
184+
else:
185+
btn.setToolTip(f"No files found in {src} to backup")
186+
187+
def start_backup(self, source_dir, patterns, destination_name):
59188
if not self.backup_location:
60189
QMessageBox.warning(self, "Error", "Please set a backup location first.")
61190
return
62191

63-
source_dir = os.path.expanduser(source_dir)
64192
if not os.path.exists(source_dir):
65193
QMessageBox.warning(self, "Error", f"Source folder not found: {source_dir}")
66194
return
67195

68-
destination = os.path.join(self.backup_location, destination_name)
69-
os.makedirs(destination, exist_ok=True)
70-
71-
files_copied = 0
72-
for pattern in patterns:
73-
for file_path in glob.glob(os.path.join(source_dir, pattern), recursive=True):
74-
if os.path.isfile(file_path): # only copy files, not directories
75-
try:
76-
shutil.copy2(file_path, destination)
77-
files_copied += 1
78-
except Exception as e:
79-
QMessageBox.warning(self, "Error", f"Failed to copy {file_path}: {e}")
80-
81-
if files_copied > 0:
82-
QMessageBox.information(self, "Success", f"Backup of {files_copied} files completed in {destination_name}.")
196+
destination_root = os.path.join(self.backup_location, destination_name)
197+
os.makedirs(destination_root, exist_ok=True)
198+
199+
self.progress_dialog = QProgressDialog(f"Backing up {destination_name}...", "Cancel", 0, 100, self)
200+
self.progress_dialog.setWindowModality(Qt.WindowModal)
201+
self.progress_dialog.setMinimumWidth(420)
202+
self.progress_dialog.setValue(0)
203+
204+
self.worker = BackupWorker(source_dir, destination_root, patterns)
205+
self.worker.progress_update.connect(self.update_progress)
206+
self.worker.finished.connect(self.backup_finished)
207+
self.progress_dialog.canceled.connect(self.worker.cancel)
208+
209+
total_files, _ = self.worker.count_files()
210+
self.progress_dialog.setMaximum(total_files if total_files > 0 else 1)
211+
212+
self.worker.start()
213+
214+
def update_progress(self, value, filename, file_size, copied_size, speed):
215+
copied_mb = copied_size / (1024 * 1024)
216+
total_mb = self.worker.total_size / (1024 * 1024) if self.worker.total_size else 0
217+
speed_mb = speed / (1024 * 1024)
218+
self.progress_dialog.setValue(value)
219+
self.progress_dialog.setLabelText(
220+
f"Copying: {filename}\n"
221+
f"File size: {file_size / 1024:.1f} KB\n"
222+
f"Copied: {copied_mb:.2f} / {total_mb:.2f} MB\n"
223+
f"Speed: {speed_mb:.2f} MB/s"
224+
)
225+
226+
def backup_finished(self, files_copied, success, message):
227+
self.progress_dialog.close()
228+
if success:
229+
QMessageBox.information(self, "Success", f"{message}\n{files_copied} files backed up.")
83230
else:
84-
QMessageBox.information(self, "Info", f"No files found in {source_dir} to backup.")
85-
86-
# Backup Methods
87-
def backup_contacts(self):
88-
self.copy_with_glob("~/Contacts", "Contacts")
89-
90-
def backup_photos(self):
91-
self.copy_with_glob("~/Pictures", "Pictures")
92-
93-
def backup_documents(self):
94-
self.copy_with_glob("~/Documents", "Documents")
95-
96-
def backup_videos(self):
97-
self.copy_with_glob("~/Videos", "Videos")
98-
99-
def backup_music(self):
100-
self.copy_with_glob("~/Music", "Music")
101-
102-
def backup_desktop(self):
103-
self.copy_with_glob("~/Desktop", "Desktop")
104-
105-
def backup_downloads(self):
106-
self.copy_with_glob("~/Downloads", "Downloads")
107-
108-
def backup_outlook_files(self):
109-
self.copy_with_glob("~/AppData/Local/Microsoft/Outlook", "Outlook", ["*.pst", "*.ost", "*.nst"])
231+
QMessageBox.warning(self, "Backup", message)
110232

111233

112234
if __name__ == "__main__":

0 commit comments

Comments
 (0)