diff --git a/ISHelp/isxclasses.pas b/ISHelp/isxclasses.pas index d7a51d35a..73ed12ddc 100644 --- a/ISHelp/isxclasses.pas +++ b/ISHelp/isxclasses.pas @@ -780,6 +780,15 @@ TDownloadWizardPage = class(TOutputProgressWizardPage) property ShowBaseNameInsteadOfUrl: Boolean; read write; end; +TExtractionWizardPage = class(TOutputProgressWizardPage) + property AbortButton: TNewButton; read; + property AbortedByUser: Boolean; read; + procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean); + procedure Clear; + function Extract: Integer; + property ShowArchiveInsteadOfFile: Boolean; read write; +end; + TUIStateForm = class(TForm) end; diff --git a/ISHelp/isxclasses_wordlists_generated.pas b/ISHelp/isxclasses_wordlists_generated.pas index 145ce99c5..2c2297452 100644 --- a/ISHelp/isxclasses_wordlists_generated.pas +++ b/ISHelp/isxclasses_wordlists_generated.pas @@ -23,21 +23,22 @@ interface 'TComponent', 'TConstraintSize', 'TControl', 'TCursor', 'TCustomCheckBox', 'TCustomComboBox', 'TCustomControl', 'TCustomEdit', 'TCustomFolderTreeView', 'TCustomLabel', 'TCustomLinkLabel', 'TCustomListBox', 'TCustomMemo', 'TCustomPanel', 'TDownloadWizardPage', 'TDuplicates', - 'TEdit', 'TEditCharCase', 'TEShiftState', 'TFileStream', 'TFolderRenameEvent', 'TFolderTreeView', - 'TFont', 'TFontStyle', 'TFontStyles', 'TForm', 'TFormBorderStyle', 'TFormStyle', 'TGraphic', - 'TGraphicControl', 'TGraphicsObject', 'THandleStream', 'TInputDirWizardPage', 'TInputFileWizardPage', - 'TInputOptionWizardPage', 'TInputQueryWizardPage', 'TKeyEvent', 'TKeyPressEvent', 'TLabel', - 'TLinkLabel', 'TListBox', 'TListBoxStyle', 'TMainForm', 'TMemo', 'TNewButton', 'TNewCheckBox', - 'TNewCheckListBox', 'TNewComboBox', 'TNewEdit', 'TNewLinkLabel', 'TNewListBox', 'TNewMemo', - 'TNewNotebook', 'TNewNotebookPage', 'TNewProgressBar', 'TNewProgressBarState', 'TNewProgressBarStyle', - 'TNewRadioButton', 'TNewStaticText', 'TNotifyEvent', 'TObject', 'TOutputMarqueeProgressWizardPage', - 'TOutputMsgMemoWizardPage', 'TOutputMsgWizardPage', 'TOutputProgressWizardPage', 'TPanel', - 'TPanelBevel', 'TPasswordEdit', 'TPen', 'TPenMode', 'TPenStyle', 'TPersistent', 'TPosition', - 'TRadioButton', 'TRichEditViewer', 'TScrollingWinControl', 'TScrollStyle', 'TSetupForm', - 'TShiftState', 'TSizeConstraints', 'TStartMenuFolderTreeView', 'TStream', 'TStringList', - 'TStrings', 'TStringStream', 'TSysLinkEvent', 'TSysLinkType', 'TUIStateForm', 'TUninstallProgressForm', - 'TWinControl', 'TWizardForm', 'TWizardPage', 'TWizardPageButtonEvent', 'TWizardPageCancelEvent', - 'TWizardPageNotifyEvent', 'TWizardPageShouldSkipEvent' + 'TEdit', 'TEditCharCase', 'TEShiftState', 'TExtractionWizardPage', 'TFileStream', 'TFolderRenameEvent', + 'TFolderTreeView', 'TFont', 'TFontStyle', 'TFontStyles', 'TForm', 'TFormBorderStyle', + 'TFormStyle', 'TGraphic', 'TGraphicControl', 'TGraphicsObject', 'THandleStream', 'TInputDirWizardPage', + 'TInputFileWizardPage', 'TInputOptionWizardPage', 'TInputQueryWizardPage', 'TKeyEvent', + 'TKeyPressEvent', 'TLabel', 'TLinkLabel', 'TListBox', 'TListBoxStyle', 'TMainForm', + 'TMemo', 'TNewButton', 'TNewCheckBox', 'TNewCheckListBox', 'TNewComboBox', 'TNewEdit', + 'TNewLinkLabel', 'TNewListBox', 'TNewMemo', 'TNewNotebook', 'TNewNotebookPage', 'TNewProgressBar', + 'TNewProgressBarState', 'TNewProgressBarStyle', 'TNewRadioButton', 'TNewStaticText', + 'TNotifyEvent', 'TObject', 'TOutputMarqueeProgressWizardPage', 'TOutputMsgMemoWizardPage', + 'TOutputMsgWizardPage', 'TOutputProgressWizardPage', 'TPanel', 'TPanelBevel', 'TPasswordEdit', + 'TPen', 'TPenMode', 'TPenStyle', 'TPersistent', 'TPosition', 'TRadioButton', 'TRichEditViewer', + 'TScrollingWinControl', 'TScrollStyle', 'TSetupForm', 'TShiftState', 'TSizeConstraints', + 'TStartMenuFolderTreeView', 'TStream', 'TStringList', 'TStrings', 'TStringStream', + 'TSysLinkEvent', 'TSysLinkType', 'TUIStateForm', 'TUninstallProgressForm', 'TWinControl', + 'TWizardForm', 'TWizardPage', 'TWizardPageButtonEvent', 'TWizardPageCancelEvent', 'TWizardPageNotifyEvent', + 'TWizardPageShouldSkipEvent' ]; PascalEnumValues_Isxclasses: array of AnsiString = [ @@ -89,6 +90,7 @@ interface 'function CheckItem(Index: Integer; AOperation: TCheckItemOperation): Boolean;', 'function CopyFrom(Source: TStream; ByteCount: Int64; BufferSize: Integer): Int64;', 'function Download: Int64;', + 'function Extract: Integer;', 'function Find(S: String; var Index: Integer): Boolean;', 'function FindComponent(AName: String): TComponent;', 'function FindNextPage(CurPage: TNewNotebookPage; GoForward: Boolean): TNewNotebookPage;', @@ -102,6 +104,7 @@ interface 'function TextHeight(Text: String): Integer;', 'function TextWidth(Text: String): Integer;', 'function Write(Buffer: AnyString; ByteCount: Longint): Longint;', + 'procedure Add(ArchiveFileName, DestDir: String; FullPaths: Boolean);', 'procedure Add(Url, BaseName, RequiredSHA256OfFile: String);', 'procedure AddEx(Url, BaseName, RequiredSHA256OfFile, UserName, Password: String);', 'procedure AddStrings(Strings: TStrings);', @@ -195,11 +198,11 @@ interface 'SelectComponentsPage', 'SelectDirBitmapImage', 'SelectDirBrowseLabel', 'SelectDirLabel', 'SelectDirPage', 'Selected', 'SelectedValueIndex', 'SelectGroupBitmapImage', 'SelectProgramGroupPage', 'SelectStartMenuFolderBrowseLabel', 'SelectStartMenuFolderLabel', 'SelectTasksLabel', - 'SelectTasksPage', 'SelLength', 'SelStart', 'SelText', 'Shape', 'ShowAccelChar', 'ShowBaseNameInsteadOfUrl', - 'ShowHint', 'Showing', 'ShowLines', 'Size', 'SizeAndCenterOnShow', 'Sorted', 'State', - 'StatusLabel', 'Stretch', 'Strings', 'Style', 'SubCaptionLabel', 'SubItemFontStyle', - 'Surface', 'SurfaceColor', 'SurfaceHeight', 'SurfaceWidth', 'TabOrder', 'TabStop', - 'Tag', 'TasksList', 'Text', 'Top', 'TypesCombo', 'UseRichEdit', 'UserInfoNameEdit', + 'SelectTasksPage', 'SelLength', 'SelStart', 'SelText', 'Shape', 'ShowAccelChar', 'ShowArchiveInsteadOfFile', + 'ShowBaseNameInsteadOfUrl', 'ShowHint', 'Showing', 'ShowLines', 'Size', 'SizeAndCenterOnShow', + 'Sorted', 'State', 'StatusLabel', 'Stretch', 'Strings', 'Style', 'SubCaptionLabel', + 'SubItemFontStyle', 'Surface', 'SurfaceColor', 'SurfaceHeight', 'SurfaceWidth', 'TabOrder', + 'TabStop', 'Tag', 'TasksList', 'Text', 'Top', 'TypesCombo', 'UseRichEdit', 'UserInfoNameEdit', 'UserInfoNameLabel', 'UserInfoOrgEdit', 'UserInfoOrgLabel', 'UserInfoPage', 'UserInfoSerialEdit', 'UserInfoSerialLabel', 'UseVisualStyle', 'Values', 'Visible', 'WantReturns', 'WantTabs', 'WelcomeLabel1', 'WelcomeLabel2', 'WelcomePage', 'Width', 'WizardBitmapImage', 'WizardBitmapImage2', diff --git a/ISHelp/isxfunc.xml b/ISHelp/isxfunc.xml index b4a47aeb7..c75ab783a 100644 --- a/ISHelp/isxfunc.xml +++ b/ISHelp/isxfunc.xml @@ -1795,7 +1795,8 @@ end; DownloadTemporaryFileSize
DownloadTemporaryFileDate
CreateDownloadPage
-ExtractTemporaryFile

+ExtractTemporaryFile
+Extract7ZipArchive

 [Code]
 function OnDownloadProgress(const Url, Filename: String; const Progress, ProgressMax: Int64): Boolean;
@@ -1841,11 +1842,16 @@ end;

See DownloadTemporaryFile for other considerations.

- Extract7ZipFile - function Extract7ZipFile(const FileName, DestDir: String; const FullPaths: Boolean): Integer; -

Extracts the specified 7-Zip archive to the specified directory, with or without using path names. Returns zero if successful, nonzero otherwise

-

The archive must not be encrypted.

-

Uses an embedded version of the "7z ANSI-C Decoder" from the LZMA SDK by Igor Pavlov, as-is, except that Unicode support and error messages were improved and that it outputs memory requirements.

+ Extract7ZipArchive + function Extract7ZipArchive(const ArchiveFileName, DestDir: String; const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress): Integer; +

Extracts the specified 7-Zip archive to the specified directory, with or without using path names.

+

Returns zero if successful, nonzero otherwise

+

The archive must not be encrypted.

+

Set OnExtractionProgress to a function to be informed of progress, or nil otherwise.

+

TOnExtractionProgress is defined as:

+

TOnExtractionProgress = function(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean;

+

Return True to allow the extraction to continue, False otherwise.

+

Extract7ZipArchive uses an embedded version of the "7z ANSI-C Decoder" from the LZMA SDK by Igor Pavlov, as-is, except that Unicode support and error messages were improved and that it outputs memory requirements.

All output of the decoder is logged if logging is enabled, including error messages but excluding empty lines.

The decoder has the following limitations, as written by Igor Pavlov in the LZMA SDK:

-It reads only "FileName", "Size", "LastWriteTime" and "CRC" information for each file in archive.
@@ -1858,6 +1864,27 @@ You can create .7z archive with 7z.exe, 7za.exe or 7zr.exe:

If you have big number of files in archive, and you need fast extracting, you can use partly-solid archives:

7za.exe a archive.7z *.htm -ms=512K -r -mx -m0fb=255 -m0d=512K

In that example 7-Zip will use 512KB solid blocks. So it needs to decompress only 512KB for extracting one file from such archive.

+

CreateExtractionPage
+CreateDownloadPage
+DownloadTemporaryFile
+ExtractTemporaryFile

+
+[Code]
+function OnExtractionProgress(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean;
+begin
+  Log(Format('  %s\%s: %d of %d bytes done.', [ArchiveFileName, FileName, Progress, ProgressMax]))
+  Result := True;
+end;
+
+function InitializeSetup: Boolean;
+begin
+  try
+    Result := Extract7ZipArchive(ExpandConstant('{tmp}\Archive.7z'), ExpandConstant('{app}'), True, @OnExtractionProgress) = 0;
+  except
+    Log(GetExceptionMessage);
+    Result := False;
+  end;
+end;
@@ -2621,6 +2648,23 @@ Page := CreateOutputMsgMemoPage(wpWelcome,

See CodeDownloadFiles.iss for an example.

TDownloadWizardPage
DownloadTemporaryFile
+CreateOutputProgressPage

+ + + CreateExtractionPage + function CreateExtractionPage(const ACaption, ADescription: String; const OnExtractionProgress: TOnExtractionProgress): ExtractionWizardPage; +

Creates a wizard page to extract 7-Zip archives and show progress.

+

Set OnExtractionProgress to a function to be informed of progress, or nil otherwise.

+

Unlike the other types of wizard pages, progress pages are not displayed as part of the normal page sequence (note that there is no AfterID parameter). A progress page can only be displayed programmatically by calling its Show method.

+

Call the Show method to activate and show the page. When you're finished with it, call the Hide method to revert to the previous page.

+

Always put the Hide call inside the finally part of a try..finally language construct, as demonstrated in CodeDownloadFiles.iss. Not calling Hide will result in the wizard being permanently stuck on the progress page.

+

To add a new archive to extract, call the Add method. Always call the Clear method before adding the first file.

+

To start the extraction, call the Extract method. An exception will be raised if there was an error. Otherwise, Extract returns the number of archives extracted.

+

Set the ShowArchiveInsteadFile property to True to show the name of the archive which is being extracted to the user instead of the names of the files inside the archive.

+

See Extract7ZipArchive for other considerations and the definition of TOnExtractionProgress.

+

See CodeDownloadFiles.iss for an example of CreateDownloadPage which works very similar to CreateExtractionPage.

+

TExtractionWizardPage
+Extract7ZipArchive
CreateOutputProgressPage

diff --git a/Projects/Src/Compiler.ScriptClasses.pas b/Projects/Src/Compiler.ScriptClasses.pas index 520bd0869..4da2bc73f 100644 --- a/Projects/Src/Compiler.ScriptClasses.pas +++ b/Projects/Src/Compiler.ScriptClasses.pas @@ -567,6 +567,20 @@ procedure RegisterDownloadWizardPage_C(Cl: TPSPascalCompiler); end; end; +procedure RegisterExtractionWizardPage_C(Cl: TPSPascalCompiler); +begin + with CL.AddClassN(Cl.FindClass('TOutputProgressWizardPage'),'TExtractionWizardPage') do + begin + RegisterProperty('AbortButton', 'TNewButton', iptr); + RegisterProperty('AbortedByUser', 'Boolean', iptr); + RegisterProperty('ShowArchiveInsteadOfFile', 'Boolean', iptrw); + RegisterMethod('procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean)'); + RegisterMethod('procedure Clear'); + RegisterMethod('function Extract: Integer'); + RegisterMethod('procedure Show'); { Without this TOutputProgressWizardPage's Show will be called } + end; +end; + procedure RegisterHandCursor_C(Cl: TPSPascalCompiler); begin cl.AddConstantN('crHand', 'Integer').Value.ts32 := crHand; @@ -675,6 +689,7 @@ procedure ScriptClassesLibraryRegister_C(Cl: TPSPascalCompiler); RegisterOutputProgressWizardPage_C(Cl); RegisterOutputMarqueeProgressWizardPage_C(Cl); RegisterDownloadWizardPage_C(Cl); + RegisterExtractionWizardPage_C(Cl); RegisterHandCursor_C(Cl); diff --git a/Projects/Src/Compiler.ScriptFunc.pas b/Projects/Src/Compiler.ScriptFunc.pas index 9f1667bfe..cf8076bac 100644 --- a/Projects/Src/Compiler.ScriptFunc.pas +++ b/Projects/Src/Compiler.ScriptFunc.pas @@ -139,6 +139,7 @@ procedure ScriptFuncLibraryRegister_C(ScriptCompiler: TPSPascalCompiler; 'end'); RegisterType('TOnDownloadProgress', 'function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean;'); + RegisterType('TOnExtractionProgress', 'function(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean;'); RegisterType('TOnLog', 'procedure(const S: String; const Error, FirstLine: Boolean);'); for var ScriptFuncTable in ScriptFuncTables do diff --git a/Projects/Src/Compression.SevenZipDecoder.pas b/Projects/Src/Compression.SevenZipDecoder.pas index 0b277d9c2..4cc4d23bf 100644 --- a/Projects/Src/Compression.SevenZipDecoder.pas +++ b/Projects/Src/Compression.SevenZipDecoder.pas @@ -12,18 +12,29 @@ interface -function SevenZipDecode(const FileName, DestDir: String; - const FullPaths: Boolean): Integer; +type + TOnExtractionProgress = function(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean of object; + +function Extract7ZipArchive(const ArchiveFileName, DestDir: String; + const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress): Integer; implementation uses Windows, SysUtils, Forms, PathFunc, - Setup.LoggingFunc, Setup.MainFunc; + Setup.LoggingFunc, Setup.MainFunc, Setup.InstFunc; + +type + TSevenZipDecodeState = record + ExpandedDestDir: String; + LogBuffer: AnsiString; + OnExtractionProgress: TOnExtractionProgress; + LastReportedProgress, LastReportedProgressMax: UInt64; + end; var - ExpandedDestDir: String; + State: TSevenZipDecodeState; { Compiled by Visual Studio 2022 using compile.bat To enable source debugging recompile using compile-bcc32c.bat and turn off the VISUALSTUDIO define below @@ -37,7 +48,7 @@ function __CreateDirectoryW(lpPathName: LPCWSTR; lpSecurityAttributes: PSecurityAttributes): BOOL; cdecl; begin var ExpandedDir: String; - if PathExpand(lpPathName, ExpandedDir) and PathStartsWith(ExpandedDir, ExpandedDestDir) then + if PathExpand(lpPathName, ExpandedDir) and PathStartsWith(ExpandedDir, State.ExpandedDestDir) then Result := CreateDirectoryW(PChar(ExpandedDir), lpSecurityAttributes) else begin Result := False; @@ -61,7 +72,7 @@ function __CreateFileW(lpFileName: LPCWSTR; dwDesiredAccess, dwShareMode: DWORD; hTemplateFile: THandle): THandle; cdecl; begin var ExpandedFileName: String; - if PathExpand(lpFileName, ExpandedFileName) and PathStartsWith(ExpandedFileName, ExpandedDestDir) then + if PathExpand(lpFileName, ExpandedFileName) and PathStartsWith(ExpandedFileName, State.ExpandedDestDir) then Result := CreateFileW(PChar(ExpandedFileName), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile) else begin Result := INVALID_HANDLE_VALUE; @@ -189,9 +200,6 @@ procedure Log(const S: AnsiString); Setup.LoggingFunc.Log(UTF8ToString(S)); end; -var - LogBuffer: AnsiString; - function __fputs(str: PAnsiChar; unused: Pointer): Integer; cdecl; function FindNewLine(const S: AnsiString): Integer; @@ -206,14 +214,14 @@ function __fputs(str: PAnsiChar; unused: Pointer): Integer; cdecl; begin try - LogBuffer := LogBuffer + str; - var P := FindNewLine(LogBuffer); + State.LogBuffer := State.LogBuffer + str; + var P := FindNewLine(State.LogBuffer); while P <> 0 do begin - Log(Copy(LogBuffer, 1, P-1)); - if (LogBuffer[P] = #13) and (P < Length(LogBuffer)) and (LogBuffer[P+1] = #10) then + Log(Copy(State.LogBuffer, 1, P-1)); + if (State.LogBuffer[P] = #13) and (P < Length(State.LogBuffer)) and (State.LogBuffer[P+1] = #10) then Inc(P); - Delete(LogBuffer, 1, P); - P := FindNewLine(LogBuffer); + Delete(State.LogBuffer, 1, P); + P := FindNewLine(State.LogBuffer); end; Result := 0; except @@ -223,23 +231,56 @@ function __fputs(str: PAnsiChar; unused: Pointer): Integer; cdecl; procedure _ReportProgress(const FileName: PChar; const Progress, ProgressMax: UInt64; var Abort: Bool); cdecl; begin - //Setup.LoggingFunc.Log(Format('%s: %d of %d', [FileName, Progress, ProgressMax])); + if Assigned(State.OnExtractionProgress) 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 (Progress = 0) or (Progress = ProgressMax) or + (Progress < State.LastReportedProgress) or (ProgressMax <> State.LastReportedProgressMax) or + ((Progress - State.LastReportedProgress) > 524288) then begin + try + var ArchiveFileName := '?'; //todo: fix + if not State.OnExtractionProgress(ArchiveFileName, FileName, Progress, ProgressMax) then + Abort := True; + finally + State.LastReportedProgress := Progress; + State.LastReportedProgressMax := ProgressMax; + end; + end; + end; + if not Abort and DownloadTemporaryFileOrSevenZipDecodeProcessMessages then Application.ProcessMessages; end; -function SevenZipDecode(const FileName, DestDir: String; - const FullPaths: Boolean): Integer; +function Extract7ZipArchive(const ArchiveFileName, DestDir: String; + const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress): Integer; begin + if ArchiveFileName = '' then + InternalError('Extract7ZipArchive: Invalid ArchiveFileName value'); + if DestDir = '' then + InternalError('Extract7ZipArchive: Invalid DestDir value'); + + LogFmt('Extracting 7-Zip archive %s to %s. Full paths? %s', [ArchiveFileName, DestDir, SYesNo[FullPaths]]); + var SaveCurDir := GetCurrentDir; if not SetCurrentDir(DestDir) then Exit(-1); try - LogBuffer := ''; - ExpandedDestDir := AddBackslash(PathExpand(DestDir)); - Result := IS_7zDec(PChar(FileName), FullPaths); - if LogBuffer <> '' then - Log(LogBuffer); + State.LogBuffer := ''; + State.ExpandedDestDir := AddBackslash(PathExpand(DestDir)); + State.OnExtractionProgress := OnExtractionProgress; + State.LastReportedProgress := 0; + State.LastReportedProgressMax := 0; + + Result := IS_7zDec(PChar(ArchiveFileName), FullPaths); + + //todo: throw exception on Result <> 0 like DownloadTemporaryFile uses exceptions? + + if State.LogBuffer <> '' then + Log(State.LogBuffer); finally SetCurrentDir(SaveCurDir); end; diff --git a/Projects/Src/IDE.ScintStylerInnoSetup.pas b/Projects/Src/IDE.ScintStylerInnoSetup.pas index 597b56e78..20ae28889 100644 --- a/Projects/Src/IDE.ScintStylerInnoSetup.pas +++ b/Projects/Src/IDE.ScintStylerInnoSetup.pas @@ -455,7 +455,7 @@ TISPPDirective = record 'TArrayOfString', 'TArrayOfChar', 'TArrayOfBoolean', 'TArrayOfInteger', 'DWORD', 'UINT', 'BOOL', 'DWORD_PTR', 'UINT_PTR', 'INT_PTR', 'TFileTime', 'TExecWait', 'TExecOutput', 'TFindRec', 'TWindowsVersion', - 'TOnDownloadProgress', 'TOnLog' + 'TOnDownloadProgress', 'TOnExtractionProgress', 'TOnLog' { ScriptClasses: see PascalTypes_Isxclasses in isxclasses_wordlists_generated } ]; diff --git a/Projects/Src/Setup.ScriptClasses.pas b/Projects/Src/Setup.ScriptClasses.pas index baf7fff35..187a85ff1 100644 --- a/Projects/Src/Setup.ScriptClasses.pas +++ b/Projects/Src/Setup.ScriptClasses.pas @@ -343,6 +343,17 @@ procedure RegisterDownloadWizardPage_R(CL: TPSRuntimeClassImporter); end; end; +procedure RegisterExtractionWizardPage_R(CL: TPSRuntimeClassImporter); +begin + with CL.Add(TExtractionWizardPage) do + begin + RegisterMethod(@TExtractionWizardPage.Add, 'Add'); + RegisterMethod(@TExtractionWizardPage.Clear, 'Clear'); + RegisterMethod(@TExtractionWizardPage.Extract, 'Extract'); + RegisterMethod(@TExtractionWizardPage.Show, 'Show'); + end; +end; + procedure RegisterHandCursor_R(Cl: TPSRuntimeClassImporter); const IDC_HAND = MakeIntResource(32649); @@ -447,6 +458,7 @@ function ScriptClassesLibraryRegister_R(ScriptInterpreter: TPSExec): TPSRuntimeC RegisterOutputProgressWizardPage_R(Cl); RegisterOutputMarqueeProgressWizardPage_R(Cl); RegisterDownloadWizardPage_R(Cl); + RegisterExtractionWizardPage_R(Cl); RegisterHandCursor_R(Cl); diff --git a/Projects/Src/Setup.ScriptDlg.pas b/Projects/Src/Setup.ScriptDlg.pas index 8c6d973e2..d55c8e88f 100644 --- a/Projects/Src/Setup.ScriptDlg.pas +++ b/Projects/Src/Setup.ScriptDlg.pas @@ -12,8 +12,8 @@ interface uses - Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, StdCtrls, Contnrs, - Setup.WizardForm, Setup.Install, + Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, StdCtrls, Contnrs, Generics.Collections, + Setup.WizardForm, Setup.Install, Compression.SevenZipDecoder, NewCheckListBox, NewStaticText, NewProgressBar, PasswordEdit, RichEditViewer, BidiCtrls, TaskbarProgressFunc; @@ -172,9 +172,14 @@ TOutputMarqueeProgressWizardPage = class(TOutputProgressWizardPage) procedure SetProgress(const Position, Max: Longint); end; + TDownloadFile = class + Url, BaseName, RequiredSHA256OfFile, UserName, Password: String; + end; + TDownloadFiles = TObjectList; + TDownloadWizardPage = class(TOutputProgressWizardPage) private - FFiles: TObjectList; + FFiles: TDownloadFiles; FOnDownloadProgress: TOnDownloadProgress; FShowBaseNameInsteadOfUrl: Boolean; FAbortButton: TNewButton; @@ -198,6 +203,37 @@ TDownloadWizardPage = class(TOutputProgressWizardPage) property ShowBaseNameInsteadOfUrl: Boolean read FShowBaseNameInsteadOfUrl write FShowBaseNameInsteadOfUrl; end; + TArchive = class + FileName, DestDir: String; + FullPaths: Boolean; + end; + TArchives = TObjectList; + + TExtractionWizardPage = class(TOutputProgressWizardPage) + private + FArchives: TArchives; + FOnExtractionProgress: TOnExtractionProgress; + FShowArchiveInsteadOfFile: Boolean; + FAbortButton: TNewButton; + FShowProgressControlsOnNextProgress, FAbortedByUser: Boolean; + procedure AbortButtonClick(Sender: TObject); + function InternalOnExtractionProgress(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean; + procedure ShowProgressControls(const AVisible: Boolean); + public + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + procedure Initialize; override; + procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean); + procedure Clear; + function Extract: Integer; + property OnExtractionProgress: TOnExtractionProgress write FOnExtractionProgress; + procedure Show; override; + published + property AbortButton: TNewButton read FAbortButton; + property AbortedByUser: Boolean read FAbortedByUser; + property ShowArchiveInsteadOfFile: Boolean read FShowArchiveInsteadOfFile write FShowArchiveInsteadOfFile; + end; + implementation uses @@ -920,11 +956,6 @@ procedure TOutputMarqueeProgressWizardPage.SetProgress(const Position, Max: Long {--- Download ---} -type - TDownloadFile = class - Url, BaseName, RequiredSHA256OfFile, UserName, Password: String; - end; - procedure TDownloadWizardPage.AbortButtonClick(Sender: TObject); begin FAbortedByUser := LoggedMsgBox(SetupMessages[msgStopDownload], '', mbConfirmation, MB_YESNO, True, ID_YES) = IDYES; @@ -970,7 +1001,7 @@ constructor TDownloadWizardPage.Create(AOwner: TComponent); begin inherited; FUseMarqueeStyle := True; - FFiles := TObjectList.Create; + FFiles := TDownloadFiles.Create; end; destructor TDownloadWizardPage.Destroy; @@ -1019,10 +1050,8 @@ procedure TDownloadWizardPage.Add(const Url, BaseName, RequiredSHA256OfFile: Str end; procedure TDownloadWizardPage.AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String); -var - F: TDownloadFile; begin - F := TDownloadFile.Create; + var F := TDownloadFile.Create; F.Url := Url; F.BaseName := BaseName; F.RequiredSHA256OfFile := RequiredSHA256OfFile; @@ -1037,15 +1066,11 @@ procedure TDownloadWizardPage.Clear; end; function TDownloadWizardPage.Download: Int64; -var - F: TDownloadFile; - I: Integer; begin FAbortedByUser := False; - + Result := 0; - for I := 0 to FFiles.Count-1 do begin - F := TDownloadFile(FFiles[I]); + for var F in FFiles do begin { Don't need to set DownloadTemporaryFileProcessMessages before downloading since we already process messages ourselves. } SetDownloadCredentials(F.UserName, F.Password); Result := Result + DownloadTemporaryFile(F.Url, F.BaseName, F.RequiredSHA256OfFile, InternalOnDownloadProgress); @@ -1053,4 +1078,118 @@ function TDownloadWizardPage.Download: Int64; SetDownloadCredentials('', ''); end; +{--- Extraction ---} + +procedure TExtractionWizardPage.AbortButtonClick(Sender: TObject); +begin + //todo: fix msg! + FAbortedByUser := LoggedMsgBox(SetupMessages[msgStopDownload], '', mbConfirmation, MB_YESNO, True, ID_YES) = IDYES; +end; + +function TExtractionWizardPage.InternalOnExtractionProgress(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean; +var + Progress32, ProgressMax32: LongInt; +begin + if FAbortedByUser then begin + Log('Need to abort extraction.'); + Result := False; + end else begin + Log(Format(' %d bytes done.', [Progress])); + + FMsg2Label.Caption := IfThen(FShowArchiveInsteadOfFile, ArchiveFileName, FileName); + if ProgressMax > MaxLongInt then begin + Progress32 := Round((Progress / ProgressMax) * MaxLongInt); + ProgressMax32 := MaxLongInt; + end else begin + Progress32 := Progress; + ProgressMax32 := ProgressMax; + end; + SetProgress(Progress32, ProgressMax32); { This will process messages which we need for the abort button to work } + + if FShowProgressControlsOnNextProgress then begin + ShowProgressControls(True); + FShowProgressControlsOnNextProgress := False; + ProcessMsgs; + end; + + if Assigned(FOnExtractionProgress) then + Result := FOnExtractionProgress(ArchiveFileName, FileName, Progress, ProgressMax) + else + Result := True; + end; +end; + +constructor TExtractionWizardPage.Create(AOwner: TComponent); +begin + inherited; + FUseMarqueeStyle := True; + FArchives := TArchives.Create; +end; + +destructor TExtractionWizardPage.Destroy; +begin + FArchives.Free; + inherited; +end; + +procedure TExtractionWizardPage.Initialize; +begin + inherited; + + FMsg1Label.Caption := SetupMessages[msgDownloadingLabel]; //todo: fix message + + FAbortButton := TNewButton.Create(Self); + with FAbortButton do begin + Caption := SetupMessages[msgButtonStopDownload]; //todo: fix message + Top := FProgressBar.Top + FProgressBar.Height + WizardForm.ScalePixelsY(8); + Width := WizardForm.CalculateButtonWidth([Caption]); + Anchors := [akLeft, akTop]; + Height := WizardForm.CancelButton.Height; + OnClick := AbortButtonClick; + end; + SetCtlParent(FAbortButton, Surface); +end; + +procedure TExtractionWizardPage.Show; +begin + if WizardForm.CurPageID <> ID then begin + ShowProgressControls(False); + FShowProgressControlsOnNextProgress := True; + end; + inherited; +end; + +procedure TExtractionWizardPage.ShowProgressControls(const AVisible: Boolean); +begin + FMsg2Label.Visible := AVisible; + FProgressBar.Visible := AVisible; + FAbortButton.Visible := AVisible; +end; + +procedure TExtractionWizardPage.Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean); +begin + var A := TArchive.Create; + A.FileName := ArchiveFileName; + A.DestDir := DestDir; + A.FullPaths := FullPaths; + FArchives.Add(A); +end; + +procedure TExtractionWizardPage.Clear; +begin + FArchives.Clear; +end; + +function TExtractionWizardPage.Extract: Integer; +begin + FAbortedByUser := False; + + Result := 0; + for var A in FArchives do begin + { Don't need to set DownloadTemporaryFileOrSevenZipDecodeProcessMessages before extraction since we already process messages ourselves. } + if Extract7ZipArchive(A.FileName, A.DestDir, A.FullPaths, InternalOnExtractionProgress) = 0 then + Inc(Result); + end; +end; + end. diff --git a/Projects/Src/Setup.ScriptFunc.pas b/Projects/Src/Setup.ScriptFunc.pas index e9b0c491e..56748bc81 100644 --- a/Projects/Src/Setup.ScriptFunc.pas +++ b/Projects/Src/Setup.ScriptFunc.pas @@ -129,18 +129,6 @@ function IsProtectedSrcExe(const Filename: String): Boolean; function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean; var PStart: Cardinal; - NewPage: TWizardPage; - NewInputQueryPage: TInputQueryWizardPage; - NewInputOptionPage: TInputOptionWizardPage; - NewInputDirPage: TInputDirWizardPage; - NewInputFilePage: TInputFileWizardPage; - NewOutputMsgPage: TOutputMsgWizardPage; - NewOutputMsgMemoPage: TOutputMsgMemoWizardPage; - NewOutputProgressPage: TOutputProgressWizardPage; - NewOutputMarqueeProgressPage: TOutputMarqueeProgressWizardPage; - NewDownloadPage: TDownloadWizardPage; - OnDownloadProgress: TOnDownloadProgress; - NewSetupForm: TSetupForm; begin PStart := Stack.Count-1; Result := True; @@ -156,7 +144,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATECUSTOMPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewPage := TWizardPage.Create(GetWizardForm); + var NewPage := TWizardPage.Create(GetWizardForm); try NewPage.Caption := Stack.GetString(PStart-2); NewPage.Description := Stack.GetString(PStart-3); @@ -169,7 +157,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATEINPUTQUERYPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewInputQueryPage := TInputQueryWizardPage.Create(GetWizardForm); + var NewInputQueryPage := TInputQueryWizardPage.Create(GetWizardForm); try NewInputQueryPage.Caption := Stack.GetString(PStart-2); NewInputQueryPage.Description := Stack.GetString(PStart-3); @@ -183,7 +171,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATEINPUTOPTIONPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewInputOptionPage := TInputOptionWizardPage.Create(GetWizardForm); + var NewInputOptionPage := TInputOptionWizardPage.Create(GetWizardForm); try NewInputOptionPage.Caption := Stack.GetString(PStart-2); NewInputOptionPage.Description := Stack.GetString(PStart-3); @@ -198,7 +186,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATEINPUTDIRPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewInputDirPage := TInputDirWizardPage.Create(GetWizardForm); + var NewInputDirPage := TInputDirWizardPage.Create(GetWizardForm); try NewInputDirPage.Caption := Stack.GetString(PStart-2); NewInputDirPage.Description := Stack.GetString(PStart-3); @@ -213,7 +201,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATEINPUTFILEPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewInputFilePage := TInputFileWizardPage.Create(GetWizardForm); + var NewInputFilePage := TInputFileWizardPage.Create(GetWizardForm); try NewInputFilePage.Caption := Stack.GetString(PStart-2); NewInputFilePage.Description := Stack.GetString(PStart-3); @@ -227,7 +215,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATEOUTPUTMSGPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewOutputMsgPage := TOutputMsgWizardPage.Create(GetWizardForm); + var NewOutputMsgPage := TOutputMsgWizardPage.Create(GetWizardForm); try NewOutputMsgPage.Caption := Stack.GetString(PStart-2); NewOutputMsgPage.Description := Stack.GetString(PStart-3); @@ -241,7 +229,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATEOUTPUTMSGMEMOPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewOutputMsgMemoPage := TOutputMsgMemoWizardPage.Create(GetWizardForm); + var NewOutputMsgMemoPage := TOutputMsgMemoWizardPage.Create(GetWizardForm); try NewOutputMsgMemoPage.Caption := Stack.GetString(PStart-2); NewOutputMsgMemoPage.Description := Stack.GetString(PStart-3); @@ -256,7 +244,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATEOUTPUTPROGRESSPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewOutputProgressPage := TOutputProgressWizardPage.Create(GetWizardForm); + var NewOutputProgressPage := TOutputProgressWizardPage.Create(GetWizardForm); try NewOutputProgressPage.Caption := Stack.GetString(PStart-1); NewOutputProgressPage.Description := Stack.GetString(PStart-2); @@ -270,7 +258,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: end else if Proc.Name = 'CREATEOUTPUTMARQUEEPROGRESSPAGE' then begin if IsUninstaller then NoUninstallFuncError(Proc.Name); - NewOutputMarqueeProgressPage := TOutputMarqueeProgressWizardPage.Create(GetWizardForm); + var NewOutputMarqueeProgressPage := TOutputMarqueeProgressWizardPage.Create(GetWizardForm); try NewOutputMarqueeProgressPage.Caption := Stack.GetString(PStart-1); NewOutputMarqueeProgressPage.Description := Stack.GetString(PStart-2); @@ -285,12 +273,13 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: if IsUninstaller then NoUninstallFuncError(Proc.Name); var P: PPSVariantProcPtr := Stack.Items[PStart-3]; + var OnDownloadProgress: TOnDownloadProgress; { ProcNo 0 means nil was passed by the script } if P.ProcNo <> 0 then OnDownloadProgress := TOnDownloadProgress(Caller.GetProcAsMethod(P.ProcNo)) else OnDownloadProgress := nil; - NewDownloadPage := TDownloadWizardPage.Create(GetWizardForm); + var NewDownloadPage := TDownloadWizardPage.Create(GetWizardForm); try NewDownloadPage.Caption := Stack.GetString(PStart-1); NewDownloadPage.Description := Stack.GetString(PStart-2); @@ -302,6 +291,28 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: raise; end; Stack.SetClass(PStart, NewDownloadPage); + end else if Proc.Name = 'CREATEXTRACTIONPAGE' then begin + if IsUninstaller then + NoUninstallFuncError(Proc.Name); + var P: PPSVariantProcPtr := Stack.Items[PStart-3]; + var OnExtractionProgress: TOnExtractionProgress; + { ProcNo 0 means nil was passed by the script } + if P.ProcNo <> 0 then + OnExtractionProgress := TOnExtractionProgress(Caller.GetProcAsMethod(P.ProcNo)) + else + OnExtractionProgress := nil; + var NewExtractionPage := TExtractionWizardPage.Create(GetWizardForm); + try + NewExtractionPage.Caption := Stack.GetString(PStart-1); + NewExtractionPage.Description := Stack.GetString(PStart-2); + GetWizardForm.AddPage(NewExtractionPage, -1); + NewExtractionPage.Initialize; + NewExtractionPage.OnExtractionProgress := OnExtractionProgress; + except + NewExtractionPage.Free; + raise; + end; + Stack.SetClass(PStart, NewExtractionPage); end else if Proc.Name = 'SCALEX' then begin InitializeScaleBaseUnits; Stack.SetInt(PStart, MulDiv(Stack.GetInt(PStart-1), ScaleBaseUnitX, OrigBaseUnitX)); @@ -309,7 +320,7 @@ function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: InitializeScaleBaseUnits; Stack.SetInt(PStart, MulDiv(Stack.GetInt(PStart-1), ScaleBaseUnitY, OrigBaseUnitY)); end else if Proc.Name = 'CREATECUSTOMFORM' then begin - NewSetupForm := TSetupForm.CreateNew(nil); + var NewSetupForm := TSetupForm.CreateNew(nil); try NewSetupForm.AutoScroll := False; NewSetupForm.BorderStyle := bsDialog; @@ -773,7 +784,6 @@ function CommonFuncProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack function InstallProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean; var PStart: Cardinal; - OnDownloadProgress: TOnDownloadProgress; begin if IsUninstaller then NoUninstallFuncError(Proc.Name); @@ -787,6 +797,7 @@ function InstallProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: T Stack.SetInt(PStart, ExtractTemporaryFiles(Stack.GetString(PStart-1))); end else if Proc.Name = 'DOWNLOADTEMPORARYFILE' then begin var P: PPSVariantProcPtr := Stack.Items[PStart-4]; + var OnDownloadProgress: TOnDownloadProgress; { ProcNo 0 means nil was passed by the script } if P.ProcNo <> 0 then OnDownloadProgress := TOnDownloadProgress(Caller.GetProcAsMethod(P.ProcNo)) @@ -2106,8 +2117,15 @@ function OtherProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPS for I := 0 to N-1 do AscendingTrySizes[I] := VNGetInt(PSGetArrayField(Arr, I)); Stack.SetBool(PStart, TBitmapImage(Stack.GetClass(PStart-1)).InitializeFromIcon(0, PChar(Stack.GetString(PStart-2)), Stack.GetInt(PStart-3), AscendingTrySizes)); - end else if Proc.Name = 'EXTRACT7ZIPFILE' then begin - Stack.SetInt(PStart, SevenZipDecode(Stack.GetString(PStart-1), Stack.GetString(PStart-2), Stack.GetBool(PStart-3))); + end else if Proc.Name = 'EXTRACT7ZIPARCHIVE' then begin + var P: PPSVariantProcPtr := Stack.Items[PStart-4]; + var OnExtractionProgress: TOnExtractionProgress; + { ProcNo 0 means nil was passed by the script } + if P.ProcNo <> 0 then + OnExtractionProgress := TOnExtractionProgress(Caller.GetProcAsMethod(P.ProcNo)) + else + OnExtractionProgress := nil; + Stack.SetInt(PStart, Extract7ZipArchive(Stack.GetString(PStart-1), Stack.GetString(PStart-2), Stack.GetBool(PStart-3), OnExtractionProgress)); end else if Proc.Name = 'DEBUGGING' then begin Stack.SetBool(PStart, Debugging); end else diff --git a/Projects/Src/Shared.ScriptFunc.pas b/Projects/Src/Shared.ScriptFunc.pas index 24848ab17..ed25b95a2 100644 --- a/Projects/Src/Shared.ScriptFunc.pas +++ b/Projects/Src/Shared.ScriptFunc.pas @@ -221,6 +221,7 @@ initialization 'function CreateOutputProgressPage(const ACaption, ADescription: String): TOutputProgressWizardPage;', 'function CreateOutputMarqueeProgressPage(const ACaption, ADescription: String): TOutputMarqueeProgressWizardPage;', 'function CreateDownloadPage(const ACaption, ADescription: String; const OnDownloadProgress: TOnDownloadProgress): TDownloadWizardPage;', + 'function CreateExtractionPage(const ACaption, ADescription: String; const OnExtractionProgress: TOnExtractionProgress): TExtractionWizardPage;', 'function ScaleX(X: Integer): Integer;', 'function ScaleY(Y: Integer): Integer;', 'function CreateCustomForm: TSetupForm;' @@ -539,7 +540,7 @@ initialization 'function IsDotNetInstalled(const MinVersion: TDotNetVersion; const MinServicePack: Cardinal): Boolean;', 'function IsMsiProductInstalled(const UpgradeCode: String; const PackedMinVersion: Int64): Boolean;', 'function InitializeBitmapImageFromIcon(const BitmapImage: TBitmapImage; const IconFilename: String; const BkColor: TColor; const AscendingTrySizes: TArrayOfInteger): Boolean;', - 'function Extract7ZipFile(const FileName, DestDir: String; const FullPaths: Boolean): Integer;', + 'function Extract7ZipArchive(const FileName, DestDir: String; const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress): Integer;', 'function Debugging: Boolean;' ]; diff --git a/whatsnew.htm b/whatsnew.htm index 717130492..64d1df0a0 100644 --- a/whatsnew.htm +++ b/whatsnew.htm @@ -95,7 +95,7 @@
  • Added a dark mode version of the documentation, automatically used by the Compiler IDE if a dark theme is chosen.
  • Pascal Scripting changes:
      - Added new Extract7ZipFile support function to extract a 7-Zip archive, based on the "7z ANSI-C Decoder" from the LZMA SDK by Igor Pavlov. See the new help topic for information about its limitations. +
    • Added new Extract7ZipArchive support function to extract a 7-Zip archive, based on the "7z ANSI-C Decoder" from the LZMA SDK by Igor Pavlov. See the new help topic for information about its limitations.
      Added new CreateExtractionPage support function to easily show the extraction progress to the user.
    • Added new ExecAndCaptureOutput support function to execute a program or batch file and capture its stdout and stderr outputs separately.
    • Output logging now raises an exception if there was an error setting up output redirection (which should be very rare). The PowerShell.iss example script has been updated to catch the exception.
    • TInputDirWizardPage: Added new NewFolderName property to update the initial value passed to CreateInputDirPage.