162 lines
5.6 KiB
PHP
162 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Services\FileStorageService;
|
|
use Illuminate\Http\Request;
|
|
use App\Models\DiseaseRecord;
|
|
use App\Models\Disease;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class FileController extends Controller
|
|
{
|
|
protected $fileStorage;
|
|
|
|
public function __construct(FileStorageService $fileStorage)
|
|
{
|
|
$this->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();
|
|
}
|
|
} |