# File Attachments System ## Overview The RSpade file attachment system provides secure, session-based file uploads with automatic thumbnail generation and polymorphic model associations. ## Upload Flow **Security Model**: Files upload UNATTACHED → validate → assign via API 1. User uploads file to `/_upload` endpoint 2. File saved with `session_id`, no model association 3. Returns unique `key` to frontend 4. Frontend calls API endpoint with key 5. Backend validates ownership and assigns to model ## Security Implementation **Session-based validation** prevents cross-user file assignment: - Files get `session_id` on upload - `can_user_assign_this_file()` validates: - File not already assigned - Same site_id as user's session - Same session_id (prevents cross-user assignment) - User-provided `fileable_*` params ignored during upload ## Attachment API ### File_Attachment_Model Methods ```php // Find attachment by key $attachment = File_Attachment_Model::find_by_key($key); // Validate user can assign if ($attachment->can_user_assign_this_file()) { // Single file attachment (replaces existing) $attachment->attach_to($user, 'profile_photo'); // Multiple file attachment (adds to collection) $attachment->add_to($project, 'documents'); } // Remove assignment $attachment->detach(); // Check assignment status if ($attachment->is_attached()) { // File is assigned to a model } ``` ### Model Helper Methods All models extending `Rsx_Model_Abstract` have attachment helpers: ```php // Get single attachment $photo = $user->get_attachment('profile_photo'); // Get multiple attachments $docs = $project->get_attachments('documents'); // Count attachments $count = $project->count_attachments('documents'); // Check if has attachments if ($user->has_attachment('profile_photo')) { // User has profile photo } ``` ## Display URLs ```php // Thumbnail with specific dimensions $photo->get_thumbnail_url('cover', 128, 128); // Full file URL $photo->get_url(); // Force download URL $photo->get_download_url(); // Get file metadata $size = $photo->file_size; $mime = $photo->mime_type; $name = $photo->original_filename; ``` ## Endpoints - `/_upload` - File upload endpoint - `/_download/:key` - Force download file - `/_thumbnail/:key/:type/:w/:h` - Generated thumbnail - `/_file/:key` - Direct file access ## Thumbnail System Thumbnails are generated on-demand and cached: **Preset types** (defined in config): - `cover` - Cover image aspect ratio - `square` - 1:1 aspect ratio - `landscape` - 16:9 aspect ratio **Dynamic thumbnails**: - Limited to `max_dynamic_size` (default 2000px) - Cached for performance - Automatic cleanup via scheduled task ## Controller Implementation Pattern ```php #[Ajax_Endpoint] public static function save_with_photo(Request $request, array $params = []) { // Validate required fields if (empty($params['name'])) { return response_form_error('Validation failed', [ 'name' => 'Name is required' ]); } // Save model $user = new User_Model(); $user->name = $params['name']; $user->save(); // Attach photo if provided if (!empty($params['photo_key'])) { $photo = File_Attachment_Model::find_by_key($params['photo_key']); if (!$photo || !$photo->can_user_assign_this_file()) { return response_form_error('Invalid file', [ 'photo' => 'File not found or access denied' ]); } $photo->attach_to($user, 'profile_photo'); } return ['user_id' => $user->id]; } ``` ## Frontend Upload Component ```javascript // Using Rsx_File_Upload component // Get uploaded file key const key = this.$id('photo_upload').component().get_file_key(); // Submit with form const data = { name: this.$id('name').val(), photo_key: key }; await Controller.save_with_photo(data); ``` ## Database Schema ```sql file_attachments ├── id (bigint) ├── key (varchar 64, unique) ├── site_id (bigint) ├── session_id (bigint) ├── fileable_type (varchar 255, nullable) ├── fileable_id (bigint, nullable) ├── fileable_key (varchar 255, nullable) ├── storage_path (varchar 500) ├── original_filename (varchar 500) ├── mime_type (varchar 255) ├── file_size (bigint) ├── metadata (json) └── timestamps ``` ## Configuration In `/system/config/rsx.php`: ```php 'attachments' => [ 'upload_dir' => storage_path('rsx-attachments'), 'max_upload_size' => 10 * 1024 * 1024, // 10MB 'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'], ], 'thumbnails' => [ 'presets' => [ 'cover' => ['width' => 800, 'height' => 600], 'square' => ['width' => 300, 'height' => 300], ], 'max_dynamic_size' => 2000, 'quotas' => [ 'preset_max_bytes' => 500 * 1024 * 1024, // 500MB 'dynamic_max_bytes' => 100 * 1024 * 1024, // 100MB ], ], ``` ## Security Considerations 1. **Never trust client-provided fileable_* params** during upload 2. **Always validate ownership** before assignment 3. **Use polymorphic associations** for flexibility 4. **Implement access control** in download endpoints 5. **Sanitize filenames** to prevent directory traversal 6. **Validate MIME types** server-side 7. **Set appropriate upload size limits** 8. **Use scheduled cleanup** for orphaned files ## Scheduled Cleanup Orphaned files (uploaded but never assigned) are cleaned automatically: - Files older than 24 hours without assignment - Runs daily via scheduled task - Preserves actively used files See also: `php artisan rsx:man file_upload`