* * @author Robin Appelman * * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License, version 3, * along with this program. If not, see * */ namespace OCA\Files_External\Lib\Storage; use Icewind\Streams\CallbackWrapper; use Icewind\Streams\CountWrapper; use Icewind\Streams\IteratorDirectory; use OC\Files\Storage\Common; use OC\Files\Storage\PolyFill\CopyDirectory; use OCP\Constants; use OCP\Files\FileInfo; use OCP\Files\StorageNotAvailableException; use Psr\Log\LoggerInterface; class FTP extends Common { use CopyDirectory; private $root; private $host; private $password; private $username; private $secure; private $port; private $utf8Mode; /** @var FtpConnection|null */ private $connection; public function __construct($params) { if (isset($params['host']) && isset($params['user']) && isset($params['password'])) { $this->host = $params['host']; $this->username = $params['user']; $this->password = $params['password']; if (isset($params['secure'])) { if (is_string($params['secure'])) { $this->secure = ($params['secure'] === 'true'); } else { $this->secure = (bool)$params['secure']; } } else { $this->secure = false; } $this->root = isset($params['root']) ? '/' . ltrim($params['root']) : '/'; $this->port = $params['port'] ?? 21; $this->utf8Mode = isset($params['utf8']) && $params['utf8']; } else { throw new \Exception('Creating ' . self::class . ' storage failed, required parameters not set'); } } public function __destruct() { $this->connection = null; } protected function getConnection(): FtpConnection { if (!$this->connection) { try { $this->connection = new FtpConnection( $this->secure, $this->host, $this->port, $this->username, $this->password ); } catch (\Exception $e) { throw new StorageNotAvailableException("Failed to create ftp connection", 0, $e); } if ($this->utf8Mode) { if (!$this->connection->setUtf8Mode()) { throw new StorageNotAvailableException("Could not set UTF-8 mode"); } } } return $this->connection; } public function getId() { return 'ftp::' . $this->username . '@' . $this->host . '/' . $this->root; } protected function buildPath($path) { return rtrim($this->root . '/' . $path, '/'); } public static function checkDependencies() { if (function_exists('ftp_login')) { return true; } else { return ['ftp']; } } public function filemtime($path) { $result = $this->getConnection()->mdtm($this->buildPath($path)); if ($result === -1) { if ($this->is_dir($path)) { $list = $this->getConnection()->mlsd($this->buildPath($path)); if (!$list) { \OC::$server->get(LoggerInterface::class)->warning("Unable to get last modified date for ftp folder ($path), failed to list folder contents"); return time(); } $currentDir = current(array_filter($list, function ($item) { return $item['type'] === 'cdir'; })); if ($currentDir) { [$modify] = explode('.', $currentDir['modify'] ?? '', 2); $time = \DateTime::createFromFormat('YmdHis', $modify); if ($time === false) { throw new \Exception("Invalid date format for directory: $currentDir"); } return $time->getTimestamp(); } else { \OC::$server->get(LoggerInterface::class)->warning("Unable to get last modified date for ftp folder ($path), folder contents doesn't include current folder"); return time(); } } else { return false; } } else { return $result; } } public function filesize($path): false|int|float { $result = $this->getConnection()->size($this->buildPath($path)); if ($result === -1) { return false; } else { return $result; } } public function rmdir($path) { if ($this->is_dir($path)) { $result = $this->getConnection()->rmdir($this->buildPath($path)); // recursive rmdir support depends on the ftp server if ($result) { return $result; } else { return $this->recursiveRmDir($path); } } elseif ($this->is_file($path)) { return $this->unlink($path); } else { return false; } } /** * @param string $path * @return bool */ private function recursiveRmDir($path): bool { $contents = $this->getDirectoryContent($path); $result = true; foreach ($contents as $content) { if ($content['mimetype'] === FileInfo::MIMETYPE_FOLDER) { $result = $result && $this->recursiveRmDir($path . '/' . $content['name']); } else { $result = $result && $this->getConnection()->delete($this->buildPath($path . '/' . $content['name'])); } } $result = $result && $this->getConnection()->rmdir($this->buildPath($path)); return $result; } public function test() { try { return $this->getConnection()->systype() !== false; } catch (\Exception $e) { return false; } } public function stat($path) { if (!$this->file_exists($path)) { return false; } return [ 'mtime' => $this->filemtime($path), 'size' => $this->filesize($path), ]; } public function file_exists($path) { if ($path === '' || $path === '.' || $path === '/') { return true; } return $this->filetype($path) !== false; } public function unlink($path) { switch ($this->filetype($path)) { case 'dir': return $this->rmdir($path); case 'file': return $this->getConnection()->delete($this->buildPath($path)); default: return false; } } public function opendir($path) { $files = $this->getConnection()->nlist($this->buildPath($path)); return IteratorDirectory::wrap($files); } public function mkdir($path) { if ($this->is_dir($path)) { return false; } return $this->getConnection()->mkdir($this->buildPath($path)) !== false; } public function is_dir($path) { if ($path === "") { return true; } if ($this->getConnection()->chdir($this->buildPath($path)) === true) { $this->getConnection()->chdir('/'); return true; } else { return false; } } public function is_file($path) { return $this->filesize($path) !== false; } public function filetype($path) { if ($this->is_dir($path)) { return 'dir'; } elseif ($this->is_file($path)) { return 'file'; } else { return false; } } public function fopen($path, $mode) { $useExisting = true; switch ($mode) { case 'r': case 'rb': return $this->readStream($path); case 'w': case 'w+': case 'wb': case 'wb+': $useExisting = false; // no break case 'a': case 'ab': case 'r+': case 'a+': case 'x': case 'x+': case 'c': case 'c+': //emulate these if ($useExisting and $this->file_exists($path)) { if (!$this->isUpdatable($path)) { return false; } $tmpFile = $this->getCachedFile($path); } else { if (!$this->isCreatable(dirname($path))) { return false; } $tmpFile = \OC::$server->getTempManager()->getTemporaryFile(); } $source = fopen($tmpFile, $mode); return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $path) { $this->writeStream($path, fopen($tmpFile, 'r')); unlink($tmpFile); }); } return false; } public function writeStream(string $path, $stream, ?int $size = null): int { if ($size === null) { $stream = CountWrapper::wrap($stream, function ($writtenSize) use (&$size) { $size = $writtenSize; }); } $this->getConnection()->fput($this->buildPath($path), $stream); fclose($stream); return $size; } public function readStream(string $path) { $stream = fopen('php://temp', 'w+'); $result = $this->getConnection()->fget($stream, $this->buildPath($path)); rewind($stream); if (!$result) { fclose($stream); return false; } return $stream; } public function touch($path, $mtime = null) { if ($this->file_exists($path)) { return false; } else { $this->file_put_contents($path, ''); return true; } } public function rename($source, $target) { $this->unlink($target); return $this->getConnection()->rename($this->buildPath($source), $this->buildPath($target)); } public function getDirectoryContent($directory): \Traversable { $files = $this->getConnection()->mlsd($this->buildPath($directory)); $mimeTypeDetector = \OC::$server->getMimeTypeDetector(); foreach ($files as $file) { $name = $file['name']; if ($file['type'] === 'cdir' || $file['type'] === 'pdir') { continue; } $permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; $isDir = $file['type'] === 'dir'; if ($isDir) { $permissions += Constants::PERMISSION_CREATE; } $data = []; $data['mimetype'] = $isDir ? FileInfo::MIMETYPE_FOLDER : $mimeTypeDetector->detectPath($name); // strip fractional seconds [$modify] = explode('.', $file['modify'], 2); $mtime = \DateTime::createFromFormat('YmdGis', $modify); $data['mtime'] = $mtime === false ? time() : $mtime->getTimestamp(); if ($isDir) { $data['size'] = -1; //unknown } elseif (isset($file['size'])) { $data['size'] = $file['size']; } else { $data['size'] = $this->filesize($directory . '/' . $name); } $data['etag'] = uniqid(); $data['storage_mtime'] = $data['mtime']; $data['permissions'] = $permissions; $data['name'] = $name; yield $data; } } }