<div dir="ltr">While $working on a RESTFul application, I came across a situation where I needed to pack up a directory of files (already gzipped) and send them to the user. Rather than pulling everything into memory with Archive::Tar or creating a temporary file with File::Temp that would meet an uncertain fate after file_send depending on the deployment, I decided I would try and forkexec using a pipe and just stream the tar to the user.<div>
<br></div><div>This was easy enough to accomplish with open and '-|' and then</div><div><br></div><div style>exec ('/path/to/tar', '-c', '-f', '-' ...);</div><div style><br></div><div style>
in the child.</div><div style><br></div><div style>After that, it was a matter of duplicating parts of send_file, and I think this is where I got it wrong. Testing with raw Telnet saw the binary data come through, so I know that at least the piping worked, but when I tested the route as users would use it, the browser downloaded an empty file and curl -v, mentioned something about "* Excess found in a non pipelined read: excess = 7989".</div>
<div style><br></div><div style>I looked in the printed headers, and saw that it defined Content-Length: 0, which I assume is part of the problem. I tried NOT sending it using the code below using:</div><div style><br></div>
<div style>$response->{headers}->remove_header('Content-Length');</div><div style><br></div><div style>but was unable to remove it, and I'm not sure that's what I want to do anyway. Hijacking the stream and just catting binary data doesn't seem to be part of the spec, though I'd swear I've seen it done elswhere before, but I thought I would pick the collective brains of the mailing list and see.</div>
<div style><div><br></div><div style>I eventually ended up just using File::Temp::tmpnam and having tar output to that file, which I then added to a var called 'cleanup' that an after hook deletes. Then I sent with file_send as normal.</div>
<div style><br></div><div style>So, I suppose my main question is: would this work if I were somehow able to remove the Content-Length header? I'm thinking not and, if not, should I be using chunked transfer instead? Does Dancer support this?</div>
<div style><br></div><div style>CODE:</div><div><br></div><div>sub send_data {</div><div> Dancer::Continuation::Route::FileSent->new(</div><div> return_value => _send_data(@_)</div><div> )->throw</div>
<div>}</div><div><br></div></div><div style><div>sub _send_data {</div><div> my ($fh, %options) = @_;</div><div> my $env = Dancer::SharedData->request->env;</div><div><br></div><div> die "Must have streaming" if ! $env->{'psgi.streaming'};</div>
<div> die "Must provide content_type\n" unless $options{content_type};</div><div> die "Must provide filename\n" unless $options{filename};</div><div> binmode $fh;<br></div><div><br></div><div>
my $response = Dancer::SharedData->response() || Dancer::Response->new();</div><div> $response->header('Content-Type' => $options{content_type});</div><div> $response->push_header('Content-Disposition' =></div>
<div> "attachment; filename=\"$options{filename}\""</div><div> );</div><div><br></div><div> $response->streamed( sub {</div><div> my ( $status, $headers ) = @_;</div><div><br>
</div><div> return sub {</div><div> my $respond = shift;</div><div><br></div><div> my $writer = $respond->( [</div><div> $status,</div><div> $headers,</div>
<div> ] );</div><div><br></div><div> my $content = $response->content;</div><div><br></div><div> my $bytes = $options{'bytes'} || '43008';</div><div> my $buf;</div>
<div><br></div><div> while ( ( my $read = sysread $fh, $buf, $bytes ) != 0 ) {</div><div> $writer->write($buf);</div><div> }</div><div> };</div><div> }</div>
<div> );</div><div> return $response if $response;</div><div>}</div></div></div>