Использование WinInet для определения общего размера файла перед его загрузкой

Я получил приведенный ниже источник со стороннего сайта, объясняющий, как загрузить файл из Интернета с помощью WinInet. Я не слишком знаком с API, и я взглянул на модуль WinInet, но не увидел вызовов API, которые мне нужны.

Что я делаю, так это добавляю возможность сообщать о ходе загрузки файла. Эту процедуру я уже завернул в TThread, и все работает нормально. Однако недостает только одного: определение общего размера исходного файла перед загрузкой.

См. ниже, где у меня есть комментарий //HOW TO GET TOTAL SIZE? Здесь мне нужно узнать, каков общий размер файла, прежде чем я начну его загрузку. Как мне это сделать? Потому что этот код, кажется, не знает размер файла, пока он не будет загружен, и это делает это дополнение неуместным.

procedure TInetThread.Execute;
const
  BufferSize = 1024;
var
  hSession, hURL: HInternet;
  Buffer: array[1..BufferSize] of Byte;
  BufferLen: DWORD;
  f: File;
  S: Bool;
  D: Integer;
  T: Integer;
  procedure DoWork(const Amt: Integer);
  begin
    if assigned(FOnWork) then
      FOnWork(Self, FSource, FDest, Amt, T);
  end;
begin
  S:= False;
  try
    try
      if not DirectoryExists(ExtractFilePath(FDest)) then begin
        ForceDirectories(ExtractFilePath(FDest));
      end;
      hSession:= InternetOpen(PChar(FAppName), INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);
      try
        hURL:= InternetOpenURL(hSession, PChar(FSource), nil, 0, 0, 0);
        try
          AssignFile(f, FDest);
          Rewrite(f, 1);
          T:= 0; //HOW TO GET TOTAL SIZE?
          D:= 0;
          DoWork(D);
          repeat
            InternetReadFile(hURL, @Buffer, SizeOf(Buffer), BufferLen);
            BlockWrite(f, Buffer, BufferLen);
            D:= D + BufferLen;
            DoWork(D);
          until BufferLen = 0;
          CloseFile(f);
          S:= True;
        finally
          InternetCloseHandle(hURL);
        end
      finally
        InternetCloseHandle(hSession);
      end;
    except
      on e: exception do begin
        S:= False;
      end;
    end;
  finally
    if assigned(FOnComplete) then
      FOnComplete(Self, FSource, FDest, S);
  end;
end;

person Jerry Dodge    schedule 06.02.2012    source источник
comment
Я реализовал именно такую ​​функцию и обнаружил, что использование WinInet вызывает в моем приложении ужасную ошибку тайм-аута. Запросы Http-Head, которые обычно занимают 100 мс, возвращались до 15 секунд. Это известная проблема при вызове WinInet из Delphi в некоторых версиях Windows/WinInet. Я упоминаю об этом на случай, если вы позже столкнетесь с такими странными глюками. Если вы можете использовать Indy здесь или что-то не-WinInet (например, WinHttp), рассмотрите это! :-)   -  person Warren P    schedule 07.02.2012
comment
..It is a known problem in calling WinInet from Delphi on some versions of Windows/WinInet @WarrenP У меня никогда не было этой проблемы при использовании WinInet из Delphi. Можете ли вы указать какую-нибудь документацию или ссылки по этой теме?   -  person RRUZ    schedule 07.02.2012
comment
Вот ссылка: jgobserve.blogspot.com/2009/ 03/ -- Мои наблюдения таковы, что проблемы не ограничиваются тем фактом, что при сбое базовой сети вы получаете долгое ожидание. Иногда все выглядит нормально, ЗА ИСКЛЮЧЕНИЕМ winInet, у которого есть тайм-ауты, которые я не могу иначе объяснить. Код, который я написал на Python или в Delphi с использованием INDY или ICS, НЕ демонстрирует такой же шаблон отказа.   -  person Warren P    schedule 07.02.2012
comment
Я опубликовал это полтора года назад, и, прочитав свой код, я понял, что я не защищал потоки событий с синхронизацией. Во всех своих последних потоках за последний год я тщательно разрабатываю критические разделы, но это было тогда, когда я не знал, как сделать что-либо потокобезопасным.   -  person Jerry Dodge    schedule 24.08.2013


Ответы (3)


Вы можете использовать метод HEAD и проверить Content-Length, чтобы получить размер файла удаленного файла.

Проверьте эти два метода

Виннет

Если вы хотите выполнить метод HEAD, вы должны использовать HttpOpenRequest., HttpSendRequest и HttpQueryInfo функции WinInet.

uses
 SysUtils,
 Windows,
 WinInet;

function GetWinInetError(ErrorCode:Cardinal): string;
const
   winetdll = 'wininet.dll';
var
  Len: Integer;
  Buffer: PChar;
begin
  Len := FormatMessage(
  FORMAT_MESSAGE_FROM_HMODULE or FORMAT_MESSAGE_FROM_SYSTEM or
  FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_IGNORE_INSERTS or  FORMAT_MESSAGE_ARGUMENT_ARRAY,
  Pointer(GetModuleHandle(winetdll)), ErrorCode, 0, @Buffer, SizeOf(Buffer), nil);
  try
    while (Len > 0) and {$IFDEF UNICODE}(CharInSet(Buffer[Len - 1], [#0..#32, '.'])) {$ELSE}(Buffer[Len - 1] in [#0..#32, '.']) {$ENDIF} do Dec(Len);
    SetString(Result, Buffer, Len);
  finally
    LocalFree(HLOCAL(Buffer));
  end;
end;


procedure ParseURL(const lpszUrl: string; var Host, Resource: string);
var
  lpszScheme      : array[0..INTERNET_MAX_SCHEME_LENGTH - 1] of Char;
  lpszHostName    : array[0..INTERNET_MAX_HOST_NAME_LENGTH - 1] of Char;
  lpszUserName    : array[0..INTERNET_MAX_USER_NAME_LENGTH - 1] of Char;
  lpszPassword    : array[0..INTERNET_MAX_PASSWORD_LENGTH - 1] of Char;
  lpszUrlPath     : array[0..INTERNET_MAX_PATH_LENGTH - 1] of Char;
  lpszExtraInfo   : array[0..1024 - 1] of Char;
  lpUrlComponents : TURLComponents;
begin
  ZeroMemory(@lpszScheme, SizeOf(lpszScheme));
  ZeroMemory(@lpszHostName, SizeOf(lpszHostName));
  ZeroMemory(@lpszUserName, SizeOf(lpszUserName));
  ZeroMemory(@lpszPassword, SizeOf(lpszPassword));
  ZeroMemory(@lpszUrlPath, SizeOf(lpszUrlPath));
  ZeroMemory(@lpszExtraInfo, SizeOf(lpszExtraInfo));
  ZeroMemory(@lpUrlComponents, SizeOf(TURLComponents));

  lpUrlComponents.dwStructSize      := SizeOf(TURLComponents);
  lpUrlComponents.lpszScheme        := lpszScheme;
  lpUrlComponents.dwSchemeLength    := SizeOf(lpszScheme);
  lpUrlComponents.lpszHostName      := lpszHostName;
  lpUrlComponents.dwHostNameLength  := SizeOf(lpszHostName);
  lpUrlComponents.lpszUserName      := lpszUserName;
  lpUrlComponents.dwUserNameLength  := SizeOf(lpszUserName);
  lpUrlComponents.lpszPassword      := lpszPassword;
  lpUrlComponents.dwPasswordLength  := SizeOf(lpszPassword);
  lpUrlComponents.lpszUrlPath       := lpszUrlPath;
  lpUrlComponents.dwUrlPathLength   := SizeOf(lpszUrlPath);
  lpUrlComponents.lpszExtraInfo     := lpszExtraInfo;
  lpUrlComponents.dwExtraInfoLength := SizeOf(lpszExtraInfo);

  InternetCrackUrl(PChar(lpszUrl), Length(lpszUrl), ICU_DECODE or ICU_ESCAPE, lpUrlComponents);

  Host := lpszHostName;
  Resource := lpszUrlPath;
end;

function GetRemoteFileSize(const Url : string): Integer;
const
  sUserAgent = 'Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101';

var
  hInet    : HINTERNET;
  hConnect : HINTERNET;
  hRequest : HINTERNET;
  lpdwBufferLength: DWORD;
  lpdwReserved    : DWORD;
  ServerName: string;
  Resource: string;
  ErrorCode : Cardinal;
begin
  ParseURL(Url,ServerName,Resource);
  Result:=0;

  hInet := InternetOpen(PChar(sUserAgent), INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);
  if hInet=nil then
  begin
    ErrorCode:=GetLastError;
    raise Exception.Create(Format('InternetOpen Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
  end;

  try
    hConnect := InternetConnect(hInet, PChar(ServerName), INTERNET_DEFAULT_HTTP_PORT, nil, nil, INTERNET_SERVICE_HTTP, 0, 0);
    if hConnect=nil then
    begin
      ErrorCode:=GetLastError;
      raise Exception.Create(Format('InternetConnect Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
    end;

    try
      hRequest := HttpOpenRequest(hConnect, PChar('HEAD'), PChar(Resource), nil, nil, nil, 0, 0);
        if hRequest<>nil then
        begin
          try
            lpdwBufferLength:=SizeOf(Result);
            lpdwReserved    :=0;
            if not HttpSendRequest(hRequest, nil, 0, nil, 0) then
            begin
              ErrorCode:=GetLastError;
              raise Exception.Create(Format('HttpOpenRequest Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
            end;

             if not HttpQueryInfo(hRequest, HTTP_QUERY_CONTENT_LENGTH or HTTP_QUERY_FLAG_NUMBER, @Result, lpdwBufferLength, lpdwReserved) then
             begin
              Result:=0;
              ErrorCode:=GetLastError;
              raise Exception.Create(Format('HttpQueryInfo Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
             end;
          finally
            InternetCloseHandle(hRequest);
          end;
        end
        else
        begin
          ErrorCode:=GetLastError;
          raise Exception.Create(Format('HttpOpenRequest Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
        end;
    finally
      InternetCloseHandle(hConnect);
    end;
  finally
    InternetCloseHandle(hInet);
  end;

end;

Инди

Также проверьте этот код, используя indy.

function GetRemoteFilesize(const Url :string) : Integer;
var
  Http: TIdHTTP;
begin
  Http := TIdHTTP.Create(nil);
  try
    Http.Head(Url);
    result:= Http.Response.ContentLength;
  finally
    Http.Free;
  end;
end;
person RRUZ    schedule 06.02.2012
comment
+1 балл принят, вместо этого я должен использовать Indy :D Просто для чистого кода - person Jerry Dodge; 07.02.2012
comment
Если вы знаете, что все равно собираетесь загружать ресурс, не могли бы вы просто отправить запрос GET и вместо этого прочитать из него заголовок Content-Length? Это сэкономит вам дополнительное HTTP-соединение. - person Rob Kennedy; 07.02.2012
comment
@RobKennedy - да, вы можете, если данные не отправляются кусками с использованием заголовка Transfer-Encoding: chunked, и в этом случае заголовок Content-Length не используется, и нет возможности узнать общий размер, пока не будет последний кусок получила. - person Remy Lebeau; 07.02.2012

Отвечая на вопрос, как получить размер загрузки с помощью WinInet. Это из одного из моих загрузчиков файлов, который основан на WinInet.

Это метод, который я использую для получения размера загрузки:

function TWebDownloader.GetContentLength(URLHandle: HINTERNET): Int64;
// returns the expected download size.  Returns -1 if one not provided
   var
     SBuffer: Array[1..20] of char;
     SBufferSize: Integer;
     srv: integer;
   begin
     srv := 0;
    SBufferSize := 20;
    if HttpQueryInfo(URLHandle, HTTP_QUERY_CONTENT_LENGTH, @SBuffer, SBufferSize, srv) then
       Result := StrToFloat(String(SBuffer))
    else
       Result := -1;
   end;

Для использования этого метода требуется открытый дескриптор запроса и НЕ требуется чтение каких-либо данных:

 URLHandle := HttpOpenRequest(ConnectHandle, 'GET', Pchar(sitepath), nil,
                  nil, nil, INTERNET_FLAG_NO_CACHE_WRITE, 0);
 ...
 DownloadSize := GetContentLength(URLHandle);

ХТН

person Glenn1234    schedule 06.02.2012
comment
+1 Отличный материал, и 1/6 кода в качестве другого ответа :) кстати, как продвигается ваш огромный проект загрузчика? - person Jerry Dodge; 07.02.2012
comment
Мне очень любопытно, как это работает, потому что условия взаимоисключающие: а) метод GET б) нет передачи запрошенного ресурса. Я предполагаю, что он закрывает соединение, когда заголовки были получены. - person OnTheFly; 07.02.2012
comment
@JerryDodge этого достаточно для моих нужд, и я перешел к другим вещам. Тем не менее, он все еще нуждается в большой работе по очистке. - person Glenn1234; 03.03.2012

после исправления типов это выглядит лучше так:

function GetContentLength(URLHandle:HINTERNET):Int64;
// returns the expected download size.  Returns -1 if one not provided
var
 SBufferSize, srv:Cardinal;
begin
 srv:=0;
 SBufferSize:=20;
 if Not HttpQueryInfo(URLHandle, HTTP_QUERY_CONTENT_LENGTH or HTTP_QUERY_FLAG_NUMBER, {@SBuffer} @Result, SBufferSize, srv) then Result:=-1;
end;

назвать это:

{get the file handle}
hURL:=InternetOpenURL(hSession, PChar(URL), nil, 0, 0, 0);
if hURL=Nil then
begin
 InternetCloseHandle(hSession);
 ShowMessage('The link is incorrect!');
 exit;
end;
{get the file size}
filesize:=GetContentLength(hURL);
person Mario Green    schedule 20.09.2014