[分享]laravel自定義檔案系統,以企鵝家cos為例

susa發表於2021-07-17

因為某些原因,到了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 協議》,轉載必須註明作者和本文連結

相關文章