Documentation

File transfers

Bound uploads and stream downloads without loading whole files into memory.

Requires skyyware/stage 0.1.3 or later. PHP receives multipart uploads into temporary files before Stage dispatches the request. UploadedFile validates one upload; FileResponse streams a regular file as an attachment. Both leave storage and access decisions in your application.

Run a local round trip

Create a new application:

mkdir stage-files
cd stage-files
composer require skyyware/stage:^0.1.3
mkdir public

Create public/index.php:

<?php
declare(strict_types=1);

use Stage\Http\Application;
use Stage\Http\FileResponse;
use Stage\Http\Route;
use Stage\Http\UploadedFile;

require dirname(__DIR__) . '/vendor/autoload.php';

(new Application(
	new Route('POST', '/files', function (): FileResponse {
		$file = UploadedFile::fromPhp($_FILES['file'] ?? [], maxBytes: 10_000_000);
		return new FileResponse($file->path, $file->name, [
			'cache-control' => 'no-store',
			'content-security-policy' => "default-src 'none'; sandbox",
		]);
	}),
))->run();

Start a local server with PHP upload limits above the application's 10 MB limit. PHP's M settings use powers of 1024; the application's limit is decimal bytes.

php -d upload_max_filesize=10M -d post_max_size=11M \
  -d output_buffering=0 -d zlib.output_compression=0 \
  -S 127.0.0.1:8080 -t public public/index.php

In a second terminal, from the application directory:

printf 'hello from Stage\n' > example.txt
curl --fail --form file=@example.txt \
  --dump-header headers.txt --output returned.txt http://127.0.0.1:8080/files
cmp example.txt returned.txt
curl --include --request POST http://127.0.0.1:8080/files

cmp succeeds without output. The download has an attachment disposition, application/octet-stream, nosniff, and the file's length. The request without a file returns 400 with {"error":400}. Stop the server with Ctrl+C.

This local example returns the temporary upload immediately and retains nothing. PHP removes the temporary file when the request ends. To keep a file, validate the caller and application rules, then use move_uploaded_file with an application-generated destination outside public/. Check the move's result before recording success. Never use the client name as a storage path.

Serve a stored file

After the application has authorized access and resolved its own storage path, return a FileResponse from a GET handler:

return new FileResponse($authorizedPath, $displayName, [
	'cache-control' => 'no-store',
	'x-robots-tag' => 'noindex',
	'content-security-policy' => "default-src 'none'; sandbox",
]);

Its optional headers survive file delivery. A HEAD request uses the GET route and receives the same length without a body. GET handlers must account for that fallback before recording download counts or other side effects. Do not rebuild this response from $response->body; that property is empty because the file is not buffered. The HTTP reference lists validation, errors, and current limitations.

Set limits at each boundary

Stage's raw request limit remains 1 MiB. It does not limit PHP-parsed multipart data. Keep enable_post_data_reading enabled and read parsed fields from $_POST. Configure upload_max_filesize, a larger post_max_size, a writable upload temporary directory, and server request limits and timeouts. PHP's upload documentation describes this lifecycle.

If the whole POST exceeds post_max_size, PHP leaves both input arrays empty. UploadedFile::fromPhp then reports a missing file with 400. To report 413 for that case, enforce a total request limit in the server or application adapter before dispatch. Application validation cannot prevent PHP receiving a request before the script runs. See PHP's core configuration.

Stage reads downloads in 64 KiB chunks. Disable application output buffers and compression that would accumulate or transform the body. Keep stored files unchanged during delivery. Disk failure or concurrent truncation can end a response early after headers have been sent. File access rules, content checks, storage quotas, concurrent use, and retention need application-level tests.

Version and source

This guide targets Stage 0.1.4. Read the versioned source.