fix: 多线程下载文件损坏的问题

This commit is contained in:
kuaifan 2025-01-15 15:27:39 +08:00
parent 34ffd96c86
commit a07913181a
4 changed files with 79 additions and 49 deletions

View File

@ -1702,7 +1702,7 @@ class DialogController extends AbstractController
} }
// //
$filePath = public_path($array['path']); $filePath = public_path($array['path']);
return Base::BinaryFileResponse($filePath, $array['name']); return Base::DownloadFileResponse($filePath, $array['name']);
} }
/** /**

View File

@ -1853,7 +1853,7 @@ class ProjectController extends AbstractController
} }
// //
$filePath = public_path($file->getRawOriginal('path')); $filePath = public_path($file->getRawOriginal('path'));
return Base::BinaryFileResponse($filePath, $file->name); return Base::DownloadFileResponse($filePath, $file->name);
} }
/** /**

View File

@ -6,6 +6,7 @@ namespace App\Models;
use App\Module\Base; use App\Module\Base;
use App\Module\Timer; use App\Module\Timer;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpFoundation\StreamedResponse;
/** /**
* App\Models\FileContent * App\Models\FileContent
@ -104,10 +105,10 @@ class FileContent extends AbstractModel
/** /**
* 获取格式内容(或下载) * 获取格式内容(或下载)
* @param File $file * @param $file
* @param $content * @param $content
* @param $download * @param $download
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse * @return array|StreamedResponse
*/ */
public static function formatContent($file, $content, $download = false) public static function formatContent($file, $content, $download = false)
{ {
@ -119,7 +120,7 @@ class FileContent extends AbstractModel
} else { } else {
$filePath = public_path($content['url']); $filePath = public_path($content['url']);
} }
return Base::BinaryFileResponse($filePath, $name); return Base::DownloadFileResponse($filePath, $name);
} }
if (empty($content)) { if (empty($content)) {
$content = match ($file->type) { $content = match ($file->type) {
@ -148,7 +149,7 @@ class FileContent extends AbstractModel
if ($download) { if ($download) {
$filePath = public_path($path); $filePath = public_path($path);
if (isset($filePath)) { if (isset($filePath)) {
return Base::BinaryFileResponse($filePath, $name); return Base::DownloadFileResponse($filePath, $name);
} else { } else {
abort(403, "This file not support download."); abort(403, "This file not support download.");
} }

View File

@ -14,7 +14,7 @@ use Overtrue\Pinyin\Pinyin;
use Redirect; use Redirect;
use Request; use Request;
use Storage; use Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
use Validator; use Validator;
@ -2759,12 +2759,12 @@ class Base
} }
/** /**
* BinaryFileResponse 下载文件 * DownloadFileResponse 下载文件
* @param File|\SplFileInfo|string $file 文件对象或路径 * @param File|\SplFileInfo|string $file 文件对象或路径
* @param string|null $name 下载文件名 * @param string|null $name 下载文件名
* @return BinaryFileResponse * @return StreamedResponse
*/ */
public static function BinaryFileResponse($file, $name = null) public static function DownloadFileResponse($file, $name = null)
{ {
try { try {
// 处理文件对象 // 处理文件对象
@ -2781,6 +2781,12 @@ class Base
throw new FileException('File must be readable and exist.'); throw new FileException('File must be readable and exist.');
} }
// 获取文件信息
$size = $file->getSize();
if ($size === false || $size < 0) {
throw new FileException('Unable to determine file size.');
}
// 处理文件名 // 处理文件名
if (empty($name)) { if (empty($name)) {
$name = basename($file->getPathname()); $name = basename($file->getPathname());
@ -2792,31 +2798,28 @@ class Base
$name = Base::cutStr($name, 180); $name = Base::cutStr($name, 180);
$name = str_replace(['"', '<', '>', '|', '/', '\\', '?', ':'], '', $name); $name = str_replace(['"', '<', '>', '|', '/', '\\', '?', ':'], '', $name);
// IE 浏览器特殊处理 // 获取MIME类型
$encodedName = $name; $mimeType = $file->getMimeType();
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/MSIE|Internet Explorer|Trident/i", $_SERVER['HTTP_USER_AGENT'])) { if (empty($mimeType)) {
$encodedName = rawurlencode($name); $mimeType = 'application/octet-stream';
} }
// 获取文件信息 // 处理 Range 请求
$size = $file->getSize();
$start = 0; $start = 0;
$end = $size - 1; $end = $size - 1;
$statusCode = 200; $length = $size;
$headers = []; $isRangeRequest = false;
// 处理断点续传请求
if (isset($_SERVER['HTTP_RANGE'])) { if (isset($_SERVER['HTTP_RANGE'])) {
$ranges = explode('=', $_SERVER['HTTP_USER_AGENT']); $range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
if (count($ranges) == 2 && str_contains($ranges[0], 'bytes')) { if (preg_match('/^(\d+)-(\d*)$/', $range, $matches)) {
$positions = explode('-', $ranges[1]); $start = intval($matches[1]);
$start = isset($positions[0]) && is_numeric($positions[0]) ? intval($positions[0]) : 0; $end = !empty($matches[2]) ? intval($matches[2]) : $size - 1;
$end = isset($positions[1]) && is_numeric($positions[1]) ? intval($positions[1]) : $size - 1;
// 验证范围的有效性 // 验证范围的有效性
if ($start >= 0 && $end < $size && $start <= $end) { if ($start >= 0 && $end < $size && $start <= $end) {
$statusCode = 206; $length = $end - $start + 1;
$headers['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $size); $isRangeRequest = true;
} else { } else {
$start = 0; $start = 0;
$end = $size - 1; $end = $size - 1;
@ -2824,43 +2827,69 @@ class Base
} }
} }
// 计算内容长度 // 设置基本响应头
$contentLength = $end - $start + 1; $headers = [
'Content-Type' => $mimeType,
// 设置响应头
$headers = array_merge($headers, [
'Content-Type' => $file->getMimeType() ?: 'application/octet-stream',
'Content-Disposition' => sprintf( 'Content-Disposition' => sprintf(
'attachment; filename="%s"; filename*=UTF-8\'\'%s', 'attachment; filename="%s"; filename*=UTF-8\'\'%s',
$encodedName, $name,
rawurlencode($name) rawurlencode($name)
), ),
'Content-Length' => $contentLength,
'Accept-Ranges' => 'bytes', 'Accept-Ranges' => 'bytes',
'Cache-Control' => 'private, no-transform, no-store, must-revalidate, max-age=0', 'Cache-Control' => 'private, no-transform, no-store, must-revalidate, max-age=0',
'Pragma' => 'public', 'Content-Length' => $length,
'Expires' => '0', 'Last-Modified' => gmdate('D, d M Y H:i:s', $file->getMTime()) . ' GMT',
'X-Content-Type-Options' => 'nosniff', 'ETag' => sprintf('"%s"', md5_file($file->getPathname()))
'ETag' => sprintf('"%s"', md5_file($file->getPathname())), ];
'Last-Modified' => gmdate('D, d M Y H:i:s', $file->getMTime()) . ' GMT'
]);
// 创建响应对象 if ($isRangeRequest) {
$response = new BinaryFileResponse($file->getPathname(), $statusCode, $headers); $headers['Content-Range'] = "bytes {$start}-{$end}/{$size}";
$statusCode = 206;
// 禁用输出缓冲 } else {
if (ob_get_level()) { $statusCode = 200;
ob_end_clean();
} }
return $response; // 创建流式响应
return new StreamedResponse(
function () use ($file, $start, $length) {
$handle = fopen($file->getPathname(), 'rb');
if ($handle === false) {
throw new FileException('Cannot open file for reading');
}
if (fseek($handle, $start) === -1) {
fclose($handle);
throw new FileException('Cannot seek to position ' . $start);
}
$remaining = $length;
$bufferSize = 8192; // 8KB chunks
while ($remaining > 0 && !feof($handle)) {
$readSize = min($bufferSize, $remaining);
$buffer = fread($handle, $readSize);
if ($buffer === false) {
break;
}
echo $buffer;
flush();
$remaining -= strlen($buffer);
}
fclose($handle);
},
$statusCode,
$headers
);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error('File download failed', [ \Log::error('File download failed', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'file' => $file->getPathname() ?? null, 'file' => $file->getPathname() ?? null,
'name' => $name ?? null, 'name' => $name ?? null,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, // 添加更多调试信息 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
'ip' => request()->ip() 'ip' => request()->ip(),
'range' => $_SERVER['HTTP_RANGE'] ?? null
]); ]);
abort(403, 'File download failed'); abort(403, 'File download failed');
} }