1212import subprocess
1313import clipboard
1414import time
15- from sys import argv
1615
1716# Set meta info
18- appVersion = '1.0.0 '
17+ appVersion = '1.0.1 '
1918
2019# TODO: When loading a config, warn if drive in config isn't connected
2120# If replacement drive is selected that gives sufficient size, prompt for replace confirmation
2625# This would prevent counting for existing data, though it's probably safe to wipe the drive of things that aren't getting copied anyway
2726# When we copy, check directory size of source and dest, and if the dest is larger than source, copy those first to free up space for ones that increased
2827# TODO: Add a button for deleting the config from selected drives
29-
30- # Disallow CLI options
3128# TODO: Add interactive CLI option if correct parameters are passed in
32- if len (argv ) > 1 :
33- print ('CLI options are not currently supported. Please use the GUI.' )
34- exit ()
3529
3630# Centers a tkinter window
3731def center (win ):
@@ -80,7 +74,7 @@ class color:
8074 GREEN = '#6db500'
8175 GOLD = '#ebb300'
8276 RED = '#c00'
83- GRAY = '#aaa '
77+ GRAY = '#999 '
8478
8579 FINISHED = GREEN
8680 RUNNING = BLUE
@@ -91,8 +85,8 @@ class color:
9185sourceDrive = None
9286backupConfigFile = 'backup.config'
9387appConfigFile = 'defaults.config'
94- appDataFolder = os .getenv ('LocalAppData' ) + '\\ TechGeek01UnraidBackup '
95- halfPadding = 8
88+ appDataFolder = os .getenv ('LocalAppData' ) + '\\ BackDrop '
89+ elemPadding = 16
9690
9791config = {
9892 'shares' : [],
@@ -270,10 +264,7 @@ def analyzeBackup(shares, drives):
270264
271265 for i , drive in enumerate (driveInfo ):
272266 # Get list of shares small enough to fit on drive
273- smallShares = {}
274- for share , size in shareInfo .items ():
275- if size <= drive ['size' ]:
276- smallShares [share ] = size
267+ smallShares = {share : size for share , size in shareInfo .items () if size <= drive ['size' ]}
277268
278269 # Try every combination of shares that fit to find result that uses most of that drive
279270 largestSum = 0
@@ -317,8 +308,7 @@ def analyzeBackup(shares, drives):
317308 driveShareList [drive ['name' ]].extend (sharesThatFit ) # Shares that fit on current drive
318309
319310 # Put remaining small shares back into pool to work with for next drive
320- for (share , size ) in remainingSmallShares .items ():
321- shareInfo [share ] = size
311+ shareInfo .update ({share : size for share , size in remainingSmallShares .items ()})
322312 else :
323313 # Fit all small shares onto drive
324314 driveShareList [drive ['name' ]].extend (sharesThatFit )
@@ -348,10 +338,7 @@ def splitShare(share):
348338
349339 for i , drive in enumerate (driveInfo ):
350340 # Get list of files small enough to fit on drive
351- totalSmallFiles = {}
352- for file , size in fileInfo .items ():
353- if size <= drive ['free' ]:
354- totalSmallFiles [file ] = size
341+ totalSmallFiles = {file : size for file , size in fileInfo .items () if size <= drive ['free' ]}
355342
356343 # Since the list of files is truncated to prevent an unreasonably large
357344 # number of combinations to check, we need to keep processing the file list
@@ -452,9 +439,7 @@ def splitShare(share):
452439 rawExclusions = allFiles .copy ()
453440 rawExclusions .pop (drive , None )
454441
455- masterExclusions = []
456- for files in rawExclusions .values ():
457- masterExclusions .extend (files )
442+ masterExclusions = [files for files in rawExclusions .values ()]
458443
459444 fileExclusions = [sourcePathStub + file for file in masterExclusions if os .path .isfile (sourcePathStub + file )]
460445 dirExclusions = [sourcePathStub + file for file in masterExclusions if os .path .isdir (sourcePathStub + file )]
@@ -521,7 +506,7 @@ def startBackupAnalysis():
521506root .attributes ('-alpha' , 1.0 )
522507
523508mainFrame = tk .Frame (root )
524- mainFrame .pack (fill = 'both' , expand = 1 , padx = halfPadding , pady = halfPadding )
509+ mainFrame .pack (fill = 'both' , expand = 1 , padx = elemPadding , pady = ( elemPadding / 2 , elemPadding ) )
525510
526511# Set some default styling
527512buttonWinStyle = ttk .Style ()
@@ -576,13 +561,13 @@ def readSettingFromFile(file, default, verifyData = None):
576561
577562# Tree frames for tree and scrollbar
578563sourceTreeFrame = tk .Frame (mainFrame )
579- sourceTreeFrame .grid (row = 1 , column = 0 , sticky = 'ns' , padx = halfPadding , pady = ( halfPadding , 0 ) )
564+ sourceTreeFrame .grid (row = 1 , column = 0 , sticky = 'ns' )
580565destTreeFrame = tk .Frame (mainFrame )
581- destTreeFrame .grid (row = 1 , column = 1 , sticky = 'ns' , padx = halfPadding , pady = ( halfPadding , 0 ))
566+ destTreeFrame .grid (row = 1 , column = 1 , sticky = 'ns' , padx = ( elemPadding , 0 ))
582567
583568# Progress/status values
584569progressBar = ttk .Progressbar (mainFrame , maximum = 100 )
585- progressBar .grid (row = 10 , column = 0 , columnspan = 3 , sticky = 'ew' , padx = halfPadding , pady = halfPadding )
570+ progressBar .grid (row = 10 , column = 0 , columnspan = 3 , sticky = 'ew' , pady = ( elemPadding , 0 ) )
586571
587572sourceTree = ttk .Treeview (sourceTreeFrame , columns = ('size' , 'rawsize' ))
588573sourceTree .heading ('#0' , text = 'Share' )
@@ -599,7 +584,7 @@ def readSettingFromFile(file, default, verifyData = None):
599584# There's an invisible 1px background on buttons. When changing this in icon buttons, it becomes
600585# visible, so 1px needs to be added back
601586sourceMetaFrame = tk .Frame (mainFrame )
602- sourceMetaFrame .grid (row = 2 , column = 0 , sticky = 'nsew' , padx = halfPadding , pady = (1 , halfPadding ))
587+ sourceMetaFrame .grid (row = 2 , column = 0 , sticky = 'nsew' , pady = (1 , elemPadding ))
603588tk .Grid .columnconfigure (sourceMetaFrame , 0 , weight = 1 )
604589
605590shareSpaceFrame = tk .Frame (sourceMetaFrame )
@@ -647,7 +632,7 @@ def changeSourceDrive(selection):
647632 writeSettingToFile (sourceDrive , appDataFolder + '\\ sourceDrive.default' )
648633
649634sourceSelectFrame = tk .Frame (mainFrame )
650- sourceSelectFrame .grid (row = 0 , column = 0 )
635+ sourceSelectFrame .grid (row = 0 , column = 0 , pady = ( 0 , elemPadding / 2 ) )
651636tk .Label (sourceSelectFrame , text = 'Source:' ).pack (side = 'left' )
652637sourceSelectMenu = ttk .OptionMenu (sourceSelectFrame , sourceDriveDefault , sourceDrive , * tuple (remoteDrives ), command = changeSourceDrive )
653638sourceSelectMenu .pack (side = 'left' , padx = (12 , 0 ))
@@ -767,8 +752,7 @@ def loadDest():
767752 try :
768753 for physicalDisk in wmi .WMI ().Win32_DiskDrive ():
769754 for partition in physicalDisk .associators ("Win32_DiskDriveToDiskPartition" ):
770- for logicalDisk in partition .associators ("Win32_LogicalDiskToPartition" ):
771- logicalPhysicalMap [logicalDisk .DeviceID [0 ]] = physicalDisk .SerialNumber .strip ()
755+ logicalPhysicalMap .update ({logicalDisk .DeviceID [0 ]: physicalDisk .SerialNumber .strip () for logicalDisk in partition .associators ("Win32_LogicalDiskToPartition" )})
772756 finally :
773757 pythoncom .CoUninitialize ()
774758
@@ -817,7 +801,7 @@ def startRefreshDest():
817801# There's an invisible 1px background on buttons. When changing this in icon buttons, it becomes
818802# visible, so 1px needs to be added back
819803destMetaFrame = tk .Frame (mainFrame )
820- destMetaFrame .grid (row = 2 , column = 1 , sticky = 'nsew' , padx = halfPadding , pady = (1 , halfPadding ))
804+ destMetaFrame .grid (row = 2 , column = 1 , sticky = 'nsew' , pady = (1 , elemPadding ))
821805tk .Grid .columnconfigure (destMetaFrame , 0 , weight = 1 )
822806
823807driveSpaceFrame = tk .Frame (destMetaFrame )
@@ -835,21 +819,15 @@ def startRefreshDest():
835819# Using the current config, make selections in the GUI to match
836820def selectFromConfig ():
837821 global driveSelectBind
838- sourceShareList = sourceTree .get_children ()
839- sourceTreeIdList = []
840- for item in sourceShareList :
841- if sourceTree .item (item , 'text' ) in config ['shares' ]:
842- sourceTreeIdList .append (item )
822+
823+ # Get list of shares in config
824+ sourceTreeIdList = [item for item in sourceTree .get_children () if sourceTree .item (item , 'text' ) in config ['shares' ]]
843825
844826 sourceTree .focus (sourceTreeIdList [- 1 ])
845827 sourceTree .selection_set (tuple (sourceTreeIdList ))
846828
847- driveDestList = destTree .get_children ()
848- driveTreeIdList = []
849- for item in driveDestList :
850- driveVid = destTree .item (item , 'values' )[3 ]
851- if driveVid in config ['vidList' ]:
852- driveTreeIdList .append (item )
829+ # Get list of drives where volume ID is in config
830+ driveTreeIdList = [item for item in destTree .get_children () if destTree .item (item , 'values' )[3 ] in config ['vidList' ]]
853831
854832 # Only redo the selection if the config data is different from the current
855833 # selection (that is, the drive we selected to load a config is not the only
@@ -962,19 +940,16 @@ def selectDriveInBackground(event):
962940# TODO: Make changes to existing config check the existing for missing drives, and delete the config file from drives we unselected if there's multiple drives in a config
963941def writeConfigFile ():
964942 if len (config ['shares' ]) > 0 and len (config ['drives' ]) > 0 :
965- driveLetters = []
966- for drive in config ['drives' ]:
967- driveLetters .append (destDriveMap [drive ['vid' ]])
943+ driveConfigList = '' .join (['\n %s,%s' % (drive ['vid' ], drive ['serial' ]) for drive in config ['drives' ]])
968944
969945 # For each drive letter, get drive info, and write file
970- for drive in driveLetters :
971- f = open ('%s:/%s' % (drive , backupConfigFile ), 'w' )
946+ for drive in config [ 'drives' ] :
947+ f = open ('%s:/%s' % (destDriveMap [ drive [ 'vid' ]] , backupConfigFile ), 'w' )
972948 # f.write('[id]\n%s,%s\n\n' % (driveInfo['vid'], driveInfo['serial']))
973949 f .write ('[shares]\n %s\n \n ' % (',' .join (config ['shares' ])))
974- f .write ('[drives]' )
975950
976- for confDrive in config [ ' drives' ]:
977- f .write (' \n %s,%s' % ( confDrive [ 'vid' ], confDrive [ 'serial' ]) )
951+ f . write ( '[ drives]' )
952+ f .write (driveConfigList )
978953
979954 f .close ()
980955 else :
@@ -984,7 +959,7 @@ def writeConfigFile():
984959# Add activity frame for backup status output
985960tk .Grid .rowconfigure (mainFrame , 5 , weight = 1 )
986961backupActivityFrame = tk .Frame (mainFrame )
987- backupActivityFrame .grid (row = 5 , column = 0 , columnspan = 2 , sticky = 'nsew' , padx = halfPadding , pady = halfPadding )
962+ backupActivityFrame .grid (row = 5 , column = 0 , columnspan = 2 , sticky = 'nsew' )
988963
989964backupActivityInfoCanvas = tk .Canvas (backupActivityFrame )
990965backupActivityInfoCanvas .pack (side = 'left' , fill = 'both' , expand = 1 )
@@ -1007,10 +982,10 @@ def writeConfigFile():
1007982tk .Grid .columnconfigure (mainFrame , 2 , weight = 1 )
1008983
1009984rightSideFrame = tk .Frame (mainFrame )
1010- rightSideFrame .grid (row = 0 , column = 2 , rowspan = 6 , sticky = 'nsew' , padx = halfPadding , pady = halfPadding )
985+ rightSideFrame .grid (row = 0 , column = 2 , rowspan = 6 , sticky = 'nsew' , pady = ( elemPadding / 2 , 0 ) )
1011986
1012987backupSummaryFrame = tk .Frame (rightSideFrame )
1013- backupSummaryFrame .pack (fill = 'both' , expand = 1 , padx = halfPadding , pady = halfPadding )
988+ backupSummaryFrame .pack (fill = 'both' , expand = 1 , padx = ( elemPadding , 0 ) )
1014989backupSummaryFrame .update ()
1015990
1016991backupTitle = tk .Label (backupSummaryFrame , text = 'Analysis Summary' , font = (None , 20 ))
@@ -1051,39 +1026,13 @@ def runBackup():
10511026 process = subprocess .Popen (cmd , shell = True , stdout = subprocess .PIPE , stdin = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
10521027 # process = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
10531028
1054- # isDone = False
1055- # finalLine = 0
1056-
10571029 while not backupHalted and process .poll () is None :
10581030 try :
10591031 out = process .stdout .readline ().decode ().strip ()
10601032 cmdInfoBlocks [i ]['state' ].configure (text = 'Running' , fg = color .RUNNING )
1061- # print(out, end = '')
1062- # out = re.sub('\s+', ' ', out)
1063- # outParts = out.split()
1064-
10651033 cmdInfoBlocks [i ]['lastOutResult' ].configure (text = out .strip (), fg = color .NORMAL )
1066-
1067- # if not isDone and outParts[0] == 'Total':
1068- # isDone = True
1069-
1070- # for headIndex, heading in enumerate(outParts):
1071- # tk.Label(cmdInfoBlocks[i]['statusStatsFrame'], text = heading, font = (None, 10, 'bold')).grid(row = 0, column = headIndex + 1)
1072- # elif isDone and out != '':
1073- # outParts = re.sub(' : ', ' ', out).split()
1074- # print(outParts[0])
1075- # if outParts[0] in ['Dirs', 'Files']:
1076- # finalLine += 1
1077- # for statIndex, stat in enumerate(outParts):
1078- # stat = (stat + ':') if statIndex == 0 else stat
1079- # font = (None, 10)
1080-
1081- # tk.Label(cmdInfoBlocks[i]['statusStatsFrame'], text = stat, font = font).grid(row = finalLine, column = statIndex)
1082- # else:
1083- # print(out)
10841034 except Exception as e :
10851035 pass
1086- # print("\n ______ _ _ _____ _ __\n | ____| | | | | / ____| | |/ /\n | |__ | | | | | | | ' / \n | __| | | | | | | | < \n | | | |__| | | |____ | . \\ \n |_| \\____/ \\_____| |_|\\_\\n \n")
10871036 process .terminate ()
10881037
10891038 if not backupHalted :
@@ -1123,7 +1072,7 @@ def killBackup():
11231072tk .Label (backupSummaryTextFrame , text = 'Please start a backup analysis to generate a summary.' ,
11241073 wraplength = backupSummaryFrame .winfo_width () - 2 , justify = 'left' ).pack (anchor = 'w' )
11251074startBackupBtn = ttk .Button (backupSummaryFrame , text = 'Run Backup' , command = startBackup , state = 'disable' , style = 'win.TButton' )
1126- startBackupBtn .pack (padx = halfPadding , pady = halfPadding )
1075+ startBackupBtn .pack (pady = elemPadding / 2 )
11271076
11281077loadThread = threading .Thread (target = loadDest , name = 'Init' , daemon = True )
11291078loadThread .start ()
0 commit comments