delphiindyindy10delphi-11-alexandria

INDY: Sending a file fragment as response to an HTTP GET command in TIdHttpServer.OnCommandGet


I am developing a tiny Web Server that should allow Download Managers to make multiple connections and download a huge file in multiple segments simultaneously.

I can detect that the download manager requests a partial download (the ARequestInfo.Ranges property) and have made a function that serves these fragments using my own TFileFragmentStream class that serves the given range of a file as a TStream descendant.

However, my Download Manager doesn't seem to accept that my server supports this, and only downloads it using one connection. I can see that it requests partial downloads, so I'm probably not satisfying it that I support it.

I currently use code like this:

PROCEDURE SendPartialFile(Response : TIdHttpResponseInfo ; CONST N : TFileName ; RangeStart,RangeEnd : Int64);
  BEGIN      
    Response.ContentType:='application/octet-stream';
    Response.ContentDisposition:='attachment; filename="'+ExrractFileName(N)+'"';
    Response.ContentLength:=SUCC(RangeEnd-RangeStart);
    Response.ContentStream:=TFileFragmentStream.Create(N,RangeStart,ContentLength)
    Response.FreeContentStream:=TRUE
  END;

Are there other properties of my Response that I need to set?

Updated Final code:

  ContentDisposition:='attachment; filename="'+N.NameOnly+'"';
  VAR S:=TIdHTTPRangeStream.Create(TFileStream.Create(N,fmOpenRead OR fmShareDenyNone),RangeStart,RangeEnd);
  ContentStream:=S;
  ResponseNo:=S.ResponseCode;
  ContentRangeStart:=S.RangeStart;
  ContentRangeEnd:=S.RangeEnd;
  ContentText:='';
  ContentRangeInstanceLength:=S.SourceStream.Size;
  OutputDebugString('Sending File Fragment: '+ContentType+' ['+IntToStr(ContentRangeStart)+'-'+IntToStr(ContentRangeEnd)+'] ('+IntToStr(ContentLength)+' bytes) "'+ContentDisposition+'" ResponseNo='+IntToStr(ResponseNo));
  FreeContentStream:=TRUE;

Here's the logging:

Sending Full File: video/mp4 XX.mp4 (6573584385 bytes)
Sending File Fragment: video/mp4 [233600-6573584384] (6573350785 bytes) "attachment; filename="XX.mp4"" ResponseNo=206
Sending File Fragment: video/mp4 [3286914832-6573584384] (3286669553 bytes) "attachment; filename="XX.mp4"" ResponseNo=206
Sending File Fragment: video/mp4 [4930249645-6573584384] (1643334740 bytes) "attachment; filename="XX.mp4"" ResponseNo=206
Sending File Fragment: video/mp4 [1655659767-3286914895] (1631255129 bytes) "attachment; filename="XX.mp4"" ResponseNo=206
Sending File Fragment: video/mp4 [4108582275-4930249708] (821667434 bytes) "attachment; filename="XX.mp4"" ResponseNo=206
Sending File Fragment: video/mp4 [5751917052-6573584384] (821667333 bytes) "attachment; filename="XX.mp4"" ResponseNo=206
Sending File Fragment: video/mp4 [2471287336-3286914895] (815627560 bytes) "attachment; filename="XX.mp4"" ResponseNo=206
Sending File Fragment: video/mp4 [855370091-1655659830] (800289740 bytes) "attachment; filename="XX.mp4"" ResponseNo=206

The Full File is just a

ContentType:='application/octet-stream'
ContentDisposition:='attachment; filename="'+ExtractFileName(FileName)+'"'
ContentStream:=TFileStream.Create(FileName,fmOpenRead OR fmShareDenyNone)
FreeContentStream:=TRUE
ResponseNo:=200

Solution

  • You need to set the various AResponseInfo.ContentRange... properties to describe the range you are actually sending back to the client:

    ContentRangeStart
    ContentRangeEnd
    ContentRangeInstanceLength
    ContentRangeUnits
    

    Also, make sure you are setting the AResponseInfo.ResponseNo to indicate whether you are sending a partial range (206) or the complete file (200).

    Read RFC 2616 sections 10.2.7 206 Partial Content and 14.16 Content-Range for more details.

    BTW, TIdHTTPServer has its own TIdHTTPRangeStream helper class for sending a partial range of a source TStream. You would construct it with the source stream (such as a TFileStream) and requested range, and then use its ResponseCode, RangeStart, and RangeEnd properties to update the AResponseInfo, eg:

    Procedure SendPartialFile(Response: TIdHTTPResponseInfo; const FileName: TFileName; RangeStart, RangeEnd: Int64);
    begin   
      Response.ContentType := 'application/octet-stream';
      Response.ContentDisposition := 'attachment; filename="'+ExtractFileName(FileName)+'"';
      Response.ContentStream := TIdHTTPRangeStream.Create(TIdReadFileExclusiveStream.Create(FileName), RangeStart, RangeEnd);
      Response.FreeContentStream := True;
      Response.ResponseNo := TIdHTTPRangeStream(Response.ContentStream).ResponseCode;
      if Response.ResponseNo = 206 then
      begin
        Response.ContentRangeStart := TIdHTTPRangeStream(Response.ContentStream).RangeStart;
        Response.ContentRangeEnd := TIdHTTPRangeStream(Response.ContentStream).RangeEnd;
        Response.ContentRangeInstanceLength := TIdHTTPRangeStream(Response.ContentStream).SourceStream.Size;
      end;
    end;