[分享]laravel自定义文件系统,以企鹅家cos为例

因为某些原因,到了k开头的“准一线城市”一家外包公司工作,拿着每月不到7k的工资996奋斗,不想今天还要挨领导的吐槽,没心情做项目了,于是乎想捣鼓一个自己的博客,然而GitHub上搜到的包上传文件到cos居然失败了…把作者的源代码翻出来看了一下,原来我神经兮兮的弄了一个子账号上传文件,而原作者可能被腾讯云的文档误导,bucket使用了账号拼接。
说实在的,自己的编程技术是有点菜,还好我还能copy。话有点多了,少说多做!
1.新建一个存储系统适配器类 CosAdapter.php
我直接放在了app/Cos目录下。至于具体放哪个位置,我觉得随个人喜好吧,本想归类到Services目录,但是觉得也不合适。
这个文件主要是继承AbstractAdapter并实现里面的方法就好了
代码如下:

<?php

namespace App\Cos;

use Carbon\Carbon;
use DateTimeInterface;
use Exception;
use JetBrains\PhpStorm\ArrayShape;
use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\AdapterInterface;
use Qcloud\Cos\Client;
use League\Flysystem\Config;

class CosAdapter extends AbstractAdapter
{
    protected Client $cosClient;
    protected string $bucket;

    /**
     * 云存储初始化
     * @throws \Exception
     */
    public function __construct($config)
    {
        if (!isset($config['cos_region'], $config['cos_secret_id'], $config['cos_secret_key'], $config['cos_bucket'])) {
            throw new Exception('缺少腾讯云云存储配置');
        }
        $config['cos_schema'] = $config['cos_schema'] ?? 'http';
        $this->cosClient = new Client([
            'region' => $config['cos_region'],
            'schema' => $config['cos_schema'],
            'credentials' => [
                'secretId' => $config['cos_secret_id'],
                'secretKey' => $config['cos_secret_key'],
            ]
        ]);
        $this->bucket = $config['cos_bucket'];
    }

    /**
     * Write a new file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function write($path, $contents, Config $config): bool|array
    {
        try {
            $stream = fopen($contents, 'rb');
            $this->cosClient->Upload($this->bucket, $path, $stream);
        } catch (Exception) {
            return false;
        }
        $size = filesize($contents);
        $type = 'file';
        $result = compact('contents', 'type', 'size', 'path');

        if ($visibility = $config->get('visibility')) {
            $result['visibility'] = $visibility;
            $this->setVisibility($path, $visibility);
        }
        return $result;
    }

    /**
     * Write a new file using a stream.
     *
     * @param string $path
     * @param resource $resource
     * @param Config $config Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function writeStream($path, $resource, Config $config): bool|array
    {
        try {
            $this->cosClient->Upload($this->bucket, $path, $resource);
        } catch (Exception) {
            return false;
        }
        $type = 'file';
        $result = compact('type', 'path');
        if ($visibility = $config->get('visibility')) {
            $this->setVisibility($path, $visibility);
            $result['visibility'] = $visibility;
        }
        return $result;
    }

    /**
     * Update a file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function update($path, $contents, Config $config): bool|array
    {
        return $this->write($path, $contents, $config);
    }

    /**
     * Update a file using a stream.
     *
     * @param string $path
     * @param resource $resource
     * @param Config $config Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function updateStream($path, $resource, Config $config): bool|array
    {
        return $this->writeStream($path, $resource, $config);
    }

    /**
     * Rename a file.
     *
     * @param string $path
     * @param string $newPath
     *
     * @return bool
     */
    public function rename($path, $newPath): bool
    {
        try {
            if ($result = $this->copy($path, $newPath)) {
                $this->delete($path);
            }
            return $result;
        } catch (Exception) {
            return false;
        }
    }

    /**
     * Copy a file.
     *
     * @param string $path
     * @param string $newpath
     *
     * @return bool
     */
    public function copy($path, $newpath): bool
    {
        try {
            $this->cosClient->copy($this->bucket, $newpath, $path);
        } catch (Exception) {
            return false;
        }
        return true;
    }

    /**
     * Delete a file.
     *
     * @param string $path
     *
     * @return bool
     */
    public function delete($path): bool
    {
        try {
            $this->cosClient->deleteObject([
                'Bucket' => $this->bucket,
                'Key' => $path,
            ]);
        } catch (Exception) {
            return false;
        }
        return true;
    }

    /**
     * Delete a directory.
     *
     * @param string $dirname
     *
     * @return bool
     */
    public function deleteDir($dirname): bool
    {
        $nextMarker = '';
        $isTruncated = true;
        while ($isTruncated) {
            try {
                $result = $this->cosClient->listObjects([
                    'Bucket' => $this->bucket,
                    'Delimiter' => '',
                    'EncodingType' => 'url',
                    'Marker' => $nextMarker,
                    'Prefix' => $dirname,
                    'MaxKeys' => 1000
                ]);
                $isTruncated = $result['IsTruncated'];
                $nextMarker = $result['NextMarker'];
                foreach ($result['Contents'] as $content) {
                    $cosFilePath = $content['Key'];
                    try {
                        $this->cosClient->deleteObject([
                            'Bucket' => $this->bucket,
                            'Key' => $cosFilePath,
                        ]);
                    } catch (Exception) {
                        return false;
                    }
                }
            } catch (Exception) {
                return false;
            }
        }
        return true;
    }

    /**
     * Create a directory.
     *
     * @param string $dirname directory name
     * @param Config $config
     *
     * @return array|false
     */
    public function createDir($dirname, Config $config): array|bool
    {
        try {
            $this->cosClient->putObject(array(
                'Bucket' => $this->bucket,
                'Key' => $dirname,
                'Body' => '',
            ));
        } catch (Exception) {
            return false;
        }
        return ['path' => $dirname, 'type' => 'dir'];
    }

    /**
     * Set the visibility for a file.
     *
     * @param string $path
     * @param string $visibility
     *
     * @return array|false file meta data
     */
    public function setVisibility($path, $visibility): bool|array
    {
        $cosAcl = strtolower($visibility) === 'public' ? 'public-read' : 'private';
        try {
            $this->cosClient->putObjectAcl([
                'Bucket' => $this->cosClient,
                'Key' => $path,
                'ACL' => $cosAcl,
            ]);
        } catch (Exception) {
            return false;
        }
        return compact('path', 'visibility');
    }

    /**
     * Check whether a file exists.
     *
     * @param string $path
     *
     * @return array|bool|null
     */
    public function has($path): bool|array|null
    {
        return $this->getMetadata($path);
    }

    /**
     * Read a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function read($path): bool|array
    {
        try {
            $result = $this->cosClient->getObject(array(
                'Bucket' => $this->bucket,
                'Key' => $path,
            ));

            return ['type' => 'file', 'path' => $path, 'contents' => $result['Body']];
        } catch (Exception) {
            return false;
        }
    }

    /**
     * Read a file as a stream.
     *
     * @param string $path
     *
     * @return array|false
     */
    #[ArrayShape(['type' => "string", 'path' => "string", 'stream' => "null|resource"])]
    public function readStream($path): bool|array
    {
        $tmpUrl = $this->SignUrl($path);
        if (!$tmpUrl) {
            return false;
        }
        $curlClient = new \GuzzleHttp\Client();
        $stream = $curlClient->get($tmpUrl, [
            'stream' => true,
        ])->getBody()
            ->detach();
        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
    }

    /**
     * 获取下载签名
     * @param string $path
     * @param \DateTimeInterface|null $expiration
     * @return bool|string
     */
    public function SignUrl(string $path, ?DateTimeInterface $expiration = null): bool|string
    {
        $expiration = $expiration ?? now()->addMinutes(30);
        try {
            $signedUrl = $this->cosClient->getObjectUrl($this->bucket, $path, $expiration);
        } catch (Exception) {
            return false;
        }
        return $signedUrl;
    }

    /**
     * List contents of a directory.
     *
     * @param string $directory
     * @param bool $recursive
     *
     * @return array
     */
    public function listContents($directory = '', $recursive = false): array
    {
        $list = [];
        $marker = '';
        while (true) {
            $response = $this->listObjects($directory, $recursive, $marker);

            foreach ((array)$response['Contents'] as $content) {
                $list[] = $this->normalizeFileInfo($content);
            }

            if (!$response['IsTruncated']) {
                break;
            }
            $marker = $response['NextMarker'] ?: '';
        }
        return $list;
    }

    /**
     * @param string $directory
     * @param bool $recursive
     * @param string $marker
     * @return array|object
     */
    protected function listObjects(string $directory = '', bool $recursive = false, string $marker = ''): object|array
    {
        try {
            return $this->cosClient->listObjects([
                'Bucket' => $this->bucket,
                'Prefix' => $directory === '' ? '' : $directory . '/',
                'Delimiter' => $recursive ? '' : '/',
                'Marker' => $marker,
                'MaxKeys' => 1000,
            ]);
        } catch (Exception) {
            return [
                'Contents' => [],
                'IsTruncated' => false,
                'NextMarker' => '',
            ];
        }
    }

    /**
     * @param array $content
     *
     * @return array
     */
    #[ArrayShape([
        'type' => "string",
        'path' => "mixed",
        'timestamp' => "int",
        'size' => "int",
        'dirname' => "string",
        'basename' => "string",
        'extension' => "mixed|string",
        'filename' => "string"
    ])]
    protected function normalizeFileInfo(array $content): array
    {
        $path = pathinfo($content['Key']);

        return [
            'type' => substr($content['Key'], -1) === '/' ? 'dir' : 'file',
            'path' => $content['Key'],
            'timestamp' => Carbon::parse($content['LastModified'])->getTimestamp(),
            'size' => (int)$content['Size'],
            'dirname' => $path['dirname'] === '.' ? '' : (string)$path['dirname'],
            'basename' => (string)$path['basename'],
            'extension' => $path['extension'] ?? '',
            'filename' => (string)$path['filename'],
        ];
    }

    /**
     * Get all the meta data of a file or directory.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getMetadata($path): bool|array
    {
        try {
            $result = $this->cosClient->headObject([
                'Bucket' => $this->bucket,
                'Key' => $path,
            ]);
            return [
                'type' => substr($result['Key'], -1) === '/' ? 'dir' : 'file',
                'path' => $path,
                'timestamp' => Carbon::parse($result['LastModified'])->getTimestamp(),
                'size' => (int)$result['ContentLength'],
                'mimetype' => $result['ContentType']
            ];
        } catch (Exception) {
            return false;
        }
    }

    /**
     * Get the size of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getSize($path): bool|array
    {
        return $this->getMetadata($path);
    }

    /**
     * Get the mimetype of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getMimetype($path): bool|array
    {
        return $this->getMetadata($path);
    }

    /**
     * Get the last modified time of a file as a timestamp.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getTimestamp($path): bool|array
    {
        return $this->getMetadata($path);
    }

    /**
     * Get the visibility of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getVisibility($path): bool|array
    {
        try {
            $meta = $this->cosClient->getObjectAcl([
                'Bucket' => $this->bucket,
                'Key' => $path,
            ]);

            foreach ($meta['Grants'] as $grant) {
                if (isset($grant['Grantee']['URI'])
                    && $grant['Permission'] === 'READ'
                    && str_contains($grant['Grantee']['URI'], 'global/AllUsers')
                ) {
                    return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
                }
            }
            return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
        } catch (Exception) {
            return false;
        }
    }
}

这些方法作用把基类的英文注释翻译一下就好了。更主要的是,不会也没有太大的关系。可以参考vendor\league\flysystem\src\Adapter\Local.php来写。(^o^)/还可以直接去copyGitHub上别人的。

2.新建一个服务提供者
运行一下命令创建Provider

php artisan make:provider CosServiceProvider

3.打开app\Providers\CosServiceProvider.php写入以下方法

<?php

namespace App\Providers;

use App\Cos\CosAdapter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;

class CosServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        Storage::extend('cos', function ($app, $config) {
            return new Filesystem(new CosAdapter($config));
        });
    }
}

4.在config/app.php 配置文件中注册服务提供者

'providers' => [
    // ...
      App\Providers\CosServiceProvider::class,
];

5.在config\filesystems.php配置文件disks项中追加cos配置

 'cos' => [
            'driver' => 'cos',
            'cos_region' => env('COS_REGION', 'ap-guangzhou'),
            'cos_secret_id' => env('COS_SECRET_ID'),
            'cos_secret_key' => env('COS_SECRET_KEY'),
            'cos_bucket' => env('COS_BUCKET'),
            'cos_schema' => env('COS_SCHEMA', 'http')
        ]

6.在环境变量.env文件中填写您的配置

COS_REGION=
COS_SECRET_ID=
COS_SECRET_KEY=
COS_BUCKET=
COS_SCHEMA=

7.引入官方sdk,一般情况下在写之前就会先引入了

composer require qcloud/cos-sdk-v5

至此,自定义文件系统完毕。由于本人是PHP8版本,一些写法不兼容PHP7。
写个方法测试一下收工!

 public function test(Request $request)
    {
        $file = $request->file('file');
        $path = $file->store('test', 'cos');
        dd($path);
    }
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

赞一个,刚好最近有需求要用到 :kissing_heart:

1周前 评论

composer require larva/laravel-flysystem-cos

1周前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!