initial commit
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user