In this version, when a new copy forces and older backup file to give space, we create a subdirectory for it in the backup directory with a name counted from 00000. There will be no name conflicts and the backup directory structure remains always at most two levels deep. If there is room for a backup file to be moved in the previous subdirectory, we use that instead, as the destination. This way the number of subdirectories can be reduced. Older backups reside in subdirectories of smaller name (the ordering is preserved).
Default values are stored in the BKup.ini file, which should be edited to the users' needs:
[Defaults]
SourceDirectory = C:\btest
BackupDirectory = C:\backup
Max_Copies = 3
Include = *.*
Exclude = .exe, temp., .tmp
The BACKUP script pops up a GUI window where the user can select (or type in) the source and backup directories, enter the limit of the number of older backup files kept, the include file pattern (currently only one, but with wildcards, like "*.doc") and the list of excluded file patterns (comma separated, but w/o wildcards, like ".exe, temp., .tmp" [use period!]). If the user accepts the settings (possibly after saving them for defaults), the backup procedure starts: from the source directory and its subdirectories each file is taken, which matches the include pattern and its name does not contain any of the exclude patterns. If it has a backup already, skip to the next file. If it has an earlier backup, move that in a subdirectory of the backup directory to make room. If the limit of the number of old backups is exceeded, remove the oldest backup of this file.
#SingleInstance Force
IniFile = BKup.ini
Gui Font, s10, Arial
Gui Add, Text, X010 Y020, Source Folder
Gui Add, Button, X185 Y013 W125 gSelectSource, Select
Gui Add, Edit, X010 Y050 W300 vDir_source
Gui Add, Text, X010 Y090, Include Pattern
Gui Add, Edit, X010 Y120 W300 vinclude
Gui Add, Text, X010 Y160, Exclude Pattern
Gui Add, Edit, X010 Y190 W300 vexclude
Gui Add, Text, X360 Y020, Backup Folder
Gui Add, Button, X535 Y013 W125 gSelectBackupDir, Select
Gui Add, Edit, X360 Y050 W300 vDir_backup
Gui Add, Text, X360 Y090, Max Copies
Gui Add, Edit, X360 Y120 W125 vmax_copies
Gui Add, UpDown, Range1-999
Gui Add, Button, X533 Y088 W125, Get Defaults
Gui Add, Button, X533 Y138 W125, Set Defaults
Gui Add, Button, X360 Y188 W125 Default, &Accept
Gui Add, Button, X533 Y188 W125, &Cancel
GoSub ButtonGetDefaults
Gui Show, W675 H240, Backup
Return
SelectSource:
Gui +OwnDialogs ; Following dialog must be closed before using main window
FileSelectFolder Folder, *%Dir_source%, 2, Select the Source folder:
if not Folder ; The user canceled the dialog
Return
Dir_source = %Folder%
GuiControl,,Dir_Source, %Dir_source%
Return
SelectBackupDir:
Gui +OwnDialogs ; Following dialog must be closed before using main window
FileSelectFolder Folder, *%Dir_backup%, 3, Select the Backup folder:
if not Folder ; The user canceled the dialog
Return
Loop %Folder%\*.*, 2 ; Reject non-backup folders
{
If A_LoopFileName is Integer
Continue
If A_LoopFileName not contains ``
{
MsgBox NOT A BACKUP FOLDER
Return
}
}
Dir_backup = %Folder%
GuiControl,,Dir_Backup, %Dir_backup%
Return
ButtonGetDefaults:
IniRead Dir_source, %IniFile%, Defaults, SourceDirectory
IniRead Dir_backup, %IniFile%, Defaults, BackupDirectory
IniRead max_copies, %IniFile%, Defaults, Max_Copies
IniRead include, %IniFile%, Defaults, Include
IniRead exclude, %IniFile%, Defaults, Exclude
GuiControl,,Dir_Backup, %Dir_backup%
GuiControl,,Dir_Source, %Dir_source%
GuiControl,,include, %include%
GuiControl,,exclude, %exclude%
GuiControl,,max_copies, %max_copies%
Return
ButtonSetDefaults:
Gui Submit, Nohide
IniWrite %Dir_source%, %IniFile%, Defaults, SourceDirectory
IniWrite %Dir_backup%, %IniFile%, Defaults, BackupDirectory
IniWrite %max_copies%, %IniFile%, Defaults, Max_Copies
IniWrite %include%, %IniFile%, Defaults, Include
IniWrite %exclude%, %IniFile%, Defaults, Exclude
Return
GuiClose:
GuiEscape:
ButtonCancel:
Gui Destroy
ExitApp
Return
; PERFORM THE BACKUP
ButtonAccept:
Gui Submit ; assign gui variables
Gui Destroy
Dir_backup = %Dir_backup%\
StringReplace Dir_backup, Dir_backup, \\, \
Dir_source = %Dir_source%\
StringReplace Dir_source, Dir_source, \\, \
StringReplace exclude, exclude, %A_SPace%,,All
StringReplace exclude, exclude, %A_Tab%,,All
Loop %Dir_backup%*, 2 ; look at subdirectories
if dir_bb < %A_LoopFileName%
dir_bb = %A_LoopFileName% ; latest subdirectory
IfEqual dir_bb,, {
Dir_prev = %Dir_backup% ; if none: previous <- backup root
dir_bb = 00000
}
Else {
Dir_prev = %Dir_backup%\%dir_bb%\
SetFormat Float, 05.0
dir_bb += 1.0
}
FileCreateDir %Dir_backup%%dir_bb% ; directory for older backup files
Loop %Dir_source%%include%,,1 ; recurse into subdirectories
{
If A_LoopFileName contains %exclude%
Continue
path = %A_LoopFileFullPath% ; assumed path does not contain "`"
ToolTip Processing %path%
StringReplace file, path, \, ``, All
StringReplace file, file, :,, All ; \ -> `, delete ":"
IfExist %Dir_backup%%file%
{
FileGetTime sourceTime, %path%
FileGetTime backupTime, %Dir_backup%%file%
IfEqual sourceTime,%backupTime%, Continue
IfExist %Dir_prev%%file% ; true if no previous subdir exists
FileMove %Dir_backup%%file%, %Dir_backup%%dir_bb%\%file%
Else
FileMove %Dir_backup%%file%, %Dir_prev%%file%
Count = 0
dir_Oldest = z ; z is larger than any dir_bb name
Loop %Dir_backup%%file%,,1
{
Count++ ; count copies in subdirectories
If A_LoopFileDir < %dir_Oldest%
dir_Oldest = %A_LoopFileDir%
}
If Count > %max_copies%
{
FileDelete %dir_Oldest%\%file%
FileRemoveDir %dir_Oldest% ; remove if empty
}
}
FileCopy %path%, %Dir_backup%%file%
}
ToolTip ; remove
FileRemoveDir %Dir_backup%%dir_bb% ; remove if empty
ExitApp
The RESTORE script has a resizable GUI, based on the ListView demo in the Help. The user loads the files from his backup directories (selecting them one-by-one) to the ListView, and there multiple backup files can be selected (Ctrl/Shift-Click) to be restored. (All the important information is shown about them, like their icon, original location, backup location, size, time of creation and the encoded filename. Double clicking on a file opens it, useful for verification.) Pressing the Restore button the selected files get processed one-by-one. They get copied over to their original place, overwriting a file with the same name there, if any.
#SingleInstance Force
#NoEnv
SetBatchLines -1
IniFile = BKup.ini
Gui +Resize
Gui Font, s9, Arial
Gui Add, Button, Default gButtonLoadFolder, Load a folder
Gui Add, Button, x+20 gButtonClear, Clear List
Gui Add, Button, x+20, Switch View
Gui Add, Button, x+20, Restore
Gui Add, ListView, xm r20 w700 vMyListView gMyListView, Name|Folder|Size KB|Date-Time|Code
LV_ModifyCol(3, "Integer")
ImageListID1 := IL_Create(10,10) ; Create ImageList so that ListView can display icons
ImageListID2 := IL_Create(10,10,1) ; A list of large icons to go with the small ones.
LV_SetImageList(ImageListID1) ; Attach the ImageLists to the ListView...
LV_SetImageList(ImageListID2) ; so that it can later display the icons
Menu MyContextMenu, Add, Open, ContextOpenFile ; Popup menu to be used as the context menu
Menu MyContextMenu, Add, Properties, ContextProperties
Menu MyContextMenu, Add, Clear from ListView, ContextClearRows
Menu MyContextMenu, Default, Open ; "Open" bold font indicating that double-click does the same
Gui Show
Return
ButtonLoadFolder:
Gui +OwnDialogs ; User must dismiss the following dialog before using main window
IniRead dir_backup, %IniFile%, Defaults, BackupDirectory
FileSelectFolder Folder, *%dir_backup%, 2, Select the Backup folder:
If not Folder ; The user canceled the dialog.
Return
Loop %Folder%\*.*, 2 ; Reject non-backup folders
{
If A_LoopFileName is Integer
Continue
If A_LoopFileName not contains ``
{
MsgBox NOT A BACKUP FOLDER
Return
}
}
StringRight, LastChar, Folder, 1
If LastChar = \
StringTrimRight, Folder, Folder, 1 ; Remove trailing backslash
VarSetCapacity(FileName, 260) ; Variable need to hold the longest path: ExtractAssociatedIconA()
sfi_size = 352
VarSetCapacity(sfi, sfi_size)
Loop %Folder%\*.* ; File names appended to the ListView
{
FileCode = %A_LoopFileName% ; Save it to a variable overwritten below
StringReplace FileName, FileCode, ``, :\
StringReplace FileName, FileName, ``, \, All
SplitPath FileName,,,FileExt ; Get extension
If FileExt in EXE,DLL,BMP,JPG,JPEG,TIF
{
ExtID := FileExt ; Special ID as a placeholder.
IconNumber = 0 ; Flag it as not found so that EXEs can each have a unique icon.
} Else { ; Non-EXE, so calculate this extension's unique ID.
ExtID = 0 ; Initializize to handle short extensions
StringLeft Ext8, FileExt, 8
Loop Parse, Ext8 ; 8 chars < 128 of ext, they fit in positive Int64
ExtID += Asc(A_LoopField) << 8*A_Index-8
IconNumber := IconArray%ExtID% ; Speed up with icons already loaded
}
If not IconNumber ; Not icon for this extension loaded yet
{ ; Get the icon associated with this file extension
If !DllCall("Shell32\SHGetFileInfoA", Str,Folder "\" FileCode, Uint,0, Str,sfi, Uint,sfi_size, Uint,0x101) ; 0x101 is SHGFI_ICON+SHGFI_SMALLICON
IconNumber = 9999999 ; Set it out of bounds to display a blank icon.
Else {
hIcon = 0 ; Add HICON to the small-icon and large-icon lists
Loop 4
hIcon += *(&sfi + A_Index-1) << 8*A_Index-8
IconNumber := 1+DllCall("ImageList_ReplaceIcon", Uint,ImageListID1, Int,-1, Uint,hIcon)
DllCall("ImageList_ReplaceIcon", UInt,ImageListID2, Int,-1, UInt,hIcon)
DllCall("DestroyIcon", Uint,hIcon)
IconArray%ExtID% :=IconNumber ; Cache the icon
}
}
FileGetTime FileTime, %A_LoopFileName%
FormatTime, FileTime, FileTime, yyyy.MM.dd-HH:mm:ss
LV_Add("Icon" IconNumber, FileName, A_LoopFileDir, A_LoopFileSizeKB, FileTime, A_LoopFileName)
}
LV_ModifyCol() ; Auto-size each column to fit its contents.
LV_ModifyCol(3,70) ; Make the Size column at little wider to reveal its header
LV_ModifyCol(4,150)
Return
ButtonClear:
LV_Delete() ; Clear the ListView, but keep icon cache intact
Return
ButtonSwitchView:
If not IconView
GuiControl +Icon, MyListView ; Switch to icon view
Else
GuiControl +Report, MyListView ; Switch back to details view
IconView := not IconView ; Invert in preparation for next time
Return
ButtonRestore:
RowNum = 0 ; Start the search at the top of the list
Loop {
RowNum := LV_GetNext(RowNum) ; Continue search after that of previous iteration
If not RowNum ; No more selected rows
break
LV_GetText(FileName,RowNum, 1) ; Get the text of the first field
LV_GetText(FileDir, RowNum, 2)
LV_GetText(FileCode,RowNum, 5)
ToolTip Restoring %FileDir%\%FileCode% -> %FileName%
SplitPath FileName,,Dir0 ; Get the file's original directory
FileCreateDir %Dir0% ; Create it, if necessary <-- add other dir selection
FileCopy %FileDir%\%FileCode%, %FileName%, 1
}
ToolTip ; remove
Return
MyListView:
If A_GuiEvent = DoubleClick
{
LV_GetText(FileCode,A_EventInfo, 5) ; Get the text of field 5
LV_GetText(FileDir, A_EventInfo, 2) ; Get the text of field 2
Run %FileDir%\%FileCode%,, UseErrorLevel
If ErrorLevel
MsgBox Could not open "%FileDir%\%FileCode%".
}
Return
GuiContextMenu: ; At right-click or press of the Apps key
If A_GuiControl <> MyListView ; Display the menu only for clicks inside the ListView
Return
Menu MyContextMenu, Show, %A_GuiX%, %A_GuiY%
Return
ContextOpenFile: ; "Open" is selected in the context menu
ContextProperties: ; "Properties" is selected in the context menu
FocusedRowNumber := LV_GetNext(0, "F") ; Find the focused row
If not FocusedRowNumber ; No row is focused
Return
LV_GetText(FileDir, FocusedRowNumber, 2) ; Get the text of the second field
LV_GetText(FileCode,FocusedRowNumber, 5) ; Get the text of the first field
IfInString A_ThisMenuItem, Open ; User selected "Open" from the context menu
Run %FileDir%\%FileCode%,, UseErrorLevel
Else ; User selected "Properties" from the context menu
Run Properties "%FileDir%\%FileCode%",, UseErrorLevel
If ErrorLevel
MsgBox Could not perform requested action on "%FileDir%\%FileCode%".
Return
ContextClearRows: ; The user selected "Clear" in the context menu
RowNumber = 0 ; The first iteration to start the search at the top
Loop {
RowNumber := LV_GetNext(RowNumber - 1)
If not RowNumber ; No more selected rows
break
LV_Delete(RowNumber) ; Clear the row from the ListView
}
Return
GuiSize: ; User resizing window
If A_EventInfo = 1 ; No action when window is minimized
Return
GuiControl Move, MyListView, % "W" (A_GuiWidth - 20) " H" (A_GuiHeight - 40)
Return
GuiClose: ; When the window is closed, exit the script automatically
ExitApp
The backup directory should be a compressed Windows directory to trade storage space to access time.
It is version 0.1, so there could be some bugs. Please report them! Also, extend the functionality:
- Error checking
- Multiple include patterns with wildcards (e.g. My Documents\*.doc, D:\Projects\*.ahk)
- Mark the directories which are to be processed recursively with a leading +. Without it only the top level is processed.
- Exclude patterns with wildcards (e.g. ~W*.tmp, $??$.t*)
- Filter restore files with patterns, time ranges…
Edit 20061020:
- Handle restore to non-existent directories
- Improved icons