PHP Luminova: Private Storage File Response
Securely serve files from the writeable or storage directory with File Response. Supports temporary signed file URLs that expire, browser caching, and optional image resizing.
The File Response class provides methods for serving files stored in private directories over HTTP.
It is designed to stream files securely from non-public storage locations, ensuring correct HTTP headers, efficient memory usage, and proper browser caching behavior. Files are delivered without exposing internal file paths, reducing the risk of unauthorized access.
Key capabilities include:
- Efficient streaming of large files with minimal memory overhead
- Support for signed URLs to allow temporary, time-limited access
- Automatic handling of cache-related headers for repeat requests
- Optional image resizing through the external library
Peterujah\NanoBlock\NanoImage
Luminova\Storage\FileResponsefunctions as a CDN-style delivery layer for private storage.It serves files from/writeable/storages/while maintaining access control, making it suitable for secure media delivery and protected file distribution.
Examples
Serving Private Files
This example shows how to serve an image stored in a private directory so it can be viewed in a browser.
The image is not placed in a public folder. Instead, it is streamed securely through a controller route.
// /app/Controllers/Http/CdnController.php
namespace App\Controllers\Http;
use Luminova\Base\Controller;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;
use Luminova\Storage\FileResponse;
use App\Errors\Controllers\ErrorController;
#[Prefix(pattern: '/cdn(:root)', onError: [ErrorController::class, 'onAssetsError'])]
class CDNController extends Controller
{
#[Route('/cdn/assets/images/([a-zA-Z0-9-]+\.(?:png|jpg|jpeg|gif|svg|webp))', methods: ['GET'])]
public function showImage(string $imageName): int
{
FileResponse::storage('images') // Folder inside /writeable/storages/
->send($imageName); // e.g person.png
return STATUS_SUCCESS;
}
}Access the image using:
https://example.com/cdn/assets/images/person.pngHow this works:
- Images are stored in
/writeable/storages/images/ - The route captures the image file name from the URI segment
FileResponse::send()streams the file to the browser- The real file path is never exposed
Temporary Signed Files
You can generate a temporary signed Hash for a private file.The link automatically expires after a defined time.
Example:
Create a link that expires after 1 hour (3600 seconds).
use Luminova\Storage\FileResponse;
$hash = FileResponse::storage('images')
->sign('person.png', 3600);
// Full URL used to access the image
echo "https://example.com/cdn/private/image/{$hash}";Note:
- The
sign()method returns only a signed hash- The hash represents the file and its expiration time
- You must append the hash to your route to form a full URL
- For long links, you may use a URL shortener if needed (e.g, https://tinyurl.com/)
Serving a Temporary Signed Files
To serve a file using the signed hash, define a route that validates and outputs it.
// /app/Controllers/Http/CdnController.php
namespace App\Controllers\Http;
use Luminova\Base\Controller;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;
use Luminova\Storage\FileResponse;
use App\Errors\Controllers\ErrorController;
#[Prefix(pattern: '/cdn(:root)', onError: [ErrorController::class, 'onAssetsError'])]
class CDNController extends Controller
{
#[Route('/cdn/private/image/(:string)', methods: ['GET'])]
public function showSignedImages(string $imageHash): int
{
if (FileResponse::storage('images')->sendSigned($imageHash)) {
return STATUS_SUCCESS;
}
return STATUS_ERROR;
}
}How this works:
- The route receives the signed hash from the URI segment
sendSigned()checks if the hash is valid and not expired- If valid, the file is streamed to the client
- If invalid or expired, an error response is returned
This approach allows you to share private files securely without making them public or permanent.
Serving Resized Images
You can serve images dynamically resized using query parameters. The system adjusts width, height, quality, and aspect ratio without modifying the original file.
// /app/Controllers/Http/CdnController.php
namespace App\Controllers\Http;
use Throwable;
use Luminova\Base\Controller;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;
use Luminova\Storage\FileResponse;
use App\Errors\Controllers\ErrorController;
use function Luminova\Funcs\logger;
#[Prefix(pattern: '/cdn(:root)', onError: [ErrorController::class, 'onAssetsError'])]
class CDNController extends Controller
{
#[Route('/cdn/assets/images/([a-zA-Z0-9-]+\.(?:png|jpg|jpeg|gif|svg|webp))', methods: ['GET'])]
private function showResizedImages(string $filename): int
{
// Read query parameters
$width = (int) $this->request->input('width', 0);
$height = (int) $this->request->input('height', 0);
$quality = (int) $this->request->input('quality', 100);
$keepRatio = (bool) $this->request->input('ratio', 1);
$expiration = 3600; // Cache time in seconds
$headers = ['Vary' => ''];
try {
$storage = FileResponse::storage('images');
// Resize or adjust quality if requested
if ($width > 0 || $height > 0 || $quality < 100) {
$options = [
'width' => $width ?: null,
'height' => $height ?: null,
'quality' => $quality,
'ratio' => $keepRatio
];
return $storage->sendImage($filename, $expiration, $options, $headers)
? STATUS_SUCCESS
: STATUS_ERROR;
}
// Serve normally if no modifications requested
if ($storage->send($filename, $expiration, $headers)) {
return STATUS_SUCCESS;
}
} catch (Throwable $e) {
logger('debug', 'Image Delivery Error: ' . $e->getMessage());
}
return STATUS_ERROR;
}
}Example URL for resizing:
https://example.com/cdn/assets/images/sample.jpg?width=300&height=200&quality=80&ratio=1How it works:
width– Target width in pixelsheight– Target height in pixelsquality– Image quality (0–100)ratio– Keep original aspect ratio (1= true,0= false)- The original file is never modified
- The system caches resized images for faster delivery (
$expiration)
Note:Resizing requires the external library
Peterujah\NanoBlock\NanoImage.
Class Definition
- Class namespace:
Luminova\Storage\FileResponse - This class is marked as final and can't be subclassed
Methods
constructor
Initialize a FileResponse instance with a base path and ETag configuration.
public __construct(string $basepath, bool $eTag = true, bool $weakEtag = false): mixedParameters:
| Parameter | Type | Description |
|---|---|---|
$basepath | string | Base path to file storage within /writeable/ (e.g., storages/images/foo/). |
$eTag | bool | Whether to generate ETag headers and perform validation (default: true). |
$weakEtag | bool | Whether to use weak ETag headers (default: false). |
Throws:
\Luminova\Exceptions\FileException - If the path is invalid or does not exist.
Note:
- Files must reside in the
/writeable/directory.Set$eTagto true even if you provide a custom ETag header,otherwise caching and validation may not behave as expected.
storage
Create a FileResponse instance using the storage directory.
public static storage(string $basepath, bool $eTag = true, bool $weakEtag = false): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$basepath | string | Relative storage path within /writeable/storages/ (e.g: /images/). |
$eTag | bool | Whether to generate ETag headers and perform validation (default: true). |
$weakEtag | bool | Whether to use weak ETag headers (default: false). |
Return Value:
self - Returns a new configured instance of FileResponse.
Throws:
\Luminova\Exceptions\FileException - If the path is invalid or does not exist.
Example:
use Luminova\Storage\FileResponse;
$cdn = FileResponse::storage('images/photos');Note:
- Files must reside in the
/writeable/storages/directory.- Do not include
/writeable/storages/in the$basepathparameter; it is prepended automatically.- Set
$eTagto true even if passing a custom ETag header.
sign
Generates a temporal signed token for a file with an expiration time.
The token encodes the filename, expiry duration, timestamp, and timezone, then encrypts the payload using Crypter.
public sign(string $basename, int $expiry = 3600, DateTimeZone|string|null $timezone = null): string|falseParameters:
| Parameter | Type | Description |
|---|---|---|
$basename | string | The file name to sign (e.g: filename.png). |
$expiry | int | Expiration time in seconds (default: 3600 1 hour). |
$timezone | DateTimeZone|string|null | Optional timezone for timestamp generation. |
Return Value:
string|false - Returns a base64-encoded encrypted token, or false if the file does not exist.
Throws:
\Luminova\Exceptions\EncryptionException - If encryption fails.
Note:This method uses Luminova\Security\Encryption\Crypter for generating signed file hash.
send
Sends a file to the client with proper cache and response headers.
The method resolves the file path, applies cache validation (ETag / Last-Modified), sets headers, and streams the file in chunks to the client.
public send(
string $basename,
int $expiry = 0,
array $headers = [],
int $length = (1 << 21),
int $delay = 0
): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$basename | string | File name relative to the configured base path (e.g: image.png). |
$expiry | int | Cache lifetime in seconds (0 disables caching). |
$headers | array<string,mixed> | Additional response headers. |
$length | int | Optional chunk size for streaming (default: 2MB). |
$delay | int | Optional delay between chunks in microseconds (default: 0). |
Return Value:
bool - Returns true on successful output or cache hit, false on failure.
Throws:
\Luminova\Exceptions\RuntimeException - Throws if an error occurred during file processing.
Note:It automatically sends 304, 404, or 500 responses when applicable.
sendImage
Resizes an image and outputs it to the client with proper headers.
This method opens the given image file, optionally resizes it using width, height, and aspect ratio options, sets cache and response headers, and outputs the image content. Returns true on success, false otherwise.
public sendImage(string $basename, int $expiry = 0, array $options = [], array $headers = []): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$basename | string | File name relative to base path (e.g., image.png). |
$expiry | int | Cache lifetime in seconds (0 disables caching). |
$options | array<string,mixed> | Image processing options: |
$headers | array<string,mixed> | Additional response headers. |
Image Options
width(int) - Output width (default: 200 if resizing).height(int) - Output height (default: 200 if resizing).ratio(bool) - Preserve aspect ratio when resizing (default: true).quality(int) - Output quality e.g, JPEG 0–100 and PNG 0–9 (default: 100, 9 for PNG).
Return Value:
bool - Returns true if image is output successfully, false otherwise.
Throws:
\Luminova\Exceptions\RuntimeException - If NanoImage is not installed or an error occurs during processing.
Note:This method relay on external image library to modify the width and height.To use this method you need to install Peter Ujah's
Peterujah\NanoBlock\NanoImagefollowing the below instructions.
Install vis Composer:
If you don't already have it, run this command
composer require peterujah/nano-imagesendSigned
Validates a temporal signed file token and streams the file if valid.
The token must contain the filename, issue timestamp, expiry duration,and timezone, encrypted using Crypter. If the token is invalid, malformed, or expired, a 404 response is returned.
public sendSigned(string $fileHash, array $headers = [], int $length = (1 << 21), int $delay = 0): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$fileHash | string | Encrypted temporal file token. (e.g: XYZ...). |
$headers | array<string,mixed> | Additional response headers. |
$length | int | Optional chunk size for streaming (default: 2MB). |
$delay | int | Optional delay between chunks in microseconds (default: 0). |
Return Value:
bool - Returns true if the file is successfully streamed, false otherwise.
Note:This method uses
EncryptionandDecryptionclass to encrypt and decryptURLhash.
Throws:
- \Luminova\Exceptions\EncryptionException - If token decryption fails.
- \Luminova\Exceptions\RuntimeException - Throws if an error occurred during file processing.