migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,790 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author André Gaul <gaul@web-yard.de>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christian Berendt <berendt@b1-systems.de>
* @author Christopher T. Johnson <ctjctj@gmail.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author enoch <lanxenet@hotmail.com>
* @author Johan Björk <johanimon@gmail.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Julius Härtl <jus@bitgrid.net>
* @author Martin Mattel <martin.mattel@diemattels.at>
* @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
*
* 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 Aws\S3\Exception\S3Exception;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Files\Cache\CacheEntry;
use OC\Files\ObjectStore\S3ConnectionTrait;
use OC\Files\ObjectStore\S3ObjectTrait;
use OCP\Cache\CappedMemoryCache;
use OCP\Constants;
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeDetector;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\Server;
use Psr\Log\LoggerInterface;
class AmazonS3 extends \OC\Files\Storage\Common {
use S3ConnectionTrait;
use S3ObjectTrait;
private LoggerInterface $logger;
public function needsPartFile() {
return false;
}
/** @var CappedMemoryCache<array|false> */
private CappedMemoryCache $objectCache;
/** @var CappedMemoryCache<bool> */
private CappedMemoryCache $directoryCache;
/** @var CappedMemoryCache<array> */
private CappedMemoryCache $filesCache;
private IMimeTypeDetector $mimeDetector;
private ?bool $versioningEnabled = null;
private ICache $memCache;
public function __construct($parameters) {
parent::__construct($parameters);
$this->parseParams($parameters);
$this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']);
$this->objectCache = new CappedMemoryCache();
$this->directoryCache = new CappedMemoryCache();
$this->filesCache = new CappedMemoryCache();
$this->mimeDetector = Server::get(IMimeTypeDetector::class);
/** @var ICacheFactory $cacheFactory */
$cacheFactory = Server::get(ICacheFactory::class);
$this->memCache = $cacheFactory->createLocal('s3-external');
$this->logger = Server::get(LoggerInterface::class);
}
/**
* @param string $path
* @return string correctly encoded path
*/
private function normalizePath($path) {
$path = trim($path, '/');
if (!$path) {
$path = '.';
}
return $path;
}
private function isRoot($path) {
return $path === '.';
}
private function cleanKey($path) {
if ($this->isRoot($path)) {
return '/';
}
return $path;
}
private function clearCache() {
$this->objectCache = new CappedMemoryCache();
$this->directoryCache = new CappedMemoryCache();
$this->filesCache = new CappedMemoryCache();
}
private function invalidateCache($key) {
unset($this->objectCache[$key]);
$keys = array_keys($this->objectCache->getData());
$keyLength = strlen($key);
foreach ($keys as $existingKey) {
if (substr($existingKey, 0, $keyLength) === $key) {
unset($this->objectCache[$existingKey]);
}
}
unset($this->filesCache[$key]);
$keys = array_keys($this->directoryCache->getData());
$keyLength = strlen($key);
foreach ($keys as $existingKey) {
if (substr($existingKey, 0, $keyLength) === $key) {
unset($this->directoryCache[$existingKey]);
}
}
unset($this->directoryCache[$key]);
}
/**
* @return array|false
*/
private function headObject(string $key) {
if (!isset($this->objectCache[$key])) {
try {
$this->objectCache[$key] = $this->getConnection()->headObject([
'Bucket' => $this->bucket,
'Key' => $key
])->toArray();
} catch (S3Exception $e) {
if ($e->getStatusCode() >= 500) {
throw $e;
}
$this->objectCache[$key] = false;
}
}
if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]["Key"])) {
/** @psalm-suppress InvalidArgument Psalm doesn't understand nested arrays well */
$this->objectCache[$key]["Key"] = $key;
}
return $this->objectCache[$key];
}
/**
* Return true if directory exists
*
* There are no folders in s3. A folder like structure could be archived
* by prefixing files with the folder name.
*
* Implementation from flysystem-aws-s3-v3:
* https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694
*
* @param $path
* @return bool
* @throws \Exception
*/
private function doesDirectoryExist($path) {
if ($path === '.' || $path === '') {
return true;
}
$path = rtrim($path, '/') . '/';
if (isset($this->directoryCache[$path])) {
return $this->directoryCache[$path];
}
try {
// Maybe this isn't an actual key, but a prefix.
// Do a prefix listing of objects to determine.
$result = $this->getConnection()->listObjectsV2([
'Bucket' => $this->bucket,
'Prefix' => $path,
'MaxKeys' => 1,
]);
if (isset($result['Contents'])) {
$this->directoryCache[$path] = true;
return true;
}
// empty directories have their own object
$object = $this->headObject($path);
if ($object) {
$this->directoryCache[$path] = true;
return true;
}
} catch (S3Exception $e) {
if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) {
$this->directoryCache[$path] = false;
}
throw $e;
}
$this->directoryCache[$path] = false;
return false;
}
/**
* Remove a file or folder
*
* @param string $path
* @return bool
*/
protected function remove($path) {
// remember fileType to reduce http calls
$fileType = $this->filetype($path);
if ($fileType === 'dir') {
return $this->rmdir($path);
} elseif ($fileType === 'file') {
return $this->unlink($path);
} else {
return false;
}
}
public function mkdir($path) {
$path = $this->normalizePath($path);
if ($this->is_dir($path)) {
return false;
}
try {
$this->getConnection()->putObject([
'Bucket' => $this->bucket,
'Key' => $path . '/',
'Body' => '',
'ContentType' => FileInfo::MIMETYPE_FOLDER
]);
$this->testTimeout();
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
$this->invalidateCache($path);
return true;
}
public function file_exists($path) {
return $this->filetype($path) !== false;
}
public function rmdir($path) {
$path = $this->normalizePath($path);
if ($this->isRoot($path)) {
return $this->clearBucket();
}
if (!$this->file_exists($path)) {
return false;
}
$this->invalidateCache($path);
return $this->batchDelete($path);
}
protected function clearBucket() {
$this->clearCache();
return $this->batchDelete();
}
private function batchDelete($path = null) {
// TODO explore using https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.S3.BatchDelete.html
$params = [
'Bucket' => $this->bucket
];
if ($path !== null) {
$params['Prefix'] = $path . '/';
}
try {
$connection = $this->getConnection();
// Since there are no real directories on S3, we need
// to delete all objects prefixed with the path.
do {
// instead of the iterator, manually loop over the list ...
$objects = $connection->listObjects($params);
// ... so we can delete the files in batches
if (isset($objects['Contents'])) {
$connection->deleteObjects([
'Bucket' => $this->bucket,
'Delete' => [
'Objects' => $objects['Contents']
]
]);
$this->testTimeout();
}
// we reached the end when the list is no longer truncated
} while ($objects['IsTruncated']);
if ($path !== '' && $path !== null) {
$this->deleteObject($path);
}
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
return true;
}
public function opendir($path) {
try {
$content = iterator_to_array($this->getDirectoryContent($path));
return IteratorDirectory::wrap(array_map(function (array $item) {
return $item['name'];
}, $content));
} catch (S3Exception $e) {
return false;
}
}
public function stat($path) {
$path = $this->normalizePath($path);
if ($this->is_dir($path)) {
$stat = $this->getDirectoryMetaData($path);
} else {
$object = $this->headObject($path);
if ($object === false) {
return false;
}
$stat = $this->objectToMetaData($object);
}
$stat['atime'] = time();
return $stat;
}
/**
* Return content length for object
*
* When the information is already present (e.g. opendir has been called before)
* this value is return. Otherwise a headObject is emitted.
*
* @param $path
* @return int|mixed
*/
private function getContentLength($path) {
if (isset($this->filesCache[$path])) {
return (int)$this->filesCache[$path]['ContentLength'];
}
$result = $this->headObject($path);
if (isset($result['ContentLength'])) {
return (int)$result['ContentLength'];
}
return 0;
}
/**
* Return last modified for object
*
* When the information is already present (e.g. opendir has been called before)
* this value is return. Otherwise a headObject is emitted.
*
* @param $path
* @return mixed|string
*/
private function getLastModified($path) {
if (isset($this->filesCache[$path])) {
return $this->filesCache[$path]['LastModified'];
}
$result = $this->headObject($path);
if (isset($result['LastModified'])) {
return $result['LastModified'];
}
return 'now';
}
public function is_dir($path) {
$path = $this->normalizePath($path);
if (isset($this->filesCache[$path])) {
return false;
}
try {
return $this->doesDirectoryExist($path);
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
}
public function filetype($path) {
$path = $this->normalizePath($path);
if ($this->isRoot($path)) {
return 'dir';
}
try {
if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) {
return 'dir';
}
if (isset($this->filesCache[$path]) || $this->headObject($path)) {
return 'file';
}
if ($this->doesDirectoryExist($path)) {
return 'dir';
}
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
return false;
}
public function getPermissions($path) {
$type = $this->filetype($path);
if (!$type) {
return 0;
}
return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
}
public function unlink($path) {
$path = $this->normalizePath($path);
if ($this->is_dir($path)) {
return $this->rmdir($path);
}
try {
$this->deleteObject($path);
$this->invalidateCache($path);
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
return true;
}
public function fopen($path, $mode) {
$path = $this->normalizePath($path);
switch ($mode) {
case 'r':
case 'rb':
// Don't try to fetch empty files
$stat = $this->stat($path);
if (is_array($stat) && isset($stat['size']) && $stat['size'] === 0) {
return fopen('php://memory', $mode);
}
try {
return $this->readObject($path);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
case 'w':
case 'wb':
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
$handle = fopen($tmpFile, 'w');
return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
$this->writeBack($tmpFile, $path);
});
case 'a':
case 'ab':
case 'r+':
case 'w+':
case 'wb+':
case 'a+':
case 'x':
case 'x+':
case 'c':
case 'c+':
if (strrpos($path, '.') !== false) {
$ext = substr($path, strrpos($path, '.'));
} else {
$ext = '';
}
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
if ($this->file_exists($path)) {
$source = $this->readObject($path);
file_put_contents($tmpFile, $source);
}
$handle = fopen($tmpFile, $mode);
return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
$this->writeBack($tmpFile, $path);
});
}
return false;
}
public function touch($path, $mtime = null) {
if (is_null($mtime)) {
$mtime = time();
}
$metadata = [
'lastmodified' => gmdate(\DateTime::RFC1123, $mtime)
];
try {
if ($this->file_exists($path)) {
return false;
}
$mimeType = $this->mimeDetector->detectPath($path);
$this->getConnection()->putObject([
'Bucket' => $this->bucket,
'Key' => $this->cleanKey($path),
'Metadata' => $metadata,
'Body' => '',
'ContentType' => $mimeType,
'MetadataDirective' => 'REPLACE',
]);
$this->testTimeout();
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
$this->invalidateCache($path);
return true;
}
public function copy($source, $target, $isFile = null) {
$source = $this->normalizePath($source);
$target = $this->normalizePath($target);
if ($isFile === true || $this->is_file($source)) {
try {
$this->copyObject($source, $target, [
'StorageClass' => $this->storageClass,
]);
$this->testTimeout();
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
} else {
$this->remove($target);
try {
$this->mkdir($target);
$this->testTimeout();
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
foreach ($this->getDirectoryContent($source) as $item) {
$childSource = $source . '/' . $item['name'];
$childTarget = $target . '/' . $item['name'];
$this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
}
}
$this->invalidateCache($target);
return true;
}
public function rename($source, $target) {
$source = $this->normalizePath($source);
$target = $this->normalizePath($target);
if ($this->is_file($source)) {
if ($this->copy($source, $target) === false) {
return false;
}
if ($this->unlink($source) === false) {
$this->unlink($target);
return false;
}
} else {
if ($this->copy($source, $target) === false) {
return false;
}
if ($this->rmdir($source) === false) {
$this->rmdir($target);
return false;
}
}
return true;
}
public function test() {
$this->getConnection()->headBucket([
'Bucket' => $this->bucket
]);
return true;
}
public function getId() {
return $this->id;
}
public function writeBack($tmpFile, $path) {
try {
$source = fopen($tmpFile, 'r');
$this->writeObject($path, $source, $this->mimeDetector->detectPath($path));
$this->invalidateCache($path);
unlink($tmpFile);
return true;
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
}
/**
* check if curl is installed
*/
public static function checkDependencies() {
return true;
}
public function getDirectoryContent($directory): \Traversable {
$path = $this->normalizePath($directory);
if ($this->isRoot($path)) {
$path = '';
} else {
$path .= '/';
}
$results = $this->getConnection()->getPaginator('ListObjectsV2', [
'Bucket' => $this->bucket,
'Delimiter' => '/',
'Prefix' => $path,
]);
foreach ($results as $result) {
// sub folders
if (is_array($result['CommonPrefixes'])) {
foreach ($result['CommonPrefixes'] as $prefix) {
$dir = $this->getDirectoryMetaData($prefix['Prefix']);
if ($dir) {
yield $dir;
}
}
}
if (is_array($result['Contents'])) {
foreach ($result['Contents'] as $object) {
$this->objectCache[$object['Key']] = $object;
if ($object['Key'] !== $path) {
yield $this->objectToMetaData($object);
}
}
}
}
}
private function objectToMetaData(array $object): array {
return [
'name' => basename($object['Key']),
'mimetype' => $this->mimeDetector->detectPath($object['Key']),
'mtime' => strtotime($object['LastModified']),
'storage_mtime' => strtotime($object['LastModified']),
'etag' => trim($object['ETag'], '"'),
'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
'size' => (int)($object['Size'] ?? $object['ContentLength']),
];
}
private function getDirectoryMetaData(string $path): ?array {
$path = trim($path, '/');
// when versioning is enabled, delete markers are returned as part of CommonPrefixes
// resulting in "ghost" folders, verify that each folder actually exists
if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) {
return null;
}
$cacheEntry = $this->getCache()->get($path);
if ($cacheEntry instanceof CacheEntry) {
return $cacheEntry->getData();
} else {
return [
'name' => basename($path),
'mimetype' => FileInfo::MIMETYPE_FOLDER,
'mtime' => time(),
'storage_mtime' => time(),
'etag' => uniqid(),
'permissions' => Constants::PERMISSION_ALL,
'size' => -1,
];
}
}
public function versioningEnabled(): bool {
if ($this->versioningEnabled === null) {
$cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
if ($cached === null) {
$this->versioningEnabled = $this->getVersioningStatusFromBucket();
$this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
} else {
$this->versioningEnabled = $cached;
}
}
return $this->versioningEnabled;
}
protected function getVersioningStatusFromBucket(): bool {
try {
$result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]);
return $result->get('Status') === 'Enabled';
} catch (S3Exception $s3Exception) {
// This is needed for compatibility with Storj gateway which does not support versioning yet
if ($s3Exception->getAwsErrorCode() === 'NotImplemented' || $s3Exception->getAwsErrorCode() === 'AccessDenied') {
return false;
}
throw $s3Exception;
}
}
public function hasUpdated($path, $time) {
// for files we can get the proper mtime
if ($path !== '' && $object = $this->headObject($path)) {
$stat = $this->objectToMetaData($object);
return $stat['mtime'] > $time;
} else {
// for directories, the only real option we have is to do a prefix listing and iterate over all objects
// however, since this is just as expensive as just re-scanning the directory, we can simply return true
// and have the scanner figure out if anything has actually changed
return true;
}
}
}
@@ -0,0 +1,380 @@
<?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;
}
}
}
@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Lib\Storage;
/**
* Low level wrapper around the ftp functions that smooths over some difference between servers
*/
class FtpConnection {
/** @var resource|\FTP\Connection */
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 $source, string $target) {
return @ftp_rename($this->connection, $source, $target);
}
public function mdtm(string $path): int {
$result = @ftp_mdtm($this->connection, $path);
// filezilla doesn't like empty path with mdtm
if ($result === -1 && $path === "") {
$result = @ftp_mdtm($this->connection, "/");
}
return $result;
}
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 (str_contains($name, '/')) {
$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 (str_contains($file['name'], '/')) {
$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>') ? 'dir' : 'file';
}
return [
'type' => $type,
'name' => $name,
'modify' => $formattedDate,
'perm' => ($type === 'file') ? 'adfrw' : 'flcdmpe',
'size' => (int)$size,
];
}
}
@@ -0,0 +1,83 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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 OCP\Files\Storage\IDisableEncryptionStorage;
use Sabre\DAV\Client;
/**
* Nextcloud backend for external storage based on DAV backend.
*
* The Nextcloud URL consists of three parts:
* http://%host/%context/remote.php/webdav/%root
*
*/
class OwnCloud extends \OC\Files\Storage\DAV implements IDisableEncryptionStorage {
public const OC_URL_SUFFIX = 'remote.php/webdav';
public function __construct($params) {
// extract context path from host if specified
// (owncloud install path on host)
$host = $params['host'];
// strip protocol
if (substr($host, 0, 8) === "https://") {
$host = substr($host, 8);
$params['secure'] = true;
} elseif (substr($host, 0, 7) === "http://") {
$host = substr($host, 7);
$params['secure'] = false;
}
$contextPath = '';
$hostSlashPos = strpos($host, '/');
if ($hostSlashPos !== false) {
$contextPath = substr($host, $hostSlashPos);
$host = substr($host, 0, $hostSlashPos);
}
if (!str_ends_with($contextPath, '/')) {
$contextPath .= '/';
}
if (isset($params['root'])) {
$root = '/' . ltrim($params['root'], '/');
} else {
$root = '/';
}
$params['host'] = $host;
$params['root'] = $contextPath . self::OC_URL_SUFFIX . $root;
$params['authType'] = Client::AUTH_BASIC;
parent::__construct($params);
}
public function needsPartFile() {
return false;
}
}
@@ -0,0 +1,587 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Andreas Fischer <bantu@owncloud.com>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author hkjolhede <hkjolhede@gmail.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lennart Rosam <lennart.rosam@medien-systempartner.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Ross Nicoll <jrn@jrn.me.uk>
* @author SA <stephen@mthosting.net>
* @author Senorsen <senorsen.zhang@gmail.com>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\CountWrapper;
use Icewind\Streams\IteratorDirectory;
use Icewind\Streams\RetryWrapper;
use OC\Files\Storage\Common;
use OCP\Constants;
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeDetector;
use phpseclib\Net\SFTP\Stream;
/**
* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
* provide access to SFTP servers.
*/
class SFTP extends Common {
private $host;
private $user;
private $root;
private $port = 22;
private $auth = [];
/**
* @var \phpseclib\Net\SFTP
*/
protected $client;
private IMimeTypeDetector $mimeTypeDetector;
public const COPY_CHUNK_SIZE = 8 * 1024 * 1024;
/**
* @param string $host protocol://server:port
* @return array [$server, $port]
*/
private function splitHost($host) {
$input = $host;
if (!str_contains($host, '://')) {
// add a protocol to fix parse_url behavior with ipv6
$host = 'http://' . $host;
}
$parsed = parse_url($host);
if (is_array($parsed) && isset($parsed['port'])) {
return [$parsed['host'], $parsed['port']];
} elseif (is_array($parsed)) {
return [$parsed['host'], 22];
} else {
return [$input, 22];
}
}
/**
* {@inheritdoc}
*/
public function __construct($params) {
// Register sftp://
Stream::register();
$parsedHost = $this->splitHost($params['host']);
$this->host = $parsedHost[0];
$this->port = $parsedHost[1];
if (!isset($params['user'])) {
throw new \UnexpectedValueException('no authentication parameters specified');
}
$this->user = $params['user'];
if (isset($params['public_key_auth'])) {
$this->auth[] = $params['public_key_auth'];
}
if (isset($params['password']) && $params['password'] !== '') {
$this->auth[] = $params['password'];
}
if ($this->auth === []) {
throw new \UnexpectedValueException('no authentication parameters specified');
}
$this->root
= isset($params['root']) ? $this->cleanPath($params['root']) : '/';
$this->root = '/' . ltrim($this->root, '/');
$this->root = rtrim($this->root, '/') . '/';
$this->mimeTypeDetector = \OC::$server->get(IMimeTypeDetector::class);
}
/**
* Returns the connection.
*
* @return \phpseclib\Net\SFTP connected client instance
* @throws \Exception when the connection failed
*/
public function getConnection() {
if (!is_null($this->client)) {
return $this->client;
}
$hostKeys = $this->readHostKeys();
$this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
// The SSH Host Key MUST be verified before login().
$currentHostKey = $this->client->getServerPublicHostKey();
if (array_key_exists($this->host, $hostKeys)) {
if ($hostKeys[$this->host] !== $currentHostKey) {
throw new \Exception('Host public key does not match known key');
}
} else {
$hostKeys[$this->host] = $currentHostKey;
$this->writeHostKeys($hostKeys);
}
$login = false;
foreach ($this->auth as $auth) {
/** @psalm-suppress TooManyArguments */
$login = $this->client->login($this->user, $auth);
if ($login === true) {
break;
}
}
if ($login === false) {
throw new \Exception('Login failed');
}
return $this->client;
}
/**
* {@inheritdoc}
*/
public function test() {
if (
!isset($this->host)
|| !isset($this->user)
) {
return false;
}
return $this->getConnection()->nlist() !== false;
}
/**
* {@inheritdoc}
*/
public function getId() {
$id = 'sftp::' . $this->user . '@' . $this->host;
if ($this->port !== 22) {
$id .= ':' . $this->port;
}
// note: this will double the root slash,
// we should not change it to keep compatible with
// old storage ids
$id .= '/' . $this->root;
return $id;
}
/**
* @return string
*/
public function getHost() {
return $this->host;
}
/**
* @return string
*/
public function getRoot() {
return $this->root;
}
/**
* @return mixed
*/
public function getUser() {
return $this->user;
}
/**
* @param string $path
* @return string
*/
private function absPath($path) {
return $this->root . $this->cleanPath($path);
}
/**
* @return string|false
*/
private function hostKeysPath() {
try {
$storage_view = \OCP\Files::getStorage('files_external');
if ($storage_view) {
return \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') .
$storage_view->getAbsolutePath('') .
'ssh_hostKeys';
}
} catch (\Exception $e) {
}
return false;
}
/**
* @param $keys
* @return bool
*/
protected function writeHostKeys($keys) {
try {
$keyPath = $this->hostKeysPath();
if ($keyPath && file_exists($keyPath)) {
$fp = fopen($keyPath, 'w');
foreach ($keys as $host => $key) {
fwrite($fp, $host . '::' . $key . "\n");
}
fclose($fp);
return true;
}
} catch (\Exception $e) {
}
return false;
}
/**
* @return array
*/
protected function readHostKeys() {
try {
$keyPath = $this->hostKeysPath();
if (file_exists($keyPath)) {
$hosts = [];
$keys = [];
$lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines) {
foreach ($lines as $line) {
$hostKeyArray = explode("::", $line, 2);
if (count($hostKeyArray) === 2) {
$hosts[] = $hostKeyArray[0];
$keys[] = $hostKeyArray[1];
}
}
return array_combine($hosts, $keys);
}
}
} catch (\Exception $e) {
}
return [];
}
/**
* {@inheritdoc}
*/
public function mkdir($path) {
try {
return $this->getConnection()->mkdir($this->absPath($path));
} catch (\Exception $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function rmdir($path) {
try {
$result = $this->getConnection()->delete($this->absPath($path), true);
// workaround: stray stat cache entry when deleting empty folders
// see https://github.com/phpseclib/phpseclib/issues/706
$this->getConnection()->clearStatCache();
return $result;
} catch (\Exception $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function opendir($path) {
try {
$list = $this->getConnection()->nlist($this->absPath($path));
if ($list === false) {
return false;
}
$id = md5('sftp:' . $path);
$dirStream = [];
foreach ($list as $file) {
if ($file !== '.' && $file !== '..') {
$dirStream[] = $file;
}
}
return IteratorDirectory::wrap($dirStream);
} catch (\Exception $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function filetype($path) {
try {
$stat = $this->getConnection()->stat($this->absPath($path));
if (!is_array($stat) || !array_key_exists('type', $stat)) {
return false;
}
if ((int) $stat['type'] === NET_SFTP_TYPE_REGULAR) {
return 'file';
}
if ((int) $stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
return 'dir';
}
} catch (\Exception $e) {
}
return false;
}
/**
* {@inheritdoc}
*/
public function file_exists($path) {
try {
return $this->getConnection()->stat($this->absPath($path)) !== false;
} catch (\Exception $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function unlink($path) {
try {
return $this->getConnection()->delete($this->absPath($path), true);
} catch (\Exception $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function fopen($path, $mode) {
try {
$absPath = $this->absPath($path);
$connection = $this->getConnection();
switch ($mode) {
case 'r':
case 'rb':
$stat = $this->stat($path);
if (!$stat) {
return false;
}
SFTPReadStream::register();
$context = stream_context_create(['sftp' => ['session' => $connection, 'size' => $stat['size']]]);
$handle = fopen('sftpread://' . trim($absPath, '/'), 'r', false, $context);
return RetryWrapper::wrap($handle);
case 'w':
case 'wb':
SFTPWriteStream::register();
// the SFTPWriteStream doesn't go through the "normal" methods so it doesn't clear the stat cache.
$connection->_remove_from_stat_cache($absPath);
$context = stream_context_create(['sftp' => ['session' => $connection]]);
return fopen('sftpwrite://' . trim($absPath, '/'), 'w', false, $context);
case 'a':
case 'ab':
case 'r+':
case 'w+':
case 'wb+':
case 'a+':
case 'x':
case 'x+':
case 'c':
case 'c+':
$context = stream_context_create(['sftp' => ['session' => $connection]]);
$handle = fopen($this->constructUrl($path), $mode, false, $context);
return RetryWrapper::wrap($handle);
}
} catch (\Exception $e) {
}
return false;
}
/**
* {@inheritdoc}
*/
public function touch($path, $mtime = null) {
try {
if (!is_null($mtime)) {
return false;
}
if (!$this->file_exists($path)) {
$this->getConnection()->put($this->absPath($path), '');
} else {
return false;
}
} catch (\Exception $e) {
return false;
}
return true;
}
/**
* @param string $path
* @param string $target
* @throws \Exception
*/
public function getFile($path, $target) {
$this->getConnection()->get($path, $target);
}
/**
* {@inheritdoc}
*/
public function rename($source, $target) {
try {
if ($this->file_exists($target)) {
$this->unlink($target);
}
return $this->getConnection()->rename(
$this->absPath($source),
$this->absPath($target)
);
} catch (\Exception $e) {
return false;
}
}
/**
* @return array{mtime: int, size: int, ctime: int}|false
*/
public function stat($path) {
try {
$stat = $this->getConnection()->stat($this->absPath($path));
$mtime = $stat ? (int)$stat['mtime'] : -1;
$size = $stat ? (int)$stat['size'] : 0;
return ['mtime' => $mtime, 'size' => $size, 'ctime' => -1];
} catch (\Exception $e) {
return false;
}
}
/**
* @param string $path
* @return string
*/
public function constructUrl($path) {
// Do not pass the password here. We want to use the Net_SFTP object
// supplied via stream context or fail. We only supply username and
// hostname because this might show up in logs (they are not used).
$url = 'sftp://' . urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path;
return $url;
}
public function file_put_contents($path, $data) {
/** @psalm-suppress InternalMethod */
$result = $this->getConnection()->put($this->absPath($path), $data);
if ($result) {
return strlen($data);
} else {
return false;
}
}
public function writeStream(string $path, $stream, int $size = null): int {
if ($size === null) {
$stream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size) {
$size = $writtenSize;
});
if (!$stream) {
throw new \Exception("Failed to wrap stream");
}
}
/** @psalm-suppress InternalMethod */
$result = $this->getConnection()->put($this->absPath($path), $stream);
fclose($stream);
if ($result) {
return $size;
} else {
throw new \Exception("Failed to write steam to sftp storage");
}
}
public function copy($source, $target) {
if ($this->is_dir($source) || $this->is_dir($target)) {
return parent::copy($source, $target);
} else {
$absSource = $this->absPath($source);
$absTarget = $this->absPath($target);
$connection = $this->getConnection();
$size = $connection->size($absSource);
if ($size === false) {
return false;
}
for ($i = 0; $i < $size; $i += self::COPY_CHUNK_SIZE) {
/** @psalm-suppress InvalidArgument */
$chunk = $connection->get($absSource, false, $i, self::COPY_CHUNK_SIZE);
if ($chunk === false) {
return false;
}
/** @psalm-suppress InternalMethod */
if (!$connection->put($absTarget, $chunk, \phpseclib\Net\SFTP::SOURCE_STRING, $i)) {
return false;
}
}
return true;
}
}
public function getPermissions($path) {
$stat = $this->getConnection()->stat($this->absPath($path));
if (!$stat) {
return 0;
}
if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
return Constants::PERMISSION_ALL;
} else {
return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
}
}
public function getMetaData($path) {
$stat = $this->getConnection()->stat($this->absPath($path));
if (!$stat) {
return null;
}
if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
$stat['permissions'] = Constants::PERMISSION_ALL;
} else {
$stat['permissions'] = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
}
if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
$stat['size'] = -1;
$stat['mimetype'] = FileInfo::MIMETYPE_FOLDER;
} else {
$stat['mimetype'] = $this->mimeTypeDetector->detectPath($path);
}
$stat['etag'] = $this->getETag($path);
$stat['storage_mtime'] = $stat['mtime'];
$stat['name'] = basename($path);
$keys = ['size', 'mtime', 'mimetype', 'etag', 'storage_mtime', 'permissions', 'name'];
return array_intersect_key($stat, array_flip($keys));
}
}
@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Lib\Storage;
use Icewind\Streams\File;
use phpseclib\Net\SSH2;
class SFTPReadStream implements File {
/** @var resource */
public $context;
/** @var \phpseclib\Net\SFTP */
private $sftp;
/** @var string */
private $handle;
/** @var int */
private $internalPosition = 0;
/** @var int */
private $readPosition = 0;
/** @var bool */
private $eof = false;
private $buffer = '';
private bool $pendingRead = false;
private int $size = 0;
public static function register($protocol = 'sftpread') {
if (in_array($protocol, stream_get_wrappers(), true)) {
return false;
}
return stream_wrapper_register($protocol, get_called_class());
}
/**
* Load the source from the stream context and return the context options
*
* @param string $name
* @throws \BadMethodCallException
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) {
$this->sftp = $context['session'];
} else {
throw new \BadMethodCallException('Invalid context, session not set');
}
if (isset($context['size'])) {
$this->size = $context['size'];
}
return $context;
}
public function stream_open($path, $mode, $options, &$opened_path) {
[, $path] = explode('://', $path);
$path = '/' . ltrim($path);
$path = str_replace('//', '/', $path);
$this->loadContext('sftp');
if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$remote_file = $this->sftp->_realpath($path);
if ($remote_file === false) {
return false;
}
$packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0);
if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
return false;
}
$response = $this->sftp->_get_sftp_packet();
switch ($this->sftp->packet_type) {
case NET_SFTP_HANDLE:
$this->handle = substr($response, 4);
break;
case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
$this->sftp->_logError($response);
return false;
default:
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
return false;
}
$this->request_chunk(256 * 1024);
return true;
}
public function stream_seek($offset, $whence = SEEK_SET) {
switch ($whence) {
case SEEK_SET:
$this->seekTo($offset);
break;
case SEEK_CUR:
$this->seekTo($this->readPosition + $offset);
break;
case SEEK_END:
$this->seekTo($this->size + $offset);
break;
}
return true;
}
private function seekTo(int $offset): void {
$this->internalPosition = $offset;
$this->readPosition = $offset;
$this->buffer = '';
$this->request_chunk(256 * 1024);
}
public function stream_tell() {
return $this->readPosition;
}
public function stream_read($count) {
if (!$this->eof && strlen($this->buffer) < $count) {
$chunk = $this->read_chunk();
$this->buffer .= $chunk;
if (!$this->eof) {
$this->request_chunk(256 * 1024);
}
}
$data = substr($this->buffer, 0, $count);
$this->buffer = substr($this->buffer, $count);
$this->readPosition += strlen($data);
return $data;
}
private function request_chunk($size) {
if ($this->pendingRead) {
$this->sftp->_get_sftp_packet();
}
$packet = pack('Na*N3', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size);
$this->pendingRead = true;
return $this->sftp->_send_sftp_packet(NET_SFTP_READ, $packet);
}
private function read_chunk() {
$this->pendingRead = false;
$response = $this->sftp->_get_sftp_packet();
switch ($this->sftp->packet_type) {
case NET_SFTP_DATA:
$temp = substr($response, 4);
$len = strlen($temp);
$this->internalPosition += $len;
return $temp;
case NET_SFTP_STATUS:
[1 => $status] = unpack('N', substr($response, 0, 4));
if ($status == NET_SFTP_STATUS_EOF) {
$this->eof = true;
}
return '';
default:
return '';
}
}
public function stream_write($data) {
return false;
}
public function stream_set_option($option, $arg1, $arg2) {
return false;
}
public function stream_truncate($size) {
return false;
}
public function stream_stat() {
return false;
}
public function stream_lock($operation) {
return false;
}
public function stream_flush() {
return false;
}
public function stream_eof() {
return $this->eof;
}
public function stream_close() {
// we still have a read request incoming that needs to be handled before we can close
if ($this->pendingRead) {
$this->sftp->_get_sftp_packet();
}
if (!$this->sftp->_close_handle($this->handle)) {
return false;
}
return true;
}
}
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Lib\Storage;
use Icewind\Streams\File;
use phpseclib\Net\SSH2;
class SFTPWriteStream implements File {
/** @var resource */
public $context;
/** @var \phpseclib\Net\SFTP */
private $sftp;
/** @var string */
private $handle;
/** @var int */
private $internalPosition = 0;
/** @var int */
private $writePosition = 0;
/** @var bool */
private $eof = false;
private $buffer = '';
public static function register($protocol = 'sftpwrite') {
if (in_array($protocol, stream_get_wrappers(), true)) {
return false;
}
return stream_wrapper_register($protocol, get_called_class());
}
/**
* Load the source from the stream context and return the context options
*
* @param string $name
* @throws \BadMethodCallException
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) {
$this->sftp = $context['session'];
} else {
throw new \BadMethodCallException('Invalid context, session not set');
}
return $context;
}
public function stream_open($path, $mode, $options, &$opened_path) {
[, $path] = explode('://', $path);
$path = '/' . ltrim($path);
$path = str_replace('//', '/', $path);
$this->loadContext('sftp');
if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$remote_file = $this->sftp->_realpath($path);
if ($remote_file === false) {
return false;
}
$packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_TRUNCATE, 0);
if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
return false;
}
$response = $this->sftp->_get_sftp_packet();
switch ($this->sftp->packet_type) {
case NET_SFTP_HANDLE:
$this->handle = substr($response, 4);
break;
case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
$this->sftp->_logError($response);
return false;
default:
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
return false;
}
return true;
}
public function stream_seek($offset, $whence = SEEK_SET) {
return false;
}
public function stream_tell() {
return $this->writePosition;
}
public function stream_read($count) {
return false;
}
public function stream_write($data) {
$written = strlen($data);
$this->writePosition += $written;
$this->buffer .= $data;
if (strlen($this->buffer) > 64 * 1024) {
if (!$this->stream_flush()) {
return false;
}
}
return $written;
}
public function stream_set_option($option, $arg1, $arg2) {
return false;
}
public function stream_truncate($size) {
return false;
}
public function stream_stat() {
return false;
}
public function stream_lock($operation) {
return false;
}
public function stream_flush() {
$size = strlen($this->buffer);
$packet = pack('Na*N3a*', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size, $this->buffer);
if (!$this->sftp->_send_sftp_packet(NET_SFTP_WRITE, $packet)) {
return false;
}
$this->internalPosition += $size;
$this->buffer = '';
return $this->sftp->_read_put_responses(1);
}
public function stream_eof() {
return $this->eof;
}
public function stream_close() {
$this->stream_flush();
if (!$this->sftp->_close_handle($this->handle)) {
return false;
}
return true;
}
}
@@ -0,0 +1,786 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jesús Macias <jmacias@solidgear.es>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Juan Pablo Villafañez <jvillafanez@solidgear.es>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Julius Härtl <jus@bitgrid.net>
* @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 Roland Tapken <roland@bitarbeiter.net>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\SMB\ACL;
use Icewind\SMB\BasicAuth;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\ForbiddenException;
use Icewind\SMB\Exception\InvalidArgumentException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\Exception\OutOfSpaceException;
use Icewind\SMB\Exception\TimedOutException;
use Icewind\SMB\IFileInfo;
use Icewind\SMB\Native\NativeServer;
use Icewind\SMB\Options;
use Icewind\SMB\ServerFactory;
use Icewind\SMB\System;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Files\Filesystem;
use OC\Files\Storage\Common;
use OCA\Files_External\Lib\Notify\SMBNotifyHandler;
use OCP\Cache\CappedMemoryCache;
use OCP\Constants;
use OCP\Files\EntityTooLargeException;
use OCP\Files\Notify\IChange;
use OCP\Files\Notify\IRenameChange;
use OCP\Files\NotPermittedException;
use OCP\Files\Storage\INotifyStorage;
use OCP\Files\StorageAuthException;
use OCP\Files\StorageNotAvailableException;
use Psr\Log\LoggerInterface;
class SMB extends Common implements INotifyStorage {
/**
* @var \Icewind\SMB\IServer
*/
protected $server;
/**
* @var \Icewind\SMB\IShare
*/
protected $share;
/**
* @var string
*/
protected $root;
/** @var CappedMemoryCache<IFileInfo> */
protected CappedMemoryCache $statCache;
/** @var LoggerInterface */
protected $logger;
/** @var bool */
protected $showHidden;
private bool $caseSensitive;
/** @var bool */
protected $checkAcl;
public function __construct($params) {
if (!isset($params['host'])) {
throw new \Exception('Invalid configuration, no host provided');
}
if (isset($params['auth'])) {
$auth = $params['auth'];
} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
[$workgroup, $user] = $this->splitUser($params['user']);
$auth = new BasicAuth($user, $workgroup, $params['password']);
} else {
throw new \Exception('Invalid configuration, no credentials provided');
}
if (isset($params['logger'])) {
if (!$params['logger'] instanceof LoggerInterface) {
throw new \Exception(
'Invalid logger. Got '
. get_class($params['logger'])
. ' Expected ' . LoggerInterface::class
);
}
$this->logger = $params['logger'];
} else {
$this->logger = \OC::$server->get(LoggerInterface::class);
}
$options = new Options();
if (isset($params['timeout'])) {
$timeout = (int)$params['timeout'];
if ($timeout > 0) {
$options->setTimeout($timeout);
}
}
$serverFactory = new ServerFactory($options);
$this->server = $serverFactory->createServer($params['host'], $auth);
$this->share = $this->server->getShare(trim($params['share'], '/'));
$this->root = $params['root'] ?? '/';
$this->root = '/' . ltrim($this->root, '/');
$this->root = rtrim($this->root, '/') . '/';
$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
$this->caseSensitive = (bool) ($params['case_sensitive'] ?? true);
$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
$this->statCache = new CappedMemoryCache();
parent::__construct($params);
}
private function splitUser($user) {
if (str_contains($user, '/')) {
return explode('/', $user, 2);
} elseif (str_contains($user, '\\')) {
return explode('\\', $user);
}
return [null, $user];
}
/**
* @return string
*/
public function getId() {
// FIXME: double slash to keep compatible with the old storage ids,
// failure to do so will lead to creation of a new storage id and
// loss of shares from the storage
return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
}
/**
* @param string $path
* @return string
*/
protected function buildPath($path) {
return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
}
protected function relativePath($fullPath) {
if ($fullPath === $this->root) {
return '';
} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
return substr($fullPath, strlen($this->root));
} else {
return null;
}
}
/**
* @param string $path
* @return IFileInfo
* @throws StorageAuthException
*/
protected function getFileInfo($path) {
try {
$path = $this->buildPath($path);
$cached = $this->statCache[$path] ?? null;
if ($cached instanceof IFileInfo) {
return $cached;
} else {
$stat = $this->share->stat($path);
$this->statCache[$path] = $stat;
return $stat;
}
} catch (ConnectException $e) {
$this->throwUnavailable($e);
} catch (NotFoundException $e) {
throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
} catch (ForbiddenException $e) {
// with php-smbclient, this exception is thrown when the provided password is invalid.
// Possible is also ForbiddenException with a different error code, so we check it.
if ($e->getCode() === 1) {
$this->throwUnavailable($e);
}
throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e);
}
}
/**
* @param \Exception $e
* @return never
* @throws StorageAuthException
*/
protected function throwUnavailable(\Exception $e) {
$this->logger->error('Error while getting file info', ['exception' => $e]);
throw new StorageAuthException($e->getMessage(), $e);
}
/**
* get the acl from fileinfo that is relevant for the configured user
*
* @param IFileInfo $file
* @return ACL|null
*/
private function getACL(IFileInfo $file): ?ACL {
$acls = $file->getAcls();
foreach ($acls as $user => $acl) {
[, $user] = $this->splitUser($user); // strip domain
if ($user === $this->server->getAuth()->getUsername()) {
return $acl;
}
}
return null;
}
/**
* @param string $path
* @return \Generator<IFileInfo>
* @throws StorageNotAvailableException
*/
protected function getFolderContents($path): iterable {
try {
$path = ltrim($this->buildPath($path), '/');
try {
$files = $this->share->dir($path);
} catch (ForbiddenException $e) {
$this->logger->critical($e->getMessage(), ['exception' => $e]);
throw new NotPermittedException();
} catch (InvalidTypeException $e) {
return;
}
foreach ($files as $file) {
$this->statCache[$path . '/' . $file->getName()] = $file;
}
foreach ($files as $file) {
try {
// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
// so we trigger the below exceptions where applicable
$hide = $file->isHidden() && !$this->showHidden;
if ($this->checkAcl && $acl = $this->getACL($file)) {
// if there is no explicit deny, we assume it's allowed
// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
// additionally, it's better to have false negatives here then false positives
if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
$this->logger->debug('Hiding non readable entry ' . $file->getName());
continue;
}
}
if ($hide) {
$this->logger->debug('hiding hidden file ' . $file->getName());
}
if (!$hide) {
yield $file;
}
} catch (ForbiddenException $e) {
$this->logger->debug($e->getMessage(), ['exception' => $e]);
} catch (NotFoundException $e) {
$this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]);
}
}
} catch (ConnectException $e) {
$this->logger->error('Error while getting folder content', ['exception' => $e]);
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
} catch (NotFoundException $e) {
throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
}
}
/**
* @param IFileInfo $info
* @return array
*/
protected function formatInfo($info) {
$result = [
'size' => $info->getSize(),
'mtime' => $info->getMTime(),
];
if ($info->isDirectory()) {
$result['type'] = 'dir';
} else {
$result['type'] = 'file';
}
return $result;
}
/**
* Rename the files. If the source or the target is the root, the rename won't happen.
*
* @param string $source the old name of the path
* @param string $target the new name of the path
* @return bool true if the rename is successful, false otherwise
*/
public function rename($source, $target, $retry = true): bool {
if ($this->isRootDir($source) || $this->isRootDir($target)) {
return false;
}
if ($this->caseSensitive === false
&& mb_strtolower($target) === mb_strtolower($source)
) {
// Forbid changing case only on case-insensitive file system
return false;
}
$absoluteSource = $this->buildPath($source);
$absoluteTarget = $this->buildPath($target);
try {
$result = $this->share->rename($absoluteSource, $absoluteTarget);
} catch (AlreadyExistsException $e) {
if ($retry) {
$this->remove($target);
$result = $this->share->rename($absoluteSource, $absoluteTarget);
} else {
$this->logger->warning($e->getMessage(), ['exception' => $e]);
return false;
}
} catch (InvalidArgumentException $e) {
if ($retry) {
$this->remove($target);
$result = $this->share->rename($absoluteSource, $absoluteTarget);
} else {
$this->logger->warning($e->getMessage(), ['exception' => $e]);
return false;
}
} catch (\Exception $e) {
$this->logger->warning($e->getMessage(), ['exception' => $e]);
return false;
}
unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
return $result;
}
public function stat($path, $retry = true) {
try {
$result = $this->formatInfo($this->getFileInfo($path));
} catch (ForbiddenException $e) {
return false;
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (TimedOutException $e) {
if ($retry) {
return $this->stat($path, false);
} else {
throw $e;
}
}
if ($this->remoteIsShare() && $this->isRootDir($path)) {
$result['mtime'] = $this->shareMTime();
}
return $result;
}
/**
* get the best guess for the modification time of the share
*
* @return int
*/
private function shareMTime() {
$highestMTime = 0;
$files = $this->share->dir($this->root);
foreach ($files as $fileInfo) {
try {
if ($fileInfo->getMTime() > $highestMTime) {
$highestMTime = $fileInfo->getMTime();
}
} catch (NotFoundException $e) {
// Ignore this, can happen on unavailable DFS shares
} catch (ForbiddenException $e) {
// Ignore this too - it's a symlink
}
}
return $highestMTime;
}
/**
* Check if the path is our root dir (not the smb one)
*
* @param string $path the path
* @return bool
*/
private function isRootDir($path) {
return $path === '' || $path === '/' || $path === '.';
}
/**
* Check if our root points to a smb share
*
* @return bool true if our root points to a share false otherwise
*/
private function remoteIsShare() {
return $this->share->getName() && (!$this->root || $this->root === '/');
}
/**
* @param string $path
* @return bool
*/
public function unlink($path) {
if ($this->isRootDir($path)) {
return false;
}
try {
if ($this->is_dir($path)) {
return $this->rmdir($path);
} else {
$path = $this->buildPath($path);
unset($this->statCache[$path]);
$this->share->del($path);
return true;
}
} catch (NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
} catch (ConnectException $e) {
$this->logger->error('Error while deleting file', ['exception' => $e]);
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* check if a file or folder has been updated since $time
*
* @param string $path
* @param int $time
* @return bool
*/
public function hasUpdated($path, $time) {
if (!$path and $this->root === '/') {
// mtime doesn't work for shares, but giving the nature of the backend,
// doing a full update is still just fast enough
return true;
} else {
$actualTime = $this->filemtime($path);
return $actualTime > $time;
}
}
/**
* @param string $path
* @param string $mode
* @return resource|bool
*/
public function fopen($path, $mode) {
$fullPath = $this->buildPath($path);
try {
switch ($mode) {
case 'r':
case 'rb':
if (!$this->file_exists($path)) {
return false;
}
return $this->share->read($fullPath);
case 'w':
case 'wb':
$source = $this->share->write($fullPath);
return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
unset($this->statCache[$fullPath]);
});
case 'a':
case 'ab':
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, '.'));
} else {
$ext = '';
}
if ($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($ext);
}
$source = fopen($tmpFile, $mode);
$share = $this->share;
return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
unset($this->statCache[$fullPath]);
$share->put($tmpFile, $fullPath);
unlink($tmpFile);
});
}
return false;
} catch (NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
} catch (OutOfSpaceException $e) {
throw new EntityTooLargeException("not enough available space to create file", 0, $e);
} catch (ConnectException $e) {
$this->logger->error('Error while opening file', ['exception' => $e]);
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
}
}
public function rmdir($path) {
if ($this->isRootDir($path)) {
return false;
}
try {
$this->statCache = new CappedMemoryCache();
$content = $this->share->dir($this->buildPath($path));
foreach ($content as $file) {
if ($file->isDirectory()) {
$this->rmdir($path . '/' . $file->getName());
} else {
$this->share->del($file->getPath());
}
}
$this->share->rmdir($this->buildPath($path));
return true;
} catch (NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
} catch (ConnectException $e) {
$this->logger->error('Error while removing folder', ['exception' => $e]);
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
}
}
public function touch($path, $mtime = null) {
try {
if (!$this->file_exists($path)) {
$fh = $this->share->write($this->buildPath($path));
fclose($fh);
return true;
}
return false;
} catch (OutOfSpaceException $e) {
throw new EntityTooLargeException("not enough available space to create file", 0, $e);
} catch (ConnectException $e) {
$this->logger->error('Error while creating file', ['exception' => $e]);
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
}
}
public function getMetaData($path) {
try {
$fileInfo = $this->getFileInfo($path);
} catch (\OCP\Files\NotFoundException $e) {
return null;
} catch (ForbiddenException $e) {
return null;
}
if (!$fileInfo) {
return null;
}
return $this->getMetaDataFromFileInfo($fileInfo);
}
private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
$permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
if (
!$fileInfo->isReadOnly() || $fileInfo->isDirectory()
) {
$permissions += Constants::PERMISSION_DELETE;
$permissions += Constants::PERMISSION_UPDATE;
if ($fileInfo->isDirectory()) {
$permissions += Constants::PERMISSION_CREATE;
}
}
$data = [];
if ($fileInfo->isDirectory()) {
$data['mimetype'] = 'httpd/unix-directory';
} else {
$data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
}
$data['mtime'] = $fileInfo->getMTime();
if ($fileInfo->isDirectory()) {
$data['size'] = -1; //unknown
} else {
$data['size'] = $fileInfo->getSize();
}
$data['etag'] = $this->getETag($fileInfo->getPath());
$data['storage_mtime'] = $data['mtime'];
$data['permissions'] = $permissions;
$data['name'] = $fileInfo->getName();
return $data;
}
public function opendir($path) {
try {
$files = $this->getFolderContents($path);
} catch (NotFoundException $e) {
return false;
} catch (NotPermittedException $e) {
return false;
}
$names = array_map(function ($info) {
/** @var IFileInfo $info */
return $info->getName();
}, iterator_to_array($files));
return IteratorDirectory::wrap($names);
}
public function getDirectoryContent($directory): \Traversable {
try {
$files = $this->getFolderContents($directory);
foreach ($files as $file) {
yield $this->getMetaDataFromFileInfo($file);
}
} catch (NotFoundException $e) {
return;
} catch (NotPermittedException $e) {
return;
}
}
public function filetype($path) {
try {
return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
}
}
public function mkdir($path) {
$path = $this->buildPath($path);
try {
$this->share->mkdir($path);
return true;
} catch (ConnectException $e) {
$this->logger->error('Error while creating folder', ['exception' => $e]);
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
} catch (Exception $e) {
return false;
}
}
public function file_exists($path) {
try {
// Case sensitive filesystem doesn't matter for root directory
if ($this->caseSensitive === false && $path !== '') {
$filename = basename($path);
$siblings = $this->getDirectoryContent(dirname($this->buildPath($path)));
foreach ($siblings as $sibling) {
if ($sibling['name'] === $filename) {
return true;
}
}
return false;
}
$this->getFileInfo($path);
return true;
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
} catch (ConnectException $e) {
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
}
}
public function isReadable($path) {
try {
$info = $this->getFileInfo($path);
return $this->showHidden || !$info->isHidden();
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
}
}
public function isUpdatable($path) {
try {
$info = $this->getFileInfo($path);
// following windows behaviour for read-only folders: they can be written into
// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
}
}
public function isDeletable($path) {
try {
$info = $this->getFileInfo($path);
return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
}
}
/**
* check if smbclient is installed
*/
public static function checkDependencies() {
return (
(bool)\OC_Helper::findBinaryPath('smbclient')
|| NativeServer::available(new System())
) ? true : ['smbclient'];
}
/**
* Test a storage for availability
*
* @return bool
*/
public function test() {
try {
return parent::test();
} catch (StorageAuthException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
} catch (Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return false;
}
}
public function listen($path, callable $callback) {
$this->notify($path)->listen(function (IChange $change) use ($callback) {
if ($change instanceof IRenameChange) {
return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
} else {
return $callback($change->getType(), $change->getPath());
}
});
}
public function notify($path) {
$path = '/' . ltrim($path, '/');
$shareNotifyHandler = $this->share->notify($this->buildPath($path));
return new SMBNotifyHandler($shareNotifyHandler, $this->root);
}
}
@@ -0,0 +1,127 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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;
abstract class StreamWrapper extends \OC\Files\Storage\Common {
/**
* @param string $path
* @return string|null
*/
abstract public function constructUrl($path);
public function mkdir($path) {
return mkdir($this->constructUrl($path));
}
public function rmdir($path) {
if ($this->is_dir($path) && $this->isDeletable($path)) {
$dh = $this->opendir($path);
if (!is_resource($dh)) {
return false;
}
while (($file = readdir($dh)) !== false) {
if ($this->is_dir($path . '/' . $file)) {
$this->rmdir($path . '/' . $file);
} else {
$this->unlink($path . '/' . $file);
}
}
$url = $this->constructUrl($path);
$success = rmdir($url);
clearstatcache(false, $url);
return $success;
} else {
return false;
}
}
public function opendir($path) {
return opendir($this->constructUrl($path));
}
public function filetype($path) {
return @filetype($this->constructUrl($path));
}
public function file_exists($path) {
return file_exists($this->constructUrl($path));
}
public function unlink($path) {
$url = $this->constructUrl($path);
$success = unlink($url);
// normally unlink() is supposed to do this implicitly,
// but doing it anyway just to be sure
clearstatcache(false, $url);
return $success;
}
public function fopen($path, $mode) {
return fopen($this->constructUrl($path), $mode);
}
public function touch($path, $mtime = null) {
if ($this->file_exists($path)) {
if (is_null($mtime)) {
$fh = $this->fopen($path, 'a');
fwrite($fh, '');
fclose($fh);
return true;
} else {
return false; //not supported
}
} else {
$this->file_put_contents($path, '');
return true;
}
}
/**
* @param string $path
* @param string $target
*/
public function getFile($path, $target) {
return copy($this->constructUrl($path), $target);
}
/**
* @param string $target
*/
public function uploadFile($path, $target) {
return copy($path, $this->constructUrl($target));
}
public function rename($source, $target) {
return rename($this->constructUrl($source), $this->constructUrl($target));
}
public function stat($path) {
return stat($this->constructUrl($path));
}
}
@@ -0,0 +1,630 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Benjamin Liles <benliles@arch.tamu.edu>
* @author Christian Berendt <berendt@b1-systems.de>
* @author Christopher Bartz <bartz@dkrz.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Felix Moeller <mail@felixmoeller.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Martin Mattel <martin.mattel@diemattels.at>
* @author Michael Zamot <michael@zamot.io>
* @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 Tim Dettrick <t.dettrick@uq.edu.au>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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 GuzzleHttp\Psr7\Uri;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Files\ObjectStore\SwiftFactory;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\StorageBadConfigException;
use OpenStack\Common\Error\BadResponseError;
use OpenStack\ObjectStore\v1\Models\StorageObject;
use Psr\Log\LoggerInterface;
class Swift extends \OC\Files\Storage\Common {
/** @var SwiftFactory */
private $connectionFactory;
/**
* @var \OpenStack\ObjectStore\v1\Models\Container
*/
private $container;
/**
* @var string
*/
private $bucket;
/**
* Connection parameters
*
* @var array
*/
private $params;
/** @var string */
private $id;
/** @var \OC\Files\ObjectStore\Swift */
private $objectStore;
/** @var IMimeTypeDetector */
private $mimeDetector;
/**
* Key value cache mapping path to data object. Maps path to
* \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject for existing
* paths and path to false for not existing paths.
*
* @var \OCP\ICache
*/
private $objectCache;
/**
* @param string $path
* @return mixed|string
*/
private function normalizePath(string $path) {
$path = trim($path, '/');
if (!$path) {
$path = '.';
}
$path = str_replace('#', '%23', $path);
return $path;
}
public const SUBCONTAINER_FILE = '.subcontainers';
/**
* translate directory path to container name
*
* @param string $path
* @return string
*/
/**
* Fetches an object from the API.
* If the object is cached already or a
* failed "doesn't exist" response was cached,
* that one will be returned.
*
* @param string $path
* @return StorageObject|bool object
* or false if the object did not exist
* @throws \OCP\Files\StorageAuthException
* @throws \OCP\Files\StorageNotAvailableException
*/
private function fetchObject(string $path) {
$cached = $this->objectCache->get($path);
if ($cached !== null) {
// might be "false" if object did not exist from last check
return $cached;
}
try {
$object = $this->getContainer()->getObject($path);
$object->retrieve();
$this->objectCache->set($path, $object);
return $object;
} catch (BadResponseError $e) {
// Expected response is "404 Not Found", so only log if it isn't
if ($e->getResponse()->getStatusCode() !== 404) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
}
$this->objectCache->set($path, false);
return false;
}
}
/**
* Returns whether the given path exists.
*
* @param string $path
*
* @return bool true if the object exist, false otherwise
* @throws \OCP\Files\StorageAuthException
* @throws \OCP\Files\StorageNotAvailableException
*/
private function doesObjectExist($path) {
return $this->fetchObject($path) !== false;
}
public function __construct($params) {
if ((empty($params['key']) and empty($params['password']))
or (empty($params['user']) && empty($params['userid'])) or empty($params['bucket'])
or empty($params['region'])
) {
throw new StorageBadConfigException("API Key or password, Username, Bucket and Region have to be configured.");
}
$user = $params['user'];
$this->id = 'swift::' . $user . md5($params['bucket']);
$bucketUrl = new Uri($params['bucket']);
if ($bucketUrl->getHost()) {
$params['bucket'] = basename($bucketUrl->getPath());
$params['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath()));
}
if (empty($params['url'])) {
$params['url'] = 'https://identity.api.rackspacecloud.com/v2.0/';
}
if (empty($params['service_name'])) {
$params['service_name'] = 'cloudFiles';
}
$params['autocreate'] = true;
if (isset($params['domain'])) {
$params['user'] = [
'name' => $params['user'],
'password' => $params['password'],
'domain' => [
'name' => $params['domain'],
]
];
}
$this->params = $params;
// FIXME: private class...
$this->objectCache = new \OCP\Cache\CappedMemoryCache();
$this->connectionFactory = new SwiftFactory(
\OC::$server->getMemCacheFactory()->createDistributed('swift/'),
$this->params,
\OC::$server->get(LoggerInterface::class)
);
$this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory);
$this->bucket = $params['bucket'];
$this->mimeDetector = \OC::$server->get(IMimeTypeDetector::class);
}
public function mkdir($path) {
$path = $this->normalizePath($path);
if ($this->is_dir($path)) {
return false;
}
if ($path !== '.') {
$path .= '/';
}
try {
$this->getContainer()->createObject([
'name' => $path,
'content' => '',
'headers' => ['content-type' => 'httpd/unix-directory']
]);
// invalidate so that the next access gets the real object
// with all properties
$this->objectCache->remove($path);
} catch (BadResponseError $e) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
return true;
}
public function file_exists($path) {
$path = $this->normalizePath($path);
if ($path !== '.' && $this->is_dir($path)) {
$path .= '/';
}
return $this->doesObjectExist($path);
}
public function rmdir($path) {
$path = $this->normalizePath($path);
if (!$this->is_dir($path) || !$this->isDeletable($path)) {
return false;
}
$dh = $this->opendir($path);
while ($file = readdir($dh)) {
if (\OC\Files\Filesystem::isIgnoredDir($file)) {
continue;
}
if ($this->is_dir($path . '/' . $file)) {
$this->rmdir($path . '/' . $file);
} else {
$this->unlink($path . '/' . $file);
}
}
try {
$this->objectStore->deleteObject($path . '/');
$this->objectCache->remove($path . '/');
} catch (BadResponseError $e) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
return true;
}
public function opendir($path) {
$path = $this->normalizePath($path);
if ($path === '.') {
$path = '';
} else {
$path .= '/';
}
// $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of #
try {
$files = [];
$objects = $this->getContainer()->listObjects([
'prefix' => $path,
'delimiter' => '/'
]);
/** @var StorageObject $object */
foreach ($objects as $object) {
$file = basename($object->name);
if ($file !== basename($path) && $file !== '.') {
$files[] = $file;
}
}
return IteratorDirectory::wrap($files);
} catch (\Exception $e) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
}
public function stat($path) {
$path = $this->normalizePath($path);
if ($path === '.') {
$path = '';
} elseif ($this->is_dir($path)) {
$path .= '/';
}
try {
$object = $this->fetchObject($path);
if (!$object) {
return false;
}
} catch (BadResponseError $e) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
$dateTime = $object->lastModified ? \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified) : false;
$mtime = $dateTime ? $dateTime->getTimestamp() : null;
$objectMetadata = $object->getMetadata();
if (isset($objectMetadata['timestamp'])) {
$mtime = $objectMetadata['timestamp'];
}
if (!empty($mtime)) {
$mtime = floor($mtime);
}
$stat = [];
$stat['size'] = (int)$object->contentLength;
$stat['mtime'] = $mtime;
$stat['atime'] = time();
return $stat;
}
public function filetype($path) {
$path = $this->normalizePath($path);
if ($path !== '.' && $this->doesObjectExist($path)) {
return 'file';
}
if ($path !== '.') {
$path .= '/';
}
if ($this->doesObjectExist($path)) {
return 'dir';
}
}
public function unlink($path) {
$path = $this->normalizePath($path);
if ($this->is_dir($path)) {
return $this->rmdir($path);
}
try {
$this->objectStore->deleteObject($path);
$this->objectCache->remove($path);
$this->objectCache->remove($path . '/');
} catch (BadResponseError $e) {
if ($e->getResponse()->getStatusCode() !== 404) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
throw $e;
}
}
return true;
}
public function fopen($path, $mode) {
$path = $this->normalizePath($path);
switch ($mode) {
case 'a':
case 'ab':
case 'a+':
return false;
case 'r':
case 'rb':
try {
return $this->objectStore->readObject($path);
} catch (BadResponseError $e) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
case 'w':
case 'wb':
case 'r+':
case 'w+':
case 'wb+':
case 'x':
case 'x+':
case 'c':
case 'c+':
if (strrpos($path, '.') !== false) {
$ext = substr($path, strrpos($path, '.'));
} else {
$ext = '';
}
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
// Fetch existing file if required
if ($mode[0] !== 'w' && $this->file_exists($path)) {
if ($mode[0] === 'x') {
// File cannot already exist
return false;
}
$source = $this->fopen($path, 'r');
file_put_contents($tmpFile, $source);
}
$handle = fopen($tmpFile, $mode);
return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
$this->writeBack($tmpFile, $path);
});
}
}
public function touch($path, $mtime = null) {
$path = $this->normalizePath($path);
if (is_null($mtime)) {
$mtime = time();
}
$metadata = ['timestamp' => (string)$mtime];
if ($this->file_exists($path)) {
if ($this->is_dir($path) && $path !== '.') {
$path .= '/';
}
$object = $this->fetchObject($path);
if ($object->mergeMetadata($metadata)) {
// invalidate target object to force repopulation on fetch
$this->objectCache->remove($path);
}
return true;
} else {
$mimeType = $this->mimeDetector->detectPath($path);
$this->getContainer()->createObject([
'name' => $path,
'content' => '',
'headers' => ['content-type' => 'httpd/unix-directory']
]);
// invalidate target object to force repopulation on fetch
$this->objectCache->remove($path);
return true;
}
}
public function copy($source, $target) {
$source = $this->normalizePath($source);
$target = $this->normalizePath($target);
$fileType = $this->filetype($source);
if ($fileType) {
// make way
$this->unlink($target);
}
if ($fileType === 'file') {
try {
$sourceObject = $this->fetchObject($source);
$sourceObject->copy([
'destination' => $this->bucket . '/' . $target
]);
// invalidate target object to force repopulation on fetch
$this->objectCache->remove($target);
$this->objectCache->remove($target . '/');
} catch (BadResponseError $e) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
} elseif ($fileType === 'dir') {
try {
$sourceObject = $this->fetchObject($source . '/');
$sourceObject->copy([
'destination' => $this->bucket . '/' . $target . '/'
]);
// invalidate target object to force repopulation on fetch
$this->objectCache->remove($target);
$this->objectCache->remove($target . '/');
} catch (BadResponseError $e) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
$dh = $this->opendir($source);
while ($file = readdir($dh)) {
if (\OC\Files\Filesystem::isIgnoredDir($file)) {
continue;
}
$source = $source . '/' . $file;
$target = $target . '/' . $file;
$this->copy($source, $target);
}
} else {
//file does not exist
return false;
}
return true;
}
public function rename($source, $target) {
$source = $this->normalizePath($source);
$target = $this->normalizePath($target);
$fileType = $this->filetype($source);
if ($fileType === 'dir' || $fileType === 'file') {
// copy
if ($this->copy($source, $target) === false) {
return false;
}
// cleanup
if ($this->unlink($source) === false) {
throw new \Exception('failed to remove original');
$this->unlink($target);
return false;
}
return true;
}
return false;
}
public function getId() {
return $this->id;
}
/**
* Returns the initialized object store container.
*
* @return \OpenStack\ObjectStore\v1\Models\Container
* @throws \OCP\Files\StorageAuthException
* @throws \OCP\Files\StorageNotAvailableException
*/
public function getContainer() {
if (is_null($this->container)) {
$this->container = $this->connectionFactory->getContainer();
if (!$this->file_exists('.')) {
$this->mkdir('.');
}
}
return $this->container;
}
public function writeBack($tmpFile, $path) {
$fileData = fopen($tmpFile, 'r');
$this->objectStore->writeObject($path, $fileData, $this->mimeDetector->detectPath($path));
// invalidate target object to force repopulation on fetch
$this->objectCache->remove($path);
unlink($tmpFile);
}
public function hasUpdated($path, $time) {
if ($this->is_file($path)) {
return parent::hasUpdated($path, $time);
}
$path = $this->normalizePath($path);
$dh = $this->opendir($path);
$content = [];
while (($file = readdir($dh)) !== false) {
$content[] = $file;
}
if ($path === '.') {
$path = '';
}
$cachedContent = $this->getCache()->getFolderContents($path);
$cachedNames = array_map(function ($content) {
return $content['name'];
}, $cachedContent);
sort($cachedNames);
sort($content);
return $cachedNames !== $content;
}
/**
* check if curl is installed
*/
public static function checkDependencies() {
return true;
}
}