add projet
This commit is contained in:
Executable
+184
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Response as HttpResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class FilesController extends BaseApiController
|
||||
{
|
||||
protected array $allowedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'];
|
||||
|
||||
public function receipt(string $name)
|
||||
{
|
||||
// 1) Path traversal guard
|
||||
if ($name !== basename($name)) {
|
||||
return $this->respondError('Invalid filename', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 2) Allow-list extensions (optional but safer)
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $this->allowedExtensions, true)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
// 3) Build path under storage/app/uploads/receipts
|
||||
$path = $this->storagePath("uploads/receipts/{$name}");
|
||||
|
||||
if (!is_file($path)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $this->streamFile($path, $name);
|
||||
}
|
||||
|
||||
public function reimb(string $name)
|
||||
{
|
||||
// 1) Path traversal guard
|
||||
if ($name !== basename($name)) {
|
||||
return $this->respondError('Invalid filename', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 2) Allow-list extensions
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $this->allowedExtensions, true)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
// 3) Build path under storage/app/uploads/reimbursements
|
||||
$path = $this->storagePath("uploads/reimbursements/{$name}");
|
||||
|
||||
if (!is_file($path)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $this->streamFile($path, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for reimb() for backward compatibility
|
||||
*/
|
||||
public function reimbursement(string $name)
|
||||
{
|
||||
return $this->reimb($name);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$type = $this->request->getGet('type') ?? 'receipts';
|
||||
$allowedTypes = ['receipts', 'reimbursements', 'checks'];
|
||||
|
||||
if (!in_array($type, $allowedTypes, true)) {
|
||||
return $this->respondError('Invalid file type', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$path = $this->storagePath("uploads/{$type}/");
|
||||
$files = [];
|
||||
|
||||
if (is_dir($path)) {
|
||||
foreach (scandir($path) as $file) {
|
||||
if ($file === '.' || $file === '..') {
|
||||
continue;
|
||||
}
|
||||
$filePath = $path . $file;
|
||||
if (!is_file($filePath)) {
|
||||
continue;
|
||||
}
|
||||
$files[] = [
|
||||
'name' => $file,
|
||||
'size' => filesize($filePath),
|
||||
'modified' => date('Y-m-d H:i:s', filemtime($filePath)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'type' => $type,
|
||||
'files' => $files,
|
||||
'count' => count($files),
|
||||
], 'Files retrieved successfully');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Files list error: ' . $e->getMessage());
|
||||
return $this->respondError('Failed to retrieve files', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
protected function streamFile(string $path, string $name): HttpResponse
|
||||
{
|
||||
// 4) MIME detection (finfo is more reliable than mime_content_type)
|
||||
$mime = 'application/octet-stream';
|
||||
if (function_exists('finfo_open')) {
|
||||
$fi = finfo_open(FILEINFO_MIME_TYPE);
|
||||
if ($fi) {
|
||||
$detected = finfo_file($fi, $path);
|
||||
if ($detected) {
|
||||
$mime = $detected;
|
||||
}
|
||||
finfo_close($fi);
|
||||
}
|
||||
} elseif (function_exists('mime_content_type')) {
|
||||
$mime = mime_content_type($path) ?: $mime;
|
||||
}
|
||||
|
||||
// 5) Caching (ETag + Last-Modified) and 304 support
|
||||
$mtime = filemtime($path) ?: time();
|
||||
$size = filesize($path) ?: 0;
|
||||
$etag = md5($name . '|' . $mtime . '|' . $size);
|
||||
|
||||
$ifNoneMatch = trim($this->laravelRequest->header('If-None-Match', ''), '"');
|
||||
$ifModifiedSince = $this->laravelRequest->header('If-Modified-Since');
|
||||
$imsTime = $ifModifiedSince ? strtotime($ifModifiedSince) : false;
|
||||
|
||||
if (($ifNoneMatch && $ifNoneMatch === $etag) ||
|
||||
($imsTime !== false && $imsTime >= $mtime)
|
||||
) {
|
||||
return response('', Response::HTTP_NOT_MODIFIED)
|
||||
->header('ETag', $etag)
|
||||
->header('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
|
||||
}
|
||||
|
||||
// 6) Stream the file inline
|
||||
return response()->make(file_get_contents($path), Response::HTTP_OK, [
|
||||
'Content-Type' => $mime,
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'Content-Disposition' => 'inline; filename="' . $name . '"',
|
||||
'Content-Length' => (string) $size,
|
||||
'ETag' => $etag,
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s', $mtime) . ' GMT',
|
||||
'Cache-Control' => 'public, max-age=86400',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function storagePath(string $relative): string
|
||||
{
|
||||
return storage_path('app/' . $relative);
|
||||
}
|
||||
|
||||
protected function isAllowedExtension(string $name): bool
|
||||
{
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
return in_array($ext, $this->allowedExtensions, true);
|
||||
}
|
||||
|
||||
protected function detectMime(string $path): string
|
||||
{
|
||||
if (function_exists('finfo_open')) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
if ($finfo) {
|
||||
$mime = finfo_file($finfo, $path);
|
||||
finfo_close($finfo);
|
||||
if ($mime) {
|
||||
return $mime;
|
||||
}
|
||||
}
|
||||
} elseif (function_exists('mime_content_type')) {
|
||||
$mime = mime_content_type($path);
|
||||
if ($mime) {
|
||||
return $mime;
|
||||
}
|
||||
}
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user