Luminova Framework

PHP Luminova: Private Storage File Response

Last updated: 2026-01-12 12:37:27

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\FileResponse functions 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.png

How 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=1

How it works:

  • width – Target width in pixels
  • height – Target height in pixels
  • quality – 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): mixed

Parameters:

ParameterTypeDescription
$basepathstringBase path to file storage within /writeable/ (e.g., storages/images/foo/).
$eTagboolWhether to generate ETag headers and perform validation (default: true).
$weakEtagboolWhether 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 $eTag to 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): self

Parameters:

ParameterTypeDescription
$basepathstringRelative storage path within /writeable/storages/ (e.g: /images/).
$eTagboolWhether to generate ETag headers and perform validation (default: true).
$weakEtagboolWhether 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 $basepath parameter; it is prepended automatically.
  • Set $eTag to 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|false

Parameters:

ParameterTypeDescription
$basenamestringThe file name to sign (e.g: filename.png).
$expiryintExpiration time in seconds (default: 3600 1 hour).
$timezoneDateTimeZone|string|nullOptional 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
): bool

Parameters:

ParameterTypeDescription
$basenamestringFile name relative to the configured base path (e.g: image.png).
$expiryintCache lifetime in seconds (0 disables caching).
$headersarray<string,mixed>Additional response headers.
$lengthintOptional chunk size for streaming (default: 2MB).
$delayintOptional 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 = []): bool

Parameters:

ParameterTypeDescription
$basenamestringFile name relative to base path (e.g., image.png).
$expiryintCache lifetime in seconds (0 disables caching).
$optionsarray<string,mixed>Image processing options:
$headersarray<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\NanoImage following the below instructions.

Install vis Composer:

If you don't already have it, run this command

composer require peterujah/nano-image

sendSigned

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): bool

Parameters:

ParameterTypeDescription
$fileHashstringEncrypted temporal file token. (e.g: XYZ...).
$headersarray<string,mixed>Additional response headers.
$lengthintOptional chunk size for streaming (default: 2MB).
$delayintOptional delay between chunks in microseconds (default: 0).

Return Value:

bool - Returns true if the file is successfully streamed, false otherwise.

Note:This method uses Encryption and Decryption class to encrypt and decrypt URL hash.

Throws: