summaryrefslogtreecommitdiff
path: root/app/Http/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'app/Http/Controllers')
-rw-r--r--app/Http/Controllers/Controller.php8
-rw-r--r--app/Http/Controllers/FileController.php447
2 files changed, 455 insertions, 0 deletions
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
new file mode 100644
index 0000000..8677cd5
--- /dev/null
+++ b/app/Http/Controllers/Controller.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace App\Http\Controllers;
+
+abstract class Controller
+{
+ //
+}
diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php
new file mode 100644
index 0000000..5d0f09a
--- /dev/null
+++ b/app/Http/Controllers/FileController.php
@@ -0,0 +1,447 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Exceptions\FileUploadException;
+use App\Http\Requests\FileUploadRequest;
+use App\Models\File;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+
+class FileController extends Controller
+{
+ /**
+ * Show the upload form
+ */
+ public function showUploadForm()
+ {
+ $config = config('fileupload');
+
+ return view('files.upload', [
+ 'maxSizeMB' => round($config['max_file_size'] / 1024, 2),
+ 'maxFiles' => $config['max_files_per_upload'],
+ 'allowedExtensions' => $config['allowed_extensions'],
+ ]);
+ }
+
+ /**
+ * Handle standard file upload (non-JS fallback)
+ */
+ public function upload(FileUploadRequest $request)
+ {
+ try {
+ $uploadedFiles = [];
+ $urls = [];
+
+ foreach ($request->file('files') as $uploadedFile) {
+ $file = $this->storeFile($uploadedFile, $request);
+ $uploadedFiles[] = $file;
+ $urls[] = $file->download_url;
+ }
+
+ Log::info('File upload successful', [
+ 'ip' => $request->ip(),
+ 'count' => count($uploadedFiles),
+ 'total_size' => array_sum(array_map(fn($f) => $f->size, $uploadedFiles)),
+ ]);
+
+ // For AJAX requests
+ if ($request->expectsJson()) {
+ return response()->json([
+ 'success' => true,
+ 'message' => count($uploadedFiles) . ' file(s) uploaded successfully',
+ 'urls' => $urls,
+ 'files' => $uploadedFiles,
+ ]);
+ }
+
+ // For standard form submission
+ return redirect()->route('files.upload.form')
+ ->with('success', 'Files uploaded successfully!')
+ ->with('urls', $urls);
+
+ } catch (FileUploadException $e) {
+ Log::warning('File upload validation failed', [
+ 'ip' => $request->ip(),
+ 'error' => $e->getMessage(),
+ ]);
+
+ if ($request->expectsJson()) {
+ return response()->json([
+ 'success' => false,
+ 'error' => $e->getMessage(),
+ ], 422);
+ }
+
+ return redirect()->back()
+ ->withErrors(['upload' => $e->getMessage()])
+ ->withInput();
+
+ } catch (\Exception $e) {
+ Log::error('File upload failed', [
+ 'ip' => $request->ip(),
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]);
+
+ if ($request->expectsJson()) {
+ return response()->json([
+ 'success' => false,
+ 'error' => 'Upload failed. Please try again.',
+ ], 500);
+ }
+
+ return redirect()->back()
+ ->withErrors(['upload' => 'Upload failed. Please try again.'])
+ ->withInput();
+ }
+ }
+
+ /**
+ * Handle individual chunk upload
+ */
+ public function uploadChunk(Request $request)
+ {
+ $request->validate([
+ 'chunk' => 'required|file',
+ 'upload_id' => 'required|string',
+ 'chunk_number' => 'required|integer|min:0',
+ 'total_chunks' => 'required|integer|min:1',
+ 'original_filename' => 'required|string',
+ 'mime_type' => 'nullable|string',
+ ]);
+
+ try {
+ $uploadId = $request->input('upload_id');
+ $chunkNumber = $request->input('chunk_number');
+ $totalChunks = $request->input('total_chunks');
+
+ // Store chunk temporarily
+ $chunkPath = "chunks/{$uploadId}";
+ $chunkFilename = "chunk_{$chunkNumber}";
+
+ Storage::put(
+ "{$chunkPath}/{$chunkFilename}",
+ file_get_contents($request->file('chunk')->getRealPath())
+ );
+
+ // Track chunk in database
+ DB::table('upload_chunks')->insert([
+ 'upload_id' => $uploadId,
+ 'session_id' => $request->session()->getId(),
+ 'ip_address' => $request->ip(),
+ 'chunk_number' => $chunkNumber,
+ 'total_chunks' => $totalChunks,
+ 'chunk_filename' => $chunkFilename,
+ 'chunk_size' => $request->file('chunk')->getSize(),
+ 'filename_og' => $request->input('original_filename'),
+ 'mime_type' => $request->input('mime_type'),
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ Log::info('Chunk uploaded', [
+ 'upload_id' => $uploadId,
+ 'chunk' => $chunkNumber + 1,
+ 'total' => $totalChunks,
+ ]);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => "Chunk {$chunkNumber} uploaded successfully",
+ ]);
+
+ } catch (\Exception $e) {
+ Log::error('Chunk upload failed', [
+ 'upload_id' => $request->input('upload_id'),
+ 'chunk_number' => $request->input('chunk_number'),
+ 'error' => $e->getMessage(),
+ ]);
+
+ return response()->json([
+ 'success' => false,
+ 'error' => 'Chunk upload failed',
+ ], 500);
+ }
+ }
+
+ /**
+ * Complete chunked upload by merging chunks
+ */
+ public function completeChunkedUpload(Request $request)
+ {
+ $request->validate([
+ 'upload_id' => 'required|string',
+ 'original_filename' => 'required|string',
+ ]);
+
+ try {
+ $uploadId = $request->input('upload_id');
+
+ // Get all chunks for this upload
+ $chunks = DB::table('upload_chunks')
+ ->where('upload_id', $uploadId)
+ ->orderBy('chunk_number')
+ ->get();
+
+ if ($chunks->isEmpty()) {
+ throw FileUploadException::chunkMismatch($uploadId);
+ }
+
+ $totalChunks = $chunks->first()->total_chunks;
+
+ if ($chunks->count() !== $totalChunks) {
+ throw FileUploadException::chunksIncomplete($uploadId, $totalChunks, $chunks->count());
+ }
+
+ // Merge chunks
+ $mergedContent = '';
+ $chunkPath = "chunks/{$uploadId}";
+
+ foreach ($chunks as $chunk) {
+ $chunkFile = "{$chunkPath}/{$chunk->chunk_filename}";
+ if (!Storage::exists($chunkFile)) {
+ throw FileUploadException::chunkMismatch($uploadId);
+ }
+ $mergedContent .= Storage::get($chunkFile);
+ }
+
+ // Create temporary file to validate
+ $tempPath = storage_path("app/temp_{$uploadId}");
+ file_put_contents($tempPath, $mergedContent);
+
+ // Validate merged file
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $mimeType = finfo_file($finfo, $tempPath);
+ finfo_close($finfo);
+
+ $config = config('fileupload');
+ if (!in_array($mimeType, $config['allowed_mimes'])) {
+ unlink($tempPath);
+ throw FileUploadException::invalidMimeType($mimeType);
+ }
+
+ // Store the merged file
+ $originalFilename = $request->input('original_filename');
+ $storagePath = $this->generateStoragePath();
+ $uniqueFilename = $this->generateUniqueFilename($originalFilename, $storagePath);
+ $fullPath = "{$storagePath}/{$uniqueFilename}";
+
+ Storage::disk($config['disk'])->put($fullPath, $mergedContent);
+
+ // Create File record
+ $file = File::create([
+ 'filename_og' => $originalFilename,
+ 'filename_stored' => $uniqueFilename,
+ 'storage_path' => $fullPath,
+ 'disk' => $config['disk'],
+ 'mime_type' => $mimeType,
+ 'extension' => pathinfo($originalFilename, PATHINFO_EXTENSION),
+ 'size' => strlen($mergedContent),
+ 'hash' => hash('sha256', $mergedContent),
+ 'ip_address' => $request->ip(),
+ 'user_agent' => $request->userAgent(),
+ 'is_chunked' => true,
+ ]);
+
+ $file->setExpiration();
+
+ // Log upload activity
+ $file->activityLogs()->create([
+ 'event_type' => 'upload',
+ 'ip_address' => $request->ip(),
+ 'user_agent' => $request->userAgent(),
+ 'data' => [
+ 'is_chunked' => true,
+ 'total_chunks' => $totalChunks,
+ ],
+ ]);
+
+ // Cleanup
+ unlink($tempPath);
+ Storage::deleteDirectory($chunkPath);
+ DB::table('upload_chunks')->where('upload_id', $uploadId)->delete();
+
+ Log::info('Chunked upload completed', [
+ 'upload_id' => $uploadId,
+ 'file_id' => $file->id,
+ 'size' => $file->size,
+ ]);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'File uploaded successfully',
+ 'download_url' => $file->download_url,
+ 'file' => $file,
+ ]);
+
+ } catch (FileUploadException $e) {
+ Log::warning('Chunked upload completion failed', [
+ 'upload_id' => $request->input('upload_id'),
+ 'error' => $e->getMessage(),
+ ]);
+
+ return response()->json([
+ 'success' => false,
+ 'error' => $e->getMessage(),
+ ], 422);
+
+ } catch (\Exception $e) {
+ Log::error('Chunked upload completion failed', [
+ 'upload_id' => $request->input('upload_id'),
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]);
+
+ return response()->json([
+ 'success' => false,
+ 'error' => 'Upload completion failed',
+ ], 500);
+ }
+ }
+
+ /**
+ * Browse uploaded files
+ */
+ public function browse(Request $request)
+ {
+ $query = File::notExpired()->latest();
+
+ // Search functionality
+ if ($search = $request->input('search')) {
+ $query->where('filename_og', 'like', "%{$search}%");
+ }
+
+ $files = $query->paginate(20);
+
+ return view('files.browse', compact('files'));
+ }
+
+ /**
+ * Gallery view for images and videos
+ */
+ public function gallery(Request $request)
+ {
+ // Filter for images and videos only
+ $query = File::notExpired()
+ ->where(function($q) {
+ $q->where('mime_type', 'like', 'image/%')
+ ->orWhere('mime_type', 'like', 'video/%');
+ })
+ ->latest();
+
+ $files = $query->paginate(30);
+
+ return view('files.gallery', compact('files'));
+ }
+
+ /**
+ * Download/view a file
+ */
+ public function download(File $file)
+ {
+ if ($file->isExpired()) {
+ abort(404, 'File not found or has expired');
+ }
+
+ // Log the download
+ $file->logDownload(request()->ip(), request()->userAgent());
+
+ // Get the full file path
+ $filePath = Storage::disk($file->disk)->path($file->storage_path);
+
+ // Serve the file inline (browser will render if possible)
+ return response()->file($filePath, [
+ 'Content-Type' => $file->mime_type,
+ 'Content-Disposition' => 'inline; filename="' . $file->filename_og . '"'
+ ]);
+ }
+
+ /**
+ * Store a single uploaded file
+ */
+ protected function storeFile($uploadedFile, Request $request)
+ {
+ $config = config('fileupload');
+
+ // Generate storage path and filename
+ $storagePath = $this->generateStoragePath();
+ $uniqueFilename = $this->generateUniqueFilename(
+ $uploadedFile->getClientOriginalName(),
+ $storagePath
+ );
+ $fullPath = "{$storagePath}/{$uniqueFilename}";
+
+ // Store the file
+ $uploadedFile->storeAs(
+ $storagePath,
+ $uniqueFilename,
+ $config['disk']
+ );
+
+ // Generate file hash
+ $hash = hash_file('sha256', $uploadedFile->getRealPath());
+
+ // Create File record
+ $file = File::create([
+ 'filename_og' => $uploadedFile->getClientOriginalName(),
+ 'filename_stored' => $uniqueFilename,
+ 'storage_path' => $fullPath,
+ 'disk' => $config['disk'],
+ 'mime_type' => $uploadedFile->getMimeType(),
+ 'extension' => $uploadedFile->getClientOriginalExtension(),
+ 'size' => $uploadedFile->getSize(),
+ 'hash' => $hash,
+ 'ip_address' => $request->ip(),
+ 'user_agent' => $request->userAgent(),
+ 'is_chunked' => false,
+ ]);
+
+ $file->setExpiration();
+
+ // Log upload activity
+ $file->activityLogs()->create([
+ 'event_type' => 'upload',
+ 'ip_address' => $request->ip(),
+ 'user_agent' => $request->userAgent(),
+ ]);
+
+ return $file;
+ }
+
+ /**
+ * Generate date-based storage path
+ */
+ protected function generateStoragePath()
+ {
+ $basePath = config('fileupload.storage_path', 'uploads');
+ $datePath = now()->format('Y/m/d');
+
+ return "{$basePath}/{$datePath}";
+ }
+
+ /**
+ * Generate unique filename, appending epoch on collision
+ */
+ protected function generateUniqueFilename($originalFilename, $storagePath)
+ {
+ $pathInfo = pathinfo($originalFilename);
+ $baseName = Str::slug($pathInfo['filename'] ?: 'file');
+ $extension = $pathInfo['extension'] ?? '';
+
+ $filename = $baseName . ($extension ? ".{$extension}" : '');
+ $fullPath = "{$storagePath}/{$filename}";
+
+ // Check for collision
+ $disk = config('fileupload.disk');
+ if (Storage::disk($disk)->exists($fullPath)) {
+ // Append epoch seconds
+ $filename = $baseName . '_' . time() . ($extension ? ".{$extension}" : '');
+ }
+
+ return $filename;
+ }
+}