--- 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 ``` ### 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) { %> Profile photo <% } %> <% for (const doc of this.data.project.documents) { %> <%= doc.file_name %> <% } %> ``` --- ## 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`