InnoBackup - A backup application developed with Inno Setup

From ISXKB

(Difference between revisions)
Jump to: navigation, search
(Possible improvements: Changed macro for network backups)
(The skeleton: PrivilegesRequired=none)
Line 25: Line 25:
OutputBaseFilename=InnoBackup
OutputBaseFilename=InnoBackup
CreateAppDir=false
CreateAppDir=false
 +
PrivilegesRequired=none
[Messages]
[Messages]
Line 42: Line 43:
Uninstallable=false
Uninstallable=false
OutputBaseFilename={#MyOutput}
OutputBaseFilename={#MyOutput}
 +
PrivilegesRequired=none
[Messages]
[Messages]

Revision as of 07:52, 15 October 2007

Contents

Introduction

Backups are an essential pillar in recovering after major hardware issues. When beeing tasked with new solutions to save time on disaster recovery a good advice is to look around for simple, effective, and transparent, ways.

This article 'abuses' Inno Setup as an easy-to-use backup utility with a nice user interface. Surely, there are enough commercial or free backup solutions out there, some determined for special purposes, and some highly configurable. Even the backup application that ships with Windows is useful for many tasks.

However, the following situations are targeted with 'InnoBackup':

  • No folder selections - The user selects a named source and source and target are determined automatically.
  • The end user must not be able to change the paths for source and target.
  • Different modes: interactive, silent, and very silent.
  • Multiple language support
  • 'State-of-the-art' interface, ie. no simple batch file.

Solution

These requirements make Inno Setup the perfect environment.

The skeleton

Since 'Inno Backup' is not an installer we can aquire the skeleton from the article Writing quick Pascal programs with Inno Setup. The only difference is that an application directory is not wished (CreateAppDir=false).

[Setup]
AppName=Inno Backup
AppVerName=Inno Backup 1.0
UsePreviousAppDir=false
DefaultDirName={pf}\EmptyProgram
Uninstallable=false
OutputBaseFilename=InnoBackup
CreateAppDir=false
PrivilegesRequired=none

[Messages]
SetupAppTitle=Inno Backup 1.0

As soon as someone wants to change the application's name it'd be necessary to find and replace the text for it several times in the script. Using the Inno Setup Preprocessor (ISPP) takes care of this restriction:

#define MyAppName "Inno Backup"
#define MyVersion "1.0"
#define MyOutput "InnoBackup"

[Setup]
AppName={#MyAppName}
AppVerName={#MyAppName} {#MyVersion}
UsePreviousAppDir=false
DefaultDirName={pf}\EmptyProgram
Uninstallable=false
OutputBaseFilename={#MyOutput}
PrivilegesRequired=none

[Messages]
SetupAppTitle={#MyAppName}

Selecting the source(s)

One of the requirements was to not have a folder selection. To implement 'named sources' we could either use a [Tasks] section or [Components]. [Components] support [Types], hence it may be easier later for the user to choose the backup sources from a components list instead of a [Tasks] page.

[Components]
Name: source1; Description: My Music
Name: source2; Description: My Pictures
Name: source3; Description: My Videos

Considering that the program only consists of 18 lines of source code and "development time" was less than 2 minutes the result looks quite impressive already:

Inno Backup

Bare in mind that all the texts will have to be changed later in order to reflect a backup application. Additionally, there are still no source or target files or folders defined.

Sources and targets

There are several different ways to 'poke' the source and target folders into the Inno Setup script.

For instance, we could have an ini file which contains a section for each component together with a 'Name', a 'Source', and a 'Target' key, and then make ISPP read this file during compile time:

[My Videos]
Name=source1
Source=...
Target=...

Another option could be to go for a somewhat Inno Setup syntax:

Name: source1; Description: My Videos; Source: ...; Target: ...

Or let's find the middle way between readability and macro complexity:

source1	"My Videos" Source:"C:\Documents..."; DestDir:"C:\Backups\MyVideos"

The file's elements are:

  • 'source1' is the name of the component. Components can be listed in a tree, for instance 'source1\subfolder'. A macro in the [Components] section will put this in the right place.
  • "My Videos" is the description displayed in the components list view. This will be handled by the [Components] macro, too.
  • "C:\Documents..." is the source directory. A macro for the [Files] section will be required to add 'Source: "C:\Documents..."' and the rest of the line.
  • "C:\Backups\MyVideos" names the target folder for the backup which is the destination folder in the [Files] entry.

The macros

Since coding the ISPP macros to examine the configuration file at compile time could be a difficult task the tip Store the preprocessed output in a file will assist a little bit:

 #expr SaveToFile(AddBackslash(SourcePath) + "Preprocessed.iss")

This line put at the end of the script stores a copy of the preprocessed output into the file 'Preprocessed.iss'. It will help to develop and debug the macros.

The [Components] macro which is fed from the file 'Components.ini':

[Components]
; Read the components and their description line by line from the configuration file.
#pragma message "Reading [Components] from " + AddBackslash(SourcePath) + "Components.ini..."
#define FileHandle
#define FileLine
#sub ProcessFileLine
  #pragma message FileLine
  #if Len(FileLine)
    #emit "Name: " + Copy (FileLine, 1, (Local[0] = Pos ('"', FileLine)) - 2) + \
    "; Description: " + Copy (FileLine, Local[0] + 1, Pos ('"', Copy (FileLine, Local[0] + 3)) + 1) + \
    "; Types: full"
  #endif
#endsub
#for {FileHandle = FileOpen(AddBackslash(SourcePath) + "Components.ini"); \
  FileHandle && !FileEof(FileHandle); FileLine = FileRead(FileHandle)} \
  ProcessFileLine
#if FileHandle
  #expr FileClose(FileHandle)
#endif

From now on ISTool can't be used anymore to edit the script. The program contains a bug that somehow fills in some characters, mainly semicolons, where they don't belong. The base for the above macro has been taken from ISPP's help section and altered to read the components list from the file 'Components.ini' in the same directory where the script file resides. The length check is required because ISPP returns an empty string in FileRead() as the first read line although it is definitely not empty. Additionally, the configuration file requires one empty line at the end. If this empty line is missing FileRead() does not return the last line properly.

The macro for the [Files] section looks similiar:

[Files]
; Assemble the [Files] section from the configuration file.
#pragma message "Reading [Files] from " + AddBackslash(SourcePath) + "Components.ini..."
#sub ProcessAgain
  #pragma message FileLine
  #if Len(FileLine)
    #emit Copy (FileLine, Pos ("Source:", FileLine)) + \
    "; Components: " + Copy (FileLine, 1, Pos ('"', FileLine) - 2) + "; " + \
    "Flags: external createallsubdirs recursesubdirs setntfscompression; BeforeInstall: BeforeInstallEachFile"
  #endif
#endsub
#for {FileHandle = FileOpen(AddBackslash(SourcePath) + "Components.ini"); \
  FileHandle && !FileEof(FileHandle); FileLine = FileRead(FileHandle)} \
  ProcessAgain
#if FileHandle
  #expr FileClose(FileHandle)
#endif

The 'BeforeInstall' invocation is only required to make backups of the backups before overwriting anything.

The code

Inno Backup's [Code] section only consists of the BeforeInstallEachFile () function, which is necessary to rename a previously existing backup, and InitializeSetup (). In InitializeSetup the current date and time is stored in a global variable to be used by BeforeinstallEachFile ().

[Code]
var
    PreviousBackupName  : String;
    
procedure BeforeInstallEachFile;
var
    FN : String;
begin
    //MsgBox('About to install MyProg.exe as ' + CurrentFileName + '.', mbInformation, MB_OK);
    FN := CurrentFileName;
    if (length (FN) > 3) then
    begin
        if (Copy (FN, length (FN), 1) = '*') then
            FN := Copy (FN, length (FN) - 1, 1);
        if (Copy (FN, Length (FN), 1) = '.') then
            FN := Copy (FN, length (FN) - 1, 1);
        if (Copy (FN, Length (FN), 1) = '*') then
            FN := Copy (FN, length (FN) - 1, 1);
        FN := RemoveBackSlash (FN);
    end;
    if (FileOrDirExists (FN)) then
    begin
        // Since the top-most directory is the first folder to be copied into
        //  we can keep the previous backup by renaming it. If only files are
        //  given they should all be renamed, one by one.
        RenameFile (FN, FN + ' until ' + PreviousBackupName + 's');
    end;
end;

function InitializeSetup : Boolean;
begin
    PreviousBackupName := GetDateTimeString ('yyyy-mm-dd hh.nn ss', '-', '.')
    Result := TRUE;
end;

The [Messages] section

After altering some messages the backup program resembles any other backup application.

[Messages]
SetupAppTitle={#MyAppName}
SetupWindowTitle=%1
ExitSetupTitle=Exit {#MyAppName}
ExitSetupMessage=The backup is not complete. If you exit now, the backup will not be finished.%n%nYou may run {#MyAppName} again at another time to complete the backup.%n%nExit {#MyAppName}?
AboutSetupMenuItem=&About {#MyAppName}...
AboutSetupTitle=About {#MyAppName}
ButtonInstall=&Start Backup
ErrorCreatingDir={#MyAppName} was unable to create the directory "%1"
SelectLanguageTitle=Select Backup Language
SelectLanguageLabel=Select the language to use during the backup:
ClickNext=Click Next to continue, or Cancel to exit {#MyAppName}.
WelcomeLabel1=Welcome to the [name] Wizard
WelcomeLabel2=This will guide you through the backup process.%n%nIt is recommended that you close all other applications before continuing.
WizardSelectComponents=Select Backup Components
SelectComponentsDesc=Which components should be backed up?
SelectComponentsLabel2=Select the components you want to backup; clear the components you do not want to backup. Click Next when you are ready to continue.
WizardReady=Ready to Backup
ReadyLabel1={#MyAppName} is now ready to start the backup.
ReadyLabel2a=Click Start Backup to continue with the backup, or click Back if you want to review or change any settings.
ReadyLabel2b=Click Start Backup to continue with the installation.
WizardPreparing=Preparing to Backup
PreparingDesc={#MyAppName} is preparing the backup.
FinishedHeadingLabel=Completing the [name] Wizard
FinishedLabelNoIcons={#MyAppName} has finished the backup.
ClickFinish=Click Finish to exit {#MyAppName}.
DiskSpaceWarning=The backup requires at least %1 KB of free space, but the target drive only has %2 KB available.%n%nDo you want to continue anyway?


Inno Backup
Inno Backup
Inno Backup
Inno Backup
Inno Backup
Inno Backup
Inno Backup
Inno Backup


The [Types] section

If there are too many backup sources the user could become slightly swamped with the amount of components to choose from. A 'Full backup', however, is not generally a good idea as the source could contain a lot of data which would blow up the copy process unnecessarily.

We'll go just for a few different types:

[Types]
Name: "none"; Description: "No backup"
Name: "full"; Description: "Full backup"
Name: "custom"; Description: "Custom backup"; Flags: iscustom

Final statistics for 'Inno Backup':

121 lines of code including empty ones.

The file 'Components.ini' with the named backups

This is an example for the file 'Components.ini' that should stay in the same directory together with the Inno Setup main script.

server1dir1	"Documents on Data Server 1" Source:"\\Server1\E$\Documents\*"; DestDir:"F:\Backups\Documents Server 1"
server2dir1	"Documents on Data Server 2" Source:"\\Server2\E$\Documents\*"; DestDir:"F:\Backups\Documents Server 2"
server1dir2	"Applications on Data Server 1" Source:"\\Server1\E$\Apps\*"; DestDir:"F:\Backups\Applications Server 1"
server2dir2	"Applications on Data Server 2" Source:"\\Server2\E$\Apps\*"; DestDir:"F:\Backups\Applications Server 2"
server1dir3	"Pages on Data Server 1" Source:"\\Server1\E$\Pages\*"; DestDir:"F:\Backups\Pages Server 1"
server2dir3	"Pages on Data Server 2" Source:"\\Server2\E$\Pages\*"; DestDir:"F:\Backups\Pages Server 2"
server1dir4	"Pictures on Data Server 1" Source:"\\Server1\E$\Pictures\*"; DestDir:"F:\Backups\Pictures Server 1"
server2dir4	"Pictures on Data Server 2" Source:"\\Server2\E$\Pictures\*"; DestDir:"F:\Backups\Pictures Server 2"
server1dir5	"Videos on Data Server 1" Source:"\\Server1\E$\Videos\*"; DestDir:"F:\Backups\Videos Server 1"
server2dir5	"Videos on Data Server 2" Source:"\\Server2\E$\Videos\*"; DestDir:"F:\Backups\Videos Server 2"
local1	"My Local Documents" Source:"C:\Documents and Settings\*"; DestDir:"F:\Backups\Local"

After compilation this list is "hard wired" into the application. This prevents the average user from changing the source and target directories.

Remember that any change to this file reqires recompiling the script.

Possible improvements

It goes without saying that this quick backup utility is not entirely free of disadvantages.

For instance, it takes a long time to start up when the source directories are spread over many machines in the network. This can be circumvented by adding an 'ExternalSize' parameter with a value of 0 to the macro in the [Files] section. Drawback is then that the sizes are not correct anymore, but the message can be overwritten as well.

[Messages]
ComponentsDiskSpaceMBLabel=
[Files]
; Assemble the [Files] section from the configuration file.
#pragma message "Reading [Files] from " + AddBackslash(SourcePath) + "Components.ini..."
#sub ProcessAgain
  #pragma message FileLine
  #if Len(FileLine)
    #emit Copy (FileLine, Pos ("Source:", FileLine)) + \
    "; Components: " + Copy (FileLine, 1, Pos ('"', FileLine) - 2) + "; " + \
    "Flags: external createallsubdirs recursesubdirs setntfscompression; BeforeInstall: BeforeInstallEachFile;" + \
    "ExternalSize: 0"
  #endif
#endsub
#for {FileHandle = FileOpen(AddBackslash(SourcePath) + "Components.ini"); \
  FileHandle && !FileEof(FileHandle); FileLine = FileRead(FileHandle)} \
  ProcessAgain
#if FileHandle
  #expr FileClose(FileHandle)
#endif

The example code does not contain multiple languages, but it's a piece of cake now to add a [Languages] section, alter the [Messages] accordingly, and define some [CustomMessages] for the [Tasks].

There's certainly much more room for improvements.

See also

External links

Personal tools
Ads: