之前一直白嫖又拍云联盟的免费额度,用了挺长时间了。但最近不太稳,搜了下口碑也差了。不将就,干脆迁到 Cloudflare R2 算了。
选 R2 的理由其实就几个:
找到 rclone.conf 文件,Windows 的话一般在 C:\Users\你的用户名\.config\rclone\rclone.conf,没有就新建一个。
打开贴进去:
[upyun]
type = s3
provider = Other
access_key_id = 又拍云给你的access_key_id
secret_access_key = 又拍云给你的secret_access_key
endpoint = s3.api.upyun.com
[r2]
type = s3
provider = Cloudflare
access_key_id = Cloudflare给你的access_key_id
secret_access_key = Cloudflare给你的secret_access_key
region = auto
endpoint = https://你的账户ID.r2.cloudflarestorage.com
有个坑注意一下:又拍云的 access_key_id 和 secret_access_key 要去控制台 → 云产品 → 对象存储 → 对应服务 → S3 API 里找,不是操作员账号密码那玩意,别搞混了。
先跑两条命令看看:
rclone ls upyun:你的桶名字
rclone ls r2:你的桶名字
要是都能列出来文件列表,说明配置没问题,继续往下走。
直接跑同步命令:
rclone copy upyun:你的桶名字 r2:你的桶名字 -P --transfers 8
解释一下几个参数:
copy 就是拷贝,目标那边多出来的文件不会删,比较安全-P 显示进度条,不然干等着心慌--transfers 8 同时传 8 个文件,速度拉满文件多的话建议先 dry-run 一下看看要传多少东西:
rclone copy upyun:你的桶名字 r2:你的桶名字 -P --transfers 8 --dry-run
搬完之后检查一下有没有漏的:
rclone check upyun:你的桶名字 r2:你的桶名字 --size-only
以前文章里引用的图片地址都是类似 https://upyun.hzzio.top/images/1.jpg 这种完整路径,不可能一篇篇去改。
我的处理办法是用 Service Worker,在前端拦截图片请求,偷偷把域名换掉。
const OLD_DOMAIN = 'xxx.b0.upaiyun.com';
const NEW_DOMAIN = '你的r2域名.r2.cloudflarestorage.com';
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.hostname === OLD_DOMAIN) {
url.hostname = NEW_DOMAIN;
const newRequest = new Request(url, event.request);
event.respondWith(
fetch(newRequest).then((response) => {
const clonedResponse = response.clone();
caches.open('r2-cache').then((cache) => {
cache.put(newRequest, clonedResponse);
});
return response;
})
);
}
});
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
在 HTML 里加一段:
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>
这样用户访问页面的时候,图片还是旧的地址,但实际请求会被劫持到 R2 上,用户无感知。
存量文件搬完了,但之后用户上传图片还是要走后端。不用 SDK,直接手撸 AWS Signature V4,少引入一堆依赖。
r2 配置放 config/r2.php:
return [
'account_id' => env('R2_ACCOUNT_ID'),
'access_key_id' => env('R2_ACCESS_KEY_ID'),
'secret_access_key' => env('R2_SECRET_ACCESS_KEY'),
'bucket' => env('R2_BUCKET'),
'region' => env('R2_REGION', 'auto'),
'domain' => env('R2_DOMAIN'), // 绑定的自定义域名,用来返回完整 URL
];
上传方法,用 GuzzleHttp 发请求:
private static function putToR2(string $localPath, string $key, string $contentType = null)
{
$config = config('r2');
$accountId = $config['account_id'];
$accessKey = $config['access_key_id'];
$secretKey = $config['secret_access_key'];
$bucket = $config['bucket'];
$region = $config['region'];
$domain = $config['domain'];
$contentType = $contentType ?: 'application/octet-stream';
$content = file_get_contents($localPath);
$contentHash = hash('sha256', $content);
$contentLength = strlen($content);
$host = "{$bucket}.{$accountId}.r2.cloudflarestorage.com";
$uri = $key;
$requestUrl = "https://{$host}{$uri}";
$service = 's3';
$timestamp = gmdate('Ymd\THis\Z');
$date = gmdate('Ymd');
$canonicalHeaders = "content-type:{$contentType}\nhost:{$host}\nx-amz-content-sha256:{$contentHash}\nx-amz-date:{$timestamp}\n";
$signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date';
$canonicalRequest = "PUT\n{$uri}\n\n{$canonicalHeaders}\n{$signedHeaders}\n{$contentHash}";
$canonicalRequestHash = hash('sha256', $canonicalRequest);
$credentialScope = "{$date}/{$region}/{$service}/aws4_request";
$stringToSign = "AWS4-HMAC-SHA256\n{$timestamp}\n{$credentialScope}\n{$canonicalRequestHash}";
$kSecret = 'AWS4' . $secretKey;
$kDate = hash_hmac('sha256', $date, $kSecret, true);
$kRegion = hash_hmac('sha256', $region, $kDate, true);
$kService = hash_hmac('sha256', $service, $kRegion, true);
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
$signature = hash_hmac('sha256', $stringToSign, $kSigning);
$authorization = "AWS4-HMAC-SHA256 Credential={$accessKey}/{$credentialScope},SignedHeaders={$signedHeaders},Signature={$signature}";
$client = new Client();
$client->put($requestUrl, [
'headers' => [
'Content-Type' => $contentType,
'Content-Length' => $contentLength,
'x-amz-content-sha256' => $contentHash,
'x-amz-date' => $timestamp,
'Authorization' => $authorization,
],
'body' => $content,
]);
return $domain . $key;
}
调用也简单:
$url = putToR2('/tmp/upload.jpg', '/images/2026/07/foo.jpg', 'image/jpeg');
走完这套,用户传的图片就直接存到 R2 了,返回的 URL 配上自定义域名,跟以前用又拍云的时候一样。
搞完收工,总结一下:
-- END
写的不错,赞助一下主机费
暂无评论~~