Link to home
Start Free TrialLog in
Avatar of cip
cip

asked on

Byte serving from ASP

I'm trying to implement byte serving from ASP. Byte serving is that technique that allows an http client to receive file chunks instead than a whole, such as acrobat reader when it display the first pages way before the entire file has been downloaded.

This is what I have implemented, using ASP and Jscript (which can be easily converted to vbscript anyway):

var adTypeBinary = 1

function set_range(range, filesize){
  /*
  Sets the first and last bytes of a range, given a range expressed as a string
  and the size of the file.

  If the end of the range is not specified, or the end of the range is greater
  than the length of the file, $last is set as the end of the file.

  If the begining of the range is not specified, the meaning of the value after
  the dash is "get the last n bytes of the file".

  If $first is greater than $last, the range is not satisfiable, and we should
  return a response with a status of 416 (Requested range not satisfiable).

  Examples:
  $range='0-499', $filesize=1000 => $first=0, $last=499 .
  $range='500-', $filesize=1000 => $first=500, $last=999 .
  $range='500-1200', $filesize=1000 => $first=500, $last=999 .
  $range='-200', $filesize=1000 => $first=800, $last=999 .

  */
  var dash  = range.indexOf('-');
  var first = Trim(range.substr(0, dash))
  var last  = Trim(range.substr(dash+1));
  if (first=='') {
    //suffix byte range: gets last n bytes
    var suffix = last;
    last       = filesize-1;
    first      = filesize-parseInt(suffix);
    if(first<0) first = 0;
  }
  else
  {
    if (last=='' || parseInt(last) > filesize-1)
          last = filesize-1;
  }
 
  first = parseInt(first);
  last  = parseInt(last);
 
  if (first > last){
    //unsatisfiable range
    Response.Status("416 Requested range not satisfiable");
    Response.addHeader("Content-Range", "*/" + filesize);
    Response.end
  }
 
  var returnobj = new Object();
  returnobj.first = first;
  returnobj.last  = last;       
 
  return returnobj;
}

function buffered_read(ostream, bytes, buffer_size)
{
      if (!buffer_size)
            buffer_size = 1024;
            
  /*
  Outputs up to $bytes from the file $file to standard output, $buffer_size bytes at a time.
  */
  var bytes_left = bytes;
  while(bytes_left > 0 && !ostream.EOS)
  {
    if(bytes_left > buffer_size)
      bytes_to_read = buffer_size;
    else
      bytes_to_read = bytes_left;
   
    bytes_left -= bytes_to_read;
   
    Response.BinaryWrite(ostream.Read(bytes_to_read));
    Response.Flush();
  }
}

function byteserve(filepath, filesize){
  /*
  Byteserves the file $filename.  

  When there is a request for a single range, the content is transmitted
  with a Content-Range header, and a Content-Length header showing the number
  of bytes actually transferred.

  When there is a request for multiple ranges, these are transmitted as a
  multipart message. The multipart media type used for this purpose is
  "multipart/byteranges".
  */
 
      Response.Clear
      var ostream = Server.CreateObject("ADODB.Stream")
      ostream.Open       
      ostream.Type = adTypeBinary
      ostream.LoadFromFile(filepath)       
      
      var boundary = "";
      var etag = '"f094b12d4e31c51:992"';
      var ipos = (""+Request.ServerVariables('HTTP_RANGE')).indexOf('bytes=')            
      
  ranges = new Array();
  if (Request.ServerVariables('REQUEST_METHOD')=='GET' && !isEmpty(Request.ServerVariables('HTTP_RANGE')) && (ipos >= 0))
  {
        range    = (""+Request.ServerVariables('HTTP_RANGE')).substr(ipos+6);
    boundary = '[lka9uw3et5vxybtp87ghq23dpu7djv84nhls9p]';//set a random boundary
    ranges   = range.split(",")
  }
 
  if (ranges.length > 0)
  {
             Response.status = "206 Partial content";
             Response.CacheControl = "No-store"
         
    Response.addHeader("Accept-Ranges", "bytes");
    if (ranges.length > 1)
    {
      /*
      More than one range is requested.
      */

      //compute content length
      content_length = 0;
      for (var i=0; i < ranges.length; i++)
      {
        rangeobj = set_range(ranges[i], filesize);
       
        content_length += ("\r\n--" + boundary + "\r\n").length;
        content_length += ("Content-type: application/pdf\r\n").length;
        content_length += ("Content-range: bytes " + rangeobj.first + "-" + rangeobj.last + "/" + filesize + "\r\n\r\n").length;
        content_length += rangeobj.last - rangeobj.first + 1;          
      }
                  content_length += ("\r\n--" + boundary + "--\r\n").length;
                  
      //output headers
      Response.addHeader("Content-Length", ""+content_length);
      Response.addHeader("ETag", etag);
      //see http://httpd.apache.org/docs/misc/known_client_problems.html for an discussion of x-byteranges vs. byteranges
      Response.ContentType = "multipart/byteranges; boundary=" + boundary
     
      //output the content
      for (var i=0; i < ranges.length; i++)
      {
        rangeobj = set_range(ranges[i], filesize);        
        Response.BinaryWrite("\r\n--" + boundary + "\r\n");
        Response.BinaryWrite("Content-type: application/pdf\r\n");
        Response.BinaryWrite("Content-range: bytes " + rangeobj.first + "-" + rangeobj.last + "/" + filesize + "\r\n\r\n");
        Response.Flush();
        ostream.position = rangeobj.first;
        buffered_read(ostream, rangeobj.last - rangeobj.first + 1);
      }
      Response.BinaryWrite("\r\n--" + boundary + "--\r\n");
      Response.Flush();
    } else {
      /*
      A single range is requested.
      */
      range = ranges[0];
      rangeobj = set_range(range, filesize);  
      Response.addHeader("Content-Length", (rangeobj.last - rangeobj.first));
      Response.addHeader("Content-Range", "bytes " + rangeobj.first + "-" + rangeobj.last + "/" + filesize);
      Response.addHeader("Content-Type", "application/pdf");
      ostream.position(rangeobj.first);
      buffered_read(ostream, rangeobj.last - rangeobj.first + 1);
    }
  }
  else
  {
    //no byteserving
    Response.addHeader("Accept-Ranges", "bytes");
    Response.addHeader("Content-Length", filesize);
    Response.addHeader("Content-Type", "application/pdf");
            buffered_read(ostream, filesize);
  }
  ostream.Close();
  ostream = null;
}

You can test the code using these simple lines:

filepath = Server.MapPath([pdf file to download]);
      
fs = Server.CreateObject("Scripting.fileSystemObject");
f = fs.getFile(filepath);
byteserve(filepath, f.size)

I have translated this from the equivalent php code found here:

http://www.coneural.org/florian/papers/04_byteserving.php

The ASP routne doesn't work. The headers seems ok, but after that the web page freezes and stops loading.

Anybody willing to help?
Avatar of wesbird
wesbird

Could it be that response.binarywrite is sending out unicode - I think I've been here before, take a look at http://www.alleged.org.uk/pdc/2003/asp.html 
Avatar of cip

ASKER

Thanks wesbird.

Do you think there is any way to handle this without going to a component? I have tried to read all the Response.Binary stuff into variants from a text file using ado.stream binary mode, but that didn't work too.

yes - I just need to find it again....
Yes, I think half the problem is that this part (and similar with quoted text):

        Response.BinaryWrite("\r\n--" + boundary + "\r\n");
        Response.BinaryWrite("Content-type: application/pdf\r\n");
        Response.BinaryWrite("Content-range: bytes " + rangeobj.first + "-" + rangeobj.last + "/" + filesize + "\r\n\r\n");

will be pumped out as unicode unless you explicitly convert the strings to bytes using widechartomultibyte or take a look at this: http://support.microsoft.com/default.aspx?scid=KB;en-us;q232580

for further ideas...

will post again when I have more.

Wes
https://www.experts-exchange.com/questions/21099501/Simple-Wscript-Shell-question.html contains a similar problem in reverse.

The solution I think is probably to convert the text strings using something like combination of chr(AscW(mid(...))) for rebuilding each character in the string for a quick inelegant solution just to make it work.  Take a look at my old thread in the link above for ideas.
Avatar of cip

ASKER

Hi wesbird,

I still cannot have it working. I am trying to keep it in javascript since I have to mix this code with lots of includes already developed.

I have tried to implement a binary streamed string function such as:

function stream_string(ss)
{
  for (i=0; i<ss.length; i++)
  {
     var b = (ss.charCodeAt(i) & 255)
     Response.BinaryWrite(b)
  }
}

But I get:

ASP 0106~Type Mismatch~An unhandled data type was encountered.

I have even tried:

function stream_string(ss)
{
  var os = Server.CreateObject(ADODB.Stream);
  os.Open;
  os.Type = 1;
  for (i=0; i<ss.length; i++)
  {
     var b = (ss.charCodeAt(i) & 255)
     os.write(b);
  }

  os.position = 0;
  Response.BinaryWrite(os.Read)
}

But now I get:

Arguments are of the wrong type, are out of acceptable range, or are in conflict with one another.
(at the "os.write(b)" line)

Is there any possible way to get a byte array so that it can be passed to Response.BinaryWrite() or ADODB.Stream.Write() using Javascript?
ASKER CERTIFIED SOLUTION
Avatar of wesbird
wesbird

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of cip

ASKER

Using Response.write instead of Reposen.BinaryWrite works perfectly fine! I can't believe I didn't make this test myself...

Lemme do another another couple of checks then I'll award the points. Thanks!
Avatar of cip

ASKER

Thanks wesbird, you've been extremely useful. Actually, I don't really understand WHY it is working, but it does. Shouldn't Response.write() send unicode characters? Maybe the acrobat plugin is able to parse them.