File Sharing Guide
Last Updated: 2026-01-12 Status: Implemented Plan Reference: 007-file-sharing-system.md, 078-file-sharing-improvement.md
Overview
The File Sharing system allows administrators and clients to upload, download, and manage files associated with projects. Files have visibility controls to determine whether clients can see them, and all file activity is logged for audit purposes.
Table of Contents
Accessing Files
Navigation
| Access Point | Location | URL | Role |
|---|---|---|---|
| Project Files | Project detail page | /admin/projects/{id}#files | Admin |
| Upload Files | Project files section | Modal on project page | Admin |
| Download File | File row | Direct download link | Both |
| Portal Files | Portal project page | /portal/projects/{id}#files | Client User |
Permissions
| Action | Admin | Client User |
|---|---|---|
| View all project files | ✅ | ❌ |
| View client-visible files | ✅ | ✅ |
| Upload files | ✅ | ✅ (own projects) |
| Download files | ✅ | ✅ (visible only) |
| Delete any file | ✅ | ❌ |
| Delete own uploads | ✅ | ✅ |
| Set visibility | ✅ | ❌ |
Features
File Upload
- Single and multiple file upload
- Drag-and-drop interface
- Progress indicator
- Automatic file type detection
- Size limit enforcement
File Management
- View file list by project
- Download individual files
- Delete files
- File metadata display
Visibility Control
- Admin-only files (internal)
- Client-visible files (shared)
- Default visibility setting
File Information
- Original filename
- File size
- Upload date
- Uploader name
- Download count
How to Use
Uploading Files (Admin)
- Navigate to Admin → Projects → [Project]
- Scroll to Files section
- Click "Upload Files" or drag files to drop zone
- Select file(s) from your computer
- Set visibility (Client Visible or Admin Only)
- Click "Upload"
- Files appear in the file list
Uploading Files (Client)
- Navigate to Portal → Projects → [Project]
- Scroll to Files section
- Click "Upload Files"
- Select file(s) from your computer
- Click "Upload"
- Files are automatically client-visible
Downloading Files
- Navigate to project files section
- Click "Download" button next to file
- File downloads to your computer
Deleting Files
Admin:
- Navigate to Admin → Projects → [Project]
- Find file in list
- Click "Delete" button
- Confirm deletion
Client:
- Navigate to Portal → Projects → [Project]
- Find file you uploaded
- Click "Delete" button
- Confirm deletion
Note: Clients can only delete files they uploaded.
Changing File Visibility
- Navigate to Admin → Projects → [Project]
- Find file in list
- Click visibility toggle
- Toggle between "Client Visible" and "Admin Only"
Visibility Settings
Client Visible
- File appears in client portal
- Client can download
- Suitable for deliverables and shared documents
Admin Only
- File hidden from client portal
- Only administrators can see and download
- Suitable for internal documents and drafts
Default Visibility
Admin uploads: Default based on settings Client uploads: Always client-visible
Technical Architecture
Model
Location: app/Models/ProjectFile.php
class ProjectFile extends Model
{
protected $fillable = [
'project_id',
'uploaded_by',
'filename',
'original_filename',
'path',
'mime_type',
'size',
'is_client_visible',
'download_count',
];
protected $casts = [
'is_client_visible' => 'boolean',
'size' => 'integer',
'download_count' => 'integer',
];
}
Relationships
// ProjectFile model
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function uploader(): BelongsTo
{
return $this->belongsTo(User::class, 'uploaded_by');
}
// Project model
public function files(): HasMany
{
return $this->hasMany(ProjectFile::class);
}
Controller
Location: app/Http/Controllers/Admin/FileController.php
| Method | Route | Description |
|---|---|---|
store() | POST /admin/projects/{project}/files | Upload file |
show() | GET /admin/files/{file} | View file details |
download() | GET /admin/files/{file}/download | Download |
destroy() | DELETE /admin/files/{file} | Delete |
toggleVisibility() | PATCH /admin/files/{file}/visibility | Toggle visibility |
Routes
// Admin file routes - Upload under project
Route::post('projects/{project}/files', [FileController::class, 'store'])->name('files.store');
// Admin file routes - Operations on file directly
Route::get('files/{file}', [FileController::class, 'show'])->name('files.show');
Route::get('files/{file}/download', [FileController::class, 'download'])->name('files.download');
Route::delete('files/{file}', [FileController::class, 'destroy'])->name('files.destroy');
Route::patch('files/{file}/visibility', [FileController::class, 'toggleVisibility'])->name('files.visibility');
// Portal file routes - Upload under project
Route::post('projects/{project}/files', [PortalFileController::class, 'store'])->name('portal.files.store');
// Portal file routes - Operations on file directly
Route::get('files/{file}/download', [PortalFileController::class, 'download'])->name('portal.files.download');
Route::delete('files/{file}', [PortalFileController::class, 'destroy'])->name('portal.files.destroy');
Database Schema
Table: project_files
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
project_id | bigint | Foreign key to projects |
uploaded_by | bigint | Foreign key to users |
filename | string | Stored filename (unique) |
original_filename | string | Original uploaded name |
path | string | Storage path |
mime_type | string | File MIME type |
size | bigint | File size in bytes |
is_client_visible | boolean | Visibility flag |
download_count | integer | Download counter |
created_at | timestamp | Upload date |
updated_at | timestamp | Last update |
File Storage
Files are stored using Laravel's Storage facade:
// Storage configuration (config/filesystems.php)
'disks' => [
'project_files' => [
'driver' => 'local',
'root' => storage_path('app/project_files'),
'visibility' => 'private',
],
],
Upload Handling
// In controller
public function store(Request $request, Project $project)
{
$request->validate([
'file' => 'required|file|max:102400', // 100MB max
]);
$file = $request->file('file');
$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
$path = $file->storeAs(
"projects/{$project->id}",
$filename,
'project_files'
);
return ProjectFile::create([
'project_id' => $project->id,
'uploaded_by' => auth()->id(),
'filename' => $filename,
'original_filename' => $file->getClientOriginalName(),
'path' => $path,
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'is_client_visible' => $request->boolean('is_client_visible', true),
]);
}
Download Handling
public function download(Project $project, ProjectFile $file)
{
$this->authorize('download', $file);
$file->increment('download_count');
return Storage::disk('project_files')->download(
$file->path,
$file->original_filename
);
}
File Size Limits
| Setting | Default | Location |
|---|---|---|
| Max upload size | 100 MB | Validation rules |
| PHP upload_max_filesize | 128M | php.ini |
| PHP post_max_size | 128M | php.ini |
| Nginx client_max_body_size | 128M | nginx.conf |
Scopes and Query Helpers
// Client-visible files only
ProjectFile::clientVisible()->get();
// Files by uploader
ProjectFile::uploadedBy($user)->get();
// Files for project
$project->files()->clientVisible()->get();
// With uploader info
ProjectFile::with('uploader')->get();
// By file type
ProjectFile::whereIn('mime_type', ['image/png', 'image/jpeg'])->get();
Related Features
Dependencies
| Feature | Relationship |
|---|---|
| Project Management | Files belong to projects |
| Authorization | ProjectFilePolicy controls access |
| Authentication | Tracks uploader |
Complementary Features
| Feature | Description |
|---|---|
| Enhanced File Management | Advanced file features |
| Activity Logging | Logs file operations |
| Notifications | File upload notifications |
Best Practices
For Administrators
- Use descriptive filenames for easy identification
- Set correct visibility before sharing
- Organize files by project for clarity
- Delete outdated files to save storage
- Use Admin Only for draft documents
For Developers
- Always authorize downloads using policies
- Store files outside webroot for security
- Use unique filenames to prevent conflicts
- Validate file types for security
- Stream large files to manage memory
Troubleshooting
| Issue | Solution |
|---|---|
| Upload fails | Check file size limits in PHP/Nginx |
| Can't download | Verify file exists and permissions |
| Client can't see file | Check is_client_visible flag |
| Wrong filename | Check original_filename storage |
| Slow upload | Increase timeout settings |
File Not Found
# Check file exists
php artisan tinker
Storage::disk('project_files')->exists($path)
Permission Issues
# Fix storage permissions
chmod -R 775 storage/app/project_files
chown -R www-data:www-data storage/app/project_files
Chunked Uploads
For large files, the system supports chunked uploads via API.
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/uploads/initiate | POST | Start a chunked upload |
/api/uploads/{id}/chunk | POST | Upload a chunk |
/api/uploads/{id}/complete | POST | Complete the upload |
/api/uploads/{id}/status | GET | Get upload status |
/api/uploads/{id} | DELETE | Cancel upload |
Usage Example
// Initiate upload
const initResponse = await fetch('/api/uploads/initiate', {
method: 'POST',
body: JSON.stringify({
filename: 'large-file.zip',
filesize: file.size,
chunk_size: 5 * 1024 * 1024 // 5MB
})
});
const { upload_id, chunks_expected } = await initResponse.json();
// Upload chunks
for (let i = 0; i < chunks_expected; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
await fetch(`/api/uploads/${upload_id}/chunk`, {
method: 'POST',
body: formData // with chunk and chunk_number
});
}
// Complete upload
await fetch(`/api/uploads/${upload_id}/complete`, { method: 'POST' });
Share Links
Create shareable links for files with optional password protection and expiration.
Creating Share Links
use App\Models\FileShareLink;
$link = FileShareLink::create([
'project_file_id' => $file->id,
'created_by' => auth()->id(),
'name' => 'Client Review Link',
'password' => 'optional-secret', // Will be hashed
'expires_at' => now()->addWeek(),
'download_limit' => 5,
'allow_preview' => true,
]);
$publicUrl = $link->getUrl(); // route('share.show', $token)
Share Link Options
| Option | Type | Description |
|---|---|---|
password | string | Optional password protection |
expires_at | datetime | Expiration date/time |
download_limit | int | Maximum downloads allowed |
allow_preview | bool | Allow file preview |
notify_on_access | bool | Notify creator on download |
Public Access Routes
| Route | Description |
|---|---|
/share/{token} | View shared file page |
/share/{token}/download | Download file |
/share/{token}/preview | Preview file content |
See Also
- Enhanced File Management - Advanced features
- Project Management - Project organization
- Activity Logging - Audit trail