Разделение файла для загрузки с jquery и php: решение - PullRequest
0 голосов
/ 30 апреля 2020

Вот решение, которое я реализовал, чтобы обойти проблему с веб-сайтом.

Давайте начнем с проблемы: 2 apache веб-серверы, использующие php. Серверы находятся за балансировщиком нагрузки. Балансировщик нагрузки и серверы "говорят" в https, но ... если вы пытаетесь загрузить файл размером более 1 МБ, затрачиваемое время составляет около 5 минут, а вместо этого - несколько секунд, если файл имеет размер 100 КБ или если вы используете прямое подключение к веб-серверу.

Итак, проблема в балансировщике нагрузки: я не менеджер сети и не системный менеджер, поэтому я не могу сказать, является ли это проблемой traffi c, неправильной конфигурацией балансировщика или чем-то другим. else.

Таким образом, один из обходных путей - попытаться отправить фрагменты размером 100 КБ (несколько секунд на каждую загрузку), а затем собрать все на стороне сервера с помощью сценария php. Еще один шаг вперед - использовать поддельный параллелизм, предлагаемый jquery (напомним, что javascript однопоточный), чтобы делать больше заявок одновременно.

Как?

Я написал полное решение (см. Ответ ниже), потому что я нашел только неполные решения или решение, ссылающееся на инструменты WordPress (которые я не использую).

Примечание: сеанс разделяется между серверами благодаря memcache, поэтому на стороне php мы будем работать с переменной $ _session, чтобы обеспечить доступность всех общих данных на всех веб-серверах.

1 Ответ

0 голосов
/ 30 апреля 2020

Так что это решение. Прежде всего, код html:

<script src="/js/lib/jquery.min.js" type="text/javascript" ></script><!-- jquery 3.4.1 -->
<form id="fupForm" enctype="multipart/form-data">
    <p id="dbi-upload-progress">Choose a file then click Upload.</p>
    <input id="dbi-file-upload" type="file" name="dbi_import_file" /><br><br>
    <input id="dbi-upload-submit" type="submit" value="Upload" />
</form>

Итак, начнем с раздела javascript. Вот переменные, используемые глобально:

var max_size   = 5$uploadedFileId='fileUpload';
$name        = $_FILES[$uploadedFileId]['name'];
$tmp_name    = $_FILES[$uploadedFileId]['tmp_name'];
$type        = $_FILES[$uploadedFileId]['type'];

if($_POST['init'] && $_POST['init']==1)  {
    // Init part, let's clean session and prepare for the upload
    if(isset($_SESSION[session_id()]['UPLOADED_FILE'])) {
        array_splice($_SESSION[session_id()]['UPLOADED_FILE'], 1);
        unset($_SESSION[session_id()]['UPLOADED_FILE']);
    }
    $_SESSION[session_id()]['UPLOADED_FILE'][0]='';
    die(new JsonSuccess("Init session ok")); // Print a simple json string and exit
} else if(substr($name, -5) === '.part') {
    // chunk upload part, this can be executed on different web servers in different threads
    $part=$_POST['part'];
    if(isset($_FILES[$uploadedFileId]['tmp_name']) && is_uploaded_file($_FILES[$uploadedFileId]['tmp_name'])) {
        if($fp = fopen($_FILES[$uploadedFileId]['tmp_name'],'rb')) {
            $contents = '';
            while (!feof($fp)) {
                $contents .= fread($fp, 8192);
            }
            $_SESSION[session_id()]['UPLOADED_FILE'][$part] = $contents;
            die(new JsonSuccess("Chunk ".$name.": loaded ".strlen($contents)." bytes"); // Print a simple json string and exit
        }
    }
} else {
    // Commit part: let's assemble the final file in /tmp folder
    $myfile = fopen("/tmp/".$name, "w")
        or die("Write error: /tmp/".$name);
    for ($row = 0; isset($_SESSION[session_id()]['UPLOADED_FILE'][$row]) ; $row++) {
        fwrite($myfile, $_SESSION[session_id()]['UPLOADED_FILE'][$row]);
    }
    fclose($myfile);

    // Clean the session
    array_splice($_SESSION[session_id()]['UPLOADED_FILE'], 1);
    unset($_SESSION[session_id()]['UPLOADED_FILE']);
    die(new JsonSuccess("Commit success!")); // print a simple json string and exit
}
1024; // Max upload size: 5MB var slice_size = 100*1024; // Max size of each fragment: 100 KB var concurrency = 4; // Max thread number var start_time_total=$.now(); // Time var, just to check upload performance var file = {}; // Pointer to file to upload var abort = false; // Flag used to abort operation var goals = 0; // Counter of parallel threads completed var percent = 0; // Percent of progress

Затем нам нужна функция инициализации jquery, чтобы предотвратить отправку стандартной формы и начать транзакцию:

$(document).ready(function(e){
    $("#fupForm").on('submit', function(e){
        e.preventDefault(); // prevent standard submit

        // Point to the file
        file = document.querySelector( '#dbi-file-upload' ).files[0];

        // Checking file size and extension
        if(file.size > max_size) {
            alert("Warning! \nFiles bigger than 5MB not supported");
            return;
        } else if(file.name.endsWith('.part')) {
            alert("Warning! \nFiles with .part extension not supported");
            return;
        }

        init_upload_file(); // Beginning the transaction
    });
});

Далее нам нужно вызвать один раз наш php скрипт для инициализации сеанса на стороне сервера, чтобы избежать ошибок параллелизма и совместного использования сеанса.

function init_upload_file() {
        // Global browser-side variables init, form disable and some console logging
        abort = false; goals = 0; percent = 0; start_time_total=$.now();
        $('#dbi-upload-submit').attr("disabled","disabled");
        $('#dbi-upload-progress').html('Upload init...');
        console.log("Start global transaction @"+ new Date(start_time_total));

        // First call to server with 'init' parameter
        var formData = new FormData();
        formData.append("init", 1);
        var start_time=$.now();
        console.log("start init call @"+new Date(start_time));
        $.ajax( {
            url: "/myPhpFileUrl", type: 'POST', dataType: 'json', contentType: false,
               cache: false, processData:false, data: formData,
            error: function( data ) {
                $('#dbi-upload-progress').html( 'Init call failed' ); // You can use an alert
                console.log( "Init call ko after "+($.now()-start_time)+" msecs; error: "+data );
                console.log( "End global transaction after "+ ($.now()-start_time_total)+" msecs");
                $('#dbi-upload-submit').removeAttr("disabled"); // Re-enable the form submit
            },
            success: function( data ) {
                $( '#dbi-upload-progress' ).html( 'Sending file...' );
                // Calling 'core' function to send multiple fragments
                for(var i=0;i<concurrency;i++)
                    upload_file( (i * (slice_size + 1)) );
            }
        });
}

Итак, здесь вы находитесь в «основной» функции, созданной для вызова (js - фальшивый) многопоточный режим:

function upload_file( start ) { // Start argument is the offset used to read a fragment
    if(abort) return; // Check if some thread is in error (abort the transaction)
    // Initialize file reader and put in blob var the fragment to send
    var reader = new FileReader();
    var next_slice = start + slice_size + 1;
    var blob = file.slice( start, next_slice );
    reader.onloadend = function( event ) {
        if ( event.target.readyState !== FileReader.DONE ) { return; }
        // preparing the form data to send and calling the server
        var slice_id=Math.floor(start / slice_size);
        var formData = new FormData();
        formData.append("fileUpload", blob, "." + slice_id + ".part");
        formData.append("part", slice_id);
        var start_time=$.now();
        console.log("Start sending slice "+slice_id+" @"+new Date(start_time));
        $.ajax( {
            url: "/myPhpFileUrl", type: 'POST', dataType: 'json', contentType: false,
            cache: false, processData:false, data: formData,
            error:   function( data ) {
                console.log("slice "+slice_id+" send error after "+($.now()-start_time)+" msecs");
                console.log("error: "+data );
                console.log("End global transaction after "+ ($.now()-start_time_total)+" msecs");
                if(abort) return; // Maybe some other thread was in error...
                // If it's the first error we inform the user and re-enable the form submit
                $('#dbi-upload-progress').html('Upload error'); // You can use an alert
                $('#dbi-upload-submit').removeAttr("disabled");
                abort=true;
            },
            success: function( data ) {
                if(abort) return; // Some other thread is in error, so we exit
                console.log("slice "+slice_id+" ok after "+($.now()-start_time)+" msecs");
                // Calculate progress and next offset
                var size_done = start + slice_size;
                var percent_done = Math.floor( ( size_done / file.size ) * 100 );
                var real_next_slice=start + ((slice_size + 1)* concurrency);
                if ( real_next_slice < file.size ) {
                    // Let's inform the user with the best upload % progress
                    if(percent < percent_done) percent=percent_done;
                    $( '#dbi-upload-progress' ).html( 'Sending file (' + percent + '%)' );
                    upload_file( real_next_slice ); // More to upload, call function recursively
                } else { 
                    // No more fragments to read
                    // if all fragments have been sent we commit the transaction 
                    goals++;
                    if(goals==concurrency) {
                        $('#dbi-upload-progress').html( 'Completing upload...' );
                        commit_uploaded_file();
                    }
                }
            }
        });
    };
    reader.readAsDataURL( blob );
}

Теперь последняя часть нашей jquery головоломки: вызов commit. В двух словах мы в последний раз призываем наш скрипт php собрать файл.

function commit_uploaded_file() {
    var formData = new FormData();
    formData.append("fileUpload", new Blob() , file.name);
    formData.append("fileType", file.type);
    var start_time=$.now();
    console.log("start final commit @"+new Date(start_time));
    $.ajax( {
        url: "/myPhpFileUrl", type: 'POST', dataType: 'json', contentType: false,
        cache: false, processData:false, data: formData,
        error:   function( data ) {
            $( '#dbi-upload-progress' ).html( 'Upload failed' );
            console.log( "Commit transaction ko after "+($.now()-start_time)+" msecs");
            console.log( "Errore: "+data );
            console.log( "End global transaction after "+ ($.now()-start_time_total)+" msecs");
            $('#dbi-upload-submit').removeAttr("disabled");
        },
        success: function( data ) {
            $( '#dbi-upload-progress' ).html( 'Upload completed' );
            console.log( "Commit transaction ok after "+($.now()-start_time)+" msecs");
            console.log( "End global transaction after "+ ($.now()-start_time_total)+" msecs");
            $('#dbi-upload-submit').removeAttr("disabled");
        }
    });
}

Вот и все, ребята!

... ок, просто шучу; -)

Нам нужен файл myPhpFileUrl php: ветвь if / else для управления фазой инициализации, фазой загрузки фрагмента и фазой фиксации.

*1024*
...