Upyun.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <?php
  2. namespace fast\service;
  3. use Exception;
  4. use think\Config;
  5. /**
  6. * 又拍云上传管理
  7. */
  8. class Upyun
  9. {
  10. const VERSION = '2.0';
  11. /* {{{ */
  12. const ED_AUTO = 'v0.api.upyun.com';
  13. const ED_TELECOM = 'v1.api.upyun.com';
  14. const ED_CNC = 'v2.api.upyun.com';
  15. const ED_CTT = 'v3.api.upyun.com';
  16. const CONTENT_TYPE = 'Content-Type';
  17. const CONTENT_MD5 = 'Content-MD5';
  18. const CONTENT_SECRET = 'Content-Secret';
  19. // 缩略图
  20. const X_GMKERL_THUMBNAIL = 'x-gmkerl-thumbnail';
  21. const X_GMKERL_TYPE = 'x-gmkerl-type';
  22. const X_GMKERL_VALUE = 'x-gmkerl-value';
  23. const X_GMKERL_QUALITY = 'x­gmkerl-quality';
  24. const X_GMKERL_UNSHARP = 'x­gmkerl-unsharp';
  25. private static $_instance;
  26. /* }}} */
  27. /**
  28. * @deprecated
  29. */
  30. private $_content_md5 = NULL;
  31. /**
  32. * @deprecated
  33. */
  34. private $_file_secret = NULL;
  35. /**
  36. * @deprecated
  37. */
  38. private $_file_infos = NULL;
  39. /**
  40. * @var string: UPYUN 请求唯一id, 出现错误时, 可以将该id报告给 UPYUN,进行调试
  41. */
  42. private $x_request_id;
  43. /**
  44. * 初始化 UpYun 存储接口
  45. */
  46. public function __construct($options = [])
  47. {
  48. if ($config = Config::get('service.upyun'))
  49. {
  50. $this->config = array_merge($this->config, $config);
  51. }
  52. $this->config = array_merge($this->config, is_array($options) ? $options : []);
  53. }
  54. /* }}} */
  55. /**
  56. * 获取当前SDK版本号
  57. */
  58. public function version()
  59. {
  60. return self::VERSION;
  61. }
  62. /**
  63. * 创建目录
  64. * @param $path 路径
  65. * @param $auto_mkdir 是否自动创建父级目录,最多10层次
  66. *
  67. * @return void
  68. */
  69. public function makeDir($path, $auto_mkdir = false)
  70. {/* {{{ */
  71. $headers = array('Folder' => 'true');
  72. if ($auto_mkdir)
  73. $headers['Mkdir'] = 'true';
  74. return $this->_do_request('PUT', $path, $headers);
  75. }
  76. /* }}} */
  77. /**
  78. * 删除目录和文件
  79. * @param string $path 路径
  80. *
  81. * @return boolean
  82. */
  83. public function delete($path)
  84. {/* {{{ */
  85. return $this->_do_request('DELETE', $path);
  86. }
  87. /* }}} */
  88. /**
  89. * 上传文件
  90. * @param string $path 存储路径或文件
  91. * @param mixed $file 需要上传的文件,可以是文件流或者文件内容
  92. * @param boolean $auto_mkdir 自动创建目录
  93. * @param array $opts 可选参数
  94. */
  95. public function upload($path, $file = NULL, $auto_mkdir = True, $opts = NULL)
  96. {
  97. return $this->writeFile($path, $file, $auto_mkdir, $opts);
  98. }
  99. public function writeFile($path, $file = NULL, $auto_mkdir = True, $opts = NULL)
  100. {/* {{{ */
  101. if (is_null($file))
  102. $file = ROOT_PATH . 'public/' . $path;
  103. if (is_null($opts))
  104. $opts = array();
  105. if (!is_null($this->_content_md5) || !is_null($this->_file_secret))
  106. {
  107. //if (!is_null($this->_content_md5)) array_push($opts, self::CONTENT_MD5 . ": {$this->_content_md5}");
  108. //if (!is_null($this->_file_secret)) array_push($opts, self::CONTENT_SECRET . ": {$this->_file_secret}");
  109. if (!is_null($this->_content_md5))
  110. $opts[self::CONTENT_MD5] = $this->_content_md5;
  111. if (!is_null($this->_file_secret))
  112. $opts[self::CONTENT_SECRET] = $this->_file_secret;
  113. }
  114. // 如果设置了缩略版本或者缩略图类型,则添加默认压缩质量和锐化参数
  115. //if (isset($opts[self::X_GMKERL_THUMBNAIL]) || isset($opts[self::X_GMKERL_TYPE])) {
  116. // if (!isset($opts[self::X_GMKERL_QUALITY])) $opts[self::X_GMKERL_QUALITY] = 95;
  117. // if (!isset($opts[self::X_GMKERL_UNSHARP])) $opts[self::X_GMKERL_UNSHARP] = 'true';
  118. //}
  119. if ($auto_mkdir === True)
  120. $opts['Mkdir'] = 'true';
  121. $this->_file_infos = $this->_do_request('PUT', $path, $opts, $file);
  122. return $this->_file_infos;
  123. }
  124. /* }}} */
  125. /**
  126. * 下载文件
  127. * @param string $path 文件路径
  128. * @param mixed $file_handle
  129. *
  130. * @return mixed
  131. */
  132. public function readFile($path, $file_handle = NULL)
  133. {/* {{{ */
  134. return $this->_do_request('GET', $path, NULL, NULL, $file_handle);
  135. }
  136. /* }}} */
  137. /**
  138. * 获取目录文件列表
  139. *
  140. * @param string $path 查询路径
  141. *
  142. * @return mixed
  143. */
  144. public function getList($path = '/')
  145. {/* {{{ */
  146. $rsp = $this->_do_request('GET', $path);
  147. $list = array();
  148. if ($rsp)
  149. {
  150. $rsp = explode("\n", $rsp);
  151. foreach ($rsp as $item)
  152. {
  153. @list($name, $type, $size, $time) = explode("\t", trim($item));
  154. if (!empty($time))
  155. {
  156. $type = $type == 'N' ? 'file' : 'folder';
  157. }
  158. $item = array(
  159. 'name' => $name,
  160. 'type' => $type,
  161. 'size' => intval($size),
  162. 'time' => intval($time),
  163. );
  164. array_push($list, $item);
  165. }
  166. }
  167. return $list;
  168. }
  169. /* }}} */
  170. /**
  171. * @deprecated
  172. * @param string $path 目录路径
  173. * @return mixed
  174. */
  175. public function getFolderUsage($path = '/')
  176. {/* {{{ */
  177. $rsp = $this->_do_request('GET', '/?usage');
  178. return floatval($rsp);
  179. }
  180. /* }}} */
  181. /**
  182. * 获取文件、目录信息
  183. *
  184. * @param string $path 路径
  185. *
  186. * @return mixed
  187. */
  188. public function getFileInfo($path)
  189. {/* {{{ */
  190. $rsp = $this->_do_request('HEAD', $path);
  191. return $rsp;
  192. }
  193. /* }}} */
  194. /**
  195. * 连接签名方法
  196. * @param $method 请求方式 {GET, POST, PUT, DELETE}
  197. * return 签名字符串
  198. */
  199. private function sign($method, $uri, $date, $length)
  200. {/* {{{ */
  201. //$uri = urlencode($uri);
  202. $sign = "{$method}&{$uri}&{$date}&{$length}&{$this->config['password']}";
  203. return 'UpYun ' . $this->config['username'] . ':' . md5($sign);
  204. }
  205. /* }}} */
  206. /**
  207. * HTTP REQUEST 封装
  208. * @param string $method HTTP REQUEST方法,包括PUT、POST、GET、OPTIONS、DELETE
  209. * @param string $path 除Bucketname之外的请求路径,包括get参数
  210. * @param array $headers 请求需要的特殊HTTP HEADERS
  211. * @param array $body 需要POST发送的数据
  212. *
  213. * @return mixed
  214. */
  215. protected function _do_request($method, $path, $headers = NULL, $body = NULL, $file_handle = NULL)
  216. {/* {{{ */
  217. $uri = "/{$this->config['bucket']}{$path}";
  218. $ch = curl_init("http://{$this->config['endpoint']}{$uri}");
  219. $_headers = array('Expect:');
  220. if (!is_null($headers) && is_array($headers))
  221. {
  222. foreach ($headers as $k => $v)
  223. {
  224. array_push($_headers, "{$k}: {$v}");
  225. }
  226. }
  227. $length = 0;
  228. $date = gmdate('D, d M Y H:i:s \G\M\T');
  229. if (!is_null($body))
  230. {
  231. if (!is_resource($body) && file_exists($body))
  232. {
  233. $body = fopen($body, "rb");
  234. }
  235. if (is_resource($body))
  236. {
  237. fseek($body, 0, SEEK_END);
  238. $length = ftell($body);
  239. fseek($body, 0);
  240. array_push($_headers, "Content-Length: {$length}");
  241. curl_setopt($ch, CURLOPT_INFILE, $body);
  242. curl_setopt($ch, CURLOPT_INFILESIZE, $length);
  243. }
  244. else
  245. {
  246. $length = @strlen($body);
  247. array_push($_headers, "Content-Length: {$length}");
  248. curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  249. }
  250. }
  251. else
  252. {
  253. array_push($_headers, "Content-Length: {$length}");
  254. }
  255. array_push($_headers, "Authorization: {$this->sign($method, $uri, $date, $length)}");
  256. array_push($_headers, "Date: {$date}");
  257. curl_setopt($ch, CURLOPT_HTTPHEADER, $_headers);
  258. curl_setopt($ch, CURLOPT_TIMEOUT, $this->config['timeout']);
  259. curl_setopt($ch, CURLOPT_HEADER, 1);
  260. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  261. //curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
  262. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
  263. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
  264. if ($method == 'PUT' || $method == 'POST')
  265. {
  266. curl_setopt($ch, CURLOPT_POST, 1);
  267. }
  268. else
  269. {
  270. curl_setopt($ch, CURLOPT_POST, 0);
  271. }
  272. if ($method == 'GET' && is_resource($file_handle))
  273. {
  274. curl_setopt($ch, CURLOPT_HEADER, 0);
  275. curl_setopt($ch, CURLOPT_FILE, $file_handle);
  276. }
  277. if ($method == 'HEAD')
  278. {
  279. curl_setopt($ch, CURLOPT_NOBODY, true);
  280. }
  281. $response = curl_exec($ch);
  282. $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  283. if ($http_code == 0)
  284. throw new Exception('Connection Failed', $http_code);
  285. curl_close($ch);
  286. $header_string = '';
  287. $body = '';
  288. if ($method == 'GET' && is_resource($file_handle))
  289. {
  290. $header_string = '';
  291. $body = $response;
  292. }
  293. else
  294. {
  295. list($header_string, $body) = explode("\r\n\r\n", $response, 2);
  296. }
  297. $this->setXRequestId($header_string);
  298. if ($http_code == 200)
  299. {
  300. if ($method == 'GET' && is_null($file_handle))
  301. {
  302. return $body;
  303. }
  304. else
  305. {
  306. $data = $this->_getHeadersData($header_string);
  307. return count($data) > 0 ? $data : true;
  308. }
  309. }
  310. else
  311. {
  312. $message = $this->_getErrorMessage($header_string);
  313. if (is_null($message) && $method == 'GET' && is_resource($file_handle))
  314. {
  315. $message = 'File Not Found';
  316. }
  317. switch ($http_code)
  318. {
  319. case 401:
  320. throw new Exception($message, $http_code);
  321. break;
  322. case 403:
  323. throw new Exception($message, $http_code);
  324. break;
  325. case 404:
  326. throw new Exception($message, $http_code);
  327. break;
  328. case 406:
  329. throw new Exception($message, $http_code);
  330. break;
  331. case 503:
  332. throw new Exception($message, $http_code);
  333. break;
  334. default:
  335. throw new Exception($message, $http_code);
  336. }
  337. }
  338. }
  339. /* }}} */
  340. /**
  341. * 处理HTTP HEADERS中返回的自定义数据
  342. *
  343. * @param string $text header字符串
  344. *
  345. * @return array
  346. */
  347. private function _getHeadersData($text)
  348. {/* {{{ */
  349. $headers = explode("\r\n", $text);
  350. $items = array();
  351. foreach ($headers as $header)
  352. {
  353. $header = trim($header);
  354. if (stripos($header, 'x-upyun') !== False)
  355. {
  356. list($k, $v) = explode(':', $header);
  357. $items[trim($k)] = in_array(substr($k, 8, 5), array('width', 'heigh', 'frame')) ? intval($v) : trim($v);
  358. }
  359. }
  360. return $items;
  361. }
  362. /* }}} */
  363. /**
  364. * 获取返回的错误信息
  365. *
  366. * @param string $header_string
  367. *
  368. * @return mixed
  369. */
  370. private function _getErrorMessage($header_string)
  371. {
  372. list($status, $stash) = explode("\r\n", $header_string, 2);
  373. list($v, $code, $message) = explode(" ", $status, 3);
  374. return $message . " X-Request-Id: " . $this->getXRequestId();
  375. }
  376. private function setXRequestId($header_string)
  377. {
  378. preg_match('~^X-Request-Id: ([0-9a-zA-Z]{32})~ism', $header_string, $result);
  379. $this->x_request_id = isset($result[1]) ? $result[1] : '';
  380. }
  381. public function getXRequestId()
  382. {
  383. return $this->x_request_id;
  384. }
  385. /**
  386. * 删除目录
  387. * @deprecated
  388. * @param $path 路径
  389. *
  390. * @return void
  391. */
  392. public function rmDir($path)
  393. {/* {{{ */
  394. $this->_do_request('DELETE', $path);
  395. }
  396. /* }}} */
  397. /**
  398. * 删除文件
  399. *
  400. * @deprecated
  401. * @param string $path 要删除的文件路径
  402. *
  403. * @return boolean
  404. */
  405. public function deleteFile($path)
  406. {/* {{{ */
  407. $rsp = $this->_do_request('DELETE', $path);
  408. }
  409. /* }}} */
  410. /**
  411. * 获取目录文件列表
  412. * @deprecated
  413. *
  414. * @param string $path 要获取列表的目录
  415. *
  416. * @return array
  417. */
  418. public function readDir($path)
  419. {/* {{{ */
  420. return $this->getList($path);
  421. }
  422. /* }}} */
  423. /**
  424. * 获取空间使用情况
  425. *
  426. * @deprecated 推荐直接使用 getFolderUsage('/')来获取
  427. * @return mixed
  428. */
  429. public function getBucketUsage()
  430. {/* {{{ */
  431. return $this->getFolderUsage('/');
  432. }
  433. /* }}} */
  434. /**
  435. * 获取文件信息
  436. *
  437. * #deprecated
  438. * @param $file 文件路径(包含文件名)
  439. * return array('type'=> file | folder, 'size'=> file size, 'date'=> unix time) 或 null
  440. */
  441. //public function getFileInfo($file){/*{{{*/
  442. // $result = $this->head($file);
  443. // if(is_null($r))return null;
  444. // return array('type'=> $this->tmp_infos['x-upyun-file-type'], 'size'=> @intval($this->tmp_infos['x-upyun-file-size']), 'date'=> @intval($this->tmp_infos['x-upyun-file-date']));
  445. //}/*}}}*/
  446. /**
  447. * 切换 API 接口的域名
  448. *
  449. * @deprecated
  450. * @param $domain {默然 v0.api.upyun.com 自动识别, v1.api.upyun.com 电信, v2.api.upyun.com 联通, v3.api.upyun.com 移动}
  451. * return null;
  452. */
  453. public function setApiDomain($domain)
  454. {/* {{{ */
  455. $this->config['endpoint'] = $domain;
  456. }
  457. /* }}} */
  458. /**
  459. * 设置待上传文件的 Content-MD5 值(如又拍云服务端收到的文件MD5值与用户设置的不一致,将回报 406 Not Acceptable 错误)
  460. *
  461. * @deprecated
  462. * @param $str (文件 MD5 校验码)
  463. * return null;
  464. */
  465. public function setContentMD5($str)
  466. {/* {{{ */
  467. $this->_content_md5 = $str;
  468. }
  469. /* }}} */
  470. /**
  471. * 设置待上传文件的 访问密钥(注意:仅支持图片空!,设置密钥后,无法根据原文件URL直接访问,需带 URL 后面加上 (缩略图间隔标志符+密钥) 进行访问)
  472. * 如缩略图间隔标志符为 ! ,密钥为 bac,上传文件路径为 /folder/test.jpg ,那么该图片的对外访问地址为: http://空间域名/folder/test.jpg!bac
  473. *
  474. * @deprecated
  475. * @param $str (文件 MD5 校验码)
  476. * return null;
  477. */
  478. public function setFileSecret($str)
  479. {/* {{{ */
  480. $this->_file_secret = $str;
  481. }
  482. /* }}} */
  483. /**
  484. * @deprecated
  485. * 获取上传文件后的信息(仅图片空间有返回数据)
  486. * @param $key 信息字段名(x-upyun-width、x-upyun-height、x-upyun-frames、x-upyun-file-type)
  487. * return value or NULL
  488. */
  489. public function getWritedFileInfo($key)
  490. {/* {{{ */
  491. if (!isset($this->_file_infos))
  492. return NULL;
  493. return $this->_file_infos[$key];
  494. }
  495. /* }}} */
  496. }