; snapcontrol.pb ; https://github.com/mhgschmidt/snapcontrol ; ------------------------------------------------------------ ; Drive snapshot backup controller ; - starts backups using drive snapshot ; - sends reports via email ; LICENSE : MIT License ; AUTHOR : Michael H.G. Schmidt ; EMAIL : michael@schmidt2.de ; DATE : 20240222 ; ------------------------------------------------------------ ; ; This tool creates an image backups of windows machines, ; it uses the Drive Snapshot tool by Tom Ehlert Software. ; Please support this great tool and buy a license. ; http://www.drivesnapshot.de/en/order.htm ; ; Copyright (c) 2019-2021 Michael H.G. Schmidt ; Released under the MIT license. ; ; ; INIT (set vars, read args, read inifile etc. pp.) ; OpenConsole() EnableGraphicalConsole(0) Dim filelist$(0) Global VERSION$="V1.13" Global updatesched = 0 Global dryrun = 0 Global silentmode = 0 Global hostname$ = GetEnvironmentVariable("COMPUTERNAME") Global jobname$ = "snapcontrol" Global month$ = FormatDate("%mm", Date()) Global day$ = FormatDate("%dd", Date()) Global DriveSnapshotVersionOk = 0 Global DoShutdown = 0 Global RC = 0 ; returncode Global RC_ALL = 0 ; cumulated returncode ; Timeout for shutodwn is 2 minutes ... Global Shutdowncommand$ = "shutdown /s /t 120 /d p:0:0" ; valid versions for Drive Snapshot we support ... NewList DriveSnapshotVersion$() For i = 46 To 55 AddElement(DriveSnapshotVersion$()) DriveSnapshotVersion$() = "V1." + Str(i) Next Procedure Usage() PrintN ("usage: snapcontrol.exe [ /S | /I | /V | /D | /? ]") PrintN (" /S = silent mode (no user interaction!)") PrintN (" /I = install/update scheduler job") PrintN (" /V = show version") PrintN (" /D = dry run") PrintN (" /? = help") PrintN ("") PrintN ("RETURNCODES:") PrintN ("-----------------------------------------------------") PrintN ("99 = usage was called / wrong arguments or parameters") PrintN ("98 = cannot read inifile (does not exist ?)") PrintN ("97 = i don't have admin rights") PrintN ("96 = mailer (cmail) not found or not working") PrintN ("95 = Drive Snapshot version is not supported") PrintN ("94 = Drive Snapshot exe not found") PrintN (" 0 = OK") End 99 EndProcedure ; check commandline... If ( CountProgramParameters() > 1 ) Usage() EndIf ; install or update job, show version, dry run ... If ( UCase(ProgramParameter(0)) = "/I" ) updatesched = 1 ElseIf ( UCase(ProgramParameter(0)) = "/V" ) PrintN ("Version = " + VERSION$) End 0 ElseIf ( UCase(ProgramParameter(0)) = "/D" ) dryrun = 1 ElseIf ( UCase(ProgramParameter(0)) = "/S" ) silentmode = 1 ElseIf ( ProgramParameter(0) = "/?" ) Usage() EndIf ; set path for inifile ... inifile$ = RTrim(GetPathPart(ProgramFilename()),"\") + "\snapcontrol.ini" ; Procedure "RunProgram" aborts after writing approx. 8GB data with snapshot64... ; so we will use native "system" command... ImportC "msvcrt.lib" system(str.p-ascii) EndImport If (Not OpenPreferences(inifile$)) PrintN ("FATAL: cannot read inifile [ " + inifile$ + " ] ---> EXIT.") End 98 EndIf PreferenceGroup("backup") Global BinPath$ = Trim(ReadPreferenceString("BinPath","c:\tools")) Global TargetPath$ = Trim(ReadPreferenceString("TargetPath","\\server\share")) Global FtpBackup$ = Trim(ReadPreferenceString("FtpBackup","no")) Global FtpServer$ = Trim(LCase(ReadPreferenceString("FtpServer","none"))) Global TargetUser$ = Trim(ReadPreferenceString("TargetUser","guest")) Global TargetPassword$ = Trim(ReadPreferenceString("TargetPassword","guest")) Global BackupschedMode$ = Trim(LCase(ReadPreferenceString("BackupschedMode","login"))) Global BackupStart$ = Trim(ReadPreferenceString("BackupStart","0005:00")) Global Disks2Dump$ = Trim(ReadPreferenceString("Disks2Dump","HD1:*")) Global ExcludeList$ = Trim(ReadPreferenceString("ExcludeList","")) Global DumpSize$ = Trim(ReadPreferenceString("DumpSize","4095")) Global Verify$ = Trim(LCase(ReadPreferenceString("Verify","yes"))) Global BurnTrash$ = Trim(LCase(ReadPreferenceString("BurnTrash","no"))) Global Encrypt$ = Trim(LCase(ReadPreferenceString("Encrypt","no"))) Global EncryptPW$ = Trim(ReadPreferenceString("EncryptPW","")) Global LimitIO$ = Trim(ReadPreferenceString("LimitIO","")) Global EjectMedia$ = Trim(LCase(ReadPreferenceString("EjectMedia","yes"))) Global AskForShutdown$ = Trim(LCase(ReadPreferenceString("AskForShutdown","no"))) Global ForcedShutdown$ = Trim(LCase(ReadPreferenceString("ForcedShutdown","no"))) PreferenceGroup("logging") Global LogDir$ = Trim(ReadPreferenceString("LogDir","C:")) Global LogFile$ = Trim(ReadPreferenceString("LogFile","snapshot-backup.log")) Global HistLog$ = Trim(ReadPreferenceString("HistoryLog","snapshot-history.log")) ; add PATH to logfiles ... LogFile$ = LogDir$ + "\" + LogFile$ HistLog$ = LogDir$ + "\" + HistLog$ PreferenceGroup("mail") Global MailReport$ = Trim(LCase(ReadPreferenceString("MailReport","no"))) Global MailDebug$ = Trim(ReadPreferenceString("MailDebug","no")) Global MailFrom$ = Trim(ReadPreferenceString("MailFrom","")) Global MailTo$ = Trim(ReadPreferenceString("MailTo","")) Global MailServer$ = Trim(ReadPreferenceString("MailServer","")) Global MailUser$ = Trim(ReadPreferenceString("MailUser","")) Global MailPass$ = Trim(ReadPreferenceString("MailPass","")) ; which Drive Snapshot version should be used ? arch$ = GetEnvironmentVariable("PROCESSOR_ARCHITECTURE") If ( arch$ = "x86" ) SnapshotBin$ = "c:\windows\snapshot.exe" Else SnapshotBin$ = "c:\windows\snapshot64.exe" EndIf ; Drive Snapshot found ? If ( FileSize ( SnapshotBin$ ) < 0 ) PrintN("ERROR: [ " + SnapshotBin$ + " ] NOT found.") End 94 EndIf Global MailCommand$ = BinPath$ + "\cmail.exe" ; CMail found ? If ( FileSize ( MailCommand$ ) < 0 And MailReport$ = "yes") PrintN("ERROR: [ " + MailCommand$ + " ] NOT found.") End 96 EndIf ; Version check for Drive Snapshot ... DriveSnapshot = RunProgram(SnapshotBin$, "/?", "", #PB_Program_Open | #PB_Program_Read) If DriveSnapshot While ProgramRunning(DriveSnapshot) Out$ = Out$ + ReadProgramString(DriveSnapshot) Wend CloseProgram(DriveSnapshot) EndIf ForEach DriveSnapshotVersion$() If ( FindString(Out$, DriveSnapshotVersion$()) >0 ) DriveSnapshotVersionOk = 1 Break EndIf Next If ( DriveSnapshotVersionOk = 0 ) PrintN("ERROR: Drive Snapshot version is NOT supported!") PrintN(" Supported versions are:") ForEach DriveSnapshotVersion$() PrintN(" - " + DriveSnapshotVersion$()) Next End 95 EndIf ; ; PROCEDURES ; Procedure LogMe(Message$) Protected h = OpenFile(#PB_Any, LogFile$, #PB_File_Append | #PB_File_NoBuffering | #PB_Ascii | #PB_File_SharedRead) If h WriteStringN(h, FormatDate("[%yyyy.%mm.%dd (%hh:%ii:%ss)] ", Date()) + Message$) PrintN(FormatDate("[%yyyy.%mm.%dd (%hh:%ii:%ss)] ", Date()) + Message$) CloseFile(h) ProcedureReturn 1 EndIf ProcedureReturn 0 EndProcedure Procedure LogMeRaw(Message$) Protected h = OpenFile(#PB_Any, LogFile$, #PB_File_Append | #PB_File_NoBuffering | #PB_Ascii | #PB_File_SharedRead) If h WriteStringN(h, Message$) PrintN(Message$) CloseFile(h) ProcedureReturn 1 EndIf ProcedureReturn 0 EndProcedure Procedure IsAdmin() ProcedureReturn(system("reg.exe ADD HKLM /F >nul 2>&1")) EndProcedure Procedure LogSend(subject$) ; use debug mode ? If (MailDebug$ = "yes" ) MailCommand$ + " -d" EndIf ; send mails ALWAYS with STARTTLS... MailCommand$ + " -starttls -host" MailCommand$ + ":" + MailUser$ MailCommand$ + ":" + Chr(34) + MailPass$ + Chr(34) MailCommand$ + "@" + MailServer$ MailCommand$ + " -from:" + MailFrom$ MailCommand$ + " -to:" + MailTo$ MailCommand$ + " -subject:" + Chr(34) + subject$ + Chr(34) MailCommand$ + " -body-file:" + LogFile$ ; show full command in debug mode ... If (MailDebug$ = "yes" ) PrintN(MailCommand$) EndIf dummy=system(MailCommand$) ProcedureReturn true EndProcedure Procedure EndProg(err) Protected s$ = "" If ( err = 0 ) LogMe("INFO: END of BACKUP with result SUCCESS !") s$ = "BACKUP report - { SUCCESS } @" + hostname$ Else LogMe("ERROR: END of BACKUP with result FAILED !") s$ = "BACKUP report - { ERROR ! } @" + hostname$ EndIf ; send a report via mail ? If ( MailReport$ = "yes" ) LogMe("INFO: sending mail to: " + MailTo$) EndIf ; write final return code to logfile ... LogMe("return=" + err) ; finally: send it! If ( MailReport$ = "yes" ) LogSend(s$) EndIf ; adding actual logfile to histlog... If ReadFile(0, Logfile$, #PB_File_SharedRead) While Eof(0) = 0 LogText$ + ReadString(0) + Chr($0d) + Chr($0a) Wend CloseFile(0) EndIf If OpenFile(0, HistLog$, #PB_File_Append | #PB_File_NoBuffering | #PB_File_SharedRead ) FileSeek(0, Lof(0)) WriteString(0,LogText$) CloseFile(0) EndIf ; remove backup share... dummy = system("net use " + TargetPath$ + " /DELETE >nul 2>&1") ClosePreferences() CloseConsole() End err EndProcedure Procedure.s mkpass(password_length) chars$ = "abcdef-" ; possible characters, keep these lcase For a = 1 To password_length Select Random(1) ; 0 = char, 1 = digit Case 1 ; is digit pass$ + Str(Random(9)) Case 0 ; is character position = Random(Len(chars$)) ; random character selector pass$ + Mid(chars$,position,1) EndSelect Next ProcedureReturn pass$ EndProcedure ; ; MAIN ; ; install or update for jobscheduler requested ? If ( updatesched = 1 ) username$ = GetEnvironmentVariable("USERNAME") ; generate call string for task... jobcmd$ = ProgramFilename() ; check jobtype ... If ( BackupschedMode$ <> LCase("login") And BackupschedMode$ <> LCase("time") ) PrintN("WARNING: unknown BackupschedMode [ " + BackupschedMode$ + " ] ---> IGNORING. Type will be set to LOGIN.") BackupschedMode$ = "LOGIN" EndIf ; update the job ... PrintN ("Updating windows jobscheduler ...") If ( BackupschedMode$ = LCase("time") ) jobcmd$ = Chr(34) + jobcmd$ + " /S" + Chr(34) + " /RU " + Chr(34) + "SYSTEM" + Chr(34) + " " PrintN ( "schtasks /create /F /RL HIGHEST /SC daily /ST " + BackupStart$ + " /TN " + jobname$ + " /TR " + jobcmd$ ) dummy = system("schtasks /create /F /RL HIGHEST /SC daily /ST " + BackupStart$ + " /TN " + jobname$ + " /TR " + jobcmd$) ElseIf ( BackupschedMode$ = LCase("login") ) PrintN ( "schtasks /create /F /RL HIGHEST /SC onlogon /DELAY " + BackupStart$ + " /TN " + jobname$ + " /TR " + jobcmd$ ) dummy = system("schtasks /create /F /RL HIGHEST /SC onlogon /DELAY " + BackupStart$ + " /TN " + jobname$ + " /TR " + jobcmd$) EndIf ; show job and print some status messages... dummy = system("schtasks /query /FO List /V /TN " + jobname$) PrintN (Chr(13) + "READY." + Chr(13)) ClosePreferences() CloseConsole() End EndIf ; FTP backup requested ? If ( FtpBackup$ = "yes" ) TargetPath$ = "ftp://" + TargetUser$ + "@" + FtpServer$ + TargetPath$ EndIf ; SILENT mode <> 0 => NO questions! If ( silentmode = 0 ) ; ; ASK the user for permission to start ... ; Result = MessageRequester("SnapControl", "Start BACKUP now?" + Chr(13) + "Targetpath => " + TargetPath$, #PB_MessageRequester_YesNo | #PB_MessageRequester_Info) If Result = #PB_MessageRequester_No Result = MessageRequester("SnapControl", "backup ABORTED.", #PB_MessageRequester_Ok | #PB_MessageRequester_Warning) CloseConsole() End 0 EndIf ; ; ASK the user for a shutdown ... ; If ForcedShutdown$ <> "yes" And AskForShutdown$ = "yes" Result$ = InputRequester("SnapControl", "SHUTDOWN system after backup?" + Chr(13) + " (type 'yes' or 'no')", "no") If LCase(Result$) = "yes" DoShutdown = 1 EndIf EndIf Else ; silent mode = true AND the admin has requested a forced shutdown ... If ForcedShutdown$ = "yes" DoShutdown = 1 EndIf EndIf ; cleanup: delete old Logfile, remove old drive letter... dummy = DeleteFile(LogFile$, #PB_FileSystem_Force) LogMe("============== starting BACKUP ==============") LogMe("snapcontrol.exe version = [ " + VERSION$ + " ]") LogMe(" snapshot.exe version = [ " + DriveSnapshotVersion$() + " ]") LogMe(" parameters = [ " + UCase(ProgramParameter(0)) + " ]") LogMe(" INIFILE = [ " + inifile$ + " ]") If ( dryrun = 1 ) LogMe("DRYRUN - (simulating a backup run) !!!") EndIf RC = IsAdmin() If ( RC = 0 ) LogMe("OK. Running as Admin ...") Else PrintN("ERROR: Please run as Administrator !") PrintN(" cannot continue ---> EXIT") End 97 EndIf ; ; GENERATE the BACKUP command... ; ; encryption enabled ? If ( Encrypt$ = "yes" ) params$ = "-PW=" + EncryptPW$ + " " If ( EncryptPW$ = "" ) LogMe("WARNING: ENCRYPTED backup requested but PASSWORD is not set in infile !") Else LogMe("WARNING: encryption requested. The backup will be encrypted.") LogMeRaw(" --> PLEASE WRITE DOWN YOUR PASSWORD AND STORE IT IN A SAFE PLACE !") EndIf EndIf If ( Encrypt$ = "dynamic" ) If ( EncryptPW$ = "" ) EncryptPW$ = mkpass(20) LogMe("WARNING: ENCRYPTED backup requested and DYNAMIC password was requested !") LogMeRaw(" Generated password is: --->>> " + EncryptPW$ + " <<<---") LogMe("INFO: writing password to inifile...") PreferenceGroup("backup") dummy = WritePreferenceString("EncryptPW", EncryptPW$) Else LogMe("WARNING: ENCRYPTED backup requested. Using DYNAMIC password in inifile !") LogMeRaw(" The password is: --->>> " + EncryptPW$ + " <<<---") EndIf params$ = "-PW=" + EncryptPW$ + " " EndIf ; limit the write rate ? If ( LimitIO$ <> "" ) params$ + "--LimitIORate:" + LimitIO$ + " " EndIf ; verify the backup ? If ( Verify$ = "yes" ) params$ + "-T " EndIf ; burn all the trash in the recyclebin ? If ( BurnTrash$ = "yes" ) params$ + "-R " EndIf ; eject media ? If ( EjectMedia$ = "yes" ) params$ + "--EjectDriveAfterBackup " EndIf ; FTP backup requested ? If ( FtpBackup$ = "yes" ) LogMe("INFO: FTP backup, using server [ " + FtpServer$ + " ] for backup ...") ; add ftp account ... RC = system(SnapshotBin$ + " --AddFTPAccount:" + TargetUser$ + "," + FtpServer$ + "," + TargetPassword$) ; generate filenames for backup ... DumpFile$ = TargetPath$ + "/" + hostname$ + "_snapshot_" + month$ + "_$type_$disk.sna" HashFile$ = "-h" + InstallTo$ + "\" + hostname$ + "_snapshot_" + month$ + "_$type_$disk.hsh" Else ; network share or local drive ? colonpos = FindString(TargetPath$, ":") If ( colonpos = 0 ) ; no colon found in path this means we are using a network share ... params$ + "--NetUse:" + TargetPath$ + "," + TargetUser$ + "," + TargetPassword$ + " " LogMe("INFO: using a NETWORK share for backup ...") Else ; found a ':' at some position means we are using a drive letter ... DRIVE$ = Left(TargetPath$, colonpos) LogMe("INFO: using DRIVE [ " + DRIVE$ + " ] for backup ...") EndIf ; check if there is a full backup file ... in case it was deleted ... If ( FileSize ( TargetPath$ + "\" + hostname$ + "_snapshot_" + month$ + "_ful*.sna" ) < 0 ) LogMe("WARNING: no full backup found.") If ( FileSize ( TargetPath$ + "\" + hostname$ + "_snapshot_" + month$ + "_ful*.hsh" ) >= 0 ) dummy = system("del /F /Q " + TargetPath$ + "\" + hostname$ + "_snapshot_" + month$ + "_ful*.hsh") LogMe("INFO: [ " + TargetPath$ + "\" + hostname$ + "_snapshot_" + month$ + "_ful*.hsh ] deleted.") EndIf EndIf ; check the actual FULL backup file for errors ... ; ( must be done BEFORE we run a backup because we don't know - for sure ; - wether the last backup was interrupted or not... ) i = 1 If ExamineDirectory(0, TargetPath$, hostname$ + "_snapshot_" + month$ + "_ful*.sna") While NextDirectoryEntry(0) If DirectoryEntryType(0) = #PB_DirectoryEntry_File ReDim filelist$(i) filelist$(i-1) = DirectoryEntryName(0) LogMe("INFO: found [ " + filelist$(i-1) + " ]") i + 1 EndIf Wend FinishDirectory(0) EndIf ; do a quickcheck ... For i = 1 To ArraySize(filelist$()) LogMe("INFO: Starting QUICKCHECK for [ " + filelist$(i-1) + " ]") RC = system(SnapshotBin$ + " --Logfile:" + LogFile$ + " --QuickCheck:" + TargetPath$ + "\" + filelist$(i-1)) If ( RC <> 0 ) LogMe("ERROR: DELETING last full backup [ REASON: corrupt file! ]") dummy = system("del /F /Q " + TargetPath$ + "\" + hostname$ + "_snapshot_" + month$ + "_ful*.*") Break EndIf Next i ; generate filenames for backup ... DumpFile$ = TargetPath$ + "\" + hostname$ + "_snapshot_" + month$ + "_$type_$disk.sna" HashFile$ = "-h" + TargetPath$ + "\" + hostname$ + "_snapshot_" + month$ + "_$type_$disk.hsh" EndIf ; ; run BACKUP... ; ; extend params ... params$ + " --FullIfHashIsMissing --CreateDir -W -L" + DumpSize$ + " " + Disks2Dump$ LogMe("INFO: executing command [ " + SnapshotBin$ + " ]") LogMe(" DumpFile: [ " + DumpFile$ + " ]") ; any excludes ? ... If ( ExcludeList$ <> "" ) params$ + " --exclude:" + ExcludeList$ LogMe(" ExcludeList: [ " + ExcludeList$ + " ]") EndIf LogMe(" HashFile: [ " + Mid(HashFile$,3) + " ]") LogMe(" LogFile: [ " + LogFile$ + " ]") ; execute snapshot64 ... If ( dryrun = 0 ) RC = system(SnapshotBin$ + " --Logfile:" + LogFile$ + " " + params$ + " " + DumpFile$ + " " + HashFile$) RC_ALL = RC_ALL + RC If ( RC > 0 ) LogMe("ERROR: while executing [ " + SnapshotBin$ + " ]!") EndIf If DoShutdown = 1 LogMe("INFO: shutdown was requested ...") LogMe("INFO: executing [ " + ShutdownCommand$ + " ]") RC = system(ShutdownCommand$) RC_ALL = RC_ALL + RC If ( RC > 0 ) LogMe("ERROR: while executing shutdown command!") EndIf EndIf EndIf ; end with return code... EndProg(RC_ALL) ; IDE Options = PureBasic 5.73 LTS (Windows - x64) ; ExecutableFormat = Console ; CursorPosition = 49 ; FirstLine = 22 ; Folding = -- ; EnableXP ; Executable = snapcontrol.exe ; DisableDebugger ; Warnings = Display