I had been coasting on Upyun's free allowance through its alliance program for quite a while, paying nothing. Lately, though, it started misbehaving every few days: images loading slowly, and sometimes simply failing to load. It got frequent enough that I gave up and moved everything to Qiniu.
The reasons for picking Qiniu were simple:
- Plenty of domestic nodes, so requests are fast
- A free tier that covers a small blog
- SDK documentation that's actually complete
What you need before starting
- rclone installed (if not, grab the Windows build from https://rclone.org/downloads/)
- A Qiniu account with Kodo object storage activated and a bucket created
- The S3 credentials for Upyun and the access keys for Qiniu
Step 1: Configure rclone
Find rclone.conf — on Windows it usually lives at C:\Users\<your-username>\.config\rclone\rclone.conf. Create the file if it isn't there.
Paste this in:
[upyun]
type = s3
provider = Other
access_key_id = your Upyun access_key_id
secret_access_key = your Upyun secret_access_key
endpoint = s3.api.upyun.com
[qiniu]
type = s3
provider = Qiniu
access_key_id = your Qiniu access_key
secret_access_key = your Qiniu secret_key
endpoint = s3-cn-east-1.qiniucs.com
region = cn-east-1
For Upyun, the access_key_id and secret_access_key come from the console under Cloud Products → Object Storage → the relevant service → S3 API. They are not your operator account username and password — don't mix those up.
On the Qiniu side, get the AccessKey/SecretKey under Console → Key Management. The endpoint depends on your bucket's region; the one above is for South China. North China uses s3-cn-east-1.qiniucs.com, and East China uses s3-cn-east-2.qiniucs.com.
Step 2: Verify the connection
Run these two commands:
rclone ls upyun:your-bucket-name
rclone ls qiniu:your-bucket-name
If both list files, the configuration is fine and you can move on.
Step 3: Copy the data
Fire off the sync:
rclone copy upyun:your-bucket-name qiniu:your-bucket-name -P --transfers 8
A quick note on the flags:
copyonly copies — extra files on the destination are left alone, which is the safer choice-Pshows a progress bar so you're not staring at a blank terminal--transfers 8moves eight files at once to maximize throughput
If you have a lot of files, do a dry run first to see how much is actually going across:
rclone copy upyun:your-bucket-name qiniu:your-bucket-name -P --transfers 8 --dry-run
After the transfer finishes, check for anything missing:
rclone check upyun:your-bucket-name qiniu:your-bucket-name --size-only
Step 4: What about the old image URLs?
Posts written earlier reference images with full paths like https://xxx.b0.upaiyun.com/images/1.jpg. Editing every single one by hand wasn't realistic.
My approach was a Service Worker that intercepts image requests on the front end and quietly swaps the domain.
The sw.js file
const OLD_DOMAIN = 'xxx.b0.upaiyun.com';
const NEW_DOMAIN = 'your-qiniu-bound-domain.xxx.clouddn.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('qiniu-cache').then((cache) => {
cache.put(newRequest, clonedResponse);
});
return response;
})
);
}
});
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
Register it on the page
Add this snippet to your HTML:
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>
Now when a visitor loads a page, the markup still points at the old addresses, but the actual requests get hijacked to Qiniu. From the user's perspective nothing changed.
Qiniu mirror storage (the alternative)
Qiniu supports mirroring back to an origin. If you'd rather not deal with a Service Worker, enable mirror storage in the Qiniu console and point the origin at Upyun. The first request for an image makes Qiniu pull a copy from Upyun and cache it; subsequent requests are served straight from Qiniu. The catch is that this depends on the Upyun origin staying alive — and since the whole point of migrating was to cut it off completely, I went with the Service Worker instead.
Step 5: PHP uploads on the backend
The existing files were migrated, but new uploads still have to go through the backend. The official Qiniu PHP SDK saves you from hand-rolling signatures.
Install it:
composer require qiniu/php-sdk
Put the configuration in config/qiniu.php:
return [
'access_key' => env('QINIU_ACCESS_KEY'),
'secret_key' => env('QINIU_SECRET_KEY'),
'bucket' => env('QINIU_BUCKET'),
'domain' => env('QINIU_DOMAIN'), // CDN acceleration domain or the default test domain
];
The upload method:
use Qiniu\Auth;
use Qiniu\Storage\UploadManager;
private static function uploadToQiniu(string $localPath, string $key)
{
$config = config('qiniu');
$accessKey = $config['access_key'];
$secretKey = $config['secret_key'];
$bucket = $config['bucket'];
$domain = $config['domain'];
$auth = new Auth($accessKey, $secretKey);
$token = $auth->uploadToken($bucket);
$uploadMgr = new UploadManager();
list($ret, $err) = $uploadMgr->putFile($token, $key, $localPath);
if ($err !== null) {
throw new \Exception($err->message());
}
return $domain . '/' . $key;
}
Calling it:
$url = uploadToQiniu('/tmp/upload.jpg', 'images/2026/07/foo.jpg');
The SDK handles upload tokens and signing for you, which is far less painful than implementing AWS V4 by hand.
Where things stand
- Existing data moved to Qiniu with rclone and no surprises.
- Old image addresses are transparently redirected via a Service Worker, so no source changes were needed.
- New uploads go straight to Qiniu through the PHP SDK.
- Images now come from Qiniu's domestic nodes and load more reliably than they did on Upyun.
A few things worth keeping in mind:
- Qiniu's free tier gives you 10 GB of storage, 10 GB of traffic per month, and 500,000 GET requests — enough for a small blog.
- Service Workers require browser support, so ancient IE and its contemporaries are out of luck.
- If new files land on Upyun during the migration window, run a sync again to pick up the delta.
- Bind your own CDN domain on Qiniu; the default test domain has a time limit.