tags:

views:

260

answers:

6

I want to download a file from Internet and InternetReadFile seem a good and easy solution at the first glance. Actually, too good to be true. Indeed, digging a bit I have started to see that actually there are a lot of issues with it. People are complaining about all kinds of problems when using this code.

Problems could appear because:

  • the application freezes temporarily until the HTTP server responds
  • the application freezes temporarily because the Internet connections breaks
  • the application locks up because the HTTP server never responds
  • the InternetOpen (I just discovered this recently) MUST be called only once during application life time

I could not find a complete example about how to use it properly and robustly. Does anybody have an idea about how to implement it in a separate thread and with a time out? There is another SIMPLE way to robustly download a file from Internet. Though I don't want to complicate my life with very large libraries like Jedi or even Indy.

function GetFileHTTP (const fileURL, FileName: String): boolean;
CONST
  BufferSize = 1024;
VAR
  hSession, hURL: HInternet;
  Buffer: array[1..BufferSize] of Byte;
  BufferLen: DWORD;
  f: File;
  sAppName: string;
begin
//  result := false;
 sAppName := ExtractFileName(Application.ExeName) ;
 hSession := InternetOpen(PChar(sAppName), INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0) ;  { be aware that InternetOpen  need only be called once in your application!!!!!!!!!!!!!! }
 TRY
  hURL := InternetOpenURL(hSession, PChar(fileURL), nil, 0, 0, 0) ;
  TRY
   AssignFile(f, FileName) ;
   Rewrite(f, 1) ;
   REPEAT
    InternetReadFile(hURL, @Buffer, SizeOf(Buffer), BufferLen);
    BlockWrite(f, Buffer, BufferLen)
   UNTIL BufferLen = 0;
   CloseFile(f) ;
   Result:= True;
  FINALLY
   InternetCloseHandle(hURL)
  end
 FINALLY
  InternetCloseHandle(hSession)
 END;
END;

Edit: This functions checks if Internet connection is available. It seems to work on Win98 also.

{  Are we connected to the Internet? }
function IsConnectedToInternet: Boolean;                                        { Call SHELL32.DLL for Win < Win98 otherwise call URL.dll }
var InetIsOffline: function(dwFlags: DWORD): BOOL; stdcall;
begin
 Result:= FALSE;
 if IsApiFunctionAvailable('URL.DLL', 'InetIsOffline', @InetIsOffline)
 then Result:= NOT InetIsOffLine(0)
 else
   if IsApiFunctionAvailable('SHELL32.DLL', 'InetIsOffline', @InetIsOffline)
   then Result:= NOT InetIsOffLine(0)
end;

I am using Delphi 7. Many thanks.


Edit:

Losing customers because the application hangs at the first start up is the perfect recipe for losing money.

Writing your code to be Microsoft platform dependent is bad. You never know if the customer has the IE version x.x installed.

Installing stuff into a user's computer is like playing with guns. It will backfire.

(see more about this here: http://thesunstroke.blogspot.com/2010/06/programmig-like-there-is-no-ms-windows.html)

+3  A: 

I basically do the same as you do. For me it works fairly flawlessly.

The only differences between my code and your code is I have an INTERNET_FLAG_RELOAD parameter to force a download from the file and not the cache. You can try that and see if it works better:

  hURL := InternetOpenURL(hSession, PChar(fileURL), nil, 0, INTERNET_FLAG_RELOAD, 0) ; 

Also check for an internet connection before downloading. Do this:

  dwConnectionTypes := INTERNET_CONNECTION_MODEM
                 + INTERNET_CONNECTION_LAN
                 + INTERNET_CONNECTION_PROXY;
  InternetConnected := InternetGetConnectedState(@dwConnectionTypes, 0);
  if InternetConnected then ...
lkessler
For me it works fairly flawlessly - ONLY if the server has no problems. Try to download a large file (1MB) and unplug your network cable. You will see.
Altar
Many thanks for "INTERNET_FLAG_RELOAD"
Altar
Well, all my users use it in my program successfully for a 4.5 MB file. I guess none of them ever unplug their network cable while downloading. But do make sure you check for an internet connection before attempting the download. I've added this to my answer.
lkessler
Unplugging the cable simulates all kind of problems such as ISP down or web server down. It is still a valid test. I have try it and it freezes the application :) Also see my "check if Internet available" function. It seems to work on Win98 also.
Altar
+2  A: 

Here's some sample code that uses Indy. This code is for Delphi 2010 (with Indy 10?), but the code for Delphi 7 would be similar. I've used Indy for years with D7 and have been very happy with it. I think in D7 we use Indy 9. Check if you need to download a new version...

You can use OnWork and OnWorkBegin to add a progress meter if you need to.

This code I excerpted from a bigger piece, editing it a bit. I did not try compiling it, but it will give you a good starting place.

function Download( const aSourceURL: String;
                   const aDestFileName: String;
                   out   aDownloadResult: TDownloadResult;
                   out   aErrm: String): boolean;
var
  Stream: TMemoryStream;
  IDAntiFreeze: TIDAntiFreeze;
begin
  aDownloadResult := DROther;
  Result := FALSE;
  fIDHTTP := TIDHTTP.Create;
  fIDHTTP.HandleRedirects := TRUE;
  fIDHTTP.AllowCookies := FALSE;
  fIDHTTP.Request.UserAgent := 'Mozilla/4.0';
  fIDHTTP.Request.Connection := 'Keep-Alive';
  fIDHTTP.Request.ProxyConnection := 'Keep-Alive';
  fIDHTTP.Request.CacheControl := 'no-cache';
  IDAntiFreeze := TIDAntiFreeze.Create;

  Stream := TMemoryStream.Create;
  try
    try
      fIDHTTP.Get(aSourceURL, Stream);
      if FileExists(aDestFileName) then
        DeleteFile(PWideChar(aDestFileName));
      Stream.SaveToFile(aDestFileName);
      Result := TRUE;
      aDownloadResult :=drSuccess;
    except
      On E: Exception do
        begin
          Result := FALSE;
          aErrm := E.Message + ' (' + IntToStr(fIDHTTP.ResponseCode) + ')';
        end;
    end;
  finally
    Stream.Free;
    IDAntiFreeze.Free;
    fIDHTTP.Free;
  end;
end;  { Download }
Tom1952
Hi. My code depends on IE. Do you know if your code has any dependencies?
Altar
@Altar: It is rather unlikely (read: impossible) to encounter a Microsoft Windows system that lacks the Internet Explorer system files.
Andreas Rejbrand
"read: impossible" - You are so wrong! There are quite many Win OS's without IE. Especially in Europe. Also, a simple Google search for "internet explorer uninstall" returns almost 4 mil results. I guess more than 4 mil users read those websites/forums about how to uninstall IE.
Altar
@Altar: While the IE application itself may not be there, that doesn't mean some or all the underlying engine isn't there. It is a fundamental part of the OS, and applications depend on it. If a user has truly removed every trace of the underlying stuff, including WinInet.dll (which InternetReadFile depends on) and urlmon.dll (which TDownloadURL uses), then software is pretty much guaranteed to fail. Even Google Chrome loads those DLLs, *both* of them.
Michael Madsen
"With Windows 7, Microsoft added the ability to safely remove Internet Explorer 8 from Windows.[99] Microsoft does not allow the dependencies to be removed through this process, but the Internet Explorer executable (iexplore.exe) is removed without harming any other Windows components" - It looks that you are right. It seems that MS guaranty the existence of those libraries.
Altar
Not working. Application's GUI is not freezing indeed. But you cannot close the application from the 'x' button - so it is kinda frozen. You have to use Ctrl+Alt+Del to kill it. So, Indy in not better than the other solutions proposed.
Altar
A: 

Instead of fiddling with the WinAPI, the ExtActns unit provides just what you need for downloading to a file.

procedure TMainForm.DownloadFile(URL: string; Dest: string); 
var 
  dl: TDownloadURL; 
begin 
  dl := TDownloadURL.Create(self); 
  try 
    dl.URL := URL; 
    dl.FileName := Dest; 
    dl.ExecuteTarget(nil); //this downloads the file 
    dl.Free; 
  except 
    dl.Free; 
  end; 
end; 

Under the hood, it uses URLDownloadToFile from the URLMon library - which is part of IE, and therefore part of Windows.

TDownloadURL doesn't handle any timeout for you - it doesn't look like such a thing is supported in URLMon at all, although there could be some default timeout that causes the call to fail - but you could use the OnProgress event on TDownloadURL to get notified when something happens, and then do something in another thread if it's been too long since the last callback.

Michael Madsen
I got the impression that the OP want to control it at a lower level, not a higher...
Andreas Rejbrand
He wants something which is simple to use in a seperate thread while still being robust, but without having to delve into Indy or JEDI. That doesn't have to exclude higher level code, as long as that higher level code works as intended.
Michael Madsen
"He wants something which is simple to use in a separate thread while still being robust" - Right. Though 'multithread' will be the best solution, it is not mandatory as long as the code implements a time-out.
Altar
A: 

My personal favorite is using the WebHttpRequest component from importing the "Microsoft WinHTTP Services" type library: http://yoy.be/item.asp?i142

var
  w:IWebHttpRequest;
  f:TFileStream;  
  os:TOleStream;
begin 
  w:=CoWebHttpRequest.Create;
  w.Open('GET',SourceURL,false);
  w.Send(EmptyParam);
  os:=TOleStream.Create(IUnknown(w.ResponseStream) as IStream);
  f:=TFileStream.Create(DestinationFilePath,fmCreate);
  os.Position:=0;
  f.CopyFrom(os,os.Size);
  f.Free;
  os.Free;
  w:=nil;
end;
Stijn Sanders
Hi Stijn. Can it handle server time-outs?
Altar
Also, is WinHTTP available on all Windows operating systems or I need to display an announcement like "This application will not work until you download and install x stuff from MS web site" (because I will never ever do that).
Altar
w.Send will throw an exception on timeout, there's also a SetTimeouts method http://msdn.microsoft.com/en-us/library/aa384061(v=VS.85).aspx
Stijn Sanders
A: 

I recommend Synapse. It's small, stable and easy-to-use (no need of any external libraries).

Example from httpsend.pas

function HttpGetText(const URL: string; const Response: TStrings): Boolean;
var
  HTTP: THTTPSend;
begin
  HTTP := THTTPSend.Create;
  try
    Result := HTTP.HTTPMethod('GET', URL);
    if Result then
      Response.LoadFromStream(HTTP.Document);
  finally
    HTTP.Free;
  end;
end;
Andrew
The question remains: does it support time outs? Or it will freeze the application?Thanks
Altar
Quote: "Synapse is not a components suite, but only a group of classes and routines. No installation is needed! Just add the units to your uses clause." - This is what I need. At the same time, look at the update ratio of this library. It one update per year or less! Still I think I will try it.
Altar
Yes, it supports timeouts. Yet it's still a good idea to organize download in a separate thread.
Andrew
I use this library for more than 4 years. Never had any problems. In any case, it's open source. The sources are easy to understand and to trace. There are no any complex cross-dependencies like in Indy. You will be able to customize anything, if you ever need.
Andrew
A: 

Solved using improved version of the above code. (it still does not solve all issues - MS does not actually implemented full support for server time out)

http://stackoverflow.com/questions/3135003/the-connection-does-not-timeout-while-downloading-file-from-internet

Altar