PageRenderTime 39ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/deployment/src/Deployment/FtpServer.php

https://gitlab.com/ilyales/vigma
PHP | 304 lines | 193 code | 43 blank | 68 comment | 30 complexity | 233714765a7bab116a5237d3d5859837 MD5 | raw file
  1. <?php
  2. /**
  3. * FTP Deployment
  4. *
  5. * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
  6. */
  7. namespace Deployment;
  8. /**
  9. * FTP server.
  10. *
  11. * @author David Grudl
  12. */
  13. class FtpServer implements Server
  14. {
  15. const RETRIES = 10;
  16. const BLOCK_SIZE = 400000;
  17. /** @var resource */
  18. private $connection;
  19. /** @var array see parse_url() */
  20. private $url;
  21. /** @var bool */
  22. private $passiveMode = TRUE;
  23. /**
  24. * @param string|array URL ftp://...
  25. * @param bool
  26. */
  27. public function __construct($url, $passiveMode = TRUE)
  28. {
  29. if (!extension_loaded('ftp')) {
  30. throw new \Exception('PHP extension FTP is not loaded.');
  31. }
  32. $this->url = $url = is_array($url) ? $url : parse_url($url);
  33. if (!isset($url['scheme'], $url['user'], $url['pass']) || ($url['scheme'] !== 'ftp' && $url['scheme'] !== 'ftps')) {
  34. throw new \InvalidArgumentException("Invalid URL or missing username or password");
  35. } elseif ($url['scheme'] === 'ftps' && !function_exists('ftp_ssl_connect')) {
  36. throw new \Exception('PHP extension OpenSSL is not built statically in PHP.');
  37. }
  38. $this->passiveMode = (bool) $passiveMode;
  39. }
  40. /**
  41. * Connects to FTP server.
  42. * @return void
  43. */
  44. public function connect()
  45. {
  46. $this->connection = $this->protect(
  47. $this->url['scheme'] === 'ftp' ? 'ftp_connect' : 'ftp_ssl_connect',
  48. [$this->url['host'], empty($this->url['port']) ? NULL : (int) $this->url['port']]
  49. );
  50. $this->ftp('login', urldecode($this->url['user']), urldecode($this->url['pass']));
  51. $this->ftp('pasv', $this->passiveMode);
  52. if (isset($this->url['path'])) {
  53. $this->ftp('chdir', $this->url['path']);
  54. }
  55. }
  56. /**
  57. * Reads remote file from FTP server.
  58. * @return void
  59. */
  60. public function readFile($remote, $local)
  61. {
  62. $this->ftp('get', $local, $remote, FTP_BINARY);
  63. }
  64. /**
  65. * Uploads file to FTP server.
  66. * @return void
  67. */
  68. public function writeFile($local, $remote, callable $progress = NULL)
  69. {
  70. $size = max(filesize($local), 1);
  71. $retry = self::RETRIES;
  72. upload:
  73. $blocks = 0;
  74. do {
  75. if ($progress) {
  76. $progress(min($blocks * self::BLOCK_SIZE / $size, 100));
  77. }
  78. try {
  79. $ret = $blocks === 0
  80. ? $this->ftp('nb_put', $remote, $local, FTP_BINARY)
  81. : $this->ftp('nb_continue');
  82. } catch (FtpException $e) {
  83. @ftp_close($this->connection); // intentionally @
  84. $this->connect();
  85. if (--$retry) {
  86. goto upload;
  87. }
  88. throw new FtpException("Cannot upload file $local, number of retries exceeded. Error: {$e->getMessage()}");
  89. }
  90. $blocks++;
  91. } while ($ret === FTP_MOREDATA);
  92. if ($progress) {
  93. $progress(100);
  94. }
  95. }
  96. /**
  97. * Removes file from FTP server if exists.
  98. * @return void
  99. */
  100. public function removeFile($file)
  101. {
  102. try {
  103. $this->ftp('delete', $file);
  104. } catch (FtpException $e) {
  105. if (in_array($file, (array) $this->ftp('nlist', $file . '*'))) {
  106. throw $e;
  107. }
  108. }
  109. }
  110. /**
  111. * Renames and rewrites file on FTP server.
  112. * @return void
  113. */
  114. public function renameFile($old, $new)
  115. {
  116. $this->removeFile($new);
  117. $this->ftp('rename', $old, $new); // TODO: zachovat permissions
  118. }
  119. /**
  120. * Creates directories on FTP server.
  121. * @return void
  122. */
  123. public function createDir($dir)
  124. {
  125. if (trim($dir, '/') === '' || $this->isDir($dir)) {
  126. return;
  127. }
  128. $parts = explode('/', $dir);
  129. $path = '';
  130. while (!empty($parts)) {
  131. $path .= array_shift($parts);
  132. try {
  133. if ($path !== '') {
  134. $this->ftp('mkdir', $path);
  135. }
  136. } catch (FtpException $e) {
  137. if (!$this->isDir($path)) {
  138. throw new FtpException("Cannot create directory '$path'.");
  139. }
  140. }
  141. $path .= '/';
  142. }
  143. }
  144. /**
  145. * Checks if directory exists.
  146. * @param string
  147. * @return bool
  148. */
  149. private function isDir($dir)
  150. {
  151. $current = $this->getDir();
  152. try {
  153. $this->ftp('chdir', $dir);
  154. } catch (FtpException $e) {
  155. }
  156. $this->ftp('chdir', $current ?: '/');
  157. return empty($e);
  158. }
  159. /**
  160. * Removes directory from FTP server if exists.
  161. * @return void
  162. */
  163. public function removeDir($dir)
  164. {
  165. try {
  166. $this->ftp('rmDir', $dir);
  167. } catch (FtpException $e) {
  168. if (in_array($dir, (array) $this->ftp('nlist', $dir . '*'))) {
  169. throw $e;
  170. }
  171. }
  172. }
  173. /**
  174. * Recursive deletes content of directory or file.
  175. * @param string
  176. * @return void
  177. */
  178. public function purge($dir, callable $progress = NULL)
  179. {
  180. $dirs = [];
  181. foreach ((array) $this->ftp('nlist', $dir) as $entry) {
  182. if ($entry == NULL || $entry === $dir || preg_match('#(^|/)\\.+$#', $entry)) { // intentionally ==
  183. continue;
  184. } elseif (strpos($entry, '/') === FALSE) {
  185. $entry = "$dir/$entry";
  186. }
  187. if ($this->isDir($entry)) {
  188. $dirs[] = $tmp = "$dir/.delete" . uniqid() . count($dirs);
  189. $this->ftp('rename', $entry, $tmp);
  190. } else {
  191. $this->ftp('delete', $entry);
  192. }
  193. if ($progress) {
  194. $progress($entry);
  195. }
  196. }
  197. foreach ($dirs as $subdir) {
  198. $this->purge($subdir, $progress);
  199. $this->ftp('rmDir', $subdir);
  200. }
  201. }
  202. /**
  203. * Returns current directory.
  204. * @return string
  205. */
  206. public function getDir()
  207. {
  208. return rtrim($this->ftp('pwd'), '/');
  209. }
  210. /**
  211. * Executes a command on a remote server.
  212. * @return string
  213. */
  214. public function execute($command)
  215. {
  216. if (preg_match('#^(mkdir|rmdir|unlink|mv|chmod)\s+(\S+)(?:\s+(\S+))?$#', $command, $m)) {
  217. if ($m[1] === 'mkdir') {
  218. $this->createDir($m[2]);
  219. } elseif ($m[1] === 'rmdir') {
  220. $this->removeDir($m[2]);
  221. } elseif ($m[1] === 'unlink') {
  222. $this->removeFile($m[2]);
  223. } elseif ($m[1] === 'mv') {
  224. $this->renameFile($m[2], $m[3]);
  225. } elseif ($m[1] === 'chmod') {
  226. $this->ftp('chmod', octdec($m[2]), $m[3]);
  227. }
  228. } else {
  229. return $this->ftp('exec', $command);
  230. }
  231. }
  232. /**
  233. * @param string method name
  234. * @param array arguments
  235. * @return mixed
  236. */
  237. private function ftp($cmd)
  238. {
  239. $args = func_get_args();
  240. $args[0] = $this->connection;
  241. return $this->protect('ftp_' . $cmd, $args);
  242. }
  243. private function protect(callable $func, $args = [])
  244. {
  245. set_error_handler(function ($severity, $message) use (& $error) {
  246. $error = $message;
  247. });
  248. $res = call_user_func_array($func, $args);
  249. restore_error_handler();
  250. if ($error) {
  251. if (ini_get('html_errors')) {
  252. $error = html_entity_decode(strip_tags($error));
  253. }
  254. if (preg_match('#^\w+\(\):\s*(.+)#', $error, $m)) {
  255. $error = $m[1];
  256. }
  257. throw new FtpException($error);
  258. }
  259. return $res;
  260. }
  261. }