Add form value persistence across cache revalidation re-renders 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
6.6 KiB
Executable File
name, description
| name | description |
|---|---|
| file-attachments | Handling file uploads and attachments in RSX including upload flow, attaching files to models, retrieving attachments, and generating URLs. Use when implementing file uploads, working with File_Attachment_Model, attaching files to records, displaying thumbnails, or handling document downloads. |
RSX File Attachments
Two-Model Architecture
RSX separates physical storage from logical metadata:
| Model | Purpose |
|---|---|
File_Storage_Model |
Physical files on disk (framework model) |
File_Attachment_Model |
Logical uploads with metadata (user model) |
This enables deduplication - identical files share physical storage while maintaining separate metadata.
Upload Flow
Files follow a secure two-step process:
- Upload - File uploaded UNATTACHED via
POST /_upload - Attach - Controller validates and assigns to model
This session-based validation prevents cross-user file assignment.
Uploading Files
Frontend Component
Use the built-in file upload component:
<File_Upload
$name="document"
$accept=".pdf,.doc,.docx"
$max_size="10MB"
/>
Upload Response
Upload returns a file_key that identifies the unattached file:
// After upload completes
const file_key = upload_component.get_file_key();
Attaching Files to Models
Single Attachment (Replaces)
For one-to-one relationships (e.g., profile photo):
#[Ajax_Endpoint]
public static function save_photo(Request $request, array $params = []) {
$user = User_Model::find($params['user_id']);
$attachment = File_Attachment_Model::find_by_key($params['photo_key']);
if ($attachment && $attachment->can_user_assign_this_file()) {
$attachment->attach_to($user, 'profile_photo');
}
return ['success' => true];
}
attach_to() replaces any existing attachment with that slot name.
Multiple Attachments (Adds)
For one-to-many relationships (e.g., project documents):
$attachment = File_Attachment_Model::find_by_key($params['document_key']);
if ($attachment && $attachment->can_user_assign_this_file()) {
$attachment->add_to($project, 'documents');
}
add_to() adds to the collection without removing existing files.
Detaching Files
$attachment->detach();
Retrieving Attachments
Single Attachment
$photo = $user->get_attachment('profile_photo');
if ($photo) {
echo $photo->get_url();
}
Multiple Attachments
$documents = $project->get_attachments('documents');
foreach ($documents as $doc) {
echo $doc->file_name;
}
Displaying Files
Direct URL
$url = $attachment->get_url();
// Returns: /_download/{key}
Download URL (Forces Download)
$url = $attachment->get_download_url();
// Returns: /_download/{key}?download=1
Thumbnail URL
For images with automatic resizing:
// Crop to exact dimensions
$url = $attachment->get_thumbnail_url('cover', 128, 128);
// Fit within dimensions (maintains aspect ratio)
$url = $attachment->get_thumbnail_url('contain', 200, 200);
// Scale to width, auto height
$url = $attachment->get_thumbnail_url('width', 300);
Thumbnail types:
cover- Crop to fill exact dimensionscontain- Fit within dimensionswidth- Scale to width, maintain aspect ratio
Template Usage
<% if (this.data.user.profile_photo) { %>
<img src="<%= this.data.user.profile_photo.get_thumbnail_url('cover', 64, 64) %>"
alt="Profile photo" />
<% } %>
<% for (const doc of this.data.project.documents) { %>
<a href="<%= doc.get_download_url() %>">
<%= doc.file_name %>
</a>
<% } %>
File Attachment Model Properties
$attachment->file_name; // Original uploaded filename
$attachment->mime_type; // MIME type (e.g., 'image/jpeg')
$attachment->file_size; // Size in bytes
$attachment->file_key; // Unique identifier
$attachment->created_at; // Upload timestamp
Creating Attachments Programmatically
From Disk
$attachment = File_Attachment_Model::create_from_disk(
'/tmp/import/document.pdf',
[
'site_id' => $site->id,
'filename' => 'imported-document.pdf',
'fileable_category' => 'import'
]
);
From String Content
$csv = "Name,Email\nJohn,john@example.com";
$attachment = File_Attachment_Model::create_from_string(
$csv,
'export.csv',
['site_id' => $site->id, 'fileable_category' => 'export']
);
From URL
$attachment = File_Attachment_Model::create_from_url(
'https://example.com/logo.png',
['site_id' => $site->id, 'fileable_category' => 'logo']
);
Security Considerations
Always Validate Before Attaching
$attachment = File_Attachment_Model::find_by_key($params['file_key']);
// REQUIRED: Check user can assign this file
if (!$attachment || !$attachment->can_user_assign_this_file()) {
return response_error(Ajax::ERROR_UNAUTHORIZED, 'Invalid file');
}
// Now safe to attach
$attachment->attach_to($model, 'slot_name');
File Validation
Check file type before attaching:
if (!in_array($attachment->mime_type, ['image/jpeg', 'image/png', 'image/gif'])) {
return response_form_error('Validation failed', ['photo' => 'Must be an image']);
}
Event Hooks for Authorization
Control access with event hooks:
// Require auth for uploads
#[OnEvent('file.upload.authorize', priority: 10)]
public static function require_auth($data) {
if (!Session::is_logged_in()) {
return response()->json(['error' => 'Authentication required'], 403);
}
return true;
}
// Restrict thumbnail access
#[OnEvent('file.thumbnail.authorize', priority: 10)]
public static function check_file_access($data) {
if ($data['attachment']->created_by !== $data['user']?->id) {
return response()->json(['error' => 'Access denied'], 403);
}
return true;
}
// Additional restrictions for downloads
#[OnEvent('file.download.authorize', priority: 10)]
public static function require_premium($data) {
if (!$data['user']?->has_premium()) {
return response()->json(['error' => 'Premium required'], 403);
}
return true;
}
System Endpoints
| Endpoint | Purpose |
|---|---|
POST /_upload |
Upload new file |
GET /_download/:key |
Download/view file |
GET /_thumbnail/:key/:type/:width/:height |
Get resized image |
GET /_icon_by_extension/:extension |
Get file type icon |
More Information
Details: php artisan rsx:man file_upload