PHP Luminova: Private File Delivery
The File Delivery class makes it easy to securely serve files from private storage, including support for generating temporary preview URLs that expire after a set time.
The File Delivery class provides tools for securely serving files from private storage directories to clients via HTTP. It handles efficient file streaming with proper headers, supports signed URLs for temporary access, and manages browser caching for faster repeat downloads.
Designed for high-performance delivery, it can handle large files with minimal overhead.
It also supports image resizing via the external library Peterujah\NanoBlock\NanoImage.
The
Luminova\Utility\Storage\FileDeliveryacts as a CDN helper. It serves private files from/writeable/storages/securely, making it ideal for building scalable content delivery systems in your application.
Examples
Serving an Image in the Browser
You can serve an image from private storage by defining a route like this:
// /app/Controllers/Http/CdnController.php
namespace App\Controllers\Http;
use Luminova\Base\Controller;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;
use Luminova\Utility\Storage\FileDelivery;
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 $image): int
{
FileDelivery::storage('images') // Subdirectory inside /writeable/storages/
->output($image);
return STATUS_SUCCESS;
}
}Access the image via URL:
https://example.com/cdn/assets/images/image.pngGenerating a Temporary Access URL
You can create a temporary signed URL that expires after a set time (e.g., 1 hour):
use Luminova\Utility\Storage\FileDelivery;
$hash = FileDelivery::storage('images')
->url('private-image.png', 3600);
// Full link to access the image
echo "https://example.com/cdn/private/image/{$hash}";Note:The
url()method returns only the signed hash for the file (e.g,private-image.png).You need to append it to your route to form a complete URL.For long URLs, you can use a URL shortener to simplify sharing.
Previewing a Temporary Signed Image
To serve an image via a temporary signed URL, create a route to handle the signed hash:
// /app/Controllers/Http/CdnController.php
namespace App\Controllers\Http;
use Luminova\Base\Controller;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;
use Luminova\Utility\Storage\FileDelivery;
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 tempImagePreview(string $imageHash): int
{
if (FileDelivery::storage('images')->temporal($imageHash)) {
return STATUS_SUCCESS;
}
return STATUS_ERROR;
}
}How it works:The route captures the signed image hash and uses
temporal()to validate and serve the file.If the hash is invalid or expired, it returns an error status.You can customize the error handling as needed.
Resizing and Serving Images
You can dynamically resize and serve images using query parameters:
// /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\Utility\Storage\FileDelivery;
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 imageResizePreview(string $filename): int
{
// Get 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;
try {
$storage = FileDelivery::storage('images');
// Resize or adjust quality if requested
if ($width > 0 || $height > 0 || $quality < 100) {
return $storage->outputImage($filename, $expiration, [
'width' => $width ?: null,
'height' => $height ?: null,
'quality' => $quality,
'ratio' => $keepRatio
], [
'Vary' => ''
]) ? STATUS_SUCCESS : STATUS_ERROR;
}
// Serve normally if no modifications
if ($storage->output($filename, $expiration, ['Vary' => ''])) {
return STATUS_SUCCESS;
}
} catch (Throwable $e) {
logger('debug', 'Image Delivery Error: ' . $e->getMessage());
}
return STATUS_ERROR;
}
}Example URL for resizing (GET):
https://example.com//cdn/assets/images/sample.jpg?width=300&height=200&quality=80&ratio=1Note:Resizing requires the external library
Peterujah\NanoBlock\NanoImage.
Class Definition
- Class namespace:
Luminova\Utility\Storage\FileDelivery - This class is marked as final and can't be subclassed
Methods
constructor
Initialize the constructor to have full access to the path where your file is located without any limitation like using storage method.
public __construct(string $filepath, bool $eTag = true, bool $weakEtag = false): mixedParameters:
| Parameter | Type | Description |
|---|---|---|
$filepath | string | Path to the private storage (e.g: /writeable/storages/images/). |
$eTag | bool | Whether to generate ETag header and apply validation as needed (default: true). |
$weakEtag | bool | Whether to use a weak ETag header or string (default: false). |
Note:Set
$eTagto true even if you are passing custom etag header.
storage
To initialize the FileDelivery with a base path, ETag option and return static instance.
public static storage(string $basepath, bool $eTag = true, bool $weakEtag = false): staticParameters:
| Parameter | Type | Description |
|---|---|---|
$basepath | string | Base path for file storage, (e.g: /images/). |
$eTag | bool | Whether to generate ETag header and apply validation as needed (default: true). |
$weakEtag | bool | Whether to use a weak ETag header or string (default: false). |
Return Value:
static - Return Instance of the FileDelivery class.
NoteYour files must be stored in the storage directory located in
/writeable/storages/.Additionally, you don't need to specify the/writeable/storages/in your$basepathparameter.Set$eTagto true even if you are passing custom etag header.
output
Read and outputs any file content in the browser, with appropriate headers.
public output(
string $basename,
int $expiry = 0,
array $headers = [],
int $length = (1 << 21),
int $delay = 0
): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$basename | string | The file name (e.g: image.png). |
$expiry | int | Expiry time in seconds for cache control (default: 0), indicating no cache. |
$headers | array<string,mixed> | An associative array for additional headers to set. |
$length | int | Optional size of each chunk to be read (default: 2MB). |
$delay | int | Optional delay in microseconds between chunk length (default: 0). |
Return Value:
bool - Returns true if file output is successfully, false otherwise.
By default
304,404and500headers will be set based file status and cache control.
outputImage
Modify and output image with appropriate headers.
This method uses an external package \Peterujah\NanoBlock\NanoImage, to customize the image's width, height, quality, and resizing ratio.
public outputImage(string $basename, int $expiry = 0, array $options = [], array $headers = []): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$basename | string | The file name (e.g: image.png). |
$expiry | int | Expiry time in seconds for cache control (default: 0), indicating no cache. |
$options | array<string,mixed> | Image filter options. |
$headers | array<string,mixed> | An associative array for additional headers to set. |
Filter Options
width(int) - Set new output width (default: 200).height(int) - Set new output height (default: 200).ratio(bool) - Specify weather to aspect ratio while resizing image (default: true).quality(int) - Set the output image quality (default: 100, 9 for PNG).
Return Value:
bool - Returns true if file output is successfully, false otherwise.
Throws:
\Luminova\Exceptions\RuntimeException - Throws if NanoImage image is not installed or if error occurred during image 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
NanoImagefollowing the below instructions.
Composer Command:
If you don't already have it, run this command
composer require peterujah/nano-imagetemporal
Temporally output file based on the URL hash key if valid and not expired.
public temporal(string $urlHash, array $headers = []): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$urlHash | string | The file encrypted URL hash. |
$headers | array | Additional headers to set. |
Return Value:
bool - Return true if file output is successful, false otherwise.
Note:This method uses
EncryptionandDecryptionclass to encrypt and decryptURLhash.
Throws:
\Luminova\Exceptions\EncryptionException - Throws if decryption failed or an error is encountered.
url
To generate a temporal signed URL with an expiration time for given filename.
public url(string $basename, int $expiry = 3600): string|falseParameters:
| Parameter | Type | Description |
|---|---|---|
$basename | string | The name of the file (e.g: filename.png). |
$expiry | int | The expiration time in seconds (default: 1 hour). |
Return Value:
string|false - Return generated based64 signed-hash URL for file, otherwise false.
Throws:
\Luminova\Exceptions\EncryptionException - Throws if encryption failed or an error occurred.
Note:This method uses {@see Luminova\Security\Encryption\Crypter} for generating signed URL.