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
589class 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
112234if __name__ == "__main__" :
0 commit comments