如何用cURL在上传文件至服务器前检查文件扩展名
问题描述
我使用cURL从URL获取文件后上传至服务器,现在需要在上传前检查文件扩展名。但部分URL不显示文件扩展名(例如示例链接:https://images.unsplash.com/photo-1533450718592-29d45635f0a9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80),希望无需完整上传即可获取文件实际扩展名,与系统允许的列表比对,同时校验文件的实际数据格式。
系统允许的扩展名列表:
- jpeg
- docx
- xlsx
- xml
- txt
- pptx
- md
- ods
- odp
- odt
- odg
- ots
- ott
- csv
- tsv
- rtf
- resx
- html
- srt
- vtt
- stl
- sbv
- sub
- ass
- dfxp
- ttml
现有代码如下:
$url = 'https://images.unsplash.com/photo-1533450718592-29d45635f0a9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80'; function collect_file($url){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_VERBOSE, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_AUTOREFERER, false); curl_setopt($ch, CURLOPT_REFERER, "https://file-examples.com"); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($ch, CURLOPT_HEADER, 0); $result = curl_exec($ch); curl_close($ch); return($result); } function write_to_file($text,$new_filename){ $fp = fopen($new_filename, 'w'); fwrite($fp, $text); fclose($fp); } // start loop here $new_file_name = uniqid() . '.jpeg'; $temp_file_contents = collect_file($url); write_to_file($temp_file_contents,$new_file_name);
解决方案
要实现需求,需分三步执行:
- 通过HEAD请求获取Content-Type:无需下载完整文件,仅请求响应头即可拿到文件的Content-Type,初步判断类型;
- 通过文件签名校验实际格式:若Content-Type不够准确,下载文件前若干字节,通过文件固定开头的签名(如JPEG的
FF D8 FF)判断真实格式; - 映射格式到扩展名并校验:将判断出的格式映射到允许的扩展名列表,校验通过后再完整下载文件。
完整改进代码
// 系统允许的扩展名列表 $allowed_extensions = [ 'jpeg', 'docx', 'xlsx', 'xml', 'txt', 'pptx', 'pdf', 'md', 'ods', 'odp', 'odt', 'odg', 'ots', 'ott', 'csv', 'tsv', 'rtf', 'resx', 'html', 'srt', 'vtt', 'stl', 'sbv', 'sub', 'ass', 'dfxp', 'ttml' ]; // 文件类型映射:Content-Type => 扩展名 + 文件签名 $file_type_mapping = [ 'image/jpeg' => [ 'ext' => 'jpeg', 'signature' => "\xFF\xD8\xFF" ], 'application/pdf' => [ 'ext' => 'pdf', 'signature' => "%PDF-" ], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => [ 'ext' => 'docx', 'signature' => "PK\x03\x04" // Office OpenXML格式通用签名 ], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => [ 'ext' => 'xlsx', 'signature' => "PK\x03\x04" ], 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => [ 'ext' => 'pptx', 'signature' => "PK\x03\x04" ], 'text/plain' => [ 'ext' => 'txt', 'signature' => null // 文本文件无固定签名,需结合业务场景补充校验 ], 'text/xml' => [ 'ext' => 'xml', 'signature' => "<?xml" ], 'text/markdown' => [ 'ext' => 'md', 'signature' => null ], 'text/csv' => [ 'ext' => 'csv', 'signature' => null ], 'application/rtf' => [ 'ext' => 'rtf', 'signature' => "{\\rtf" ], 'text/html' => [ 'ext' => 'html', 'signature' => "<!DOCTYPE html" ], // 剩余允许的扩展名对应的映射需自行补充,例如ods/odt等的Content-Type和签名 ]; // 获取URL对应的Content-Type function get_content_type($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_NOBODY, true); // 仅请求响应头 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_REFERER, "https://file-examples.com"); curl_exec($ch); $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); // 处理带编码后缀的Content-Type(如image/jpeg; charset=utf-8) if ($content_type) { $content_type = explode(';', $content_type)[0]; } return $content_type; } // 获取文件前N个字节用于校验签名 function get_file_header($url, $length = 10) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RANGE, "0-".($length-1)); // 仅请求前length字节 curl_setopt($ch, CURLOPT_REFERER, "https://file-examples.com"); $header_data = curl_exec($ch); curl_close($ch); return $header_data; } // 校验文件类型是否合法 function validate_file_type($url, $allowed_extensions, $file_type_mapping) { $content_type = get_content_type($url); $valid_ext = false; // 先通过Content-Type判断 if ($content_type && isset($file_type_mapping[$content_type])) { $ext = $file_type_mapping[$content_type]['ext']; $valid_ext = in_array($ext, $allowed_extensions) ? $ext : false; } // Content-Type无法判断时,通过文件签名校验 if (!$valid_ext) { $file_header = get_file_header($url); foreach ($file_type_mapping as $type => $info) { if ($info['signature'] && str_starts_with($file_header, $info['signature'])) { $ext = $info['ext']; if (in_array($ext, $allowed_extensions)) { $valid_ext = $ext; break; } } } } return $valid_ext; } // 原文件下载函数保留,仅在校验通过后调用 function collect_file($url){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_VERBOSE, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_AUTOREFERER, false); curl_setopt($ch, CURLOPT_REFERER, "https://file-examples.com"); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($ch, CURLOPT_HEADER, 0); $result = curl_exec($ch); curl_close($ch); return($result); } function write_to_file($text,$new_filename){ $fp = fopen($new_filename, 'w'); fwrite($fp, $text); fclose($fp); } // 主执行流程 $url = 'https://images.unsplash.com/photo-1533450718592-29d45635f0a9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80'; // 先校验文件类型 $valid_ext = validate_file_type($url, $allowed_extensions, $file_type_mapping); if ($valid_ext) { // 校验通过,生成正确文件名并下载保存 $new_file_name = uniqid() . '.' . $valid_ext; $temp_file_contents = collect_file($url); write_to_file($temp_file_contents, $new_file_name); echo "文件已成功保存为: $new_file_name"; } else { echo "文件类型不允许"; }
注意事项
- 上述
file_type_mapping仅补充了部分常见类型,需根据允许的扩展名列表补全剩余类型的Content-Type和签名(例如ODS对应application/vnd.oasis.opendocument.spreadsheet,签名同样为PK\x03\x04); - 对于无固定签名的文本类文件(如txt、md、csv等),可结合业务场景增加额外校验逻辑(如检查内容格式);
CURLOPT_RANGE依赖服务器支持Range请求,若服务器不支持会返回完整文件,可根据需求增加错误处理逻辑。
内容的提问来源于stack exchange,提问作者savegramer
相关产品推荐
相关产品推荐

