🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1438 lines
52 KiB
Plaintext
Executable File
1438 lines
52 KiB
Plaintext
Executable File
NAME
|
|
File Upload System - Physical storage and logical attachment models
|
|
|
|
SYNOPSIS
|
|
File upload system with deduplication, hash collision handling, and
|
|
polymorphic attachments.
|
|
|
|
DESCRIPTION
|
|
The RSpade file upload system uses a two-model architecture to separate
|
|
physical file storage from logical file metadata. This enables efficient
|
|
deduplication while maintaining rich metadata for each upload.
|
|
|
|
MODELS
|
|
|
|
File_Storage_Model (Framework Model)
|
|
Location: /system/app/Models/File_Storage_Model.php
|
|
Table: file_storage
|
|
Purpose: Represents unique physical files on disk
|
|
|
|
Fields:
|
|
id Primary key
|
|
hash SHA-256 hash (or incremented variant for collisions)
|
|
size File size in bytes
|
|
created_at Timestamp
|
|
updated_at Timestamp
|
|
created_by User ID who first uploaded this physical file
|
|
updated_by User ID who last referenced this physical file
|
|
|
|
Multiple File_Attachment_Model records can reference the same
|
|
File_Storage_Model if they have identical file content (deduplication).
|
|
|
|
File_Attachment_Model (User Model)
|
|
Location: /rsx/models/File_Attachment_Model.php
|
|
Table: file_attachments
|
|
Purpose: Represents a logical file upload with metadata
|
|
|
|
Fields:
|
|
id Primary key
|
|
key 64-char cryptographically secure random hex
|
|
file_storage_id Foreign key to file_storage table
|
|
file_name Original filename
|
|
file_extension File extension
|
|
file_type_id Enum (1=image, 2=animated_image, 3=video,
|
|
4=archive, 5=text, 6=document, 7=other)
|
|
fileable_type Polymorphic: parent model class name
|
|
fileable_id Polymorphic: parent model ID
|
|
fileable_category Optional category for grouping files
|
|
fileable_type_meta Indexed VARCHAR for common searchable metadata
|
|
fileable_order Optional sort order
|
|
fileable_meta JSON blob for additional unstructured metadata
|
|
site_id Multi-tenant site ID
|
|
session_id Session ID that uploaded this file (security)
|
|
created_at Upload timestamp
|
|
updated_at Last modified timestamp
|
|
created_by User ID who uploaded
|
|
updated_by User ID who last modified
|
|
|
|
Relationships:
|
|
- file_storage() - BelongsTo File_Storage_Model
|
|
- fileable() - MorphTo any model
|
|
- site() - BelongsTo Site_Model
|
|
|
|
STORAGE STRUCTURE
|
|
|
|
Physical File Location:
|
|
storage/files/{dir1}/{dir2}/{hash}
|
|
|
|
Where:
|
|
{dir1} = First 2 characters of hash
|
|
{dir2} = Next 2 characters of hash
|
|
{hash} = Full SHA-256 hash or incremented variant
|
|
|
|
Example:
|
|
Hash: abc123def456789...
|
|
Path: /var/www/html/system/storage/files/ab/c1/abc123def456789...
|
|
|
|
Directory Structure Benefits:
|
|
- Prevents too many files in single directory
|
|
- Improves filesystem performance
|
|
- Enables efficient file distribution across storage
|
|
|
|
HASH COLLISION HANDLING
|
|
|
|
When a file is uploaded, the system uses a sophisticated collision
|
|
detection algorithm:
|
|
|
|
Algorithm:
|
|
1. Calculate SHA-256 hash of uploaded file
|
|
2. Check if file exists on disk at hash path
|
|
3. If exists:
|
|
- Perform byte-by-byte comparison
|
|
- If match: Reuse existing File_Storage_Model record
|
|
- If no match (hash collision):
|
|
* Increment hash as base-16 number
|
|
* Repeat from step 2
|
|
4. If doesn't exist:
|
|
- Save file to disk
|
|
- Create new File_Storage_Model record
|
|
|
|
Hash Increment:
|
|
The entire SHA-256 hash is treated as a base-16 number and
|
|
incremented by 1. This provides effectively unlimited collision
|
|
slots while maintaining hash-like distribution.
|
|
|
|
Example:
|
|
abc123 → abc124
|
|
abc1ff → abc200
|
|
ffffff → 1000000
|
|
|
|
Implementation:
|
|
File_Storage_Model::increment_base16_value($hash)
|
|
File_Storage_Model::find_or_create($temp_file_path)
|
|
File_Storage_Model::files_match($file1, $file2)
|
|
|
|
DEDUPLICATION
|
|
|
|
Multiple users can upload identical files without duplicating physical
|
|
storage:
|
|
|
|
User A uploads logo.png (hash: abc123...)
|
|
→ Creates File_Storage_Model (id: 1, hash: abc123...)
|
|
→ Creates File_Attachment_Model (storage_id: 1, name: logo.png)
|
|
|
|
User B uploads company-logo.png (identical content)
|
|
→ Reuses File_Storage_Model (id: 1)
|
|
→ Creates File_Attachment_Model (storage_id: 1, name: company-logo.png)
|
|
|
|
Only one physical file exists on disk, but each user has their own
|
|
logical attachment record with their chosen filename and metadata.
|
|
|
|
FILE LIFECYCLE
|
|
|
|
Creation:
|
|
ALL file uploads or file creation operations MUST create a
|
|
File_Attachment_Model record. This is the only supported way
|
|
to create files in the system.
|
|
|
|
Orphaned Files:
|
|
Files not associated to a model via polymorphic relationship
|
|
(fileable_type/fileable_id are NULL) within 24 hours may be
|
|
deleted by cleanup operations.
|
|
|
|
Storage Cleanup:
|
|
File_Storage_Model records with no associated File_Attachment_Model
|
|
records are automatically deleted along with their physical files.
|
|
|
|
Implementation: File_Attachment_Model::boot() deleted event handler
|
|
checks if storage has no remaining references and deletes it
|
|
automatically. This ensures storage is cleaned up immediately when
|
|
the last attachment is deleted, preventing orphaned files.
|
|
|
|
ATTACHMENT API
|
|
|
|
The framework provides a clean, secure API for attaching files to models.
|
|
This is the STANDARD APPROACH for all file attachments going forward.
|
|
|
|
Security Model:
|
|
Files are uploaded UNATTACHED (no fileable_* fields set) and assigned
|
|
later using attach_to() or add_to(). This ensures:
|
|
- Users can only assign files they uploaded (session validation)
|
|
- Files belong to the correct site (multi-tenant isolation)
|
|
- No pre-assignment to records the user doesn't own
|
|
|
|
Attachment Methods (on File_Attachment_Model):
|
|
|
|
attach_to($model, $category)
|
|
Attach file to model, REPLACING any existing attachment in category.
|
|
Use for single-file attachments (profile photos, logos, etc).
|
|
|
|
Example:
|
|
$attachment = File_Attachment_Model::find_by_key($key);
|
|
if ($attachment->can_user_assign_this_file()) {
|
|
$attachment->attach_to($user, 'profile_photo');
|
|
}
|
|
|
|
Behavior:
|
|
1. Validates can_user_assign_this_file() - throws exception if fails
|
|
2. Detaches any existing attachments with same category
|
|
3. Assigns this attachment to the model
|
|
|
|
add_to($model, $category)
|
|
Add file to model WITHOUT replacing existing attachments in category.
|
|
Use for multi-file attachments (documents, photos, etc).
|
|
|
|
Example:
|
|
$attachment = File_Attachment_Model::find_by_key($key);
|
|
if ($attachment->can_user_assign_this_file()) {
|
|
$attachment->add_to($project, 'documents');
|
|
}
|
|
|
|
Behavior:
|
|
1. Validates can_user_assign_this_file() - throws exception if fails
|
|
2. Assigns this attachment alongside existing ones
|
|
|
|
detach()
|
|
Detach file from its current assignment without deleting it.
|
|
|
|
Example:
|
|
$attachment->detach();
|
|
|
|
Warning: Detached files may be cleaned up by periodic maintenance
|
|
|
|
can_user_assign_this_file()
|
|
Validates whether the current user can assign this attachment.
|
|
|
|
Checks:
|
|
1. File not already assigned (fileable_type/id are NULL)
|
|
2. Same site_id as current site
|
|
3. Same session_id as current session
|
|
|
|
Returns: bool
|
|
|
|
Example:
|
|
if ($attachment->can_user_assign_this_file()) {
|
|
$attachment->attach_to($user, 'profile_photo');
|
|
} else {
|
|
return ['success' => false, 'error' => 'Cannot assign file'];
|
|
}
|
|
|
|
Helper Methods (on all models via Rsx_Model_Abstract):
|
|
|
|
get_attachment($category)
|
|
Get single attachment by category. Use for single-file fields.
|
|
|
|
Example:
|
|
$profile_photo = $user->get_attachment('profile_photo');
|
|
if ($profile_photo) {
|
|
echo $profile_photo->get_thumbnail_url('cover', 128, 128);
|
|
}
|
|
|
|
get_attachments($category)
|
|
Get all attachments by category. Use for multi-file fields.
|
|
|
|
Example:
|
|
$documents = $project->get_attachments('documents');
|
|
foreach ($documents as $doc) {
|
|
echo $doc->file_name;
|
|
}
|
|
|
|
Complete Workflow Example:
|
|
|
|
// Frontend: Upload file
|
|
$.ajax({
|
|
url: '/_upload',
|
|
type: 'POST',
|
|
data: formData,
|
|
success: function(response) {
|
|
// Store key for form submission
|
|
$('#profile_photo').val(response.attachment.key);
|
|
}
|
|
});
|
|
|
|
// Backend: Assign file on form save
|
|
#[Ajax_Endpoint]
|
|
public static function save_profile(Request $request, array $params = []) {
|
|
$user = Session::get_user();
|
|
|
|
if (!empty($params['profile_photo'])) {
|
|
$attachment = File_Attachment_Model::find_by_key($params['profile_photo']);
|
|
if ($attachment && $attachment->can_user_assign_this_file()) {
|
|
$attachment->attach_to($user, 'profile_photo');
|
|
}
|
|
}
|
|
|
|
return ['success' => true];
|
|
}
|
|
|
|
// Display: Show attachment
|
|
@php
|
|
$profile_photo = $user->get_attachment('profile_photo');
|
|
@endphp
|
|
@if($profile_photo)
|
|
<img src="{{ $profile_photo->get_thumbnail_url('cover', 128, 128) }}">
|
|
@else
|
|
<div>No photo</div>
|
|
@endif
|
|
|
|
POLYMORPHIC ATTACHMENTS (Legacy)
|
|
|
|
NOTE: Direct manipulation of fileable_* fields is considered LEGACY.
|
|
Use the Attachment API (attach_to/add_to) instead.
|
|
|
|
For programmatic/internal use, files can still be attached using
|
|
direct field assignment:
|
|
|
|
// Internal/programmatic attachment (bypasses security checks)
|
|
$attachment->fileable_type = 'User_Model';
|
|
$attachment->fileable_id = $user->id;
|
|
$attachment->fileable_category = 'system_generated';
|
|
$attachment->save();
|
|
|
|
Querying Attached Files:
|
|
File_Attachment_Model::forModel('Project_Model', $id)->get()
|
|
File_Attachment_Model::inCategory('documents')->get()
|
|
|
|
ATTACHMENT METADATA
|
|
|
|
Three levels of metadata provide flexibility for different use cases:
|
|
|
|
fileable_category (VARCHAR, nullable)
|
|
Broad category for grouping files
|
|
Examples: 'avatar', 'background', 'document', 'image', 'video'
|
|
|
|
fileable_type_meta (VARCHAR, indexed, nullable)
|
|
Specific type within category - indexed for efficient queries
|
|
Examples: 'profile', 'cover', 'header', 'thumbnail', 'original'
|
|
|
|
fileable_meta (TEXT/JSON, nullable)
|
|
Unstructured JSON blob for additional metadata
|
|
Examples: {"variant":"winter", "size":"large", "crop":"center"}
|
|
|
|
Usage Examples:
|
|
|
|
// User has profile photo and cover photo
|
|
$profile = new File_Attachment_Model();
|
|
$profile->fileable_type = 'User_Model';
|
|
$profile->fileable_id = $user->id;
|
|
$profile->fileable_category = 'image';
|
|
$profile->fileable_type_meta = 'profile';
|
|
$profile->save();
|
|
|
|
$cover = new File_Attachment_Model();
|
|
$cover->fileable_type = 'User_Model';
|
|
$cover->fileable_id = $user->id;
|
|
$cover->fileable_category = 'image';
|
|
$cover->fileable_type_meta = 'cover';
|
|
$cover->save();
|
|
|
|
// Query by type_meta
|
|
$profile = File_Attachment_Model::forModel('User_Model', $user->id)
|
|
->byTypeMeta('profile')
|
|
->first();
|
|
|
|
// Using JSON metadata
|
|
$attachment->set_meta([
|
|
'variant' => 'winter',
|
|
'processed' => true,
|
|
'filters' => ['brightness', 'contrast']
|
|
]);
|
|
$attachment->save();
|
|
|
|
$meta = $attachment->get_meta();
|
|
// ['variant' => 'winter', 'processed' => true, ...]
|
|
|
|
FILE ACCESS
|
|
|
|
Each File_Attachment_Model has a unique 64-character hex key for
|
|
secure access:
|
|
|
|
URL Generation:
|
|
$attachment->get_url() // View/inline
|
|
$attachment->get_download_url() // Force download
|
|
|
|
Example URLs:
|
|
/files/abc123def456... // Inline viewing
|
|
/files/abc123def456...?download=1 // Download
|
|
|
|
The key is generated using cryptographically secure random bytes:
|
|
File_Attachment_Model::generate_key()
|
|
|
|
FILE SIZE
|
|
|
|
File size is stored ONLY in File_Storage_Model to avoid denormalization.
|
|
File_Attachment_Model retrieves size through the relationship:
|
|
|
|
$attachment->get_size() // Returns bytes (int)
|
|
bytes_to_human($attachment->get_size()) // e.g., "2.5 MB"
|
|
|
|
The bytes_to_human() helper function formats byte values for display.
|
|
|
|
FILE TYPE DETECTION
|
|
|
|
Files are categorized by MIME type:
|
|
|
|
File_Attachment_Model::FILE_TYPE_IMAGE (1)
|
|
File_Attachment_Model::FILE_TYPE_ANIMATED_IMAGE (2)
|
|
File_Attachment_Model::FILE_TYPE_VIDEO (3)
|
|
File_Attachment_Model::FILE_TYPE_ARCHIVE (4)
|
|
File_Attachment_Model::FILE_TYPE_TEXT (5)
|
|
File_Attachment_Model::FILE_TYPE_DOCUMENT (6)
|
|
File_Attachment_Model::FILE_TYPE_OTHER (7)
|
|
|
|
Helper Methods:
|
|
$attachment->is_image()
|
|
$attachment->is_video()
|
|
$attachment->is_document()
|
|
$attachment->file_type_id_label
|
|
|
|
HTTP ENDPOINTS
|
|
|
|
All file attachment operations are handled by a unified controller with
|
|
comprehensive security hooks and event support.
|
|
|
|
Controller: App\RSpade\Core\Files\File_Attachment_Controller
|
|
|
|
Available Endpoints:
|
|
POST /_upload - Upload file
|
|
GET /_download/:key - Download file
|
|
GET /_inline/:key - View file inline
|
|
GET /_thumbnail/:key/:type/:width/:height? - Generate thumbnail
|
|
GET /_icon_by_extension/:extension - Get file type icon
|
|
|
|
UPLOAD ENDPOINT
|
|
|
|
POST /_upload
|
|
|
|
Upload files with automatic deduplication, metadata extraction, and session-based
|
|
security tracking. Files are uploaded UNATTACHED and must be assigned to models
|
|
using the attach_to() or add_to() methods after validation.
|
|
|
|
Auth: Permission::anybody() (use event hooks for authorization)
|
|
|
|
Request Parameters:
|
|
file (required) - The uploaded file
|
|
site_id (required) - Site ID for multi-tenant isolation
|
|
filename_override (optional) - Override the uploaded filename
|
|
|
|
SECURITY: User-provided fileable_* parameters are IGNORED
|
|
For security reasons, files uploaded via the web endpoint cannot be
|
|
pre-assigned to models. This prevents users from attaching files to
|
|
records they don't own.
|
|
|
|
Workflow:
|
|
1. Upload file → Gets session_id, no fileable_* fields set
|
|
2. Submit form with attachment key
|
|
3. Backend validates can_user_assign_this_file()
|
|
4. Backend calls attach_to() or add_to() to assign file
|
|
|
|
Session Tracking:
|
|
All uploaded files automatically receive:
|
|
- session_id: Current session ID for security validation
|
|
- site_id: Multi-tenant site identifier
|
|
|
|
These fields enable can_user_assign_this_file() to verify users
|
|
can only assign files they uploaded in their current session
|
|
|
|
Response Format (JSON):
|
|
{
|
|
"success": true,
|
|
"attachment": {
|
|
"key": "hash...",
|
|
"file_name": "document.pdf",
|
|
"file_type": "Document",
|
|
"file_extension": "pdf",
|
|
"url": "http://example.com/files/hash...",
|
|
"download_url": "http://example.com/files/hash...?download=1",
|
|
"size": 1048576,
|
|
"width": null,
|
|
"height": 1920,
|
|
"is_animated": false,
|
|
"duration": null
|
|
}
|
|
}
|
|
|
|
Example Request:
|
|
curl -X POST http://localhost/_upload \
|
|
-F "file=@document.pdf" \
|
|
-F "site_id=1" \
|
|
-F "fileable_type=post" \
|
|
-F "fileable_id=42" \
|
|
-F "fileable_category=attachment"
|
|
|
|
Event Hooks:
|
|
The upload endpoint fires four events for customization:
|
|
|
|
1. file.upload.authorize (gate)
|
|
Purpose: Authorization check
|
|
Data: {request: Request, user: User|null, params: array}
|
|
Return: true to allow, or Response to deny
|
|
|
|
Example:
|
|
#[OnEvent('file.upload.authorize', priority: 10)]
|
|
public static function require_auth($data) {
|
|
if (!Session::is_logged_in()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'Authentication required'
|
|
], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
2. file.upload.params (filter)
|
|
Purpose: Modify upload parameters before file is saved
|
|
Data: array of upload params
|
|
Return: modified params array
|
|
|
|
Example:
|
|
#[OnEvent('file.upload.params', priority: 10)]
|
|
public static function add_metadata($params) {
|
|
$params['fileable_type_meta'] = 'user_uploaded';
|
|
return $params;
|
|
}
|
|
|
|
3. file.upload.complete (action)
|
|
Purpose: Post-upload processing (logging, notifications)
|
|
Data: {attachment: File_Attachment_Model, request: Request, params: array}
|
|
Return: void (ignored)
|
|
|
|
Example:
|
|
#[OnEvent('file.upload.complete', priority: 10)]
|
|
public static function log_upload($data) {
|
|
Log::info("File uploaded: " . $data['attachment']->file_name);
|
|
}
|
|
|
|
4. file.upload.response (filter)
|
|
Purpose: Modify response data before sending to client
|
|
Data: {success: bool, attachment: array}
|
|
Return: modified response array
|
|
|
|
Example:
|
|
#[OnEvent('file.upload.response', priority: 10)]
|
|
public static function add_cdn_url($response) {
|
|
$response['cdn_url'] = cdn_url($response['attachment']['key']);
|
|
return $response;
|
|
}
|
|
|
|
See Also:
|
|
php artisan rsx:man event_hooks
|
|
|
|
DOWNLOAD AND INLINE VIEWING
|
|
|
|
GET /_download/:key
|
|
GET /_inline/:key
|
|
|
|
Serve uploaded files for download or inline browser display.
|
|
|
|
Both endpoints implement cascading security checks using event hooks.
|
|
|
|
URL Parameters:
|
|
key - 64-character attachment key
|
|
|
|
Headers:
|
|
Cache-Control: public, max-age=31536000 (1 year)
|
|
|
|
Security: CASCADING MODEL
|
|
Both endpoints check TWO security hooks in order:
|
|
1. file.thumbnail.authorize - Base file access permission
|
|
2. file.download.authorize - Additional download restrictions
|
|
|
|
This cascading model ensures:
|
|
- Thumbnail security automatically protects downloads
|
|
- Download restrictions don't affect thumbnail viewing
|
|
- Developers can't forget to secure downloads if they secure thumbnails
|
|
|
|
Event Hooks:
|
|
|
|
1. file.thumbnail.authorize (gate)
|
|
Purpose: Control file access (thumbnails and downloads)
|
|
Data: {attachment: File_Attachment_Model, user: User|null, request: Request}
|
|
Return: true to allow, or Response to deny
|
|
|
|
Example:
|
|
#[OnEvent('file.thumbnail.authorize', priority: 10)]
|
|
public static function check_file_access($data) {
|
|
// Only allow access to files the user owns
|
|
if ($data['attachment']->created_by !== $data['user']?->id) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'Access denied'
|
|
], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
2. file.download.authorize (gate)
|
|
Purpose: Additional restrictions for full file downloads
|
|
Data: {attachment: File_Attachment_Model, user: User|null, request: Request}
|
|
Return: true to allow, or Response to deny
|
|
|
|
Example:
|
|
#[OnEvent('file.download.authorize', priority: 10)]
|
|
public static function check_download_access($data) {
|
|
// Require premium subscription for downloads
|
|
if (!$data['user']?->has_premium()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'Premium subscription required'
|
|
], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Response Headers:
|
|
/_download - Content-Disposition: attachment (forces download)
|
|
/_inline - Content-Disposition: inline (displays in browser)
|
|
|
|
Example Usage:
|
|
// Link to view image in browser
|
|
<img src="/_inline/<?= $attachment->key ?>">
|
|
|
|
// Link to download file
|
|
<a href="/_download/<?= $attachment->key ?>">Download</a>
|
|
|
|
THUMBNAIL GENERATION
|
|
|
|
GET /_thumbnail/:key/:type/:width/:height?
|
|
|
|
Generate thumbnails for uploaded files with automatic fallback to file type icons.
|
|
|
|
URL Parameters:
|
|
key - 64-character attachment key
|
|
type - Thumbnail mode: 'cover' or 'fit'
|
|
width - Width in pixels (10-256)
|
|
height - Height in pixels (10-256, optional)
|
|
|
|
Thumbnail Modes:
|
|
cover - Fill entire dimensions, crop excess (like CSS background-size: cover)
|
|
fit - Maintain aspect ratio within bounds, transparent background
|
|
|
|
Automatic Icon Fallback:
|
|
For non-image files (PDF, documents, archives, etc.), the system automatically
|
|
generates icon-based thumbnails instead of returning errors:
|
|
|
|
- Small thumbnails (< 72x72): Icon scaled to fill dimensions
|
|
- Large thumbnails (≥ 72x72): 64x64 icon centered on white canvas
|
|
|
|
This provides consistent visual representation for all file types.
|
|
|
|
Supported Image Formats:
|
|
JPG, PNG, GIF, WEBP, BMP
|
|
|
|
Future Format Support:
|
|
PDF pages, PSD layers, DOCX previews, video frames
|
|
|
|
Output Format:
|
|
WebP with 85% quality (smaller file sizes, broad browser support)
|
|
|
|
Cache Headers:
|
|
Cache-Control: public, max-age=31536000 (1 year)
|
|
|
|
Security:
|
|
Checks file.thumbnail.authorize event hook only
|
|
(Downloads check both thumbnail AND download hooks)
|
|
|
|
Example Usage:
|
|
// Profile photo (96x96, cropped to square)
|
|
<img src="/_thumbnail/<?= $key ?>/cover/96/96">
|
|
|
|
// Gallery thumbnail (200x200)
|
|
<img src="/_thumbnail/<?= $key ?>/cover/200/200">
|
|
|
|
// Document preview (maintain aspect ratio)
|
|
<img src="/_thumbnail/<?= $key ?>/fit/240/180">
|
|
|
|
// PDF will show PDF icon, images show actual thumbnail
|
|
|
|
FILE TYPE ICONS
|
|
|
|
GET /_icon_by_extension/:extension
|
|
|
|
Retrieve file type icon as PNG for displaying file types visually.
|
|
|
|
URL Parameters:
|
|
extension - File extension without dot (e.g., 'pdf', 'jpg', 'stl')
|
|
|
|
Query Parameters:
|
|
width - Icon width in pixels (10-256, default: 64)
|
|
height - Icon height in pixels (10-256, default: 64)
|
|
|
|
Icon Library:
|
|
80+ file type icons from open-source libraries:
|
|
- Papirus (GPL-3.0) - Generic file type icons
|
|
- Free File Icons (MIT) - PDF, PSD, AI icons
|
|
- Lucide (ISC) - 3D model icons
|
|
- Custom - Code files with {} brackets
|
|
|
|
Icon Categories:
|
|
- Images (jpg, png, gif, svg, psd, ai)
|
|
- Videos (mp4, avi, mov, webm)
|
|
- Audio (mp3, wav, flac)
|
|
- Documents (pdf, doc, xls, ppt, txt)
|
|
- Archives (zip, rar, 7z)
|
|
- Code (php, js, py, html, css)
|
|
- 3D Models (stl, obj, fbx, blend)
|
|
|
|
Output Format:
|
|
PNG with transparent background
|
|
|
|
Cache Headers:
|
|
Cache-Control: public, max-age=86400 (24 hours)
|
|
|
|
Example Usage:
|
|
// Show PDF icon
|
|
<img src="/_icon_by_extension/pdf?width=64&height=64">
|
|
|
|
// Show file type icon from attachment
|
|
<img src="/_icon_by_extension/<?= $attachment->file_extension ?>">
|
|
|
|
JavaScript Usage:
|
|
// After upload, show file type icon
|
|
const extension = response.attachment.file_extension;
|
|
$('#icon').attr('src', `/_icon_by_extension/${extension}?width=100&height=100`);
|
|
|
|
Icon Caching:
|
|
Icons are cached by extension and dimensions, allowing efficient
|
|
deduplication across all files of the same type.
|
|
|
|
SECURITY ARCHITECTURE
|
|
|
|
The file attachment system implements a cascading security model using
|
|
event hooks to provide flexible, layered access control.
|
|
|
|
Security Hook Types:
|
|
1. file.upload.authorize - Controls who can upload files
|
|
2. file.thumbnail.authorize - Controls who can view thumbnails/files
|
|
3. file.download.authorize - Additional restrictions for downloads
|
|
|
|
Cascading Model:
|
|
|
|
Thumbnails:
|
|
/_thumbnail/:key checks:
|
|
- file.thumbnail.authorize ✓
|
|
|
|
Downloads:
|
|
/_download/:key and /_inline/:key check:
|
|
- file.thumbnail.authorize ✓
|
|
- file.download.authorize ✓
|
|
|
|
Benefits:
|
|
- Implement thumbnail security once, downloads are automatically protected
|
|
- Add stricter download rules without affecting thumbnail viewing
|
|
- Impossible to forget download security if thumbnail security exists
|
|
- Thumbnails can be public while downloads require authentication
|
|
|
|
Common Patterns:
|
|
|
|
Public Thumbnails, Authenticated Downloads:
|
|
#[OnEvent('file.thumbnail.authorize', priority: 10)]
|
|
public static function allow_thumbnails($data) {
|
|
// Allow anyone to view thumbnails
|
|
return true;
|
|
}
|
|
|
|
#[OnEvent('file.download.authorize', priority: 10)]
|
|
public static function require_auth_for_downloads($data) {
|
|
if (!Session::is_logged_in()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'Login required to download files'
|
|
], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Private Files (Thumbnails and Downloads):
|
|
#[OnEvent('file.thumbnail.authorize', priority: 10)]
|
|
public static function restrict_all_access($data) {
|
|
// Check ownership or permissions
|
|
if (!can_access_file($data['attachment'], $data['user'])) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'Access denied'
|
|
], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// No need for download hook - thumbnail security covers it
|
|
|
|
Premium Content:
|
|
#[OnEvent('file.thumbnail.authorize', priority: 10)]
|
|
public static function allow_previews($data) {
|
|
// Allow anyone to view thumbnails (previews)
|
|
return true;
|
|
}
|
|
|
|
#[OnEvent('file.download.authorize', priority: 10)]
|
|
public static function require_premium($data) {
|
|
// Require premium subscription for full downloads
|
|
if (!$data['user']?->has_premium_subscription()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'Premium subscription required'
|
|
], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Event Hook Data:
|
|
All download/view hooks receive:
|
|
- attachment: File_Attachment_Model instance
|
|
- user: Currently authenticated user (or null)
|
|
- request: Illuminate\Http\Request instance
|
|
|
|
Access attachment metadata:
|
|
$data['attachment']->fileable_type
|
|
$data['attachment']->fileable_id
|
|
$data['attachment']->fileable_category
|
|
$data['attachment']->file_type_id
|
|
$data['attachment']->created_by
|
|
|
|
See Also:
|
|
php artisan rsx:man event_hooks
|
|
|
|
SEARCH INDEX SYSTEM
|
|
|
|
Search_Index_Model (Framework Model)
|
|
Location: /system/app/Models/Search_Index_Model.php
|
|
Table: search_indexes
|
|
Purpose: Full-text searchable content extracted from files and other sources
|
|
|
|
Fields:
|
|
id Primary key
|
|
indexable_type Polymorphic: source model class name
|
|
indexable_id Polymorphic: source model ID
|
|
content LONGTEXT with FULLTEXT index
|
|
metadata JSON: extracted metadata (author, title, page count, etc.)
|
|
indexed_at When content was extracted (NULL = stale/needs reindex)
|
|
extraction_method Method used (e.g., 'pdftotext', 'tesseract_ocr', 'docx_parser')
|
|
language Language code for stemming (e.g., 'en', 'es', 'fr')
|
|
site_id Multi-tenant site ID
|
|
created_at First indexing timestamp
|
|
updated_at Last update timestamp
|
|
created_by User who triggered indexing
|
|
updated_by User who last updated
|
|
|
|
Relationships:
|
|
- indexable() - MorphTo any model (primary: File_Attachment_Model)
|
|
|
|
Primary Use Case: File Content Indexing
|
|
While this model is polymorphic and can index any content type (blog posts,
|
|
comments, pages, etc.), its primary purpose is indexing file content:
|
|
- PDF documents → extract text from pages
|
|
- Word documents → extract text content
|
|
- Images → OCR extracted text
|
|
- Excel sheets → extract cell data
|
|
- PowerPoint → extract slide text
|
|
|
|
Why Polymorphic:
|
|
Future flexibility for indexing non-file content without creating
|
|
duplicate search infrastructure. A single FULLTEXT index across all
|
|
searchable content enables powerful cross-content search.
|
|
|
|
Text Extraction is Expensive:
|
|
Parsing PDFs, running OCR, extracting from Office documents is
|
|
computationally expensive. This model caches extracted content to avoid
|
|
re-processing files on every search.
|
|
|
|
Stale Index Detection:
|
|
indexed_at NULL indicates content needs re-extraction:
|
|
- File was updated/replaced
|
|
- Extraction method improved (new parser version)
|
|
- Initial extraction failed
|
|
|
|
Search Capabilities:
|
|
// Full-text search across all content
|
|
Search_Index_Model::search('+urgent +report', 'BOOLEAN')
|
|
->where('site_id', $site_id)
|
|
->get();
|
|
|
|
// Search specific model type
|
|
Search_Index_Model::search('contract', 'NATURAL LANGUAGE')
|
|
->where('indexable_type', 'File_Attachment_Model')
|
|
->get();
|
|
|
|
// Filter by language
|
|
Search_Index_Model::search('documento')
|
|
->byLanguage('es')
|
|
->get();
|
|
|
|
Metadata Usage:
|
|
Extracted metadata enables faceted search and filtering:
|
|
- Document author
|
|
- Page count
|
|
- Creation date (from file metadata, not upload date)
|
|
- EXIF data from images (camera, location, timestamp)
|
|
- Office document properties
|
|
|
|
Example:
|
|
$index->set_metadata([
|
|
'author' => 'John Doe',
|
|
'pages' => 42,
|
|
'created_date' => '2024-03-15',
|
|
'keywords' => ['contract', 'legal', 'confidential']
|
|
]);
|
|
|
|
Index Management:
|
|
// Create or update index
|
|
$index = Search_Index_Model::find_or_create_for_model(
|
|
'File_Attachment_Model',
|
|
$attachment->id
|
|
);
|
|
$index->content = $extracted_text;
|
|
$index->extraction_method = 'pdftotext';
|
|
$index->language = 'en';
|
|
$index->indexed_at = now();
|
|
$index->save();
|
|
|
|
// Mark index as stale (needs re-indexing)
|
|
$index->mark_stale();
|
|
|
|
// Check if needs re-indexing
|
|
if ($index->is_stale()) {
|
|
// Re-extract content
|
|
}
|
|
|
|
TODO: Future Documentation
|
|
Search indexing will be moved to its own man page (search_index.txt)
|
|
when non-file indexing features are implemented. For now, it's
|
|
documented here as part of the file upload system since that's its
|
|
primary use case.
|
|
|
|
THUMBNAIL SYSTEM
|
|
|
|
File_Thumbnail_Model (Framework Model)
|
|
Location: /system/app/Models/File_Thumbnail_Model.php
|
|
Table: file_thumbnails
|
|
Purpose: Cache generated thumbnails with deduplication
|
|
|
|
Fields:
|
|
id Primary key
|
|
source_storage_id FK to file_storage (original file)
|
|
thumbnail_storage_id FK to file_storage (thumbnail bytes)
|
|
params JSON: {width, height, crop, format, quality}
|
|
detected_mime_type Actual MIME type from file content
|
|
created_at Generation timestamp
|
|
updated_at Last accessed timestamp
|
|
created_by User who first requested this thumbnail
|
|
updated_by User who last requested this thumbnail
|
|
|
|
Thumbnail Deduplication:
|
|
Thumbnails are cached and deduplicated based on three factors:
|
|
- Source file hash (File_Storage_Model.hash)
|
|
- Thumbnail parameters (width, height, crop, format, quality)
|
|
- Detected MIME type (from actual file content, not user-provided)
|
|
|
|
Thumbnail Cache Key Formula:
|
|
SHA-256(source_hash + params_json + detected_mime_type)
|
|
|
|
This ensures that:
|
|
- Same source file + same params = same thumbnail (deduplicated)
|
|
- Different users uploading identical files share thumbnails
|
|
- MIME type is based on actual content, not filename
|
|
- Cache keys are deterministic and collision-resistant
|
|
|
|
Example:
|
|
User A uploads photo.jpg (hash: abc123..., detected: image/jpeg)
|
|
User B uploads photo.png (same bytes, hash: abc123..., detected: image/jpeg)
|
|
|
|
Request: /files/{key}?thumb=200x200&crop=center
|
|
|
|
Both generate the same thumbnail cache key:
|
|
SHA-256("abc123..." + '{"width":200,"height":200,"crop":"center"}' + "image/jpeg")
|
|
|
|
Result: One physical thumbnail file shared by both users
|
|
|
|
Thumbnail Generation Flow:
|
|
1. Client requests: /files/{attachment_key}?thumb=200x200&crop=center
|
|
2. Load File_Attachment_Model by key
|
|
3. Load File_Storage_Model via relationship
|
|
4. Detect actual MIME type from physical file on disk
|
|
5. Calculate thumbnail cache key
|
|
6. Check File_Thumbnail_Model for existing thumbnail
|
|
- If found: serve cached thumbnail from thumbnail_storage_id
|
|
- If not found: generate thumbnail, create File_Storage_Model
|
|
for thumbnail bytes, create File_Thumbnail_Model record
|
|
7. Return thumbnail to client
|
|
|
|
Why Detect MIME Type:
|
|
Users can upload the same file with different extensions:
|
|
- photo.jpg (JPEG data)
|
|
- photo.png (same JPEG data, wrong extension)
|
|
|
|
The system detects actual content type (image/jpeg) from file headers,
|
|
ensuring both files share the same thumbnail regardless of filename.
|
|
|
|
Thumbnail as Derivative Artifact:
|
|
Thumbnails are NOT File_Attachment_Model records because:
|
|
- They are generated artifacts, not user uploads
|
|
- They are tied to source file + params, not user metadata
|
|
- They should be deleted when source is deleted (cascade)
|
|
- They don't need polymorphic relationships or site-scoping
|
|
|
|
Cleanup:
|
|
When a File_Storage_Model is deleted:
|
|
- All File_Thumbnail_Model records cascade delete (foreign key)
|
|
- Orphaned thumbnail File_Storage_Model records can be cleaned up
|
|
- This is automatic via database foreign key constraints
|
|
|
|
CLI COMMANDS
|
|
|
|
User Commands (rsx:file:*)
|
|
These commands are always visible and handle file attachment operations.
|
|
All operations use file locks to prevent race conditions.
|
|
|
|
rsx:file:upload
|
|
Upload a file from disk and create a File_Attachment_Model.
|
|
|
|
Usage:
|
|
rsx:file:upload /path/to/file.pdf --name="Report" --site=1
|
|
|
|
Required Options:
|
|
--site=ID Site ID for multi-tenant scoping
|
|
|
|
Optional Options:
|
|
--name="Name" Override filename (default: actual filename)
|
|
--category="cat" Set fileable_category
|
|
--type-meta="meta" Set fileable_type_meta
|
|
--meta='{"key":"value"}' Set fileable_meta JSON
|
|
|
|
Attachment Options:
|
|
--model=Model:ID Attach to model (e.g., User_Model:42)
|
|
|
|
Returns:
|
|
File attachment key for accessing the file
|
|
|
|
Examples:
|
|
# Upload file for specific site
|
|
rsx:file:upload /tmp/document.pdf --site=1
|
|
|
|
# Upload and attach to user profile
|
|
rsx:file:upload /tmp/avatar.jpg --site=1 \
|
|
--model=User_Model:42 \
|
|
--category="avatar" \
|
|
--type-meta="profile"
|
|
|
|
# Upload with metadata
|
|
rsx:file:upload /tmp/report.pdf --site=1 \
|
|
--meta='{"version":"2.1","department":"Sales"}'
|
|
|
|
rsx:file:info
|
|
Display detailed information about a file attachment.
|
|
|
|
Usage:
|
|
rsx:file:info {key}
|
|
|
|
Arguments:
|
|
key 64-char hex attachment key
|
|
|
|
Displays:
|
|
- File name and extension
|
|
- File type and size
|
|
- Storage hash
|
|
- Attachment metadata
|
|
- Polymorphic relationship (if attached)
|
|
- Site and user information
|
|
- Created/updated timestamps
|
|
|
|
Example:
|
|
rsx:file:info abc123def456...
|
|
|
|
rsx:file:list
|
|
List file attachments with filtering options.
|
|
|
|
Usage:
|
|
rsx:file:list [options]
|
|
|
|
Required Options (at least one):
|
|
--site=ID List files for site
|
|
--user=ID List files created by user
|
|
--model=Model:ID List files attached to model
|
|
|
|
Filter Options:
|
|
--category="cat" Filter by fileable_category
|
|
--type-meta="meta" Filter by fileable_type_meta
|
|
--type=image Filter by file_type (image, video, document, etc.)
|
|
|
|
Format Options:
|
|
--format=table Display format (table, json, csv)
|
|
--limit=50 Limit results (default: 50)
|
|
|
|
Examples:
|
|
# List all files for site
|
|
rsx:file:list --site=1
|
|
|
|
# List user's profile images
|
|
rsx:file:list --user=42 --category="avatar"
|
|
|
|
# List project documents in JSON
|
|
rsx:file:list --model=Project_Model:10 --format=json
|
|
|
|
rsx:file:attach
|
|
Attach an existing file to a different model or update attachment metadata.
|
|
|
|
Usage:
|
|
rsx:file:attach {key} --model=Model:ID [options]
|
|
|
|
Arguments:
|
|
key 64-char hex attachment key
|
|
|
|
Required Options:
|
|
--model=Model:ID Model to attach to
|
|
|
|
Optional Options:
|
|
--category="cat" Update fileable_category
|
|
--type-meta="meta" Update fileable_type_meta
|
|
--meta='{"key":"value"}' Update fileable_meta JSON
|
|
--order=1 Set fileable_order
|
|
|
|
Example:
|
|
# Attach file to project
|
|
rsx:file:attach abc123... --model=Project_Model:5 \
|
|
--category="document" \
|
|
--order=1
|
|
|
|
rsx:file:detach
|
|
Detach a file from its parent model (sets fileable_type/id to NULL).
|
|
|
|
Usage:
|
|
rsx:file:detach {key}
|
|
|
|
Arguments:
|
|
key 64-char hex attachment key
|
|
|
|
Warning:
|
|
Detached files become orphans and may be deleted by cleanup operations
|
|
after 24 hours if not re-attached to a model.
|
|
|
|
Example:
|
|
rsx:file:detach abc123def456...
|
|
|
|
rsx:file:delete
|
|
Delete a file attachment and cleanup orphaned storage.
|
|
|
|
Usage:
|
|
rsx:file:delete {key} [--force]
|
|
|
|
Arguments:
|
|
key 64-char hex attachment key
|
|
|
|
Options:
|
|
--force Skip confirmation prompt
|
|
|
|
Behavior:
|
|
1. Deletes File_Attachment_Model record
|
|
2. If no other attachments reference the same File_Storage_Model:
|
|
- Deletes physical file from disk
|
|
- Deletes File_Storage_Model record
|
|
3. If other attachments exist, storage is preserved
|
|
|
|
Example:
|
|
rsx:file:delete abc123def456...
|
|
|
|
Storage Commands (rsx:storage:*)
|
|
These commands are hidden by default and shown only when
|
|
IS_FRAMEWORK_DEVELOPER=true in .env. They provide low-level
|
|
access to the physical file storage system.
|
|
|
|
rsx:storage:info
|
|
Display detailed information about physical file storage.
|
|
|
|
Usage:
|
|
rsx:storage:info {hash}
|
|
|
|
Arguments:
|
|
hash SHA-256 hash or incremented variant
|
|
|
|
Displays:
|
|
- Storage hash
|
|
- File size (bytes and human-readable)
|
|
- Storage path on disk
|
|
- Physical file existence
|
|
- Reference count (number of attachments)
|
|
- Created/updated timestamps
|
|
- Creator/updater user IDs
|
|
|
|
Example:
|
|
rsx:storage:info abc123def456...
|
|
|
|
rsx:storage:stats
|
|
Display statistics about the file storage system.
|
|
|
|
Usage:
|
|
rsx:storage:stats [--site=ID]
|
|
|
|
Options:
|
|
--site=ID Filter stats for specific site
|
|
|
|
Displays:
|
|
- Total physical files (File_Storage_Model count)
|
|
- Total attachments (File_Attachment_Model count)
|
|
- Deduplication ratio (attachments / storage)
|
|
- Total disk usage (sum of file sizes)
|
|
- Average file size
|
|
- Files by type breakdown
|
|
- Orphaned storage count
|
|
- Orphaned attachments count (>24hrs, no fileable)
|
|
|
|
Example:
|
|
rsx:storage:stats
|
|
rsx:storage:stats --site=1
|
|
|
|
rsx:storage:cleanup
|
|
Force cleanup of orphaned storage and attachments.
|
|
|
|
Usage:
|
|
rsx:storage:cleanup [options]
|
|
|
|
Options:
|
|
--dry-run Show what would be deleted without deleting
|
|
--force Skip confirmation prompts
|
|
--orphan-age=24 Hours before orphaned attachment is deleted (default: 24)
|
|
|
|
Cleanup Operations:
|
|
1. Delete orphaned attachments (no fileable, older than orphan-age)
|
|
2. Delete orphaned storage (no attachments)
|
|
3. Delete physical files for deleted storage
|
|
|
|
Example:
|
|
# Preview cleanup operations
|
|
rsx:storage:cleanup --dry-run
|
|
|
|
# Force cleanup of 48-hour orphans
|
|
rsx:storage:cleanup --force --orphan-age=48
|
|
|
|
Locking Strategy:
|
|
All file operations acquire a write lock using RsxLocks to prevent
|
|
race conditions:
|
|
|
|
RsxLocks::get_lock(
|
|
RsxLocks::SERVER_LOCK,
|
|
RsxLocks::LOCK_FILE_WRITE,
|
|
RsxLocks::WRITE_LOCK,
|
|
30
|
|
)
|
|
|
|
This ensures:
|
|
- No concurrent file uploads cause hash collisions
|
|
- Storage cleanup doesn't delete files mid-upload
|
|
- Attachment operations are atomic
|
|
|
|
UPLOAD METHODS
|
|
|
|
File_Attachment_Model provides factory methods for creating attachments
|
|
from various sources. All methods handle storage creation, deduplication,
|
|
and metadata automatically.
|
|
|
|
create_from_upload($file, $params)
|
|
Primary method for handling HTTP file uploads.
|
|
|
|
Parameters:
|
|
$file - Illuminate\Http\UploadedFile instance
|
|
$params - Array with:
|
|
site_id (required) - Site ID
|
|
fileable_type - Model class to attach to
|
|
fileable_id - Model ID to attach to
|
|
fileable_category - Category (e.g., 'avatar')
|
|
fileable_type_meta - Searchable metadata
|
|
fileable_meta - Array of additional metadata
|
|
fileable_order - Sort order
|
|
filename_override - Override original filename
|
|
|
|
Example:
|
|
$attachment = File_Attachment_Model::create_from_upload(
|
|
$request->file('upload'),
|
|
[
|
|
'site_id' => $site->id,
|
|
'fileable_type' => 'User_Model',
|
|
'fileable_id' => $user->id,
|
|
'fileable_category' => 'avatar'
|
|
]
|
|
);
|
|
|
|
create_from_disk($path, $params)
|
|
Create attachment from file already on disk.
|
|
Useful for importing files or processing temporary files.
|
|
|
|
Parameters:
|
|
$path - Absolute path to file on disk
|
|
$params - Same as create_from_upload() plus:
|
|
filename - Filename to use (defaults to basename)
|
|
|
|
Example:
|
|
$attachment = File_Attachment_Model::create_from_disk(
|
|
'/tmp/import/document.pdf',
|
|
[
|
|
'site_id' => $site->id,
|
|
'filename' => 'imported-document.pdf',
|
|
'fileable_category' => 'import'
|
|
]
|
|
);
|
|
|
|
create_from_string($content, $filename, $params)
|
|
Create attachment from string content.
|
|
Useful for generated files (exports, reports, rendered images).
|
|
|
|
Parameters:
|
|
$content - File content as string
|
|
$filename - Filename (must include extension)
|
|
$params - Same as create_from_upload()
|
|
|
|
Example:
|
|
$csv = "Name,Email\nJohn,john@example.com";
|
|
$attachment = File_Attachment_Model::create_from_string(
|
|
$csv,
|
|
'export.csv',
|
|
[
|
|
'site_id' => $site->id,
|
|
'fileable_type' => 'Report_Model',
|
|
'fileable_id' => $report->id,
|
|
'fileable_category' => 'export'
|
|
]
|
|
);
|
|
|
|
create_from_url($url, $params)
|
|
Download file from URL and create attachment.
|
|
Useful for importing external files.
|
|
|
|
Parameters:
|
|
$url - URL to download file from
|
|
$params - Same as create_from_upload() plus:
|
|
filename - Filename to use (defaults to URL basename)
|
|
timeout - Download timeout in seconds (default: 30)
|
|
|
|
Example:
|
|
$attachment = File_Attachment_Model::create_from_url(
|
|
'https://example.com/logo.png',
|
|
[
|
|
'site_id' => $site->id,
|
|
'fileable_category' => 'logo'
|
|
]
|
|
);
|
|
|
|
All Methods:
|
|
- Automatically create or reuse File_Storage_Model (deduplication)
|
|
- Auto-detect MIME type and file type
|
|
- Generate cryptographically secure 64-char key
|
|
- Handle temporary file cleanup
|
|
- Thread-safe (File_Storage_Model uses locking internally)
|
|
|
|
FUTURE DEVELOPMENT
|
|
|
|
Completed Features:
|
|
✓ Image thumbnail generation with Imagick (cover and fit modes)
|
|
✓ Icon-based thumbnail fallback for non-image files
|
|
✓ File type icon system (80+ types)
|
|
✓ Cascading security hooks for downloads and thumbnails
|
|
✓ WebP output format with quality control
|
|
✓ Session-based attachment security with can_user_assign_this_file()
|
|
✓ Attachment API (attach_to, add_to, detach)
|
|
✓ Model helper methods (get_attachment, get_attachments)
|
|
✓ Droppable - Global drag-and-drop file interception system
|
|
|
|
Planned Enhancements:
|
|
|
|
Periodic Cleanup System:
|
|
- Scheduled task to remove unattached files older than 24 hours
|
|
- Configurable retention period for orphaned attachments
|
|
- Cleanup of orphaned File_Storage_Model records
|
|
- Removal of physical files with no storage references
|
|
- Dry-run mode for testing cleanup operations
|
|
- Metrics and reporting for cleaned files
|
|
|
|
Implementation:
|
|
- Artisan command: rsx:file:cleanup --dry-run
|
|
- Scheduled via Laravel task scheduler
|
|
- Runs daily at off-peak hours
|
|
- Logs cleanup operations for auditing
|
|
|
|
Cleanup Rules:
|
|
- Unattached files (fileable_type/id NULL) > 24hrs → deleted
|
|
- Storage with no attachments → physical file deleted
|
|
- Session validation prevents premature deletion
|
|
|
|
Additional Thumbnail Generators:
|
|
- PDF page rendering (first page or specific page)
|
|
- PSD layer previews
|
|
- DOCX/Office document previews
|
|
- Video frame extraction
|
|
- Audio waveform visualization
|
|
|
|
Thumbnail Caching:
|
|
- Database-backed thumbnail cache (File_Thumbnail_Model)
|
|
- Deduplication across identical source files
|
|
- Icon-based thumbnail caching by extension+dimensions
|
|
- Cache pre-warming for common sizes
|
|
- Automatic cleanup of unused thumbnails
|
|
|
|
JQHTML Upload Widgets:
|
|
✓ Droppable - Global drag-and-drop file interception (see droppable.txt)
|
|
- Multi-file upload queue with progress
|
|
- Image cropper/editor widget
|
|
- Camera capture widget
|
|
- URL import widget
|
|
|
|
Advanced Thumbnail Features:
|
|
- Smart cropping (face detection, subject detection)
|
|
- Additional crop positions (top, bottom, left, right)
|
|
- Animated GIF/WebP support
|
|
- Custom watermarking
|
|
- Image filters and transformations
|
|
|
|
EXAMPLES
|
|
|
|
Basic Upload Flow:
|
|
// Receive uploaded file
|
|
$uploaded = $request->file('upload');
|
|
|
|
// Create physical storage
|
|
$storage = File_Storage_Model::find_or_create(
|
|
$uploaded->getPathname()
|
|
);
|
|
|
|
// Create logical attachment
|
|
$attachment = new File_Attachment_Model();
|
|
$attachment->file_storage_id = $storage->id;
|
|
$attachment->file_name = $uploaded->getClientOriginalName();
|
|
$attachment->file_extension = pathinfo($name, PATHINFO_EXTENSION);
|
|
$attachment->file_type_id = File_Attachment_Model::determine_file_type(
|
|
$uploaded->getMimeType()
|
|
);
|
|
$attachment->key = File_Attachment_Model::generate_key();
|
|
$attachment->save();
|
|
|
|
Attach to Model:
|
|
$attachment->fileable_type = 'User_Model';
|
|
$attachment->fileable_id = $user->id;
|
|
$attachment->fileable_category = 'profile_photo';
|
|
$attachment->save();
|
|
|
|
Retrieve and Display:
|
|
$photo = File_Attachment_Model::where('key', $key)->first();
|
|
$url = $photo->get_url();
|
|
$size = $photo->get_human_size();
|
|
$type = $photo->file_type_id_label;
|
|
|
|
SECURITY
|
|
|
|
File Access:
|
|
Files are accessed via cryptographically secure 64-character keys,
|
|
making enumeration attacks infeasible.
|
|
|
|
Permission Checking:
|
|
Routes serving files should implement permission checks based on
|
|
the fileable relationship to ensure users can only access files
|
|
they own or have permission to view.
|
|
|
|
SEE ALSO
|
|
droppable.txt - Global drag-and-drop file interception system
|
|
model.txt - Model system documentation
|
|
storage_directories.txt - Storage directory conventions
|
|
migrations.txt - Database migration system
|
|
|
|
VERSION
|
|
RSpade Framework 1.0
|
|
Last Updated: 2026-01-15
|