Speed test for files (join in)

The question of whether your own Nextcloud is slow when transferring files, especially during bulk uploads, comes up again and again.

I have now come up with a test that all Nextcloud users who have a “normal” web server on the same hardware as Nextcloud, e.g. as a virtual host, can try out. This allows you to rule out or confirm Nextcloud as the cause of the error.

You can select any number of files to upload. In my test, I selected 10 images between 1.5 and 3 MB with a total size of 20 MB.

Now upload the images to Nextcloud as normal. You can use the WebUI or Nextcloud clients for this. Stop the time.

Create a “/speed” folder on the normal web server and copy the file listed below into it, call it up and upload all the files together again. The files are saved in “.” e.g. “/speed”. The basis of the file was from jcampbell1.

Result:
In my case, the upload took about 37 seconds for two Nextclouds and the two associated virtual web servers. I therefore have a throughput of around 4 MBit/s regardless of whether Nextcloud or upload script. I also tested it with other web editor with similar times. I think Nextcloud is therefore not a bottleneck and does not require optimisation for upload (asynchronous DSL), at least for me.

If you like, you can also do an upload test. Test it with Nextcloud and my script or another web editor. Please post your results.

index.php

<?php
setlocale(LC_ALL, 'en_US.UTF-8');
$tmp = realpath($_REQUEST['file'] ?? '.');
if ($tmp === false || substr($tmp, 0, strlen(__DIR__)) !== __DIR__) {
    http_response_code(403);
    exit('Forbidden');
}

if (!isset($_COOKIE['_sfm_xsrf'])) {
    setcookie('_sfm_xsrf', bin2hex(random_bytes(16)));
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if ($_COOKIE['_sfm_xsrf'] !== ($_POST['xsrf'] ?? '') || !$_POST['xsrf']) {
        http_response_code(403);
        exit('XSRF Failure');
    }

    if ($_POST['do'] === 'upload') {
        $chunk = isset($_POST['chunk']) ? intval($_POST['chunk']) : 0;
        $chunks = isset($_POST['chunks']) ? intval($_POST['chunks']) : 0;

        $file = $_FILES['file_data'] ?? null;
        if ($file && is_uploaded_file($file['tmp_name'])) {
            $name = $file['name'];
            if (!preg_match('/\.(html|htm|php|php[2-5]|phtml|pl|py|jsp|asp|sh|cgi|js|htaccess)$/i', $name)) {
                $targetPath = $name . '.part';
                
                if ($chunk === 0) {
                    move_uploaded_file($file['tmp_name'], $targetPath);
                } else {
                    file_put_contents($targetPath, file_get_contents($file['tmp_name']), FILE_APPEND);
                }

                if ($chunks === 0 || $chunk === $chunks - 1) {
                    rename($targetPath, $name);
                    exit(json_encode(['success' => true]));
                } else {
                    exit(json_encode(['success' => true, 'chunk' => $chunk]));
                }
            }
        }
        http_response_code(400);
        exit(json_encode(['error' => 'Upload failed']));
    }
}

$MAX_UPLOAD_SIZE = min(
    filter_var(ini_get('post_max_size'), FILTER_SANITIZE_NUMBER_INT) * 1024 * 1024,
    filter_var(ini_get('upload_max_filesize'), FILTER_SANITIZE_NUMBER_INT) * 1024 * 1024
);
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Upload</title>
    <style>
        body {font-family: "lucida grande","Segoe UI",Arial, sans-serif; padding:1em;margin:0;}
        #file_drop_target {width:500px; padding:12px 0; border: 4px dashed #ccc;font-size:12px;color:#ccc;
            text-align: center;float:right;margin-right:20px;}
        #file_drop_target.drag_over {border: 4px dashed #96C4EA; color: #96C4EA;}
        #upload_progress {padding: 4px 0;}
        #upload_progress .error {color:#a00;}
        #upload_progress > div { padding:3px 0;}
        .progress_track {display:inline-block;width:200px;height:10px;border:1px solid #333;margin: 0 4px 0 10px;}
        .progress {background-color: #82CFFA;height:10px; }
    </style>
</head>
<body>
    <div id="top">
        <div id="file_drop_target">
            Drop files here to upload
            <b>or</b>
            <input type="file" multiple>
        </div>
    </div>
    <div id="upload_progress"></div>

    <script>
    document.addEventListener('DOMContentLoaded', () => {
        const XSRF = document.cookie.match('(^|; )_sfm_xsrf=([^;]*)')?.pop() ?? '';
        const MAX_UPLOAD_SIZE = <?php echo $MAX_UPLOAD_SIZE ?>;
        const dropTarget = document.getElementById('file_drop_target');
        const uploadProgress = document.getElementById('upload_progress');

        dropTarget.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropTarget.classList.add('drag_over');
        });

        dropTarget.addEventListener('dragleave', (e) => {
            e.preventDefault();
            dropTarget.classList.remove('drag_over');
        });

        dropTarget.addEventListener('drop', (e) => {
            e.preventDefault();
            dropTarget.classList.remove('drag_over');
            Array.from(e.dataTransfer.files).forEach(uploadFile);
        });

        document.querySelector('input[type=file]').addEventListener('change', (e) => {
            Array.from(e.target.files).forEach(uploadFile);
        });

        function uploadFile(file) {
            const folder = window.location.hash.substr(1);
            if (/\.(html|htm|php|php[2-5]|js|htaccess)$/i.test(file.name)) {
                showError('File has incorrect extension', file, folder);
                return;
            }
            if (file.size > MAX_UPLOAD_SIZE) {
                showError(`File size exceeds ${formatFileSize(MAX_UPLOAD_SIZE)}`, file, folder);
                return;
            }

            const row = createProgressRow(file, folder);
            uploadProgress.appendChild(row);

            const chunkSize = 1024 * 1024;
            let start = 0;
            const chunks = Math.ceil(file.size / chunkSize);
            const uploadStartTime = new Date().getTime();

            const uploadChunk = () => {
                const end = Math.min(start + chunkSize, file.size);
                const chunk = file.slice(start, end);

                const formData = new FormData();
                formData.append('file_data', chunk, file.name);
                formData.append('file', folder);
                formData.append('xsrf', XSRF);
                formData.append('do', 'upload');
                formData.append('chunk', start / chunkSize);
                formData.append('chunks', chunks);

                const xhr = new XMLHttpRequest();
                xhr.open('POST', '?', true);
                xhr.timeout = 300000;

                xhr.upload.onprogress = (e) => {
                    if (e.lengthComputable) {
                        const percentComplete = ((start + e.loaded) / file.size) * 100;
                        row.querySelector('.progress').style.width = percentComplete + '%';
                    }
                };

                xhr.onload = function() {
                    if (xhr.status === 200) {
                        try {
                            const response = JSON.parse(xhr.responseText);
                            if (response.success) {
                                start += chunkSize;
                                if (start < file.size) {
                                    uploadChunk();
                                } else {
                                    const uploadEndTime = new Date().getTime();
                                    const uploadTime = (uploadEndTime - uploadStartTime) / 1000;
                                    row.classList.add('success');
                                    row.querySelector('.status').textContent = `Successfully uploaded in ${uploadTime.toFixed(2)} seconds`;
                                }
                            } else {
                                throw new Error(response.error || 'Unknown error');
                            }
                        } catch (error) {
                            row.classList.add('error');
                            row.querySelector('.status').textContent = `Upload error: ${error.message}`;
                        }
                    } else {
                        row.classList.add('error');
                        row.querySelector('.status').textContent = `Upload error: ${xhr.statusText}`;
                    }
                };

                xhr.onerror = function() {
                    row.classList.add('error');
                    row.querySelector('.status').textContent = 'Network error during upload';
                };

                xhr.ontimeout = function() {
                    row.classList.add('error');
                    row.querySelector('.status').textContent = 'Upload timed out';
                };

                xhr.send(formData);
            };

            uploadChunk();
        }

        function createProgressRow(file, folder) {
            const row = document.createElement('div');
            row.innerHTML = `
                <a href="${folder}${file.name}" class="name">${folder}${file.name}</a>
                <div class="progress_track"><div class="progress"></div></div>
                <span class="size">${formatFileSize(file.size)}</span>
                <span class="status"></span>
            `;
            return row;
        }

        function showError(message, file, folder) {
            const errorRow = document.createElement('div');
            errorRow.className = 'error';
            errorRow.textContent = `Error: ${folder ? folder + '/' : ''}${file.name} - ${message}`;
            uploadProgress.appendChild(errorRow);
            setTimeout(() => errorRow.remove(), 5000);
        }

        function formatFileSize(bytes) {
            const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
            if (bytes === 0) return '0 Bytes';
            const i = Math.floor(Math.log(bytes) / Math.log(1024));
            return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
        }
    });
    </script>
</body>
</html>

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.