overtrue/flysystem-cos
Flysystem adapter for Tencent Cloud COS (Cloud Object Storage). Plug Tencent COS into League Flysystem and Laravel's filesystem so you can read, write, and manage remote objects using the familiar Storage API.
overtrue/flysystem-cos is a thin Flysystem adapter over Tencent Cloud COS (the SDK is qcloud/cos-sdk-v5). It gives you the standard Flysystem API — write, read, delete, listContents, streams, URLs — against a COS bucket. It is not a Laravel package by itself; Laravel users normally pair it with the overtrue/laravel-filesystem-cos bridge or register a custom disk driver.
Install and check the version matrix first. The package follows Flysystem majors. Current releases (^5.x) target Flysystem 3; older majors target Flysystem 1/2. Verify the league/flysystem constraint in composer.json before installing.
composer require overtrue/flysystem-cos
Minimal plain-PHP setup:
use League\Flysystem\Filesystem;
use Overtrue\Flysystem\Cos\CosAdapter;
$adapter = new CosAdapter([
'region' => 'ap-guangzhou',
'bucket' => 'my-bucket-1250000000', // full bucket name including APPID
'credentials' => [
'secretId' => getenv('COS_SECRET_ID'),
'secretKey' => getenv('COS_SECRET_KEY'),
],
'domain' => 'https://cdn.example.com', // optional, for URL generation
]);
$fs = new Filesystem($adapter);
$fs->write('healthcheck.txt', 'ok');
var_dump($fs->fileExists('healthcheck.txt')); // true
echo $fs->publicUrl('healthcheck.txt');
$fs->delete('healthcheck.txt');
Laravel setup — either install the bridge, or register the driver yourself:
// AppServiceProvider::boot()
use Illuminate\Filesystem\FilesystemAdapter;
use League\Flysystem\Filesystem as Flysystem;
use Overtrue\Flysystem\Cos\CosAdapter;
Storage::extend('cos', function ($app, $config) {
$adapter = new CosAdapter([
'region' => $config['region'],
'bucket' => $config['bucket'],
'credentials' => [
'secretId' => $config['credentials']['secretId'],
'secretKey' => $config['credentials']['secretKey'],
],
'domain' => $config['domain'] ?? null,
]);
return new FilesystemAdapter(new Flysystem($adapter), $adapter, $config);
});
Where to look first:
composer.json — Flysystem major compatibility.src/CosAdapter.php — the real supported surface (URL generation, visibility, streams, getClient() escape hatch).qcloud/cos-sdk-v5 docs — region names, bucket naming, ACLs, signed URLs.First real use case: user uploads (avatars, documents) written to a prefix and served through a CDN domain or short-lived signed URLs, with listContents() used for simple inventory/cleanup jobs.
Use one disk per environment, prefixes per tenant/type. Bucket-per-environment (app-prod-125..., app-staging-125...) keeps IAM and lifecycle rules simple. Inside a bucket, namespace aggressively: tenants/{id}/avatars/, exports/{date}/, backups/. COS is flat, so prefixes are your only structure.
Standard CRUD through Laravel Storage:
Storage::disk('cos')->put('reports/2024/q1.pdf', $contents);
Storage::disk('cos')->putFileAs('avatars', $request->file('avatar'), "{$userId}.png");
$contents = Storage::disk('cos')->get('reports/2024/q1.pdf');
Storage::disk('cos')->delete('reports/2024/q1.pdf');
Stream large files rather than buffering into strings:
$stream = fopen($localPath, 'r');
$fs->writeStream('backups/db.sql.gz', $stream);
fclose($stream);
$read = $fs->readStream('backups/db.sql.gz');
// stream_copy_to_stream(...)
fclose($read);
URL strategy: CDN for public, signed for private. Configure domain when assets are publicly served through CDN; otherwise the adapter falls back to the bucket's COS endpoint. For private objects use temporaryUrl():
$public = Storage::disk('cos')->url('avatars/42.png'); // CDN URL
$private = Storage::disk('cos')->temporaryUrl(
'invoices/42.pdf',
now()->addMinutes(30)
);
Listing and reconciliation jobs. Flysystem 3 returns an iterable DirectoryListing; the SDK handles pagination:
foreach ($fs->listContents('exports/2024/', true) as $item) {
if ($item->isFile()) {
// $item->path(), $item->fileSize(), $item->lastModified()
}
}
Always pass a narrow prefix. Recursive listing of a huge bucket is slow and billed per request.
Upload options and metadata. Pass per-write config where the adapter supports forwarding to putObject; check CosAdapter::write() for exactly which keys survive. Setting ContentType and visibility explicitly is safer than relying on extension guessing, especially for extensionless keys.
$fs->write('assets/logo', $bytes, [
'ContentType' => 'image/svg+xml',
'visibility' => 'public',
]);
Escape hatch to the SDK. This adapter is intentionally thin. For multipart upload tuning, object tagging, batch deletes, inventory, or COS CI (image processing) parameters, drop to the underlying client:
$client = $adapter->getClient(); // confirm method name in your installed version
$client->putObject([...]);
Keep that code isolated in a small service class so the rest of the app stays on Flysystem.
Presigned direct uploads. The adapter generates read URLs; it does not implement browser-side upload orchestration. If you need direct-to-COS uploads (large video, mobile), generate presigned PUT URLs with the SDK and only use the adapter for subsequent reads/deletes.
Testing. Unit-test application code against League\Flysystem\InMemory\InMemoryFilesystemAdapter; mock the COS client for adapter-level tests. Run a small integration suite against a dedicated test bucket in CI, gated behind env vars, and clean up by prefix.
Config hygiene. Keep credentials in .env, use a CAM sub-account scoped to the bucket/prefix, and remember php artisan config:cache freezes values — never call env() outside config files.
my-bucket-1250000000). Passing only my-bucket produces confusing 404/signature errors. Some Laravel bridges split bucket + app_id; the core adapter expects the combined string.ap-guangzhou, ap-shanghai, ap-beijing, ...). A wrong region usually shows up as an endpoint/signature error, not a clean "bucket not found."domain needs a scheme and no trailing slash. cdn.example.com or https://cdn.example.com/ can produce malformed or double-slashed URLs depending on version.temporaryUrl() signature is computed for the COS host. Rewriting it to a CDN hostname breaks the signature unless the CDN is configured with origin authentication. Use the COS endpoint for signed links, or CDN signed URLs separately.createDirectory() may be a no-op or zero-byte marker; has('some/dir') is not a reliable directory check. Use directoryExists() or list by prefix.deleteDirectory() can be very slow and expensive on large prefixes because it must enumerate and delete objects. For bulk expiry, use COS lifecycle rules instead of application code.Visibility::PUBLIC may fail or be effectively ignored if the bucket blocks public access; conversely a "private" object served through an open CDN is still publicly reachable. Treat ACL and CDN configuration as one decision.Qcloud\Cos\Exception\*; Flysystem wraps failures in UnableToWriteFile, UnableToReadFile, etc.; Laravel's put() returns false unless you use throw. Catch at the layer you actually call, and log the COS RequestId from the exception — it's what support asks for.application/octet-stream, particularly for extensionless keys or uploads routed through putFileAs. Set ContentType explicitly for anything browsers will render.url() output for spaces, +, #, and CJK filenames before shipping; encode at the presentation layer if needed.listContents() cost and latency. Every page is an API call. Never list recursively on a hot request path; cache inventories or store metadata in your database at write time.void vs bool), exception classes, and publicUrl()/temporaryUrl() availability changed. Copying snippets from old blog posts is a common source of "method does not exist" errors.CosAdapter.php before promising a feature, and pin the version in composer.json.cos disk for you; use the bridge or the Storage::extend pattern above. Also watch for league/flysystem version conflicts with other adapters via composer why-not league/flysystem.How can I help you explore Laravel packages today?