Files
rspade_system/docs/skills/file-attachments/SKILL.md
root 1b46c5270c Add skills documentation and misc updates
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>
2025-12-29 04:38:06 +00:00

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:

  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:

<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 dimensions
  • contain - Fit within dimensions
  • width - 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