sarkis-sh / spray-media
Secure uploads, signed URLs, and flexible media delivery for Laravel.
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/sarkis-sh/spray-media
Requires
- php: ^8.1
- laravel/framework: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-01-04 10:57:03 UTC
README
Spray Media gives you signed links, a hardened upload flow, and swappable building blocks so you can ship secure media delivery (view/download) quickly and confidently.
Table of Contents
- Why Spray Media
- Requirements
- Install
- Configuration Essentials
- Default Routes
- Database Schema
- Quickstart (3 Steps)
- Helpers & Resource
- How It Works
- Security Notes
- Performance
- Extensibility
- Translations
- Testing
- License
Why Spray Media
- 🔒 HMAC-signed URLs with optional expiration and embedded metadata.
- 📥 HTTP upload endpoint with size/MIME controls, custom rules, and automatic filename sanitizing.
- 📄 Inline or attachment responses with Cache-Control, ETag, and 304 support.
- ⚡ Helpers, Resource, and Facade ready to drop into APIs or views.
- 🔧 Fully swappable components: uploader, storage, URL generator, validator, file server, repository, response adapter.
- 🌐 Built-in translations (en/ar), configurable routes, and ready migrations.
Requirements
- PHP >= 8.1
- Laravel 10, 11, or 12
Install
composer require sarkis-sh/spray-media # publish assets (config, migrations, lang) php artisan vendor:publish --provider="SprayMedia\Providers\SprayMediaServiceProvider" --tag=config php artisan vendor:publish --provider="SprayMedia\Providers\SprayMediaServiceProvider" --tag=migrations php artisan vendor:publish --provider="SprayMedia\Providers\SprayMediaServiceProvider" --tag=lang php artisan migrate
Configuration Essentials
Full config lives in src/Config/media.php. Key options:
| Key | ENV | Default | Purpose |
|---|---|---|---|
disk |
SPRAY_MEDIA_DISK |
local |
Filesystem disk from config/filesystems.php |
base_dir |
SPRAY_MEDIA_BASE_DIR |
uploads |
Root folder inside the disk |
upload.max_kb |
SPRAY_MEDIA_MAX_UPLOAD_KB |
51200 |
Max size (KB) |
upload.mimetypes / upload.mimes |
SPRAY_MEDIA_MIMETYPES / SPRAY_MEDIA_MIMES |
image/jpeg,image/png,application/pdf |
Allowed types |
route.prefix |
SPRAY_MEDIA_ROUTE_PREFIX |
api/media-items |
Route group prefix |
route.path |
SPRAY_MEDIA_ROUTE_PATH |
secure |
Signed serve endpoint path |
route.middleware_public |
SPRAY_MEDIA_ROUTE_MIDDLEWARE_PUBLIC |
api |
Middleware for serve endpoint |
route.middleware_admin |
SPRAY_MEDIA_ROUTE_MIDDLEWARE_ADMIN |
api |
Middleware for upload/CRUD |
hmac.secret |
SPRAY_MEDIA_HMAC_SECRET |
APP_KEY |
HMAC key |
hmac.algorithm |
SPRAY_MEDIA_HMAC_ALGO |
sha256 |
HMAC algo |
hmac.default_expiration_minutes |
SPRAY_MEDIA_DEFAULT_EXPIRATION_MINUTES |
60 |
Default link TTL |
performance.cache_control |
SPRAY_MEDIA_CACHE_CONTROL |
private, max-age=3600 |
Cache-Control header |
performance.enable_etag |
SPRAY_MEDIA_ENABLE_ETAG |
true |
Add ETag |
Default Routes
Defined in src/Routes/api.php (prefix/path/middleware are configurable):
| HTTP | Path | Description | Middleware |
|---|---|---|---|
| GET | /api/media-items/secure |
Serve signed file (view/download) | route.middleware_public |
| POST | /api/media-items |
Upload file + create record | route.middleware_admin |
| PUT | /api/media-items/{id}/update-filename |
Update filename only | route.middleware_admin |
| DELETE | /api/media-items/{id} |
Delete file + record | route.middleware_admin |
Update filename endpoint
- Path:
PUT /api/media-items/{id}/update-filename - Body (JSON or form):
new_file_name(string, required) — the desired base name (extension stays the same)
Example:
curl -X PUT http://your-app.test/api/media-items/123/update-filename \ -H "Content-Type: application/json" \ -d '{"new_file_name": "project-spec-v2"}'
Database Schema
Migration: src/Database/Migrations/0001_01_01_000003_create_media_items_table.php
- Columns:
path,disk,formatted_filename,filename,extension,mime_type,size, timestamps.
Quickstart (3 Steps)
- Upload via HTTP
curl -X POST http://your-app.test/api/media-items \ -F "file=@/path/to/file.png" \ -F "custom_filename=My File"
Validation applies max/mime/mimetypes/custom_rules from config.
Sample success payload (see src/Infrastructure/Http/DefaultResponseAdapter.php):
{
"result": "success",
"message": "...",
"model": {
"id": 1,
"filename": "my-file",
"formatted_filename": "my-file.png",
"extension": "png",
"mime_type": "image/png",
"size": 12345,
"url": "https://.../secure?data=...&signature=...",
"expires_at": 1700000000
},
"error_list": [],
"code": 201
}
- Generate a signed URL (inline or download)
use SprayMedia\Domain\Enums\MediaAction; use SprayMedia\Facades\SprayMedia; $url = SprayMedia::generateProtectedUrl($mediaItem, MediaAction::VIEW, [ 'expiration_minutes' => 30, 'metadata' => ['user_id' => 5], ]); $download = media_item_generate_protected_download_url($mediaItem); $temporary = media_item_generate_protected_temporary_url($mediaItem, 15);
Links carry data (base64 JSON) + signature (HMAC). Pass expiration_minutes => null for non-expiring links.
- Serve the file
Hit the signed URL. The package validates signature/expiry, sets headers based on
action(inline/attachment), and emits Cache-Control/ETag.
Helpers & Resource
media_item_upload_file($file, ?$dir, ?$disk)returns storage metadata only.media_item_upload_and_create($file, $attributes = [])uploads and creates the record.media_item_generate_protected_url($media, MediaAction::VIEW, $options = [])signed URL helper.media_item_with_signed_url($media, $action, $options)andmedia_item_collection_with_signed_url(...)wrap in Resource withurlandexpires_atset (src/Http/Resources/MediaItemResource.php).media_item_get_absolute_path($media)returns filesystem path.
How It Works
- Upload: LocalFileUploader stores to configured disk/base_dir and sanitizes filenames via FilenameSanitizer.
- Persist: MediaItemManager persists metadata through the repository binding.
- Sign: HmacMediaItemUrlGenerator builds payload, base64 encodes, signs with HMAC.
- Validate: HmacPayloadValidator checks presence, signature (hash_equals), payload JSON, action, and expiry.
- Serve: LocalFileServer streams inline/attachment, applies Cache-Control/ETag, returns 304 when ETag matches.
Security Notes
- Rotate
hmac.secretper environment; keep it distinct fromAPP_KEYwhen possible. - Choose a strong
hmac.algorithm(sha256+). Links can be expiring or non-expiring per call. - Filenames are sanitized to a slug to avoid header/FS issues.
- Validator rejects missing/invalid signatures, malformed payloads, wrong actions, or expired links.
Performance
- If
expires_atis present, Cache-Control uses the remaining lifetime automatically; otherwise uses the configured header. - ETag is based on
id+updated_atto enable 304 responses.
Extensibility
- Swap any contract via
spray-media.bindingsin src/Config/media.php: repository, uploader, url_generator, payload_validator, file_server, response_adapter. - Override model/resource via
spray-media.modelandspray-media.resourceto expose custom fields or relations. - Routes, names, and middleware are fully configurable for API compatibility.
Translations
- English and Arabic strings ship with the package; publish lang to customize.
Testing
./vendor/bin/phpunit
License
MIT