Download, pause and resume an download using Indy components - delphi

Actually i'm using the TIdHTTP component for download a file from internet. i'm wondering if is possible pause and resume the download using this component o maybe another indy component.
this is my current code, this works ok for download a file (without resume), but . now i want pause the download close my app ,and when my app restart then resume the download from the last position saved.
var
Http: TIdHTTP;
MS : TMemoryStream;
begin
Result:= True;
Http := TIdHTTP.Create(nil);
MS := TMemoryStream.Create;
try
try
Http.OnWork:= HttpWork;//this event give me the actual progress of the download process
Http.Head(Url);
FSize := Http.Response.ContentLength;
AddLog('Downloading File '+GetURLFilename(Url)+' - '+FormatFloat('#,',FSize)+' Bytes');
Http.Get(Url, MS);
MS.SaveToFile(LocalFile);
except
on E : Exception do
Begin
Result:=False;
AddLog(E.Message);
end;
end;
finally
Http.Free;
MS.Free;
end;
end;

the following code worked to me. It downloads the file by chunks:
procedure Download(Url,LocalFile:String;
WorkBegin:TWorkBeginEvent;Work:TWorkEvent;WorkEnd:TWorkEndEvent);
var
Http: TIdHTTP;
quit:Boolean;
FLength,aRangeEnd:Integer;
begin
Http := TIdHTTP.Create(nil);
fFileStream:=nil;
try
try
Http.OnWork:= Work;
Http.OnWorkEnd := WorkEnd;
Http.Head(Url);
FLength := Http.Response.ContentLength;
quit:=false;
repeat
if not FileExists(LocalFile) then begin
fFileStream := TFileStream.Create(LocalFile, fmCreate);
end
else begin
fFileStream := TFileStream.Create(LocalFile, fmOpenReadWrite);
quit:= fFileStream.Size >= FLength;
if not quit then
fFileStream.Seek(Max(0, fFileStream.Size-4096), soFromBeginning);
end;
try
aRangeEnd:=fFileStream.Size + 50000;
if aRangeEnd < fLength then begin
Http.Request.Range := IntToStr(fFileStream.Position) + '-'+ IntToStr(aRangeEnd);
end
else begin
Http.Request.Range := IntToStr(fFileStream.Position) + '-';
quit:=true;
end;
Http.Get(Url, fFileStream);
finally
fFileStream.Free;
end;
until quit;
Http.Disconnect;
except
on E : Exception do
Begin
//Result:=False;
//AddLog(E.Message);
end;
end;
finally
Http.Free;
end;
end;

Maybe the HTTP RANGE header can help you here. Have a look at archive.org's copy of http://www.west-wind.com/Weblog/posts/244.aspx for more info on resuming HTTP downloads:
(2004-02-07) A couple of days ago somebody on the Message Board asked an interesting question about how to provide resumable HTTP downloads. My first response to this question was that this isn't possible since HTTP is a stateless protocol that has no concept of file pointers and thus can't resume an HTTP download.
However it turns out HTTP 1.1 does have the ability to specify ranges in downloads by using the Range: header in the Http header sent form the client. You can do things like:
Range: 0-10000
Range: 100000-
Range: -100000
which download the first 100000 bytes, everything over 100000 bytes or the last 100000 bytes. There are more combinations but the first two are the ones that are of interest for a resumable download.
To demonstrate this feature I used wwHTTP (in Web Connection/VFP) to download a first 400k chunk of a file into a file with HTTPGetEx which is meant to simulate an aborted download. Next I do a second request to pick up the existing file and download the remainder:
#INCLUDE wconnect.h
CLEAR
CLOSE DATA
DO WCONNECT
LOCAL o as wwHTTP
lcDownloadedFile = "d:\temp\wwipstuff.zip"
*** Simulate partial output
lcOutput = ""
Text=""
tnSize = 0
o = CREATEOBJECT("wwHTTP")
o.HttpConnect("www.west-wind.com")
? o.httpgetex("/files/wwipstuff.zip",#Text,#tnSize,"Range: bytes=0-400000"+CRLF,lcDownloadedFile)
o.Httpclose()
lcOutput = Text
? LEN(lcOutput)
*** Figure out how much we downloaded
lnOpenAt = FILESIZE(lcDownloadedFile)
*** Do a partial download starting at this byte count
Text=""
tnSize =0
o = CREATEOBJECT("wwHTTP")
o.HttpConnect("www.west-wind.com")
? o.httpgetex("/files/wwipstuff.zip",#Text,#tnSize,"Range: bytes=" + TRANSFORM(lnOpenAt) + "-" + CRLF)
o.Httpclose()
? LEN(Text)
*** Read the existing partial download and append current download
lcOutput = FILETOSTR(lcDownloadedFile) + TEXT
? LEN(lcOutput)
STRTOFILE(lcOutput,lcDownloadedFile)
RETURN
Note that this approach uses a file on disk, so you have to use HTTPGetEx (with Web Connection). The second download can also be done to disk if you choose, but things will get tricky if you have multiple aborts and you need to piece them together. In that case you might want to try to keep track of each file and add a number to it, then combine the result at the very end.
If you download to memory using WinInet (which is what wwHTTP uses behind the scenes) you can also try to peel out the file from the Temporary Internet Files cache. Although this works I suspect this process will become very convoluted quickly so if you plan on providing the ability to resume I would highly recommend that you write your output to file yourself using the approach above.
Some additional information on WinInet and some of the requirements for this approach to work with it are described here: http://www.clevercomponents.com/articles/article015/resuming.asp.
The same can be done with wwHTTP for .Net by adding the Range header to the wwHTTP:WebRequest.Headers object.
(Randy Pearson) Say you don't know what the file size is at the server. Is there a way to find this out, so you can know how many chunks to request, for example? Would you send a HEAD request first, or does the header of the GET response tell you the total size also?
(Rick Strahl) You have to read the Content-Length: header to get the size of the file downloaded. If you're resuming this shouldn't matter - you just use Range: (existingsize)- to get the rest. For chunky downloads you can read the content length and only download the first x bytes. This gets tricky with wwHTTP - you have to make individual calls with HTTPGetEx and set the tnBufferSize parameter to the chunk size to retrieve to have it stop after the size is reached.
(Randy Pearson) Follow-up: It looks like a compliant server would send you enough to know the size. If it provides chunks it should reply with something like:
Content-Range: 0-10000/85432
so you could (if desired) extract that and use it in a loop to continue with intelligent chunk requests.
Also look here https://forums.embarcadero.com/message.jspa?messageID=219481 for TIdHTTP related discussion on the same topic:
(at least partly as per tfilestream.seek and offset confusion)
if FileExists(dstFile) then
begin
Fs := TFileStream.Create(dstFile, fmOpenReadWrite);
try
Fs.Seek(Max(0, Fs.Size-1024), soFromBeginning);
// alternatively:
// Fs.Seek(-1024, soFromEnd);
Http.Request.Range := IntToStr(Fs.Position) + '-';
Http.Get(Url, Fs);
finally
Fs.Free;
end;
end;

Related

IdHTTP Post returns connection reset by peer for Streams over 32KB

I have a problem posting to a web server using HTTPS. I am not sure if the problem is with me or with the server. So it appeared that if I try to post a Stream greater than 32KB, Delphi crashes with Socket Error 10054 - Connection reset by peer.
I am using Delphi XE5 with the internal version of Indy and latest to date open ssl dlls.
I also try this on XE with latest to date Indy and ssl dlls.
Here is part of my code
function TForm1.SendItemsList(aDataList: TStringList): Boolean;
var
aHTTP: TIdHTTP;
aRes: String;
aURL: String;
aErrMsg: String;
aStrm: TMemoryStream;
aResStrm: TMemoryStream;
aXML: TNativeXML;
aTmpNode: TXmlNode;
aErrNode: TXmlNode;
aList: TList;
i: Integer;
begin
Result := False;
aStrm := TMemoryStream.Create;
aResStrm := TMemoryStream.Create;
aXML := TNativeXML.Create(nil);
aHTTP := CreateHTTP('application/x-www-form-urlencoded');
try
aDataList.SaveToStream(aStrm);
aStrm.Position := 0;
aURL := Format(cIRPURL, ['1']);
try
aHTTP.Post(aURL, aStrm, aResStrm);
aResStrm.Position := 0;
aXML.LoadFromStream(aResStrm);
aTmpNode := aXML.Root.FindNode('ResponseCode');
if aTmpNode <> nil then
begin
if aTmpNode.Value <> '0' then
begin
aErrNode := aXML.Root.FindNode('ResponseText');
aErrMsg := '';
if aErrNode <> nil then
aErrMsg := aErrNode.Value;
aList := TList.Create;
try
aXML.Root.FindNodes('Detail', aList);
for i := 0 to aList.Count-1 do
begin
aErrMsg := aErrMsg+#13#10+TXmlNode(aList[i]).Value;
end;
finally
aList.Free;
end;
end;
end;
except
on E:Exception do
begin
if E is EIdHTTPProtocolException then
aErrMsg := E.Message + #13#10 + (E as EIdHTTPProtocolException).ErrorMessage
else
aErrMsg := E.Message;
Exit;
end;
end;
finally
aXML.Free;
aStrm.Free;
aHTTP.Free;
aResStrm.Free;
end;
Result := True;
end;
Where CreateHTTP looks like
function TForm1.CreateHTTP(aContentType: String): TIdHTTP;
begin
Result := TIdHTTP.Create(nil);
Result.ConnectTimeout:=60000;
Result.ReadTimeout:=90000;
Result.ProtocolVersion:=pv1_1;
Result.HTTPOptions := [hoForceEncodeParams];
Result.HandleRedirects:=True;
Result.IOHandler := SSLHandler;
SSLHandler.ReadTimeout := 30000;
Result.Request.Accept:='*/*';
Result.Request.AcceptLanguage:='en-US';
Result.Request.ContentType:=aContentType;
Result.Request.CharSet:='utf-8';
Result.Request.UserAgent := 'Mozilla/5.0';
end;
All these timeouts exist just because I was testing why I get that error. Then I realized that the problem is when the stream to be sent is larger than 32KB.
I can't really say that there is something wrong with the code at all, because in the same way I send data to several other services like Amazon and Walmart for example where I send sometimes megabytes and I don't receive any errors.
The server is IIS but I don't know what version, the support doesn't seem to believe me that I am doing everything OK.
What I notice is that the SSL handler has some default buffer sizes - SendBufferSize and RecvBufferSize which default to 32KB. Well I tried setting that to 1MB but still I get the same error.
If I send something which is less than 32KB then everything is OK. The error is returned immediately after line with POST is executed - there is no delay, just immediate error. Otherwise sending small streams results in having a delay of a second or two before it gets processed and then the debugger goes to the next line. I started believing it is a setting of the IIS and there is really such a setting, but the guys there say everything on their side is ok and they have 4MB of limit for the requests.
The service provider is IRPCommerce but unfortunately I can't give links for testing because of IP filtering which takes place at the moment there.
I spent several days discussing this with them, searching the web for problems any limitations etc.
So is there something I am missing here, any limitations in Indy which may cause this problem, I doubt but just to be sure I am asking? Anything else I can do to make it clearer where the problem might be?
EDIT:
Here is excerpt of the aDataList:
Stock_ExternalStockID|Brands_Active|Brands_Brand|Models_Active|Models_Model|Models_Description|Models_AdditionalInformation1|Models_AdditionalInformation2|Stock_DisplayOrder|Stock_Option|Stock_Price|Stock_RRP|Stock_SupplierCost|Categories_Active|Categories_Name|Stock_PostageWeight|Stock_PartCode|Stock_ISBNNumber|Stock_UPCAPartCode|Stock_EAN13PartCode|Models_ImageURLs|OptionSelector|OptionSelectorAttributes|OptionSelectorCount|Stock_OutOfStockStatus
17664-00001|TRUE|Polypads|TRUE|Polypads Plus One Outsider Pet Bed|<ul><li>The perfect pet bed for any animal around the house or for covering car seats or boots for travelling. </li><li>Convenient to use. </li><li>6cm Plus One thickness. </li><li>Fully machine washable and quick drying. </li><li>As there is such an extensive range of colours available for the Polypad collection many colour combinations will have to be ordered in specifically; this service could take up to two weeks. </li><li>If you do not have any specific colours in mind please select, Colour Not Important, from the drop down menu.</li></ul>| | |10|Royal Blue-Navy|43.95|48.99|21.69|TRUE|Dog Beds|1000|160||||https://saddlery.biz/media/catalog/product/o/u/outp1.jpg|1|21,44|2|10
17664-00002|TRUE|Polypads|TRUE|Polypads Plus One Outsider Pet Bed|<ul><li>The perfect pet bed for any animal around the house or for covering car seats or boots for travelling. </li><li>Convenient to use. </li><li>6cm Plus One thickness. </li><li>Fully machine washable and quick drying. </li><li>As there is such an extensive range of colours available for the Polypad collection many colour combinations will have to be ordered in specifically; this service could take up to two weeks. </li><li>If you do not have any specific colours in mind please select, Colour Not Important, from the drop down menu.</li></ul>| | |20|Soft Blue-Royal Blue|43.95|48.99|21.69|TRUE|Dog Beds|1000|160||||https://saddlery.biz/media/catalog/product/o/u/outp1.jpg|1|21,44|2|10
17664-00003|TRUE|Polypads|TRUE|Polypads Plus One Outsider Pet Bed|<ul><li>The perfect pet bed for any animal around the house or for covering car seats or boots for travelling. </li><li>Convenient to use. </li><li>6cm Plus One thickness. </li><li>Fully machine washable and quick drying. </li><li>As there is such an extensive range of colours available for the Polypad collection many colour combinations will have to be ordered in specifically; this service could take up to two weeks. </li><li>If you do not have any specific colours in mind please select, Colour Not Important, from the drop down menu.</li></ul>| | |30|Black-Purple|43.95|48.99|21.69|TRUE|Dog Beds|1000|160||||https://saddlery.biz/media/catalog/product/o/u/outp1.jpg|1|21,44|2|10
Here I have 165 rows. If I send about 40 of them they go, just because 40 are just about 32KB. I have confirmed that data is not the problem, because I have tried sending one by one each of the lines.
I tried multipart/form-data with no luck. Actually they haven't told me what to use, no matter how much times I have asked about that, so I used the same thing I am using with Walmart.
I think the server is IIS 8.5.
It seems that the "solution" is to change the Content-type to text/xml. None of the other mentioned content-types work with streams larger than 32KB. At the same time I got a confirmation from the developers of the site that the content-type is not considered at all on the server side.
So I am really confused what is going on here and why only 'text/xml' works fine.

How can I trim / replace the first character in a FileStream in Delphi?

I have the following code for a series of file drawdowns using IDHttp.Get, the contents of the files
procedure Tform1.GetData;
{***************************}
var
fs2 : tfilestream;
s : char;
begin
Sleep(1000);
idhttp1.HandleRedirects := TRUE;
fsjson2 := tfilestream.Create((GstrPath+GstrRep+'-'+GstrHome+'.json'),fmcreate);;
idhttp1.IOHandler := idssl;
IdSSL.SSLOptions.SSLVersions := [sslvTLSv1, sslvTLSv1_1, sslvTLSv1_2];
IdSSL.SSLOptions.Mode := sslmUnassigned;
try
idhttp1.Get(GstrURL,fs2);
except
on E: Exception do
begin
rememo1.Lines.Add('Seems to be an issue, trying again...');
Sleep(500);
idhttp1.Get(GstrURL,fs2);
end;
end;
I would like there to be a method of either trimming the first character although I don't think this is possible, or replacing the first character (which comes with the info by default) with a blank character. I think it's a little out of my skillset at the moment to do, and so would appreciate any help someone can give.
Thanks
Ant
There are several possible solutions:
You are receiving data into a stream. If you want to trim the first character, you could, at the place you handle the stream, skip reading the first character and throw it away.
If you have no control on where the receiving stream is handled, then you may simply create a new stream and loop reading all characters from the receiving stream and write them into the destination stream. Then throw away the received stream and keep the one you created.
If the data received (currently the stream) is not too big, you could receive it into a string instead of a stream, then you can trim/delete/insert anything with simple string manipulation, and finally write the modified string back to a stream for later use.

Handling BabyFTP MKD 250 response with Indy

I'm using BabyFTP as an embedded FTP server for a Delphi 10.1 Berlin application. Even though I'm still not sure it's the best lightweight FTP server I can use with my requirements, it seems to be working well enough... except for one detail:
On a successful MKD command, BabyFTP responds with a 250, instead of the expected (by RFC and Indy) 257. Because of this the TIdFTP raises an EIdReplyRFCError exception which breaks the flow of what I'm trying to do (save a blob field to a file):
s := TBytesStream.Create;
Try
Field.SaveToStream(s);
MakeDir(TPath.GetDirectoryName(url));
Put(s, url);
Finally
s.Free;
End;
Of course, I know I could wrap the MakeDir line in a Try Except block and ignore the specific exception type. But this seems a bit risky as from the raised exception I can't be sure I got a 250 or some other real error.
I've tried looking if Indy's response codes are somehow configurable, but it seems they are hard coded in the specific methods. Subclassing TIdFTP is not feasible as the methods are not virtual. I could customize the IdFTP.pas unit, but I don't want to do that as I'm working in a team and I prefer not having to distribute patches to standard Delphi units.
I could use another FTP server... but I suppose most of them have some not perfectly standard feature like this one.
Does anyone know of other workarounds? Remy?
Ondrej's answer explains how to address the issue of handling a 250 reply in TIdFTP.MakeDir().
Note that TIdFTP.MakeDir() is just a wrapper for TIdTCPConnection.SendCmd():
procedure TIdFTP.MakeDir(const ADirName: string);
begin
SendCmd('MKD ' + ADirName, 257); {do not localize}
end;
SendCmd() is public, so an alternative solution would be to call SendCmd() directly and tell it that 250 is an acceptable reply code:
s := TBytesStream.Create;
Try
Field.SaveToStream(s);
//MakeDir(TPath.GetDirectoryName(url));
SendCmd('MKD ' + TPath.GetDirectoryName(url), [250, 257]);
Put(s, url);
Finally
s.Free;
End;
Or, to accept any 2xx reply code, you can do this:
s := TBytesStream.Create;
Try
Field.SaveToStream(s);
//MakeDir(TPath.GetDirectoryName(url));
if (SendCmd('MKD ' + TPath.GetDirectoryName(url)) div 100) <> 2 then
RaiseExceptionForLastCmdResult;
Put(s, url);
Finally
s.Free;
End;
You could handle just the specific case of EIdReplyRFCError when its ErrorCode property equals 250, re-raising in any other case.
s := TBytesStream.Create;
Try
Field.SaveToStream(s);
Try
MakeDir(TPath.GetDirectoryName(url));
Except
on E: EIdReplyRFCError do
if E.ErrorCode <> 250 then raise;
end
Put(s, url);
Finally
s.Free;
End;

how do i save image and use them later in Idhttp

by Follow up to my previous question about update listview inside thread here
i start to think in different way to get my previous question solved , because of download process takes too long and takes much bandwidth i want to download GIFimage then save it on disk then use it later inside my application
this is current download thread code
procedure TDownloadImage.Execute;
var
aMs: TMemoryStream;
aIdHttp: TIdHttp;
begin
FGif := TGifImage.Create;
try
aMs := TMemoryStream.Create;
try
aIdHttp := TIdHttp.Create(nil);
try
aIdHttp.Request.UserAgent := 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0';
aIdHttp.Get(FURL, aMs); // here is the client FURL
finally
aIdHttp.Free;
end;
aMs.Position := 0;
FGif.LoadFromStream(aMs);
FGif.Transparent := True;
finally
aMs.Free;
end;
if Assigned(FOnImageReady) then
Synchronize(DoImageReady);
end;
finally
FGif.Free;
end;
end;
i want to save image of FURL on computer client then if this image requested to be download again abort download process and load it from client computer how possibly i can do that ?
As TLama Said wisely, you are looking for a Full Cache mechanism. In Delphi you may want to call it TDictionary.
If you want to save your Gif and URI in memory, then you can use a TDictionary pair, to hold each memory stream and uri, and everything will work fast.
If you want to store the data on files, and each time the application starts to remember the state of previous run, you may want to find a way to store the data, a database, or text file.
If you want a simple text file that can help, an Ini file can do the job, however, as text file grow, then tend to get slow, and make the search slower, then the file fetch.
If you work with a good portable database , maybe the search will be fast.
example code with Ini file to get you started:
pseudo code.
var
filename:string;//where is the key value is stored.
GifLocation:string;//result of the cache hit.
const
URI = 'http://sstatic.net/stackexchange/img/logos/so/so-logo-med.png?v=a4a65015804e';
begin
with TIniFile.create(filename) do
try
GifLocation := readString('FilesLocation',URI,'');
finally
free;
end;
if ('' = GifLocation) then
fetchBoy(URI)
else IGotIt_IGotIt(GifLocation);
end;
Implement your fetchBoy, for doing an http get, and IGotIt_IGotIt to load file stream.

Full size assigned right from the start when downloading a file using Indy

I have this delphi code that basically download a file (using Delphi 2010 + Indy 10.5.8 r4743), everything works just fine, except that when I download 100Mb (for example), it seems that Indy assign the full size (ie. a file with 100MB of dummy content(*) is instantly created), then download the file.
In the end the 100MB is correctly downloaded, but since the download process is done in the background using a hidden EXEecutable, I based my code to rely on the temporary file size to update the main UI
with IdHTTP do
begin
if FileExists(LocalFile) then
iLength := FileSize2(LocalFile)
else
iLength := 0;
DoExit := False;
try
try
repeat
if ExitApp then
Exit;
if Not FileExists(LocalFile) then
AFileStream := TFileStream.Create(LocalFile, fmCreate)
else
begin
// File already exist, resume download
AFileStream := TFileStream.Create(LocalFile, fmOpenReadWrite);
DoExit := (AFileStream.Size >= iLength);
if (Not DoExit) then
AFileStream.Seek(Max(0, AFileStream.Size - 4096), soFromBeginning);
end;
iRangeEnd := AFileStream.Size + 50000;
if (iRangeEnd < iLength) then
Request.Range := IntToStr(AFileStream.Position) + '-' + IntToStr(iRangeEnd)
else
begin
Request.Range := IntToStr(AFileStream.Position) + '-';
DoExit := True;
end;
PostTime := Now;
Get(TheURL, AFileStream);
IsError := Not (ResponseCode in [200, 206]);
until DoExit;
Disconnect;
except
IsError := True;
end; // try/except
finally
FreeAndNil(AFileStream);
end; // try/finally
end; // with
My question is Is there a way to avoid this behavior from indy? I know I can use the OnWork event, but then I will need to keep track of file names.
Ideally, I'd also like to avoid IPC (kinda overkill + I don't want to use that, say every second, for multiple downloads, I very much prefer to use the file size as indication of download progress as it gives more freedom when updating the UI)
(*) I assume it's dummy content since I need between 60-100 seconds on my current internet connection speed to really get the file
Yes, TIdHTTP pre-allocates the full file size if it knows the size ahead of time, based on the HTTP response headers. This is an optimization technique. Pre-allocating the file avoids unnecessary file system overhead when writing larger files, since the file does not have to keep growing over time, hunting for available sectors on the HDD, slowing down the writing process. There is currently no way to disable the pre-allocation, it is hard-coded behavior in Indy's internals. So relying on the file's actual size as a progress indicator will not work for you. You are going to have to communicate the actual progress information from your background app to the main app.

Resources