diff --git a/.github/workflows/ftp.yml b/.github/workflows/ftp.yml new file mode 100644 index 0000000000..e67bd12693 --- /dev/null +++ b/.github/workflows/ftp.yml @@ -0,0 +1,73 @@ +name: FTP +on: + push: + branches: + - master + - stable* + paths: + - 'apps/files_external/**' + pull_request: + paths: + - 'apps/files_external/**' + +env: + APP_NAME: files_external + +jobs: + ftp-tests: + runs-on: ubuntu-latest + + strategy: + # do not stop on another job's failure + fail-fast: false + matrix: + php-versions: ['7.4', '8.0'] + ftpd: ['proftpd', 'vsftpd', 'pure-ftpd'] + + name: php${{ matrix.php-versions }}-${{ matrix.ftpd }} + + steps: + - name: Checkout server + uses: actions/checkout@v2 + + - name: Checkout submodules + shell: bash + run: | + auth_header="$(git config --local --get http.https://github.com/.extraheader)" + git submodule sync --recursive + git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 + + - name: Set up ftpd + run: | + sudo mkdir /tmp/ftp + sudo chown -R 0777 /tmp/ftp + if [[ "${{ matrix.ftpd }}" == 'proftpd' ]]; then docker run --name ftp -d --net host -e FTP_USERNAME=test -e FTP_PASSWORD=test -v /tmp/ftp:/home/test hauptmedia/proftpd; fi + if [[ "${{ matrix.ftpd }}" == 'vsftpd' ]]; then docker run --name ftp -d --net host -e FTP_USER=test -e FTP_PASS=test -e PASV_ADDRESS=127.0.0.1 -v /tmp/ftp:/home/vsftpd/test fauria/vsftpd; fi + if [[ "${{ matrix.ftpd }}" == 'pure-ftpd' ]]; then docker run --name ftp -d --net host -e "PUBLICHOST=localhost" -e FTP_USER_NAME=test -e FTP_USER_PASS=test -e FTP_USER_HOME=/home/test -v /tmp/ftp2:/home/test -v /tmp/ftp2:/etc/pure-ftpd/passwd stilliard/pure-ftpd; fi + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: phpunit + extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, zip, gd + + - name: Set up Nextcloud + run: | + mkdir data + ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password + ./occ app:enable --force ${{ env.APP_NAME }} + php -S localhost:8080 & + - name: smoketest ftp + run: | + php -r 'var_dump(file_put_contents("ftp://test:test@localhost/ftp.txt", "asd"));' + php -r 'var_dump(file_get_contents("ftp://test:test@localhost/ftp.txt"));' + php -r 'var_dump(mkdir("ftp://test:test@localhost/asdads"));' + ls -l /tmp/ftp + - name: PHPUnit + run: | + echo " true,'host' => 'localhost','user' => 'test','password' => 'test', 'root' => ''];" > apps/${{ env.APP_NAME }}/tests/config.ftp.php + phpunit --configuration tests/phpunit-autotest-external.xml apps/files_external/tests/Storage/FtpTest.php + - name: ftpd logs + if: always() + run: | + docker logs ftp diff --git a/apps/files_external/lib/Lib/Storage/FTP.php b/apps/files_external/lib/Lib/Storage/FTP.php index 48e312ecd7..0207ff5492 100644 --- a/apps/files_external/lib/Lib/Storage/FTP.php +++ b/apps/files_external/lib/Lib/Storage/FTP.php @@ -1,20 +1,8 @@ - * @author Christoph Wurst - * @author Felix Moeller - * @author Jörn Friedrich Dreyer - * @author Michael Gapczynski - * @author Morris Jobke - * @author Philipp Kapfer * @author Robin Appelman - * @author Robin McCorkell - * @author Roeland Jago Douma - * @author Thomas Müller - * @author Vincent Petry * + * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify @@ -27,144 +15,363 @@ * 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 + * 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 Icewind\Streams\RetryWrapper; +use OC\Files\Storage\Common; +use OC\Files\Storage\PolyFill\CopyDirectory; +use OCP\Constants; +use OCP\Files\FileInfo; +use OCP\Files\StorageNotAvailableException; + +class FTP extends Common { + use CopyDirectory; -class FTP extends StreamWrapper { - private $password; - private $user; - private $host; - private $secure; 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->user = $params['user']; + $this->username = $params['user']; $this->password = $params['password']; if (isset($params['secure'])) { - $this->secure = $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'])?$params['root']:'/'; - if (! $this->root || $this->root[0] !== '/') { - $this->root = '/'.$this->root; - } - if (substr($this->root, -1) !== '/') { - $this->root .= '/'; - } + $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 FTP storage failed'); + 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->user . '@' . $this->host . '/' . $this->root; + return 'ftp::' . $this->username . '@' . $this->host . '/' . $this->root; } - /** - * construct the ftp url - * @param string $path - * @return string - */ - public function constructUrl($path) { - $url = 'ftp'; - if ($this->secure) { - $url .= 's'; - } - $url .= '://'.urlencode($this->user).':'.urlencode($this->password).'@'.$this->host.$this->root.$path; - return $url; + protected function buildPath($path) { + return rtrim($this->root . '/' . $path, '/'); } - /** - * Unlinks file or directory - * @param string $path - */ - public function unlink($path) { - if ($this->is_dir($path)) { - return $this->rmdir($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->getLogger()->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) { + $time = \DateTime::createFromFormat('YmdHis', $currentDir['modify']); + if ($time === false) { + throw new \Exception("Invalid date format for directory: $currentDir"); + } + return $time->getTimestamp(); + } else { + \OC::$server->getLogger()->warning("Unable to get last modified date for ftp folder ($path), folder contents doesn't include current folder"); + return time(); + } + } else { + return false; + } } else { - $url = $this->constructUrl($path); - $result = unlink($url); - clearstatcache(true, $url); return $result; } } - public function fopen($path,$mode) { + + public function filesize($path) { + $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': - //these are supported by the wrapper - $context = stream_context_create(['ftp' => ['overwrite' => true]]); - $handle = fopen($this->constructUrl($path), $mode, false, $context); - return RetryWrapper::wrap($handle); case 'r+': - case 'w+': - case 'wb+': case 'a+': case 'x': case 'x+': case 'c': case 'c+': //emulate these - if (strrpos($path, '.') !== false) { - $ext = substr($path, strrpos($path, '.')); + if ($useExisting and $this->file_exists($path)) { + if (!$this->isUpdatable($path)) { + return false; + } + $tmpFile = $this->getCachedFile($path); } else { - $ext = ''; + if (!$this->isCreatable(dirname($path))) { + return false; + } + $tmpFile = \OC::$server->getTempManager()->getTemporaryFile(); } - $tmpFile = \OC::$server->getTempManager()->getTemporaryFile(); - if ($this->file_exists($path)) { - $this->getFile($path, $tmpFile); - } - $handle = fopen($tmpFile, $mode); - return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { - $this->writeBack($tmpFile, $path); + $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 opendir($path) { - $dh = parent::opendir($path); - if (is_resource($dh)) { - $files = []; - while (($file = readdir($dh)) !== false) { - if ($file != '.' && $file != '..' && strpos($file, '#') === false) { - $files[] = $file; - } - } - return IteratorDirectory::wrap($files); - } else { + 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 writeBack($tmpFile, $path) { - $this->uploadFile($tmpFile, $path); - unlink($tmpFile); + public function rename($path1, $path2) { + $this->unlink($path2); + return $this->getConnection()->rename($this->buildPath($path1), $this->buildPath($path2)); } - /** - * check if php-ftp is installed - */ - public static function checkDependencies() { - if (function_exists('ftp_login')) { - return true; - } else { - return ['ftp']; + 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); + $data['mtime'] = \DateTime::createFromFormat('YmdGis', $file['modify'])->getTimestamp(); + if ($data['mtime'] === false) { + $data['mtime'] = time(); + } + 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; } } } diff --git a/apps/files_external/lib/Lib/Storage/FtpConnection.php b/apps/files_external/lib/Lib/Storage/FtpConnection.php new file mode 100644 index 0000000000..d87c44656f --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/FtpConnection.php @@ -0,0 +1,234 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCA\Files_External\Lib\Storage; + +/** + * Low level wrapper around the ftp functions that smooths over some difference between servers + */ +class FtpConnection { + /** @var resource */ + private $connection; + + public function __construct(bool $secure, string $hostname, int $port, string $username, string $password) { + if ($secure) { + $connection = ftp_ssl_connect($hostname, $port); + } else { + $connection = ftp_connect($hostname, $port); + } + + if ($connection === false) { + throw new \Exception("Failed to connect to ftp"); + } + + if (ftp_login($connection, $username, $password) === false) { + throw new \Exception("Failed to connect to login to ftp"); + } + + ftp_pasv($connection, true); + $this->connection = $connection; + } + + public function __destruct() { + if ($this->connection) { + ftp_close($this->connection); + } + $this->connection = null; + } + + public function setUtf8Mode(): bool { + $response = ftp_raw($this->connection, "OPTS UTF8 ON"); + return substr($response[0], 0, 3) === '200'; + } + + public function fput(string $path, $handle) { + return @ftp_fput($this->connection, $path, $handle, FTP_BINARY); + } + + public function fget($handle, string $path) { + return @ftp_fget($this->connection, $handle, $path, FTP_BINARY); + } + + public function mkdir(string $path) { + return @ftp_mkdir($this->connection, $path); + } + + public function chdir(string $path) { + return @ftp_chdir($this->connection, $path); + } + + public function delete(string $path) { + return @ftp_delete($this->connection, $path); + } + + public function rmdir(string $path) { + return @ftp_rmdir($this->connection, $path); + } + + public function rename(string $path1, string $path2) { + return @ftp_rename($this->connection, $path1, $path2); + } + + public function mdtm(string $path) { + return @ftp_mdtm($this->connection, $path); + } + + public function size(string $path) { + return @ftp_size($this->connection, $path); + } + + public function systype() { + return @ftp_systype($this->connection); + } + + public function nlist(string $path) { + $files = @ftp_nlist($this->connection, $path); + return array_map(function ($name) { + if (strpos($name, '/') !== false) { + $name = basename($name); + } + return $name; + }, $files); + } + + public function mlsd(string $path) { + $files = @ftp_mlsd($this->connection, $path); + + if ($files !== false) { + return array_map(function ($file) { + if (strpos($file['name'], '/') !== false) { + $file['name'] = basename($file['name']); + } + return $file; + }, $files); + } else { + // not all servers support mlsd, in those cases we parse the raw list ourselves + $rawList = @ftp_rawlist($this->connection, '-aln ' . $path); + if ($rawList === false) { + return false; + } + return $this->parseRawList($rawList, $path); + } + } + + // rawlist parsing logic is based on the ftp implementation from https://github.com/thephpleague/flysystem + private function parseRawList(array $rawList, string $directory): array { + return array_map(function ($item) use ($directory) { + return $this->parseRawListItem($item, $directory); + }, $rawList); + } + + private function parseRawListItem(string $item, string $directory): array { + $isWindows = preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item); + + return $isWindows ? $this->parseWindowsItem($item, $directory) : $this->parseUnixItem($item, $directory); + } + + private function parseUnixItem(string $item, string $directory): array { + $item = preg_replace('#\s+#', ' ', $item, 7); + + if (count(explode(' ', $item, 9)) !== 9) { + throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); + } + + [$permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $time, $name] = explode(' ', $item, 9); + if ($name === '.') { + $type = 'cdir'; + } elseif ($name === '..') { + $type = 'pdir'; + } else { + $type = substr($permissions, 0, 1) === 'd' ? 'dir' : 'file'; + } + + $parsedDate = (new \DateTime()) + ->setTimestamp(strtotime("$month $day $time")); + $tomorrow = (new \DateTime())->add(new \DateInterval("P1D")); + + // since the provided date doesn't include the year, we either set it to the correct year + // or when the date would otherwise be in the future (by more then 1 day to account for timezone errors) + // we use last year + if ($parsedDate > $tomorrow) { + $parsedDate = $parsedDate->sub(new \DateInterval("P1Y")); + } + + $formattedDate = $parsedDate + ->format('YmdHis'); + + return [ + 'type' => $type, + 'name' => $name, + 'modify' => $formattedDate, + 'perm' => $this->normalizePermissions($permissions), + 'size' => (int)$size, + ]; + } + + private function normalizePermissions(string $permissions) { + $isDir = substr($permissions, 0, 1) === 'd'; + // remove the type identifier and only use owner permissions + $permissions = substr($permissions, 1, 4); + + // map the string rights to the ftp counterparts + $filePermissionsMap = ['r' => 'r', 'w' => 'fadfw']; + $dirPermissionsMap = ['r' => 'e', 'w' => 'flcdmp']; + + $map = $isDir ? $dirPermissionsMap : $filePermissionsMap; + + return array_reduce(str_split($permissions), function ($ftpPermissions, $permission) use ($map) { + if (isset($map[$permission])) { + $ftpPermissions .= $map[$permission]; + } + return $ftpPermissions; + }, ''); + } + + private function parseWindowsItem(string $item, string $directory): array { + $item = preg_replace('#\s+#', ' ', trim($item), 3); + + if (count(explode(' ', $item, 4)) !== 4) { + throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); + } + + [$date, $time, $size, $name] = explode(' ', $item, 4); + + // Check for the correct date/time format + $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i'; + $formattedDate = \DateTime::createFromFormat($format, $date . $time)->format('YmdGis'); + + if ($name === '.') { + $type = 'cdir'; + } elseif ($name === '..') { + $type = 'pdir'; + } else { + $type = ($size === '') ? 'dir' : 'file'; + } + + return [ + 'type' => $type, + 'name' => $name, + 'modify' => $formattedDate, + 'perm' => ($type === 'file') ? 'adfrw' : 'flcdmpe', + 'size' => (int)$size, + ]; + } +} diff --git a/apps/files_external/tests/Storage/FtpTest.php b/apps/files_external/tests/Storage/FtpTest.php index b471b2ebdf..2355ab7243 100644 --- a/apps/files_external/tests/Storage/FtpTest.php +++ b/apps/files_external/tests/Storage/FtpTest.php @@ -49,50 +49,63 @@ class FtpTest extends \Test\Files\Storage\Storage { if (! is_array($this->config) or ! $this->config['run']) { $this->markTestSkipped('FTP backend not configured'); } + $rootInstance = new FTP($this->config); + $rootInstance->mkdir($id); + $this->config['root'] .= '/' . $id; //make sure we have an new empty folder to work in $this->instance = new FTP($this->config); - $this->instance->mkdir('/'); } protected function tearDown(): void { if ($this->instance) { - \OCP\Files::rmdirr($this->instance->constructUrl('')); + $this->instance->rmdir(''); } + $this->instance = null; parent::tearDown(); } - public function testConstructUrl() { - $config = [ 'host' => 'localhost', - 'user' => 'ftp', - 'password' => 'ftp', - 'root' => '/', - 'secure' => false ]; - $instance = new FTP($config); - $this->assertEquals('ftp://ftp:ftp@localhost/', $instance->constructUrl('')); + /** + * ftp has no proper way to handle spaces at the end of file names + */ + public function directoryProvider() { + return array_filter(parent::directoryProvider(), function ($item) { + return substr($item[0], -1) !== ' '; + }); + } - $config['secure'] = true; - $instance = new FTP($config); - $this->assertEquals('ftps://ftp:ftp@localhost/', $instance->constructUrl('')); - $config['secure'] = 'false'; - $instance = new FTP($config); - $this->assertEquals('ftp://ftp:ftp@localhost/', $instance->constructUrl('')); + /** + * mtime for folders is only with a minute resolution + */ + public function testStat() { + $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; + $ctimeStart = time(); + $this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile)); + $this->assertTrue($this->instance->isReadable('/lorem.txt')); + $ctimeEnd = time(); + $mTime = $this->instance->filemtime('/lorem.txt'); + $this->assertTrue($this->instance->hasUpdated('/lorem.txt', $ctimeStart - 5)); + $this->assertTrue($this->instance->hasUpdated('/', $ctimeStart - 61)); - $config['secure'] = 'true'; - $instance = new FTP($config); - $this->assertEquals('ftps://ftp:ftp@localhost/', $instance->constructUrl('')); + // check that ($ctimeStart - 5) <= $mTime <= ($ctimeEnd + 1) + $this->assertGreaterThanOrEqual(($ctimeStart - 5), $mTime); + $this->assertLessThanOrEqual(($ctimeEnd + 1), $mTime); + $this->assertEquals(filesize($textFile), $this->instance->filesize('/lorem.txt')); - $config['root'] = ''; - $instance = new FTP($config); - $this->assertEquals('ftps://ftp:ftp@localhost/somefile.txt', $instance->constructUrl('somefile.txt')); + $stat = $this->instance->stat('/lorem.txt'); + //only size and mtime are required in the result + $this->assertEquals($stat['size'], $this->instance->filesize('/lorem.txt')); + $this->assertEquals($stat['mtime'], $mTime); - $config['root'] = '/abc'; - $instance = new FTP($config); - $this->assertEquals('ftps://ftp:ftp@localhost/abc/somefile.txt', $instance->constructUrl('somefile.txt')); + if ($this->instance->touch('/lorem.txt', 100) !== false) { + $mTime = $this->instance->filemtime('/lorem.txt'); + $this->assertEquals($mTime, 100); + } - $config['root'] = '/abc/'; - $instance = new FTP($config); - $this->assertEquals('ftps://ftp:ftp@localhost/abc/somefile.txt', $instance->constructUrl('somefile.txt')); + $mtimeStart = time(); + + $this->instance->unlink('/lorem.txt'); + $this->assertTrue($this->instance->hasUpdated('/', $mtimeStart - 61)); } } diff --git a/lib/private/Files/Storage/PolyFill/CopyDirectory.php b/lib/private/Files/Storage/PolyFill/CopyDirectory.php index 6a12089c70..ccc579bef9 100644 --- a/lib/private/Files/Storage/PolyFill/CopyDirectory.php +++ b/lib/private/Files/Storage/PolyFill/CopyDirectory.php @@ -65,15 +65,15 @@ trait CopyDirectory { */ abstract public function mkdir($path); - public function copy($source, $target) { - if ($this->is_dir($source)) { - if ($this->file_exists($target)) { - $this->unlink($target); + public function copy($path1, $path2) { + if ($this->is_dir($path1)) { + if ($this->file_exists($path2)) { + $this->unlink($path2); } - $this->mkdir($target); - return $this->copyRecursive($source, $target); + $this->mkdir($path2); + return $this->copyRecursive($path1, $path2); } else { - return parent::copy($source, $target); + return parent::copy($path1, $path2); } }