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) { Log::info('uploading'); try { $uploadedFiles = []; $urls = []; foreach ($request->file('f') 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 * returns json or htmx, depending on value of request->input('response_type') */ 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) { Log::info('uploading chunked'); $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; } }