旧版 @/path/to/file 语法已弃用(PHP 7.4 移除),必须使用 CURLFile 类构建上传对象,自动设置 Content-Type: multipart/form-data。
<?php
function uploadFile(string $url, string $filePath, array $fields = []): array
{
if (!file_exists($filePath)) throw new \InvalidArgumentException("File not found: $filePath");
$cFile = new CURLFile($filePath, mime_content_type($filePath), basename($filePath));
// 字段名 'file' 对应服务端 $_FILES['file']
$postData = array_merge(['file' => $cFile], $fields);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => [
'Expect:', // 禁用 100 Continue,避免某些代理/服务器卡顿
],
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($errno) throw new \RuntimeException("cURL error ($errno): $error");
return ['body' => $response, 'http_code' => $info['http_code']];
}
// 使用
$result = uploadFile(
'https://api.example.com/upload',
'/data/report.pdf',
['user_id' => 1001, 'category' => 'report']
);
var_dump($result);
<?php
function uploadMultiple(string $url, array $filePaths, array $fields = []): array
{
$postData = $fields;
foreach ($filePaths as $i => $path) {
if (!file_exists($path)) continue;
$postData["file_$i"] = new CURLFile($path, mime_content_type($path), basename($path));
}
// 同单文件上传逻辑...
}
<?php
// upload.php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$maxSize = 10 * 1024 * 1024; // 10MB
$allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
$response = ['success' => false, 'message' => '', 'data' => null];
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$response['message'] = 'Only POST allowed';
} elseif (empty($_FILES['file'])) {
$response['message'] = 'No file uploaded';
} else {
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$response['message'] = 'Upload error: ' . $file['error'];
} elseif ($file['size'] > $maxSize) {
$response['message'] = 'File too large';
} elseif (!in_array($file['type'], $allowedTypes, true)) {
$response['message'] = 'Invalid file type';
} else {
// 安全文件名:UUID + 原扩展名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = $uploadDir . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
$response['success'] = true;
$response['data'] = ['filename' => $newName, 'size' => $file['size'], 'type' => $file['type']];
} else {
$response['message'] = 'Move failed';
}
}
}
header('Content-Type: application/json');
echo json_encode($response);
<?php
// 服务端:基于 session.upload_progress (php.ini 开启)
/*
session.upload_progress.enabled = On
session.upload_progress.cleanup = On
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
session.upload_progress.freq = "1%"
session.upload_progress.min_freq = "1"
*/
// 前端表单必须包含隐藏字段:
//
// 进度查询接口 progress.php
session_start();
$key = "upload_progress_unique_form_id";
if (!empty($_SESSION[$key])) {
$prog = $_SESSION[$key];
echo json_encode([
'start_time' => $prog['start_time'],
'content_length' => $prog['content_length'],
'bytes_processed' => $prog['bytes_processed'],
'done' => $prog['done'],
]);
} else {
echo json_encode(['done' => true]); // 未开始或已结束
}
<?php
// 服务端分片合并逻辑
$chunkDir = __DIR__ . '/chunks/';
$fileId = $_POST['file_id'] ?? ''; // 客户端生成 UUID
$chunkIndex = (int)($_POST['chunk_index'] ?? 0);
$totalChunks = (int)($_POST['total_chunks'] ?? 1);
$fileName = $_POST['file_name'] ?? '';
if ($fileId && $fileName) {
$dir = $chunkDir . $fileId . '/';
if (!is_dir($dir)) mkdir($dir, 0755, true);
$chunkFile = $dir . $chunkIndex;
move_uploaded_file($_FILES['file']['tmp_name'], $chunkFile);
// 检查是否全部到齐
$received = glob("$dir/*");
if (count($received) === $totalChunks) {
// 合并
$finalPath = __DIR__ . '/uploads/' . $fileName;
$fp = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
fwrite($fp, file_get_contents("$dir/$i"));
}
fclose($fp);
// 清理分片
array_map('unlink', $received);
rmdir($dir);
echo json_encode(['done' => true, 'path' => $finalPath]);
} else {
echo json_encode(['done' => false, 'received' => count($received)]);
}
}
<?php
function uploadConcurrent(array $tasks, int $maxConcurrent = 5): array
{
$mh = curl_multi_init();
$handles = [];
$results = [];
// 初始化所有句柄
foreach ($tasks as $i => $task) {
$ch = curl_init($task['url']);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $task['data'],
CURLOPT_RETURNTRANSFER => true,
]);
curl_multi_add_handle($mh, $ch);
$handles[$i] = $ch;
}
$running = null;
do {
// 添加新句柄直到达到并发上限
while (count($handles) < $maxConcurrent && $next = array_shift($tasks)) {
$ch = curl_init($next['url']);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $next['data'], CURLOPT_RETURNTRANSFER => true]);
curl_multi_add_handle($mh, $ch);
$handles[] = $ch;
}
curl_multi_exec($mh, $running);
// 读取完成的句柄
while ($info = curl_multi_info_read($mh)) {
$ch = $info['handle'];
$idx = array_search($ch, $handles);
$results[$idx] = [
'body' => curl_multi_getcontent($ch),
'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
'error' => curl_error($ch),
];
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
unset($handles[$idx]);
}
if ($running) curl_multi_select($mh, 0.1);
} while ($running || $handles);
curl_multi_cleanup($mh);
return $results;
}
| 风险 | 对策 |
|---|---|
| 任意文件上传 | 白名单 MIME/扩展名、重命名存储、存储目录不可执行、病毒扫描 |
| 大文件撑爆内存/磁盘 | 限制单文件/总大小、分片上传、流式处理、定时清理临时文件 |
| CSRF | 上传接口验证 Token、SameSite Cookie |
| 路径遍历 | basename() 只取文件名、存储路径拼接用 DIRECTORY_SEPARATOR |
| 钓鱼/恶意内容 | 内容安全策略、文件类型二次校验、用户举报机制 |
PHP cURL 文件上传核心三要素:CURLFile 对象构建 multipart/form-data、服务端 $_FILES 规范处理 + 安全重命名、大文件分片/断点续传/并发控制。生产环境建议封装为 Uploader 服务类,统一错误码、日志、监控指标(上传成功率、平均耗时、带宽占用),并对接对象存储(S3/OSS/COS)实现直传,减轻应用服务器压力。