Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Files & Documents/File Sharing

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

  1. Accessing Files
  2. Features
  3. How to Use
  4. Visibility Settings
  5. Technical Architecture
  6. Related Features

Accessing Files

Access PointLocationURLRole
Project FilesProject detail page/admin/projects/{id}#filesAdmin
Upload FilesProject files sectionModal on project pageAdmin
Download FileFile rowDirect download linkBoth
Portal FilesPortal project page/portal/projects/{id}#filesClient User

Permissions

ActionAdminClient 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)

  1. Navigate to Admin → Projects → [Project]
  2. Scroll to Files section
  3. Click "Upload Files" or drag files to drop zone
  4. Select file(s) from your computer
  5. Set visibility (Client Visible or Admin Only)
  6. Click "Upload"
  7. Files appear in the file list

Uploading Files (Client)

  1. Navigate to Portal → Projects → [Project]
  2. Scroll to Files section
  3. Click "Upload Files"
  4. Select file(s) from your computer
  5. Click "Upload"
  6. Files are automatically client-visible

Downloading Files

  1. Navigate to project files section
  2. Click "Download" button next to file
  3. File downloads to your computer

Deleting Files

Admin:

  1. Navigate to Admin → Projects → [Project]
  2. Find file in list
  3. Click "Delete" button
  4. Confirm deletion

Client:

  1. Navigate to Portal → Projects → [Project]
  2. Find file you uploaded
  3. Click "Delete" button
  4. Confirm deletion

Note: Clients can only delete files they uploaded.

Changing File Visibility

  1. Navigate to Admin → Projects → [Project]
  2. Find file in list
  3. Click visibility toggle
  4. 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

MethodRouteDescription
store()POST /admin/projects/{project}/filesUpload file
show()GET /admin/files/{file}View file details
download()GET /admin/files/{file}/downloadDownload
destroy()DELETE /admin/files/{file}Delete
toggleVisibility()PATCH /admin/files/{file}/visibilityToggle 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

ColumnTypeDescription
idbigintPrimary key
project_idbigintForeign key to projects
uploaded_bybigintForeign key to users
filenamestringStored filename (unique)
original_filenamestringOriginal uploaded name
pathstringStorage path
mime_typestringFile MIME type
sizebigintFile size in bytes
is_client_visiblebooleanVisibility flag
download_countintegerDownload counter
created_attimestampUpload date
updated_attimestampLast 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

SettingDefaultLocation
Max upload size100 MBValidation rules
PHP upload_max_filesize128Mphp.ini
PHP post_max_size128Mphp.ini
Nginx client_max_body_size128Mnginx.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();

Dependencies

FeatureRelationship
Project ManagementFiles belong to projects
AuthorizationProjectFilePolicy controls access
AuthenticationTracks uploader

Complementary Features

FeatureDescription
Enhanced File ManagementAdvanced file features
Activity LoggingLogs file operations
NotificationsFile upload notifications

Best Practices

For Administrators

  1. Use descriptive filenames for easy identification
  2. Set correct visibility before sharing
  3. Organize files by project for clarity
  4. Delete outdated files to save storage
  5. Use Admin Only for draft documents

For Developers

  1. Always authorize downloads using policies
  2. Store files outside webroot for security
  3. Use unique filenames to prevent conflicts
  4. Validate file types for security
  5. Stream large files to manage memory

Troubleshooting

IssueSolution
Upload failsCheck file size limits in PHP/Nginx
Can't downloadVerify file exists and permissions
Client can't see fileCheck is_client_visible flag
Wrong filenameCheck original_filename storage
Slow uploadIncrease 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

EndpointMethodDescription
/api/uploads/initiatePOSTStart a chunked upload
/api/uploads/{id}/chunkPOSTUpload a chunk
/api/uploads/{id}/completePOSTComplete the upload
/api/uploads/{id}/statusGETGet upload status
/api/uploads/{id}DELETECancel 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' });

Create shareable links for files with optional password protection and expiration.

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)
OptionTypeDescription
passwordstringOptional password protection
expires_atdatetimeExpiration date/time
download_limitintMaximum downloads allowed
allow_previewboolAllow file preview
notify_on_accessboolNotify creator on download

Public Access Routes

RouteDescription
/share/{token}View shared file page
/share/{token}/downloadDownload file
/share/{token}/previewPreview file content

See Also