浏览代码

Merge branch 'downloadtemporaryfile' into main

Some minor todo's left but don't need a branch anymore.
Martijn Laan 5 年之前
父节点
当前提交
4ef7dbff1b
共有 10 个文件被更改,包括 469 次插入26 次删除
  1. 145 0
      Examples/CodeDownloadFiles.iss
  2. 47 1
      ISHelp/isxfunc.xml
  3. 224 11
      Projects/Install.pas
  4. 1 1
      Projects/Main.pas
  5. 10 2
      Projects/ScriptFunc.pas
  6. 4 0
      Projects/ScriptFunc_C.pas
  7. 14 0
      Projects/ScriptFunc_R.pas
  8. 8 8
      Projects/Wizard.pas
  9. 1 0
      setup.iss
  10. 15 3
      whatsnew.htm

+ 145 - 0
Examples/CodeDownloadFiles.iss

@@ -0,0 +1,145 @@
+; -- CodeDownloadFiles.iss --
+;
+; This script shows how the PrepareToInstall event function can be used to
+; download temporary files.
+
+[Setup]
+AppName=My Program
+AppVersion=1.5
+WizardStyle=modern
+DefaultDirName={autopf}\My Program
+DefaultGroupName=My Program
+UninstallDisplayIcon={app}\MyProg.exe
+OutputDir=userdocs:Inno Setup Examples Output
+
+[Files]
+; Place any regular files here
+Source: "MyProg.exe"; DestDir: "{app}";
+Source: "MyProg.chm"; DestDir: "{app}";
+Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme;
+; These files will be downloaded
+Source: "{tmp}\innosetup-latest.exe"; DestDir: "{app}"; Flags: external
+Source: "{tmp}\ISCrypt.dll"; DestDir: "{app}"; Flags: external
+
+[Icons]
+Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
+
+[Code]
+var
+  DownloadStatusLabel, DownloadFilenameLabel: TNewStaticText;
+  DownloadProgressBar: TNewProgressBar;
+  DownloadAbortButton: TNewButton;
+  DownloadControls: array of TControl;
+  NeedToAbortDownload: Boolean;
+
+procedure SetupDownloadControl(const Dest, Src: TControl; const Parent: TWinControl);
+var
+  N: Integer;
+begin
+  N := GetArrayLength(DownloadControls);
+  SetArrayLength(DownloadControls, N+1);
+  DownloadControls[N] := Dest;
+
+  if Src <> nil then begin
+    Dest.Left := Src.Left;
+    Dest.Top := Src.Top;
+    Dest.Width := Src.Width;
+    Dest.Height := Src.Height;
+    if Src is TNewStaticText then
+      TNewStaticText(Dest).Anchors := TNewStaticText(Src).Anchors
+    else if Src is TNewProgressBar then
+      TNewProgressBar(Dest).Anchors := TNewProgressBar(Src).Anchors;
+  end;
+  Dest.Visible := False;
+  Dest.Parent := Parent;
+end;
+
+procedure DownloadAbortButtonClick(Sender: TObject);
+begin
+  NeedToAbortDownload := MsgBox('Are you sure you want to stop the download?', mbConfirmation, MB_YESNO) = IDYES;
+end;
+
+procedure CreateDownloadControls;
+var
+  Page: TWizardPage;
+begin
+  Page := PageFromID(wpPreparing);
+
+  DownloadStatusLabel := TNewStaticText.Create(Page);
+  SetupDownloadControl(DownloadStatusLabel, WizardForm.StatusLabel, Page.Surface);
+  DownloadFilenameLabel := TNewStaticText.Create(Page);
+  SetupDownloadControl(DownloadFilenameLabel, WizardForm.FilenameLabel, Page.Surface);
+  DownloadProgressBar:= TNewProgressBar.Create(Page);
+  SetupDownloadControl(DownloadProgressBar, WizardForm.ProgressGauge, Page.Surface);
+  DownloadAbortButton := TNewButton.Create(Page);
+  SetupDownloadControl(DownloadAbortButton, nil, Page.Surface);
+
+  DownloadAbortButton.Caption := '&Stop download';
+  DownloadAbortButton.Top := DownloadProgressBar.Top + DownloadProgressBar.Height + ScaleY(8);
+  DownloadAbortButton.Height := WizardForm.CancelButton.Height;
+  DownloadAbortButton.Width := WizardForm.CalculateButtonWidth([DownloadAbortButton.Caption]);
+  DownloadAbortButton.Anchors := [akLeft, akTop];
+  DownloadAbortButton.OnClick := @DownloadAbortButtonClick;
+end;
+
+procedure InitializeWizard;
+begin
+  CreateDownloadControls;
+end;
+
+function OnDownloadProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean;
+begin
+  if NeedToAbortDownload then begin
+    Log('Need to abort download.');
+    Result := False;
+  end else begin
+    if ProgressMax <> 0 then
+      Log(Format('  %d of %d bytes done.', [Progress, ProgressMax]))
+    else
+      Log(Format('  %d bytes done.', [Progress]));
+    
+    DownloadFilenameLabel.Caption := Url;
+    DownloadFilenameLabel.Update;
+
+    if ProgressMax <> 0 then begin
+      DownloadProgressBar.Style := npbstNormal;
+      DownloadProgressBar.Max := ProgressMax;
+      DownloadProgressBar.Position := Progress;
+    end else
+      DownloadProgressBar.Style := npbstMarquee;
+    DownloadProgressBar.Update;
+
+    Result := True;
+  end;
+end;
+
+procedure ShowDownloadControls(const AVisible: Boolean);
+var
+  I: Integer;
+begin
+  for I := 0 to GetArrayLength(DownloadControls)-1 do
+    DownloadControls[I].Visible := AVisible;
+end;
+
+procedure DownloadFiles;
+begin
+  try
+    DownloadStatusLabel.Caption := 'Downloading additional files...';
+    ShowDownloadControls(True);
+    NeedToAbortDownload := False;
+    DownloadTemporaryFile('https://jrsoftware.org/download.php/is.exe', 'innosetup-latest.exe', '', @OnDownloadProgress);
+    DownloadTemporaryFile('https://jrsoftware.org/download.php/iscrypt.dll', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc', @OnDownloadProgress);
+  finally
+    ShowDownloadControls(False);
+  end;
+end;
+
+function PrepareToInstall(var NeedsRestart: Boolean): String;
+begin
+  try
+    DownloadFiles;
+    Result := '';
+  except
+    Result := 'Failed to download files: ' + GetExceptionMessage;
+  end;
+end;

+ 47 - 1
ISHelp/isxfunc.xml

@@ -1687,7 +1687,8 @@ end;</pre></example>
         <remarks><p>Use <tt>Flags: dontcopy</tt> in the [Files] section to tell Setup to skip the file during the normal file copying stage.</p>
 <p>Use <tt>Flags: noencryption</tt> in the [Files] section if encryption is enabled and you call the ExtractTemporaryFile function prior to the user entering the correct password.</p>
 <p>When solid compression is enabled, be sure to list your temporary files at (or near) the top of the [Files] section. In order to extract an arbitrary file in a solid-compressed installation, Setup must first decompress all prior files (to a temporary buffer in memory). This can result in a substantial delay if a number of other files are listed above the specified file in the [Files] section.</p></remarks>
-        <seealso><p><link topic="isxfunc_ExtractTemporaryFiles">ExtractTemporaryFiles</link></p></seealso>
+        <seealso><p><link topic="isxfunc_ExtractTemporaryFiles">ExtractTemporaryFiles</link><br />
+<link topic="isxfunc_DownloadTemporaryFile">DownloadTemporaryFile</link></p></seealso>
         <example><pre>[Files]
 Source: "Readme.txt"; Flags: dontcopy noencryption
 
@@ -1741,6 +1742,51 @@ begin
   Result := True;
 end;</pre></example>
       </function>
+      <function>
+        <name>DownloadTemporaryFile</name>
+        <prototype>function DownloadTemporaryFile(const Url, FileName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;</prototype>
+        <description><p>Downloads the file from the specified URL to a temporary directory using the specified name. To find the location of the temporary directory, use <tt>ExpandConstant('{tmp}')</tt>.</p>
+<p>An exception will be raised if there was an error. Otherwise, returns the number of bytes downloaded.</p>
+<p>Supports HTTPS (but not expired or self-signed certificates) and HTTP. Redirects are automatically followed and proxy settings are automatically used. Safe to use from services.</p>
+<p>If RequiredSHA256OfFile is not empty it will compare this to the SHA-256 of the downloaded file and raise an exception if the hashes don't match.</p>
+<p>Set OnDownloadProgress to a function to be informed of progress, or <tt>nil</tt> otherwise.</p></description>
+        <remarks><p>TOnDownloadProgress is defined as:</p>
+<p><tt>TOnDownloadProgress = function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean;</tt></p>
+<p>ProgressMax will be 0 if the file size is still unknown. Return True to allow the download to continue, False otherwise.</p>
+<p>For basic authentication use a special URL format like this: http://username:[email protected]/</p></remarks>
+        <seealso><p><link topic="isxfunc_DownloadTemporaryFileSize">DownloadTemporaryFileSize</link><br />
+<link topic="isxfunc_ExtractTemporaryFile">ExtractTemporaryFile</link></p></seealso>
+        <example><pre>
+[Code]
+function OnDownloadProgress(const Url, Filename: string; const Progress, ProgressMax: Int64): Boolean;
+begin
+  if ProgressMax &lt;&gt; 0 then
+    Log(Format('  %d of %d bytes done.', [Progress, ProgressMax]))
+  else
+    Log(Format('  %d bytes done.', [Progress]));
+  Result := True;
+end;
+
+function InitializeSetup: Boolean;
+begin
+  try
+    DownloadTemporaryFile('https://jrsoftware.org/download.php/is.exe', 'innosetup-latest.exe', '', @OnDownloadProgress);
+    DownloadTemporaryFile('https://jrsoftware.org/download.php/iscrypt.dll', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc', @OnDownloadProgress);
+    Result := True;
+  except
+    Log(GetExceptionMessage);
+    Result := False;
+  end;
+end;</pre>
+<p>See <i>CodeDownloadFiles.iss</i> for another example.</p></example>
+      </function>
+      <function>
+        <name>DownloadTemporaryFileSize</name>
+        <prototype>function DownloadTemporaryFileSize(const Url): Int64;</prototype>
+        <description><p>Returns the size of the file from the specified URL, without downloading the file. If the server does not provide the file size, -1 will be returned.</p>
+<p>An exception will be raised if there was an error.</p>
+<p>See <link topic="isxfunc_DownloadTemporaryFile">DownloadTemporaryFile</link> for other considerations.</p></description>
+      </function>
     </subcategory>
     <subcategory>
       <function>

+ 224 - 11
Projects/Install.pas

@@ -16,8 +16,14 @@ interface
 procedure PerformInstall(var Succeeded: Boolean; const ChangesEnvironment,
   ChangesAssociations: Boolean);
 
+
+type
+  TOnDownloadProgress = function(const Url, BaseName: string; const Progress, ProgressMax: Int64): Boolean of object;
+
 procedure ExtractTemporaryFile(const BaseName: String);
 function ExtractTemporaryFiles(const Pattern: String): Integer;
+function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;
+function DownloadTemporaryFileSize(const Url: String): Int64;
 
 implementation
 
@@ -26,7 +32,7 @@ uses
   InstFunc, InstFnc2, SecurityFunc, Msgs, Main, Logging, Extract, FileClass,
   Compress, SHA1, PathFunc, CmnFunc, CmnFunc2, RedirFunc, Int64Em, MsgIDs,
   Wizard, DebugStruct, DebugClient, VerInfo, ScriptRunner, RegDLL, Helper,
-  ResUpdate, DotNet, TaskbarProgressFunc, NewProgressBar, RestartManager;
+  ResUpdate, DotNet, TaskbarProgressFunc, NewProgressBar, RestartManager, Net.HTTPClient;
 
 type
   TSetupUninstallLog = class(TUninstallLog)
@@ -310,6 +316,13 @@ begin
       Result := PathExtractName(Result);
 end;
 
+function LastErrorIndicatesPossiblyInUse(const LastError: DWORD; const CheckAlreadyExists: Boolean): Boolean;
+begin
+  Result := (LastError = ERROR_ACCESS_DENIED) or
+            (LastError = ERROR_SHARING_VIOLATION) or
+            (CheckAlreadyExists and (LastError = ERROR_ALREADY_EXISTS));
+end;
+
 procedure PerformInstall(var Succeeded: Boolean; const ChangesEnvironment,
   ChangesAssociations: Boolean);
 type
@@ -1507,8 +1520,7 @@ var
             if LastError = ERROR_FILE_NOT_FOUND then
               Break;
             { Does the error code indicate that it is possibly in use? }
-            if (LastError = ERROR_ACCESS_DENIED) or
-               (LastError = ERROR_SHARING_VIOLATION) then begin
+            if LastErrorIndicatesPossiblyInUse(LastError, False) then begin
               DoHandleFailedDeleteOrMoveFileTry('DeleteFile', TempFile, DestFile,
                 LastError, RetriesLeft, LastOperation, NeedsRestart, ReplaceOnRestart,
                 DoBreak, DoContinue);
@@ -1533,15 +1545,13 @@ var
                 ((CurFile^.FileType = ftUninstExe) and DestFileExistedBefore)) then begin
           LastOperation := SetupMessages[msgErrorRenamingTemp];
           { Since the DeleteFile above succeeded you would expect the rename to
-            also always succeed, but if it doesn't anyway. }
+            also always succeed, but if it doesn't retry anyway. }
           RetriesLeft := 4;
           while not MoveFileRedir(DisableFsRedir, TempFile, DestFile) do begin
             { Couldn't rename the temporary file... }
             LastError := GetLastError;
             { Does the error code indicate that it is possibly in use? }
-            if (LastError = ERROR_ACCESS_DENIED) or
-               (LastError = ERROR_SHARING_VIOLATION) or
-               (LastError = ERROR_ALREADY_EXISTS) then begin
+            if LastErrorIndicatesPossiblyInUse(LastError, True) then begin
               DoHandleFailedDeleteOrMoveFileTry('MoveFile', TempFile, DestFile,
                 LastError, RetriesLeft, LastOperation, NeedsRestart, ReplaceOnRestart,
                 DoBreak, DoContinue);
@@ -2963,8 +2973,7 @@ var
         Break;
       LastError := GetLastError;
       { Does the error code indicate that the file is possibly in use? }
-      if (LastError = ERROR_ACCESS_DENIED) or
-         (LastError = ERROR_SHARING_VIOLATION) then begin
+      if LastErrorIndicatesPossiblyInUse(LastError, False) then begin
         if RetriesLeft > 0 then begin
           LogFmt('The existing file appears to be in use (%d). ' +
             'Retrying.', [LastError]);
@@ -3396,7 +3405,7 @@ begin
       Exit;
     end;
   end;
-  InternalError(Format('ExtractTemporaryFile: The file "%s" was not found', [BaseName]));
+  InternalErrorFmt('ExtractTemporaryFile: The file "%s" was not found', [BaseName]);
 end;
 
 function ExtractTemporaryFiles(const Pattern: String): Integer;
@@ -3430,7 +3439,211 @@ begin
   end;
 
   if Result = 0 then
-    InternalError(Format('ExtractTemporaryFiles: No files matching "%s" found', [Pattern]));
+    InternalErrorFmt('ExtractTemporaryFiles: No files matching "%s" found', [Pattern]);
+end;
+
+type
+  THTTPDataReceiver = class
+  private
+    FBaseName, FUrl: String;
+    FOnDownloadProgress: TOnDownloadProgress;
+    FAborted: Boolean;
+    FProgress, FProgressMax: Int64;
+    FLastReportedProgress, FLastReportedProgressMax: Int64;
+  public
+    property BaseName: String write FBaseName;
+    property Url: String write FUrl;
+    property OnDownloadProgress: TOnDownloadProgress write FOnDownloadProgress;
+    property Aborted: Boolean read FAborted;
+    property Progress: Int64 read FProgress;
+    property ProgressMax: Int64 read FProgressMax;
+    procedure OnReceiveData(const Sender: TObject; AContentLength: Int64; AReadCount: Int64; var Abort: Boolean);
+  end;
+
+procedure THTTPDataReceiver.OnReceiveData(const Sender: TObject; AContentLength: Int64; AReadCount: Int64; var Abort: Boolean);
+begin
+  FProgress := AReadCount;
+  FProgressMax := AContentLength;
+
+  if Assigned(FOnDownloadProgress) then begin
+    { Make sure script isn't called crazy often because that would slow the download significantly. Only report:
+      -At start or finish
+      -Or if somehow Progress decreased or Max changed
+      -Or if at least 512 KB progress was made since last report
+    }
+    if (FProgress = 0) or (FProgress = FProgressMax) or
+       (FProgress < FLastReportedProgress) or (FProgressMax <> FLastReportedProgressMax) or
+       ((FProgress - FLastReportedProgress) > 524288) then begin
+      try
+        if not FOnDownloadProgress(FUrl, FBaseName, FProgress, FProgressMax) then
+          Abort := True;
+      finally
+        FLastReportedProgress := FProgress;
+        FLastReportedProgressMax := FProgressMax;
+      end;
+    end;
+  end;
+
+  if not Abort and DownloadTemporaryFileAllowProcessMessages then
+    Application.ProcessMessages;
+
+  if Abort then
+    FAborted := True
+end;
+
+procedure SetSecureProtocols(const AHTTPClient: THTTPClient);
+begin
+  { TLS 1.2 isn't enabled by default on older versions of Windows }
+  AHTTPClient.SecureProtocols := [THTTPSecureProtocol.TLS1, THTTPSecureProtocol.TLS11, THTTPSecureProtocol.TLS12];
+end;
+
+function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;
+var
+  DisableFsRedir: Boolean;
+  PrevState: TPreviousFsRedirectionState;
+  DestFile, TempFile: String;
+  TempF: TFileStream;
+  TempFileLeftOver: Boolean;
+  HTTPDataReceiver: THTTPDataReceiver;
+  HTTPClient: THTTPClient;
+  HTTPResponse: IHTTPResponse;
+  SHA256OfFile: String;
+  RetriesLeft: Integer;
+  LastError: DWORD;
+begin
+  if Url = '' then
+    InternalError('DownloadTemporaryFile: Invalid Url value');
+  if BaseName = '' then
+    InternalError('DownloadTemporaryFile: Invalid BaseName value');
+
+  DestFile := AddBackslash(TempInstallDir) + BaseName;
+
+  LogFmt('Downloading temporary file from %s: %s', [Url, DestFile]);
+
+  DisableFsRedir := InstallDefaultDisableFsRedir;
+
+  { Prepare directory }
+  if FileExists(DestFile) then begin
+    if (RequiredSHA256OfFile <> '') and (RequiredSHA256OfFile = GetSHA256OfFile(DisableFsRedir, DestFile)) then begin
+      Log('  File already downloaded.');
+      Result := 0;
+      Exit;
+    end;    
+    SetFileAttributesRedir(DisableFsRedir, DestFile, GetFileAttributesRedir(DisableFsRedir, DestFile) and not FILE_ATTRIBUTE_READONLY);
+    DelayDeleteFile(DisableFsRedir, DestFile, 13, 50, 250);
+  end else
+    ForceDirectories(DisableFsRedir, PathExtractPath(DestFile));
+
+  HTTPDataReceiver := nil;
+  HTTPClient := nil;
+  TempF := nil;
+  TempFileLeftOver := False;
+  try
+    { Setup downloader }
+    HTTPDataReceiver := THTTPDataReceiver.Create;
+    HTTPDataReceiver.BaseName := BaseName;
+    HTTPDataReceiver.Url := Url;
+    HTTPDataReceiver.OnDownloadProgress := OnDownloadProgress;
+
+    HTTPClient := THTTPClient.Create; { http://docwiki.embarcadero.com/RADStudio/Rio/en/Using_an_HTTP_Client }
+    SetSecureProtocols(HTTPClient);
+    HTTPClient.OnReceiveData := HTTPDataReceiver.OnReceiveData;
+
+    { Create temporary file }
+    TempFile := GenerateUniqueName(DisableFsRedir, PathExtractPath(DestFile), '.tmp');
+    if not DisableFsRedirectionIf(DisableFsRedir, PrevState) then
+      raise Exception.Create('DisableFsRedirectionIf failed');
+    try
+      TempF := TFileStream.Create(TempFile, fmCreate);
+      TempFileLeftOver := True;
+    finally
+      RestoreFsRedirection(PrevState);
+    end;
+
+    { To test redirects: https://jrsoftware.org/download.php/is.exe
+      To test expired certificates: https://expired.badssl.com/
+      To test self-signed certificates: https://self-signed.badssl.com/
+      To test basic authentication: https://guest:[email protected]/HTTP/Basic/
+      To test 100 MB file: https://speed.hetzner.de/100MB.bin
+      To test 1 GB file: https://speed.hetzner.de/1GB.bin }
+
+    { Download to temporary file}
+    HTTPResponse := HTTPClient.Get(Url, TempF);
+    if HTTPDataReceiver.Aborted then
+      raise Exception.Create('Download aborted')
+    else if (HTTPResponse.StatusCode < 200) or (HTTPResponse.StatusCode > 299) then
+      raise Exception.CreateFmt('Download failed: %d %s', [HTTPResponse.StatusCode, HTTPResponse.StatusText])
+    else begin
+      { Download completed, get temporary file size and close it }
+      Result := TempF.Size;
+      FreeAndNil(TempF);
+
+      { Check hash if specified, otherwise check everything else we can check }
+      if RequiredSHA256OfFile <> '' then begin
+        try
+          SHA256OfFile := GetSHA256OfFile(DisableFsRedir, TempFile);
+        except on E: Exception do
+          raise Exception.CreateFmt('File hash failed: %s', [E.Message]);
+        end;
+        if RequiredSHA256OfFile <> SHA256OfFile then
+          raise Exception.CreateFmt('Invalid file hash: expected %s, found %s', [RequiredSHA256OfFile, SHA256OfFile]);
+      end else begin
+        if HTTPDataReceiver.Progress <> HTTPDataReceiver.ProgressMax then
+          raise Exception.CreateFmt('Invalid progress: %d of %d', [HTTPDataReceiver.Progress, HTTPDataReceiver.ProgressMax])
+        else if HTTPDataReceiver.ProgressMax <> Result then
+          raise Exception.CreateFmt('Invalid file size: expected %d, found %d', [HTTPDataReceiver.ProgressMax, Result]);
+      end;
+
+      { Rename the temporary file to the new name now, with retries if needed }
+      RetriesLeft := 4;
+      while not MoveFileRedir(DisableFsRedir, TempFile, DestFile) do begin
+        { Couldn't rename the temporary file... }
+        LastError := GetLastError;
+        { Does the error code indicate that it is possibly in use? }
+        if LastErrorIndicatesPossiblyInUse(LastError, True) then begin
+          LogFmt('  The existing file appears to be in use (%d). ' +
+            'Retrying.', [LastError]);
+          Dec(RetriesLeft);
+          Sleep(1000);
+          if RetriesLeft > 0 then
+            Continue;
+        end;
+        { Some other error occurred, or we ran out of tries }
+        SetLastError(LastError);
+        Win32ErrorMsg('MoveFile'); { Throws an exception }
+      end;
+      TempFileLeftOver := False;
+    end;
+  finally
+    TempF.Free;
+    HTTPClient.Free;
+    HTTPDataReceiver.Free;
+    if TempFileLeftOver then
+      DeleteFileRedir(DisableFsRedir, TempFile);
+  end;
+end;
+
+function DownloadTemporaryFileSize(const Url: String): Int64;
+var
+  HTTPClient: THTTPClient;
+  HTTPResponse: IHTTPResponse;
+begin
+  if Url = '' then
+    InternalError('DownloadTemporaryFileSize: Invalid Url value');
+
+  LogFmt('Getting size of %s.', [Url]);
+
+  HTTPClient := THTTPClient.Create;
+  try
+    SetSecureProtocols(HTTPClient);
+    HTTPResponse := HTTPClient.Head(Url);
+    if (HTTPResponse.StatusCode < 200) or (HTTPResponse.StatusCode > 299) then
+      raise Exception.CreateFmt('Getting size failed: %d %s', [HTTPResponse.StatusCode, HTTPResponse.StatusText])
+    else
+      Result := HTTPResponse.ContentLength; { Could be -1 }
+  finally
+    HTTPClient.Free;
+  end;
 end;
 
 end.

+ 1 - 1
Projects/Main.pas

@@ -179,7 +179,7 @@ var
   DisableCodeConsts: Integer;
   SetupExitCode: Integer;
   CreatedIcon: Boolean;
-  RestartInitiatedByThisProcess: Boolean;
+  RestartInitiatedByThisProcess, DownloadTemporaryFileAllowProcessMessages: Boolean;
 {$IFDEF IS_D12}
   TaskbarButtonHidden: Boolean;
 {$ENDIF}

+ 10 - 2
Projects/ScriptFunc.pas

@@ -120,10 +120,18 @@ const
   );
 
   { Install }
-  InstallTable: array [0..1] of AnsiString =
+{$IFNDEF PS_NOINT64}
+  InstallTable: array [0..3] of AnsiString =
+{$ELSE}
+  InstallTable: array [0..2] of AnsiString =
+{$ENDIF}
   (
     'procedure ExtractTemporaryFile(const FileName: String);',
-    'function ExtractTemporaryFiles(const Pattern: String): Integer;'
+    'function ExtractTemporaryFiles(const Pattern: String): Integer;',
+{$IFNDEF PS_NOINT64}
+    'function DownloadTemporaryFile(const Url, FileName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;',
+    'function DownloadTemporaryFileSize(const Url: String): Int64;'
+{$ENDIF}
   );
 
   { InstFunc }

+ 4 - 0
Projects/ScriptFunc_C.pas

@@ -142,6 +142,10 @@ begin
     '  SuiteMask: Word;' +
     'end');
 
+{$IFNDEF PS_NOINT64}
+  RegisterType('TOnDownloadProgress', 'function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean;');
+{$ENDIF}
+
   RegisterFunctionTable(ScriptDlgTable);
   RegisterFunctionTable(NewDiskTable);
   RegisterFunctionTable(BrowseFuncTable);

+ 14 - 0
Projects/ScriptFunc_R.pas

@@ -739,6 +739,8 @@ end;
 function InstallProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean;
 var
   PStart: Cardinal;
+  P: PPSVariantProcPtr;
+  OnDownloadProgress: TOnDownloadProgress;
 begin
   if IsUninstaller then
     NoUninstallFuncError(Proc.Name);
@@ -750,6 +752,18 @@ begin
     ExtractTemporaryFile(Stack.GetString(PStart));
   end else if Proc.Name = 'EXTRACTTEMPORARYFILES' then begin
     Stack.SetInt(PStart, ExtractTemporaryFiles(Stack.GetString(PStart-1)));
+{$IFNDEF PS_NOINT64}
+  end else if Proc.Name = 'DOWNLOADTEMPORARYFILE' then begin
+    P := Stack.Items[PStart-4];
+    { ProcNo 0 means nil was passed by the script }
+    if P.ProcNo <> 0 then
+      OnDownloadProgress := TOnDownloadProgress(Caller.GetProcAsMethod(P.ProcNo))
+    else
+      OnDownloadProgress := nil;
+    Stack.SetInt64(PStart, DownloadTemporaryFile(Stack.GetString(PStart-1), Stack.GetString(PStart-2), Stack.GetString(PStart-3), OnDownloadProgress));
+  end else if Proc.Name = 'DOWNLOADTEMPORARYFILESIZE' then begin
+    Stack.SetInt64(PStart, DownloadTemporaryFileSize(Stack.GetString(PStart-1)));
+{$ENDIF}
   end else
     Result := False;
 end;

+ 8 - 8
Projects/Wizard.pas

@@ -208,6 +208,7 @@ type
     function ShouldSkipPage(const PageID: Integer): Boolean;
     procedure UpdateComponentSizes;
     procedure UpdateComponentSizesEnum(Index: Integer; HasChildren: Boolean; Ext: LongInt);
+    procedure UpdateCurPageButtonState;
     procedure UpdatePage(const PageID: Integer);
     procedure UpdateSelectTasksPage;
     procedure WMSysCommand(var Message: TWMSysCommand); message WM_SYSCOMMAND;
@@ -234,7 +235,6 @@ type
     procedure IncTopDecHeight(const AControl: TControl; const Amount: Integer);
     function PageFromID(const ID: Integer): TWizardPage;
     function PageIndexFromID(const ID: Integer): Integer;
-    procedure UpdateCurPageButtonVisibility;
     procedure SetCurPage(const NewPageID: Integer);
     procedure SelectComponents(const ASelectComponents: TStringList); overload;
     procedure SelectTasks(const ASelectTasks: TStringList); overload;
@@ -1860,7 +1860,6 @@ end;
 
 function TWizardForm.PrepareToInstall(const WizardComponents, WizardTasks: TStringList): String;
 var
-  WindowDisabler: TWindowDisabler;
   CodeNeedsRestart: Boolean;
   Y: Integer;
   S: String;
@@ -1879,19 +1878,20 @@ begin
     SetCurPage(wpPreparing);
     BackButton.Visible := False;
     NextButton.Visible := False;
+    CancelButton.Enabled := False;
     if InstallMode = imSilent then begin
       SetActiveWindow(Application.Handle);  { ensure taskbar button is selected }
       WizardForm.Show;
     end;
     WizardForm.Update;
-    WindowDisabler := TWindowDisabler.Create;
     try
+      DownloadTemporaryFileAllowProcessMessages := True;
       CodeNeedsRestart := False;
       Result := CodeRunner.RunStringFunctions('PrepareToInstall', [@CodeNeedsRestart], bcNonEmpty, True, '');
       PrepareToInstallNeedsRestart := (Result <> '') and CodeNeedsRestart;
     finally
-      WindowDisabler.Free;
-      UpdateCurPageButtonVisibility;
+      DownloadTemporaryFileAllowProcessMessages := False;
+      UpdateCurPageButtonState;
     end;
     Application.BringToFront;
   end;
@@ -2193,7 +2193,7 @@ begin
   Result := -1;
 end;
 
-procedure TWizardForm.UpdateCurPageButtonVisibility;
+procedure TWizardForm.UpdateCurPageButtonState;
 var
   PageIndex: Integer;
   Page: TWizardPage;
@@ -2249,7 +2249,7 @@ begin
     not(CurPageID in [wpWelcome, wpFinished]);
 
   { Set button visibility and captions }
-  UpdateCurPageButtonVisibility;
+  UpdateCurPageButtonState;
 
   BackButton.Caption := SetupMessages[msgButtonBack];
   if CurPageID = wpReady then begin
@@ -2573,7 +2573,7 @@ begin
                 if RmFoundApplications then
                   Break;  { stop on the page }
               finally
-                UpdateCurPageButtonVisibility;
+                UpdateCurPageButtonState;
               end;
             end;
           finally

+ 1 - 0
setup.iss

@@ -137,6 +137,7 @@ Source: "Examples\CodeAutomation2.iss"; DestDir: "{app}\Examples"; Flags: ignore
 Source: "Examples\CodeClasses.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeDlg.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeDll.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
+Source: "Examples\CodeDownloadFiles.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeExample1.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodePrepareToInstall.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\Components.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch

+ 15 - 3
whatsnew.htm

@@ -47,10 +47,22 @@ For conditions of distribution and use, see <a href="https://jrsoftware.org/file
   <li>Added buttons to the Welcome dialog to <a href="https://jrsoftware.org/isdonate.php">Donate</a> to support Inno Setup (Thank you!) and to <a href="https://jrsoftware.org/ismail.php">Subscribe</a> to the Inno Setup Mailing List to be notified by e-mail of new Inno Setup releases.</li>
   <li>The Run Parameters dialog now shows a list of most recently used parameters.</li>
 </ul>
-<p><span class="head2">Pascal Scripting ([Code] section) updates</span></p>
-<p>Pascal Scripting now supports SHA-256 hashes:</p>
+<p><span class="head2">Built-in download support for [Code]</span></p>
+<p>Pascal Scripting now supports downloading files and checking SHA-256 hashes:</p>
 <ul>
-  <li>Added new <tt>GetSHA256OfFile</tt>, <tt>GetSHA256OfString</tt>, and <tt>GetSHA256OfUnicodeString</tt> support functions.</li>
+  <li>Added new <tt>DownloadTemporaryFile</tt> support function to download files without using a third-party tool:
+  <ul>
+    <li>Allows you to <a href="https://i.imgur.com/ZLmiS3t.png">show the download progress</a> to the user. See the new <i>CodeDownloadFiles.iss</i> example script for an example.</li>
+    <li>Supports HTTPS (but not expired or self-signed certificates) and HTTP.</li>
+    <li>Redirects are automatically followed and proxy settings are automatically used.</li>
+    <li>Safe to use from services unlike existing third-party tools.</li>
+    <li>Supports SHA-256 hash checking of the downloaded file.</li>
+    <li>Supports basic authentication.</li>
+  </ul>
+  </li>
+  <li>Added new <tt>DownloadTemporaryFileSize</tt> support function to get the size of a file without downloading it.</li>
+  <li>Added new <tt>GetSHA256OfFile</tt>, <tt>GetSHA256OfString</tt>, and <tt>GetSHA256OfUnicodeString</tt> support functions to calculate SHA-256 hashes.</li>
+  <li><b>Change in default behavior:</b> Setup no longer disables itself entirely while <tt>PrepareToInstall</tt> is running. Instead only the Cancel button is disabled.</li>
 </ul>
 <p><span class="head2">Inno Setup Preprocessor (ISPP) updates</span></p>
 <p>ISPP now uses 64-bit integers and has new functions to more easily compare version numbers:</p>