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
- Code on github (https://github.com/heryTz/resumable_upload_backend_php.git)
- I use local server
- here is my code
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 :)