Document event handler placement and model fetch clarification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
521 lines
16 KiB
Plaintext
Executable File
521 lines
16 KiB
Plaintext
Executable File
NAME
|
|
File Upload Examples - Practical examples for implementing file uploads
|
|
|
|
SYNOPSIS
|
|
Examples for implementing file upload endpoints and processing files
|
|
in RSX applications using the File_Attachment_Model factory methods.
|
|
|
|
DESCRIPTION
|
|
This document provides practical, copy-paste ready examples for common
|
|
file upload scenarios. All examples use the factory methods on
|
|
File_Attachment_Model which handle storage, deduplication, and metadata
|
|
automatically.
|
|
|
|
BASIC HTTP FILE UPLOAD
|
|
|
|
Simple file upload endpoint with validation:
|
|
|
|
Controller: /rsx/app/frontend/files/files_controller.php
|
|
|
|
<?php
|
|
namespace Rsx\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Rsx\Models\File_Attachment_Model;
|
|
use App\RSpade\Core\Controller\Rsx_Controller_Abstract;
|
|
|
|
class Files_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
#[Route('/files/upload', method: 'POST')]
|
|
#[Auth('Permission::authenticated()')]
|
|
#[Ajax_Endpoint]
|
|
public static function upload(Request $request, array $params = [])
|
|
{
|
|
// Validate uploaded file
|
|
$request->validate([
|
|
'file' => 'required|file|max:10240', // 10MB max
|
|
]);
|
|
|
|
// Get site_id from authenticated user
|
|
$site_id = RsxAuth::session()->site_id;
|
|
|
|
// Create attachment
|
|
$attachment = File_Attachment_Model::create_from_upload(
|
|
$request->file('file'),
|
|
[
|
|
'site_id' => $site_id,
|
|
'fileable_category' => 'general'
|
|
]
|
|
);
|
|
|
|
return [
|
|
'success' => true,
|
|
'key' => $attachment->key,
|
|
'filename' => $attachment->file_name,
|
|
'size' => $attachment->file_storage->size,
|
|
'url' => $attachment->get_url()
|
|
];
|
|
}
|
|
}
|
|
|
|
USER AVATAR UPLOAD
|
|
|
|
Upload and attach to user model with category:
|
|
|
|
#[Route('/profile/avatar/upload', method: 'POST')]
|
|
#[Auth('Permission::authenticated()')]
|
|
#[Ajax_Endpoint]
|
|
public static function upload_avatar(Request $request, array $params = [])
|
|
{
|
|
$request->validate([
|
|
'avatar' => 'required|image|max:5120', // 5MB, images only
|
|
]);
|
|
|
|
$user = RsxAuth::user();
|
|
$site_id = RsxAuth::session()->site_id;
|
|
|
|
// Delete old avatar if exists
|
|
$old_avatar = File_Attachment_Model::where('fileable_type', 'User_Model')
|
|
->where('fileable_id', $user->id)
|
|
->where('fileable_category', 'avatar')
|
|
->first();
|
|
|
|
if ($old_avatar) {
|
|
$old_avatar->delete(); // Auto-cleanup handles storage
|
|
}
|
|
|
|
// Create new avatar
|
|
$attachment = File_Attachment_Model::create_from_upload(
|
|
$request->file('avatar'),
|
|
[
|
|
'site_id' => $site_id,
|
|
'fileable_type' => 'User_Model',
|
|
'fileable_id' => $user->id,
|
|
'fileable_category' => 'avatar',
|
|
'fileable_type_meta' => 'profile'
|
|
]
|
|
);
|
|
|
|
return [
|
|
'success' => true,
|
|
'url' => $attachment->get_url()
|
|
];
|
|
}
|
|
|
|
MULTIPLE FILE UPLOAD
|
|
|
|
Handle multiple files with ordering:
|
|
|
|
#[Route('/project/{id}/documents/upload', method: 'POST')]
|
|
#[Auth('Permission::authenticated()')]
|
|
#[Ajax_Endpoint]
|
|
public static function upload_documents(Request $request, array $params = [])
|
|
{
|
|
$project_id = $params['id'];
|
|
$site_id = RsxAuth::session()->site_id;
|
|
|
|
$request->validate([
|
|
'documents' => 'required|array',
|
|
'documents.*' => 'file|max:20480', // 20MB per file
|
|
]);
|
|
|
|
$attachments = [];
|
|
$order = 1;
|
|
|
|
foreach ($request->file('documents') as $file) {
|
|
$attachment = File_Attachment_Model::create_from_upload(
|
|
$file,
|
|
[
|
|
'site_id' => $site_id,
|
|
'fileable_type' => 'Project_Model',
|
|
'fileable_id' => $project_id,
|
|
'fileable_category' => 'document',
|
|
'fileable_order' => $order++
|
|
]
|
|
);
|
|
|
|
$attachments[] = [
|
|
'key' => $attachment->key,
|
|
'filename' => $attachment->file_name,
|
|
'url' => $attachment->get_url()
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'files' => $attachments
|
|
];
|
|
}
|
|
|
|
GENERATED FILE EXPORT
|
|
|
|
Generate CSV and attach to report:
|
|
|
|
#[Route('/reports/{id}/export', method: 'POST')]
|
|
#[Auth('Permission::authenticated()')]
|
|
#[Ajax_Endpoint]
|
|
public static function export_report(Request $request, array $params = [])
|
|
{
|
|
$report_id = $params['id'];
|
|
$site_id = RsxAuth::session()->site_id;
|
|
|
|
// Generate CSV content
|
|
$data = Report_Model::find($report_id)->get_data();
|
|
$csv = "Name,Value,Date\n";
|
|
foreach ($data as $row) {
|
|
$csv .= "{$row->name},{$row->value},{$row->date}\n";
|
|
}
|
|
|
|
// Create attachment from string
|
|
$attachment = File_Attachment_Model::create_from_string(
|
|
$csv,
|
|
"report-{$report_id}-" . date('Y-m-d') . ".csv",
|
|
[
|
|
'site_id' => $site_id,
|
|
'fileable_type' => 'Report_Model',
|
|
'fileable_id' => $report_id,
|
|
'fileable_category' => 'export',
|
|
'fileable_meta' => [
|
|
'generated_at' => now()->toIso8601String(),
|
|
'generated_by' => RsxAuth::id()
|
|
]
|
|
]
|
|
);
|
|
|
|
return [
|
|
'success' => true,
|
|
'download_url' => $attachment->get_download_url()
|
|
];
|
|
}
|
|
|
|
IMPORT FROM URL
|
|
|
|
Download and import external file:
|
|
|
|
#[Route('/resources/import', method: 'POST')]
|
|
#[Auth('Permission::admin()')]
|
|
#[Ajax_Endpoint]
|
|
public static function import_from_url(Request $request, array $params = [])
|
|
{
|
|
$site_id = RsxAuth::session()->site_id;
|
|
|
|
$request->validate([
|
|
'url' => 'required|url',
|
|
]);
|
|
|
|
try {
|
|
$attachment = File_Attachment_Model::create_from_url(
|
|
$request->input('url'),
|
|
[
|
|
'site_id' => $site_id,
|
|
'fileable_category' => 'import',
|
|
'fileable_meta' => [
|
|
'source_url' => $request->input('url'),
|
|
'imported_at' => now()->toIso8601String(),
|
|
'imported_by' => RsxAuth::id()
|
|
]
|
|
]
|
|
);
|
|
|
|
return [
|
|
'success' => true,
|
|
'key' => $attachment->key,
|
|
'filename' => $attachment->file_name
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
BULK DISK IMPORT
|
|
|
|
Import files from directory:
|
|
|
|
#[Route('/admin/import/bulk', method: 'POST')]
|
|
#[Auth('Permission::admin()')]
|
|
#[Ajax_Endpoint]
|
|
public static function bulk_import(Request $request, array $params = [])
|
|
{
|
|
$site_id = RsxAuth::session()->site_id;
|
|
|
|
$request->validate([
|
|
'directory' => 'required|string',
|
|
]);
|
|
|
|
$directory = $request->input('directory');
|
|
|
|
if (!is_dir($directory)) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Directory not found'
|
|
];
|
|
}
|
|
|
|
$files = glob($directory . '/*');
|
|
$imported = [];
|
|
$errors = [];
|
|
|
|
foreach ($files as $file_path) {
|
|
if (!is_file($file_path)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$attachment = File_Attachment_Model::create_from_disk(
|
|
$file_path,
|
|
[
|
|
'site_id' => $site_id,
|
|
'fileable_category' => 'bulk_import',
|
|
'fileable_meta' => [
|
|
'original_path' => $file_path,
|
|
'imported_at' => now()->toIso8601String()
|
|
]
|
|
]
|
|
);
|
|
|
|
$imported[] = [
|
|
'filename' => $attachment->file_name,
|
|
'key' => $attachment->key
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
$errors[] = [
|
|
'file' => basename($file_path),
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'imported' => count($imported),
|
|
'errors' => count($errors),
|
|
'files' => $imported,
|
|
'failed' => $errors
|
|
];
|
|
}
|
|
|
|
FILE VALIDATION EXAMPLES
|
|
|
|
Common validation rules:
|
|
|
|
// Images only, max 5MB
|
|
'file' => 'required|image|max:5120'
|
|
|
|
// PDFs only, max 10MB
|
|
'file' => 'required|mimes:pdf|max:10240'
|
|
|
|
// Documents (PDF, Word, Excel), max 20MB
|
|
'file' => 'required|mimes:pdf,doc,docx,xls,xlsx|max:20480'
|
|
|
|
// Videos, max 100MB
|
|
'file' => 'required|mimes:mp4,mov,avi|max:102400'
|
|
|
|
// Archives, max 50MB
|
|
'file' => 'required|mimes:zip,tar,gz|max:51200'
|
|
|
|
// Multiple files, each max 10MB
|
|
'files' => 'required|array',
|
|
'files.*' => 'file|max:10240'
|
|
|
|
ERROR HANDLING
|
|
|
|
All factory methods throw exceptions on failure:
|
|
|
|
try {
|
|
$attachment = File_Attachment_Model::create_from_upload(
|
|
$request->file('file'),
|
|
['site_id' => $site_id]
|
|
);
|
|
|
|
return ['success' => true, 'key' => $attachment->key];
|
|
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
|
|
Common exceptions:
|
|
- File not found (create_from_disk)
|
|
- Network timeout (create_from_url)
|
|
- Missing site_id parameter
|
|
- Disk write failure
|
|
- Invalid file type
|
|
|
|
RETRIEVING UPLOADED FILES
|
|
|
|
Get files attached to a model:
|
|
|
|
// Get all files for a project
|
|
$files = File_Attachment_Model::where('fileable_type', 'Project_Model')
|
|
->where('fileable_id', $project_id)
|
|
->get();
|
|
|
|
// Get specific category
|
|
$documents = File_Attachment_Model::where('fileable_type', 'Project_Model')
|
|
->where('fileable_id', $project_id)
|
|
->where('fileable_category', 'document')
|
|
->orderBy('fileable_order')
|
|
->get();
|
|
|
|
// Get by type meta
|
|
$avatar = File_Attachment_Model::where('fileable_type', 'User_Model')
|
|
->where('fileable_id', $user_id)
|
|
->where('fileable_type_meta', 'profile')
|
|
->first();
|
|
|
|
DELETING FILES
|
|
|
|
Simple deletion (auto-cleanup handles storage):
|
|
|
|
$attachment = File_Attachment_Model::find_by_key($key);
|
|
$attachment->delete();
|
|
|
|
// Storage automatically deleted if no other attachments reference it
|
|
|
|
Delete all files for a model:
|
|
|
|
File_Attachment_Model::where('fileable_type', 'Project_Model')
|
|
->where('fileable_id', $project_id)
|
|
->delete();
|
|
|
|
DRAG-AND-DROP UPLOADS WITH DROPPABLE
|
|
|
|
Use Droppable to enable drag-and-drop file uploads in JQHTML components.
|
|
See droppable.txt for full documentation.
|
|
|
|
Basic Integration:
|
|
|
|
Template (My_Uploader.jqhtml):
|
|
|
|
<Define:My_Uploader tag="div" class="rsx-droppable My_Uploader">
|
|
<div class="drop-hint">Drop files here or click to upload</div>
|
|
<input type="file" $sid="file_input" style="display: none" />
|
|
<ul $sid="file_list"></ul>
|
|
</Define:My_Uploader>
|
|
|
|
JavaScript (My_Uploader.js):
|
|
|
|
class My_Uploader extends Jqhtml_Component {
|
|
on_create() {
|
|
// Component event - register once, persists across re-renders
|
|
this.on('file-drop', (_, data) => {
|
|
this._upload_files(Array.from(data.files));
|
|
});
|
|
}
|
|
|
|
on_render() {
|
|
// DOM events on children - re-attach after each render
|
|
this.$.on('click', () => this.$sid('file_input').click());
|
|
this.$sid('file_input').on('change', (e) => {
|
|
this._upload_files(Array.from(e.target.files));
|
|
});
|
|
}
|
|
|
|
async _upload_files(files) {
|
|
for (let file of files) {
|
|
// Validate
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
Flash.error(`${file.name} exceeds 10 MB limit`);
|
|
continue;
|
|
}
|
|
|
|
// Upload
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const response = await $.ajax({
|
|
url: '/_upload',
|
|
type: 'POST',
|
|
data: formData,
|
|
processData: false,
|
|
contentType: false
|
|
});
|
|
|
|
if (response.success) {
|
|
this._add_to_list(response.attachment);
|
|
}
|
|
}
|
|
}
|
|
|
|
_add_to_list(attachment) {
|
|
this.$sid('file_list').append(
|
|
`<li>${attachment.file_name}</li>`
|
|
);
|
|
}
|
|
}
|
|
|
|
SCSS (My_Uploader.scss):
|
|
|
|
.My_Uploader {
|
|
border: 2px dashed #ccc;
|
|
padding: 20px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
|
|
&.rsx-drop-active {
|
|
border-color: #007bff;
|
|
background: rgba(0, 123, 255, 0.05);
|
|
}
|
|
|
|
&.rsx-drop-target {
|
|
border-style: solid;
|
|
background: rgba(0, 123, 255, 0.15);
|
|
}
|
|
}
|
|
|
|
SECURITY CONSIDERATIONS
|
|
|
|
1. Always validate files:
|
|
- File type (MIME type validation)
|
|
- File size (prevent DoS)
|
|
- Filename sanitization (handled automatically)
|
|
|
|
2. Always require authentication:
|
|
- Use #[Auth('Permission::authenticated()')]
|
|
- Check user permissions for file access
|
|
|
|
3. Never trust user input:
|
|
- Validate all parameters
|
|
- Use Laravel validation rules
|
|
|
|
4. Rate limiting:
|
|
- Add rate limiting to upload endpoints
|
|
- Prevent abuse
|
|
|
|
Example with rate limiting:
|
|
|
|
#[Route('/files/upload', method: 'POST')]
|
|
#[Auth('Permission::authenticated()')]
|
|
#[Ajax_Endpoint]
|
|
public static function upload(Request $request, array $params = [])
|
|
{
|
|
// Rate limit: 10 uploads per minute
|
|
if (RateLimiter::tooManyAttempts('file-upload:' . RsxAuth::id(), 10)) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Too many uploads. Please try again later.'
|
|
];
|
|
}
|
|
|
|
RateLimiter::hit('file-upload:' . RsxAuth::id(), 60);
|
|
|
|
// ... rest of upload logic
|
|
}
|
|
|
|
SEE ALSO
|
|
file_upload.txt - Complete file upload system documentation
|
|
droppable.txt - Global drag-and-drop file interception system
|
|
model.txt - Model system documentation
|
|
routing.txt - Route and endpoint documentation
|
|
|
|
VERSION
|
|
RSpade Framework 1.0
|
|
Last Updated: 2026-01-15
|