3

I'm trying to resume an upload using indy (HTTP Post), the code looks like this (using Delphi 2010, Indy 10.4736):

 IdHttp.Head('http://localhost/_tests/resume/large-file.bin');
 ByteRange           := IdHttp.Response.ContentLength + 1;

 // Attach the file to post/upload
 Stream              := TIdMultipartFormDataStream.Create;
 with Stream.AddFile('upload_file', 'D:\large-file.bin', 'application/octet-stream') do
 begin
      HeaderCharset  := 'utf-8';
      HeaderEncoding := '8';
 end;    // with

 with IdHTTP do
 begin
      IOHandler.LargeStream           := True;

      with Request do
      begin
           ContentRangeStart          := ByteRange;
           ContentRangeEnd            := (Stream.Size - ByteRange);
           ContentLength              := ContentRangeEnd;
           ContentRangeInstanceLength := ContentLength;
      end;    // with

      Post('http://localhost/_tests/resume/t1.php', Stream);
 end;    // with

but upload resume doesn't work :(

I looked into Indy's code, it seems that this function in IdIOHandler.pas

TIdIOHandler.Write()

always deal with complete streams/files (since the parameter ASize: TIdStreamSize seems to be always 0, which according to the code means sending the full file/stream).

This prevents indy from resuming the upload.

My question is: is it possible to avoid sending the full file?

Setting content range didn't change anything. I also tweaked indy's code (modified 3 lines) to make indy obey to the content range / stream position, but it's buggy and indy always end up hanging in IdStackWindows.pas because of an infinite timeout here:

TIdSocketListWindows.FDSelect()

TheDude
  • 3,045
  • 4
  • 46
  • 95
  • You should use `PUT` for that. – OnTheFly Mar 07 '12 at 13:23
  • Here's what remy lebeau [said about put](http://stackoverflow.com/questions/9476744/how-to-optimize-upload-routine-using-delphi-2010#comment12034421_9492180) – TheDude Mar 07 '12 at 13:56
  • No idea, he probably talks about PHP4. But yes, your desires are incompatible with POST. – OnTheFly Mar 07 '12 at 14:56
  • Resuming files is not compatible with `PUT`. I said as much, `POST`, on the other hand, is just arbitrary data. The receiving script decides what to do with the data, so it could, in theory, be used for resuming. In practice, it rarely is. – Remy Lebeau Mar 12 '12 at 00:19

1 Answers1

4

As I told you in your earlier question, you have to post a TStream containing the remaining file data to upload. Don't use TIdMultipartFormDataStream.AddFile(), as that will send the entire file. Use the TStream overloaded version of TIdMultipartFormDataStream.AddFormField() instead.

And TIdHTTP is not designed to respect the ContentRange... properties. Most of the Request properties merely set the corresponding HTTP headers only, they do not influence the data. That is why your edits broke it.

Try this:

IdHttp.Head('http://localhost/_tests/resume/large-file.bin');
FileSize := IdHttp.Response.ContentLength;

FileStrm := TFileStream.Create('D:\large-file.bin', fmOpenRead or fmShareDenyWrite);
try
  if FileSize < FileStrm.Size then
  begin
    FileStrm.Position := FileSize;

    Stream := TIdMultipartFormDataStream.Create;
    try
      with Stream.AddFormField('upload_file', 'application/octet-stream', '', FileStrm, 'large-file.bin') do
      begin
        HeaderCharset  := 'utf-8';
        HeaderEncoding := '8';
      end;

      with IdHTTP do
      begin
        with Request do
        begin
          ContentRangeStart := FileSize + 1;
          ContentRangeEnd   := FileStrm.Size;
        end;

        Post('http://localhost/_tests/resume/t1.php', Stream);
      end;
    finally
      Stream.Free;
    end;
  end;
finally
  FileStrm.Free;
end;

With that said, this is a GROSS MISUSE of HTTP and multipart/form-data. For starters, the ContentRange values are in the wrong place. You are applying them to the entire Request as a whole, which is wrong. They would need to be applied at the FormField instead, but TIdMultipartFormDataStream does not currently support that. Second, multipart/form-data was not designed to be used like this anyway. It is fine for uploading a file from the beginning, but not for resuming a broken upload. You really should stop using TIdMultipartFormDataStream and just pass the file data directly to TIdHTTP.Post() like I suggested earlier, eg:

FileStrm := TFileStream.Create('D:\large-file.bin', fmOpenRead or fmShareDenyWrite);
try
  IdHTTP.Post('http://localhost/_tests/upload.php?name=large-file.bin', FileStrm);
finally
  FileStrm.Free;
end;

.

IdHTTP.Head('http://localhost/_tests/files/large-file.bin');
FileSize := IdHTTP.Response.ContentLength;

FileStrm := TFileStream.Create('D:\large-file.bin', fmOpenRead or fmShareDenyWrite);
try
  if FileSize < FileStrm.Size then
  begin
    FileStrm.Position := FileSize;
    IdHTTP.Post('http://localhost/_tests/resume.php?name=large-file.bin', FileStrm);
  end;
finally
  FileStrm.Free;
end;

I already explained earlier how to access the raw POST data in PHP.

Community
  • 1
  • 1
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I'm sorry but it wasn't clear in your last post. Also, the reason why I used TIdMultipartFormDataStream is because it handle very well large files. This solution means having to split/copy files (which can slow down the process in case of huge files) and just . – TheDude Mar 07 '12 at 13:54
  • Thank you for the additional help, I maintain that it's not a bad idea to update indy to support resuming upload, but you know way better indy/http than me. – TheDude Mar 07 '12 at 13:54
  • 2
    As I explained earlier, you do not need to, nor should you, be splitting up the files anyway. Posting a `TIdMultipartFormDataStream` versus a plain `TStream` is WAY overkill for what you are attempting to do. Posting a plain `TStream` handles large files just as well as, if not better than, posting a `TIdMultipartFormDataStream` without dealing with all the extra overhead. And as I explained earlier, **HTTP does not natively support upload resuming**, so any solution you use is going to be a hack since you have to write the backend PHP script to support it manually anyway. – Remy Lebeau Mar 07 '12 at 17:31
  • Remy, [You said in the other question](http://stackoverflow.com/questions/9476744/how-to-optimize-upload-routine-using-delphi-2010#comment12022953_9492180) that "You could alternatively write separate scripts (...) and another one to POST remaining data to when resuming a previous upload." So (Talking at Indy level, not php) I **do** need to split the file (not just set the Stream.Position), otherwise Indy is going to send the file in full (that's what I experienced at least). – TheDude Mar 07 '12 at 19:37
  • I do understand that PHP script will be *hackish*, but I don't understand how I can send only part of the stream without spliting it first (since Indy won't do resume upload & based on my tests it will send the full file regardless of Stream.Position value) – TheDude Mar 07 '12 at 19:37
  • Looking at it more closely, `TIdMultipartFormDataStream` forces the input stream's `Position` back to 0, so that is not helpful. There are third-party `TStream` implementations available (or write your own, such as based on `TIdEventStream`) that wraps an input `TStream` to expose a subset of its data. You can try using that as the input to `AddFormField()`. Otherwise, just **STOP** using `multipart/form-data` submissions for your uploads. It is **NOT DESIGNED** for what you are using it for. – Remy Lebeau Mar 07 '12 at 20:27
  • So I should create a new stream class based on TIdEventStream and override read/write/seek/SetSize routines (or use the related events) to handle sending partial content without altering Indy source, then override AddFormField(), did I understood you correctly? – TheDude Mar 08 '12 at 14:22
  • Thank you Remy! BTW I owe you an apology as I only found your reply in chat yesterday (I'm new to SO, actually I only found about it while googleling!) – TheDude Mar 09 '12 at 20:06
  • Overriding the TIdEventStream isn't going to be easy (talking about the Read/Seek routines since I have to read how it's done in TIdMultiPartFormDataStream & eventually adapt it to my needs), I'll do my best and post my findings here, thanks! – TheDude Mar 09 '12 at 20:17
  • I have seen implementations before but don't recall where they are right now, but basically let's say you have a `TFileStream` of `Size=50` and you wanted to skip the first 10 bytes. You would implement the wrapper TStream's `Seek()` to never seek the source `TFileStream` prior to `Position=10`, and to return a value that is offset by 10. In other words, seeking the wrapper TStream to `Position=0` would seek the source `TFileStream` to `Position=10`. When the wrapper's `Size` is queried, it would return 40, not 50. Thus, Indy would only see 40 bytes that started 10 bytes into the file. – Remy Lebeau Mar 09 '12 at 22:23
  • It just occured to me that Indy has a similar wrapper `TStream` implementation that does just that - `TIdHTTPRangeStream` in IdCustomHTTPServer.pas. It is designed for sending partial blocks of data from a source `TStream`. Granted, it is HTTP-oriented, though, but you can strip off the HTTP handling. – Remy Lebeau Mar 09 '12 at 22:28
  • I'm trying to override AddFormField() but I'm stuck, it seems that I need [TheItem := FFields.Add;](http://pastebin.com/kqFXRyWn), which means copying all the FFields code. That means that I'm back to square one with multiform/data logic that you told me to avoid altogether. – TheDude Mar 10 '12 at 16:27
  • FWIW, I put the [delphi code here](http://pastebin.com/cFHQL8hx), it's still a embarrassingly rough code/attempt to override the stream. See my comment in line #102 regarding the FFields (I tried hard to make code as short as possible)...thanks! – TheDude Mar 10 '12 at 16:30
  • Having now found `TIdHTTPRangeStream`, I would suggest just using it as-is, that would be much simplier: `FStream := TFileStream.Create(...); RStream := TIdHTTPRangeStream.Create(FStream, StartPos, -1, True); If RStream.ResponseCode = 206 then begin MStream := TIdMultipartFormDataStream.Create; MStream.AddFormField(..., RStream, ...); IdHTTP.Post(..., MStream); MStream.Free; end; RStream.Free;` – Remy Lebeau Mar 11 '12 at 01:07
  • That's great Remy! I still have two issues though: [the delphi code](http://pastebin.com/YsNXxUrd) does upload/resume correctly (1) **IF** total file size is larger than 8MB and (2) the uploaded file is wraped around [this form post data](http://pastebin.com/Ugdsb1Mt) – TheDude Mar 11 '12 at 16:12
  • I'm using XAMPP (I have yet to test this code on a live server), I'm using this [PHP code](http://pastebin.com/XDmrAvwU) – TheDude Mar 11 '12 at 16:14
  • Just to clarify: when I say it doesn't work if I upload files **smaller than** 8MB, I mean that the uploaded file *is* created, but its size is always 0 byte – TheDude Mar 11 '12 at 16:25
  • I can confirm the exact same behavior when uploading to remote server, even when sending the file in one shot (ie. the entire file sent, no stop/resume) – TheDude Mar 11 '12 at 18:03
  • Did you verify that the correct file byte chunks are being posted every time? What do the PHP scripts look like? Also, your form-data's `Content-Type` header is completely messed up, which means your `AddFormField()` parameters are wrong. You are passing the filename (which should only be a filename by itself, not a full path) where the content type is expected, and are passing the content type where that charset is expected. – Remy Lebeau Mar 12 '12 at 00:27
  • As I said, I POSTed the file in one shot, no pause/resume so far). The [PHP code is here](http://pastebin.com/XDmrAvwU), I changed the [delphi code to this](http://pastebin.com/ic0dHnWb), now I'm getting the correct headers, but they'are still "attached" to the uploaded file! – TheDude Mar 12 '12 at 01:36
  • To clarify my last comment, **Yes** the correct file is being sent (except the header being attached & the size issues). I changed the `AddFormField()` call to `MultipartStream.AddFormField('upload_file', 'application/octet-stream', 'utf-8', RangeStream, ExtractFileName(FileName));`. Now the [uploaded file looks like this](http://pastebin.com/zgpttrdf) – TheDude Mar 12 '12 at 01:46
  • As I told you earlier, you cannot use `php://input` with `multipart/form-data` posts unless you are prepared to parse the raw MIME data yoourself manually. I said to switch to `php://input` ONLY IF you stop using `TIdMultipartFormDataStream` and just `POST` the source file directly, eg: `IdHTTP.Post('http://localhost/_tests/resume/t1.php', RangeStream)`. If you keep using `multipart/form-data` posts, then you have to use `$_FILES` in the PHP code to access the posted file data (pass `$_FILES['upload_file']['tmp_name']` to fopen() instead of `php://input`). – Remy Lebeau Mar 12 '12 at 19:08
  • Since you are posting binary data, I would suggest taking out the charset altogether, just set it to a blank string. – Remy Lebeau Mar 12 '12 at 19:08
  • Thank you Remy, I was actually following [your suggestion](http://stackoverflow.com/questions/9598273/resume-http-post-upload-with-indy/9600133#comment12255508_9600133) to use the range stream with multipart form. I can't pass the `$_FILES['upload_file']['tmp_name'] to fopen() instead of php://input` since (based on my *tests*) this method doesn't support resume (ie. either file is posted in full or not). so I'm back to `php://input` I guess? – TheDude Mar 12 '12 at 20:30
  • I **can** strip the extra headers (no pb), the **real** issue is the upload of files **less than 8MB** that fails, how can avoid this? – TheDude Mar 12 '12 at 20:31
  • You should be able to pass `$_FILES['upload_name']['tmp_nme`]` to `fopen()`. It is a new temp file created just for that single piece of posted data. Open the file, append its contents to your real destination file, then close and delete the temp file. – Remy Lebeau Mar 12 '12 at 21:32
  • Thank you Remy, but [my comment](http://stackoverflow.com/questions/9598273/resume-http-post-upload-with-indy/9600133#comment12289406_9600133) still stand. Let's say I have [this php code](http://pastebin.com/WhfExy1i); my tests shows that I can **only** move the data from the temp file to the final one **once the upload is completed**, otherwise I have to **restart from scratch**, and if I call the PHP script again [from Delphi](http://pastebin.com/EKPdWe5b), I'd have no idea which temp file was used previously. – TheDude Mar 12 '12 at 22:20
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/8801/discussion-between-remy-lebeau-and-gdhami) – Remy Lebeau Mar 12 '12 at 22:48