FTP.php 9.7 KB

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