fileStorage = $fileStorage; $this->middleware(['auth:sanctum', 'checkDiseaseAccess'])->only(['previewFile']); // $this->middleware('signed')->only(['downloadRecord']); // Add signed middleware for downloads } public function previewFile(Request $request, string $path) { try { $path = $this->sanitizePath($path); $storagePath = storage_path('app/public/' . $path); if (!file_exists($storagePath)) { Log::error('File not found', ['path' => $path]); abort(404, 'File not found'); } // Determine MIME type $mimeType = mime_content_type($storagePath); // Only allow audio files for preview (extendable later) if (!str_starts_with($mimeType, 'audio/')) { abort(415, 'Unsupported media type for inline preview'); } // Stream audio file return response()->file($storagePath, [ 'Content-Type' => $mimeType, 'Content-Disposition' => 'inline', ]); } catch (\Exception $e) { Log::error('File preview failed', [ 'error' => $e->getMessage(), 'path' => $path, 'user_id' => $request->user()->id ?? null, ]); abort(500, 'Failed to preview file'); } } public function downloadRecord(Request $request, string $path) { try { if (!$request->hasValidSignature()) { abort(401, 'Invalid signature'); } // Clean up the path to match storage structure $path = $this->sanitizePath($path); // Determine the full storage path $storagePath = storage_path('app/public/' . $path); if (!file_exists($storagePath)) { Log::error('File not found', [ 'path' => $path, 'full_path' => $storagePath ]); abort(404, 'File not found'); } // Find record containing this file path $record = $this->findRecordByPath($path); if (!$record) { abort(404, 'Associated record not found'); } // Log download attempt Log::info('File download initiated via signed URL', [ 'path' => $path, 'disease_id' => $record->disease_id, 'record_id' => $record->id ]); return $this->fileStorage->streamFile('public/' . $path); } catch (\Exception $e) { Log::error('File download failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'path' => $path ?? 'undefined' ]); abort(500, 'Failed to download file: ' . $e->getMessage()); } } private function sanitizePath(string $path): string { // Remove any directory traversal attempts and extra slashes $path = str_replace(['..', '//'], ['', '/'], $path); // Remove /api/files/records if present at the start $path = preg_replace('/^\/?(api\/files\/records\/)?/', '', $path); return trim($path, '/'); } private function findRecordByPath(string $path): ?DiseaseRecord { // First find all diseases that have file fields $diseasesWithFiles = Disease::whereRaw("jsonb_path_exists(schema->'columns', '$.** ? (@.type == \"file\")')") ->get(); if ($diseasesWithFiles->isEmpty()) { return null; } // Get all file field names from the schemas $fileFields = []; foreach ($diseasesWithFiles as $disease) { $columns = $disease->schema['columns'] ?? []; foreach ($columns as $column) { if (($column['type'] ?? '') === 'file') { $fileFields[$disease->id][] = [ 'name' => $column['name'], 'multiple' => $column['multiple'] ?? false ]; } } } // Build query based on discovered file fields return DiseaseRecord::where(function ($query) use ($path, $fileFields) { foreach ($fileFields as $diseaseId => $fields) { foreach ($fields as $field) { if ($field['multiple']) { // Handle array fields using ? operator $query->orWhere(function ($q) use ($path, $diseaseId, $field) { $q->where('disease_id', $diseaseId) ->whereRaw("data->? ?? ?", [$field['name'], $path]); }); } else { // Handle single file fields $query->orWhere(function ($q) use ($path, $diseaseId, $field) { $q->where('disease_id', $diseaseId) ->whereRaw("data->>? = ?", [$field['name'], $path]); }); } } } })->first(); } }