Attachment Visibility and Access
Visibility Modes
private: protected download URLpublic: direct disk URLtemp_private: protected download URL + timeout lifecycletemp_public: direct disk URL by default + timeout lifecycle
Filesystem Disks (local vs public)
This module assumes you use different filesystem disks for public vs private assets:
public/temp_public→attachments.disk_public(default:public)private/temp_private→attachments.disk_private(default:local)
When using local storage, the secure setup is:
publicdisk root:storage/app/publicand exposed via/storage/*(Laravelphp artisan storage:link)localdisk root:storage/appand NOT exposed by the web server
If a private attachment can be opened by concatenating APP_URL + path, it means the private storage directory is being served publicly (usually a misconfigured symlink or web server rule). The fix is to ensure only storage/app/public is web-accessible.
For S3 (or any cloud disk), public attachments will typically return a bucket/CDN URL (via disk->url()), while private attachments should still return API-proxied URLs (/view and /download) so authentication + tenant scope + expiry checks are enforced.
R2 Configuration
Cloudflare R2 uses Laravel's standard s3 filesystem driver. Configure separate disks when public and private attachments use separate buckets:
// config/filesystems.php
'r2-public' => [
'driver' => 's3',
'key' => env('R2_PUBLIC_ACCESS_KEY'),
'secret' => env('R2_PUBLIC_SECRET'),
'region' => 'auto',
'bucket' => env('R2_PUBLIC_BUCKET', 'karunafilm-public'),
'endpoint' => env('R2_PUBLIC_ENDPOINT'),
'url' => env('R2_PUBLIC_URL'),
'use_path_style_endpoint' => true,
],
'r2-private' => [
'driver' => 's3',
'key' => env('R2_PRIVATE_ACCESS_KEY'),
'secret' => env('R2_PRIVATE_SECRET'),
'region' => 'auto',
'bucket' => env('R2_PRIVATE_BUCKET', 'karunafilm-private'),
'endpoint' => env('R2_PRIVATE_ENDPOINT'),
'use_path_style_endpoint' => true,
],// config/sp-attachments.php
'disk_public' => 'r2-public',
'disk_private' => 'r2-private',
'direct_upload' => [
'enabled' => false,
'presign_ttl_seconds' => 1800,
'min_multipart_size_bytes' => 104857600, // 100 MiB
],
'preview_url_enabled' => false,
'preview_url_ttl_seconds' => 300,Bucket selection is based on visibility, not file type. Images and videos may use either bucket. The public and private buckets must be provisioned before integration testing.
Apply this CORS policy to both buckets, replacing the origins with the approved dashboard and development origins:
[
{
"AllowedOrigins": ["https://admin.karunafilm.example", "http://localhost:3000"],
"AllowedMethods": ["GET", "HEAD", "PUT", "POST"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag", "Content-Length", "Content-Type"],
"MaxAgeSeconds": 3600
}
]CORS does not make the private bucket public. Private object access remains restricted to presigned requests or the package's signed preview/API endpoints.
Direct upload size limit: S3 presigned PUT URLs cannot carry a content-length-range, so the object is uploaded to the bucket first and max_upload_size is enforced at complete-upload time: oversized objects are deleted and the attachment row is refused (422). Server-side POST fallback uploads are limited by the same max_upload_size validation rule on the file input. Multipart uploads (>= min_multipart_size_bytes) are bounded only by the bucket configuration, as agreed in the client contract.
Signed preview URL shape: {id}-pattern functions are dispatched with the id inside the function name, e.g. {api_prefix}/{route_prefix}/rpc/{id}/preview under the default rpc_prefix='rpc'. A non-empty rpc_prefix is required for id-bearing attachment functions (preview, download, view).
URL Strategy
Use attachments.url_strategy to choose how the url field is generated:
auto(default): keeps existing behavior, returning direct URLs for public visibility and API URLs for private visibility.api: always returns the API/viewURL.temporary: uses the disk driver'stemporaryUrl()when available, otherwise falls back to API/view.direct: returns direct public disk URLs.
Protection Option for temp_public
Use attachments.protect_temp_public_via_download:
false(default):temp_publicreturns direct URLtrue:temp_publicreturns/downloadURL so API checks execute before access
Runtime Expiry Enforcement
Download endpoint denies expired temp files with HTTP 410 Gone.
This protects access immediately, even before scheduled cleanup removes stale files.
temp_timeout_at is capped by attachments.max_temp_timeout_minutes, matching the existing cap for temp_timeout_minutes.