|
@@ -1,19 +1,8 @@
|
|
|
<?php
|
|
|
/**
|
|
|
- * @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
|
+ * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
|
|
|
*
|
|
|
- * @author Björn Schießle <bjoern@schiessle.org>
|
|
|
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
|
|
- * @author Felix Moeller <mail@felixmoeller.de>
|
|
|
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
|
|
|
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
|
|
|
- * @author Morris Jobke <hey@morrisjobke.de>
|
|
|
- * @author Philipp Kapfer <philipp.kapfer@gmx.at>
|
|
|
* @author Robin Appelman <robin@icewind.nl>
|
|
|
- * @author Robin McCorkell <robin@mccorkell.me.uk>
|
|
|
- * @author Roeland Jago Douma <roeland@famdouma.nl>
|
|
|
- * @author Thomas Müller <thomas.mueller@tmit.eu>
|
|
|
- * @author Vincent Petry <vincent@nextcloud.com>
|
|
|
*
|
|
|
* @license AGPL-3.0
|
|
|
*
|
|
@@ -27,143 +16,362 @@
|
|
|
* 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/>
|
|
|
+ * 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 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 StreamWrapper {
|
|
|
- private $password;
|
|
|
- private $user;
|
|
|
+class FTP extends Common {
|
|
|
+ use CopyDirectory;
|
|
|
+
|
|
|
+ private $root;
|
|
|
private $host;
|
|
|
+ private $password;
|
|
|
+ private $username;
|
|
|
private $secure;
|
|
|
- private $root;
|
|
|
+ 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;
|
|
|
+ $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 (substr($this->root, -1) !== '/') {
|
|
|
- $this->root .= '/';
|
|
|
+ if ($this->utf8Mode) {
|
|
|
+ if (!$this->connection->setUtf8Mode()) {
|
|
|
+ throw new StorageNotAvailableException("Could not set UTF-8 mode");
|
|
|
+ }
|
|
|
}
|
|
|
- } else {
|
|
|
- throw new \Exception('Creating FTP storage failed');
|
|
|
}
|
|
|
+
|
|
|
+ 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';
|
|
|
+ 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->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 {
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
}
|
|
|
- $url .= '://'.urlencode($this->user).':'.urlencode($this->password).'@'.$this->host.$this->root.$path;
|
|
|
- return $url;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Unlinks file or directory
|
|
|
* @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 $this->rmdir($path);
|
|
|
+ return 'dir';
|
|
|
+ } elseif ($this->is_file($path)) {
|
|
|
+ return 'file';
|
|
|
} else {
|
|
|
- $url = $this->constructUrl($path);
|
|
|
- $result = unlink($url);
|
|
|
- clearstatcache(true, $url);
|
|
|
- return $result;
|
|
|
+ return false;
|
|
|
}
|
|
|
}
|
|
|
- public function fopen($path,$mode) {
|
|
|
+
|
|
|
+ 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 = '';
|
|
|
- }
|
|
|
- $tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
|
|
|
- if ($this->file_exists($path)) {
|
|
|
- $this->getFile($path, $tmpFile);
|
|
|
+ if (!$this->isCreatable(dirname($path))) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
|
|
|
}
|
|
|
- $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 {
|
|
|
- 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);
|
|
|
|
|
|
- public function writeBack($tmpFile, $path) {
|
|
|
- $this->uploadFile($tmpFile, $path);
|
|
|
- unlink($tmpFile);
|
|
|
+ if (!$result) {
|
|
|
+ fclose($stream);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return $stream;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * check if php-ftp is installed
|
|
|
- */
|
|
|
- public static function checkDependencies() {
|
|
|
- if (function_exists('ftp_login')) {
|
|
|
- return true;
|
|
|
+ public function touch($path, $mtime = null) {
|
|
|
+ if ($this->file_exists($path)) {
|
|
|
+ return false;
|
|
|
} else {
|
|
|
- return ['ftp'];
|
|
|
+ $this->file_put_contents($path, '');
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public function rename($path1, $path2) {
|
|
|
+ $this->unlink($path2);
|
|
|
+ return $this->getConnection()->rename($this->buildPath($path1), $this->buildPath($path2));
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
}
|
|
|
}
|
|
|
}
|