Storage
This guide covers the framework's storage layer: configuring disks, reading/writing files, safe path handling, URL generation, and error handling.
Overview
- Storage core:
Glueful\Storage\StorageManagerwraps Flysystem and manages multiple disks based onconfig/storage.php. - Path safety:
Glueful\Storage\PathGuardvalidates and normalizes paths (prevents traversal, null bytes, and disallows absolute paths by default). - URLs:
Glueful\Storage\Support\UrlGeneratorformats public URLs using diskbase_url/cdn_base_url. - Errors:
Glueful\Storage\Exceptions\StorageExceptionwraps Flysystem exceptions with machine‑readable reason + suggested HTTP status. - DI: Provided via the StorageProvider with a string alias
storage.
Configuration
Edit config/storage.php to define your disks.
Basic Configuration
return [
// Default disk (respects STORAGE_DEFAULT_DISK, or falls back to STORAGE_DRIVER)
'default' => env('STORAGE_DEFAULT_DISK', env('STORAGE_DRIVER', 'uploads')),
// PathGuard configuration
'path_guard' => [
'allow_absolute' => false, // Reject absolute paths by default
'max_path_length' => 4096, // Maximum path length
// Traversal ('..') and null bytes ("\0") are always rejected
],
'disks' => [
// Local filesystem
'uploads' => [
'driver' => 'local',
'root' => dirname(__DIR__) . '/storage/uploads',
'visibility' => 'private', // 'private' or 'public'
'base_url' => env('CDN_URL'), // Public URL base
],
// In-memory filesystem (great for testing)
'memory' => [
'driver' => 'memory',
],
],
];
Config files have no $context: use env() and plain paths (dirname(__DIR__)), never
config($context, …).
Core drivers: local, memory.
Cloud Disks (glueful/storage-*)
Since framework 1.54 the cloud drivers ship as first-party packs. Install the one you use; a disk
whose driver has no pack fails fast with an UnsupportedStorageDriverException naming the package.
composer require glueful/storage-s3 && php glueful extensions:enable storage-s3 # driver: s3 (S3, R2, MinIO, Spaces, Wasabi)
composer require glueful/storage-gcs && php glueful extensions:enable storage-gcs # driver: gcs
composer require glueful/storage-azure && php glueful extensions:enable storage-azure # driver: azure
// config/storage.php — disks (env names are your choice)
's3' => [
'driver' => 's3',
'bucket' => env('AWS_BUCKET'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('AWS_PREFIX', ''),
'cdn_base_url' => env('AWS_CDN_BASE_URL'),
'signed_ttl' => (int) env('AWS_SIGNED_URL_TTL', 3600),
'max_signed_ttl' => (int) env('AWS_MAX_SIGNED_URL_TTL', 86400),
],
// S3-compatible services use a preset instead of a separate driver
'r2' => [
'driver' => 's3',
'preset' => 'r2', // also: minio, spaces, wasabi
'account' => env('R2_ACCOUNT_ID'),
'bucket' => env('R2_BUCKET'),
'key' => env('R2_ACCESS_KEY_ID'),
'secret' => env('R2_SECRET_ACCESS_KEY'),
],
'gcs' => [
'driver' => 'gcs',
'bucket' => env('GCS_BUCKET'),
'project_id' => env('GCS_PROJECT_ID'),
'key_file' => env('GCS_KEY_FILE'), // service-account JSON
'prefix' => env('GCS_PREFIX', ''),
'signed_ttl' => (int) env('GCS_SIGNED_URL_TTL', 3600),
],
'azure' => [
'driver' => 'azure',
'container' => env('AZURE_STORAGE_CONTAINER'),
'connection_string' => env('AZURE_STORAGE_CONNECTION_STRING'),
'prefix' => env('AZURE_STORAGE_PREFIX', ''),
'signed_ttl' => (int) env('AZURE_SIGNED_URL_TTL', 3600),
],
Each pack's README lists its full options.
Diagnose Disks
php glueful storage:test # registration, adapter availability, liveness (read-only)
php glueful storage:test s3 --write # one disk, with a write/read/delete smoke test
Public URLs (base_url/cdn_base_url)
Set base_url (or cdn_base_url) on a disk to enable UrlGenerator to build public links:
// config/storage.php
return [
'default' => env('STORAGE_DEFAULT_DISK', env('STORAGE_DRIVER', 'uploads')),
'disks' => [
'uploads' => [
'driver' => 'local',
'root' => dirname(__DIR__) . '/storage/uploads',
'visibility' => 'public',
'base_url' => env('CDN_URL'), // e.g. https://cdn.example.com
],
's3' => [
'driver' => 's3', // requires glueful/storage-s3
'bucket' => env('AWS_BUCKET'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'cdn_base_url' => env('AWS_CDN_BASE_URL'), // takes precedence if set
'signed_ttl' => (int) env('AWS_SIGNED_URL_TTL', 3600),
],
],
];
UrlGenerator::url($path, $disk) prefers cdn_base_url if present; otherwise uses base_url. If neither is set, it returns the path unchanged.
Dependency Injection
Resolve services:
use Glueful\Storage\{StorageManager, PathGuard};
use Glueful\Storage\Support\UrlGenerator;
$storage = app($context, StorageManager::class); // or app($context, 'storage')
$guard = app($context, PathGuard::class); // Path validation service
$urls = app($context, UrlGenerator::class); // URL generator service
The provider also binds a convenient string alias: app($context, 'storage').
Common Tasks
Choose a disk
$fs = app($context, StorageManager::class)->disk(); // default disk
$s3 = app($context, StorageManager::class)->disk('s3'); // named disk
if (!app($context, StorageManager::class)->diskExists('s3')) {
// Log or fall back to default
}
Native Flysystem Operations
The disk() method returns a Flysystem FilesystemOperator, giving you access to all native Flysystem methods (v3):
$disk = app($context, StorageManager::class)->disk();
// File operations
if ($disk->fileExists('path/to/file.txt')) {
$content = $disk->read('path/to/file.txt');
$disk->write('backup/file.txt', $content);
$disk->delete('path/to/file.txt');
}
// Copy and move files
$disk->copy('source.txt', 'destination.txt');
$disk->move('old-path.txt', 'new-path.txt');
// Directory operations
$disk->createDirectory('logs/2025');
$disk->deleteDirectory('temp');
// File metadata
$size = $disk->fileSize('document.pdf'); // bytes
$modified = $disk->lastModified('document.pdf'); // timestamp
$mime = $disk->mimeType('image.jpg'); // MIME type
// Visibility (permissions)
$disk->setVisibility('public/file.txt', 'public');
$visibility = $disk->visibility('public/file.txt'); // 'public' or 'private'
// Stream operations
$stream = fopen('large-file.dat', 'r');
$disk->writeStream('uploads/large.dat', $stream);
$readStream = $disk->readStream('uploads/large.dat');
Visibility semantics (Local driver)
- On the Local driver, permissions are mapped via Flysystem’s
PortableVisibilityConverter:- Files: public =
0644, private =0600 - Directories: public =
0755, private =0700
- Files: public =
- Default disk visibility is
privateunless overridden inconfig/storage.php. - For non-local drivers, visibility semantics are adapter-defined.
Write and read JSON
$storage = app($context, StorageManager::class);
$storage->putJson('reports/daily.json', [
'generated_at' => date(DATE_ATOM),
'items' => [1, 2, 3],
]);
$data = $storage->getJson('reports/daily.json');
Stream large uploads atomically
$fp = fopen('/path/to/bigfile', 'rb');
app($context, StorageManager::class)->putStream('uploads/big.dat', $fp);
This uses a temporary file strategy: writes to a .tmp file first, then atomically moves it into place. The temp file is cleaned up on failure.
List contents
foreach (app($context, StorageManager::class)->listContents('backups', true) as $entry) {
// $entry is a StorageAttributes instance
$path = $entry->path();
$isFile = $entry->isFile();
$isDir = $entry->isDir();
}
Generate public URLs
$urlGen = app($context, Glueful\Storage\Support\UrlGenerator::class);
// Generate URL for a file
$url = $urlGen->url('images/logo.png');
// Uses disk base_url/cdn_base_url when configured, else returns the path
// Get disk configuration (useful for signed URLs)
$diskConfig = $urlGen->diskConfig('s3'); // Returns array of disk config
Signed URLs
Presigned links come from the disk's driver: the glueful/storage-* packs implement
Glueful\Storage\Contracts\NativeSignedUrlProviderInterface. Use the uploader's Flysystem-backed
storage adapter to ask for one:
use Glueful\Uploader\Storage\FlysystemStorage;
use Glueful\Storage\StorageManager;
use Glueful\Storage\Support\UrlGenerator;
$storage = new FlysystemStorage(
app($context, StorageManager::class),
app($context, UrlGenerator::class),
's3'
);
$signedUrl = $storage->getSignedUrl('protected/reports/monthly.pdf', 600); // 10 minutes
Notes:
- Falls back to the plain URL when the driver cannot sign (or on error).
- If the expiry is
<= 0, uses the disk'ssigned_ttl. - For uploaded blobs, prefer the blob API's app-signed URLs (
POST /blobs/{uuid}/signed-url), which keep access control and revocation;uploads.native_urlscan add a directnative_urlper disk. See File Uploads.
Helper Functions
// Get absolute path to storage directory
$path = storage_path($context); // /path/to/project/storage
$path = storage_path($context, 'logs/app.log'); // /path/to/project/storage/logs/app.log
Path Safety
PathGuard enforces security rules on file paths:
Default Configuration
// config/storage.php
'path_guard' => [
'allow_absolute' => false, // Reject absolute paths like /etc/passwd
'max_path_length' => 4096, // Maximum allowed path length
// Traversal ('..') and null bytes ("\0") are always rejected
]
Path Validation Rules
- No null bytes (
\0) - prevents string termination attacks - No
..traversal - prevents directory traversal attacks - Normalized separators - converts backslashes to forward slashes
- Removes redundant
./segments - Absolute paths rejected unless explicitly allowed
- Path length limits enforced
Manual Validation
$guard = app($context, Glueful\Storage\PathGuard::class);
try {
// Validates and normalizes the path
$safe = $guard->validate('reports/2025/../2025/summary.json');
// Returns: 'reports/2025/summary.json' (normalized)
} catch (\InvalidArgumentException $e) {
// Path validation failed
}
Error Handling
Most StorageManager operations convert Flysystem exceptions to StorageException with useful metadata:
Basic Error Handling
use Glueful\Storage\Exceptions\StorageException;
try {
app($context, StorageManager::class)->getJson('missing/file.json');
} catch (StorageException $e) {
$reason = $e->reason(); // Machine-readable reason code
$status = $e->httpStatus(); // Suggested HTTP status code
// Log with structured data
$logger = app($context, \Psr\Log\LoggerInterface::class);
$logger->error('Storage operation failed', [
'reason' => $reason,
'status' => $status,
'message' => $e->getMessage()
]);
}
Complete Error Reason Codes
| Reason Code | HTTP Status | Description |
|---|---|---|
io_read_failed | 404 | Unable to read file |
io_write_failed | 500 | Unable to write file |
io_delete_failed | 500 | Unable to delete file |
io_move_failed | 500 | Unable to move/rename file |
io_copy_failed | 500 | Unable to copy file |
dir_create_failed | 500 | Unable to create directory |
dir_delete_failed | 500 | Unable to delete directory |
existence_check_failed | 500 | Unable to check if file exists |
metadata_retrieve_failed | 500 | Unable to get file metadata |
visibility_set_failed | 403 | Unable to set permissions |
list_failed | 500 | Unable to list directory contents |
unknown_error | 500 | Unclassified error |
Parsing Exception Messages
use Glueful\Storage\Support\ExceptionClassifier;
// Parse structured data from exception message
$parsed = ExceptionClassifier::parseFromMessage($e->getMessage());
// Returns: ['reason' => 'io_read_failed', 'http_status' => 404]
Advanced Usage
Custom Disk at Runtime
$disk = app($context, StorageManager::class)->disk('uploads');
// All Flysystem methods available
$disk->write('file.txt', 'content');
$disk->setVisibility('file.txt', 'public');
// Get underlying adapter for advanced operations
if ($disk instanceof \League\Flysystem\Filesystem) {
$adapter = $disk->getAdapter();
// Adapter-specific operations
}
Working with Streams
$disk = app($context, StorageManager::class)->disk();
// Read large file as stream
$stream = $disk->readStream('large-file.zip');
while (!feof($stream)) {
$chunk = fread($stream, 8192);
// Process chunk
}
fclose($stream);
// Write from stream
$input = fopen('php://input', 'r');
$disk->writeStream('uploads/posted.dat', $input);
Atomic Operations
// StorageManager::putStream() is atomic by default
$fp = fopen('critical-data.json', 'r');
app($context, StorageManager::class)->putStream('config/settings.json', $fp);
// Writes to temp file first, then moves atomically
Testing Tips
Memory Driver for Tests
// config/storage.php (testing environment)
'testing' => [
'driver' => 'memory',
]
// In your test
$storage = app($context, StorageManager::class)->disk('testing');
$storage->write('test.txt', 'content');
// No actual filesystem writes!
Local Temp Directory
use Glueful\Storage\{StorageManager, PathGuard};
// Create an isolated disk for the test
$tempDir = sys_get_temp_dir() . '/test-' . uniqid();
mkdir($tempDir);
$sm = new StorageManager([
'default' => 'test',
'disks' => [
'test' => [
'driver' => 'local',
'root' => $tempDir,
'visibility' => 'private',
],
],
], new PathGuard());
$disk = $sm->disk('test');
$disk->write('example.txt', 'ok');
// Cleanup: delete all entries under the temp root
foreach ($disk->listContents('', true) as $entry) {
if ($entry->isFile()) {
$disk->delete($entry->path());
} elseif ($entry->isDir()) {
$disk->deleteDirectory($entry->path());
}
}
rmdir($tempDir);
URL Testing
config() reads configuration; it is not a setter. Build a UrlGenerator with the config you want
to test:
use Glueful\Storage\PathGuard;
use Glueful\Storage\Support\UrlGenerator;
$urls = new UrlGenerator([
'default' => 'uploads',
'disks' => ['uploads' => ['driver' => 'local', 'base_url' => 'https://cdn.test']],
], new PathGuard());
$this->assertEquals('https://cdn.test/image.jpg', $urls->url('image.jpg', 'uploads'));
Reference
Core Classes
Glueful\Storage\StorageManagerdisk(?string $name = null): FilesystemOperator- Get disk instancediskExists(string $name): bool- Check if disk configuredputJson(string $path, mixed $data, ?string $disk = null): voidgetJson(string $path, ?string $disk = null): mixedput(string $path, string $contents, ?string $disk = null): voidfileExists(string $path, ?string $disk = null): booldelete(string $path, ?string $disk = null): voidputStream(string $path, $stream, ?string $disk = null): void- Atomic writelistContents(string $path, bool $recursive = false, ?string $disk = null): iterable
Glueful\Storage\PathGuardvalidate(string $path): string- Validate and normalize path
Glueful\Storage\Support\UrlGeneratorurl(string $path, ?string $disk = null): string- Generate public URLdiskConfig(string $disk): array- Get disk configuration
Glueful\Storage\Exceptions\StorageExceptionreason(): ?string- Get error reason codehttpStatus(): ?int- Get suggested HTTP statusfromFlysystem(FilesystemException $e, string $path = ''): self
Helper Functions
storage_path($context, string $path = ''): string- Get storage directory path