FTP.php 9.5 KB

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