{
***************************************************************************
*                                                                         *
*   This source is free software; you can redistribute it and/or modify   *
*   it under the terms of the GNU General Public License as published by  *
*   the Free Software Foundation; either version 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
*   This code is distributed in the hope that it will be useful, but      *
*   WITHOUT ANY WARRANTY; without even the implied warranty of            *
*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
*   General Public License for more details.                              *
*                                                                         *
*   A copy of the GNU General Public License is available on the World    *
*   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
*   obtain it by writing to the Free Software Foundation,                 *
*   Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.   *
*                                                                         *
***************************************************************************

Author: Balázs Székely
Abstract:
  Implementation of the package zipper class.
}
unit opkman_zipper;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, strutils,
  // LazUtils
  FileUtil, LazFileUtils,
  // OpkMan
  opkman_serializablepackages, opkman_common,
  {$IF FPC_FULLVERSION>=30200}zipper{$ELSE}opkman_zip{$ENDIF};

type
  TOnProgressEx = procedure(Sender : TObject; const ATotPos, ATotSize: Int64);
  TOnZipProgress = procedure(Sender: TObject; AZipfile: String; ACnt, ATotCnt: Integer; ACurPos, ACurSize, ATotPos, ATotSize: Int64;
    AElapsed, ARemaining, ASpeed: LongInt) of object;
  TOnZipError = procedure(Sender: TObject; APackageName: String; const AErrMsg: String) of object;
  TOnZipCompleted = TNotifyEvent;

  { TPackageUnzipper }

  TPackageUnzipper = class(TThread)
  private
    FSrcDir: String;
    FDstDir: String;
    FStarted: Boolean;
    FNeedToBreak: Boolean;
    FZipFile: String;
    FCnt: Integer;
    FTotCnt: Integer;
    FCurPos: Int64;
    FCurSize: Int64;
    FTotPos: Int64;
    FTotPosTmp: Int64;
    FTotSize: Int64;
    FElapsed: QWord;
    FStartTime: QWord;
    FRemaining: Integer;
    FSpeed: Integer;
    FErrMsg: String;
    FIsUpdate: Boolean;
    FUnZipper: TUnZipper;
    FOnZipProgress: TOnZipProgress;
    FOnZipError: TOnZipError;
    FOnZipCompleted: TOnZipCompleted;
    procedure DoOnProgressEx(Sender : TObject; const ATotPos, {%H-}ATotSize: Int64);
    procedure DoOnZipProgress;
    procedure DoOnZipError;
    procedure DoOnZipCompleted;
    function GetZipSize(var AIsDirZipped: Boolean; var ABaseDir: String): Int64;
  protected
    procedure Execute; override;
  public
    constructor Create;
    destructor Destroy; override;
    procedure StartUnZip(const ASrcDir, ADstDir: String; const AIsUpdate: Boolean = False);
    procedure StopUnZip;
  published
    property OnZipProgress: TOnZipProgress read FOnZipProgress write FOnZipProgress;
    property OnZipError: TOnZipError read FOnZipError write FOnZipError;
    property OnZipCompleted: TOnZipCompleted read FOnZipCompleted write FOnZipCompleted;
  end;

  { TPackageZipper }

  TPackageZipper = class(TThread)
  private
    FZipper: TZipper;
    FSrcDir: String;
    FZipFile: String;
    FStarted: Boolean;
    FNeedToBreak: Boolean;
    FErrMsg: String;
    FOnZipError: TOnZipError;
    FOnZipCompleted: TOnZipCompleted;
    procedure DoOnZipError;
    procedure DoOnZipCompleted;
  protected
    procedure Execute; override;
  public
    constructor Create;
    destructor Destroy; override;
    procedure StartZip(const ASrcDir, AZipFile: String);
    procedure StopZip;
  published
    property OnZipError: TOnZipError read FOnZipError write FOnZipError;
    property OnZipCompleted: TOnZipCompleted read FOnZipCompleted write FOnZipCompleted;
  end;

var
  PackageUnzipper: TPackageUnzipper = nil;

implementation

{ TPackageUnZipper }

procedure TPackageUnzipper.DoOnZipProgress;
begin
  if Assigned(FOnZipProgress) then
    FOnZipProgress(Self, FZipfile, FCnt, FTotCnt, FCurPos, FCurSize, FTotPosTmp, FTotSize, FElapsed, FRemaining, FSpeed);
end;

procedure TPackageUnzipper.DoOnZipError;
begin
  if Assigned(FOnZipError) then
    FOnZipError(Self, FZipFile, FErrMsg);
end;

procedure TPackageUnzipper.DoOnZipCompleted;
begin
  if Assigned(FOnZipCompleted) then
    FOnZipCompleted(Self);
end;

procedure TPackageUnzipper.Execute;
var
  I: Integer;
  DelDir: String;
  MPkg: TMetaPackage;
begin
  Sleep(50);
  FCnt := 0;
  FStartTime := GetTickCount64;
  for I := 0 to SerializablePackages.Count - 1 do
  begin
    MPkg := SerializablePackages.Items[I];
    if MPkg.IsExtractable then
    begin
      if FNeedToBreak then
        Break;
      Inc(FCnt);
      FCurPos := 0;
      FZipFile := MPkg.RepositoryFileName;
      DelDir := FDstDir + MPkg.PackageBaseDir;
      if DirectoryExists(DelDir) then
        CleanDirectory(DelDir);
      try
        FUnZipper.Clear;
        FUnZipper.FileName := FSrcDir + MPkg.RepositoryFileName;
        if MPkg.IsDirZipped then
          FUnZipper.OutputPath := FDstDir
        else
          FUnZipper.OutputPath := FDstDir + MPkg.PackageBaseDir;
        FUnZipper.OnProgressEx := @DoOnProgressEx;
        FUnZipper.Examine;
        FUnZipper.UnZipAllFiles;
        MPkg.ChangePackageStates(ctAdd, psExtracted);
        if (MPkg.IsDirZipped )
        and (CompareText(MPkg.PackageBaseDir, MPkg.ZippedBaseDir) <> 0) then
        begin
          CopyDirTree(FUnZipper.OutputPath + MPkg.ZippedBaseDir, DelDir, [cffOverwriteFile]);
          CleanDirectory(FUnZipper.OutputPath + MPkg.ZippedBaseDir);
        end;
        Synchronize(@DoOnZipProgress);
        FTotPos := FTotPos + FCurSize;
      except
        on E: Exception do
        begin
          FErrMsg := E.Message;
          MPkg.ChangePackageStates(ctRemove, psExtracted);
          MPkg.ChangePackageStates(ctAdd, psError);
          CleanDirectory(DelDir);
          Synchronize(@DoOnZipError);
        end;
      end;
    end;
  end;
  if (FNeedToBreak) then
  begin
    if DirectoryExists(DelDir) then
      CleanDirectory(DelDir);
  end
  else
  begin
    SerializablePackages.MarkRuntimePackages;
    Synchronize(@DoOnZipCompleted);
  end;
end;

constructor TPackageUnzipper.Create;
begin
  inherited Create(True);
  FreeOnTerminate := True;
  FUnZipper := TUnZipper.Create;
end;

destructor TPackageUnzipper.Destroy;
begin
  FUnZipper.Free;
  inherited Destroy;
end;

procedure TPackageUnzipper.DoOnProgressEx(Sender : TObject; const ATotPos, ATotSize: Int64);
begin
  FElapsed := GetTickCount64 - FStartTime;
  if FElapsed < 1000 then
    Exit;
  FElapsed := FElapsed div 1000;

  FCurPos := ATotPos;
  FCurSize := ATotSize;
  FTotPosTmp := FTotPos + FCurPos;
  FSpeed := Round(FTotPosTmp/FElapsed);
  if FSpeed > 0 then
    FRemaining := Round((FTotSize - FTotPosTmp)/FSpeed);
  Synchronize(@DoOnZipProgress);
  Sleep(5);
end;

function TPackageUnzipper.GetZipSize(var AIsDirZipped: Boolean; var ABaseDir: String): Int64;
var
  I: Integer;
  Item: TFullZipFileEntry;
  AllFiles: Boolean;
  P: Integer;
begin
  FUnZipper.Examine;
  AllFiles := (FUnZipper.Files.Count = 0);
  Result := 0;
  if FUnZipper.Entries.Count > 0 then
  begin
    P := Pos('/', TZipFileEntry(FUnZipper.Entries.Items[0]).ArchiveFileName);
    if P = 0 then
      P := Pos('\', TZipFileEntry(FUnZipper.Entries.Items[0]).ArchiveFileName);
    if P <> 0 then
      ABaseDir := Copy(TZipFileEntry(FUnZipper.Entries.Items[0]).ArchiveFileName, 1, P);
  end;
  for I := 0 to FUnZipper.Entries.Count-1 do
  begin
      Item := FUnZipper.Entries[i];
      if AllFiles or (FUnZipper.Files.IndexOf(Item.ArchiveFileName)<>-1) then
      begin
        Result := Result + TZipFileEntry(Item).Size;
        if AIsDirZipped then
          if Pos(ABaseDir, Item.ArchiveFileName) = 0 then
            AIsDirZipped := False;
      end;
  end;
  if not AIsDirZipped then
    ABaseDir := ''
  else
    SetLength(ABaseDir, Length(ABaseDir)-1);
end;


procedure TPackageUnzipper.StartUnZip(const ASrcDir, ADstDir: String;
  const AIsUpdate: Boolean);
var
  I: Integer;
  IsDirZipped: Boolean;
  BaseDir: String;
begin
  if FStarted then
    Exit;
  FDstDir := ADstDir;
  FSrcDir := ASrcDir;
  FTotCnt := 0;
  FTotSize := 0;
  FIsUpdate := AIsUpdate;
  for I := 0 to SerializablePackages.Count - 1 do
  begin
    if SerializablePackages.Items[I].IsExtractable then
    begin
      try
        FUnZipper.Clear;
        FUnZipper.FileName := FSrcDir + SerializablePackages.Items[I].RepositoryFileName;
        FUnZipper.Examine;
        IsDirZipped := True;
        BaseDir := '';
        FTotSize := FTotSize + GetZipSize(IsDirZipped, BaseDir);
        SerializablePackages.Items[I].IsDirZipped := IsDirZipped;
        if BaseDir <> '' then
          BaseDir := AppendPathDelim(BaseDir);
        SerializablePackages.Items[I].ZippedBaseDir := BaseDir;
        Inc(FTotCnt);
      except
        on E: Exception do
        begin
          FZipFile := SerializablePackages.Items[I].RepositoryFileName;
          FErrMsg := E.Message;
          Synchronize(@DoOnZipError);
        end;
      end
    end;
  end;
  FStarted := True;
  Start;
end;

procedure TPackageUnzipper.StopUnZip;
begin
  if Assigned(FUnZipper) then
    FUnZipper.Terminate;
  FNeedToBreak := True;
  FStarted := False;
end;


{ TPackageZipper }

procedure TPackageZipper.DoOnZipError;
begin
  if Assigned(FOnZipError) then
    FOnZipError(Self, FZipFile, FErrMsg);
end;

procedure TPackageZipper.DoOnZipCompleted;
begin
  if Assigned(FOnZipCompleted) then
    FOnZipCompleted(Self);
end;

procedure TPackageZipper.Execute;
var
  PathEntry: String;
  ZipFileEntries: TZipFileEntries;
  FileList: TStringList;
  SrcDir: String;
  P, I: Integer;
  CanGo: Boolean;
begin
  CanGo := True;
  FZipper.Filename := FZipFile;
  SrcDir := FSrcDir;
  FZipper.Clear;
  ZipFileEntries := TZipFileEntries.Create(TZipFileEntry);
  try
    if DirPathExists(SrcDir) then
    begin
      P := RPos(PathDelim, ChompPathDelim(SrcDir));
      PathEntry := LeftStr(SrcDir, P);
      FileList := TStringList.Create;
      try
        FindAllFilesEx(SrcDir, FileList);
        for I := 0 to FileList.Count - 1 do
          ZipFileEntries.AddFileEntry(FileList[I], CreateRelativePath(FileList[I], PathEntry));
      finally
        FileList.Free;
      end;

      if (ZipFileEntries.Count > 0) then
      begin
        try
          FZipper.ZipFiles(ZipFileEntries);
        except
          on E: EZipError do
          begin
            CanGo := False;
            FErrMsg := E.Message;
            Synchronize(@DoOnZipError);
          end;
        end;
      end;
    end;
  finally
    ZipFileEntries.Free;
  end;
  if CanGo then
    Synchronize(@DoOnZipCompleted);
end;

constructor TPackageZipper.Create;
begin
  inherited Create(True);
  FreeOnTerminate := True;
  FZipper := TZipper.Create;
end;

destructor TPackageZipper.Destroy;
begin
  FZipper.Free;
  inherited Destroy;
end;

procedure TPackageZipper.StartZip(const ASrcDir, AZipFile: String);
begin
  if FStarted then
    Exit;
  FSrcDir := ASrcDir;
  FZipFile := AZipFile;
  FStarted := True;
  Start;
end;

procedure TPackageZipper.StopZip;
begin
  if Assigned(FZipper) then
    FZipper.Terminate;
  FNeedToBreak := True;
  FStarted := False;
end;

end.

