Потоковая передача файла журнала с использованием ajax не работает при втором запросе

#php #jquery #http-headers

#php #jquery #http-заголовки

Вопрос:

У меня есть очень большой текстовый файл журнала, который мне нужно передать в браузер. У меня есть PHP-приложение, работающее в фоновом режиме, которое добавляет информацию в файл журнала по мере выполнения своих функций. Я хотел бы, чтобы поддержка браузера просматривала журнал в режиме реального времени. Ожидаемое поведение заключается в том, что при первой загрузке он захватывает все содержимое файла, затем перезагружается со следующими 30 КБ и повторяется. У меня выполнен первоначальный запрос, но как только он пытается захватить следующие 30 КБ, я получаю следующую ошибку:

 An error occured :-(.
Reloading may help; no promises.
Uncaught Invalid integer accept-ranges: bytes
cache-control: public, must-revalidate, max-age=0
content-disposition: inline; filename=log.txt
content-length: 5747975
content-range: bytes 0-5747975/5747976
content-transfer-encoding: binary
content-type: text/plain;charset=UTF-8
date: Fri, 27 Nov 2020 17:46:54 GMT
last-modified: Fri, 27 Nov 2020 17:39:28  0000
pragma: no-cache
server: LiteSpeed
status: 206
x-litespeed-cache-control: no-cache
x-powered-by: PHP/7.4.12
 

Это потоковая передача txt-файла на стороне PHP (https://www.php.net/manual/en/function.readfile.php#86244 ):

 function smartReadFile($location, $filename, $mimeType='application/octet-stream')
{ 
  $size=filesize($location);
  $time=date('r',filemtime($location));

  if(!file_exists($location)) { header ("HTTP/1.0 404 Not Found");
    return;
  }
 
  $fm=@fopen($location,'rb');
  if(!$fm)
  { header ("HTTP/1.0 505 Internal server error");
    return;
  }
 
  $begin=0;
  $end=$size;
 
  if(isset($_SERVER['HTTP_RANGE']))
  { if(preg_match('/bytes=h*(d )-(d*)[D.*]?/i', $_SERVER['HTTP_RANGE'], $matches))
    { $begin=intval($matches[0]);
      if(!empty($matches[1]))
        $end=intval($matches[1]);
    }
  }
 
  if($begin>0||$end<$size)
    header('HTTP/1.0 206 Partial Content');
  else
    header('HTTP/1.0 200 OK'); 
 
  header("Content-Type: $mimeType");
  header('Cache-Control: public, must-revalidate, max-age=0');
  header('Pragma: no-cache'); 
  header('Accept-Ranges: bytes');
  header('Content-Length:'.($end-$begin));
  header("Content-Range: bytes $begin-$end/$size");
  header("Content-Disposition: inline; filename=$filename");
  header("Content-Transfer-Encoding: binaryn");
  header("Last-Modified: $time");
  header('Connection: close'); 
 
  $cur=$begin;
  fseek($fm,$begin,0);

  while(!feof($fm)amp;amp;$cur<$endamp;amp;(connection_status()==0))
  { print fread($fm,min(1024*16,$end-$cur));
    $cur =1024*16;
  }
}
 

Это сторона javascript, выполняющая запрос ajax (https://github.com/ukhas/js-logtail )

 (function () {

    var dataelem = "#data";
    var pausetoggle = "#pause";
    var scrollelems = ["html", "body"];
    
    var url = "?mod=log";
    var fix_rn = true;
    var load = 30 * 1024; /* 30KB */
    var poll = 1000; /* 1s */
    
    var kill = false;
    var loading = false;
    var pause = false;
    var reverse = true;
    var log_data = "";
    var log_file_size = 0;
    
    /* :-( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt */
    function parseInt2(value) {
        if(!(/^[0-9] $/.test(value))) throw "Invalid integer "   value;
        var v = Number(value);
        if (isNaN(v))                 throw "Invalid integer "   value;
        return v;
    }
    
    function get_log() {
        if (kill | loading) return;
        loading = true;
    
        var range;
        var first_load;
        var must_get_206;
        if (log_file_size === 0) {
            /* Get the last 'load' bytes */
            range = "-"   load.toString();
            first_load = true;
            must_get_206 = false;
        } else {
            /* Get the (log_file_size - 1)th byte, onwards. */
            range = (log_file_size - 1).toString()   "-";
            first_load = false;
            must_get_206 = log_file_size > 1;
        }
    
        /* The "log_file_size - 1" deliberately reloads the last byte, which we already
         * have. This is to prevent a 416 "Range unsatisfiable" error: a response
         * of length 1 tells us that the file hasn't changed yet. A 416 shows that
         * the file has been trucnated */
    
        $.ajax(url, {
            dataType: "text",
            cache: false,
            headers: {Range: "bytes="   range},
            success: function (data, s, xhr) {
                loading = false;
    
                var content_size;
    
                if (xhr.status === 206) {
                    var c_r = xhr.getResponseHeader("Content-Range");
                    if (!c_r)
                        throw "Server did not respond with a Content-Range";
    
                    log_file_size = parseInt2(c_r.split("/")[1]);
                    content_size = parseInt2(xhr.getAllResponseHeaders("Content-Length"));
                } else if (xhr.status === 200) {
                    if (must_get_206)
                        throw "Expected 206 Partial Content";   
                    content_size = log_file_size =
                            parseInt2(xhr.getResponseHeader("Content-Length"));
                } else {
                    throw "Unexpected status "   xhr.status;
                }
    
                /* amp;amp; data.length > load)
                    throw "Server's response was too long";*/
    
                var added = false;
    
                if (first_load) {
                    /* Clip leading part-line if not the whole file */
                    if (content_size < log_file_size) {
                        var start = data.indexOf("n");
                        log_data = data.substring(start   1);
                    } else {
                        log_data = data;
                    }
    
                    added = true;
                } else {
                    /* Drop the first byte (see above) */
                    log_data  = data.substring(1);
    
                    if (log_data.length > load) {
                        var start = log_data.indexOf("n", log_data.length - load);
                        log_data = log_data.substring(start   1);
                    }
    
                    if (data.length > 1)
                        added = true;
                }
    
                if (added)
                    show_log(added);
                setTimeout(get_log, poll);
            },
            error: function (xhr, s, t) {
                loading = false;
    
                if (xhr.status === 416 || xhr.status == 404) {
                    /* 416: Requested range not satisfiable: log was truncated. */
                    /* 404: Retry soon, I guess */
    
                    log_file_size = 0;
                    log_data = "";
                    show_log();
    
                    setTimeout(get_log, poll);
                } else {
                    throw "Unknown AJAX Error (status "   xhr.status   ")";
                }
            }
        });
    }
    
    function scroll(where) {
        for (var i = 0; i < scrollelems.length; i  ) {
            var s = $(scrollelems[i]);
            if (where === -1)
                s.scrollTop(s.height());
            else
                s.scrollTop(where);
        }
    }
    
    function show_log() {
        if (pause) return;
    
        var t = log_data;
    
        if (reverse) {
            var t_a = t.split(/n/g);
            t_a.reverse();
            if (t_a[0] == "") 
                t_a.shift();
            t = t_a.join("n");
        }
    
        if (fix_rn)
            t = t.replace(/n/g, "rn");
    
        $(dataelem).text(t);
        if (!reverse)
            scroll(-1);
    }
    
    function error(what) {
        kill = true;
    
        $(dataelem).text("An error occured :-(.rn"  
                         "Reloading may help; no promises.rn"   
                         what);
        scroll(0);
    
        return false;
    }
    
    $(document).ready(function () {
        window.onerror = error;
    
        /* If URL is /logtail/?noreverse display in chronological order */
        var hash = location.search.replace(/^?/, "");
        if (hash == "noreverse")
            reverse = false;
    
        /* Add pause toggle */
        $(pausetoggle).click(function (e) {
            pause = !pause;
            $(pausetoggle).text(pause ? "Unpause" : "Pause");
            show_log();
            e.preventDefault();
        });
    
        get_log();
    });
    
})();
 

Как только он переходит ко второму запросу с 206, я получаю следующую ошибку:
введите описание изображения здесь

Ответ №1:

Хорошо, я сильно изменил свой код, и иногда проще, чем лучше…

Код Js выглядит следующим образом:

 $(function(){
    $("#data").load("index.php?mod=logamp;do=refresh");
    var auto_refresh = setInterval(
    (function () {
        $("#data").load("index.php?mod=log");
    }), 1000);
    function ScrollDiv(){

        if(document.getElementById('data').scrollTop<(document.getElementById('data').scrollHeight-document.getElementById('data').offsetHeight)){-1
              document.getElementById('data').scrollTop=document.getElementById('data').scrollTop 1
              }
        else {/*document.getElementById('data').scrollTop=0;*/}
     }
     
     setInterval(ScrollDiv, 1)
});