瀏覽代碼

Add support for dowload+extractarchives in a simple and clean way 👍

For such entries the archive is downloaded to {tmp}\_isetup\<randomdir>\<destname> using a TDownloadWizardPage, as the first step of PrepareToInstall. Supports verification.

On success the entries' SourceFilename is updated to the temp file, the download flag is removed and also DestName and verification. Áfter that the rest (PreviousInstallCompleted, RegisterResourcesWithRestartManager, and installation) works normally and required no changes.

On error the problem is displayed by the ready page. Also didn't require changes, except for an extract on BaseName display.

Todo:
-Rename CodeDownloadFiles.iss since there's no [Code] in it anymore.
-Offer Abort/Retry when a download fails? Or even Ignore somehow?
-Let the user choose if it should show BaseNames or URLs while downloading with a new directive? Both for archives and files.
-Document
Martijn Laan 3 月之前
父節點
當前提交
2d0ec7b9e5

+ 15 - 57
Examples/CodeDownloadFiles.iss

@@ -3,6 +3,10 @@
 ; This script shows how the [Files] section can be used to download files and
 ; archives while showing the download and extraction progress to the user.
 ;
+; Archives will be downloaded to temporary files at the start of the Preparing
+; To Install step. Other files will be downloaded directly to their destination
+; during the actual installation. 
+;
 ; To verify the downloaded files, this script shows two methods:
 ; -For innosetup-latest.exe and MyProg-ExtraReadmes.7z: using Inno Setup
 ;  Signature Tool, the [ISSigKeys] section, and the AddWithISSigVerify support
@@ -20,9 +24,9 @@ DefaultDirName={autopf}\My Program
 DefaultGroupName=My Program
 UninstallDisplayIcon={app}\MyProg.exe
 OutputDir=userdocs:Inno Setup Examples Output
-;Use "ArchiveExtraction=enhanced" if your archive has a password
-;Use "ArchiveExtraction=full" if your archive is not a .7z file but for example a .zip file
-ArchiveExtraction=enhanced/nopassword
+ArchiveExtraction=full
+;Use "ArchiveExtraction=enhanced" if all your archives are .7z files
+;Use "ArchiveExtraction=enhanced/nopassword" if all your archives are not password-protected
 
 [ISSigKeys]
 Name: mykey; RuntimeID: def02; \
@@ -35,64 +39,18 @@ Name: mykey; RuntimeID: def02; \
 Source: "MyProg.exe"; DestDir: "{app}"
 Source: "MyProg.chm"; DestDir: "{app}"
 Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme
-; These files will be downloaded using [Files] only
+; These files will be downloaded and verified
 Source: "https://jrsoftware.org/download.php/is.exe?dontcount=1"; DestName: "innosetup-latest.exe"; DestDir: "{app}"; \
   ExternalSize: 7_000_000; Flags: external download ignoreversion issigverify
 Source: "https://jrsoftware.org/download.php/iscrypt.dll?dontcount=1"; DestName: "ISCrypt.dll"; DestDir: "{app}"; \
   Hash: "2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc"; \
   ExternalSize: 2560; Flags: external download ignoreversion
-; These files will be downloaded by [Code]. If you include flag issigverify here the file will be verified
-; a second time while copying. Verification while copying is efficient, except for archives.
-Source: "{tmp}\MyProg-ExtraReadmes.7z"; DestDir: "{app}"; Flags: external extractarchive recursesubdirs ignoreversion
+; This file will be downloaded, verified and extracted
+Source: "https://jrsoftware.org/download.php/myprog-extrareadmes.7z"; DestName: "MyProg.ExtraReadmes.7z"; DestDir: "{app}"; \
+  ExternalSize: 269; Flags: external download extractarchive recursesubdirs ignoreversion issigverify
+; This file will be downloaded and extracted without verificaton
+Source: "https://github.com/jrsoftware/issrc/archive/refs/heads/main.zip"; DestName: "issrc-main.zip"; DestDir: "{app}"; \
+  ExternalSize: 15_000_000; Flags: external download extractarchive recursesubdirs ignoreversion
 
 [Icons]
-Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
-
-[Code]
-var
-  DownloadPage: TDownloadWizardPage;
-  AllowedKeysRuntimeIDs: TStringList;
-
-procedure InitializeWizard;
-begin
-  DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
-  DownloadPage.ShowBaseNameInsteadOfUrl := True;
-  
-  // To allow all keys you can also just pass nil instead of this list to AddWithISSigVerify 
-  AllowedKeysRuntimeIDs := TStringList.Create;
-  AllowedKeysRuntimeIDs.Add('def02');
-end;
-
-procedure DeinitializeSetup;
-begin
-  if AllowedKeysRuntimeIDs <> nil then
-    AllowedKeysRuntimeIDs.Free;
-end;
-
-function NextButtonClick(CurPageID: Integer): Boolean;
-begin
-  if CurPageID = wpReady then begin
-    DownloadPage.Clear;
-    // Use AddEx or AddExWithISSigVerify to specify a username and password
-    DownloadPage.AddWithISSigVerify(
-      'https://jrsoftware.org/download.php/myprog-extrareadmes.7z', '',
-      'MyProg-ExtraReadmes.7z', AllowedKeysRuntimeIDs);
-    DownloadPage.Show;
-    try
-      try
-        // Downloads the files to {tmp}
-        DownloadPage.Download;
-        Result := True;
-      except
-        if DownloadPage.AbortedByUser then
-          Log('Aborted by user.')
-        else
-          SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
-        Result := False;
-      end;
-    finally
-      DownloadPage.Hide;
-    end;
-  end else
-    Result := True;
-end;
+Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"

+ 1 - 1
Projects/Src/Compiler.Messages.pas

@@ -290,7 +290,7 @@ const
     '"dontcopy" flag is used';
   SCompilerFilesWildcardNotMatched = 'No files found matching "%s"';
   SCompilerFilesDestNameCantBeSpecified = 'Parameter "DestName" cannot be specified if ' +
-    'the "Source" parameter contains wildcards or flag "extractarchive" is used';
+    'the "Source" parameter contains wildcards or flag "extractarchive" is used but "download" is not';
   SCompilerFilesParamRequiresFlag = 'Parameter "%s" may only be used when the "%s" flag is used';
   SCompilerFilesParamFlagConflict = 'Parameter "%s" may not be used when the "%s" flag is used';
   SCompilerFilesParamFlagConflictSameSource = 'Parameter "%s" and the "%s" flag cannot both be used on a single source file';

+ 2 - 4
Projects/Src/Compiler.SetupCompiler.pas

@@ -5482,13 +5482,11 @@ begin
             AbortCompileFmt(SCompilerParamFlagMissing, ['external', 'download']);
           if not(foIgnoreVersion in Options) then
             AbortCompileFmt(SCompilerParamFlagMissing, ['ignoreversion', 'download']);
-          if foExtractArchive in Options then
-            AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'extractarchive']);
           if foCompareTimeStamp in Options then
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'comparetimestamp']);
           if foSkipIfSourceDoesntExist in Options then
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'skipifsourcedoesntexist']);
-          if RecurseSubdirs then
+            if not(foExtractArchive in Options) and RecurseSubdirs then
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'recursesubdirs', 'download']);
           if ADestName = '' then
             AbortCompileFmt(SCompilerParamFlagMissingParam, ['DestName', 'download']);
@@ -5542,7 +5540,7 @@ begin
             Include(Options, foRecurseSubDirsExternal);
           CheckConst(SourceWildcard, MinVersion, []);
         end;
-        if (ADestName <> '') and (SourceIsWildcard or (foExtractArchive in Options)) then
+        if (ADestName <> '') and (SourceIsWildcard or (not (foDownload in Options) and (foExtractArchive in Options))) then
           AbortCompile(SCompilerFilesDestNameCantBeSpecified);
         CheckConst(ADestDir, MinVersion, []);
         ADestDir := AddBackslash(ADestDir);

+ 3 - 0
Projects/Src/Setup.Install.pas

@@ -2174,6 +2174,9 @@ var
               SetProgress(ProgressBefore);
               ExpectedBytesLeft := CurFile^.ExternalSize;
               if foDownload in CurFile^.Options then begin
+                { Archive download should have been done already by Setup.WizardForm's DownloadArchivesToExtract }
+                if foExtractArchive in CurFile^.Options then
+                  InternalError('Unexpected Download flag');
                 if foSkipIfSourceDoesntExist in CurFile^.Options then
                   InternalError('Unexpected SkipIfSourceDoesntExist flag');
                 if not(foCustomDestName in CurFile^.Options) then

+ 3 - 0
Projects/Src/Setup.MainFunc.pas

@@ -1868,6 +1868,9 @@ begin
         else begin
           { External file }
           if foDownload in CurFile^.Options then begin
+           { Archive download should have been done already by Setup.WizardForm's DownloadArchivesToExtract }
+            if foExtractArchive in CurFile^.Options then
+              InternalError('Unexpected Download flag');
             if not(foCustomDestName in CurFile^.Options) then
               InternalError('Expected CustomDestName flag');
             { CurFile^.DestName now includes a a filename, see TSetupCompiler.EnumFilesProc.ProcessFileList }

+ 11 - 3
Projects/Src/Setup.ScriptDlg.pas

@@ -200,7 +200,9 @@ type
         const AllowedKeysRuntimeIDs: TStringList): Integer;
       function AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String): Integer;
       function AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName, Password: String;
-        const AllowedKeysRuntimeIDs: TStringList): Integer;
+        const AllowedKeysRuntimeIDs: TStringList): Integer; overload;
+      function AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName, Password: String;
+        const ISSigAllowedKeys: AnsiString): Integer; overload;
       procedure Clear;
       function Download: Int64;
       property OnDownloadProgress: TOnDownloadProgress write FOnDownloadProgress;
@@ -986,7 +988,7 @@ begin
     else
       Log(Format('  %d bytes done.', [Progress]));
 
-    FMsg2Label.Caption := IfThen(FShowBaseNameInsteadOfUrl, BaseName, Url);
+    FMsg2Label.Caption := IfThen(FShowBaseNameInsteadOfUrl, PathExtractName(BaseName), Url);
     if ProgressMax > MaxLongInt then begin
       Progress32 := Round((Progress / ProgressMax) * MaxLongInt);
       ProgressMax32 := MaxLongInt;
@@ -1117,8 +1119,14 @@ end;
 function TDownloadWizardPage.AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName,
   Password: String; const AllowedKeysRuntimeIDs: TStringList): Integer;
 begin
-  { Also see Setup.ScriptFunc DownloadTemporaryFileWithISSigVerify }
   const ISSigAllowedKeys = ConvertAllowedKeysRuntimeIDsToISSigAllowedKeys(AllowedKeysRuntimeIDs);
+  AddExWithISSigVerify(Url, ISSigUrl, BaseName, UserName, Password, ISSigAllowedKeys);
+end;
+
+function TDownloadWizardPage.AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName,
+  Password: String; const ISSigAllowedKeys: AnsiString): Integer;
+begin
+  { Also see Setup.ScriptFunc DownloadTemporaryFileWithISSigVerify }
   DoAdd(GetISSigUrl(Url, ISSigUrl), BaseName + ISSigExt, '', UserName, Password, False, '');
   Result := DoAdd(Url, BaseName, '', UserName, Password, True, ISSigAllowedKeys);
 end;

+ 122 - 24
Projects/Src/Setup.WizardForm.pas

@@ -188,6 +188,7 @@ type
     PrepareToInstallNeedsRestart: Boolean;
     EnableAnchorOuterPagesOnResize: Boolean;
     EnableAdjustReadyLabelHeightOnResize: Boolean;
+    FDownloadArchivesPage: TWizardPage; { TWizardPage to avoid circular reference. Is always a TDownloadWizardPage. }
     procedure AdjustFocus;
     procedure AnchorOuterPages;
     procedure CalcCurrentComponentsSpace;
@@ -342,11 +343,12 @@ function ValidateCustomDirEdit(const AEdit: TEdit;
 implementation
 
 uses
-  ShellApi, ShlObj, Types,
-  PathFunc, RestartManager,
+  ShellApi, ShlObj, Types, Generics.Collections,
+  PathFunc, RestartManager, SHA256,
   SetupLdrAndSetup.Messages, Setup.MainForm, Setup.MainFunc, Shared.CommonFunc.Vcl,
   Shared.CommonFunc, Setup.InstFunc, Setup.SelectFolderForm, Setup.FileExtractor,
-  Setup.LoggingFunc, Setup.ScriptRunner, Shared.SetupTypes, Shared.SetupSteps;
+  Setup.LoggingFunc, Setup.ScriptRunner, Shared.SetupTypes, Shared.SetupSteps,
+  Setup.ScriptDlg, SetupLdrAndSetup.InstFunc, Setup.Install;
 
 {$R *.DFM}
 
@@ -1325,6 +1327,7 @@ begin
   FreeAndNil(PrevSelectedComponents);
   FreeAndNil(InitialSelectedComponents);
   FreeAndNil(FPageList);
+  FreeAndNil(FDownloadArchivesPage);
   inherited;
 end;
 
@@ -1819,6 +1822,91 @@ begin
 end;
 
 function TWizardForm.PrepareToInstall(const WizardComponents, WizardTasks: TStringList): String;
+
+  function GetClearedDownloadArchivesPage: TDownloadWizardPage;
+  begin
+    if FDownloadArchivesPage = nil then begin
+      Result := TDownloadWizardPage.Create(Self);
+      try
+        Result.Caption := SetupMessages[msgWizardPreparing];
+        Result.Description := SetupMessages[msgPreparingDesc];
+        Result.ShowBaseNameInsteadOfUrl := True;
+        AddPage(Result, -1);
+        Result.Initialize;
+        FDownloadArchivesPage := Result;
+      except
+        FreeAndNil(Result);
+        raise;
+      end;
+    end else begin
+      Result := FDownloadArchivesPage as TDownloadWizardPage;
+      Result.Clear;
+    end;
+  end;
+
+  procedure DownloadArchivesToExtract;
+  begin
+    var DownloadPage: TDownloadWizardPage := nil;
+
+    const ArchivesToDownload = TDictionary<Integer, String>.Create;
+    try
+      for var I := 0 to Entries[seFile].Count-1 do begin
+        with PSetupFileEntry(Entries[seFile][I])^ do begin
+          if (foDownload in Options) and (foExtractArchive in Options) then begin
+            if DownloadPage = nil then
+              DownloadPage := GetClearedDownloadArchivesPage;
+            if not(foCustomDestName in Options) then
+              InternalError('Expected CustomDestName flag');
+            { Prepare }
+            const TempDir = AddBackslash(TempInstallDir);
+            const DestDir = GenerateUniqueName(False, TempDir + '_isetup', '.tmp');
+            const DestFile = AddBackslash(DestDir) + PathExtractName(DestName);
+            const BaseName = Copy(DestFile, Length(TempDir)+1, MaxInt);
+             { Add to ArchivesToDownload }
+            ArchivesToDownload.Add(I, DestFile);
+            { Add to DownloadPage }
+            const SourceFile = ExpandConst(SourceFilename);
+            const UserName = ExpandConst(DownloadUserName);
+            const Password = ExpandConst(DownloadPassword);
+            if Verification.Typ = fvISSig then begin
+              const ISSigUrl = GetISSigUrl(SourceFile, ExpandConst(DownloadISSigSource));
+              DownloadPage.AddExWithISSigVerify(SourceFile, ISSigUrl, BaseName, UserName, Password, Verification.ISSigAllowedKeys)
+            end else begin
+              var RequiredSHA256OfFile: String;
+              if Verification.Typ = fvHash then
+                RequiredSHA256OfFile := SHA256DigestToString(Verification.Hash)
+              else
+                RequiredSHA256OfFile := '';
+              DownloadPage.AddEx(SourceFile, BaseName, RequiredSHA256OfFile, UserName, Password);
+            end;
+          end;
+        end;
+      end;
+
+      if DownloadPage <> nil then begin
+        DownloadPage.Show;
+        try
+          DownloadPage.Download;
+          for var A in ArchivesToDownload do begin
+            with PSetupFileEntry(Entries[seFile][A.Key])^ do begin
+              SourceFilename := A.Value;
+              { Remove Download flag since download has been done, and remove CustomDestName flag
+                since ExtractArchive flag doesn't like that }
+              Options := Options - [foDownload, foCustomDestName];
+              { DestName should now not include a filename, see TSetupCompiler.EnumFilesProc.ProcessFileList }
+              DestName := PathExtractPath(DestName);
+              Verification.Typ := fvNone;
+            end;
+          end;
+        finally
+          DownloadPage.Hide;
+        end;
+      end;
+    finally
+      ArchivesToDownload.Free;
+    end;
+  end;
+
 var
   CodeNeedsRestart: Boolean;
   Y: Integer;
@@ -1830,29 +1918,39 @@ begin
   PreparingYesRadio.Visible := False;
   PreparingNoRadio.Visible := False;
   PreparingMemo.Visible := False;
-  if not PreviousInstallCompleted(WizardComponents, WizardTasks) then begin
-    Result := ExpandSetupMessage(msgPreviousInstallNotCompleted);
-    PrepareToInstallNeedsRestart := True;
-  end else if (CodeRunner <> nil) and CodeRunner.FunctionExists('PrepareToInstall', True) then begin
-    SetCurPage(wpPreparing);
-    BackButton.Visible := False;
-    NextButton.Visible := False;
-    CancelButton.Enabled := False;
-    if InstallMode = imSilent then
-      WizardForm.Visible := True;
-    WizardForm.Update;
-    try
-      DownloadTemporaryFileOrExtractArchiveProcessMessages := True;
-      CodeNeedsRestart := False;
-      Result := CodeRunner.RunStringFunctions('PrepareToInstall', [@CodeNeedsRestart], bcNonEmpty, True, '');
-      PrepareToInstallNeedsRestart := (Result <> '') and CodeNeedsRestart;
-    finally
-      DownloadTemporaryFileOrExtractArchiveProcessMessages := False;
-      UpdateCurPageButtonState;
+
+  try
+    DownloadArchivesToExtract;
+  except
+    Result := GetExceptMessage;
+  end;
+
+  if Result = '' then begin
+    if not PreviousInstallCompleted(WizardComponents, WizardTasks) then begin
+      Result := ExpandSetupMessage(msgPreviousInstallNotCompleted);
+      PrepareToInstallNeedsRestart := True;
+    end else if (CodeRunner <> nil) and CodeRunner.FunctionExists('PrepareToInstall', True) then begin
+      SetCurPage(wpPreparing);
+      BackButton.Visible := False;
+      NextButton.Visible := False;
+      CancelButton.Enabled := False;
+      if InstallMode = imSilent then
+        WizardForm.Visible := True;
+      WizardForm.Update;
+      try
+        DownloadTemporaryFileOrExtractArchiveProcessMessages := True;
+        CodeNeedsRestart := False;
+        Result := CodeRunner.RunStringFunctions('PrepareToInstall', [@CodeNeedsRestart], bcNonEmpty, True, '');
+        PrepareToInstallNeedsRestart := (Result <> '') and CodeNeedsRestart;
+      finally
+        DownloadTemporaryFileOrExtractArchiveProcessMessages := False;
+        UpdateCurPageButtonState;
+      end;
+      if WindowState <> wsMinimized then  { VCL bug workaround }
+        Application.BringToFront;
     end;
-    if WindowState <> wsMinimized then  { VCL bug workaround }
-      Application.BringToFront;
   end;
+
   if Result <> '' then begin
     if PrepareToInstallNeedsRestart then
       PreparingLabel.Caption := Result +