FTP.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-only
  5. */
  6. namespace OCA\Files_External\Lib\Storage;
  7. use Icewind\Streams\CallbackWrapper;
  8. use Icewind\Streams\CountWrapper;
  9. use Icewind\Streams\IteratorDirectory;
  10. use OC\Files\Storage\Common;
  11. use OC\Files\Storage\PolyFill\CopyDirectory;
  12. use OCP\Constants;
  13. use OCP\Files\FileInfo;
  14. use OCP\Files\StorageNotAvailableException;
  15. use Psr\Log\LoggerInterface;
  16. class FTP extends Common {
  17. use CopyDirectory;
  18. private $root;
  19. private $host;
  20. private $password;
  21. private $username;
  22. private $secure;
  23. private $port;
  24. private $utf8Mode;
  25. /** @var FtpConnection|null */
  26. private $connection;
  27. public function __construct($params) {
  28. if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
  29. $this->host = $params['host'];
  30. $this->username = $params['user'];
  31. $this->password = $params['password'];
  32. if (isset($params['secure'])) {
  33. if (is_string($params['secure'])) {
  34. $this->secure = ($params['secure'] === 'true');
  35. } else {
  36. $this->secure = (bool)$params['secure'];
  37. }
  38. } else {
  39. $this->secure = false;
  40. }
  41. $this->root = isset($params['root']) ? '/' . ltrim($params['root']) : '/';
  42. $this->port = $params['port'] ?? 21;
  43. $this->utf8Mode = isset($params['utf8']) && $params['utf8'];
  44. } else {
  45. throw new \Exception('Creating ' . self::class . ' storage failed, required parameters not set');
  46. }
  47. }
  48. public function __destruct() {
  49. $this->connection = null;
  50. }
  51. protected function getConnection(): FtpConnection {
  52. if (!$this->connection) {
  53. try {
  54. $this->connection = new FtpConnection(
  55. $this->secure,
  56. $this->host,
  57. $this->port,
  58. $this->username,
  59. $this->password
  60. );
  61. } catch (\Exception $e) {
  62. throw new StorageNotAvailableException('Failed to create ftp connection', 0, $e);
  63. }
  64. if ($this->utf8Mode) {
  65. if (!$this->connection->setUtf8Mode()) {
  66. throw new StorageNotAvailableException('Could not set UTF-8 mode');
  67. }
  68. }
  69. }
  70. return $this->connection;
  71. }
  72. public function getId() {
  73. return 'ftp::' . $this->username . '@' . $this->host . '/' . $this->root;
  74. }
  75. protected function buildPath($path) {
  76. return rtrim($this->root . '/' . $path, '/');
  77. }
  78. public static function checkDependencies() {
  79. if (function_exists('ftp_login')) {
  80. return true;
  81. } else {
  82. return ['ftp'];
  83. }
  84. }
  85. public function filemtime($path) {
  86. $result = $this->getConnection()->mdtm($this->buildPath($path));
  87. if ($result === -1) {
  88. if ($this->is_dir($path)) {
  89. $list = $this->getConnection()->mlsd($this->buildPath($path));
  90. if (!$list) {
  91. \OC::$server->get(LoggerInterface::class)->warning("Unable to get last modified date for ftp folder ($path), failed to list folder contents");
  92. return time();
  93. }
  94. $currentDir = current(array_filter($list, function ($item) {
  95. return $item['type'] === 'cdir';
  96. }));
  97. if ($currentDir) {
  98. [$modify] = explode('.', $currentDir['modify'] ?? '', 2);
  99. $time = \DateTime::createFromFormat('YmdHis', $modify);
  100. if ($time === false) {
  101. throw new \Exception("Invalid date format for directory: $currentDir");
  102. }
  103. return $time->getTimestamp();
  104. } else {
  105. \OC::$server->get(LoggerInterface::class)->warning("Unable to get last modified date for ftp folder ($path), folder contents doesn't include current folder");
  106. return time();
  107. }
  108. } else {
  109. return false;
  110. }
  111. } else {
  112. return $result;
  113. }
  114. }
  115. public function filesize($path): false|int|float {
  116. $result = $this->getConnection()->size($this->buildPath($path));
  117. if ($result === -1) {
  118. return false;
  119. } else {
  120. return $result;
  121. }
  122. }
  123. public function rmdir($path) {
  124. if ($this->is_dir($path)) {
  125. $result = $this->getConnection()->rmdir($this->buildPath($path));
  126. // recursive rmdir support depends on the ftp server
  127. if ($result) {
  128. return $result;
  129. } else {
  130. return $this->recursiveRmDir($path);
  131. }
  132. } elseif ($this->is_file($path)) {
  133. return $this->unlink($path);
  134. } else {
  135. return false;
  136. }
  137. }
  138. /**
  139. * @param string $path
  140. * @return bool
  141. */
  142. private function recursiveRmDir($path): bool {
  143. $contents = $this->getDirectoryContent($path);
  144. $result = true;
  145. foreach ($contents as $content) {
  146. if ($content['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
  147. $result = $result && $this->recursiveRmDir($path . '/' . $content['name']);
  148. } else {
  149. $result = $result && $this->getConnection()->delete($this->buildPath($path . '/' . $content['name']));
  150. }
  151. }
  152. $result = $result && $this->getConnection()->rmdir($this->buildPath($path));
  153. return $result;
  154. }
  155. public function test() {
  156. try {
  157. return $this->getConnection()->systype() !== false;
  158. } catch (\Exception $e) {
  159. return false;
  160. }
  161. }
  162. public function stat($path) {
  163. if (!$this->file_exists($path)) {
  164. return false;
  165. }
  166. return [
  167. 'mtime' => $this->filemtime($path),
  168. 'size' => $this->filesize($path),
  169. ];
  170. }
  171. public function file_exists($path) {
  172. if ($path === '' || $path === '.' || $path === '/') {
  173. return true;
  174. }
  175. return $this->filetype($path) !== false;
  176. }
  177. public function unlink($path) {
  178. switch ($this->filetype($path)) {
  179. case 'dir':
  180. return $this->rmdir($path);
  181. case 'file':
  182. return $this->getConnection()->delete($this->buildPath($path));
  183. default:
  184. return false;
  185. }
  186. }
  187. public function opendir($path) {
  188. $files = $this->getConnection()->nlist($this->buildPath($path));
  189. return IteratorDirectory::wrap($files);
  190. }
  191. public function mkdir($path) {
  192. if ($this->is_dir($path)) {
  193. return false;
  194. }
  195. return $this->getConnection()->mkdir($this->buildPath($path)) !== false;
  196. }
  197. public function is_dir($path) {
  198. if ($path === '') {
  199. return true;
  200. }
  201. if ($this->getConnection()->chdir($this->buildPath($path)) === true) {
  202. $this->getConnection()->chdir('/');
  203. return true;
  204. } else {
  205. return false;
  206. }
  207. }
  208. public function is_file($path) {
  209. return $this->filesize($path) !== false;
  210. }
  211. public function filetype($path) {
  212. if ($this->is_dir($path)) {
  213. return 'dir';
  214. } elseif ($this->is_file($path)) {
  215. return 'file';
  216. } else {
  217. return false;
  218. }
  219. }
  220. public function fopen($path, $mode) {
  221. $useExisting = true;
  222. switch ($mode) {
  223. case 'r':
  224. case 'rb':
  225. return $this->readStream($path);
  226. case 'w':
  227. case 'w+':
  228. case 'wb':
  229. case 'wb+':
  230. $useExisting = false;
  231. // no break
  232. case 'a':
  233. case 'ab':
  234. case 'r+':
  235. case 'a+':
  236. case 'x':
  237. case 'x+':
  238. case 'c':
  239. case 'c+':
  240. //emulate these
  241. if ($useExisting and $this->file_exists($path)) {
  242. if (!$this->isUpdatable($path)) {
  243. return false;
  244. }
  245. $tmpFile = $this->getCachedFile($path);
  246. } else {
  247. if (!$this->isCreatable(dirname($path))) {
  248. return false;
  249. }
  250. $tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
  251. }
  252. $source = fopen($tmpFile, $mode);
  253. return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $path) {
  254. $this->writeStream($path, fopen($tmpFile, 'r'));
  255. unlink($tmpFile);
  256. });
  257. }
  258. return false;
  259. }
  260. public function writeStream(string $path, $stream, ?int $size = null): int {
  261. if ($size === null) {
  262. $stream = CountWrapper::wrap($stream, function ($writtenSize) use (&$size) {
  263. $size = $writtenSize;
  264. });
  265. }
  266. $this->getConnection()->fput($this->buildPath($path), $stream);
  267. fclose($stream);
  268. return $size;
  269. }
  270. public function readStream(string $path) {
  271. $stream = fopen('php://temp', 'w+');
  272. $result = $this->getConnection()->fget($stream, $this->buildPath($path));
  273. rewind($stream);
  274. if (!$result) {
  275. fclose($stream);
  276. return false;
  277. }
  278. return $stream;
  279. }
  280. public function touch($path, $mtime = null) {
  281. if ($this->file_exists($path)) {
  282. return false;
  283. } else {
  284. $this->file_put_contents($path, '');
  285. return true;
  286. }
  287. }
  288. public function rename($source, $target) {
  289. $this->unlink($target);
  290. return $this->getConnection()->rename($this->buildPath($source), $this->buildPath($target));
  291. }
  292. public function getDirectoryContent($directory): \Traversable {
  293. $files = $this->getConnection()->mlsd($this->buildPath($directory));
  294. $mimeTypeDetector = \OC::$server->getMimeTypeDetector();
  295. foreach ($files as $file) {
  296. $name = $file['name'];
  297. if ($file['type'] === 'cdir' || $file['type'] === 'pdir') {
  298. continue;
  299. }
  300. $permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
  301. $isDir = $file['type'] === 'dir';
  302. if ($isDir) {
  303. $permissions += Constants::PERMISSION_CREATE;
  304. }
  305. $data = [];
  306. $data['mimetype'] = $isDir ? FileInfo::MIMETYPE_FOLDER : $mimeTypeDetector->detectPath($name);
  307. // strip fractional seconds
  308. [$modify] = explode('.', $file['modify'], 2);
  309. $mtime = \DateTime::createFromFormat('YmdGis', $modify);
  310. $data['mtime'] = $mtime === false ? time() : $mtime->getTimestamp();
  311. if ($isDir) {
  312. $data['size'] = -1; //unknown
  313. } elseif (isset($file['size'])) {
  314. $data['size'] = $file['size'];
  315. } else {
  316. $data['size'] = $this->filesize($directory . '/' . $name);
  317. }
  318. $data['etag'] = uniqid();
  319. $data['storage_mtime'] = $data['mtime'];
  320. $data['permissions'] = $permissions;
  321. $data['name'] = $name;
  322. yield $data;
  323. }
  324. }
  325. }