1

goal

Make an API that is used to upload a file with resumed in the event of a connection cut.I' m trying to imitate this approach with nodeJS (https://javascript.info/resume-upload)

problem

As soon as the client-server connection is aborted. The PHP script stops its execution, except I need to store the last uploaded byte at this time. I have already tested something with ignore_user_abort and connection_status but I see that the script is still stopping its execution.

information

upload.php (backend php)

<?php 
ignore_user_abort(true);
set_time_limit(0);

header('Access-Control-Allow-Origin: *');

define('FILE_ID', 'HTTP_X_FILE_ID');
define('START_BYTE', 'HTTP_X_START_BYTE');
define('FILE_SIZE', 'HTTP_X_FILE_SIZE');
define('VIDEO_TMP', '/tmp');
define('UPLOADED_FILE_STATUS', 'status.json');

$statusPath = __DIR__ . '/' . UPLOADED_FILE_STATUS;

// GET last byte uploaded
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    $statusFile = file_get_contents($statusPath);
    $uploadedFileStatus = json_decode($statusFile, true);
    $byte = $uploadedFileStatus[$_SERVER[FILE_ID]] ?? 0;
    echo json_encode([
        'byte' => $byte, 
        'uplo$uploadedFileStatus' => $uploadedFileStatus
    ]);

// POST file uploaded
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $fileId = $_SERVER[FILE_ID];
    $startByte = (int) $_SERVER[START_BYTE] ?? 0;
    $fileSize = (int) $_SERVER[FILE_SIZE];

    if (!$fileId) {
        http_response_code(400);
        echo json_encode([ 'message' => 'file id not found' ]);
        return;
    }
    
    $inputHandler = fopen('php://input', "r");
    $videoTmp = __DIR__ . VIDEO_TMP . '/' . $fileId;
    $fileHandler = null;
    $dataResponse = array();

    $statusFile = file_get_contents($statusPath);
    $uploadedFileStatus = json_decode($statusFile, true);

    if (!$fileId) $uploadedFileStatus[$fileId] = 0;
    
    if (!$startByte) {
        $uploadedFileStatus[$fileId] = 0;
        $fileHandler = fopen($videoTmp, 'w+');
    } else {
        $fileHandler = fopen($videoTmp, 'a+');
    }

    while(true) {
        $buffer = fgets($inputHandler, 796);
        if (!$buffer) {
            break;
        }
        if(connection_status() != CONNECTION_NORMAL) {
            // NEVER CALL WHEN CLIENT ABORT CONNECTION
            file_put_contents($statusPath, json_encode($uploadedFileStatus, JSON_PRETTY_PRINT));
            break;
        }
        fwrite($fileHandler, $buffer);
        $uploadedFileStatus[$fileId] += strlen($buffer);
    }

    fclose($inputHandler);
    fclose($fileHandler);
    
    if ($uploadedFileStatus[$fileId] === $fileSize) {
        $dataResponse['videoPath'] = VIDEO_TMP . '/'. $fileId;
        $dataResponse['message'] = 'finished';
        $uploadedFileStatus[$fileId] = 0;
        rename($videoTmp, __DIR__ . '/upload/' . $fileId);
        file_put_contents($statusPath, json_encode($uploadedFileStatus, JSON_PRETTY_PRINT));
    } else {
        $dataResponse['message'] = 'unfinished';
        file_put_contents($statusPath, json_encode($uploadedFileStatus, JSON_PRETTY_PRINT));
    }
    echo json_encode($dataResponse);
}

index.php (upload interface)

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Upload</title>
</head>
<body>
    <div>
        <input type="file" name="file">
        <button>upload</button>
    </div>
    <script src="upload.js"></script>
</body>
</html>

upload.js (client javascript)

let file

document.querySelector('input').addEventListener('change', e => {
    file = e.target.files[0]
})

document.querySelector('button').addEventListener('click', async e => {
    if (file) {
        let fileId = `${file.size}-${file.lastModified}-${file.name}`
        let startByte = await getLastByteUploaded(fileId)
        console.log('startByte', startByte)
        let xhr = new XMLHttpRequest()
        xhr.open('POST', 'http://localhost:8080/upload.php', true)
        xhr.responseType = 'json'
        xhr.setRequestHeader('X-Start-Byte', startByte)
        xhr.setRequestHeader('X-File-Id', fileId)
        xhr.setRequestHeader('X-File-Size', file.size)
        xhr.send(file.slice(startByte))
        xhr.onload = e => {
            console.log('onload', xhr.response.message)
        }
        xhr.onerror = e => {
            console.log('error', xhr)
        }
    }
})

async function getLastByteUploaded(fileId) {
    let response = await fetch('http://localhost:8080/upload.php', {
        headers: {
            'X-File-Id': fileId
        }
    })
    if (response.status != 200) {
        throw new Error("Can't get uploaded bytes: " + response.statusText);
    }
    let data = await response.json()
    return parseInt(data.byte)
}

All comments are welcome :)

Hery
  • 372
  • 2
  • 4
  • 11
  • Linking these relevant questions: [Will a script continue to run even after closing a page?](https://stackoverflow.com/questions/5997102/will-a-script-continue-to-run-even-after-closing-a-page) and [Does php execution stop after a user leaves the page?](https://stackoverflow.com/questions/1280291/does-php-execution-stop-after-a-user-leaves-the-page) – Wyck May 08 '21 at 15:06

0 Answers0