├── .gitignore
├── sonar-project.properties
├── .travis.yml
├── src
├── Plugins
│ ├── GetUrl.php
│ ├── PutRemoteFileAs.php
│ ├── PutRemoteFile.php
│ ├── FaceId.php
│ ├── TCaptchaV3.php
│ ├── TCaptcha.php
│ ├── GetFederationTokenV3.php
│ ├── CloudInfinite.php
│ ├── Traits
│ │ └── TencentCloudAuthV3.php
│ ├── GetFederationToken.php
│ └── CDN.php
├── ServiceProvider.php
├── filesystems.php
└── Adapter.php
├── .github
├── FUNDING.yml
└── workflows
│ └── php.yml
├── .scrutinizer.yml
├── phpunit.xml.dist
├── LICENSE
├── composer.json
├── tests
├── GetFederationTokenTest.php
├── CDNSignatureTest.php
└── AdapterTest.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /composer.lock
3 | /vendor
4 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.sources=src
2 | sonar.tests=tests
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.6
5 | - 7.0
6 | - 7.1
7 | - 7.2
8 |
9 | sudo: false
10 |
11 | install: travis_retry composer install --no-interaction --prefer-source
12 |
13 | script: vendor/bin/phpunit --verbose
14 |
--------------------------------------------------------------------------------
/src/Plugins/GetUrl.php:
--------------------------------------------------------------------------------
1 | filesystem->getAdapter()->applyPathPrefix($path);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: flysystem-qcloud-cos-v5
2 |
3 | # These are supported funding model platforms
4 |
5 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
6 | # patreon: # Replace with a single Patreon username
7 | # open_collective: # Replace with a single Open Collective username
8 | # ko_fi: # Replace with a single Ko-fi username
9 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
10 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
11 | # custom: # Replace with a single custom sponsorship URL
12 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP Composer
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 |
13 | - name: Validate composer.json and composer.lock
14 | run: composer validate
15 |
16 | - name: Install dependencies
17 | run: composer install --prefer-dist --no-progress --no-suggest
18 |
19 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit"
20 | # Docs: https://getcomposer.org/doc/articles/scripts.md
21 |
22 | # - name: Run test suite
23 | # run: composer run-script test
24 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | tools:
2 | # external_code_coverage: true
3 | php_mess_detector: true
4 | php_code_sniffer: true
5 | sensiolabs_security_checker: true
6 | # php_code_coverage: true
7 | php_pdepend: true
8 | php_loc:
9 | enabled: true
10 | excluded_dirs: [vendor, tests]
11 | checks:
12 | php:
13 | code_rating: true
14 | duplication: true
15 | filter:
16 | excluded_paths:
17 | - 'tests/*'
18 | build:
19 | tests:
20 | override:
21 | -
22 | command: vendor/bin/phpunit --coverage-clover=my-coverage-file
23 | coverage:
24 | file: my-coverage-file
25 | format: php-clover
26 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
19 | src/
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/Plugins/PutRemoteFileAs.php:
--------------------------------------------------------------------------------
1 | filesystem
31 | ->getAdapter()
32 | ->getHttpClient()
33 | ->get($remoteUrl)
34 | ->getBody()
35 | ->getContents();
36 |
37 | $path = trim($path.'/'.$name, '/');
38 |
39 | return $this->filesystem->put($path, $contents, $options) ? $path : false;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "freyo/flysystem-qcloud-cos-v5",
3 | "description": "Flysystem Adapter for Tencent Qcloud COS SDK V5",
4 | "keywords": ["flysystem-adapter", "qcloud-sdk", "qcloud", "qcloud-cos"],
5 | "require": {
6 | "php": ">=5.5.0",
7 | "league/flysystem": "^1.0 || ^2.0 || ^3.0",
8 | "guzzlehttp/guzzle": "~6.0 || ^7.0",
9 | "qcloud/cos-sdk-v5": "^2.0 || dev-guzzle7",
10 | "nesbot/carbon": "~1.0 || ^2.0",
11 | "ext-json": "*"
12 | },
13 | "require-dev": {
14 | "phpunit/phpunit": "~4.8"
15 | },
16 | "autoload": {
17 | "psr-4": {
18 | "Freyo\\Flysystem\\QcloudCOSv5\\": "src/"
19 | }
20 | },
21 | "autoload-dev": {
22 | "psr-4": {
23 | "Freyo\\Flysystem\\QcloudCOSv5\\Tests\\": "tests"
24 | }
25 | },
26 | "license": "MIT",
27 | "authors": [
28 | {
29 | "name": "freyo",
30 | "email": "freyhsiao@gmail.com"
31 | }
32 | ],
33 | "extra": {
34 | "laravel": {
35 | "providers": [
36 | "Freyo\\Flysystem\\QcloudCOSv5\\ServiceProvider"
37 | ]
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Plugins/PutRemoteFile.php:
--------------------------------------------------------------------------------
1 | filesystem
32 | ->getAdapter()
33 | ->getHttpClient()
34 | ->get($remoteUrl)
35 | ->getBody()
36 | ->getContents();
37 |
38 | $filename = md5($contents);
39 | $extension = ExtensionGuesser::getInstance()->guess(MimeType::detectByContent($contents));
40 | $name = $filename.'.'.$extension;
41 |
42 | $path = trim($path.'/'.$name, '/');
43 |
44 | return $this->filesystem->put($path, $contents, $options) ? $path : false;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Plugins/FaceId.php:
--------------------------------------------------------------------------------
1 | (string) $ruleId,
40 | ]);
41 |
42 | return $this->request(
43 | $params, 'DetectAuth', 'faceid', '2018-03-01'
44 | );
45 | }
46 |
47 | /**
48 | * @param string $ruleId
49 | * @param string $bizToken
50 | * @param string $infoType
51 | *
52 | * @return array|bool
53 | */
54 | public function getDetectInfo($ruleId, $bizToken, $infoType = '0')
55 | {
56 | $params = [
57 | 'RuleId' => (string) $ruleId,
58 | 'BizToken' => $bizToken,
59 | 'InfoType' => (string) $infoType,
60 | ];
61 |
62 | return $this->request(
63 | $params, 'GetDetectInfo', 'faceid', '2018-03-01'
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Plugins/TCaptchaV3.php:
--------------------------------------------------------------------------------
1 | aid = $aid;
36 | $this->appSecretKey = $appSecretKey;
37 |
38 | return $this;
39 | }
40 |
41 | /**
42 | * @param $ticket
43 | * @param $randStr
44 | * @param $userIP
45 | * @param array $options
46 | *
47 | * @return bool|array
48 | */
49 | public function verify($ticket, $randStr, $userIP, array $options = [])
50 | {
51 | $params = array_merge([
52 | 'CaptchaType' => 9,
53 | 'Ticket' => $ticket,
54 | 'UserIp' => $userIP,
55 | 'Randstr' => $randStr,
56 | 'CaptchaAppId' => $this->aid,
57 | 'AppSecretKey' => $this->appSecretKey,
58 | ], $options);
59 |
60 | return $this->request(
61 | $params, 'DescribeCaptchaResult', 'captcha', '2019-07-22'
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Plugins/TCaptcha.php:
--------------------------------------------------------------------------------
1 | aid = $aid;
35 | $this->appSecretKey = $appSecretKey;
36 |
37 | return $this;
38 | }
39 |
40 | /**
41 | * @param $ticket
42 | * @param $randStr
43 | * @param $userIP
44 | *
45 | * @return mixed
46 | */
47 | public function verify($ticket, $randStr, $userIP)
48 | {
49 | $contents = $this->filesystem
50 | ->getAdapter()
51 | ->getHttpClient()
52 | ->get(self::TICKET_VERIFY, ['query' => [
53 | 'aid' => $this->aid,
54 | 'AppSecretKey' => $this->appSecretKey,
55 | 'Ticket' => $ticket,
56 | 'Randstr' => $randStr,
57 | 'UserIP' => $userIP,
58 | ]])
59 | ->getBody()
60 | ->getContents();
61 |
62 | return \GuzzleHttp\json_decode($contents);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | app instanceof LumenApplication) {
31 | $this->app->configure('filesystems');
32 | }
33 |
34 | $this->app->make('filesystem')
35 | ->extend('cosv5', function ($app, $config) {
36 | $client = new Client($config);
37 | $flysystem = new Filesystem(new Adapter($client, $config), $config);
38 |
39 | $flysystem->addPlugin(new PutRemoteFile());
40 | $flysystem->addPlugin(new PutRemoteFileAs());
41 | $flysystem->addPlugin(new GetUrl());
42 | $flysystem->addPlugin(new CDN());
43 | $flysystem->addPlugin(new TCaptcha());
44 | $flysystem->addPlugin(new GetFederationToken());
45 | $flysystem->addPlugin(new GetFederationTokenV3());
46 | $flysystem->addPlugin(new CloudInfinite());
47 |
48 | return $flysystem;
49 | });
50 | }
51 |
52 | /**
53 | * Register any application services.
54 | *
55 | * @return void
56 | */
57 | public function register()
58 | {
59 | $this->mergeConfigFrom(
60 | __DIR__.'/filesystems.php', 'filesystems'
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/GetFederationTokenTest.php:
--------------------------------------------------------------------------------
1 | getenv('COSV5_REGION'),
17 | 'credentials' => [
18 | 'appId' => getenv('COSV5_APP_ID'),
19 | 'secretId' => getenv('COSV5_SECRET_ID'),
20 | 'secretKey' => getenv('COSV5_SECRET_KEY'),
21 | ],
22 | 'timeout' => getenv('COSV5_TIMEOUT'),
23 | 'connect_timeout' => getenv('COSV5_CONNECT_TIMEOUT'),
24 | 'bucket' => getenv('COSV5_BUCKET'),
25 | 'cdn' => getenv('COSV5_CDN'),
26 | 'scheme' => getenv('COSV5_SCHEME'),
27 | 'read_from_cdn' => getenv('COSV5_READ_FROM_CDN'),
28 | ];
29 |
30 | $client = new Client($config);
31 |
32 | $adapter = new Adapter($client, $config);
33 |
34 | $filesystem = new Filesystem($adapter, $config);
35 |
36 | $filesystem->addPlugin(new GetFederationToken());
37 |
38 | return [
39 | [$filesystem],
40 | ];
41 | }
42 |
43 | /**
44 | * @dataProvider Provider
45 | */
46 | public function testDefault(Filesystem $filesystem)
47 | {
48 | $this->assertArrayHasKey('credentials', $filesystem->getFederationToken());
49 | }
50 |
51 | /**
52 | * @dataProvider Provider
53 | */
54 | public function testCustom(Filesystem $filesystem)
55 | {
56 | $this->assertArrayHasKey(
57 | 'credentials',
58 | $filesystem->getFederationToken('custom/path/to', 7200, function ($path, $config) {
59 | $appId = $config->get('credentials')['appId'];
60 | $region = $config->get('region');
61 | $bucket = $config->get('bucket');
62 |
63 | return [
64 | 'version' => '2.0',
65 | 'statement' => [
66 | 'action' => [
67 | 'name/cos:PutObject',
68 | ],
69 | 'effect' => 'allow',
70 | 'principal' => ['qcs' => ['*']],
71 | 'resource' => [
72 | "qcs::cos:$region:uid/$appId:prefix//$appId/$bucket/$path",
73 | ],
74 | ],
75 | ];
76 | })
77 | );
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/filesystems.php:
--------------------------------------------------------------------------------
1 | 'local',
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Default Cloud Filesystem Disk
21 | |--------------------------------------------------------------------------
22 | |
23 | | Many applications store files both locally and in the cloud. For this
24 | | reason, you may specify a default "cloud" driver here. This driver
25 | | will be bound as the Cloud disk implementation in the container.
26 | |
27 | */
28 |
29 | 'cloud' => 's3',
30 |
31 | /*
32 | |--------------------------------------------------------------------------
33 | | Filesystem Disks
34 | |--------------------------------------------------------------------------
35 | |
36 | | Here you may configure as many filesystem "disks" as you wish, and you
37 | | may even configure multiple disks of the same driver. Defaults have
38 | | been setup for each driver as an example of the required options.
39 | |
40 | | Supported Drivers: "local", "ftp", "s3", "rackspace"
41 | |
42 | */
43 |
44 | 'disks' => [
45 |
46 | 'local' => [
47 | 'driver' => 'local',
48 | 'root' => storage_path('app'),
49 | ],
50 |
51 | 'public' => [
52 | 'driver' => 'local',
53 | 'root' => storage_path('app/public'),
54 | 'url' => env('APP_URL').'/storage',
55 | 'visibility' => 'public',
56 | ],
57 |
58 | 's3' => [
59 | 'driver' => 's3',
60 | 'key' => env('AWS_KEY'),
61 | 'secret' => env('AWS_SECRET'),
62 | 'region' => env('AWS_REGION'),
63 | 'bucket' => env('AWS_BUCKET'),
64 | ],
65 |
66 | 'cosv5' => [
67 | 'driver' => 'cosv5',
68 | 'region' => env('COSV5_REGION', 'cn-east'),
69 | 'credentials' => [
70 | 'appId' => env('COSV5_APP_ID'),
71 | 'secretId' => env('COSV5_SECRET_ID'),
72 | 'secretKey' => env('COSV5_SECRET_KEY'),
73 | 'token' => env('COSV5_TOKEN'),
74 | ],
75 | 'timeout' => env('COSV5_TIMEOUT', 60),
76 | 'connect_timeout' => env('COSV5_CONNECT_TIMEOUT', 60),
77 | 'bucket' => env('COSV5_BUCKET'),
78 | 'cdn' => env('COSV5_CDN'),
79 | 'scheme' => env('COSV5_SCHEME', 'https'),
80 | 'read_from_cdn' => env('COSV5_READ_FROM_CDN', false),
81 | 'cdn_key' => env('COSV5_CDN_KEY'),
82 | 'encrypt' => env('COSV5_ENCRYPT', false),
83 | ],
84 |
85 | ],
86 |
87 | ];
88 |
--------------------------------------------------------------------------------
/src/Plugins/GetFederationTokenV3.php:
--------------------------------------------------------------------------------
1 | getCustomPolicy($customPolicy, $path)
40 | : $this->getDefaultPolicy($path);
41 |
42 | $params = [
43 | 'DurationSeconds' => $seconds,
44 | 'Name' => $name,
45 | 'Policy' => urlencode($policy),
46 | ];
47 |
48 | return $this->request(
49 | $params, 'GetFederationToken', 'sts', '2018-08-13'
50 | );
51 | }
52 |
53 | /**
54 | * @param Closure $callable
55 | * @param $path
56 | *
57 | * @return string
58 | */
59 | protected function getCustomPolicy(Closure $callable, $path)
60 | {
61 | $policy = call_user_func($callable, $path, $this->getConfig());
62 |
63 | return \GuzzleHttp\json_encode(
64 | $policy, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
65 | );
66 | }
67 |
68 | /**
69 | * @see https://cloud.tencent.com/document/product/436/31923
70 | *
71 | * @param $path
72 | *
73 | * @return string
74 | */
75 | protected function getDefaultPolicy($path)
76 | {
77 | $appId = $this->getCredentials()['appId'];
78 |
79 | $region = $this->getConfig()->get('region');
80 | $bucket = $this->getConfig()->get('bucket');
81 |
82 | $policy = [
83 | 'version' => '2.0',
84 | 'statement' => [
85 | 'action' => [
86 | // 简单上传
87 | 'name/cos:PutObject',
88 | 'name/cos:PostObject',
89 | // 分片上传
90 | 'name/cos:InitiateMultipartUpload',
91 | 'name/cos:ListParts',
92 | 'name/cos:UploadPart',
93 | 'name/cos:CompleteMultipartUpload',
94 | 'name/cos:AbortMultipartUpload',
95 | ],
96 | 'effect' => 'allow',
97 | 'resource' => [
98 | "qcs::cos:$region:uid/$appId:$bucket-$appId/$path",
99 | ],
100 | ],
101 | ];
102 |
103 | return \GuzzleHttp\json_encode(
104 | $policy, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
105 | );
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/CDNSignatureTest.php:
--------------------------------------------------------------------------------
1 | getenv('COSV5_REGION'),
17 | 'credentials' => [
18 | 'appId' => getenv('COSV5_APP_ID'),
19 | 'secretId' => getenv('COSV5_SECRET_ID'),
20 | 'secretKey' => getenv('COSV5_SECRET_KEY'),
21 | ],
22 | 'timeout' => getenv('COSV5_TIMEOUT'),
23 | 'connect_timeout' => getenv('COSV5_CONNECT_TIMEOUT'),
24 | 'bucket' => getenv('COSV5_BUCKET'),
25 | 'cdn' => getenv('COSV5_CDN'),
26 | 'scheme' => getenv('COSV5_SCHEME'),
27 | 'read_from_cdn' => getenv('COSV5_READ_FROM_CDN'),
28 | 'cdn_key' => getenv('COSV5_CDN_KEY'),
29 | ];
30 |
31 | $client = new Client($config);
32 |
33 | $adapter = new Adapter($client, $config);
34 |
35 | $filesystem = new Filesystem($adapter, $config);
36 |
37 | $filesystem->addPlugin(new CDN());
38 |
39 | return [
40 | [$filesystem],
41 | ];
42 | }
43 |
44 | /**
45 | * @dataProvider Provider
46 | */
47 | public function testSignature(Filesystem $filesystem)
48 | {
49 | $this->assertSame(
50 | 'http://www.test.com/1.mp4?sign=8fe5c42d7b0dfa7afabef2a33cd96459&t=5a66b340',
51 | $filesystem->cdn()->signature('http://www.test.com/1.mp4', null, 1516680000)
52 | );
53 | }
54 |
55 | /**
56 | * @dataProvider Provider
57 | */
58 | public function testSignatureA(Filesystem $filesystem)
59 | {
60 | $this->assertSame(
61 | 'http://www.test.com/1.mp4?sign=1516680000-e9pmhkb21sjqfeh33f9-0-9a15f74f326dbb6dd485911eb0d9c629',
62 | $filesystem->cdn()->signatureA('http://www.test.com/1.mp4', null, 1516680000, 'e9pmhkb21sjqfeh33f9')
63 | );
64 | }
65 |
66 | /**
67 | * @dataProvider Provider
68 | */
69 | public function testSignatureB(Filesystem $filesystem)
70 | {
71 | date_default_timezone_set('UTC');
72 |
73 | $this->assertSame(
74 | 'http://www.test.com/201801230400/8eee4e932f285743fa23c79030139459/1.mp4',
75 | $filesystem->cdn()->signatureB('http://www.test.com/1.mp4', null, 1516680000)
76 | );
77 | }
78 |
79 | /**
80 | * @dataProvider Provider
81 | */
82 | public function testSignatureC(Filesystem $filesystem)
83 | {
84 | $this->assertSame(
85 | 'http://www.test.com/8fe5c42d7b0dfa7afabef2a33cd96459/5a66b340/1.mp4',
86 | $filesystem->cdn()->signatureC('http://www.test.com/1.mp4', null, 1516680000)
87 | );
88 | }
89 |
90 | /**
91 | * @dataProvider Provider
92 | */
93 | public function testSignatureD(Filesystem $filesystem)
94 | {
95 | $this->assertSame(
96 | 'http://www.test.com/1.mp4?sign=8fe5c42d7b0dfa7afabef2a33cd96459&t=5a66b340',
97 | $filesystem->cdn()->signatureD('http://www.test.com/1.mp4', null, 1516680000)
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Plugins/CloudInfinite.php:
--------------------------------------------------------------------------------
1 | filesystem->getAdapter();
37 |
38 | $url = 'https://'.$adapter->getPicturePath($objectKey).'?image_process';
39 |
40 | $response = $adapter->getHttpClient()->post($url, [
41 | 'http_errors' => false,
42 | 'headers' => [
43 | 'Authorization' => $adapter->getAuthorization('POST', $url),
44 | 'Pic-Operations' => \GuzzleHttp\json_encode(
45 | $picOperations, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
46 | ),
47 | ],
48 | ]);
49 |
50 | return $this->parse(
51 | $response->getBody()->getContents()
52 | );
53 | }
54 |
55 | /**
56 | * @param string $objectKey
57 | * @param array $contentRecognition
58 | *
59 | * @return array
60 | */
61 | public function contentRecognition($objectKey, array $contentRecognition)
62 | {
63 | $adapter = $this->filesystem->getAdapter();
64 |
65 | $url = 'https://'.$adapter->getPicturePath($objectKey).'?CR';
66 |
67 | $response = $adapter->getHttpClient()->get($url, [
68 | 'http_errors' => false,
69 | 'headers' => [
70 | 'Authorization' => $adapter->getAuthorization('GET', $url),
71 | 'Content-Recognition' => \GuzzleHttp\json_encode(
72 | $contentRecognition, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
73 | ),
74 | ],
75 | ]);
76 |
77 | return $this->parse(
78 | $response->getBody()->getContents()
79 | );
80 | }
81 |
82 | /**
83 | * @param string $xml
84 | *
85 | * @return array
86 | */
87 | protected function parse($xml)
88 | {
89 | $backup = libxml_disable_entity_loader(true);
90 |
91 | $result = $this->normalize(
92 | simplexml_load_string(
93 | $this->sanitize($xml),
94 | 'SimpleXMLElement',
95 | LIBXML_COMPACT | LIBXML_NOCDATA | LIBXML_NOBLANKS
96 | )
97 | );
98 |
99 | libxml_disable_entity_loader($backup);
100 |
101 | return $result;
102 | }
103 |
104 | /**
105 | * Object to array.
106 | *
107 | *
108 | * @param SimpleXMLElement $obj
109 | *
110 | * @return array
111 | */
112 | protected function normalize($obj)
113 | {
114 | $result = null;
115 |
116 | if (is_object($obj)) {
117 | $obj = (array) $obj;
118 | }
119 |
120 | if (is_array($obj)) {
121 | foreach ($obj as $key => $value) {
122 | $res = $this->normalize($value);
123 | if (('@attributes' === $key) && ($key)) {
124 | $result = $res; // @codeCoverageIgnore
125 | } else {
126 | $result[$key] = $res;
127 | }
128 | }
129 | } else {
130 | $result = $obj;
131 | }
132 |
133 | return $result;
134 | }
135 |
136 | /**
137 | * Delete invalid characters in XML.
138 | *
139 | * @see https://www.w3.org/TR/2008/REC-xml-20081126/#charsets - XML charset range
140 | * @see http://php.net/manual/en/regexp.reference.escape.php - escape in UTF-8 mode
141 | *
142 | * @param string $xml
143 | *
144 | * @return string
145 | */
146 | protected function sanitize($xml)
147 | {
148 | return preg_replace(
149 | '/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+/u',
150 | '',
151 | $xml
152 | );
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/Plugins/Traits/TencentCloudAuthV3.php:
--------------------------------------------------------------------------------
1 | filesystem->getConfig();
15 | }
16 |
17 | /**
18 | * @return array
19 | */
20 | protected function getCredentials()
21 | {
22 | return $this->getConfig()->get('credentials');
23 | }
24 |
25 | /**
26 | * @param array $args
27 | * @param string $action
28 | * @param string $service
29 | * @param string $version
30 | * @param string|int|null $timestamp
31 | *
32 | * @return bool|array
33 | */
34 | protected function request(array $args, $action, $service, $version, $timestamp = null)
35 | {
36 | $client = $this->getHttpClient($service);
37 |
38 | $response = $client->post('/', [
39 | 'body' => $body = \GuzzleHttp\json_encode(
40 | $args, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
41 | ),
42 | 'headers' => [
43 | 'X-TC-Action' => $action,
44 | 'X-TC-Region' => $this->getConfig()->get('region'),
45 | 'X-TC-Timestamp' => $timestamp = $timestamp ?: time(),
46 | 'X-TC-Version' => $version,
47 | 'Authorization' => $this->getAuthorization($timestamp, $service, $body),
48 | 'Content-Type' => 'application/json',
49 | ],
50 | ]);
51 |
52 | $contents = $response->getBody()->getContents();
53 |
54 | return $this->normalize($contents);
55 | }
56 |
57 | /**
58 | * @param $service
59 | *
60 | * @return \GuzzleHttp\Client
61 | */
62 | protected function getHttpClient($service)
63 | {
64 | return new \GuzzleHttp\Client([
65 | 'base_uri' => "https://{$service}.tencentcloudapi.com",
66 | ]);
67 | }
68 |
69 | /**
70 | * @param string $contents
71 | *
72 | * @return bool|array
73 | */
74 | protected function normalize($contents)
75 | {
76 | $data = json_decode($contents, true);
77 |
78 | if (json_last_error() !== JSON_ERROR_NONE || !isset($data['Response'])) {
79 | return false;
80 | }
81 |
82 | return $data['Response'];
83 | }
84 |
85 | /**
86 | * @param string|int|null $timestamp
87 | * @param string $service
88 | * @param string $body
89 | *
90 | * @return string
91 | */
92 | protected function getAuthorization($timestamp, $service, $body)
93 | {
94 | return sprintf(
95 | '%s Credential=%s/%s, SignedHeaders=%s, Signature=%s',
96 | 'TC3-HMAC-SHA256',
97 | $this->getCredentials()['secretId'],
98 | Carbon::createFromTimestampUTC($timestamp)->toDateString()."/{$service}/tc3_request",
99 | 'content-type;host',
100 | hash_hmac(
101 | 'SHA256',
102 | $this->getSignatureString($timestamp, $service, $body),
103 | $this->getRequestKey($timestamp, $service)
104 | )
105 | );
106 | }
107 |
108 | /**
109 | * @param string|int|null $timestamp
110 | * @param string $service
111 | *
112 | * @return string
113 | */
114 | protected function getRequestKey($timestamp, $service)
115 | {
116 | $secretDate = hash_hmac(
117 | 'SHA256',
118 | Carbon::createFromTimestampUTC($timestamp)->toDateString(),
119 | 'TC3'.$this->getCredentials()['secretKey'],
120 | true
121 | );
122 | $secretService = hash_hmac('SHA256', $service, $secretDate, true);
123 |
124 | return hash_hmac('SHA256', 'tc3_request', $secretService, true);
125 | }
126 |
127 | /**
128 | * @param string $service
129 | * @param string $body
130 | *
131 | * @return string
132 | */
133 | protected function getCanonicalRequest($service, $body)
134 | {
135 | return implode("\n", [
136 | 'POST',
137 | '/',
138 | '',
139 | 'content-type:application/json',
140 | "host:{$service}.tencentcloudapi.com",
141 | '',
142 | 'content-type;host',
143 | hash('SHA256', $body),
144 | ]);
145 | }
146 |
147 | /**
148 | * @param string|int|null $timestamp
149 | * @param string $service
150 | * @param string $body
151 | *
152 | * @return string
153 | */
154 | protected function getSignatureString($timestamp, $service, $body)
155 | {
156 | return implode("\n", [
157 | 'TC3-HMAC-SHA256',
158 | $timestamp,
159 | Carbon::createFromTimestampUTC($timestamp)->toDateString()."/{$service}/tc3_request",
160 | hash('SHA256', $this->getCanonicalRequest($service, $body)),
161 | ]);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/Plugins/GetFederationToken.php:
--------------------------------------------------------------------------------
1 | getCustomPolicy($customPolicy, $path)
37 | : $this->getDefaultPolicy($path);
38 |
39 | $params = [
40 | 'durationSeconds' => $seconds,
41 | 'name' => $name,
42 | 'policy' => urlencode($policy),
43 | ];
44 |
45 | return $this->request($params, 'GetFederationToken');
46 | }
47 |
48 | /**
49 | * @param Closure $callable
50 | * @param $path
51 | *
52 | * @return string
53 | */
54 | protected function getCustomPolicy(Closure $callable, $path)
55 | {
56 | $policy = call_user_func($callable, $path, $this->getConfig());
57 |
58 | return \GuzzleHttp\json_encode($policy, JSON_UNESCAPED_SLASHES);
59 | }
60 |
61 | /**
62 | * @see https://cloud.tencent.com/document/product/436/31923
63 | *
64 | * @param $path
65 | *
66 | * @return string
67 | */
68 | protected function getDefaultPolicy($path)
69 | {
70 | $appId = $this->getCredentials()['appId'];
71 |
72 | $region = $this->getConfig()->get('region');
73 | $bucket = $this->getConfig()->get('bucket');
74 |
75 | $policy = [
76 | 'version' => '2.0',
77 | 'statement' => [
78 | 'action' => [
79 | // 简单上传
80 | 'name/cos:PutObject',
81 | 'name/cos:PostObject',
82 | // 分片上传
83 | 'name/cos:InitiateMultipartUpload',
84 | 'name/cos:ListParts',
85 | 'name/cos:UploadPart',
86 | 'name/cos:CompleteMultipartUpload',
87 | 'name/cos:AbortMultipartUpload',
88 | ],
89 | 'effect' => 'allow',
90 | 'principal' => ['qcs' => ['*']],
91 | 'resource' => [
92 | "qcs::cos:$region:uid/$appId:prefix//$appId/$bucket/$path",
93 | ],
94 | ],
95 | ];
96 |
97 | return \GuzzleHttp\json_encode($policy, JSON_UNESCAPED_SLASHES);
98 | }
99 |
100 | /**
101 | * @return \League\Flysystem\Config
102 | */
103 | protected function getConfig()
104 | {
105 | return $this->filesystem->getConfig();
106 | }
107 |
108 | /**
109 | * @return array
110 | */
111 | protected function getCredentials()
112 | {
113 | return $this->getConfig()->get('credentials');
114 | }
115 |
116 | /**
117 | * @param array $args
118 | * @param $action
119 | *
120 | * @return bool|array
121 | */
122 | protected function request(array $args, $action)
123 | {
124 | $client = $this->getHttpClient();
125 |
126 | $response = $client->post('/v2/index.php', [
127 | 'form_params' => $this->buildFormParams($args, $action),
128 | ]);
129 |
130 | $contents = $response->getBody()->getContents();
131 |
132 | return $this->normalize($contents);
133 | }
134 |
135 | /**
136 | * @return \GuzzleHttp\Client
137 | */
138 | protected function getHttpClient()
139 | {
140 | return new \GuzzleHttp\Client([
141 | 'base_uri' => 'https://sts.api.qcloud.com',
142 | ]);
143 | }
144 |
145 | /**
146 | * @param array $params
147 | * @param string $action
148 | *
149 | * @return array
150 | */
151 | protected function buildFormParams(array $params, $action)
152 | {
153 | $params = $this->addCommonParams($params, $action);
154 |
155 | return $this->addSignature($params);
156 | }
157 |
158 | /**
159 | * @param array $params
160 | * @param string $action
161 | *
162 | * @return array
163 | */
164 | protected function addCommonParams(array $params, $action)
165 | {
166 | return array_merge([
167 | 'Region' => $this->getConfig()->get('region'),
168 | 'Action' => $action,
169 | 'SecretId' => $this->getCredentials()['secretId'],
170 | 'Timestamp' => time(),
171 | 'Nonce' => rand(1, 65535),
172 | ], $params);
173 | }
174 |
175 | /**
176 | * @param array $params
177 | *
178 | * @return array
179 | */
180 | protected function addSignature(array $params)
181 | {
182 | $params['Signature'] = $this->getSignature($params);
183 |
184 | return $params;
185 | }
186 |
187 | /**
188 | * @param array $params
189 | *
190 | * @return string
191 | */
192 | protected function getSignature(array $params)
193 | {
194 | ksort($params);
195 |
196 | $srcStr = 'POSTsts.api.qcloud.com/v2/index.php?'.urldecode(http_build_query($params));
197 |
198 | return base64_encode(hash_hmac('sha1', $srcStr, $this->getCredentials()['secretKey'], true));
199 | }
200 |
201 | /**
202 | * @param string $contents
203 | *
204 | * @return bool|array
205 | */
206 | protected function normalize($contents)
207 | {
208 | $data = json_decode($contents, true);
209 |
210 | if (json_last_error() !== JSON_ERROR_NONE || $data['code'] !== 0) {
211 | return false;
212 | }
213 |
214 | return $data['data'];
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/tests/AdapterTest.php:
--------------------------------------------------------------------------------
1 | getenv('COSV5_REGION'),
18 | 'credentials' => [
19 | 'appId' => getenv('COSV5_APP_ID'),
20 | 'secretId' => getenv('COSV5_SECRET_ID'),
21 | 'secretKey' => getenv('COSV5_SECRET_KEY'),
22 | ],
23 | 'timeout' => getenv('COSV5_TIMEOUT'),
24 | 'connect_timeout' => getenv('COSV5_CONNECT_TIMEOUT'),
25 | 'bucket' => getenv('COSV5_BUCKET'),
26 | 'cdn' => getenv('COSV5_CDN'),
27 | 'scheme' => getenv('COSV5_SCHEME'),
28 | 'read_from_cdn' => getenv('COSV5_READ_FROM_CDN'),
29 | 'encrypt' => getenv('COSV5_ENCRYPT'),
30 | ];
31 |
32 | $cosApi = new Client($config);
33 |
34 | $adapter = new Adapter($cosApi, $config);
35 |
36 | $options = [
37 | 'machineId' => PHP_OS.PHP_VERSION,
38 | ];
39 |
40 | return [
41 | [$adapter, $config, $options],
42 | ];
43 | }
44 |
45 | /**
46 | * @dataProvider Provider
47 | */
48 | public function testWrite(AdapterInterface $adapter, $config, $options)
49 | {
50 | $this->assertTrue((bool) $adapter->write(
51 | "foo/{$options['machineId']}/foo.md",
52 | 'content',
53 | new Config()
54 | ));
55 | }
56 |
57 | /**
58 | * @dataProvider Provider
59 | */
60 | public function testWriteStream(AdapterInterface $adapter, $config, $options)
61 | {
62 | $temp = tmpfile();
63 | fwrite($temp, 'writing to tempfile');
64 | $this->assertTrue((bool) $adapter->writeStream(
65 | "foo/{$options['machineId']}/bar.md",
66 | $temp,
67 | new Config()
68 | ));
69 | fclose($temp);
70 | }
71 |
72 | /**
73 | * @dataProvider Provider
74 | */
75 | public function testUpdate(AdapterInterface $adapter, $config, $options)
76 | {
77 | $this->assertTrue((bool) $adapter->update(
78 | "foo/{$options['machineId']}/bar.md",
79 | uniqid(),
80 | new Config()
81 | ));
82 | }
83 |
84 | /**
85 | * @dataProvider Provider
86 | */
87 | public function testUpdateStream(AdapterInterface $adapter, $config, $options)
88 | {
89 | $temp = tmpfile();
90 | fwrite($temp, 'writing to tempfile');
91 | $this->assertTrue((bool) $adapter->updateStream(
92 | "foo/{$options['machineId']}/bar.md",
93 | $temp,
94 | new Config()
95 | ));
96 | fclose($temp);
97 | }
98 |
99 | /**
100 | * @dataProvider Provider
101 | */
102 | public function testRename(AdapterInterface $adapter, $config, $options)
103 | {
104 | $this->assertTrue($adapter->rename(
105 | "foo/{$options['machineId']}/foo.md",
106 | "/foo/{$options['machineId']}/rename.md"
107 | ));
108 | }
109 |
110 | /**
111 | * @dataProvider Provider
112 | */
113 | public function testCopy(AdapterInterface $adapter, $config, $options)
114 | {
115 | $this->assertTrue($adapter->copy(
116 | "foo/{$options['machineId']}/bar.md",
117 | "/foo/{$options['machineId']}/copy.md"
118 | ));
119 | }
120 |
121 | /**
122 | * @dataProvider Provider
123 | */
124 | public function testDelete(AdapterInterface $adapter, $config, $options)
125 | {
126 | $this->assertTrue($adapter->delete("foo/{$options['machineId']}/rename.md"));
127 | }
128 |
129 | /**
130 | * @dataProvider Provider
131 | */
132 | public function testCreateDir(AdapterInterface $adapter, $config, $options)
133 | {
134 | $this->assertTrue((bool) $adapter->createDir(
135 | "bar/{$options['machineId']}", new Config()
136 | ));
137 | }
138 |
139 | /**
140 | * @dataProvider Provider
141 | */
142 | public function testDeleteDir(AdapterInterface $adapter, $config, $options)
143 | {
144 | $this->assertTrue($adapter->deleteDir("bar/{$options['machineId']}"));
145 | }
146 |
147 | /**
148 | * @dataProvider Provider
149 | */
150 | public function testSetVisibility(AdapterInterface $adapter, $config, $options)
151 | {
152 | $this->assertTrue($adapter->setVisibility(
153 | "foo/{$options['machineId']}/copy.md", 'private'
154 | ));
155 | }
156 |
157 | /**
158 | * @dataProvider Provider
159 | */
160 | public function testHas(AdapterInterface $adapter, $config, $options)
161 | {
162 | $this->assertTrue($adapter->has("foo/{$options['machineId']}/bar.md"));
163 | }
164 |
165 | /**
166 | * @dataProvider Provider
167 | */
168 | public function testRead(AdapterInterface $adapter, $config, $options)
169 | {
170 | $this->assertArrayHasKey(
171 | 'contents',
172 | $adapter->read("foo/{$options['machineId']}/bar.md")
173 | );
174 |
175 | $this->assertSame(
176 | file_get_contents($adapter->getTemporaryUrl(
177 | "foo/{$options['machineId']}/bar.md", Carbon::now()->addMinutes(5)
178 | )),
179 | $adapter->read("foo/{$options['machineId']}/bar.md")['contents']
180 | );
181 | }
182 |
183 | /**
184 | * @dataProvider Provider
185 | */
186 | public function testGetUrl(AdapterInterface $adapter, $config, $options)
187 | {
188 | $this->assertContains(
189 | "foo/{$options['machineId']}/bar.md",
190 | $adapter->getUrl("foo/{$options['machineId']}/bar.md")
191 | );
192 | }
193 |
194 | /**
195 | * @dataProvider Provider
196 | */
197 | public function testReadStream(AdapterInterface $adapter, $config, $options)
198 | {
199 | $this->assertArrayHasKey(
200 | 'stream',
201 | $adapter->readStream("foo/{$options['machineId']}/bar.md")
202 | );
203 |
204 | $this->assertSame(
205 | stream_get_contents(fopen($adapter->getTemporaryUrl(
206 | "foo/{$options['machineId']}/bar.md", Carbon::now()->addMinutes(5)
207 | ), 'rb', false)),
208 | stream_get_contents($adapter->readStream(
209 | "foo/{$options['machineId']}/bar.md")['stream']
210 | )
211 | );
212 | }
213 |
214 | /**
215 | * @dataProvider Provider
216 | */
217 | public function testListContents(AdapterInterface $adapter, $config, $options)
218 | {
219 | $this->assertArrayHasKey(
220 | 0,
221 | $adapter->listContents("foo/{$options['machineId']}")
222 | );
223 | }
224 |
225 | /**
226 | * @dataProvider Provider
227 | */
228 | public function testGetMetadata(AdapterInterface $adapter, $config, $options)
229 | {
230 | $this->assertArrayHasKey(
231 | 'ContentLength',
232 | $adapter->getMetadata("foo/{$options['machineId']}/bar.md")
233 | );
234 | }
235 |
236 | /**
237 | * @dataProvider Provider
238 | */
239 | public function testGetSize(AdapterInterface $adapter, $config, $options)
240 | {
241 | $this->assertArrayHasKey(
242 | 'size',
243 | $adapter->getSize("foo/{$options['machineId']}/bar.md")
244 | );
245 | }
246 |
247 | /**
248 | * @dataProvider Provider
249 | */
250 | public function testGetMimetype(AdapterInterface $adapter, $config, $options)
251 | {
252 | $this->assertNotSame(
253 | ['mimetype' => ''],
254 | $adapter->getMimetype("foo/{$options['machineId']}/bar.md")
255 | );
256 | }
257 |
258 | /**
259 | * @dataProvider Provider
260 | */
261 | public function testGetTimestamp(AdapterInterface $adapter, $config, $options)
262 | {
263 | $this->assertNotSame(
264 | ['timestamp' => 0],
265 | $adapter->getTimestamp("foo/{$options['machineId']}/bar.md")
266 | );
267 | }
268 |
269 | /**
270 | * @dataProvider Provider
271 | */
272 | public function testGetVisibility(AdapterInterface $adapter, $config, $options)
273 | {
274 | $this->assertSame(
275 | ['visibility' => 'private'],
276 | $adapter->getVisibility("foo/{$options['machineId']}/copy.md")
277 | );
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/src/Plugins/CDN.php:
--------------------------------------------------------------------------------
1 | signatureD($url, $key, $timestamp, $signName, $timeName);
39 | }
40 |
41 | /**
42 | * @param string $url
43 | * @param string $key
44 | * @param int $timestamp
45 | * @param string $random
46 | * @param string $signName
47 | *
48 | * @return string
49 | */
50 | public function signatureA($url, $key = null, $timestamp = null, $random = null, $signName = 'sign')
51 | {
52 | $key = $key ?: $this->getConfig()->get('cdn_key');
53 | $timestamp = $timestamp ?: time();
54 | $random = $random ?: sha1(uniqid('', true));
55 |
56 | $parsed = parse_url($url);
57 | $hash = md5(sprintf('%s-%s-%s-%s-%s', $parsed['path'], $timestamp, $random, 0, $key));
58 | $signature = sprintf('%s-%s-%s-%s', $timestamp, $random, 0, $hash);
59 | $query = http_build_query([$signName => $signature]);
60 | $separator = empty($parsed['query']) ? '?' : '&';
61 |
62 | return $url.$separator.$query;
63 | }
64 |
65 | /**
66 | * @param string $url
67 | * @param string $key
68 | * @param int $timestamp
69 | *
70 | * @return string
71 | */
72 | public function signatureB($url, $key = null, $timestamp = null)
73 | {
74 | $key = $key ?: $this->getConfig()->get('cdn_key');
75 | $timestamp = date('YmdHi', $timestamp ?: time());
76 |
77 | $parsed = parse_url($url);
78 | $hash = md5($key.$timestamp.$parsed['path']);
79 |
80 | return sprintf(
81 | '%s://%s/%s/%s%s',
82 | $parsed['scheme'], $parsed['host'], $timestamp, $hash, $parsed['path']
83 | );
84 | }
85 |
86 | /**
87 | * @param string $url
88 | * @param string $key
89 | * @param int $timestamp
90 | *
91 | * @return string
92 | */
93 | public function signatureC($url, $key = null, $timestamp = null)
94 | {
95 | $key = $key ?: $this->getConfig()->get('cdn_key');
96 | $timestamp = dechex($timestamp ?: time());
97 |
98 | $parsed = parse_url($url);
99 | $hash = md5($key.$parsed['path'].$timestamp);
100 |
101 | return sprintf(
102 | '%s://%s/%s/%s%s',
103 | $parsed['scheme'], $parsed['host'], $hash, $timestamp, $parsed['path']
104 | );
105 | }
106 |
107 | /**
108 | * @param string $url
109 | * @param string $key
110 | * @param int $timestamp
111 | * @param string $signName
112 | * @param string $timeName
113 | *
114 | * @return string
115 | */
116 | public function signatureD($url, $key = null, $timestamp = null, $signName = 'sign', $timeName = 't')
117 | {
118 | $key = $key ?: $this->getConfig()->get('cdn_key');
119 | $timestamp = dechex($timestamp ?: time());
120 |
121 | $parsed = parse_url($url);
122 | $signature = md5($key.$parsed['path'].$timestamp);
123 | $query = http_build_query([$signName => $signature, $timeName => $timestamp]);
124 | $separator = empty($parsed['query']) ? '?' : '&';
125 |
126 | return $url.$separator.$query;
127 | }
128 |
129 | /**
130 | * @param $url
131 | *
132 | * @return array
133 | */
134 | public function pushUrl($url)
135 | {
136 | $urls = is_array($url) ? $url : func_get_args();
137 |
138 | return $this->request($urls, 'urls', 'CdnUrlPusher');
139 | }
140 |
141 | /**
142 | * @param $url
143 | *
144 | * @return array
145 | */
146 | public function pushOverseaUrl($url)
147 | {
148 | $urls = is_array($url) ? $url : func_get_args();
149 |
150 | return $this->request($urls, 'urls', 'CdnOverseaPushser');
151 | }
152 |
153 | /**
154 | * @param $url
155 | *
156 | * @return array
157 | */
158 | public function pushUrlV2($url)
159 | {
160 | $urls = is_array($url) ? $url : func_get_args();
161 |
162 | return $this->request($urls, 'urls', 'CdnPusherV2');
163 | }
164 |
165 | /**
166 | * @param $url
167 | *
168 | * @return array
169 | */
170 | public function refreshUrl($url)
171 | {
172 | $urls = is_array($url) ? $url : func_get_args();
173 |
174 | return $this->request($urls, 'urls', 'RefreshCdnUrl');
175 | }
176 |
177 | /**
178 | * @param $url
179 | *
180 | * @return array
181 | */
182 | public function refreshOverseaUrl($url)
183 | {
184 | $urls = is_array($url) ? $url : func_get_args();
185 |
186 | return $this->request($urls, 'urls', 'RefreshCdnOverSeaUrl');
187 | }
188 |
189 | /**
190 | * @param $dir
191 | *
192 | * @return array
193 | */
194 | public function refreshDir($dir)
195 | {
196 | $dirs = is_array($dir) ? $dir : func_get_args();
197 |
198 | return $this->request($dirs, 'dirs', 'RefreshCdnDir');
199 | }
200 |
201 | /**
202 | * @param $dir
203 | *
204 | * @return array
205 | */
206 | public function refreshOverseaDir($dir)
207 | {
208 | $dirs = is_array($dir) ? $dir : func_get_args();
209 |
210 | return $this->request($dirs, 'dirs', 'RefreshCdnOverSeaDir');
211 | }
212 |
213 | /**
214 | * @param array $args
215 | * @param string $key
216 | * @param string $action
217 | *
218 | * @return array
219 | */
220 | protected function request(array $args, $key, $action)
221 | {
222 | $client = $this->getHttpClient();
223 |
224 | $response = $client->post('/v2/index.php', [
225 | 'form_params' => $this->buildFormParams($args, $key, $action),
226 | ]);
227 |
228 | $contents = $response->getBody()->getContents();
229 |
230 | return $this->normalize($contents);
231 | }
232 |
233 | /**
234 | * @return \GuzzleHttp\Client
235 | */
236 | protected function getHttpClient()
237 | {
238 | return new \GuzzleHttp\Client([
239 | 'base_uri' => 'https://cdn.api.qcloud.com',
240 | ]);
241 | }
242 |
243 | /**
244 | * @param array $values
245 | * @param string $key
246 | * @param string $action
247 | *
248 | * @return array
249 | */
250 | protected function buildFormParams(array $values, $key, $action)
251 | {
252 | $keys = array_map(function ($n) use ($key) {
253 | return sprintf("{$key}.%d", $n);
254 | }, range(0, count($values) - 1));
255 |
256 | $params = array_combine($keys, $values);
257 |
258 | $params = $this->addCommonParams($params, $action);
259 |
260 | return $this->addSignature($params);
261 | }
262 |
263 | /**
264 | * @param array $params
265 | * @param string $action
266 | *
267 | * @return array
268 | */
269 | protected function addCommonParams(array $params, $action)
270 | {
271 | return array_merge([
272 | 'Action' => $action,
273 | 'SecretId' => $this->getCredentials()['secretId'],
274 | 'Timestamp' => time(),
275 | 'Nonce' => rand(1, 65535),
276 | ], $params);
277 | }
278 |
279 | /**
280 | * @return array
281 | */
282 | protected function getCredentials()
283 | {
284 | return $this->getConfig()->get('credentials');
285 | }
286 |
287 | /**
288 | * @param array $params
289 | *
290 | * @return array
291 | */
292 | protected function addSignature(array $params)
293 | {
294 | $params['Signature'] = $this->getSignature($params);
295 |
296 | return $params;
297 | }
298 |
299 | /**
300 | * @param array $params
301 | *
302 | * @return string
303 | */
304 | protected function getSignature(array $params)
305 | {
306 | ksort($params);
307 |
308 | $srcStr = 'POSTcdn.api.qcloud.com/v2/index.php?'.urldecode(http_build_query($params));
309 |
310 | return base64_encode(hash_hmac('sha1', $srcStr, $this->getCredentials()['secretKey'], true));
311 | }
312 |
313 | /**
314 | * @param string $contents
315 | *
316 | * @throws \InvalidArgumentException if the JSON cannot be decoded.
317 | *
318 | * @return array
319 | */
320 | protected function normalize($contents)
321 | {
322 | return \GuzzleHttp\json_decode($contents, true);
323 | }
324 |
325 | /**
326 | * @return \League\Flysystem\Config
327 | */
328 | protected function getConfig()
329 | {
330 | return $this->filesystem->getConfig();
331 | }
332 | }
333 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
32 |
33 | ## Installation
34 |
35 | > Support Laravel/Lumen 5.x/6.x/7.x/8.x
36 |
37 | ```shell
38 | composer require "freyo/flysystem-qcloud-cos-v5:^2.0" -vvv
39 | ```
40 |
41 | ## Bootstrap
42 |
43 | ```php
44 | 'ap-guangzhou',
53 | 'credentials' => [
54 | 'appId' => 'your-app-id',
55 | 'secretId' => 'your-secret-id',
56 | 'secretKey' => 'your-secret-key',
57 | 'token' => null,
58 | ],
59 | 'timeout' => 60,
60 | 'connect_timeout' => 60,
61 | 'bucket' => 'your-bucket-name',
62 | 'cdn' => '',
63 | 'scheme' => 'https',
64 | 'read_from_cdn' => false,
65 | 'cdn_key' => '',
66 | 'encrypt' => false,
67 | ];
68 |
69 | $client = new Client($config);
70 | $adapter = new Adapter($client, $config);
71 | $filesystem = new Filesystem($adapter);
72 | ```
73 |
74 | ### API
75 |
76 | ```php
77 | bool $flysystem->write('file.md', 'contents');
78 |
79 | bool $flysystem->writeStream('file.md', fopen('path/to/your/local/file.jpg', 'r'));
80 |
81 | bool $flysystem->update('file.md', 'new contents');
82 |
83 | bool $flysystem->updateStram('file.md', fopen('path/to/your/local/file.jpg', 'r'));
84 |
85 | bool $flysystem->rename('foo.md', 'bar.md');
86 |
87 | bool $flysystem->copy('foo.md', 'foo2.md');
88 |
89 | bool $flysystem->delete('file.md');
90 |
91 | bool $flysystem->has('file.md');
92 |
93 | string|false $flysystem->read('file.md');
94 |
95 | array $flysystem->listContents();
96 |
97 | array $flysystem->getMetadata('file.md');
98 |
99 | int $flysystem->getSize('file.md');
100 |
101 | string $flysystem->getUrl('file.md');
102 |
103 | string $flysystem->getTemporaryUrl('file.md', date_create('2018-12-31 18:12:31'));
104 |
105 | string $flysystem->getMimetype('file.md');
106 |
107 | int $flysystem->getTimestamp('file.md');
108 |
109 | string $flysystem->getVisibility('file.md');
110 |
111 | bool $flysystem->setVisibility('file.md', 'public'); //or 'private', 'default'
112 | ```
113 |
114 | [Full API documentation.](http://flysystem.thephpleague.com/api/)
115 |
116 | ## Use in Laravel
117 |
118 | **Laravel 5.5+ uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider.**
119 |
120 | 1. Register the service provider in `config/app.php`:
121 |
122 | ```php
123 | 'providers' => [
124 | // ...
125 | Freyo\Flysystem\QcloudCOSv5\ServiceProvider::class,
126 | ]
127 | ```
128 |
129 | 2. Configure `config/filesystems.php`:
130 |
131 | ```php
132 | 'disks'=>[
133 | // ...
134 | 'cosv5' => [
135 | 'driver' => 'cosv5',
136 | 'region' => env('COSV5_REGION', 'ap-guangzhou'),
137 | 'credentials' => [
138 | 'appId' => env('COSV5_APP_ID'),
139 | 'secretId' => env('COSV5_SECRET_ID'),
140 | 'secretKey' => env('COSV5_SECRET_KEY'),
141 | 'token' => env('COSV5_TOKEN'),
142 | ],
143 | 'timeout' => env('COSV5_TIMEOUT', 60),
144 | 'connect_timeout' => env('COSV5_CONNECT_TIMEOUT', 60),
145 | 'bucket' => env('COSV5_BUCKET'),
146 | 'cdn' => env('COSV5_CDN'),
147 | 'scheme' => env('COSV5_SCHEME', 'https'),
148 | 'read_from_cdn' => env('COSV5_READ_FROM_CDN', false),
149 | 'cdn_key' => env('COSV5_CDN_KEY'),
150 | 'encrypt' => env('COSV5_ENCRYPT', false),
151 | ],
152 | ],
153 | ```
154 |
155 | 3. Configure `.env`:
156 |
157 | ```php
158 | COSV5_APP_ID=
159 | COSV5_SECRET_ID=
160 | COSV5_SECRET_KEY=
161 | COSV5_TOKEN=null
162 | COSV5_TIMEOUT=60
163 | COSV5_CONNECT_TIMEOUT=60
164 | COSV5_BUCKET=
165 | COSV5_REGION=ap-guangzhou
166 | COSV5_CDN=
167 | COSV5_SCHEME=https
168 | COSV5_READ_FROM_CDN=false
169 | COSV5_CDN_KEY=
170 | COSV5_ENCRYPT=false
171 | ```
172 |
173 | ## Use in Lumen
174 |
175 | 1. Add the following code to your `bootstrap/app.php`:
176 |
177 | ```php
178 | $app->singleton('filesystem', function ($app) {
179 | $app->alias('filesystem', Illuminate\Contracts\Filesystem\Factory::class);
180 | return $app->loadComponent(
181 | 'filesystems',
182 | Illuminate\Filesystem\FilesystemServiceProvider::class,
183 | 'filesystem'
184 | );
185 | });
186 | ```
187 |
188 | 2. And this:
189 |
190 | ```php
191 | $app->register(Freyo\Flysystem\QcloudCOSv5\ServiceProvider::class);
192 | ```
193 |
194 | 3. Configure `.env`:
195 |
196 | ```php
197 | COSV5_APP_ID=
198 | COSV5_SECRET_ID=
199 | COSV5_SECRET_KEY=
200 | COSV5_TOKEN=null
201 | COSV5_TIMEOUT=60
202 | COSV5_CONNECT_TIMEOUT=60
203 | COSV5_BUCKET=
204 | COSV5_REGION=ap-guangzhou
205 | COSV5_CDN=
206 | COSV5_SCHEME=https
207 | COSV5_READ_FROM_CDN=false
208 | COSV5_CDN_KEY=
209 | COSV5_ENCRYPT=false
210 | ```
211 |
212 | ### Usage
213 |
214 | ```php
215 | $disk = Storage::disk('cosv5');
216 |
217 | // create a file
218 | $disk->put('avatars/1', $fileContents);
219 |
220 | // check if a file exists
221 | $exists = $disk->has('file.jpg');
222 |
223 | // get timestamp
224 | $time = $disk->lastModified('file1.jpg');
225 |
226 | // copy a file
227 | $disk->copy('old/file1.jpg', 'new/file1.jpg');
228 |
229 | // move a file
230 | $disk->move('old/file1.jpg', 'new/file1.jpg');
231 |
232 | // get file contents
233 | $contents = $disk->read('folder/my_file.txt');
234 |
235 | // get url
236 | $url = $disk->url('new/file1.jpg');
237 | $temporaryUrl = $disk->temporaryUrl('new/file1.jpg', Carbon::now()->addMinutes(5));
238 |
239 | // create a file from remote(plugin)
240 | $disk->putRemoteFile('avatars/1', 'http://example.org/avatar.jpg');
241 | $disk->putRemoteFileAs('avatars/1', 'http://example.org/avatar.jpg', 'file1.jpg');
242 |
243 | // refresh cdn cache(plugin)
244 | $disk->cdn()->refreshUrl(['http://your-cdn-host/path/to/avatar.jpg']);
245 | $disk->cdn()->refreshDir(['http://your-cdn-host/path/to/']);
246 | $disk->cdn()->pushUrl(['http://your-cdn-host/path/to/avatar.jpg']);
247 | $disk->cdn()->refreshOverseaUrl(['http://your-cdn-host/path/to/avatar.jpg']);
248 | $disk->cdn()->refreshOverseaDir(['http://your-cdn-host/path/to/']);
249 | $disk->cdn()->pushOverseaUrl(['http://your-cdn-host/path/to/avatar.jpg']);
250 |
251 | // cdn url signature(plugin)
252 | $url = 'http://www.test.com/1.mp4';
253 | $disk->cdn()->signatureA($url, $key = null, $timestamp = null, $random = null, $signName = 'sign');
254 | $disk->cdn()->signatureB($url, $key = null, $timestamp = null);
255 | $disk->cdn()->signatureC($url, $key = null, $timestamp = null);
256 | $disk->cdn()->signatureD($url, $key = null, $timestamp = null, $signName = 'sign', $timeName = 't');
257 |
258 | // tencent captcha(plugin)
259 | $disk->tcaptcha($aid, $appSecretKey)->verify($ticket, $randStr, $userIP);
260 |
261 | // get federation token(plugin)
262 | $disk->getFederationToken($path = '*', $seconds = 7200, Closure $customPolicy = null, $name = 'cos')
263 | $disk->getFederationTokenV3($path = '*', $seconds = 7200, Closure $customPolicy = null, $name = 'cos')
264 |
265 | // tencent image process(plugin)
266 | $disk->cloudInfinite()->imageProcess($objectKey, array $picOperations);
267 | $disk->cloudInfinite()->contentRecognition($objectKey, array $contentRecognition);
268 | ```
269 |
270 | [Full API documentation.](https://laravel.com/api/8.x/Illuminate/Contracts/Filesystem/Cloud.html)
271 |
272 | ## Regions & Endpoints
273 |
274 | [Official Documentation](https://intl.cloud.tencent.com/document/product/436/6224?lang=en)
275 |
276 | ## License
277 |
278 | The MIT License (MIT). Please see [License File](LICENSE) for more information.
279 |
280 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Ffreyo%2Fflysystem-qcloud-cos-v5?ref=badge_large)
281 |
--------------------------------------------------------------------------------
/src/Adapter.php:
--------------------------------------------------------------------------------
1 | 'ap-shanghai',
34 | 'cn-sorth' => 'ap-guangzhou',
35 | 'cn-north' => 'ap-beijing-1',
36 | 'cn-south-2' => 'ap-guangzhou-2',
37 | 'cn-southwest' => 'ap-chengdu',
38 | 'sg' => 'ap-singapore',
39 | 'tj' => 'ap-beijing-1',
40 | 'bj' => 'ap-beijing',
41 | 'sh' => 'ap-shanghai',
42 | 'gz' => 'ap-guangzhou',
43 | 'cd' => 'ap-chengdu',
44 | 'sgp' => 'ap-singapore',
45 | ];
46 |
47 | /**
48 | * Adapter constructor.
49 | *
50 | * @param Client $client
51 | * @param array $config
52 | */
53 | public function __construct(Client $client, array $config)
54 | {
55 | $this->client = $client;
56 | $this->config = $config;
57 |
58 | $this->setPathPrefix($config['cdn']);
59 | }
60 |
61 | /**
62 | * @return string
63 | */
64 | public function getBucketWithAppId()
65 | {
66 | return $this->getBucket().'-'.$this->getAppId();
67 | }
68 |
69 | /**
70 | * @return string
71 | */
72 | public function getBucket()
73 | {
74 | return preg_replace(
75 | "/-{$this->getAppId()}$/",
76 | '',
77 | $this->config['bucket']
78 | );
79 | }
80 |
81 | /**
82 | * @return string
83 | */
84 | public function getAppId()
85 | {
86 | return $this->config['credentials']['appId'];
87 | }
88 |
89 | /**
90 | * @return string
91 | */
92 | public function getRegion()
93 | {
94 | return array_key_exists($this->config['region'], $this->regionMap)
95 | ? $this->regionMap[$this->config['region']] : $this->config['region'];
96 | }
97 |
98 | /**
99 | * @param $path
100 | *
101 | * @return string
102 | */
103 | public function getSourcePath($path)
104 | {
105 | return sprintf('%s.cos.%s.myqcloud.com/%s',
106 | $this->getBucketWithAppId(), $this->getRegion(), $path
107 | );
108 | }
109 |
110 | /**
111 | * @param $path
112 | *
113 | * @return string
114 | */
115 | public function getPicturePath($path)
116 | {
117 | return sprintf('%s.pic.%s.myqcloud.com/%s',
118 | $this->getBucketWithAppId(), $this->getRegion(), $path
119 | );
120 | }
121 |
122 | /**
123 | * @param string $path
124 | *
125 | * @return string
126 | */
127 | public function getUrl($path)
128 | {
129 | if ($this->config['cdn']) {
130 | return $this->applyPathPrefix($path);
131 | }
132 |
133 | $options = [
134 | 'Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http',
135 | ];
136 |
137 | /** @var \GuzzleHttp\Psr7\Uri $objectUrl */
138 | $objectUrl = $this->client->getObjectUrl(
139 | $this->getBucketWithAppId(), $path, "+30 minutes", $options
140 | );
141 |
142 | return (string) $objectUrl;
143 | }
144 |
145 | /**
146 | * @param string $path
147 | * @param \DateTimeInterface $expiration
148 | * @param array $options
149 | *
150 | * @return string
151 | */
152 | public function getTemporaryUrl($path, DateTimeInterface $expiration, array $options = [])
153 | {
154 | $options = array_merge(
155 | $options,
156 | ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']
157 | );
158 |
159 | /** @var \GuzzleHttp\Psr7\Uri $objectUrl */
160 | $objectUrl = $this->client->getObjectUrl(
161 | $this->getBucketWithAppId(), $path, $expiration->format('c'), $options
162 | );
163 |
164 | return (string) $objectUrl;
165 | }
166 |
167 | /**
168 | * @param string $path
169 | * @param string $contents
170 | * @param Config $config
171 | *
172 | * @return array|false
173 | */
174 | public function write($path, $contents, Config $config)
175 | {
176 | try {
177 | return $this->client->upload(
178 | $this->getBucketWithAppId(),
179 | $path,
180 | $contents,
181 | $this->prepareUploadConfig($config)
182 | );
183 | } catch (ServiceResponseException $e) {
184 | return false;
185 | }
186 | }
187 |
188 | /**
189 | * @param string $path
190 | * @param resource $resource
191 | * @param Config $config
192 | *
193 | * @return array|false
194 | */
195 | public function writeStream($path, $resource, Config $config)
196 | {
197 | try {
198 | return $this->client->upload(
199 | $this->getBucketWithAppId(),
200 | $path,
201 | stream_get_contents($resource, -1, 0),
202 | $this->prepareUploadConfig($config)
203 | );
204 | } catch (ServiceResponseException $e) {
205 | return false;
206 | }
207 | }
208 |
209 | /**
210 | * @param string $path
211 | * @param string $contents
212 | * @param Config $config
213 | *
214 | * @return array|false
215 | */
216 | public function update($path, $contents, Config $config)
217 | {
218 | return $this->write($path, $contents, $config);
219 | }
220 |
221 | /**
222 | * @param string $path
223 | * @param resource $resource
224 | * @param Config $config
225 | *
226 | * @return array|false
227 | */
228 | public function updateStream($path, $resource, Config $config)
229 | {
230 | return $this->writeStream($path, $resource, $config);
231 | }
232 |
233 | /**
234 | * @param string $path
235 | * @param string $newpath
236 | *
237 | * @return bool
238 | */
239 | public function rename($path, $newpath)
240 | {
241 | try {
242 | if ($result = $this->copy($path, $newpath)) {
243 | $this->delete($path);
244 | }
245 |
246 | return $result;
247 | } catch (ServiceResponseException $e) {
248 | return false;
249 | }
250 | }
251 |
252 | /**
253 | * @param string $path
254 | * @param string $newpath
255 | *
256 | * @return bool
257 | */
258 | public function copy($path, $newpath)
259 | {
260 | try {
261 | return (bool) $this->client->copyObject([
262 | 'Bucket' => $this->getBucketWithAppId(),
263 | 'Key' => $newpath,
264 | 'CopySource' => $this->getSourcePath($path),
265 | ]);
266 | } catch (ServiceResponseException $e) {
267 | return false;
268 | }
269 | }
270 |
271 | /**
272 | * @param string $path
273 | *
274 | * @return bool
275 | */
276 | public function delete($path)
277 | {
278 | try {
279 | return (bool) $this->client->deleteObject([
280 | 'Bucket' => $this->getBucketWithAppId(),
281 | 'Key' => $path,
282 | ]);
283 | } catch (ServiceResponseException $e) {
284 | return false;
285 | }
286 | }
287 |
288 | /**
289 | * @param string $dirname
290 | *
291 | * @return bool
292 | */
293 | public function deleteDir($dirname)
294 | {
295 | try {
296 | return (bool) $this->client->deleteObject([
297 | 'Bucket' => $this->getBucketWithAppId(),
298 | 'Key' => $dirname.'/',
299 | ]);
300 | } catch (ServiceResponseException $e) {
301 | return false;
302 | }
303 | }
304 |
305 | /**
306 | * @param string $dirname
307 | * @param Config $config
308 | *
309 | * @return array|false
310 | */
311 | public function createDir($dirname, Config $config)
312 | {
313 | try {
314 | return $this->client->putObject([
315 | 'Bucket' => $this->getBucketWithAppId(),
316 | 'Key' => $dirname.'/',
317 | 'Body' => '',
318 | ]);
319 | } catch (ServiceResponseException $e) {
320 | return false;
321 | }
322 | }
323 |
324 | /**
325 | * @param string $path
326 | * @param string $visibility
327 | *
328 | * @return bool
329 | */
330 | public function setVisibility($path, $visibility)
331 | {
332 | try {
333 | return (bool) $this->client->putObjectAcl([
334 | 'Bucket' => $this->getBucketWithAppId(),
335 | 'Key' => $path,
336 | 'ACL' => $this->normalizeVisibility($visibility),
337 | ]);
338 | } catch (ServiceResponseException $e) {
339 | return false;
340 | }
341 | }
342 |
343 | /**
344 | * @param string $path
345 | *
346 | * @return bool
347 | */
348 | public function has($path)
349 | {
350 | try {
351 | return (bool) $this->getMetadata($path);
352 | } catch (ServiceResponseException $e) {
353 | return false;
354 | }
355 | }
356 |
357 | /**
358 | * @param string $path
359 | *
360 | * @return array|bool
361 | */
362 | public function read($path)
363 | {
364 | try {
365 | $response = $this->forceReadFromCDN()
366 | ? $this->readFromCDN($path)
367 | : $this->readFromSource($path);
368 |
369 | return ['contents' => (string) $response];
370 | } catch (ServiceResponseException $e) {
371 | return false;
372 | }
373 | }
374 |
375 | /**
376 | * @return bool
377 | */
378 | protected function forceReadFromCDN()
379 | {
380 | return $this->config['cdn']
381 | && isset($this->config['read_from_cdn'])
382 | && $this->config['read_from_cdn'];
383 | }
384 |
385 | /**
386 | * @param $path
387 | *
388 | * @return string
389 | */
390 | protected function readFromCDN($path)
391 | {
392 | return $this->getHttpClient()
393 | ->get($this->applyPathPrefix($path))
394 | ->getBody()
395 | ->getContents();
396 | }
397 |
398 | /**
399 | * @param $path
400 | *
401 | * @return string
402 | */
403 | protected function readFromSource($path)
404 | {
405 | try {
406 | $response = $this->client->getObject([
407 | 'Bucket' => $this->getBucketWithAppId(),
408 | 'Key' => $path,
409 | ]);
410 |
411 | return $response['Body'];
412 | } catch (ServiceResponseException $e) {
413 | return false;
414 | }
415 | }
416 |
417 | /**
418 | * @return \GuzzleHttp\Client
419 | */
420 | public function getHttpClient()
421 | {
422 | return new \GuzzleHttp\Client([
423 | 'timeout' => $this->config['timeout'],
424 | 'connect_timeout' => $this->config['connect_timeout'],
425 | ]);
426 | }
427 |
428 | /**
429 | * @param string $path
430 | *
431 | * @return array|bool
432 | */
433 | public function readStream($path)
434 | {
435 | try {
436 | $temporaryUrl = $this->getTemporaryUrl($path, Carbon::now()->addMinutes(5));
437 |
438 | $stream = $this->getHttpClient()
439 | ->get($temporaryUrl, ['stream' => true])
440 | ->getBody()
441 | ->detach();
442 |
443 | return ['stream' => $stream];
444 | } catch (ServiceResponseException $e) {
445 | return false;
446 | }
447 | }
448 |
449 | /**
450 | * @param string $directory
451 | * @param bool $recursive
452 | *
453 | * @return array|bool
454 | */
455 | public function listContents($directory = '', $recursive = false)
456 | {
457 | $list = [];
458 |
459 | $marker = '';
460 | while (true) {
461 | $response = $this->listObjects($directory, $recursive, $marker);
462 |
463 | foreach ((array) $response['Contents'] as $content) {
464 | $list[] = $this->normalizeFileInfo($content);
465 | }
466 |
467 | if (!$response['IsTruncated']) {
468 | break;
469 | }
470 | $marker = $response['NextMarker'] ?: '';
471 | }
472 |
473 | return $list;
474 | }
475 |
476 | /**
477 | * @param string $path
478 | *
479 | * @return array|bool
480 | */
481 | public function getMetadata($path)
482 | {
483 | try {
484 | return $this->client->headObject([
485 | 'Bucket' => $this->getBucketWithAppId(),
486 | 'Key' => $path,
487 | ]);
488 | } catch (ServiceResponseException $e) {
489 | return false;
490 | }
491 | }
492 |
493 | /**
494 | * @param string $path
495 | *
496 | * @return array|bool
497 | */
498 | public function getSize($path)
499 | {
500 | $meta = $this->getMetadata($path);
501 |
502 | return isset($meta['ContentLength'])
503 | ? ['size' => $meta['ContentLength']] : false;
504 | }
505 |
506 | /**
507 | * @param string $path
508 | *
509 | * @return array|bool
510 | */
511 | public function getMimetype($path)
512 | {
513 | $meta = $this->getMetadata($path);
514 |
515 | return isset($meta['ContentType'])
516 | ? ['mimetype' => $meta['ContentType']] : false;
517 | }
518 |
519 | /**
520 | * @param string $path
521 | *
522 | * @return array|bool
523 | */
524 | public function getTimestamp($path)
525 | {
526 | $meta = $this->getMetadata($path);
527 |
528 | return isset($meta['LastModified'])
529 | ? ['timestamp' => strtotime($meta['LastModified'])] : false;
530 | }
531 |
532 | /**
533 | * @param string $path
534 | *
535 | * @return array|bool
536 | */
537 | public function getVisibility($path)
538 | {
539 | try {
540 | $meta = $this->client->getObjectAcl([
541 | 'Bucket' => $this->getBucketWithAppId(),
542 | 'Key' => $path,
543 | ]);
544 |
545 | foreach ($meta['Grants'] as $grant) {
546 | if (isset($grant['Grantee']['URI'])
547 | && $grant['Permission'] === 'READ'
548 | && strpos($grant['Grantee']['URI'], 'global/AllUsers') !== false
549 | ) {
550 | return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
551 | }
552 | }
553 |
554 | return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
555 | } catch (ServiceResponseException $e) {
556 | return false;
557 | }
558 | }
559 |
560 | /**
561 | * @param array $content
562 | *
563 | * @return array
564 | */
565 | private function normalizeFileInfo(array $content)
566 | {
567 | $path = pathinfo($content['Key']);
568 |
569 | return [
570 | 'type' => substr($content['Key'], -1) === '/' ? 'dir' : 'file',
571 | 'path' => $content['Key'],
572 | 'timestamp' => Carbon::parse($content['LastModified'])->getTimestamp(),
573 | 'size' => (int) $content['Size'],
574 | 'dirname' => $path['dirname'] === '.' ? '' : (string) $path['dirname'],
575 | 'basename' => (string) $path['basename'],
576 | 'extension' => isset($path['extension']) ? $path['extension'] : '',
577 | 'filename' => (string) $path['filename'],
578 | ];
579 | }
580 |
581 | /**
582 | * @param string $directory
583 | * @param bool $recursive
584 | * @param string $marker max return 1000 record, if record greater than 1000
585 | * you should set the next marker to get the full list
586 | *
587 | * @return \GuzzleHttp\Command\Result|array
588 | */
589 | private function listObjects($directory = '', $recursive = false, $marker = '')
590 | {
591 | try {
592 | return $this->client->listObjects([
593 | 'Bucket' => $this->getBucketWithAppId(),
594 | 'Prefix' => ((string) $directory === '') ? '' : ($directory.'/'),
595 | 'Delimiter' => $recursive ? '' : '/',
596 | 'Marker' => $marker,
597 | 'MaxKeys' => 1000,
598 | ]);
599 | } catch (ServiceResponseException $e) {
600 | return [
601 | 'Contents' => [],
602 | 'IsTruncated' => false,
603 | 'NextMarker' => '',
604 | ];
605 | }
606 | }
607 |
608 | /**
609 | * @param Config $config
610 | *
611 | * @return array
612 | */
613 | private function prepareUploadConfig(Config $config)
614 | {
615 | $options = [];
616 |
617 | if (isset($this->config['encrypt']) && $this->config['encrypt']) {
618 | $options['ServerSideEncryption'] = 'AES256';
619 | }
620 |
621 | if ($config->has('params')) {
622 | $options = array_merge($options, $config->get('params'));
623 | }
624 |
625 | if ($config->has('visibility')) {
626 | $options['ACL'] = $this->normalizeVisibility($config->get('visibility'));
627 | }
628 |
629 | return $options;
630 | }
631 |
632 | /**
633 | * @param $visibility
634 | *
635 | * @return string
636 | */
637 | private function normalizeVisibility($visibility)
638 | {
639 | switch ($visibility) {
640 | case AdapterInterface::VISIBILITY_PUBLIC:
641 | $visibility = 'public-read';
642 | break;
643 | }
644 |
645 | return $visibility;
646 | }
647 |
648 | /**
649 | * @return Client
650 | */
651 | public function getCOSClient()
652 | {
653 | return $this->client;
654 | }
655 |
656 | /**
657 | * @param $method
658 | * @param $url
659 | *
660 | * @return string
661 | */
662 | public function getAuthorization($method, $url)
663 | {
664 | $cosRequest = new \GuzzleHttp\Psr7\Request($method, $url);
665 |
666 | $signature = new \Qcloud\Cos\Signature(
667 | $this->config['credentials']['secretId'],
668 | $this->config['credentials']['secretKey']
669 | );
670 |
671 | return $signature->createAuthorization($cosRequest);
672 | }
673 | }
674 |
--------------------------------------------------------------------------------