Client.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. <?php
  2. /**
  3. * +----------------------------------------------------------------------
  4. * | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  5. * +----------------------------------------------------------------------
  6. * | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
  7. * +----------------------------------------------------------------------
  8. * | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  9. * +----------------------------------------------------------------------
  10. * | Author: CRMEB Team <admin@crmeb.com>
  11. * +----------------------------------------------------------------------
  12. */
  13. namespace crmeb\services\upload\extend\obs;
  14. use crmeb\exceptions\UploadException;
  15. use crmeb\services\upload\BaseClient;
  16. use crmeb\services\upload\extend\cos\XML;
  17. /**
  18. * 华为云上传
  19. * Class Client
  20. * @author 等风来
  21. * @email 136327134@qq.com
  22. * @date 2023/5/18
  23. * @package crmeb\services\upload\extend\obs
  24. */
  25. class Client extends BaseClient
  26. {
  27. const HEADER_PREFIX = 'x-obs-';
  28. const INTEREST_HEADER_KEY_LIST = ['content-type', 'content-md5', 'date'];
  29. const ALTERNATIVE_DATE_HEADER = 'x-obs-date';
  30. const ALLOWED_RESOURCE_PARAMTER_NAMES = [
  31. 'acl',
  32. 'policy',
  33. 'torrent',
  34. 'logging',
  35. 'location',
  36. 'storageinfo',
  37. 'quota',
  38. 'storagepolicy',
  39. 'requestpayment',
  40. 'versions',
  41. 'versioning',
  42. 'versionid',
  43. 'uploads',
  44. 'uploadid',
  45. 'partnumber',
  46. 'website',
  47. 'notification',
  48. 'lifecycle',
  49. 'deletebucket',
  50. 'delete',
  51. 'cors',
  52. 'restore',
  53. 'tagging',
  54. 'response-content-type',
  55. 'response-content-language',
  56. 'response-expires',
  57. 'response-cache-control',
  58. 'response-content-disposition',
  59. 'response-content-encoding',
  60. 'x-image-process',
  61. 'backtosource',
  62. 'storageclass',
  63. 'replication',
  64. 'append',
  65. 'position',
  66. 'x-oss-process'
  67. ];
  68. //桶acl
  69. const OBS_ACL = [
  70. [
  71. 'value' => 'public-read',
  72. 'label' => '公共读(推荐)',
  73. ],
  74. [
  75. 'value' => 'public-read-write',
  76. 'label' => '公共读写',
  77. ],
  78. ];
  79. //默认acl
  80. const DEFAULT_OBS_ACL = 'public-read';
  81. protected $isCname = false;
  82. protected $pathStyle;
  83. /**
  84. * @var
  85. */
  86. protected $accessKeyId;
  87. /**
  88. * @var
  89. */
  90. protected $secretKey;
  91. /**
  92. * 桶名
  93. * @var string
  94. */
  95. protected $bucketName;
  96. /**
  97. * 地区
  98. * @var string
  99. */
  100. protected $region;
  101. /**
  102. * @var mixed|string
  103. */
  104. protected $uploadUrl;
  105. /**
  106. * @var string
  107. */
  108. protected $baseUrl = 'obs.cn-north-1.myhuaweicloud.com';
  109. protected $type = 'hw';
  110. /**
  111. * Client constructor.
  112. * @param array $config
  113. */
  114. public function __construct(array $config = [])
  115. {
  116. $this->accessKeyId = $config['accessKey'] ?? '';
  117. $this->secretKey = $config['secretKey'] ?? '';
  118. $this->bucketName = $config['bucket'] ?? '';
  119. $this->region = $config['region'] ?? 'ap-chengdu';
  120. $this->uploadUrl = $config['uploadUrl'] ?? '';
  121. $this->type = $config['type'] ?? 'hw';
  122. }
  123. /**
  124. * 检测
  125. * @author 等风来
  126. * @email 136327134@qq.com
  127. * @date 2023/5/18
  128. */
  129. protected function checkOptions()
  130. {
  131. if (!$this->bucketName) {
  132. throw new UploadException('请传入桶名');
  133. }
  134. if (!$this->region) {
  135. throw new UploadException('请传入所属地域');
  136. }
  137. if (!$this->accessKeyId) {
  138. throw new UploadException('请传入SecretId');
  139. }
  140. if (!$this->secretKey) {
  141. throw new UploadException('请传入SecretKey');
  142. }
  143. return $this;
  144. }
  145. /**
  146. * 上传图片
  147. * @param string $key
  148. * @param $body
  149. * @return mixed
  150. * @author 等风来
  151. * @email 136327134@qq.com
  152. * @date 2023/5/18
  153. */
  154. public function putObject(string $key, $body, string $contentType = 'image/jpeg')
  155. {
  156. $header = [
  157. 'Host' => $this->getRequestUrl($this->bucketName, $this->region),
  158. 'Content-Type' => $contentType,
  159. 'Content-Length' => strlen($body),
  160. ];
  161. $res = $this->checkOptions()->request('https://' . $header['Host'] . '/' . $key, 'PUT', [
  162. 'bucket' => $this->bucketName,
  163. 'body' => $body
  164. ], $header);
  165. return $this->response($res);
  166. }
  167. /**
  168. * 删除上传对象
  169. * @param string $key
  170. * @return mixed
  171. * @author 等风来
  172. * @email 136327134@qq.com
  173. * @date 2023/5/18
  174. */
  175. public function deleteObject(string $key)
  176. {
  177. $header = [
  178. 'Host' => $this->getRequestUrl($this->bucketName, $this->region),
  179. ];
  180. $res = $this->request('https://' . $header['Host'] . '/' . $key, 'DELETE', [
  181. 'bucket' => $this->bucketName
  182. ], $header);
  183. return $this->response($res);
  184. }
  185. /**
  186. * 获取桶
  187. * @return false|string
  188. * @author 等风来
  189. * @email 136327134@qq.com
  190. * @date 2023/5/16
  191. */
  192. public function listBuckets()
  193. {
  194. $header = [
  195. 'Host' => $this->getRequestUrl('', $this->region),
  196. ];
  197. $res = $this->request('https://' . $header['Host'] . '/', 'GET', [], []);
  198. return $this->response($res);
  199. }
  200. public function headBucket(string $bucket, string $region)
  201. {
  202. $header = [
  203. 'Host' => $this->getRequestUrl($bucket, $region),
  204. ];
  205. $res = $this->request('https://' . $header['Host'] . '/', 'HEAD', [], []);
  206. return $this->response($res);
  207. }
  208. /**
  209. * 设置桶的策略
  210. * @param string $bucket
  211. * @param string $region
  212. * @param array $data
  213. * @return mixed
  214. *
  215. * @date 2023/06/08
  216. * @author yyw
  217. */
  218. public function putPolicy(string $bucket, string $region, array $data)
  219. {
  220. $header = [
  221. 'Host' => $this->getRequestUrl($bucket, $region),
  222. "Content-Type" => "application/json"
  223. ];
  224. $res = $this->request('https://' . $header['Host'] . '/?policy', 'PUT', [
  225. 'bucket' => $bucket,
  226. 'json' => $data
  227. ], $header);
  228. return $this->response($res);
  229. }
  230. /**
  231. * 创建桶
  232. * @param string $bucket
  233. * @param string $region
  234. * @param string $acl
  235. * @return mixed
  236. * @author 等风来
  237. * @email 136327134@qq.com
  238. * @date 2023/5/18
  239. */
  240. public function createBucket(string $bucket, string $region, string $acl = self::DEFAULT_OBS_ACL)
  241. {
  242. $header = [
  243. 'x-obs-acl' => $acl,
  244. 'Host' => $this->getRequestUrl($bucket, $region),
  245. "Content-Type" => "application/xml"
  246. ];
  247. $xml = "<CreateBucketConfiguration><Location>{$region}</Location></CreateBucketConfiguration>";
  248. $res = $this->request('https://' . $header['Host'] . '/', 'PUT', [
  249. 'bucket' => $bucket,
  250. 'body' => $xml
  251. ], $header);
  252. return $this->response($res);
  253. }
  254. /**
  255. * 删除桶
  256. * @param string $bucket
  257. * @param string $region
  258. * @return mixed
  259. * @author 等风来
  260. * @email 136327134@qq.com
  261. * @date 2023/5/18
  262. */
  263. public function deleteBucket(string $bucket, string $region)
  264. {
  265. $header = [
  266. 'Host' => $this->getRequestUrl($bucket, $region),
  267. ];
  268. $res = $this->request('https://' . $header['Host'] . '/', 'DELETE', [
  269. 'bucket' => $bucket
  270. ], $header);
  271. return $this->response($res);
  272. }
  273. /**
  274. * 获取桶的自定义域名
  275. * @param string $bucket
  276. * @param string $region
  277. * @author 等风来
  278. * @email 136327134@qq.com
  279. * @date 2023/5/18
  280. */
  281. public function getBucketDomain(string $bucket, string $region)
  282. {
  283. $header = [
  284. 'Host' => $this->getRequestUrl($bucket, $region),
  285. ];
  286. $res = $this->request('https://' . $header['Host'] . '/?customdomain', 'GET', [
  287. 'bucket' => $bucket
  288. ], $header);
  289. return $this->response($res);
  290. }
  291. /**
  292. * 设置桶的自定义域名
  293. * @param string $bucket
  294. * @param string $region
  295. * @param array $data
  296. * @return mixed
  297. * @author 等风来
  298. * @email 136327134@qq.com
  299. * @date 2023/5/18
  300. */
  301. public function putBucketDomain(string $bucket, string $region, array $data = [])
  302. {
  303. $header = [
  304. 'Host' => $this->getRequestUrl($bucket, $region),
  305. ];
  306. $res = $this->request('https://' . $header['Host'] . '/?customdomain=' . $data['domainname'], 'PUT', [
  307. 'bucket' => $bucket
  308. ], $header);
  309. return $this->response($res);
  310. }
  311. /**
  312. * 设置跨域
  313. * @return bool
  314. * @author 等风来
  315. * @email 136327134@qq.com
  316. * @date 2023/5/18
  317. */
  318. public function putBucketCors(string $bucket, string $region, array $data = [])
  319. {
  320. $xml = $this->xmlBuild($data, 'CORSConfiguration', 'CORSRule');
  321. $header = [
  322. 'Host' => $this->getRequestUrl($bucket, $region),
  323. 'Content-Type' => 'application/xml',
  324. 'Content-Length' => strlen($xml),
  325. 'Content-MD5' => base64_encode(md5($xml, true))
  326. ];
  327. $res = $this->request('https://' . $header['Host'] . '/?cors', 'PUT', [
  328. 'bucket' => $bucket,
  329. 'body' => $xml
  330. ], $header);
  331. return $this->response($res);
  332. }
  333. /**
  334. * 删除跨域
  335. * @param string $bucket
  336. * @param string $region
  337. * @return mixed
  338. *
  339. * @date 2023/06/08
  340. * @author yyw
  341. */
  342. public function deleteBucketCors(string $bucket, string $region)
  343. {
  344. $header = [
  345. 'Host' => $this->getRequestUrl($bucket, $region),
  346. ];
  347. $res = $this->request('https://' . $header['Host'] . '/?cors', 'DELETE', [
  348. 'bucket' => $bucket,
  349. ], $header);
  350. return $this->response($res);
  351. }
  352. /**
  353. * @param $res
  354. * @return mixed
  355. * @author 等风来
  356. * @email 136327134@qq.com
  357. * @date 2023/5/18
  358. */
  359. protected function response($res)
  360. {
  361. if (!empty($res['Code']) && !empty($res['Message'])) {
  362. throw new UploadException($res['Message']);
  363. }
  364. return $res;
  365. }
  366. /**
  367. * 获取请求域名
  368. * @param string $bucket
  369. * @param string $region
  370. * @return string
  371. *
  372. * @date 2023/06/08
  373. * @author yyw
  374. */
  375. protected function getRequestUrl(string $bucket = '', string $region = '')
  376. {
  377. if ($this->type == 'hw') {
  378. $url = '.myhuaweicloud.com'; // 华为
  379. } else {
  380. $url = '.ctyun.cn'; // 天翼
  381. }
  382. if ($bucket) {
  383. return $bucket . '.obs.' . $region . $url;
  384. } else {
  385. return 'obs.' . $region . $url;
  386. }
  387. }
  388. /**
  389. * 地域名称
  390. * @return \string[][]
  391. * @author 等风来
  392. * @email 136327134@qq.com
  393. * @date 2023/5/17
  394. */
  395. public function getRegion()
  396. {
  397. return [
  398. [
  399. 'value' => 'cn-north-1',
  400. 'label' => '华北-北京一',
  401. ],
  402. // [
  403. // 'value' => 'cn-north-4',
  404. // 'label' => '华北-北京四',
  405. // ],
  406. [
  407. 'value' => 'cn-north-9',
  408. 'label' => '华北-乌兰察布一',
  409. ],
  410. [
  411. 'value' => 'cn-east-2',
  412. 'label' => '华东-上海二',
  413. ],
  414. [
  415. 'value' => 'cn-east-3',
  416. 'label' => '华东-上海一',
  417. ],
  418. [
  419. 'value' => 'cn-south-1',
  420. 'label' => '华南-广州',
  421. ],
  422. [
  423. 'value' => 'ap-southeast-1',
  424. 'label' => '中国-香港',
  425. ],
  426. [
  427. 'value' => 'cn-south-4',
  428. 'label' => '华南-广州-友好用户环境',
  429. ],
  430. [
  431. 'value' => 'cn-southwest-2',
  432. 'label' => '西南-贵阳一',
  433. ],
  434. [
  435. 'value' => 'la-north-2',
  436. 'label' => '拉美-墨西哥城二',
  437. ],
  438. [
  439. 'value' => 'na-mexico-1',
  440. 'label' => '拉美-墨西哥城一',
  441. ],
  442. [
  443. 'value' => 'sa-brazil-1',
  444. 'label' => '拉美-圣保罗一',
  445. ],
  446. [
  447. 'value' => 'la-south-2',
  448. 'label' => '拉美-圣地亚哥',
  449. ],
  450. [
  451. 'value' => 'tr-west-1',
  452. 'label' => '土耳其-伊斯坦布尔',
  453. ],
  454. [
  455. 'value' => 'ap-southeast-2',
  456. 'label' => '亚太-曼谷',
  457. ],
  458. [
  459. 'value' => 'ap-southeast-3',
  460. 'label' => '亚太-新加坡',
  461. ],
  462. [
  463. 'value' => 'af-south-1',
  464. 'label' => '非洲-约翰内斯堡',
  465. ]
  466. ];
  467. }
  468. /**
  469. * 设置桶名
  470. * @param string $bucketName
  471. * @return $this
  472. * @author 等风来
  473. * @email 136327134@qq.com
  474. * @date 2023/5/16
  475. */
  476. public function setBucketName(string $bucketName)
  477. {
  478. $this->bucketName = $bucketName;
  479. return $this;
  480. }
  481. /**
  482. * 获取签名
  483. * @param array $result
  484. * @return array
  485. * @author 等风来
  486. * @email 136327134@qq.com
  487. * @date 2023/5/17
  488. */
  489. protected function getSign(array $result)
  490. {
  491. $result['headers']['Date'] = gmdate('D, d M Y H:i:s \G\M\T');
  492. $canonicalstring = $this->makeCanonicalstring($result['method'], $result['headers'], $result['pathArgs'], $result['dnsParam'], $result['uriParam']);
  493. $result['cannonicalRequest'] = $canonicalstring;
  494. $signature = base64_encode(hash_hmac('sha1', $canonicalstring, $this->secretKey, true));
  495. $authorization = 'OBS ' . $this->accessKeyId . ':' . $signature;
  496. $result['headers']['Authorization'] = $authorization;
  497. return $result;
  498. }
  499. /**
  500. * 处理签名数据
  501. * @param $method
  502. * @param $headers
  503. * @param $pathArgs
  504. * @param $bucketName
  505. * @param $objectKey
  506. * @param null $expires
  507. * @return string
  508. * @author 等风来
  509. * @email 136327134@qq.com
  510. * @date 2023/5/17
  511. */
  512. public function makeCanonicalstring($method, $headers, $pathArgs, $bucketName, $objectKey, $expires = null)
  513. {
  514. $buffer = [];
  515. $buffer[] = $method;
  516. $buffer[] = "\n";
  517. $interestHeaders = [];
  518. foreach ($headers as $key => $value) {
  519. $key = strtolower($key);
  520. if (in_array($key, self::INTEREST_HEADER_KEY_LIST) || strpos($key, self::HEADER_PREFIX) === 0) {
  521. $interestHeaders[$key] = $value;
  522. }
  523. }
  524. if (array_key_exists(self::ALTERNATIVE_DATE_HEADER, $interestHeaders)) {
  525. $interestHeaders['date'] = '';
  526. }
  527. if ($expires !== null) {
  528. $interestHeaders['date'] = strval($expires);
  529. }
  530. if (!array_key_exists('content-type', $interestHeaders)) {
  531. $interestHeaders['content-type'] = '';
  532. }
  533. if (!array_key_exists('content-md5', $interestHeaders)) {
  534. $interestHeaders['content-md5'] = '';
  535. }
  536. ksort($interestHeaders);
  537. foreach ($interestHeaders as $key => $value) {
  538. if (strpos($key, self::HEADER_PREFIX) === 0) {
  539. $buffer[] = $key . ':' . $value;
  540. } else {
  541. $buffer[] = $value;
  542. }
  543. $buffer[] = "\n";
  544. }
  545. $uri = '';
  546. $bucketName = $this->isCname ? $headers['Host'] : $bucketName;
  547. if ($bucketName) {
  548. $uri .= '/';
  549. $uri .= $bucketName;
  550. if (!$this->pathStyle) {
  551. $uri .= '/';
  552. }
  553. }
  554. if ($objectKey) {
  555. if (!($pos = strripos($uri, '/')) || strlen($uri) - 1 !== $pos) {
  556. $uri .= '/';
  557. }
  558. $uri .= $objectKey;
  559. }
  560. $buffer[] = $uri === '' ? '/' : $uri;
  561. if (!empty($pathArgs)) {
  562. ksort($pathArgs);
  563. $_pathArgs = [];
  564. foreach ($pathArgs as $key => $value) {
  565. if (in_array(strtolower($key), self::ALLOWED_RESOURCE_PARAMTER_NAMES) || strpos($key, self::HEADER_PREFIX) === 0) {
  566. $_pathArgs[] = $value === null || $value === '' ? $key : $key . '=' . urldecode($value);
  567. }
  568. }
  569. if (!empty($_pathArgs)) {
  570. $buffer[] = '?';
  571. $buffer[] = implode('&', $_pathArgs);
  572. }
  573. }
  574. return implode('', $buffer);
  575. }
  576. /**
  577. * 发起请求
  578. * @param string $url
  579. * @param string $method
  580. * @param array $data
  581. * @param array $clientHeader
  582. * @param int $timeout
  583. * @return false|string
  584. * @author 等风来
  585. * @email 136327134@qq.com
  586. * @date 2023/5/16
  587. */
  588. public function request(string $url, string $method, array $data = [], array $clientHeader = [], int $timeout = 10)
  589. {
  590. $method = strtoupper($method);
  591. $urlAttr = pathinfo($url);
  592. $urlParse = parse_url($urlAttr['dirname'] ?? '');
  593. $uriParam = '';
  594. if ($urlAttr['dirname'] !== 'https:') {
  595. if (isset($urlParse['path'])) {
  596. $uriParam .= substr($urlParse['path'], 1) . '/';
  597. }
  598. if (isset($urlAttr['basename'])) {
  599. $uriParam .= $urlAttr['basename'];
  600. }
  601. }
  602. $result = $this->getSign([
  603. 'method' => $method,
  604. 'headers' => $clientHeader,
  605. 'pathArgs' => '',
  606. 'dnsParam' => $data['bucket'] ?? '',
  607. 'uriParam' => $uriParam,
  608. ]);
  609. return $this->requestClient($url, $method, $data, $result['headers'], $timeout);
  610. }
  611. /**
  612. * 组合成xml
  613. * @param array $data
  614. * @param string $root
  615. * @param string $itemKey
  616. * @return string
  617. * @author 等风来
  618. * @email 136327134@qq.com
  619. * @date 2022/10/17
  620. */
  621. protected function xmlBuild(array $xmlAttr, string $root = 'xml', string $itemKey = 'item')
  622. {
  623. $xml = '<' . $root . '>';
  624. $xml .= '<' . $itemKey . '>';
  625. foreach ($xmlAttr as $kk => $vv) {
  626. if (is_array($vv)) {
  627. foreach ($vv as $v) {
  628. $xml .= '<' . $kk . '>' . $v . '</' . $kk . '>';
  629. }
  630. } else {
  631. $xml .= '<' . $kk . '>' . $vv . '</' . $kk . '>';
  632. }
  633. }
  634. $xml .= '</' . $itemKey . '>';
  635. $xml .= '</' . $root . '>';
  636. return $xml;
  637. }
  638. }