Cos.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace crmeb\services\upload\storage;
  12. use crmeb\basic\BaseUpload;
  13. use crmeb\exceptions\UploadException;
  14. use GuzzleHttp\Psr7\Utils;
  15. use think\exception\ValidateException;
  16. use QCloud\COSSTS\Sts;
  17. use crmeb\services\upload\extend\cos\Client as CrmebClient;
  18. /**
  19. * 腾讯云COS文件上传
  20. * Class COS
  21. * @package crmeb\services\upload\storage
  22. */
  23. class Cos extends BaseUpload
  24. {
  25. /**
  26. * 应用id
  27. * @var string
  28. */
  29. protected $appid;
  30. /**
  31. * accessKey
  32. * @var mixed
  33. */
  34. protected $accessKey;
  35. /**
  36. * secretKey
  37. * @var mixed
  38. */
  39. protected $secretKey;
  40. /**
  41. * 句柄
  42. * @var CrmebClient
  43. */
  44. protected $handle;
  45. /**
  46. * 空间域名 Domain
  47. * @var mixed
  48. */
  49. protected $uploadUrl;
  50. /**
  51. * 存储空间名称 公开空间
  52. * @var mixed
  53. */
  54. protected $storageName;
  55. /**
  56. * COS使用 所属地域
  57. * @var mixed|null
  58. */
  59. protected $storageRegion;
  60. /**
  61. * 水印位置
  62. * @var string[]
  63. */
  64. protected $position = [
  65. '1' => 'northwest',//:左上
  66. '2' => 'north',//:中上
  67. '3' => 'northeast',//:右上
  68. '4' => 'west',//:左中
  69. '5' => 'center',//:中部
  70. '6' => 'east',//:右中
  71. '7' => 'southwest',//:左下
  72. '8' => 'south',//:中下
  73. '9' => 'southeast',//:右下
  74. ];
  75. /**
  76. * 初始化
  77. * @param array $config
  78. * @return mixed|void
  79. */
  80. public function initialize(array $config)
  81. {
  82. parent::initialize($config);
  83. $this->accessKey = $config['accessKey'] ?? null;
  84. $this->secretKey = $config['secretKey'] ?? null;
  85. $this->appid = $config['appid'] ?? null;
  86. $this->uploadUrl = $this->checkUploadUrl($config['uploadUrl'] ?? '');
  87. $this->storageName = $config['storageName'] ?? null;
  88. $this->storageRegion = $config['storageRegion'] ?? null;
  89. $this->waterConfig['watermark_text_font'] = 'simfang仿宋.ttf';
  90. }
  91. /**
  92. *
  93. * @return CrmebClient
  94. * @author 等风来
  95. * @email 136327134@qq.com
  96. * @date 2022/9/29
  97. */
  98. protected function app()
  99. {
  100. $this->handle = new CrmebClient([
  101. 'accessKey' => $this->accessKey,
  102. 'secretKey' => $this->secretKey,
  103. 'region' => $this->storageRegion ?: 'ap-chengdu',
  104. 'bucket' => $this->storageName,
  105. 'appid' => $this->appid,
  106. 'uploadUrl' => $this->uploadUrl
  107. ]);
  108. return $this->handle;
  109. }
  110. /**
  111. * 上传文件
  112. * @param string|null $file
  113. * @param bool $isStream 是否为流上传
  114. * @param string|null $fileContent 流内容
  115. * @return array|bool|\StdClass
  116. */
  117. protected function upload(string $file = null, bool $isStream = false, string $fileContent = null)
  118. {
  119. if (!$isStream) {
  120. $fileHandle = app()->request->file($file);
  121. if (!$fileHandle) {
  122. return $this->setError('Upload file does not exist');
  123. }
  124. if ($this->validate) {
  125. try {
  126. $error = [
  127. $file . '.filesize' => 'Upload filesize error',
  128. $file . '.fileExt' => 'Upload fileExt error',
  129. $file . '.fileMime' => 'Upload fileMine error'
  130. ];
  131. validate([$file => $this->validate], $error)->check([$file => $fileHandle]);
  132. } catch (ValidateException $e) {
  133. return $this->setError($e->getMessage());
  134. }
  135. }
  136. $key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
  137. $body = fopen($fileHandle->getRealPath(), 'rb');
  138. $body = (string)Utils::streamFor($body);
  139. } else {
  140. $key = $file;
  141. $body = $fileContent;
  142. }
  143. try {
  144. $key = $this->getUploadPath($key);
  145. $this->fileInfo->uploadInfo = $this->app()->putObject($key, $body);
  146. $this->fileInfo->filePath = $this->uploadUrl . '/' . $key;
  147. $this->fileInfo->realName = isset($fileHandle) ? $fileHandle->getOriginalName() : $key;
  148. $this->fileInfo->fileName = $key;
  149. $this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
  150. $this->authThumb && $this->thumb($this->fileInfo->filePath);
  151. return $this->fileInfo;
  152. } catch (UploadException $e) {
  153. return $this->setError($e->getMessage());
  154. }
  155. }
  156. /**
  157. * 文件流上传
  158. * @param string $fileContent
  159. * @param string|null $key
  160. * @return array|bool|mixed|\StdClass
  161. */
  162. public function stream(string $fileContent, string $key = null)
  163. {
  164. if (!$key) {
  165. $key = $this->saveFileName();
  166. }
  167. return $this->upload($key, true, $fileContent);
  168. }
  169. /**
  170. * 文件上传
  171. * @param string $file
  172. * @return array|bool|mixed|\StdClass
  173. */
  174. public function move(string $file = 'file')
  175. {
  176. return $this->upload($file);
  177. }
  178. /**
  179. * 缩略图
  180. * @param string $filePath
  181. * @param string $type
  182. * @return mixed|string[]
  183. */
  184. public function thumb(string $filePath = '', string $type = 'all')
  185. {
  186. $filePath = $this->getFilePath($filePath);
  187. $data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
  188. $this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
  189. if ($filePath) {
  190. $config = $this->thumbConfig;
  191. foreach ($this->thumb as $v) {
  192. if ($type == 'all' || $type == $v) {
  193. $height = 'thumb_' . $v . '_height';
  194. $width = 'thumb_' . $v . '_width';
  195. if (isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
  196. $key = 'filePath' . ucfirst($v);
  197. $this->fileInfo->$key = $filePath . '?imageMogr2/thumbnail/' . $config[$width] . 'x' . $config[$height];
  198. $this->fileInfo->$key = $this->water($this->fileInfo->$key);
  199. $data[$v] = $this->fileInfo->$key;
  200. }
  201. }
  202. }
  203. }
  204. return $data;
  205. }
  206. /**
  207. * 水印
  208. * @param string $filePath
  209. * @return mixed|string
  210. */
  211. public function water(string $filePath = '')
  212. {
  213. $filePath = $this->getFilePath($filePath);
  214. $waterConfig = $this->waterConfig;
  215. $waterPath = $filePath;
  216. if ($waterConfig['image_watermark_status'] && $filePath) {
  217. if (strpos($filePath, '?') === false) {
  218. $filePath .= '?watermark';
  219. } else {
  220. $filePath .= '&watermark';
  221. }
  222. switch ($waterConfig['watermark_type']) {
  223. case 1://图片
  224. if (!$waterConfig['watermark_image']) {
  225. throw new ValidateException('请先配置水印图片');
  226. }
  227. $waterPath = $filePath .= '/1/image/' . base64_encode($waterConfig['watermark_image']) . '/gravity/' . ($this->position[$waterConfig['watermark_position']] ?? 'northwest') . '/blogo/1/dx/' . $waterConfig['watermark_x'] . '/dy/' . $waterConfig['watermark_y'];
  228. break;
  229. case 2://文字
  230. if (!$waterConfig['watermark_text']) {
  231. throw new ValidateException('请先配置水印文字');
  232. }
  233. $waterPath = $filePath .= '/2/text/' . base64_encode($waterConfig['watermark_text']) . '/font/' . base64_encode($waterConfig['watermark_text_font']) . '/fill/' . base64_encode($waterConfig['watermark_text_color']) . '/fontsize/' . $waterConfig['watermark_text_size'] . '/gravity/' . ($this->position[$waterConfig['watermark_position']] ?? 'northwest') . '/dx/' . $waterConfig['watermark_x'] . '/dy/' . $waterConfig['watermark_y'];
  234. break;
  235. }
  236. }
  237. return $waterPath;
  238. }
  239. /**
  240. * 获取视频封面图
  241. * @param string $filePath
  242. * @param string $type
  243. * @param int $time
  244. * @return array
  245. */
  246. public function videoCoverImage(string $filePath = '', string $type = 'all', int $time = 1)
  247. {
  248. $data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
  249. $this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
  250. if ($filePath) {
  251. //?ci-process=snapshot
  252. foreach ($this->thumb as $v) {
  253. if ($type == 'all' || $type == $v) {
  254. $height = 600;
  255. $width = 400;
  256. $key = 'filePath' . ucfirst($v);
  257. $this->fileInfo->$key = $filePath . 'ci-process=snapshot,t_' . $time . ',f_jpg,w_' . $width . ',h_' . $height . ',m_fast';
  258. }
  259. }
  260. }
  261. return $data;
  262. }
  263. /**
  264. * 删除资源
  265. * @param $key
  266. * @return mixed
  267. */
  268. public function delete(string $filePath)
  269. {
  270. try {
  271. return $this->app()->deleteObject($this->storageName, $filePath);
  272. } catch (\Exception $e) {
  273. return $this->setError($e->getMessage());
  274. }
  275. }
  276. /**
  277. * 生成签名
  278. * @return array|mixed
  279. * @throws \Exception
  280. */
  281. public function getTempKeys()
  282. {
  283. $sts = new Sts();
  284. $config = [
  285. 'url' => 'https://sts.tencentcloudapi.com/',
  286. 'domain' => 'sts.tencentcloudapi.com',
  287. 'proxy' => '',
  288. 'secretId' => $this->accessKey, // 固定密钥
  289. 'secretKey' => $this->secretKey, // 固定密钥
  290. 'bucket' => $this->storageName, // 换成你的 bucket
  291. 'region' => $this->storageRegion, // 换成 bucket 所在园区
  292. 'durationSeconds' => 1800, // 密钥有效期
  293. 'allowPrefix' => '*', // 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径,例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
  294. // 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
  295. 'allowActions' => [
  296. // 简单上传
  297. 'name/cos:PutObject',
  298. 'name/cos:PostObject',
  299. // 分片上传
  300. 'name/cos:InitiateMultipartUpload',
  301. 'name/cos:ListMultipartUploads',
  302. 'name/cos:ListParts',
  303. 'name/cos:UploadPart',
  304. 'name/cos:CompleteMultipartUpload'
  305. ]
  306. ];
  307. // 获取临时密钥,计算签名
  308. $result = $sts->getTempKeys($config);
  309. $result['url'] = $this->uploadUrl . '/';
  310. $result['type'] = 'COS';
  311. $result['bucket'] = $this->storageName;
  312. $result['region'] = $this->storageRegion;
  313. return $result;
  314. }
  315. /**
  316. * 计算临时密钥用的签名
  317. * @param $opt
  318. * @param $key
  319. * @param $method
  320. * @param $config
  321. * @return string
  322. */
  323. public function getSignature($opt, $key, $method, $config)
  324. {
  325. $formatString = $method . $config['domain'] . '/?' . $this->json2str($opt, 1);
  326. $sign = hash_hmac('sha1', $formatString, $key);
  327. $sign = base64_encode($this->_hex2bin($sign));
  328. return $sign;
  329. }
  330. public function _hex2bin($data)
  331. {
  332. $len = strlen($data);
  333. return pack("H" . $len, $data);
  334. }
  335. // obj 转 query string
  336. public function json2str($obj, $notEncode = false)
  337. {
  338. ksort($obj);
  339. $arr = array();
  340. if (!is_array($obj)) {
  341. return $this->setError($obj . " must be a array");
  342. }
  343. foreach ($obj as $key => $val) {
  344. array_push($arr, $key . '=' . ($notEncode ? $val : rawurlencode($val)));
  345. }
  346. return join('&', $arr);
  347. }
  348. // v2接口的key首字母小写,v3改成大写,此处做了向下兼容
  349. public function backwardCompat($result)
  350. {
  351. if (!is_array($result)) {
  352. return $this->setError($result . " must be a array");
  353. }
  354. $compat = array();
  355. foreach ($result as $key => $value) {
  356. if (is_array($value)) {
  357. $compat[lcfirst($key)] = $this->backwardCompat($value);
  358. } elseif ($key == 'Token') {
  359. $compat['sessionToken'] = $value;
  360. } else {
  361. $compat[lcfirst($key)] = $value;
  362. }
  363. }
  364. return $compat;
  365. }
  366. /**
  367. * 桶列表
  368. * @param string|null $region
  369. * @param bool $line
  370. * @param bool $shared
  371. * @return array|mixed
  372. * "Name" => "record-1254950941"
  373. * "Location" => "ap-chengdu"
  374. * "CreationDate" => "2019-05-16T08:33:29Z"
  375. * "BucketType" => "cos"
  376. */
  377. public function listbuckets(string $region = null, bool $line = false, bool $shared = false)
  378. {
  379. try {
  380. $res = $this->app()->listBuckets();
  381. $bucket = [];
  382. if (isset($res['Buckets'][0]['Bucket'])) {
  383. if (isset($res['Buckets'][0]['Bucket']['Name'])) {
  384. $bucket[] = $res['Buckets'][0]['Bucket'];
  385. } else {
  386. $bucket = $res['Buckets'][0]['Bucket'];
  387. }
  388. } else if (isset($res['Buckets']['Bucket'])) {
  389. $bucket = $res['Buckets']['Bucket'];
  390. }
  391. return $bucket;
  392. } catch (\Throwable $e) {
  393. return [];
  394. }
  395. }
  396. /**
  397. * 创建桶
  398. * @param string $name
  399. * @param string $region
  400. * @param string $acl public-read=公共独写
  401. * @return bool|mixed
  402. */
  403. public function createBucket(string $name, string $region = '', string $acl = 'public-read')
  404. {
  405. $regionData = $this->getRegion();
  406. $regionData = array_column($regionData, 'value');
  407. if (!in_array($region, $regionData)) {
  408. return $this->setError('COS:无效的区域!');
  409. }
  410. $this->storageRegion = $region;
  411. $app = $this->app();
  412. //检测桶
  413. try {
  414. $res1 = $app->headBucket($name, $region);
  415. if ($res1 !== true) {
  416. return $this->setError('COS:设置的桶名已经存在!');
  417. }
  418. } catch (\Throwable $e) {
  419. //桶不存在返回404
  420. if (strstr('404', $e->getMessage())) {
  421. return $this->setError('COS:' . $e->getMessage());
  422. }
  423. }
  424. //创建桶
  425. try {
  426. $res = $app->createBucket($name, $region, $acl);
  427. } catch (\Throwable $e) {
  428. if (strstr('[curl] 6', $e->getMessage())) {
  429. return $this->setError('COS:无效的区域!!');
  430. } else if (strstr('Access Denied.', $e->getMessage())) {
  431. return $this->setError('COS:无权访问');
  432. }
  433. return $this->setError('COS:' . $e->getMessage());
  434. }
  435. return $res;
  436. }
  437. /**
  438. * 删除桶
  439. * @param string $name
  440. * @return bool|mixed
  441. */
  442. public function deleteBucket(string $name)
  443. {
  444. try {
  445. $this->app()->deleteBucket($name);
  446. } catch (\Throwable $e) {
  447. return $this->setError($e->getMessage());
  448. }
  449. return true;
  450. }
  451. /**
  452. * @param string $name
  453. * @param string|null $region
  454. * @return array|object
  455. */
  456. public function getDomian(string $name, string $region = null)
  457. {
  458. try {
  459. $res = $this->app()->getBucketDomain($name, $region);
  460. if (isset($res['DomainRule'])) {
  461. $domainRules[] = $res['DomainRule']['Name'];
  462. } else {
  463. $domainRules = array_column($res['DomainRules'], 'Name');
  464. }
  465. return $domainRules;
  466. } catch (\Throwable $e) {
  467. }
  468. return [];
  469. }
  470. /**
  471. * 绑定域名
  472. * @param string $name
  473. * @param string $domain
  474. * @param string|null $region
  475. * @return bool|mixed
  476. */
  477. public function bindDomian(string $name, string $domain, string $region = null)
  478. {
  479. $this->storageRegion = $region;
  480. $parseDomin = parse_url($domain);
  481. try {
  482. $this->app()->putBucketDomain($name, $region, [
  483. 'Name' => $parseDomin['host'],
  484. 'Status' => 'ENABLED',
  485. 'Type' => 'REST',
  486. 'ForcedReplacement' => 'CNAME'
  487. ]);
  488. return true;
  489. } catch (\Throwable $e) {
  490. if ($message = $this->setMessage($e->getMessage())) {
  491. return $this->setError($message);
  492. }
  493. return $this->setError($e->getMessage());
  494. }
  495. return false;
  496. }
  497. /**
  498. * 处理
  499. * @param string $message
  500. * @return string
  501. */
  502. protected function setMessage(string $message)
  503. {
  504. $data = [
  505. 'The specified bucket does not exist.' => '指定的存储桶不存在。',
  506. 'Please add CNAME/TXT record to DNS then try again later. Please allow up to 10 mins before your DNS takes effect.' => '请将CNAME记录添加到DNS,然后稍后重试。在DNS生效前,请等待最多10分钟。'
  507. ];
  508. $msg = $data[$message] ?? '';
  509. if ($msg) {
  510. return $msg;
  511. }
  512. foreach ($data as $item) {
  513. if (strstr($message, $item)) {
  514. return $item;
  515. }
  516. }
  517. return '';
  518. }
  519. /**
  520. * 设置跨域
  521. * @param string $name
  522. * @param string $region
  523. * @return bool
  524. */
  525. public function setBucketCors(string $name, string $region)
  526. {
  527. $this->storageRegion = $region;
  528. try {
  529. $this->app()->putBucketCors($name, [
  530. 'AllowedHeader' => ['*'],
  531. 'AllowedMethod' => ['PUT', 'GET', 'POST', 'DELETE', 'HEAD'],
  532. 'AllowedOrigin' => ['*'],
  533. 'ExposeHeader' => ['ETag', 'Content-Length', 'x-cos-request-id'],
  534. 'MaxAgeSeconds' => 12
  535. ], $region);
  536. } catch (\Throwable $e) {
  537. return $this->setError($e->getMessage());
  538. }
  539. return true;
  540. }
  541. /**
  542. * 地域
  543. * @return mixed|\string[][]
  544. */
  545. public function getRegion()
  546. {
  547. return [
  548. [
  549. 'value' => 'ap-chengdu',
  550. 'label' => '成都'
  551. ],
  552. [
  553. 'value' => 'ap-shanghai',
  554. 'label' => '上海'
  555. ],
  556. [
  557. 'value' => 'ap-nanjing',
  558. 'label' => '南京'
  559. ],
  560. [
  561. 'value' => 'ap-beijing',
  562. 'label' => '北京'
  563. ],
  564. [
  565. 'value' => 'ap-chongqing',
  566. 'label' => '重庆'
  567. ],
  568. [
  569. 'value' => 'ap-shenzhen-fsi',
  570. 'label' => '深圳金融'
  571. ],
  572. [
  573. 'value' => 'ap-shanghai-fsi',
  574. 'label' => '上海金融'
  575. ],
  576. [
  577. 'value' => 'ap-beijing-fsi',
  578. 'label' => '北京金融'
  579. ],
  580. [
  581. 'value' => 'ap-hongkong',
  582. 'label' => '中国香港'
  583. ],
  584. [
  585. 'value' => 'ap-singapore',
  586. 'label' => '新加坡'
  587. ],
  588. [
  589. 'value' => 'ap-mumbai',
  590. 'label' => '孟买'
  591. ],
  592. [
  593. 'value' => 'ap-jakarta',
  594. 'label' => '雅加达'
  595. ],
  596. [
  597. 'value' => 'ap-seoul',
  598. 'label' => '首尔'
  599. ],
  600. [
  601. 'value' => 'ap-bangkok',
  602. 'label' => '曼谷'
  603. ],
  604. [
  605. 'value' => 'ap-tokyo',
  606. 'label' => '东京'
  607. ],
  608. [
  609. 'value' => 'na-siliconvalley',
  610. 'label' => '硅谷(美西)'
  611. ],
  612. [
  613. 'value' => 'na-ashburn',
  614. 'label' => '弗吉尼亚(美东)'
  615. ],
  616. [
  617. 'value' => 'na-toronto',
  618. 'label' => '多伦多'
  619. ],
  620. [
  621. 'value' => 'sa-saopaulo',
  622. 'label' => '圣保罗'
  623. ],
  624. [
  625. 'value' => 'eu-frankfurt',
  626. 'label' => '法兰克福'
  627. ],
  628. [
  629. 'value' => 'eu-moscow',
  630. 'label' => '莫斯科'
  631. ]
  632. ];
  633. }
  634. }