How to tell InnoSetup to not uninstall (text) files which had been changed by the user (== are different from those installed by InnoSetup)?
Or maybe more difficult: when installing a new version over an existing, InnoSetup should ask the user whether to overwrite the changed file, but on a pure uninstall, it should uninstall it without asking.
I recently had a similar problem. This was my solution to detect if a text file (profile) has been changed from the one installed during the last installation run:
Use ISPP (Inno Setup Pre-Processor) to create the list of text files and their hashes at compile time:
[Files]
; ...
#define FindHandle
#define FindResult
#define Mask "Profiles\*.ini"
#sub ProcessFoundFile
#define FileName "Profiles\" + FindGetFileName(FindHandle)
#define FileMd5 GetMd5OfFile(FileName)
Source: {#FileName}; DestDir: {app}\Profiles; Components: profiles; \
Check: ProfileCheck('{#FileMd5}'); AfterInstall: ProfileAfterInstall('{#FileMd5}');
#endsub
#for {FindHandle = FindResult = FindFirst(Mask, 0); FindResult; FindResult = FindNext(FindHandle)} ProcessFoundFile
At the top of the "Code" section I define some useful things:
[Code]
var
PreviousDataCache : tStringList;
function InitializeSetup() : boolean;
begin
// Initialize global variable
PreviousDataCache := tStringList.Create();
result := TRUE;
end;
function BoolToStr( Value : boolean ) : string;
begin
if ( not Value ) then
result := 'false'
else
result := 'true';
end;
In the "Check" event handler I compare the hashes of previous install and current file:
function ProfileCheck( FileMd5 : string ) : boolean;
var
TargetFileName, TargetFileMd5, PreviousFileMd5 : string;
r : integer;
begin
result := FALSE;
TargetFileName := ExpandConstant(CurrentFileName());
Log('Running check procedure for file: ' + TargetFileName);
if not FileExists(TargetFileName) then
begin
Log('Check result: Target file does not exist yet.');
result := TRUE;
exit;
end;
try
TargetFileMd5 := GetMd5OfFile(TargetFileName);
except
TargetFileMd5 := '(error)';
end;
if ( CompareText(TargetFileMd5, FileMd5) = 0 ) then
begin
Log('Check result: Target matches file from setup.');
result := TRUE;
exit;
end;
PreviousFileMd5 := GetPreviousData(ExtractFileName(TargetFileName), '');
if ( PreviousFileMd5 = '' ) then
begin
r := MsgBox(TargetFileName + #10#10 +
'The existing file is different from the one Setup is trying to install. ' +
'It is recommended that you keep the existing file.' + #10#10 +
'Do you want to keep the existing file?', mbConfirmation, MB_YESNO);
result := (r = idNo);
Log('Check result: ' + BoolToStr(result));
end
else if ( CompareText(PreviousFileMd5, TargetFileMd5) <> 0 ) then
begin
r := MsgBox(TargetFileName + #10#10 +
'The existing file has been modified since the last run of Setup. ' +
'It is recommended that you keep the existing file.' + #10#10 +
'Do you want to keep the existing file?', mbConfirmation, MB_YESNO);
result := (r = idNo);
Log('Check result: ' + BoolToStr(result));
end
else
begin
Log('Check result: Existing target has no local modifications.');
result := TRUE;
end;
end;
In the "AfterInstall" event handler I mark the file hash to be stored in
Registry later. Because in my tests the event was triggered even if the file move failed (target file is read-only) I compare the hash again to find out if the file move was successful:
procedure ProfileAfterInstall( FileMd5 : string );
var
TargetFileName, TargetFileMd5 : string;
begin
TargetFileName := ExpandConstant(CurrentFileName());
try
TargetFileMd5 := GetMd5OfFile(TargetFileName);
except
TargetFileMd5 := '(error)';
end;
if ( CompareText(TargetFileMd5, FileMd5) = 0 ) then
begin
Log('Storing hash of installed file: ' + TargetFileName);
PreviousDataCache.Add(ExtractFileName(TargetFileName) + '=' + FileMd5);
end;
end;
procedure RegisterPreviousData( PreviousDataKey : integer );
var
Name, Value : string;
i, n : integer;
begin
for i := 0 to PreviousDataCache.Count-1 do
begin
Value := PreviousDataCache.Strings[i];
n := Pos('=', Value);
if ( n > 0 ) then
begin
Name := Copy(Value, 1, n-1);
Value := Copy(Value, n+1, MaxInt);
SetPreviousData(PreviousDataKey, Name, Value);
end;
end;
end;
Inno can't do this check natively.
To not replace changed files during install, you'll need to use custom [Code] to do a checksum and compare against a known good value that is precomputed or saved from the previous install.
To avoid removing them during uninstall, you'll need to disable Inno's own uninstall for that file and check against the same checksum before removing them, again in [Code].
Note that it's better to keep any files the user can edit outside of the setup to handle this situation better and to correctly adhere to the application guidelines.
Related
function CreateLICStopServiceBatch(): boolean;
begin
Result := true;
fileName := ExpandConstant('{code:GetBasicDirectoryValue}\{code:GetInstance}\_Service\Stop_LIC_Service.bat');
SetArrayLength(lines, 3);
lines[0] := ExpandConstant('set wrapdir={code:GetBasicDirectoryValue}\{code:GetInstance}\_Service');
lines[1] := 'cd /d %wrapdir%';
lines[2] := ExpandConstant('commons-daemon\prunsrv.exe //SS//{code:GetLSDVBServiceNameTEdit}');
Result := SaveStringsToFile(filename,lines,false);
exit;
end;
CreateLICStopServiceBatch();
Right now I'm just writing the file in to the directory, doesn't matter if the file exist or not. But I want to check if the file exist (FileExists) and also have the opportunity to choose (in a dialog) overwrite or take the old file which already exist.
Use FileExists function to test for file existence.
See Inno Setup - Check if file exist in destination or else if doesn't abort the installation
Use MsgBox function for confirmation.
See Inno Setup: Conditionally delete non-empty directory in user's home folder
Side note: Do not use ExpandConstant('{code:Function}') is Pascal code. That makes no sense. Call Function directly. Not to mention that your functions probably do something trivial, which you can inline to your code anyway.
function CreateLICStopServiceBatch(): boolean;
var
FileName: string;
Lines: TArrayOfString;
begin
FileName := GetBasicDirectoryValue + '\' + GetInstance + '\_Service\Stop_LIC_Service.bat';
SetArrayLength(Lines, 3);
Lines[0] := 'set wrapdir=' + GetBasicDirectoryValue + '\' + GetInstance + '\_Service';
Lines[1] := 'cd /d %wrapdir%';
Lines[2] := 'commons-daemon\prunsrv.exe //SS//' + GetLSDVBServiceNameTEdit;
if (not FileExists(FileName)) or
(MsgBox('Overwrite?', mbConfirmation, MB_YESNO) = idYes) then
begin
Result := SaveStringsToFile(FileName, Lines, False);
end
else
begin
{ Not sure what you want to return when user does not confirm overwrite }
Result := False;
end;
end;
I want to make Steam backups installer. However, Steam allow to user make multiple library folders which makes installation difficult.
There are a few tasks that I want to perform.
Installer should identify path from registry to identify where Steam is installed.
The resulting path from registry open file
X:\Steam\config\config.vdf
and read value of "BaseInstallFolder_1", "BaseInstallFolder_2", "BaseInstallFolder_3" and etc.
Example of config.vdf:
"NoSavePersonalInfo" "0"
"MaxServerBrowserPingsPerMin" "0"
"DownloadThrottleKbps" "0"
"AllowDownloadsDuringGameplay" "0"
"StreamingThrottleEnabled" "1"
"AutoUpdateWindowStart" "-1"
"AutoUpdateWindowEnd" "-1"
"LastConfigstoreUploadTime" "1461497849"
"BaseInstallFolder_1" "E:\\Steam_GAMES"
The resulting path or paths of the file config.vdf bring in DirEdit
If user have multiple paths to the folder in different locations then give option select through DirTreeView or Radiobuttons.
How it should look like:
I know how to identify Steam path
WizardForm.DirEdit.Text := ExpandConstant('{reg:HKLM\SOFTWARE\Valve\Steam,InstallPath|{pf}\Steam}')+ '\steamapps\common\gamename';
But it is difficult to perform other tasks
Thanks in advance for your help.
To parse the config.vdf file:
Load the file using LoadStringsFromFile.
And use string functions like Copy, Pos, Delete, CompareText to parse it.
To keep the results use array of string (or predefined TArrayOfString). To allocate the array, use SetArrayLength.
The code may be like:
function ParseSteamConfig(FileName: string; var Paths: TArrayOfString): Boolean;
var
Lines: TArrayOfString;
I: Integer;
Line: string;
P: Integer;
Key: string;
Value: string;
Count: Integer;
begin
Result := LoadStringsFromFile(FileName, Lines);
Count := 0;
for I := 0 to GetArrayLength(Lines) - 1 do
begin
Line := Trim(Lines[I]);
if Copy(Line, 1, 1) = '"' then
begin
Delete(Line, 1, 1);
P := Pos('"', Line);
if P > 0 then
begin
Key := Trim(Copy(Line, 1, P - 1));
Delete(Line, 1, P);
Line := Trim(Line);
Log(Format('Found key "%s"', [Key]));
if (CompareText(
Copy(Key, 1, Length(BaseInstallFolderKeyPrefix)),
BaseInstallFolderKeyPrefix) = 0) and
(Line[1] = '"') then
begin
Log(Format('Found base install folder key "%s"', [Key]));
Delete(Line, 1, 1);
P := Pos('"', Line);
if P > 0 then
begin
Value := Trim(Copy(Line, 1, P - 1));
StringChange(Value, '\\', '\');
Log(Format('Found base install folder "%s"', [Value]));
Inc(Count);
SetArrayLength(Paths, Count);
Paths[Count - 1] := Value;
end;
end;
end;
end;
end;
end;
I need to create multiple keys with the same name, without overwriting the existing keys with that name, preferably right below or above them. For example, a key named App already exists in the ini, I need to create another key (or several) named App without overwriting the existing one. As far as I understand that's impossible to do from the [ini] section. So, how can I force create a new key even if it exists?
There is no built-in function for this because what you're asking for breaks the INI file format. In INI files, within each section, every key name must be unique, which you are going to violate. But the following function might do what you want:
[Code]
procedure AppendKey(const FileName, Section, KeyName, KeyValue: string);
var
S: string;
I: Integer;
CurLine: string;
LineIdx: Integer;
SnFound: Boolean;
Strings: TStringList;
begin
Strings := TStringList.Create;
try
S := Format('[%s]', [Section]);
Strings.LoadFromFile(FileName);
SnFound := False;
LineIdx := Strings.Count;
for I := Strings.Count - 1 downto 0 do
begin
CurLine := Trim(Strings[I]);
// if the iterated line is a section, then...
if (Length(CurLine) > 2) and (CurLine[1] = '[') and (CurLine[Length(CurLine)] = ']') then
begin
// if the iterated line is the section we are looking for, then...
if CompareText(S, CurLine) = 0 then
begin
SnFound := True;
Break;
end;
end
else
if CurLine = '' then
LineIdx := I;
end;
if not SnFound then
begin
Strings.Add(S);
Strings.Add(Format('%s=%s', [KeyName, KeyValue]));
end
else
Strings.Insert(LineIdx, Format('%s=%s', [KeyName, KeyValue]));
Strings.SaveToFile(FileName);
finally
Strings.Free;
end;
end;
Call it like:
AppendKey('C:\File.ini', 'Section', 'KeyName', 'KeyValue');
I'm using ExcelDocTypeUtils pkg for exporting Query data to EXCEL FILE.
When I run the procedure I'm getting a excel file, but when I try to open it this error pops out:
Program came up in the following areas during load: Style
The original line that I think that fails is:
-- Prepare Headers
owa_util.mime_header('application/vnd.ms-excel',FALSE);
I have already tried setting:
<?xml version="1.0" encoding="UTF-8"?>'
But when I do that, the error changes to the following:
Program came up in the following areas during load: 'Strict Parse Error'
Here is how I execute my procedure:
/* Starts */
execute employeereport;
CREATE OR REPLACE PROCEDURE employeeReport AS
v_sql_salary VARCHAR2(200) := 'SELECT last_name,first_name,salary FROM hr.employees ORDER BY last_name,first_name';
v_sql_contact VARCHAR2(200) := 'SELECT last_name,first_name,phone_number,email FROM hr.employees ORDER BY last_name,first_name';
v_sql_hiredate VARCHAR2(200) := 'SELECT last_name,first_name,to_char(hire_date,''MM/DD/YYYY'') hire_date FROM hr.employees ORDER BY last_name,first_name';
excelReport ExcelDocumentType := ExcelDocumentType();
v_worksheet_rec ExcelDocTypeUtils.T_WORKSHEET_DATA := NULL;
v_worksheet_array ExcelDocTypeUtils.WORKSHEET_TABLE := }
ExcelDocTypeUtils.WORKSHEET_TABLE();
documentArray ExcelDocumentLine := ExcelDocumentLine();
v_file UTL_FILE.FILE_TYPE;
BEGIN
-- Salary
v_worksheet_rec.query := v_sql_salary;
v_worksheet_rec.worksheet_name := 'Salaries';
v_worksheet_rec.col_count := 3;
v_worksheet_rec.col_width_list := '25,20,15';
v_worksheet_rec.col_header_list := 'Lastname,Firstname,Salary';
v_worksheet_array.EXTEND;
v_worksheet_array(v_worksheet_array.count) := v_worksheet_rec;
-- Contact
v_worksheet_rec.query := v_sql_contact;
v_worksheet_rec.worksheet_name := 'Contact_Info';
v_worksheet_rec.col_count := 4;
v_worksheet_rec.col_width_list := '25,20,20,25';
v_worksheet_rec.col_header_list := 'Lastname,Firstname,Phone,Email';
v_worksheet_array.EXTEND;
v_worksheet_array(v_worksheet_array.count) := v_worksheet_rec;
-- Contact
v_worksheet_rec.query := v_sql_hiredate;
v_worksheet_rec.worksheet_name := 'Hiredate';
v_worksheet_rec.col_count := 3;
v_worksheet_rec.col_width_list := '25,20,20';
v_worksheet_rec.col_header_list := 'Lastname,Firstname,Hiredate';
v_worksheet_array.EXTEND;
v_worksheet_array(v_worksheet_array.count) := v_worksheet_rec;
owa.num_cgi_vars := NVL(owa.num_cgi_vars, 0);
excelReport := ExcelDocTypeUtils.createExcelDocument(v_worksheet_array);
documentArray := excelReport.getDocumentData;
v_file := UTL_FILE.fopen('C:\','test.xml','W',4000);
FOR x IN 1 .. documentArray.COUNT LOOP
UTL_FILE.put_line(v_file,documentArray(x));
END LOOP;
UTL_FILE.fclose(v_file);
--excelReport.displayDocument;
END;
It's solved.
I don't know what did it, the only thing that I changed it's .xls extension instead of .xml
Thanks.
I have a list of strings and the values they are to be replaced with. I'm trying to combine them in a list like 'O'='0',' .'='.', ... so it's easy for me to edit it and add more pairs of replacements to make.
Right now the best way I could think of it is:
var
ListaLimpeza : TStringList;
begin
ListaLimpeza := TStringList.Create;
ListaLimpeza.Delimiter := '|';
ListaLimpeza.QuoteChar := '"';
ListaLimpeza.DelimitedText := 'O=0 | " .=."';
ShowMessage('1o Valor = '+ListaLimpeza.Names[1]+' e 2o Valor = '+ListaLimpeza.ValueFromIndex[1]);
This works, but it's not good for visuals, since I can't code the before string (for ex ' .') like that (which is very visual for the SPACE character), only like (" .) so that the = works to assign a name and value in the TStringList.
The Names and Values by default have to be separated by =, in the style of Windows INI files. There's no way AFAICT to change that separator. As #SirRufo indicates in the comment (and which I had never noticed), you can change that using the TStringList.NameValueSeparator property.
This will give you an idea of what Delphi thinks is in your TStringList, which is not what you think it is:
procedure TForm1.FormCreate(Sender: TObject);
var
SL: TStringList;
Temp: string;
i: Integer;
begin
SL := TStringList.Create;
SL.Delimiter := '|';
SL.QuoteChar := '"';
SL.StrictDelimiter := True;
SL.DelimitedText := 'O=0 | ! .!=!.!';
Temp := 'Count: ' + IntToStr(SL.Count) + #13;
for i := 0 to SL.Count - 1 do
Temp := Temp + Format('Name: %s Value: %s'#13,
[SL.Names[i], SL.ValueFromIndex[i]]);
ShowMessage(Temp);
end;
This produces this output:
TStringList Names/Values probably isn't going to do what you need. It's not clear what your actual goal is, but it appears that a simple text file with a simple list of text|replacement and plain parsing of that file would work, and you can easily use TStringList to read/write from that file, but I don't see any way to do the parsing easily except to do it yourself. You could use an array to store the pairs when you parse them:
type
TReplacePair = record
TextValue: string;
ReplaceValue: string;
end;
TReplacePairs = array of TReplacePair;
function GetReplacementPairs: TReplacePairs;
var
ConfigInfo: TStringList;
i, Split: Integer;
begin
ConfigInfo := TStringList.Create;
try
ConfigInfo.LoadFromFile('ReplacementPairs.txt');
SetLength(Result, ConfigInfo.Count);
for i := 0 to ConfigInfo.Count - 1 do
begin
Split := Pos('|`, ConfigInfo[i];
Result[i].TextValue := Copy(ConfigInfo[i], 1, Split - 1);
Result[i].ReplaceValue := Copy(ConfigInfo[i], Split + 1, MaxInt);
end;
finally
ConfigInfo.Free;
end;
end;
You can then populate whatever controls you need to edit/add/delete the replacement pairs, and just reverse the read operation to write them back out to save.