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>
301 lines
6.6 KiB
Markdown
Executable File
301 lines
6.6 KiB
Markdown
Executable File
---
|
|
name: file-attachments
|
|
description: 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:
|
|
|
|
1. **Upload** - File uploaded UNATTACHED via `POST /_upload`
|
|
2. **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:
|
|
|
|
```jqhtml
|
|
<File_Upload
|
|
$name="document"
|
|
$accept=".pdf,.doc,.docx"
|
|
$max_size="10MB"
|
|
/>
|
|
```
|
|
|
|
### Upload Response
|
|
|
|
Upload returns a `file_key` that identifies the unattached file:
|
|
|
|
```javascript
|
|
// 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):
|
|
|
|
```php
|
|
#[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):
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$attachment->detach();
|
|
```
|
|
|
|
---
|
|
|
|
## Retrieving Attachments
|
|
|
|
### Single Attachment
|
|
|
|
```php
|
|
$photo = $user->get_attachment('profile_photo');
|
|
if ($photo) {
|
|
echo $photo->get_url();
|
|
}
|
|
```
|
|
|
|
### Multiple Attachments
|
|
|
|
```php
|
|
$documents = $project->get_attachments('documents');
|
|
foreach ($documents as $doc) {
|
|
echo $doc->file_name;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Displaying Files
|
|
|
|
### Direct URL
|
|
|
|
```php
|
|
$url = $attachment->get_url();
|
|
// Returns: /_download/{key}
|
|
```
|
|
|
|
### Download URL (Forces Download)
|
|
|
|
```php
|
|
$url = $attachment->get_download_url();
|
|
// Returns: /_download/{key}?download=1
|
|
```
|
|
|
|
### Thumbnail URL
|
|
|
|
For images with automatic resizing:
|
|
|
|
```php
|
|
// 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 dimensions
|
|
- `contain` - Fit within dimensions
|
|
- `width` - Scale to width, maintain aspect ratio
|
|
|
|
---
|
|
|
|
## Template Usage
|
|
|
|
```jqhtml
|
|
<% 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
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
// 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`
|