Browse Source

Add [Files] flag "download" for integrated download support. Was rather easy 👍

Todo:
-Handle username + password
-Add param for .issig url, like IssigSource? Now it requires a second entry
-Doc
-Allow download+extractarchive? I suppose the download part would need to be integrated differently since it first would need to download (all?) the archive(s) to {tmp} using CreateDownloadPage. Hooking up the download steam to the 7-Zip instream wouldn't work since it needs non-sequential access.
Martijn Laan 3 months ago
parent
commit
3c67d36bff

+ 2 - 0
Files/Default.isl

@@ -285,6 +285,7 @@ AbortRetryIgnoreCancel=Cancel installation
 StatusClosingApplications=Closing applications...
 StatusCreateDirs=Creating directories...
 StatusExtractFiles=Extracting files...
+StatusDownloadFiles=Downloading files...
 StatusCreateIcons=Creating shortcuts...
 StatusCreateIniEntries=Creating INI entries...
 StatusCreateRegistryEntries=Creating registry entries...
@@ -338,6 +339,7 @@ ErrorChangingAttr=An error occurred while trying to change the attributes of the
 ErrorCreatingTemp=An error occurred while trying to create a file in the destination directory:
 ErrorReadingSource=An error occurred while trying to read the source file:
 ErrorCopying=An error occurred while trying to copy a file:
+ErrorDownloading=An error occurred while trying to download a file:
 ErrorExtracting=An error occurred while trying to extract an archive:
 ErrorReplacingExistingFile=An error occurred while trying to replace the existing file:
 ErrorRestartReplace=RestartReplace failed:

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

@@ -223,6 +223,7 @@ const
   SCompilerParamFlagMissing = 'Flag "%s" must be used if flag "%s" is used';
   SCompilerParamFlagMissing2 = 'Flag "%s" must be used if parameter "%s" is used';
   SCompilerParamFlagMissing3 = 'Flag "%s" must be used if flags "%s" and "%s" are both used';
+  SCompilerParamFlagMissingParam = 'Parameter "%s" must be specified if flag "%s" is used';
 
   { Types, components, tasks, check, beforeinstall, afterinstall }
   SCompilerParamUnknownType = 'Parameter "%s" includes an unknown type';
@@ -290,8 +291,6 @@ const
   SCompilerFilesWildcardNotMatched = 'No files found matching "%s"';
   SCompilerFilesDestNameCantBeSpecified = 'Parameter "DestName" cannot be specified if ' +
     'the "Source" parameter contains wildcards or flag "extractarchive" is used';
-  SCompilerFilesStrongAssemblyNameMustBeSpecified = 'Parameter "StrongAssemblyName" must be specified if ' +
-    'the flag "gacinstall" is used';
   SCompilerFilesCantHaveNonExternalExternalSize = 'Parameter "ExternalSize" may only be used when ' +
     'the "external" flag is used';
   SCompilerFilesExcludeTooLong = 'Parameter "Excludes" contains a pattern that is too long';

+ 35 - 6
Projects/Src/Compiler.SetupCompiler.pas

@@ -4669,7 +4669,8 @@ type
   TParam = (paFlags, paSource, paDestDir, paDestName, paCopyMode, paAttribs,
     paPermissions, paFontInstall, paExcludes, paExternalSize, paExtractArchivePassword,
     paStrongAssemblyName, paISSigAllowedKeys, paComponents, paTasks, paLanguages,
-    paCheck, paBeforeInstall, paAfterInstall, paMinVersion, paOnlyBelowVersion);
+    paCheck, paBeforeInstall, paAfterInstall, paMinVersion, paOnlyBelowVersion,
+    paDownloadUserName, paDownloadPassword);
 const
   ParamFilesSource = 'Source';
   ParamFilesDestDir = 'DestDir';
@@ -4683,6 +4684,8 @@ const
   ParamFilesExtractArchivePassword = 'ExtractArchivePassword';
   ParamFilesStrongAssemblyName = 'StrongAssemblyName';
   ParamFilesISSigAllowedKeys = 'ISSigAllowedKeys';
+  ParamFilesDownloadUserName = 'DownloadUserName';
+  ParamFilesDownloadPassword = 'DownloadPassword';
   ParamInfo: array[TParam] of TParamInfo = (
     (Name: ParamCommonFlags; Flags: []),
     (Name: ParamFilesSource; Flags: [piRequired, piNoEmpty, piNoQuotes]),
@@ -4697,6 +4700,8 @@ const
     (Name: ParamFilesExtractArchivePassword; Flags: []),
     (Name: ParamFilesStrongAssemblyName; Flags: [piNoEmpty]),
     (Name: ParamFilesISSigAllowedKeys; Flags: [piNoEmpty]),
+    (Name: ParamFilesDownloadUserName; Flags: [piNoEmpty]),
+    (Name: ParamFilesDownloadPassword; Flags: [piNoEmpty]),
     (Name: ParamCommonComponents; Flags: []),
     (Name: ParamCommonTasks; Flags: []),
     (Name: ParamCommonLanguages; Flags: []),
@@ -4705,7 +4710,7 @@ const
     (Name: ParamCommonAfterInstall; Flags: []),
     (Name: ParamCommonMinVersion; Flags: []),
     (Name: ParamCommonOnlyBelowVersion; Flags: []));
-  Flags: array[0..42] of PChar = (
+  Flags: array[0..43] of PChar = (
     'confirmoverwrite', 'uninsneveruninstall', 'isreadme', 'regserver',
     'sharedfile', 'restartreplace', 'deleteafterinstall',
     'comparetimestamp', 'fontisnttruetype', 'regtypelib', 'external',
@@ -4717,7 +4722,7 @@ const
     'uninsnosharedfileprompt', 'createallsubdirs', '32bit', '64bit',
     'solidbreak', 'setntfscompression', 'unsetntfscompression',
     'sortfilesbyname', 'gacinstall', 'sign', 'signonce', 'signcheck',
-    'issigverify', 'extractarchive');
+    'issigverify', 'download', 'extractarchive');
   SignFlags: array[TFileLocationSign] of String = (
     '', 'sign', 'signonce', 'signcheck');
   AttribsFlags: array[0..3] of PChar = (
@@ -5258,7 +5263,8 @@ begin
                    39: ApplyNewSign(Sign, fsOnce, SCompilerParamErrorBadCombo2);
                    40: ApplyNewSign(Sign, fsCheck, SCompilerParamErrorBadCombo2);
                    41: Include(Options, foISSigVerify);
-                   42: Include(Options, foExtractArchive);
+                   42: Include(Options, foDownload);
+                   43: Include(Options, foExtractArchive);
                  end;
 
                { Source }
@@ -5344,6 +5350,12 @@ begin
                  Include(Options, foExternalSizePreset);
                end;
 
+               { DownloadUserName }
+               DownloadUserName := Values[paDownloadUserName].Data;
+
+               { DownloadPassword }
+               DownloadPassword := Values[paDownloadPassword].Data;
+
                { ExtractArchivePassword }
                ExtractArchivePassword := Values[paExtractArchivePassword].Data;
 
@@ -5417,7 +5429,7 @@ begin
         end;
 
         if (foGacInstall in Options) and (AStrongAssemblyName = '') then
-          AbortCompile(SCompilerFilesStrongAssemblyNameMustBeSpecified);
+          AbortCompileFmt(SCompilerParamFlagMissingParam, ['StrongAssemblyName', 'gacinstall']);
         if AStrongAssemblyName <> '' then
           StrongAssemblyName := AStrongAssemblyName;
 
@@ -5431,6 +5443,23 @@ begin
           Excludes := AExcludes.DelimitedText;
         end;
 
+        if foDownload in Options then begin
+          if not ExternalFile then
+            AbortCompileFmt(SCompilerParamFlagMissing, ['external', 'download'])
+          else if not(foIgnoreVersion in Options) then
+            AbortCompileFmt(SCompilerParamFlagMissing, ['ignoreversion', 'download'])
+          else if foExtractArchive in Options then
+            AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'extractarchive'])
+          else if foCompareTimeStamp in Options then
+            AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'comparetimestamp'])
+          else if RecurseSubdirs then
+            AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'recursesubdirs', 'download'])
+          else if ADestName = '' then
+            AbortCompileFmt(SCompilerParamFlagMissingParam, ['DestName', 'download'])
+          else if not(foExternalSizePreset in Options) then
+            AbortCompileFmt(SCompilerParamFlagMissingParam, ['ExternalSize', 'download']);
+        end;
+
         if foExtractArchive in Options then begin
           if not ExternalFile then
             AbortCompileFmt(SCompilerParamFlagMissing, ['external', 'extractarchive'])
@@ -5468,7 +5497,7 @@ begin
            (Copy(ADestDir, 1, Length('{syswow64}')) = '{syswow64}') then
           WarningsList.Add(SCompilerFilesWarningSharedFileSysWow64);
 
-        SourceIsWildcard := IsWildcard(SourceWildcard);
+        SourceIsWildcard := not(foDownload in Options) and IsWildcard(SourceWildcard);
         if ExternalFile then begin
           if RecurseSubdirs then
             Include(Options, foRecurseSubDirsExternal);

+ 1 - 1
Projects/Src/Compression.SevenZipDLLDecoder.pas

@@ -837,6 +837,7 @@ begin
       System.TMonitor.Exit(FLock);
     end;
 
+    { Also see Setup.Install THTTPDataReceiver.OnReceiveData }
     var Bytes := Progress - FPreviousProgress;
     while Bytes > 0 do begin
       var BytesToReport: Cardinal;
@@ -847,7 +848,6 @@ begin
       FOnExtractToHandleProgress(BytesToReport);
       Dec(Bytes, BytesToReport);
     end;
-
     FPreviousProgress := Progress;
   end;
 end;

+ 5 - 4
Projects/Src/IDE.ScintStylerInnoSetup.pas

@@ -242,14 +242,15 @@ const
 
   FilesSectionParameters: array of TScintRawString = [
     'AfterInstall', 'Attribs', 'BeforeInstall', 'Check', 'Components', 'CopyMode',
-    'DestDir', 'DestName', 'Excludes', 'ExternalSize', 'ExtractArchivePassword',
-    'Flags', 'FontInstall', 'ISSigAllowedKeys', 'Languages', 'MinVersion',
-    'OnlyBelowVersion', 'Permissions', 'Source', 'StrongAssemblyName', 'Tasks'
+    'DestDir', 'DestName', 'DownloadPassword', 'DownloadUserName', 'Excludes',
+    'ExternalSize', 'ExtractArchivePassword', 'Flags', 'FontInstall',
+    'ISSigAllowedKeys', 'Languages', 'MinVersion', 'OnlyBelowVersion',
+    'Permissions', 'Source', 'StrongAssemblyName', 'Tasks'
   ];
 
   FilesSectionFlags: array of TScintRawString = [
     '32bit', '64bit', 'allowunsafefiles', 'comparetimestamp', 'confirmoverwrite',
-    'createallsubdirs', 'deleteafterinstall', 'dontcopy', 'dontverifychecksum',
+    'createallsubdirs', 'deleteafterinstall', 'dontcopy', 'dontverifychecksum', 'download',
     'external', 'extractarchive', 'fontisnttruetype', 'gacinstall', 'ignoreversion',
     'isreadme', 'issigverify', 'nocompression', 'noencryption', 'noregerror',
     'onlyifdestfileexists', 'onlyifdoesntexist', 'overwritereadonly', 'promptifolder',

+ 140 - 11
Projects/Src/Setup.Install.pas

@@ -26,9 +26,13 @@ procedure PerformInstall(var Succeeded: Boolean; const ChangesEnvironment,
 
 type
   TOnDownloadProgress = function(const Url, BaseName: string; const Progress, ProgressMax: Int64): Boolean of object;
+  TOnSimpleDownloadProgress = procedure(Bytes: Cardinal);
 
 procedure ExtractTemporaryFile(const BaseName: String);
 function ExtractTemporaryFiles(const Pattern: String): Integer;
+function DownloadFile(const Url: String; const DestF: TFile;
+  const ISSigVerify: Boolean; const ISSigAllowedKeys: AnsiString;
+  const OnSimpleDownloadProgress: TOnSimpleDownloadProgress): Int64;
 function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String;
   const ISSigVerify: Boolean; const ISSigAllowedKeys: AnsiString;
   const OnDownloadProgress: TOnDownloadProgress): Int64;
@@ -73,11 +77,15 @@ begin
     WizardForm.FilenameLabel.Update;
 end;
 
-procedure SetStatusLabelText(const S: String);
+procedure SetStatusLabelText(const S: String; const CallUpdate: Boolean = True;
+  const ClearFilenameLabelText: Boolean = True);
 begin
-  WizardForm.StatusLabel.Caption := S;
-  WizardForm.StatusLabel.Update;
-  SetFilenameLabelText('', True);
+  if WizardForm.StatusLabel.Caption <> S then begin
+    WizardForm.StatusLabel.Caption := S;
+    WizardForm.StatusLabel.Update;
+  end;
+  if ClearFilenameLabelText then
+    SetFilenameLabelText('', True);
 end;
 
 procedure InstallMessageBoxCallback(const Flags: LongInt; const After: Boolean;
@@ -1000,7 +1008,9 @@ var
                   AExternalFileDate should not be set
     External    : Opposite except AExternalFileDate still not set
     Ext. Archive: Same as external except AExternalFileDate set and
-                  AExternalSourceFile should be set to ArchiveFindHandle as a string }
+                  AExternalSourceFile should be set to ArchiveFindHandle as a string
+    Ext. Downl. : Same as external except
+                  AExternalSourceFile should be set to an URL }
 
     procedure InstallFont(const Filename, FontName: String;
       const PerUserFont, AddToFontTableNow: Boolean; var WarnedPerUserFonts: Boolean);
@@ -1243,6 +1253,10 @@ var
         end;
 
         { Update the filename label }
+        if foDownload in CurFile^.Options then
+          SetStatusLabelText(SetupMessages[msgStatusDownloadFiles], False)
+        else
+          SetStatusLabelText(SetupMessages[msgStatusExtractFiles], False);
         SetFilenameLabelText(DestFile, True);
         LogFmt('Dest filename: %s', [DestFile]);
         if DisableFsRedir <> InstallDefaultDisableFsRedir then begin
@@ -1267,6 +1281,7 @@ var
         if DestFileExistedBefore then
           DeleteFlags := DeleteFlags or utDeleteFile_ExistedBeforeInstall;
 
+        var CurFileDateDidRead := True; { Set to False later if needed }
         if Assigned(CurFileLocation) then begin
           if floTimeStampInUTC in CurFileLocation^.Flags then
             CurFileDate := CurFileLocation^.SourceTimeStamp
@@ -1276,11 +1291,15 @@ var
         end else if Assigned(AExternalFileDate) then begin
           CurFileDate := AExternalFileDate^;
           CurFileDateValid := CurFileDate.HasTime;
-        end else
-          CurFileDateValid := GetFileDateTime(DisableFsRedir, AExternalSourceFile, CurFileDate);
+        end else if not(foDownload in CurFile^.Options) then
+          CurFileDateValid := GetFileDateTime(DisableFsRedir, AExternalSourceFile, CurFileDate)
+        else begin
+          CurFileDateValid := False;
+          CurFileDateDidRead := False;
+        end;
         if CurFileDateValid then
           LogFmt('Time stamp of our file: %s', [FileTimeToStr(CurFileDate)])
-        else
+        else if CurFileDateDidRead then
           Log('Time stamp of our file: (failed to read)');
 
         if DestFileExists then begin
@@ -1302,8 +1321,10 @@ var
           if not(foIgnoreVersion in CurFile^.Options) then begin
             AllowTimeStampComparison := False;
             { Read version info of file being installed }
-            if foExtractArchive in CurFile^.Options then
-              InternalError('Unexpected extractarchive flag');
+            if foDownload in CurFile^.Options then
+              InternalError('Unexpected Download flag')
+            else if foExtractArchive in CurFile^.Options then
+              InternalError('Unexpected ExtractArchive flag');
             if Assigned(CurFileLocation) then begin
               CurFileVersionInfoValid := floVersionInfoValid in CurFileLocation^.Flags;
               CurFileVersionInfo.MS := CurFileLocation^.FileVersionMS;
@@ -1404,6 +1425,8 @@ var
           { Fall back to comparing time stamps if needed }
           if AllowTimeStampComparison and
              (foCompareTimeStamp in CurFile^.Options) then begin
+            if foDownload in CurFile^.Options then
+              InternalError('Unexpected Download flag');
             if not CurFileDateValid or not ExistingFileDateValid then begin
               { If we failed to read one of the time stamps, do the safe thing
                 and just skip the file }
@@ -1542,6 +1565,11 @@ var
               LastOperation := SetupMessages[msgErrorExtracting];
               ArchiveFindExtract(StrToInt(SourceFile), DestF, ExtractorProgressProc);
             end
+            else if foDownload in CurFile^.Options then begin
+              { Download a file }
+              LastOperation := SetupMessages[msgErrorDownloading];
+              DownloadFile(SourceFile, DestF, foISSigVerify in CurFile^.Options, CurFile^.ISSigAllowedKeys, ExtractorProgressProc);
+            end
             else begin
               { Copy a duplicated non-external file, or an external file }
               SourceF := TFileRedir.Create(DisableFsRedir, SourceFile, fdOpenExisting, faRead, fsRead);
@@ -2086,7 +2114,15 @@ var
             repeat
               SetProgress(ProgressBefore);
               ExpectedBytesLeft := CurFile^.ExternalSize;
-              if foExtractArchive in CurFile^.Options then
+              if foDownload in CurFile^.Options then begin
+                if not(foCustomDestName in CurFile^.Options) then
+                  InternalError('Expected CustomDestName flag');
+                { CurFile^.DestName now includes a a filename, see TSetupCompiler.EnumFilesProc.ProcessFileList }
+                ProcessFileEntry(CurFile, DisableFsRedir, SourceWildcard, ExpandConst(CurFile^.DestName),
+                  nil, ExpectedBytesLeft, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
+                  WarnedPerUserFonts, nil);
+                FoundFiles := True;
+              end else if foExtractArchive in CurFile^.Options then
                 FoundFiles := RecurseExternalArchiveCopyFiles(DisableFsRedir,
                   SourceWildcard, CurFile^.ExtractArchivePassword, Excludes, CurFile,
                   ExpectedBytesLeft, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
@@ -3598,6 +3634,7 @@ type
   private
     FBaseName, FUrl: String;
     FOnDownloadProgress: TOnDownloadProgress;
+    FOnSimpleDownloadProgress: TOnSimpleDownloadProgress;
     FAborted: Boolean;
     FProgress, FProgressMax: Int64;
     FLastReportedProgress, FLastReportedProgressMax: Int64;
@@ -3605,6 +3642,7 @@ type
     property BaseName: String write FBaseName;
     property Url: String write FUrl;
     property OnDownloadProgress: TOnDownloadProgress write FOnDownloadProgress;
+    property OnSimpleDownloadProgress: TOnSimpleDownloadProgress write FOnSimpleDownloadProgress;
     property Aborted: Boolean read FAborted;
     property Progress: Int64 read FProgress;
     property ProgressMax: Int64 read FProgressMax;
@@ -3633,6 +3671,19 @@ begin
         FLastReportedProgressMax := FProgressMax;
       end;
     end;
+  end else if Assigned(FOnSimpleDownloadProgress) then begin
+    { Also see Compression.SevenZipDLLDecoder TArchiveExtractToHandleCallback.HandleProgress }
+    var Bytes := Progress - FLastReportedProgress;
+    while Bytes > 0 do begin
+      var BytesToReport: Cardinal;
+      if Bytes > High(BytesToReport) then
+        BytesToReport := High(BytesToReport)
+      else
+        BytesToReport := Bytes;
+      FOnSimpleDownloadProgress(BytesToReport);
+      Dec(Bytes, BytesToReport);
+    end;
+    FLastReportedProgress := Progress;
   end;
 
   if not Abort and DownloadTemporaryFileOrExtractArchiveProcessMessages then
@@ -3690,6 +3741,84 @@ begin
     Log('Download is not using basic authentication');
 end;
 
+function DownloadFile(const Url: String; const DestF: TFile;
+  const ISSigVerify: Boolean; const ISSigAllowedKeys: AnsiString;
+  const OnSimpleDownloadProgress: TOnSimpleDownloadProgress): Int64;
+var
+  DestFile: String;
+  HandleStream: THandleStream;
+  HTTPDataReceiver: THTTPDataReceiver;
+  HTTPClient: THTTPClient;
+  HTTPResponse: IHTTPResponse;
+  User, Pass, CleanUrl: String;
+  HasCredentials : Boolean;
+begin
+  if Url = '' then
+    InternalError('DownloadFile: Invalid Url value');
+
+  LogFmt('Downloading file from %s', [MaskPasswordInURL(Url)]);
+
+  HTTPDataReceiver := nil;
+  HTTPClient := nil;
+  HandleStream := nil;
+
+  try
+    HasCredentials := GetCredentialsAndCleanUrl(URL, User, Pass, CleanUrl);
+
+    { Setup downloader }
+    HTTPDataReceiver := THTTPDataReceiver.Create;
+    HTTPDataReceiver.Url := CleanUrl;
+    HTTPDataReceiver.OnSimpleDownloadProgress := OnSimpleDownloadProgress;
+
+    HTTPClient := THTTPClient.Create; { http://docwiki.embarcadero.com/RADStudio/Rio/en/Using_an_HTTP_Client }
+    SetUserAgentAndSecureProtocols(HTTPClient);
+    HTTPClient.OnReceiveData := HTTPDataReceiver.OnReceiveData;
+
+    { Download to specified handle }
+    HandleStream := THandleStream.Create(DestF.Handle);
+    if HasCredentials then begin
+      const Base64 = TBase64Encoding.Create(0);
+      try
+        HTTPClient.CustomHeaders['Authorization'] := 'Basic ' + Base64.Encode(User + ':' + Pass);
+      finally
+        Base64.Free;
+      end;
+    end;
+    HTTPResponse := HTTPClient.Get(CleanUrl, HandleStream);
+    Result := 0; { silence compiler }
+    if HTTPDataReceiver.Aborted then
+      Abort
+    else if (HTTPResponse.StatusCode < 200) or (HTTPResponse.StatusCode > 299) then
+      raise Exception.Create(Format('%d %s', [HTTPResponse.StatusCode, HTTPResponse.StatusText]))
+    else begin
+      { Download completed, get size and close it }
+      Result := HandleStream.Size;
+      FreeAndNil(HandleStream);
+
+      { Check .issig if specified, otherwise check everything else we can check }
+      if ISSigVerify then begin
+        var ExpectedFileHash: TSHA256Digest;
+        DoISSigVerify(DestF, nil, DestFile, ISSigAllowedKeys, ExpectedFileHash);
+        const FileHash = GetSHA256OfFile(DestF);
+        if not SHA256DigestsEqual(FileHash, ExpectedFileHash) then
+          ISSigVerifyError(vseFileHashIncorrect, SetupMessages[msgSourceIsCorrupted]);
+        Log(ISSigVerificationSuccessfulLogMessage);
+      end else begin
+        if HTTPDataReceiver.ProgressMax > 0 then begin
+          if HTTPDataReceiver.Progress <> HTTPDataReceiver.ProgressMax then
+            raise Exception.Create(FmtSetupMessage(msgErrorProgress, [IntToStr(HTTPDataReceiver.Progress), IntToStr(HTTPDataReceiver.ProgressMax)]))
+          else if HTTPDataReceiver.ProgressMax <> Result then
+            raise Exception.Create(FmtSetupMessage(msgErrorFileSize, [IntToStr(HTTPDataReceiver.ProgressMax), IntToStr(Result)]));
+        end;
+      end;
+    end;
+  finally
+    HandleStream.Free;
+    HTTPClient.Free;
+    HTTPDataReceiver.Free;
+  end;
+end;
+
 function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String;
   const ISSigVerify: Boolean; const ISSigAllowedKeys: AnsiString;
   const OnDownloadProgress: TOnDownloadProgress): Int64;

+ 24 - 14
Projects/Src/Setup.MainFunc.pas

@@ -1811,7 +1811,7 @@ function EnumFiles(const EnumFilesProc: TEnumFilesProc;
     Result := True;
 
     if foCustomDestName in CurFile^.Options then
-      InternalError('Unexpected custom DestName');
+      InternalError('Unexpected CustomDestName flag');
     const DestDir = ExpandConst(CurFile^.DestName);
 
     var FindData: TWin32FindData;
@@ -1864,21 +1864,29 @@ begin
         end
         else begin
           { External file }
-          SourceWildcard := ExpandConst(CurFile^.SourceFilename);
-          Excludes.DelimitedText := CurFile^.Excludes;
-          if foExtractArchive in CurFile^.Options then begin
-            try
-              if not RecurseExternalArchiveFiles(DisableFsRedir, SourceWildcard,
-                 CurFile^.ExtractArchivePassword, Excludes, CurFile) then
+          if foDownload in CurFile^.Options then begin
+            if not(foCustomDestName in CurFile^.Options) then
+              InternalError('Expected CustomDestName flag');
+            { CurFile^.DestName now includes a a filename, see TSetupCompiler.EnumFilesProc.ProcessFileList }
+            if not EnumFilesProc(DisableFsRedir, ExpandConst(CurFile^.DestName), Param) then
+              Exit(False);
+          end else begin
+            SourceWildcard := ExpandConst(CurFile^.SourceFilename);
+            Excludes.DelimitedText := CurFile^.Excludes;
+            if foExtractArchive in CurFile^.Options then begin
+              try
+                if not RecurseExternalArchiveFiles(DisableFsRedir, SourceWildcard,
+                   CurFile^.ExtractArchivePassword, Excludes, CurFile) then
+                  Exit(False);
+              except on E: ESevenZipError do
+                { Ignore archive errors for now, will show up with proper UI during
+                  installation }
+              end;
+            end else begin
+              if not RecurseExternalFiles(DisableFsRedir, PathExtractPath(SourceWildcard), '',
+                 PathExtractName(SourceWildcard), IsWildcard(SourceWildcard), Excludes, CurFile) then
                 Exit(False);
-            except on E: ESevenZipError do
-              { Ignore archive errors for now, will show up with proper UI during
-                installation }
             end;
-          end else begin
-            if not RecurseExternalFiles(DisableFsRedir, PathExtractPath(SourceWildcard), '',
-               PathExtractName(SourceWildcard), IsWildcard(SourceWildcard), Excludes, CurFile) then
-              Exit(False);
           end;
         end;
       end;
@@ -3516,6 +3524,8 @@ begin
               Inc6464(MinimumSpace, PSetupFileLocationEntry(Entries[seFileLocation][LocationEntry])^.OriginalSize)
         end else begin
           if not(foExternalSizePreset in Options) then begin
+            if foDownload in Options then
+              InternalError('Unexpected download flag');
             try
               LExcludes.DelimitedText := Excludes;
               if foExtractArchive in Options then begin

+ 2 - 0
Projects/Src/Shared.SetupMessageIDs.pas

@@ -87,6 +87,7 @@ type
     msgErrorCreatingTemp,
     msgErrorDownloadAborted,
     msgErrorDownloadFailed,
+    msgErrorDownloading,
     msgErrorDownloadSizeFailed,
     msgErrorExecutingProgram,
     msgErrorExtracting,
@@ -235,6 +236,7 @@ type
     msgStatusCreateIcons,
     msgStatusCreateIniEntries,
     msgStatusCreateRegistryEntries,
+    msgStatusDownloadFiles,
     msgStatusExtractFiles,
     msgStatusRegisterFiles,
     msgStatusRestartingApplications,

+ 4 - 3
Projects/Src/Shared.Struct.pas

@@ -227,14 +227,14 @@ type
     PublicX, PublicY, RuntimeID: String;
   end;
 const
-  SetupFileEntryStrings = 12;
+  SetupFileEntryStrings = 14;
   SetupFileEntryAnsiStrings = 1;
 type
   PSetupFileEntry = ^TSetupFileEntry;
   TSetupFileEntry = packed record
     SourceFilename, DestName, InstallFontName, StrongAssemblyName, Components,
     Tasks, Languages, Check, AfterInstall, BeforeInstall, Excludes,
-    ExtractArchivePassword: String;
+    DownloadUserName, DownloadPassword, ExtractArchivePassword: String;
     ISSigAllowedKeys: AnsiString;
     MinVersion, OnlyBelowVersion: TSetupVersionData;
     LocationEntry: Integer;
@@ -251,7 +251,8 @@ type
       foRecurseSubDirsExternal, foReplaceSameVersionIfContentsDiffer,
       foDontVerifyChecksum, foUninsNoSharedFilePrompt, foCreateAllSubDirs,
       fo32Bit, fo64Bit, foExternalSizePreset, foSetNTFSCompression,
-      foUnsetNTFSCompression, foGacInstall, foISSigVerify, foExtractArchive);
+      foUnsetNTFSCompression, foGacInstall, foISSigVerify, foDownload,
+      foExtractArchive);
     FileType: (ftUserFile, ftUninstExe);
   end;
 const