{
********************************************************************************

SPackGui
Copyright (C) 2012-2013 Geoffray Levasseur <geoffray.levasseurbrandin@numericable.fr>.
Copyright (C) <date> <add your name and mail address here>

Parts inspired from AstroSurf souce code:
  Copyright (C) 2006 Patrick Chevalley <pch@freesurf.ch>

http://www.geoffray-levasseur.org/
http://0.tuxfamilly.org/

This software is governed by the CeCILL license under French law and
abiding by the rules of distribution of free software.  You can  use,
modify and/ or redistribute the software under the terms of the CeCILL
license as circulated by CEA, CNRS and INRIA at the following URL
"http://www.cecill.info".

As a counterpart to the access to the source code and  rights to copy,
modify and redistribute granted by the license, users are provided only
with a limited warranty  and the software's author,  the holder of the
economic rights,  and the successive licensors  have only  limited
liability.

In this respect, the user's attention is drawn to the risks associated
with loading,  using,  modifying and/or developing or reproducing the
software by the user in light of its specific status of free software,
that may mean  that it is complicated to manipulate,  and  that  also
therefore means  that it is reserved for developers  and  experienced
professionals having in-depth computer knowledge. Users are therefore
encouraged to load and test the software's suitability as regards their
requirements in conditions enabling the security of their systems and/or
data to be ensured and,  more generally, to use and operate it in the
same conditions as regards security.

The fact that you are presently reading this means that you have had
knowledge of the CeCILL license and that you accept its terms.

********************************************************************************

Description:
  simple downloader class

********************************************************************************
}
unit uDownload;

{$include defines.inc}

interface

uses
  Classes, SysUtils, HTTPsend, FTPSend, blcksock;

type
  TDownloadProtocol = (prHttp, prFtp);
  TDownloadProc = procedure of object;
  TDownloadFeedback = procedure (Txt: string) of object;
  TDownloadResult = (drNone, drDownloading, drError, drDone,
    drCanceled, drPaused);

  TDownloader = class(TObject)
  private
    FDownloadFeedback: TDownloadFeedback;
    FProtocol: TDownloadProtocol;
    FSockReadCount, FSockWriteCount: Integer;
    FLastRead, FLastWrite: integer;
    FUrl, FFirstUrl: string;
    FDwlOk: Boolean;
    FHttp: THTTPSend;
    FFtp: TFTPSend;
    FResponse: string;
    FFile: string;
    FProxy, FProxyPort, FProxyUser, FProxyPass: string;
    FSocksProxy, FSocksType: string;
    FFWMode: Integer;
    FFWPassive: Boolean;
    FUserName, FPassword: string;
    FFWhost, FFWport, FFWUsername, FFWPassword: string;
    FFileSize: LongInt;
    FStatus: string;
    FFtpDir, FFtpFile: string;
    FState: TDownloadResult;
    FPaused: Boolean;
    FProgressText: string;
    FOnDownloadComplete: TDownloadProc;
    FOnProgress: TDownloadProc;
    function InitHttp(const GetSize: Boolean): Boolean;
    function InitFtp(const GetSize: Boolean): Boolean;
    function GetDownloadedSize: LongInt;
    procedure SetUrl(const Value: string);
    procedure DoDownload;
    procedure SockStatus(Sender: TObject; Reason: THookSocketReason;
      const Value: string);
    procedure FTPStatus(Sender: TObject; Response: Boolean;
      const Value: string);
  protected
    procedure HTTPComplete;
    procedure FTPComplete;
    procedure ProgressReport;
  public
    constructor Create;
    destructor Destroy; override;
    function StartDownload: Boolean;
    procedure Cancel;
    procedure Pause;
    property URL: string read FUrl write SetUrl;
    property SaveToFile: string read FFile write FFile;
    property ResponseText: string read FResponse;
    property HttpProxy: string read FProxy write FProxy ;
    property HttpProxyPort: string read FProxyPort write FProxyPort;
    property HttpProxyUser: string read FProxyUser write FProxyUser;
    property HttpProxyPass: string read FProxyPass write FProxyPass;
    property SocksProxy: string read FSocksproxy write FSocksProxy;
    property SocksType: string read FSocksType write FSocksType;
    property FtpUserName: string read FUserName write FUserName;
    property FtpPassword: string read FPassword  write FPassword;
    property FtpFwMode: integer read FFWMode write FFWMode;
    property FtpFwPassive: Boolean read FFWpassive write FFWpassive;
    property FtpFwHost: string read FFWhost write FFWhost;
    property FtpFwPort: string read FFWport write FFWport;
    property FtpFwUserName: string read FFWUsername write FFWUsername;
    property FtpFwPassword: string read FFWPassword write FFWPassword;
    property FileSize: LongInt read FFileSize;
    property Status: string read FStatus;
    property DownloadedSize: LongInt read GetDownloadedSize;
    property State: TDownloadResult read FState;
    property OnFeedback: TDownloadFeedback read FDownloadFeedback
      write FDownloadFeedback;
    property OnDownloadComplete: TDownloadProc read FOnDownloadComplete
      write FOnDownloadComplete;
    property OnProgress: TDownloadProc read FOnProgress write FOnProgress;
  end;

implementation

uses
  uDebug, uCommon, uStrings, Forms;


procedure TDownloader.DoDownload;
begin
  PrintLnDbg(rsDebugDownloadStarted);
  try
    FSockReadCount := 0;
    FSockWriteCount := 0;
    FLastRead := 0;
    FLastWrite := 0;
    if FProtocol = prHttp then
    begin
      FHttp.Sock.OnStatus := @SockStatus;
      FDwlOk := FHttp.HTTPMethod('GET', FUrl);
    end;
    if FProtocol = prFtp then
    begin
      FFtp.OnStatus := @FTPStatus;
      if FFtp.Login then
      begin
        FFtp.ChangeWorkingDir(FFtpDir);
        FDwlOk := FFtp.RetrieveFile(FFtpFile, False);
      end;
    end;
    if Assigned(FOnDownloadComplete) then
      FOnDownloadComplete;
  except
    if FProtocol = prHttp then
      PrintLnDbg(Format(rsErrorDownloadingFailed, [FUrl]), vlLow);
    if FProtocol = prFtp then
      PrintLnDbg(Format(rsErrorDownloadingFailed, [FFtpFile]), vlLow);
    FState := drError;
  end;
  PrintLnDbg(rsDebugDownloadFinished);
end;


procedure TDownloader.SockStatus(Sender: TObject;
  Reason: THookSocketReason; const Value: string);
var
  ReasonTxt: string;
begin
  ReasonTxt := '';
  case Reason of
    HR_ResolvingBegin: ReasonTxt := rsNetStatResolving + Value;
    HR_Connect: ReasonTxt := rsNetStatConnect + Value;
    HR_Accept: ReasonTxt := rsNetStatAccept + Value;
    HR_ReadCount: begin
      FSockReadCount := FSockReadCount + StrToInt(Value);
      if (FSockReadCount - FLastRead) > 100000 then
      begin
        ReasonTxt := rsNetStatReadBytes + IntToStr(FSockReadCount);
        FLastRead := FSockReadCount;
      end;
    end;
    HR_WriteCount: begin
      FSockWriteCount := FSockWriteCount + StrToInt(Value);
      ReasonTxt := rsNetStatRequesting;
    end;
  else
    ReasonTxt := '';
  end;
  if (ReasonTxt > '') and assigned(FOnProgress) then
  begin
    FProgressText := ReasonTxt;
    FOnProgress;
  end;
end;


procedure TDownloader.FTPStatus(Sender: TObject; Response: Boolean;
  const Value: string);
begin
  if Response and Assigned(FOnProgress) then
  begin
    FProgressText := Value;
    FOnProgress;
  end;
end;


function TDownloader.InitHttp(const GetSize: Boolean): Boolean;
var
  I, N: integer;
const
  CL = 'CONTENT-LENGTH';
begin
  Result := True;
  try
    FResponse := '';
    FHttp.Clear;
    FHttp.Sock.SocksIP := '';
    FHttp.ProxyHost := '';
    if FSocksProxy <> '' then
    begin
      FHttp.Sock.SocksIP := FSocksProxy;
      if FProxyPort <> '' then
        FHttp.Sock.SocksPort := FProxyPort;
      if FSocksType = 'Socks4' then
        FHttp.Sock.SocksType := ST_Socks4
      else
        FHttp.Sock.SocksType := ST_Socks5;
      if FProxyUser <> '' then
        FHttp.Sock.SocksUsername := FProxyUser;
      if FProxyPass <> '' then
        FHttp.Sock.SocksPassword := FProxyPass;
    end else
      if FProxy <> '' then
      begin
        FHttp.ProxyHost := FProxy;
        if FProxyPort <> '' then
          FHttp.ProxyPort := FProxyPort;
        if FProxyUser <> '' then
          FHttp.ProxyUser := FProxyUser;
        if FProxyPass <> '' then
          FHttp.ProxyPass := FProxyPass;
      end;
    if GetSize then
      if FHttp.HTTPMethod('HEAD', FUrl) then
      try
        for I := 0 to FHttp.Headers.Count - 1 do
        begin
          N := Pos(CL, UpperCase(FHttp.Headers[I]));
          if N <> 0 then
          begin
            FFileSize := StrToInt(Trim(Copy(FHttp.Headers[I],
              N + Length(CL) + 1,
              Length(FHttp.Headers[I]) - Length(CL) - N)));
            PrintLnDbg(Format(rsInfoInitializedDownload, [FUrl,
              DispSize(FFileSize)]), vlLow);
            Break;
          end;
        end;
      except
        FFileSize := -1;
      end else //can't get the filesize from the server
        FFileSize := -1;
  except
    Result := False;
    FState := drError;
  end;
end;


function TDownloader.InitFtp(const GetSize: Boolean): Boolean;
var
  Buf: string;
  I: integer;
begin
  Result := True;
  try
    FResponse := '';
    I := Pos('://', FUrl);
    Buf := Copy(Furl, I + 3, Length(FUrl) - I - 2);
    I := Pos('/', Buf);
    FFtp.TargetHost := Copy(Buf, 1, I - 1);
    FFtp.PassiveMode := FFWpassive;
    if FUserName <> '' then
      FFtp.UserName := FUserName
    else
      FFtp.UserName := 'anonymous';
    FFtp.Password := FPassword;
    FFtp.FWMode := FFWMode;
    if FFWhost <> '' then
      FFtp.FWHost := FFWHost;
    if FFWport <> '' then
      FFtp.FWPort := FFWPort;
    if FFWUsername <> '' then
      FFtp.FWUsername := FFWUsername;
    if FFWPassword <> '' then
      FFtp.FWPassword := FFWPassword;
    Buf := Copy(Buf, I, Length(Buf) - I + 1);
    I := LastDelimiter('/', Buf);
    FFtpDir := Copy(Buf, 1, I);
    FFtpFile := Copy(Buf, I + 1, Length(Buf) - I);
    FFtp.DirectFile := True;
    FFtp.DirectFileName := FFile;
    if GetSize then
    begin
      if FFtp.Login then
      begin
        FFtp.ChangeWorkingDir(FFtpDir);
        FFileSize := FFtp.FileSize(FFtpFile);
        FFtp.Logout;
      end else
        FFileSize := -1;
      PrintLnDbg(Format(rsInfoInitializedDownload, [FUrl,
        DispSize(FFileSize)]), vlLow);
    end;
  except
    Result := False;
    FState := drError;
  end;
end;


function TDownloader.GetDownloadedSize: Longint;
begin
  Result := 0;
  case FProtocol of
    prHttp: Result := FHttp.Sock.RecvCounter;
    prFtp: Result := FFtp.DSock.RecvCounter;
  end
end;


procedure TDownloader.SetUrl(const Value: string);
begin
  FUrl := Value;
  if Trim(FUrl) <> '' then
    if Copy(FUrl, 1, 7) = sHttpPrefix then
      InitHttp(True)
    else
      if Copy(Furl, 1, 6) = sFtpPrefix then {$warning What about sftp ?}
        InitFtp(True);
end;


constructor TDownloader.Create;
begin
  inherited Create;
  FHttp := THTTPSend.Create;
  FFtp := TFTPSend.Create;
  FProxy := '';
  FSocksProxy := '';
  FFWMode := 0;
  FFWpassive := True;
  FStatus := '';
  FResponse := '';
  FState := drNone;
  FUserName := '';
  FPassword := '';
  FPaused := False;
  FDwlOk := False;
end;


destructor TDownloader.Destroy;
begin
  FHttp.Free;
  FFtp.Free;
  inherited Destroy;
end;


procedure TDownloader.Cancel;
begin
  if FProtocol = prHttp then
  begin
    FHttp.Sock.OnStatus := nil;
    FHttp.Abort;
  end;
  if FProtocol = prFtp then
  begin
    FFtp.Sock.OnStatus := nil;
    FFtp.OnStatus := nil;
    FFtp.Abort;
  end;
  FState := drCanceled;
  FOnProgress := nil;
  FOnDownloadComplete := nil;
end;


procedure TDownloader.Pause;
begin
  if (FState <> drDownloading) and (FState <> drPaused)  then
    Exit;
{  if FPaused then
  begin
    FDownloadDaemon.Resume;
    FPaused := False;
  end else
  begin
    FDownloadDaemon.Suspend;
    FPaused := True;
  end;
  if FPaused then
    FState := drPaused
  else
    FState := drDownloading;} {$warning pause system need complete rewrite}
end;


function TDownloader.StartDownload: Boolean;
begin
  Result := True;
  if Trim(FUrl) = '' then
    raise Exception.Create(rsExceptEmptyAddress);
  if Copy(FUrl, 1, 7) = sHttpPrefix then
  begin        // HTTP protocol
    if InitHttp(False) then
    try
      FProtocol := prHttp;
      FOnProgress := @ProgressReport;
      FOnDownloadComplete := @HTTPComplete;
      FState := drDownloading;
      DoDownload;
    except
      FState := drError;
      Result := False;
    end else
    begin
      FState := drError;
      Result := False;
    end;
  end else
  begin        // FTP protocol
    if Copy(Furl, 1, 6) <> sFtpPrefix then
    begin
      FState := drError;
      Result := False;
      raise Exception.Create(rsExceptUnsupportedProtocol);
    end;
    if InitFtp(False) then
    try
      FProtocol := prFtp;
      FOnProgress := @ProgressReport;
      FOnDownloadComplete := @FTPComplete;
      FState := drDownloading;
      DoDownload;
    except
      FState := drError;
      Result := False;
    end else
    begin
      FState := drError;
      Result := False;
    end;
  end;
end;


procedure TDownloader.HTTPComplete;
var
  Ok: Boolean;
  I: Integer;
  NewUrl: string;
begin
  Ok := FDwlOk;
  if Ok and (((FHttp.ResultCode >= 100) and (FHttp.ResultCode <= 299)) or
    (FHttp.ResultCode = 0)) then
  begin  // success
    FHttp.Document.Position := 0;
    FHttp.Document.SaveToFile(FFile);
    FResponse := 'Finished: ' + FStatus;
    if Assigned(FDownloadFeedback) then
      FDownloadFeedback(FResponse);
    FHttp.Clear;
    if Assigned(FDownloadFeedback) then
      FDownloadFeedback(FStatus);
    FState := drDone;
  end else
    if (FHttp.ResultCode = 301) or (FHttp.ResultCode = 302) or
      (FHttp.ResultCode = 307) then
    begin
      for I := 0 to FHttp.Headers.Count - 1 do
      begin
        if UpperCase(Copy(FHttp.Headers[I], 1, 9)) = 'LOCATION:' then
        begin
          NewUrl := Trim(Copy(FHttp.Headers[I], 10, 9999));
          if (NewUrl = FUrl) or (NewUrl = FFirstUrl) then
          begin
            Ok := False;
            FState := drError;
          end else
          begin
            FStatus := 'Redirect to: ' + NewUrl;
            if Assigned(FDownloadFeedback) then
              FDownloadFeedback(FStatus);
            FUrl := NewUrl;
            StartDownload;
            Exit;
          end;
        end;
      end;
      Ok := False;
    end else
    begin // error
      Ok := False;
      if  FHttp.ResultCode = 0 then
        FResponse := 'Finished: '+ FStatus +' / Error: Timeout '+
          FHttp.ResultString
      else
        FResponse := 'Finished: '+ FStatus + ' / Error: '+
          IntToStr(FHttp.ResultCode) + ' ' + FHttp.ResultString;
      FStatus := FResponse;
      FHttp.Clear;
      if FState <> drCanceled then
        FState := drError;
    end;
end;


procedure TDownloader.FTPComplete;
var
  Ok: Boolean;
begin
  Ok := FDwlOk;
  FResponse := FStatus;
  if Ok then
  begin
    FFtp.Sock.OnStatus := nil;
    FFtp.OnStatus := nil;
    FFtp.Logout;
    FState := drDone;
  end else
  begin
    FFtp.Sock.OnStatus := nil;
    FFtp.OnStatus := nil;
    FFtp.Abort;
    FStatus := FResponse;
    if FState <> drCanceled then
      FState := drError;
  end;
end;


procedure TDownloader.ProgressReport;
begin
  FStatus := FProgressText;
  if Assigned(FDownloadFeedback) then
    FDownloadFeedback(FStatus);
end;


initialization
end.
