113 lines
3.6 KiB
PHP
113 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Files;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
class FileServeService
|
|
{
|
|
public function meta(string $baseDir, string $name, array $allowedExtensions, ?string $downloadName = null): array
|
|
{
|
|
return $this->resolveFile($baseDir, $name, $allowedExtensions, $downloadName);
|
|
}
|
|
|
|
public function serveInline(
|
|
string $baseDir,
|
|
string $name,
|
|
array $allowedExtensions,
|
|
Request $request,
|
|
?string $downloadName = null,
|
|
bool $nosniff = false
|
|
): Response {
|
|
$meta = $this->resolveFile($baseDir, $name, $allowedExtensions, $downloadName);
|
|
|
|
$ifNoneMatch = trim((string) $request->headers->get('If-None-Match', ''), '"');
|
|
$ifModifiedSince = (string) $request->headers->get('If-Modified-Since', '');
|
|
$imsTime = $ifModifiedSince !== '' ? strtotime($ifModifiedSince) : false;
|
|
|
|
if (($ifNoneMatch !== '' && $ifNoneMatch === $meta['etag']) ||
|
|
($imsTime !== false && $imsTime >= $meta['mtime'])
|
|
) {
|
|
return response('', 304, $this->notModifiedHeaders($meta));
|
|
}
|
|
|
|
$headers = [
|
|
'Content-Type' => $meta['mime'],
|
|
'Content-Disposition' => 'inline; filename="' . $meta['download_name'] . '"',
|
|
'Content-Length' => (string) $meta['size'],
|
|
'ETag' => $meta['etag'],
|
|
'Last-Modified' => $meta['last_modified'],
|
|
'Cache-Control' => 'public, max-age=86400',
|
|
];
|
|
|
|
if ($nosniff) {
|
|
$headers['X-Content-Type-Options'] = 'nosniff';
|
|
}
|
|
|
|
return response(file_get_contents($meta['path']), 200, $headers);
|
|
}
|
|
|
|
private function resolveFile(string $baseDir, string $name, array $allowedExtensions, ?string $downloadName): array
|
|
{
|
|
if ($name !== basename($name)) {
|
|
throw new HttpException(400, 'Invalid filename');
|
|
}
|
|
|
|
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
if (!in_array($ext, $allowedExtensions, true)) {
|
|
throw new HttpException(404, 'File not found');
|
|
}
|
|
|
|
$path = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
|
|
if (!is_file($path)) {
|
|
throw new HttpException(404, 'File not found');
|
|
}
|
|
|
|
$mime = $this->detectMime($path);
|
|
$mtime = filemtime($path) ?: time();
|
|
$size = filesize($path) ?: 0;
|
|
$etag = md5($name . '|' . $mtime . '|' . $size);
|
|
$downloadName = $downloadName ? $downloadName . '.' . $ext : $name;
|
|
|
|
return [
|
|
'name' => $name,
|
|
'path' => $path,
|
|
'mime' => $mime,
|
|
'mtime' => $mtime,
|
|
'size' => $size,
|
|
'etag' => $etag,
|
|
'download_name' => $downloadName,
|
|
'last_modified' => gmdate('D, d M Y H:i:s', $mtime) . ' GMT',
|
|
];
|
|
}
|
|
|
|
private function detectMime(string $path): string
|
|
{
|
|
$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;
|
|
}
|
|
|
|
return $mime;
|
|
}
|
|
|
|
private function notModifiedHeaders(array $meta): array
|
|
{
|
|
return [
|
|
'ETag' => $meta['etag'],
|
|
'Last-Modified' => $meta['last_modified'],
|
|
];
|
|
}
|
|
}
|