├── .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 |
2 |

3 | 4 |

5 |

Flysystem Adapter for Tencent Cloud Object Storage

6 |

7 | 8 | Software License 9 | 10 | 11 | Build Status 12 | 13 | 14 | Coverage Status 15 | 16 | 17 | Quality Score 18 | 19 | 20 | Packagist Version 21 | 22 | 23 | Total Downloads 24 | 25 |

26 |

27 | 28 | FOSSA Status 29 | 30 |

31 |
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 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffreyo%2Fflysystem-qcloud-cos-v5.svg?type=large)](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 | --------------------------------------------------------------------------------