123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- <?php
- /**
- * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
- *
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @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 <http://www.gnu.org/licenses/>
- *
- */
- 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;
- }
- }
- }
|