66from pathlib import Path
77from tkinter .ttk import Progressbar
88
9- PHOTO_EXTENSIONS = {
10- ".jpeg" , ".jpg" , ".gif" , ".pdf" , ".png" ,
11- ".heic" , ".tiff" , ".tif" , ".bmp" , ".webp" , ".raw"
9+ EXTENSIONS = {
10+ "Photos" : {".jpeg" , ".jpg" , ".gif" , ".pdf" , ".png" , ".heic" , ".tiff" , ".tif" , ".bmp" , ".webp" , ".raw" },
11+ "Documents" : {".doc" , ".docx" , ".xls" , ".xlsx" , ".ppt" , ".pptx" , ".pdf" , ".txt" , ".rtf" , ".odt" },
12+ "Music" : {".mp3" , ".wav" , ".flac" , ".aac" , ".wma" , ".ogg" , ".m4a" },
13+ "Videos" : {".mp4" , ".avi" , ".mov" , ".mkv" , ".wmv" , ".flv" , ".webm" , ".mpeg" },
14+ "QuickBooks" : {".qbw" , ".qbb" , ".qbm" , ".qbo" , ".des" , ".qbr" , ".qwc" , ".qbstbl2.usa" , ".qbx" ,
15+ ".qba" , ".qby" , ".qbj" , ".iif" , ".nd" , ".tlg" , ".ecml" , ".qbp" , ".qsm" , ".qss" ,
16+ ".qst" , ".qb2016" , ".qb2019" , ".mac.qbb" }
1217}
1318
14- class FileScannerApp :
19+ class FileCopyTool :
1520 def __init__ (self , root ):
1621 self .root = root
17- self .root .title ("Drive Scanner and Photo Copier " )
18- self .root .geometry ("620x570 " )
22+ self .root .title ("Data Recovery Tool " )
23+ self .root .geometry ("660x700 " )
1924 self .root .resizable (False , False )
2025
21- self .source_drive = tk .StringVar ()
22- self .destination_folder = tk .StringVar ()
26+ self .source = tk .StringVar ()
27+ self .destination = tk .StringVar ()
2328 self .min_file_size_kb = tk .IntVar (value = 50 )
2429 self .cancel_requested = False
30+ self .file_type_mode = None
2531
26- self .create_widgets ()
32+ self .build_menu ()
2733
28- def create_widgets (self ):
29- # Source drive dropdown
30- tk .Label (self .root , text = "Select Drive to Scan:" ).pack (pady = (10 , 0 ))
31- drives = self .get_available_drives ()
32- self .source_drive .set (drives [0 ] if drives else "" )
33- tk .OptionMenu (self .root , self .source_drive , * drives ).pack ()
34+ def build_menu (self ):
35+ tk .Label (self .root , text = "Select Operation" , font = ("Arial" , 14 )).pack (pady = 10 )
3436
35- # Destination folder picker
36- tk .Label (self .root , text = "Select Destination Folder:" ).pack (pady = (10 , 0 ))
37- entry_frame = tk .Frame (self .root )
38- entry_frame .pack ()
39- tk .Entry (entry_frame , textvariable = self .destination_folder , width = 50 ).pack (side = tk .LEFT , padx = (5 , 5 ))
40- tk .Button (entry_frame , text = "Browse" , command = self .browse_folder ).pack (side = tk .LEFT )
37+ btns = [
38+ ("Copy Photos" , lambda : self .select_and_copy ("Photos" )),
39+ ("Copy Documents" , lambda : self .select_and_copy ("Documents" )),
40+ ("Copy Music" , lambda : self .select_and_copy ("Music" )),
41+ ("Copy Videos" , lambda : self .select_and_copy ("Videos" )),
42+ ("Copy QB Files" , lambda : self .select_and_copy ("QuickBooks" )),
43+ ("Copy User Data" , self .copy_user_data )
44+ ]
45+
46+ for label , cmd in btns :
47+ tk .Button (self .root , text = label , width = 30 , command = cmd ).pack (pady = 5 )
4148
42- # Minimum file size filter
4349 tk .Label (self .root , text = "Minimum File Size to Copy (KB):" ).pack (pady = (10 , 0 ))
4450 tk .Entry (self .root , textvariable = self .min_file_size_kb , width = 10 ).pack ()
4551
46- # Progress bar
4752 tk .Label (self .root , text = "Progress:" ).pack (pady = (10 , 0 ))
4853 self .progress = Progressbar (self .root , length = 580 , mode = 'determinate' )
49- self .progress .pack (pady = (0 , 10 ))
54+ self .progress .pack (pady = (0 , 5 ))
55+
56+ self .status_label = tk .Label (self .root , text = "" , font = ("Arial" , 10 ), fg = "blue" )
57+ self .status_label .pack (pady = (0 , 5 ))
5058
51- # Start and Cancel buttons
5259 button_frame = tk .Frame (self .root )
53- button_frame .pack (pady = 5 )
54- tk .Button (button_frame , text = "Start Scan" , command = self .start_scan_thread ).pack (side = tk .LEFT , padx = 10 )
55- self .cancel_button = tk .Button (button_frame , text = "Cancel Scan" , command = self .cancel_scan )
56- self .cancel_button .pack (side = tk .LEFT )
60+ button_frame .pack (pady = (0 , 10 ))
61+ self .cancel_button = tk .Button (button_frame , text = "Cancel Scan" , command = self .cancel_scan_confirm )
62+ self .cancel_button .pack ()
5763 self .cancel_button .config (state = tk .DISABLED )
5864
59- # Log output
60- self .log_output = scrolledtext .ScrolledText (self .root , height = 15 , width = 72 , state = 'disabled' )
61- self .log_output .pack (padx = 10 , pady = 10 )
62-
63- # Footer
64- tk .Label (self .root , text = "Designed by Neverklear Technologies 2025" , font = ("Arial" , 9 ), fg = "gray" ).pack (pady = (5 , 5 ))
65-
66- def get_available_drives (self ):
67- drives = []
68- for letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' :
69- if os .path .exists (f"{ letter } :\\ " ):
70- drives .append (f"{ letter } :\\ " )
71- return drives
72-
73- def browse_folder (self ):
74- folder = filedialog .askdirectory ()
75- if folder :
76- self .destination_folder .set (folder )
77-
78- def start_scan_thread (self ):
79- self .cancel_requested = False
80- self .cancel_button .config (state = tk .NORMAL )
81- thread = Thread (target = self .scan_and_copy_files )
82- thread .start ()
65+ self .log_output = scrolledtext .ScrolledText (self .root , height = 15 , width = 75 , state = 'disabled' )
66+ self .log_output .pack (padx = 10 , pady = 5 )
8367
84- def cancel_scan (self ):
85- self .cancel_requested = True
86- self .cancel_button .config (state = tk .DISABLED )
68+ tk .Label (self .root , text = "Designed by Neverklear Technologies 2025" , font = ("Arial" , 9 ), fg = "gray" ).pack (pady = 5 )
8769
8870 def log (self , message ):
8971 self .log_output .configure (state = 'normal' )
9072 self .log_output .insert (tk .END , message + "\n " )
9173 self .log_output .see (tk .END )
9274 self .log_output .configure (state = 'disabled' )
75+ with open ("logfile.txt" , "a" , encoding = "utf-8" ) as logf :
76+ logf .write (message + "\n " )
9377
94- def scan_and_copy_files (self ):
95- source = self .source_drive .get ()
96- dest = self .destination_folder .get ()
97- min_size_bytes = self .min_file_size_kb .get () * 1024
78+ def cancel_scan_confirm (self ):
79+ if messagebox .askyesno ("Confirm Cancel" , "Are you sure you want to cancel the scan?" ):
80+ self .cancel_requested = True
81+ self .status_label .config (text = "Scan cancelled by user." )
82+ self .cancel_button .config (state = tk .DISABLED )
9883
99- if not source or not os .path .exists (source ):
100- messagebox .showerror ("Error" , "Please select a valid source drive." )
84+ def select_and_copy (self , file_type ):
85+ self .cancel_requested = False
86+ self .file_type_mode = file_type
87+ source = filedialog .askdirectory (title = "Select Drive to Scan" )
88+ dest = filedialog .askdirectory (title = "Select Destination Folder" )
89+ if source and dest :
90+ self .source .set (source )
91+ self .destination .set (dest )
92+ self .start_thread ()
93+
94+ def copy_user_data (self ):
95+ self .cancel_requested = False
96+ drive = filedialog .askdirectory (title = "Select Windows Installation Drive (e.g., C:\\ )" )
97+ dest = filedialog .askdirectory (title = "Select Destination Folder" )
98+ if not drive or not dest :
10199 return
102-
103- if not dest :
104- messagebox .showerror ("Error" , "Please select a destination folder." )
100+ user_root = os . path . join ( drive , "Users" )
101+ if not os . path . exists ( user_root ) :
102+ messagebox .showerror ("Error" , "The selected drive does not appear to contain a Users folder." )
105103 return
106104
107- self .log (f"Scanning { source } for image/PDF files > { self .min_file_size_kb .get ()} KB...\n " )
105+ self .source .set (user_root )
106+ self .destination .set (dest )
107+ self .file_type_mode = "UserData"
108+ self .start_thread ()
109+
110+ def start_thread (self ):
111+ self .progress ["value" ] = 0
112+ self .progress .update ()
113+ self .status_label .config (text = "Scanning and copying files..." )
114+ self .cancel_button .config (state = tk .NORMAL )
115+ thread = Thread (target = self .perform_copy )
116+ thread .start ()
117+
118+ def perform_copy (self ):
119+ source = self .source .get ()
120+ dest = self .destination .get ()
121+ min_size_bytes = self .min_file_size_kb .get () * 1024
108122
109- # Step 1: Find all files to copy
110123 files_to_copy = []
111- for root_dir , _ , files in os .walk (source ):
112- for file in files :
113- if self .cancel_requested :
114- self .log ("\n 🚫 Scan canceled by user." )
115- return
116- if any (file .lower ().endswith (ext ) for ext in PHOTO_EXTENSIONS ):
117- full_path = os .path .join (root_dir , file )
118- try :
119- if os .path .getsize (full_path ) >= min_size_bytes :
120- files_to_copy .append (full_path )
121- except Exception :
122- continue
124+
125+ if self .file_type_mode == "UserData" :
126+ for user in os .listdir (source ):
127+ user_path = os .path .join (source , user )
128+ if os .path .isdir (user_path ):
129+ files_to_copy .extend ([os .path .join (dp , f ) for dp , dn , filenames in os .walk (user_path ) for f in filenames ])
130+ else :
131+ extensions = EXTENSIONS .get (self .file_type_mode , set ())
132+ for root_dir , _ , files in os .walk (source ):
133+ for file in files :
134+ if self .cancel_requested :
135+ return
136+ if any (file .lower ().endswith (ext ) for ext in extensions ):
137+ full_path = os .path .join (root_dir , file )
138+ try :
139+ if os .path .getsize (full_path ) >= min_size_bytes :
140+ files_to_copy .append (full_path )
141+ except Exception as e :
142+ self .log (f"✘ Error reading size of { full_path } : { e } " )
123143
124144 total_files = len (files_to_copy )
125145 self .progress ["maximum" ] = total_files
126- self .progress ["value" ] = 0
127-
128- # Step 2: Copy files
129146 copied = 0
130147 failed = 0
131148
132149 for full_path in files_to_copy :
133150 if self .cancel_requested :
134- self .log ("\n 🚫 Scan canceled by user." )
151+ self .log ("🚫 Scan cancelled by user." )
135152 break
136153 try :
137154 relative_path = os .path .relpath (full_path , source )
@@ -146,12 +163,10 @@ def scan_and_copy_files(self):
146163 self .progress ["value" ] += 1
147164 self .progress .update ()
148165
149- if not self .cancel_requested :
150- self .log (f"\n ✅ Scan Complete. { copied } copied, { failed } failed.\n " )
166+ self .status_label .config (text = f"✅ Done: { copied } copied, { failed } failed, { total_files } total." )
151167 self .cancel_button .config (state = tk .DISABLED )
152168
153- # Launch the app
154169if __name__ == "__main__" :
155170 root = tk .Tk ()
156- app = FileScannerApp (root )
171+ app = FileCopyTool (root )
157172 root .mainloop ()
0 commit comments