Tom Gould

Products

TurboPush

Upload large files from the browser in chunks that survive dropped connections and page reloads, with a PHP endpoint that checks and reassembles them safely.

Demo Source on GitHub

The TurboPush demo: a drop zone for up to five files, with options to make chunks fail and to carry on after a reload. The TurboPush demo: a drop zone for up to five files, with options to make chunks fail and to carry on after a reload.

TurboPush is for web applications that need users to upload big files: video, archives, datasets. A single multi-gigabyte POST is fragile. One dropped connection and the user starts again, and PHP’s upload limits get in the way. TurboPush splits each file into chunks, sends several at once and puts the file back together on the server.

What it does

The browser client:

  • splits files into chunks, 1MB by default, and sends up to three requests at a time across all files
  • retries failed chunks with exponential backoff and random jitter, so many clients don’t retry at the same moment
  • can pause, resume and cancel an upload, and reports progress per file
  • with resumable: true, carries on after a page reload, asking the server which chunks it already has and sending only the rest
  • also speaks tus 1.0.0, the open protocol for resumable uploads, so it can upload to tusd or any other tus server

The PHP endpoint is the part that faces the internet, so it’s strict. It checks every ID, chunk index and size against limits you set. It cleans file names, blocks extensions a web server might execute or a browser might run scripts in, and can check an extension allowlist and the MIME type detected from the file’s contents. Chunks wait outside the web root, and each upload is locked so parallel or repeated requests can’t corrupt a file. Its error messages never reveal server paths. It’s same-origin only unless you list other origins.

How it’s built

The client is one TypeScript file with no dependencies, published as ESM and CommonJS builds with type declarations. It targets ES2020 and current browsers, and Node.js 18 or later.

The server is a single PHP class with no dependencies, for PHP 7.4 to 8.5. Each chunk is written to a unique temporary name and then renamed into place, so a retried or duplicate chunk can never leave a half-written file behind. A running total of bytes received stops a client storing more than it declared. Finalising holds the upload’s lock, joins the chunks into a temporary file in the upload directory and renames it into place. It’s idempotent: repeat it after success and you get the same answer.

The protocol between the two halves is small and written down in PROTOCOL.md, and a conformance suite tests any server against it, so either half can be paired with your own code. The tests include Jest for the client, PHPUnit with concurrency tests that start real parallel PHP processes, and interop tests that run the client against tusd and tus-php in Docker. GitHub Actions runs the client tests on Node.js 18 to 24 and the PHP tests on PHP 7.4 to 8.5.

How to use it

Install the PHP endpoint with Composer, and the client from GitHub:

composer require tomgould/turbopush
npm install github:tomgould/TurboPush

Then point the client at your endpoint:

import { TurboPush } from 'turbopush';

const uploader = new TurboPush({ endpoint: '/upload.php', resumable: true })
    .onProgress(files => files.forEach(f => console.log(`${f.fileName}: ${Math.round(f.percentage)}%`)));

input.addEventListener('change', () => uploader.addFiles(input.files).push());

And handle the uploads on the server, with the files stored outside the web root:

use TurboPush\TurboPushEndpoint;

(new TurboPushEndpoint('/var/lib/myapp/uploads'))
    ->setMaxFileSize(2 * 1024 * 1024 * 1024)
    ->setAllowedExtensions(['jpg', 'png', 'pdf', 'zip'])
    ->handle();

Check the user’s session before calling handle(): anyone who can reach the endpoint can upload. The demo lets you try it, and deletes each upload as soon as it’s checked.