之前是后端上传,现在计划改前端直接传。
使用直接创建者上传还无需中间存储桶,也能省去相关的存储/流出成本
参考文档
Presigned URLs · Cloudflare R2 docs
https://developers.cloudflare.com/r2/api/s3/presigned-urls/ 有php版本sdk
https://developers.cloudflare.com/r2/examples/aws/aws-sdk-php/#generate-presigned-urls
生成秘钥地址
https://developers.cloudflare.com/r2/api/tokens/
示例代码
Route::post('/upload/video/presign', [UploadController::class, 'videoPresign']);
/**
* 签发视频直传预签名 URL(前端拿 upload_url 直接 PUT 到 R2,文件不过服务端)。
*
* 入参:filename(必填)、content_type、dir、expires
*/
public function videoPresign(Request $request): Response
{
return $this->run($request, function () use ($request): Response {
$payload = $this->readParams($request);
$signed = Container::get(UploadService::class)->createVideoUploadUrl([
'filename' => trim((string) ($payload['filename'] ?? '')),
'content_type' => trim((string) ($payload['content_type'] ?? '')),
'dir' => trim((string) ($payload['dir'] ?? '')),
'expires' => $payload['expires'] ?? null,
]);
return success($signed, 'success');
});
}
/**
* JSON body 优先,表单提交时回退到 post 参数。
*
* @return array<string, mixed>
*/
private function readParams(Request $request): array
{
if (str_contains(strtolower((string) $request->header('content-type', '')), 'application/json')) {
return $this->readPayload($request);
}
$post = $request->post();
return is_array($post) ? $post : [];
}
/**
* 生成 Cloudflare R2 视频直传预签名 URL。
*
* 后端只负责签发,文件不经过服务端;前端拿到 upload_url 后用 PUT 直传到 R2。
*
* @param array<string, mixed> $options 支持 filename / content_type / dir / expires
* @return array<string, mixed>
*/
public function createVideoUploadUrl(array $options = []): array
{
$filename = trim((string) ($options['filename'] ?? ''));
if ($filename === '') {
throw ValidationException::single('filename', 'required', 'filename is required.');
}
$contentType = $this->resolveVideoMimeType($options, $filename);
$r2 = $this->resolveR2Config($this->cloudflareConfig());
$expires = $this->resolveExpires($options, $r2);
$key = $this->buildVideoKey($options, $r2, $filename, $contentType);
$objectUrl = sprintf('https://%s/%s', $this->r2Host($r2), $key);
$presigner = new S3Presigner(
(string) $r2['access_key_id'],
(string) $r2['secret_access_key'],
trim((string) ($r2['region'] ?? 'auto')) ?: 'auto'
);
$requestTime = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$signedHeaders = $this->shouldSignContentType($r2) ? ['Content-Type' => $contentType] : [];
$uploadUrl = $presigner->presign('PUT', $objectUrl, $signedHeaders, $expires, $requestTime);
return [
'upload_url' => $uploadUrl,
'method' => 'PUT',
// 前端上传时必须原样带上这些请求头,否则签名校验会失败。
'headers' => ['Content-Type' => $contentType],
'key' => $key,
'url' => $this->resolveVideoReadUrl($presigner, $r2, $objectUrl, $key, $expires, $requestTime),
'expires_in' => $expires,
'expires_at' => gmdate('Y-m-d\TH:i:s\Z', (int) $requestTime->getTimestamp() + $expires),
];
}
方案 B:绑定自定义域名 video.com(生产推荐)
当前 video.com 指向你的应用服务器,需要修改 DNS,把它指向 R2。
步骤 1:R2 桶内绑定自定义域名
1.Cloudflare 控制台→R2→桶 assets-video→Settings→Custom Domains
2.点击Add custom domain
3.输入域名:video.com
4.选择:Enable public access(开启公开访问)
5.Cloudflare 会给出 DNS 记录提示,需要在域名 DNS 面板添加一条 CNAME 记录
步骤 2:修改 DNS(关键!)
域名 video.com的 DNS 记录:
。删除原有 video.com 指向你应用的 A/AAAA 记录
。新增一条 CNAME 记录:
Name: video
Type: CNAME
Target:<你的桶ID>.r2.cloudflarestorage.com
TTL:自动
注意:这个 CNAME 是 Cloudflare 提供的,不要填 IP。
步骤 3:等待 DNS 生效(几分钟~半小时)
等待 SSL 证书自动颁发完成。
步骤 4:设置环境变量
env ^
句
R2 PUBLIC URL=https://video.com
播放地址格式
plaintext
印 、7
https://video.com/videos/abc123.mp4
这条链路是这样的:
你手动加的 CNAME(upsell → <bucket>.r2.cloudflarestorage.com)✅ 已生效(DNS 查询确认)
但 R2 的 S3 端点只认在「桶 → Custom Domains」里注册过的域名。没注册的话,R2 不认识 Host: video.com 这个请求
结果:Cloudflare 边缘收到请求后按 zone 的常规路由走(CF-RAY ...-LAX 证明到了 Cloudflare),最终落到你的 Ouyify 应用,返回了那个 1972 字节的 404 HTML
结论:手动 CNAME 方式对 R2 是无效的,必须在桶的 Custom Domains 功能里完成绑定——它会自动创建特殊的 DNS 记录(不需要你手动加)、签发证书、并注册路由。