Saturday, September 04, 2004

Just a part of it, please

A few days ago, I had to write an ISAPI extension DLL which would return partial content from a local file stored on the web server. This was meant as a workaround for some proxy configurations which (so I heard) strip some HTTP headers and thus make it impossible for our client code to simply use standard HTTP 1.1 GET requests with the Range header specified. (And before you ask, no, it was not acceptable to simply ask them to change their network configuration.)
So my ISAPI DLL should accept parameters like file name, start position and length in its URL and return only the requested bytes to the client (after translating the given file path to the actual local file path on the server).
The initial problem with Delphi's TWebResponse implementation (or so it seemed at first) was that it had no support for sending just a given number of bytes from a stream. The methods for sending response to the client are:


TWebResponse = class(TObject)
...
public
...
procedure SendResponse; virtual; abstract;
procedure SendRedirect(const URI: string); virtual; abstract;
procedure SendStream(AStream: TStream); virtual; abstract;
...
end;

The problem with TISAPIResponse.SendStream (TWebResponse is an abstract class, TISAPIResponse is the concrete descendant used in ISAPI applications) is that it will send the whole stream (or the rest of it from its current position) in 8K chunks until it reaches the end of the stream. So what are my options?

  1. Create a memory stream, copy the requested bytes to it and use it as the response content stream. Not a very good idea, if the requested range is large. This was my first, quick and dirty implementation, though.

  2. Write my own TWebResponse descendant and plug it into the framework - while perhaps possible, it might be a bit of work.

  3. Modify the VCL source code ;-) I don't like doing this unless for a very good reason.

  4. Anything else? Actually, yes.

My solution was to write a new TStream descendant:


type
TPartialFileStream = class(TStream)
private
FFileStream: TFileStream;
FSize: Int64;
FStartPos: Int64;
public
constructor Create(const FileName: string; AStartPos, ASize: Int64);
destructor Destroy; override;
function Read(var Buffer; Count: Longint): Longint; override;
function Write(const Buffer; Count: Longint): Longint; override;
function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override;
end;

{ TPartialFileStream public }

constructor TPartialFileStream.Create(const FileName: string; AStartPos, ASize: Int64);
var
FileSize: Int64;
begin
inherited Create;
FFileStream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyNone);
FileSize := FFileStream.Size;
if (ASize < 0) or (ASize > FileSize) or (AStartPos < 0) or (AStartPos > FileSize) or
(FFileStream.Seek(AStartPos, soBeginning) <> AStartPos) then
raise EReadError.CreateRes(@SReadError);
FStartPos := AStartPos;
if ASize = 0 then
FSize := FileSize - FStartPos
else
FSize := ASize;
end;

destructor TPartialFileStream.Destroy;
begin
FFileStream.Free;
inherited Destroy;
end;

function TPartialFileStream.Read(var Buffer; Count: Longint): Longint;
begin
if FFileStream.Position + Count > FStartPos + FSize then
Count := FStartPos + FSize - FFileStream.Position;
if Count > 0 then
Result := FFileStream.Read(Buffer, Count)
else
Result := 0;
end;

function TPartialFileStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;
var
NewPos: Int64;
begin
case Origin of
soBeginning:
Result := FFileStream.Seek(FStartPos + Offset, soBeginning) - FStartPos;
soCurrent:
begin
NewPos := FFileStream.Position + Offset - FStartPos;
if NewPos < 0 then
NewPos := 0;
if NewPos > FSize then
NewPos := FSize;
Result := FFileStream.Seek(FStartPos + NewPos, soBeginning) - FStartPos;
end;
soEnd:
Result := FFileStream.Seek(FStartPos + FSize + Offset, soBeginning) - FStartPos;
else
Result := -1;
end;
end;

function TPartialFileStream.Write(const Buffer; Count: Longint): Longint;
begin
Result := 0; // this stream is read-only
end;

It uses TFileStream internally (and therefore accesses the file directly) but only "publishes" the specified part of the file. The code for sending the response is now very simple:


Stream := TPartialFileStream.Create(FileName, StartPos, Len);
Response.StatusCode := HTTP_STATUS_OK;
Response.ContentType := 'application/octet-stream';
Response.ContentStream := Stream;
Response.SendResponse;
Post a Comment