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,172 @@
<?php
declare(strict_types=1);
/**
* @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\AppData;
use OC\Files\SimpleFS\SimpleFolder;
use OC\SystemConfig;
use OCP\Cache\CappedMemoryCache;
use OCP\Files\Folder;
use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFolder;
class AppData implements IAppData {
private IRootFolder $rootFolder;
private SystemConfig $config;
private string $appId;
private ?Folder $folder = null;
/** @var CappedMemoryCache<ISimpleFolder|NotFoundException> */
private CappedMemoryCache $folders;
/**
* AppData constructor.
*
* @param IRootFolder $rootFolder
* @param SystemConfig $systemConfig
* @param string $appId
*/
public function __construct(IRootFolder $rootFolder,
SystemConfig $systemConfig,
string $appId) {
$this->rootFolder = $rootFolder;
$this->config = $systemConfig;
$this->appId = $appId;
$this->folders = new CappedMemoryCache();
}
private function getAppDataFolderName() {
$instanceId = $this->config->getValue('instanceid', null);
if ($instanceId === null) {
throw new \RuntimeException('no instance id!');
}
return 'appdata_' . $instanceId;
}
protected function getAppDataRootFolder(): Folder {
$name = $this->getAppDataFolderName();
try {
/** @var Folder $node */
$node = $this->rootFolder->get($name);
return $node;
} catch (NotFoundException $e) {
try {
return $this->rootFolder->newFolder($name);
} catch (NotPermittedException $e) {
throw new \RuntimeException('Could not get appdata folder');
}
}
}
/**
* @return Folder
* @throws \RuntimeException
*/
private function getAppDataFolder(): Folder {
if ($this->folder === null) {
$name = $this->getAppDataFolderName();
try {
$this->folder = $this->rootFolder->get($name . '/' . $this->appId);
} catch (NotFoundException $e) {
$appDataRootFolder = $this->getAppDataRootFolder();
try {
$this->folder = $appDataRootFolder->get($this->appId);
} catch (NotFoundException $e) {
try {
$this->folder = $appDataRootFolder->newFolder($this->appId);
} catch (NotPermittedException $e) {
throw new \RuntimeException('Could not get appdata folder for ' . $this->appId);
}
}
}
}
return $this->folder;
}
public function getFolder(string $name): ISimpleFolder {
$key = $this->appId . '/' . $name;
if ($cachedFolder = $this->folders->get($key)) {
if ($cachedFolder instanceof \Exception) {
throw $cachedFolder;
} else {
return $cachedFolder;
}
}
try {
// Hardening if somebody wants to retrieve '/'
if ($name === '/') {
$node = $this->getAppDataFolder();
} else {
$path = $this->getAppDataFolderName() . '/' . $this->appId . '/' . $name;
$node = $this->rootFolder->get($path);
}
} catch (NotFoundException $e) {
$this->folders->set($key, $e);
throw $e;
}
/** @var Folder $node */
$folder = new SimpleFolder($node);
$this->folders->set($key, $folder);
return $folder;
}
public function newFolder(string $name): ISimpleFolder {
$key = $this->appId . '/' . $name;
$folder = $this->getAppDataFolder()->newFolder($name);
$simpleFolder = new SimpleFolder($folder);
$this->folders->set($key, $simpleFolder);
return $simpleFolder;
}
public function getDirectoryListing(): array {
$listing = $this->getAppDataFolder()->getDirectoryListing();
$fileListing = array_map(function (Node $folder) {
if ($folder instanceof Folder) {
return new SimpleFolder($folder);
}
return null;
}, $listing);
$fileListing = array_filter($fileListing);
return array_values($fileListing);
}
public function getId(): int {
return $this->getAppDataFolder()->getId();
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\AppData;
use OC\SystemConfig;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
class Factory implements IAppDataFactory {
private IRootFolder $rootFolder;
private SystemConfig $config;
/** @var array<string, IAppData> */
private array $folders = [];
public function __construct(IRootFolder $rootFolder,
SystemConfig $systemConfig) {
$this->rootFolder = $rootFolder;
$this->config = $systemConfig;
}
public function get(string $appId): IAppData {
if (!isset($this->folders[$appId])) {
$this->folders[$appId] = new AppData($this->rootFolder, $this->config, $appId);
}
return $this->folders[$appId];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @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 OC\Files\Cache;
use OCP\Files\Cache\ICacheEntry;
/**
* meta data for a file or folder
*/
class CacheEntry implements ICacheEntry {
/**
* @var array
*/
private $data;
public function __construct(array $data) {
$this->data = $data;
}
public function offsetSet($offset, $value): void {
$this->data[$offset] = $value;
}
public function offsetExists($offset): bool {
return isset($this->data[$offset]);
}
public function offsetUnset($offset): void {
unset($this->data[$offset]);
}
/**
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
if (isset($this->data[$offset])) {
return $this->data[$offset];
} else {
return null;
}
}
public function getId() {
return (int)$this->data['fileid'];
}
public function getStorageId() {
return $this->data['storage'];
}
public function getPath() {
return (string)$this->data['path'];
}
public function getName() {
return $this->data['name'];
}
public function getMimeType() {
return $this->data['mimetype'];
}
public function getMimePart() {
return $this->data['mimepart'];
}
public function getSize() {
return $this->data['size'];
}
public function getMTime() {
return $this->data['mtime'];
}
public function getStorageMTime() {
return $this->data['storage_mtime'];
}
public function getEtag() {
return $this->data['etag'];
}
public function getPermissions() {
return $this->data['permissions'];
}
public function isEncrypted() {
return isset($this->data['encrypted']) && $this->data['encrypted'];
}
public function getMetadataEtag(): ?string {
return $this->data['metadata_etag'] ?? null;
}
public function getCreationTime(): ?int {
return $this->data['creation_time'] ?? null;
}
public function getUploadTime(): ?int {
return $this->data['upload_time'] ?? null;
}
public function getData() {
return $this->data;
}
public function __clone() {
$this->data = array_merge([], $this->data);
}
public function getUnencryptedSize(): int {
if ($this->data['encrypted'] && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) {
return $this->data['unencrypted_size'];
} else {
return $this->data['size'] ?? 0;
}
}
}
@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl>
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @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 OC\Files\Cache;
use OC\DB\QueryBuilder\QueryBuilder;
use OC\SystemConfig;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\FilesMetadata\IMetadataQuery;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
/**
* Query builder with commonly used helpers for filecache queries
*/
class CacheQueryBuilder extends QueryBuilder {
private ?string $alias = null;
public function __construct(
IDBConnection $connection,
SystemConfig $systemConfig,
LoggerInterface $logger,
private IFilesMetadataManager $filesMetadataManager,
) {
parent::__construct($connection, $systemConfig, $logger);
}
public function selectTagUsage(): self {
$this
->select('systemtag.name', 'systemtag.id', 'systemtag.visibility', 'systemtag.editable')
->selectAlias($this->createFunction('COUNT(filecache.fileid)'), 'number_files')
->selectAlias($this->createFunction('MAX(filecache.fileid)'), 'ref_file_id')
->from('filecache', 'filecache')
->leftJoin('filecache', 'systemtag_object_mapping', 'systemtagmap', $this->expr()->andX(
$this->expr()->eq('filecache.fileid', $this->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)),
$this->expr()->eq('systemtagmap.objecttype', $this->createNamedParameter('files'))
))
->leftJoin('systemtagmap', 'systemtag', 'systemtag', $this->expr()->andX(
$this->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'),
$this->expr()->eq('systemtag.visibility', $this->createNamedParameter(true))
))
->groupBy('systemtag.name', 'systemtag.id', 'systemtag.visibility', 'systemtag.editable');
return $this;
}
public function selectFileCache(string $alias = null, bool $joinExtendedCache = true) {
$name = $alias ?: 'filecache';
$this->select("$name.fileid", 'storage', 'path', 'path_hash', "$name.parent", "$name.name", 'mimetype', 'mimepart', 'size', 'mtime',
'storage_mtime', 'encrypted', 'etag', "$name.permissions", 'checksum', 'unencrypted_size')
->from('filecache', $name);
if ($joinExtendedCache) {
$this->addSelect('metadata_etag', 'creation_time', 'upload_time');
$this->leftJoin($name, 'filecache_extended', 'fe', $this->expr()->eq("$name.fileid", 'fe.fileid'));
}
$this->alias = $name;
return $this;
}
public function whereStorageId(int $storageId) {
$this->andWhere($this->expr()->eq('storage', $this->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
return $this;
}
public function whereFileId(int $fileId) {
$alias = $this->alias;
if ($alias) {
$alias .= '.';
} else {
$alias = '';
}
$this->andWhere($this->expr()->eq("{$alias}fileid", $this->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
return $this;
}
public function wherePath(string $path) {
$this->andWhere($this->expr()->eq('path_hash', $this->createNamedParameter(md5($path))));
return $this;
}
public function whereParent(int $parent) {
$alias = $this->alias;
if ($alias) {
$alias .= '.';
} else {
$alias = '';
}
$this->andWhere($this->expr()->eq("{$alias}parent", $this->createNamedParameter($parent, IQueryBuilder::PARAM_INT)));
return $this;
}
public function whereParentInParameter(string $parameter) {
$alias = $this->alias;
if ($alias) {
$alias .= '.';
} else {
$alias = '';
}
$this->andWhere($this->expr()->in("{$alias}parent", $this->createParameter($parameter)));
return $this;
}
/**
* join metadata to current query builder and returns an helper
*
* @return IMetadataQuery|null NULL if no metadata have never been generated
*/
public function selectMetadata(): ?IMetadataQuery {
$metadataQuery = $this->filesMetadataManager->getMetadataQuery($this, $this->alias, 'fileid');
$metadataQuery?->retrieveMetadata();
return $metadataQuery;
}
}
@@ -0,0 +1,152 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @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 OC\Files\Cache;
use OC\Files\Search\SearchComparison;
use OCP\Constants;
use OCP\Files\Cache\ICache;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchQuery;
/**
* Storage placeholder to represent a missing precondition, storage unavailable
*/
class FailedCache implements ICache {
/** @var bool whether to show the failed storage in the ui */
private $visible;
/**
* FailedCache constructor.
*
* @param bool $visible
*/
public function __construct($visible = true) {
$this->visible = $visible;
}
public function getNumericStorageId() {
return -1;
}
public function get($file) {
if ($file === '') {
return new CacheEntry([
'fileid' => -1,
'size' => 0,
'mimetype' => 'httpd/unix-directory',
'mimepart' => 'httpd',
'permissions' => $this->visible ? Constants::PERMISSION_READ : 0,
'mtime' => time()
]);
} else {
return false;
}
}
public function getFolderContents($folder) {
return [];
}
public function getFolderContentsById($fileId) {
return [];
}
public function put($file, array $data) {
}
public function insert($file, array $data) {
}
public function update($id, array $data) {
}
public function getId($file) {
return -1;
}
public function getParentId($file) {
return -1;
}
public function inCache($file) {
return false;
}
public function remove($file) {
}
public function move($source, $target) {
}
public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
}
public function clear() {
}
public function getStatus($file) {
return ICache::NOT_FOUND;
}
public function search($pattern) {
return [];
}
public function searchByMime($mimetype) {
return [];
}
public function searchQuery(ISearchQuery $query) {
return [];
}
public function getAll() {
return [];
}
public function getIncomplete() {
return [];
}
public function getPathById($id) {
return null;
}
public function normalize($path) {
return $path;
}
public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
throw new \Exception("Invalid cache");
}
public function getQueryFilterForStorage(): ISearchOperator {
return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', -1);
}
public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
return null;
}
}
@@ -0,0 +1,68 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Andreas Fischer <bantu@owncloud.com>
* @author Björn Schießle <bjoern@schiessle.org>
* @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 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 OC\Files\Cache;
use OCP\Files\Cache\ICacheEntry;
class HomeCache extends Cache {
/**
* get the size of a folder and set it in the cache
*
* @param string $path
* @param array|null|ICacheEntry $entry (optional) meta data of the folder
* @return int|float
*/
public function calculateFolderSize($path, $entry = null) {
if ($path !== '/' and $path !== '' and $path !== 'files' and $path !== 'files_trashbin' and $path !== 'files_versions') {
return parent::calculateFolderSize($path, $entry);
} elseif ($path === '' or $path === '/') {
// since the size of / isn't used (the size of /files is used instead) there is no use in calculating it
return 0;
} else {
return $this->calculateFolderSizeInner($path, $entry, true);
}
}
/**
* @param string $file
* @return ICacheEntry
*/
public function get($file) {
$data = parent::get($file);
if ($file === '' or $file === '/') {
// only the size of the "files" dir counts
$filesData = parent::get('files');
if (isset($filesData['size'])) {
$data['size'] = $filesData['size'];
}
}
return $data;
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @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 OC\Files\Cache;
use OCP\IDBConnection;
class HomePropagator extends Propagator {
private $ignoredBaseFolders;
/**
* @param \OC\Files\Storage\Storage $storage
*/
public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection) {
parent::__construct($storage, $connection);
$this->ignoredBaseFolders = ['files_encryption'];
}
/**
* @param string $internalPath
* @param int $time
* @param int $sizeDifference number of bytes the file has grown
*/
public function propagateChange($internalPath, $time, $sizeDifference = 0) {
[$baseFolder] = explode('/', $internalPath, 2);
if (in_array($baseFolder, $this->ignoredBaseFolders)) {
return [];
} else {
parent::propagateChange($internalPath, $time, $sizeDifference);
}
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\Cache;
class LocalRootScanner extends Scanner {
public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
if ($this->shouldScanPath($file)) {
return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock, $data);
} else {
return null;
}
}
public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
if ($this->shouldScanPath($path)) {
return parent::scan($path, $recursive, $reuse, $lock);
} else {
return null;
}
}
private function shouldScanPath(string $path): bool {
$path = trim($path, '/');
return $path === '' || str_starts_with($path, 'appdata_') || str_starts_with($path, '__groupfolders');
}
}
@@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @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 OC\Files\Cache;
use OCP\Files\Cache\ICache;
use OCP\Files\Cache\ICacheEntry;
/**
* Fallback implementation for moveFromCache
*/
trait MoveFromCacheTrait {
/**
* store meta data for a file or folder
*
* @param string $file
* @param array $data
*
* @return int file id
* @throws \RuntimeException
*/
abstract public function put($file, array $data);
abstract public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int;
/**
* Move a file or folder in the cache
*
* @param \OCP\Files\Cache\ICache $sourceCache
* @param string $sourcePath
* @param string $targetPath
*/
public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
$sourceEntry = $sourceCache->get($sourcePath);
$this->copyFromCache($sourceCache, $sourceEntry, $targetPath);
$sourceCache->remove($sourcePath);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\Cache;
class NullWatcher extends Watcher {
private $policy;
public function __construct() {
}
public function setPolicy($policy) {
$this->policy = $policy;
}
public function getPolicy() {
return $this->policy;
}
public function checkUpdate($path, $cachedEntry = null) {
return false;
}
public function update($path, $cachedData) {
}
public function needsUpdate($path, $cachedData) {
return false;
}
public function cleanFolder($path) {
}
}
@@ -0,0 +1,234 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\Cache;
use OC\DB\Exceptions\DbalException;
use OC\Files\Storage\Wrapper\Encryption;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Cache\IPropagator;
use OCP\Files\Storage\IReliableEtagStorage;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
/**
* Propagate etags and mtimes within the storage
*/
class Propagator implements IPropagator {
public const MAX_RETRIES = 3;
private $inBatch = false;
private $batch = [];
/**
* @var \OC\Files\Storage\Storage
*/
protected $storage;
/**
* @var IDBConnection
*/
private $connection;
/**
* @var array
*/
private $ignore = [];
public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) {
$this->storage = $storage;
$this->connection = $connection;
$this->ignore = $ignore;
}
/**
* @param string $internalPath
* @param int $time
* @param int $sizeDifference number of bytes the file has grown
*/
public function propagateChange($internalPath, $time, $sizeDifference = 0) {
// Do not propagate changes in ignored paths
foreach ($this->ignore as $ignore) {
if (str_starts_with($internalPath, $ignore)) {
return;
}
}
$storageId = (int)$this->storage->getStorageCache()->getNumericId();
$parents = $this->getParents($internalPath);
if ($this->inBatch) {
foreach ($parents as $parent) {
$this->addToBatch($parent, $time, $sizeDifference);
}
return;
}
$parentHashes = array_map('md5', $parents);
$etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
$builder = $this->connection->getQueryBuilder();
$hashParams = array_map(function ($hash) use ($builder) {
return $builder->expr()->literal($hash);
}, $parentHashes);
$builder->update('filecache')
->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT)))
->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($builder->expr()->in('path_hash', $hashParams));
if (!$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
$builder->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR));
}
if ($sizeDifference !== 0) {
$hasCalculatedSize = $builder->expr()->gt('size', $builder->expr()->literal(-1, IQUeryBuilder::PARAM_INT));
$sizeColumn = $builder->getColumnName('size');
$newSize = $builder->func()->greatest(
$builder->func()->add('size', $builder->createNamedParameter($sizeDifference)),
$builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
);
// Only update if row had a previously calculated size
$builder->set('size', $builder->createFunction("CASE WHEN $hasCalculatedSize THEN $newSize ELSE $sizeColumn END"));
if ($this->storage->instanceOfStorage(Encryption::class)) {
// in case of encryption being enabled after some files are already uploaded, some entries will have an unencrypted_size of 0 and a non-zero size
$hasUnencryptedSize = $builder->expr()->neq('unencrypted_size', $builder->expr()->literal(0, IQueryBuilder::PARAM_INT));
$sizeColumn = $builder->getColumnName('size');
$unencryptedSizeColumn = $builder->getColumnName('unencrypted_size');
$newUnencryptedSize = $builder->func()->greatest(
$builder->func()->add(
$builder->createFunction("CASE WHEN $hasUnencryptedSize THEN $unencryptedSizeColumn ELSE $sizeColumn END"),
$builder->createNamedParameter($sizeDifference)
),
$builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
);
// Only update if row had a previously calculated size
$builder->set('unencrypted_size', $builder->createFunction("CASE WHEN $hasCalculatedSize THEN $newUnencryptedSize ELSE $unencryptedSizeColumn END"));
}
}
for ($i = 0; $i < self::MAX_RETRIES; $i++) {
try {
$builder->executeStatement();
break;
} catch (DbalException $e) {
if (!$e->isRetryable()) {
throw $e;
}
/** @var LoggerInterface $loggerInterface */
$loggerInterface = \OCP\Server::get(LoggerInterface::class);
$loggerInterface->warning('Retrying propagation query after retryable exception.', [ 'exception' => $e ]);
}
}
}
protected function getParents($path) {
$parts = explode('/', $path);
$parent = '';
$parents = [];
foreach ($parts as $part) {
$parents[] = $parent;
$parent = trim($parent . '/' . $part, '/');
}
return $parents;
}
/**
* Mark the beginning of a propagation batch
*
* Note that not all cache setups support propagation in which case this will be a noop
*
* Batching for cache setups that do support it has to be explicit since the cache state is not fully consistent
* before the batch is committed.
*/
public function beginBatch() {
$this->inBatch = true;
}
private function addToBatch($internalPath, $time, $sizeDifference) {
if (!isset($this->batch[$internalPath])) {
$this->batch[$internalPath] = [
'hash' => md5($internalPath),
'time' => $time,
'size' => $sizeDifference,
];
} else {
$this->batch[$internalPath]['size'] += $sizeDifference;
if ($time > $this->batch[$internalPath]['time']) {
$this->batch[$internalPath]['time'] = $time;
}
}
}
/**
* Commit the active propagation batch
*/
public function commitBatch() {
if (!$this->inBatch) {
throw new \BadMethodCallException('Not in batch');
}
$this->inBatch = false;
$this->connection->beginTransaction();
$query = $this->connection->getQueryBuilder();
$storageId = (int)$this->storage->getStorageCache()->getNumericId();
$query->update('filecache')
->set('mtime', $query->func()->greatest('mtime', $query->createParameter('time')))
->set('etag', $query->expr()->literal(uniqid()))
->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
$sizeQuery = $this->connection->getQueryBuilder();
$sizeQuery->update('filecache')
->set('size', $sizeQuery->func()->add('size', $sizeQuery->createParameter('size')))
->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')))
->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
foreach ($this->batch as $item) {
$query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT);
$query->setParameter('hash', $item['hash']);
$query->execute();
if ($item['size']) {
$sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT);
$sizeQuery->setParameter('hash', $item['hash']);
$sizeQuery->execute();
}
}
$this->batch = [];
$this->connection->commit();
}
}
@@ -0,0 +1,268 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tobias Kaminsky <tobias@kaminsky.me>
*
* @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 OC\Files\Cache;
use OC\Files\Cache\Wrapper\CacheJail;
use OC\Files\Search\QueryOptimizer\QueryOptimizer;
use OC\Files\Search\SearchBinaryOperator;
use OC\SystemConfig;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Cache\ICache;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchQuery;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\FilesMetadata\IMetadataQuery;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUser;
use Psr\Log\LoggerInterface;
class QuerySearchHelper {
public function __construct(
private IMimeTypeLoader $mimetypeLoader,
private IDBConnection $connection,
private SystemConfig $systemConfig,
private LoggerInterface $logger,
private SearchBuilder $searchBuilder,
private QueryOptimizer $queryOptimizer,
private IGroupManager $groupManager,
private IFilesMetadataManager $filesMetadataManager,
) {
}
protected function getQueryBuilder() {
return new CacheQueryBuilder(
$this->connection,
$this->systemConfig,
$this->logger,
$this->filesMetadataManager,
);
}
/**
* @param CacheQueryBuilder $query
* @param ISearchQuery $searchQuery
* @param array $caches
* @param IMetadataQuery|null $metadataQuery
*/
protected function applySearchConstraints(
CacheQueryBuilder $query,
ISearchQuery $searchQuery,
array $caches,
?IMetadataQuery $metadataQuery = null
): void {
$storageFilters = array_values(array_map(function (ICache $cache) {
return $cache->getQueryFilterForStorage();
}, $caches));
$storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters);
$filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]);
$this->queryOptimizer->processOperator($filter);
$searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter, $metadataQuery);
if ($searchExpr) {
$query->andWhere($searchExpr);
}
$this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder(), $metadataQuery);
if ($searchQuery->getLimit()) {
$query->setMaxResults($searchQuery->getLimit());
}
if ($searchQuery->getOffset()) {
$query->setFirstResult($searchQuery->getOffset());
}
}
/**
* @return array<array-key, array{id: int, name: string, visibility: int, editable: int, ref_file_id: int, number_files: int}>
*/
public function findUsedTagsInCaches(ISearchQuery $searchQuery, array $caches): array {
$query = $this->getQueryBuilder();
$query->selectTagUsage();
$this->applySearchConstraints($query, $searchQuery, $caches);
$result = $query->execute();
$tags = $result->fetchAll();
$result->closeCursor();
return $tags;
}
protected function equipQueryForSystemTags(CacheQueryBuilder $query, IUser $user): void {
$query->leftJoin('file', 'systemtag_object_mapping', 'systemtagmap', $query->expr()->andX(
$query->expr()->eq('file.fileid', $query->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)),
$query->expr()->eq('systemtagmap.objecttype', $query->createNamedParameter('files'))
));
$on = $query->expr()->andX($query->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'));
if (!$this->groupManager->isAdmin($user->getUID())) {
$on->add($query->expr()->eq('systemtag.visibility', $query->createNamedParameter(true)));
}
$query->leftJoin('systemtagmap', 'systemtag', 'systemtag', $on);
}
protected function equipQueryForDavTags(CacheQueryBuilder $query, IUser $user): void {
$query
->leftJoin('file', 'vcategory_to_object', 'tagmap', $query->expr()->eq('file.fileid', 'tagmap.objid'))
->leftJoin('tagmap', 'vcategory', 'tag', $query->expr()->andX(
$query->expr()->eq('tagmap.type', 'tag.type'),
$query->expr()->eq('tagmap.categoryid', 'tag.id'),
$query->expr()->eq('tag.type', $query->createNamedParameter('files')),
$query->expr()->eq('tag.uid', $query->createNamedParameter($user->getUID()))
));
}
protected function equipQueryForShares(CacheQueryBuilder $query): void {
$query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source'));
}
/**
* Perform a file system search in multiple caches
*
* the results will be grouped by the same array keys as the $caches argument to allow
* post-processing based on which cache the result came from
*
* @template T of array-key
* @param ISearchQuery $searchQuery
* @param array<T, ICache> $caches
* @return array<T, ICacheEntry[]>
*/
public function searchInCaches(ISearchQuery $searchQuery, array $caches): array {
// search in multiple caches at once by creating one query in the following format
// SELECT ... FROM oc_filecache WHERE
// <filter expressions from the search query>
// AND (
// <filter expression for storage1> OR
// <filter expression for storage2> OR
// ...
// );
//
// This gives us all the files matching the search query from all caches
//
// while the resulting rows don't have a way to tell what storage they came from (multiple storages/caches can share storage_id)
// we can just ask every cache if the row belongs to them and give them the cache to do any post processing on the result.
$builder = $this->getQueryBuilder();
$query = $builder->selectFileCache('file', false);
$requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());
if (in_array('systemtag', $requestedFields)) {
$this->equipQueryForSystemTags($query, $this->requireUser($searchQuery));
}
if (in_array('tagname', $requestedFields) || in_array('favorite', $requestedFields)) {
$this->equipQueryForDavTags($query, $this->requireUser($searchQuery));
}
if (in_array('owner', $requestedFields) || in_array('share_with', $requestedFields) || in_array('share_type', $requestedFields)) {
$this->equipQueryForShares($query);
}
$metadataQuery = $query->selectMetadata();
$this->applySearchConstraints($query, $searchQuery, $caches, $metadataQuery);
$result = $query->execute();
$files = $result->fetchAll();
$rawEntries = array_map(function (array $data) use ($metadataQuery) {
// migrate to null safe ...
if ($metadataQuery === null) {
$data['metadata'] = [];
} else {
$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
}
return Cache::cacheEntryFromData($data, $this->mimetypeLoader);
}, $files);
$result->closeCursor();
// loop through all caches for each result to see if the result matches that storage
// results are grouped by the same array keys as the caches argument to allow the caller to distinguish the source of the results
$results = array_fill_keys(array_keys($caches), []);
foreach ($rawEntries as $rawEntry) {
foreach ($caches as $cacheKey => $cache) {
$entry = $cache->getCacheEntryFromSearchResult($rawEntry);
if ($entry) {
$results[$cacheKey][] = $entry;
}
}
}
return $results;
}
protected function requireUser(ISearchQuery $searchQuery): IUser {
$user = $searchQuery->getUser();
if ($user === null) {
throw new \InvalidArgumentException("This search operation requires the user to be set in the query");
}
return $user;
}
/**
* @return list{0?: array<array-key, ICache>, 1?: array<array-key, IMountPoint>}
*/
public function getCachesAndMountPointsForSearch(IRootFolder $root, string $path, bool $limitToHome = false): array {
$rootLength = strlen($path);
$mount = $root->getMount($path);
$storage = $mount->getStorage();
if ($storage === null) {
return [];
}
$internalPath = $mount->getInternalPath($path);
if ($internalPath !== '') {
// a temporary CacheJail is used to handle filtering down the results to within this folder
/** @var ICache[] $caches */
$caches = ['' => new CacheJail($storage->getCache(''), $internalPath)];
} else {
/** @var ICache[] $caches */
$caches = ['' => $storage->getCache('')];
}
/** @var IMountPoint[] $mountByMountPoint */
$mountByMountPoint = ['' => $mount];
if (!$limitToHome) {
$mounts = $root->getMountsIn($path);
foreach ($mounts as $mount) {
$storage = $mount->getStorage();
if ($storage) {
$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
$caches[$relativeMountPoint] = $storage->getCache('');
$mountByMountPoint[$relativeMountPoint] = $mount;
}
}
}
return [$caches, $mountByMountPoint];
}
}
@@ -0,0 +1,613 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Ari Selseng <ari@selseng.net>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Jagszent <daniel@jagszent.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Martin Mattel <martin.mattel@diemattels.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Owen Winkler <a_github@midnightcircus.com>
* @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 OC\Files\Cache;
use Doctrine\DBAL\Exception;
use OC\Files\Storage\Wrapper\Encryption;
use OC\Files\Storage\Wrapper\Jail;
use OC\Hooks\BasicEmitter;
use OCP\Files\Cache\IScanner;
use OCP\Files\ForbiddenException;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\IReliableEtagStorage;
use OCP\IDBConnection;
use OCP\Lock\ILockingProvider;
use Psr\Log\LoggerInterface;
/**
* Class Scanner
*
* Hooks available in scope \OC\Files\Cache\Scanner:
* - scanFile(string $path, string $storageId)
* - scanFolder(string $path, string $storageId)
* - postScanFile(string $path, string $storageId)
* - postScanFolder(string $path, string $storageId)
*
* @package OC\Files\Cache
*/
class Scanner extends BasicEmitter implements IScanner {
/**
* @var \OC\Files\Storage\Storage $storage
*/
protected $storage;
/**
* @var string $storageId
*/
protected $storageId;
/**
* @var \OC\Files\Cache\Cache $cache
*/
protected $cache;
/**
* @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
*/
protected $cacheActive;
/**
* @var bool $useTransactions whether to use transactions
*/
protected $useTransactions = true;
/**
* @var \OCP\Lock\ILockingProvider
*/
protected $lockingProvider;
protected IDBConnection $connection;
public function __construct(\OC\Files\Storage\Storage $storage) {
$this->storage = $storage;
$this->storageId = $this->storage->getId();
$this->cache = $storage->getCache();
$this->cacheActive = !\OC::$server->getConfig()->getSystemValueBool('filesystem_cache_readonly', false);
$this->lockingProvider = \OC::$server->getLockingProvider();
$this->connection = \OC::$server->get(IDBConnection::class);
}
/**
* Whether to wrap the scanning of a folder in a database transaction
* On default transactions are used
*
* @param bool $useTransactions
*/
public function setUseTransactions($useTransactions) {
$this->useTransactions = $useTransactions;
}
/**
* get all the metadata of a file or folder
* *
*
* @param string $path
* @return array|null an array of metadata of the file
*/
protected function getData($path) {
$data = $this->storage->getMetaData($path);
if (is_null($data)) {
\OC::$server->get(LoggerInterface::class)->debug("!!! Path '$path' is not accessible or present !!!", ['app' => 'core']);
}
return $data;
}
/**
* scan a single file and store it in the cache
*
* @param string $file
* @param int $reuseExisting
* @param int $parentId
* @param array|null|false $cacheData existing data in the cache for the file to be scanned
* @param bool $lock set to false to disable getting an additional read lock during scanning
* @param null $data the metadata for the file, as returned by the storage
* @return array|null an array of metadata of the scanned file
* @throws \OCP\Lock\LockedException
*/
public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
if ($file !== '') {
try {
$this->storage->verifyPath(dirname($file), basename($file));
} catch (\Exception $e) {
return null;
}
}
// only proceed if $file is not a partial file, blacklist is handled by the storage
if (!self::isPartialFile($file)) {
// acquire a lock
if ($lock) {
if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
}
}
try {
$data = $data ?? $this->getData($file);
} catch (ForbiddenException $e) {
if ($lock) {
if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
}
}
return null;
}
try {
if ($data) {
// pre-emit only if it was a file. By that we avoid counting/treating folders as files
if ($data['mimetype'] !== 'httpd/unix-directory') {
$this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
\OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
}
$parent = dirname($file);
if ($parent === '.' || $parent === '/') {
$parent = '';
}
if ($parentId === -1) {
$parentId = $this->cache->getParentId($file);
}
// scan the parent if it's not in the cache (id -1) and the current file is not the root folder
if ($file && $parentId === -1) {
$parentData = $this->scanFile($parent);
if (!$parentData) {
return null;
}
$parentId = $parentData['fileid'];
}
if ($parent) {
$data['parent'] = $parentId;
}
if (is_null($cacheData)) {
/** @var CacheEntry $cacheData */
$cacheData = $this->cache->get($file);
}
if ($cacheData && $reuseExisting && isset($cacheData['fileid'])) {
// prevent empty etag
$etag = empty($cacheData['etag']) ? $data['etag'] : $cacheData['etag'];
$fileId = $cacheData['fileid'];
$data['fileid'] = $fileId;
// only reuse data if the file hasn't explicitly changed
$mtimeUnchanged = isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime'];
// if the folder is marked as unscanned, never reuse etags
if ($mtimeUnchanged && $cacheData['size'] !== -1) {
$data['mtime'] = $cacheData['mtime'];
if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
$data['size'] = $cacheData['size'];
}
if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
$data['etag'] = $etag;
}
}
// we only updated unencrypted_size if it's already set
if ($cacheData['unencrypted_size'] === 0) {
unset($data['unencrypted_size']);
}
// Only update metadata that has changed
$newData = array_diff_assoc($data, $cacheData->getData());
// make it known to the caller that etag has been changed and needs propagation
if (isset($newData['etag'])) {
$data['etag_changed'] = true;
}
} else {
// we only updated unencrypted_size if it's already set
unset($data['unencrypted_size']);
$newData = $data;
$fileId = -1;
}
if (!empty($newData)) {
// Reset the checksum if the data has changed
$newData['checksum'] = '';
$newData['parent'] = $parentId;
$data['fileid'] = $this->addToCache($file, $newData, $fileId);
}
$data['oldSize'] = ($cacheData && isset($cacheData['size'])) ? $cacheData['size'] : 0;
if ($cacheData && isset($cacheData['encrypted'])) {
$data['encrypted'] = $cacheData['encrypted'];
}
// post-emit only if it was a file. By that we avoid counting/treating folders as files
if ($data['mimetype'] !== 'httpd/unix-directory') {
$this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
\OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
}
} else {
$this->removeFromCache($file);
}
} catch (\Exception $e) {
if ($lock) {
if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
}
}
throw $e;
}
// release the acquired lock
if ($lock) {
if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
}
}
if ($data && !isset($data['encrypted'])) {
$data['encrypted'] = false;
}
return $data;
}
return null;
}
protected function removeFromCache($path) {
\OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
$this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
if ($this->cacheActive) {
$this->cache->remove($path);
}
}
/**
* @param string $path
* @param array $data
* @param int $fileId
* @return int the id of the added file
*/
protected function addToCache($path, $data, $fileId = -1) {
if (isset($data['scan_permissions'])) {
$data['permissions'] = $data['scan_permissions'];
}
\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
$this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data, $fileId]);
if ($this->cacheActive) {
if ($fileId !== -1) {
$this->cache->update($fileId, $data);
return $fileId;
} else {
return $this->cache->insert($path, $data);
}
} else {
return -1;
}
}
/**
* @param string $path
* @param array $data
* @param int $fileId
*/
protected function updateCache($path, $data, $fileId = -1) {
\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
$this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
if ($this->cacheActive) {
if ($fileId !== -1) {
$this->cache->update($fileId, $data);
} else {
$this->cache->put($path, $data);
}
}
}
/**
* scan a folder and all it's children
*
* @param string $path
* @param bool $recursive
* @param int $reuse
* @param bool $lock set to false to disable getting an additional read lock during scanning
* @return array|null an array of the meta data of the scanned file or folder
*/
public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
if ($reuse === -1) {
$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
}
if ($lock) {
if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
}
}
try {
try {
$data = $this->scanFile($path, $reuse, -1, null, $lock);
if ($data && $data['mimetype'] === 'httpd/unix-directory') {
$size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock, $data['size']);
$data['size'] = $size;
}
} catch (NotFoundException $e) {
$this->removeFromCache($path);
return null;
}
} finally {
if ($lock) {
if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
}
}
}
return $data;
}
/**
* Get the children currently in the cache
*
* @param int $folderId
* @return array[]
*/
protected function getExistingChildren($folderId) {
$existingChildren = [];
$children = $this->cache->getFolderContentsById($folderId);
foreach ($children as $child) {
$existingChildren[$child['name']] = $child;
}
return $existingChildren;
}
/**
* scan all the files and folders in a folder
*
* @param string $path
* @param bool|IScanner::SCAN_RECURSIVE_INCOMPLETE $recursive
* @param int $reuse a combination of self::REUSE_*
* @param int $folderId id for the folder to be scanned
* @param bool $lock set to false to disable getting an additional read lock during scanning
* @param int|float $oldSize the size of the folder before (re)scanning the children
* @return int|float the size of the scanned folder or -1 if the size is unknown at this stage
*/
protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float $oldSize, &$etagChanged = false) {
if ($reuse === -1) {
$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
}
$this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
$size = 0;
$childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size, $etagChanged);
foreach ($childQueue as $child => [$childId, $childSize]) {
// "etag changed" propagates up, but not down, so we pass `false` to the children even if we already know that the etag of the current folder changed
$childEtagChanged = false;
$childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock, $childSize, $childEtagChanged);
$etagChanged |= $childEtagChanged;
if ($childSize === -1) {
$size = -1;
} elseif ($size !== -1) {
$size += $childSize;
}
}
// for encrypted storages, we trigger a regular folder size calculation instead of using the calculated size
// to make sure we also updated the unencrypted-size where applicable
if ($this->storage->instanceOfStorage(Encryption::class)) {
$this->cache->calculateFolderSize($path);
} else {
if ($this->cacheActive) {
$updatedData = [];
if ($oldSize !== $size) {
$updatedData['size'] = $size;
}
if ($etagChanged) {
$updatedData['etag'] = uniqid();
}
if ($updatedData) {
$this->cache->update($folderId, $updatedData);
}
}
}
$this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
return $size;
}
/**
* @param bool|IScanner::SCAN_RECURSIVE_INCOMPLETE $recursive
*/
private function handleChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float &$size, bool &$etagChanged): array {
// we put this in it's own function so it cleans up the memory before we start recursing
$existingChildren = $this->getExistingChildren($folderId);
$newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
if (count($existingChildren) === 0 && count($newChildren) === 0) {
// no need to do a transaction
return [];
}
if ($this->useTransactions) {
$this->connection->beginTransaction();
}
$exceptionOccurred = false;
$childQueue = [];
$newChildNames = [];
foreach ($newChildren as $fileMeta) {
$permissions = $fileMeta['scan_permissions'] ?? $fileMeta['permissions'];
if ($permissions === 0) {
continue;
}
$originalFile = $fileMeta['name'];
$file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
if (trim($originalFile, '/') !== $file) {
// encoding mismatch, might require compatibility wrapper
\OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "'. $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
$this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
// skip this entry
continue;
}
$newChildNames[] = $file;
$child = $path ? $path . '/' . $file : $file;
try {
$existingData = $existingChildren[$file] ?? false;
$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
if ($data) {
if ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE) {
$childQueue[$child] = [$data['fileid'], $data['size']];
} elseif ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE_INCOMPLETE && $data['size'] === -1) {
// only recurse into folders which aren't fully scanned
$childQueue[$child] = [$data['fileid'], $data['size']];
} elseif ($data['size'] === -1) {
$size = -1;
} elseif ($size !== -1) {
$size += $data['size'];
}
if (isset($data['etag_changed']) && $data['etag_changed']) {
$etagChanged = true;
}
}
} catch (Exception $ex) {
// might happen if inserting duplicate while a scanning
// process is running in parallel
// log and ignore
if ($this->useTransactions) {
$this->connection->rollback();
$this->connection->beginTransaction();
}
\OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "' . $child . '"', [
'app' => 'core',
'exception' => $ex,
]);
$exceptionOccurred = true;
} catch (\OCP\Lock\LockedException $e) {
if ($this->useTransactions) {
$this->connection->rollback();
}
throw $e;
}
}
$removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
foreach ($removedChildren as $childName) {
$child = $path ? $path . '/' . $childName : $childName;
$this->removeFromCache($child);
}
if ($this->useTransactions) {
$this->connection->commit();
}
if ($exceptionOccurred) {
// It might happen that the parallel scan process has already
// inserted mimetypes but those weren't available yet inside the transaction
// To make sure to have the updated mime types in such cases,
// we reload them here
\OC::$server->getMimeTypeLoader()->reset();
}
return $childQueue;
}
/**
* check if the file should be ignored when scanning
* NOTE: files with a '.part' extension are ignored as well!
* prevents unfinished put requests to be scanned
*
* @param string $file
* @return boolean
*/
public static function isPartialFile($file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
return true;
}
if (str_contains($file, '.part/')) {
return true;
}
return false;
}
/**
* walk over any folders that are not fully scanned yet and scan them
*/
public function backgroundScan() {
if ($this->storage->instanceOfStorage(Jail::class)) {
// for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
// this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
//
// Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
// have to be scanned at some point anyway.
$unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
$unJailedScanner->backgroundScan();
} else {
if (!$this->cache->inCache('')) {
// if the storage isn't in the cache yet, just scan the root completely
$this->runBackgroundScanJob(function () {
$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
}, '');
} else {
$lastPath = null;
// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
$this->runBackgroundScanJob(function () use ($path) {
$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
}, $path);
// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
// to make this possible
$lastPath = $path;
}
}
}
}
protected function runBackgroundScanJob(callable $callback, $path) {
try {
$callback();
\OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
if ($this->cacheActive && $this->cache instanceof Cache) {
$this->cache->correctFolderSize($path, null, true);
}
} catch (\OCP\Files\StorageInvalidException $e) {
// skip unavailable storages
} catch (\OCP\Files\StorageNotAvailableException $e) {
// skip unavailable storages
} catch (\OCP\Files\ForbiddenException $e) {
// skip forbidden storages
} catch (\OCP\Lock\LockedException $e) {
// skip unavailable storages
}
}
/**
* Set whether the cache is affected by scan operations
*
* @param boolean $active The active state of the cache
*/
public function setCacheActive($active) {
$this->cacheActive = $active;
}
}
@@ -0,0 +1,309 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tobias Kaminsky <tobias@kaminsky.me>
*
* @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 OC\Files\Cache;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchOrder;
use OCP\FilesMetadata\IMetadataQuery;
/**
* Tools for transforming search queries into database queries
*/
class SearchBuilder {
protected static $searchOperatorMap = [
ISearchComparison::COMPARE_LIKE => 'iLike',
ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'like',
ISearchComparison::COMPARE_EQUAL => 'eq',
ISearchComparison::COMPARE_GREATER_THAN => 'gt',
ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte',
ISearchComparison::COMPARE_LESS_THAN => 'lt',
ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte',
ISearchComparison::COMPARE_DEFINED => 'isNotNull',
];
protected static $searchOperatorNegativeMap = [
ISearchComparison::COMPARE_LIKE => 'notLike',
ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'notLike',
ISearchComparison::COMPARE_EQUAL => 'neq',
ISearchComparison::COMPARE_GREATER_THAN => 'lte',
ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt',
ISearchComparison::COMPARE_LESS_THAN => 'gte',
ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'gt',
ISearchComparison::COMPARE_DEFINED => 'isNull',
];
public const TAG_FAVORITE = '_$!<Favorite>!$_';
/** @var IMimeTypeLoader */
private $mimetypeLoader;
public function __construct(
IMimeTypeLoader $mimetypeLoader
) {
$this->mimetypeLoader = $mimetypeLoader;
}
/**
* @return string[]
*/
public function extractRequestedFields(ISearchOperator $operator): array {
if ($operator instanceof ISearchBinaryOperator) {
return array_reduce($operator->getArguments(), function (array $fields, ISearchOperator $operator) {
return array_unique(array_merge($fields, $this->extractRequestedFields($operator)));
}, []);
} elseif ($operator instanceof ISearchComparison && !$operator->getExtra()) {
return [$operator->getField()];
}
return [];
}
/**
* @param IQueryBuilder $builder
* @param ISearchOperator[] $operators
*/
public function searchOperatorArrayToDBExprArray(
IQueryBuilder $builder,
array $operators,
?IMetadataQuery $metadataQuery = null
) {
return array_filter(array_map(function ($operator) use ($builder, $metadataQuery) {
return $this->searchOperatorToDBExpr($builder, $operator, $metadataQuery);
}, $operators));
}
public function searchOperatorToDBExpr(
IQueryBuilder $builder,
ISearchOperator $operator,
?IMetadataQuery $metadataQuery = null
) {
$expr = $builder->expr();
if ($operator instanceof ISearchBinaryOperator) {
if (count($operator->getArguments()) === 0) {
return null;
}
switch ($operator->getType()) {
case ISearchBinaryOperator::OPERATOR_NOT:
$negativeOperator = $operator->getArguments()[0];
if ($negativeOperator instanceof ISearchComparison) {
return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap, $metadataQuery);
} else {
throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
}
// no break
case ISearchBinaryOperator::OPERATOR_AND:
return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments(), $metadataQuery));
case ISearchBinaryOperator::OPERATOR_OR:
return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments(), $metadataQuery));
default:
throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
}
} elseif ($operator instanceof ISearchComparison) {
return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap, $metadataQuery);
} else {
throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
}
}
private function searchComparisonToDBExpr(
IQueryBuilder $builder,
ISearchComparison $comparison,
array $operatorMap,
?IMetadataQuery $metadataQuery = null
) {
if ($comparison->getExtra()) {
[$field, $value, $type] = $this->getExtraOperatorField($comparison, $metadataQuery);
} else {
[$field, $value, $type] = $this->getOperatorFieldAndValue($comparison);
}
if (isset($operatorMap[$type])) {
$queryOperator = $operatorMap[$type];
return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value));
} else {
throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
}
}
private function getOperatorFieldAndValue(ISearchComparison $operator) {
$this->validateComparison($operator);
$field = $operator->getField();
$value = $operator->getValue();
$type = $operator->getType();
if ($field === 'mimetype') {
$value = (string)$value;
if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
$value = (int)$this->mimetypeLoader->getId($value);
} elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
// transform "mimetype='foo/%'" to "mimepart='foo'"
if (preg_match('|(.+)/%|', $value, $matches)) {
$field = 'mimepart';
$value = (int)$this->mimetypeLoader->getId($matches[1]);
$type = ISearchComparison::COMPARE_EQUAL;
} elseif (str_contains($value, '%')) {
throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
} else {
$field = 'mimetype';
$value = (int)$this->mimetypeLoader->getId($value);
$type = ISearchComparison::COMPARE_EQUAL;
}
}
} elseif ($field === 'favorite') {
$field = 'tag.category';
$value = self::TAG_FAVORITE;
} elseif ($field === 'name') {
$field = 'file.name';
} elseif ($field === 'tagname') {
$field = 'tag.category';
} elseif ($field === 'systemtag') {
$field = 'systemtag.name';
} elseif ($field === 'fileid') {
$field = 'file.fileid';
} elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) {
$field = 'path_hash';
$value = md5((string)$value);
} elseif ($field === 'owner') {
$field = 'uid_owner';
}
return [$field, $value, $type];
}
private function validateComparison(ISearchComparison $operator) {
$types = [
'mimetype' => 'string',
'mtime' => 'integer',
'name' => 'string',
'path' => 'string',
'size' => 'integer',
'tagname' => 'string',
'systemtag' => 'string',
'favorite' => 'boolean',
'fileid' => 'integer',
'storage' => 'integer',
'share_with' => 'string',
'share_type' => 'integer',
'owner' => 'string',
];
$comparisons = [
'mimetype' => ['eq', 'like'],
'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
'name' => ['eq', 'like', 'clike'],
'path' => ['eq', 'like', 'clike'],
'size' => ['eq', 'gt', 'lt', 'gte', 'lte'],
'tagname' => ['eq', 'like'],
'systemtag' => ['eq', 'like'],
'favorite' => ['eq'],
'fileid' => ['eq'],
'storage' => ['eq'],
'share_with' => ['eq'],
'share_type' => ['eq'],
'owner' => ['eq'],
];
if (!isset($types[$operator->getField()])) {
throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
}
$type = $types[$operator->getField()];
if (gettype($operator->getValue()) !== $type) {
throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
}
if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
throw new \InvalidArgumentException('Unsupported comparison for field ' . $operator->getField() . ': ' . $operator->getType());
}
}
private function getExtraOperatorField(ISearchComparison $operator, IMetadataQuery $metadataQuery): array {
$field = $operator->getField();
$value = $operator->getValue();
$type = $operator->getType();
switch($operator->getExtra()) {
case IMetadataQuery::EXTRA:
$metadataQuery->joinIndex($field); // join index table if not joined yet
$field = $metadataQuery->getMetadataValueField($field);
break;
default:
throw new \InvalidArgumentException('Invalid extra type: ' . $operator->getExtra());
}
return [$field, $value, $type];
}
private function getParameterForValue(IQueryBuilder $builder, $value) {
if ($value instanceof \DateTime) {
$value = $value->getTimestamp();
}
if (is_numeric($value)) {
$type = IQueryBuilder::PARAM_INT;
} else {
$type = IQueryBuilder::PARAM_STR;
}
return $builder->createNamedParameter($value, $type);
}
/**
* @param IQueryBuilder $query
* @param ISearchOrder[] $orders
* @param IMetadataQuery|null $metadataQuery
*/
public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders, ?IMetadataQuery $metadataQuery = null): void {
foreach ($orders as $order) {
$field = $order->getField();
switch ($order->getExtra()) {
case IMetadataQuery::EXTRA:
$metadataQuery->joinIndex($field); // join index table if not joined yet
$field = $metadataQuery->getMetadataValueField($order->getField());
break;
default:
if ($field === 'fileid') {
$field = 'file.fileid';
}
// Mysql really likes to pick an index for sorting if it can't fully satisfy the where
// filter with an index, since search queries pretty much never are fully filtered by index
// mysql often picks an index for sorting instead of the much more useful index for filtering.
//
// By changing the order by to an expression, mysql isn't smart enough to see that it could still
// use the index, so it instead picks an index for the filtering
if ($field === 'mtime') {
$field = $query->func()->add($field, $query->createNamedParameter(0));
}
}
$query->addOrderBy($field, $order->getDirection());
}
}
}
@@ -0,0 +1,256 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @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 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 OC\Files\Cache;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Storage\IStorage;
use Psr\Log\LoggerInterface;
/**
* Handle the mapping between the string and numeric storage ids
*
* Each storage has 2 different ids
* a string id which is generated by the storage backend and reflects the configuration of the storage (e.g. 'smb://user@host/share')
* and a numeric storage id which is referenced in the file cache
*
* A mapping between the two storage ids is stored in the database and accessible through this class
*
* @package OC\Files\Cache
*/
class Storage {
/** @var StorageGlobal|null */
private static $globalCache = null;
private $storageId;
private $numericId;
/**
* @return StorageGlobal
*/
public static function getGlobalCache() {
if (is_null(self::$globalCache)) {
self::$globalCache = new StorageGlobal(\OC::$server->getDatabaseConnection());
}
return self::$globalCache;
}
/**
* @param \OC\Files\Storage\Storage|string $storage
* @param bool $isAvailable
* @throws \RuntimeException
*/
public function __construct($storage, $isAvailable = true) {
if ($storage instanceof IStorage) {
$this->storageId = $storage->getId();
} else {
$this->storageId = $storage;
}
$this->storageId = self::adjustStorageId($this->storageId);
if ($row = self::getStorageById($this->storageId)) {
$this->numericId = (int)$row['numeric_id'];
} else {
$connection = \OC::$server->getDatabaseConnection();
$available = $isAvailable ? 1 : 0;
if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId, 'available' => $available])) {
$this->numericId = $connection->lastInsertId('*PREFIX*storages');
} else {
if ($row = self::getStorageById($this->storageId)) {
$this->numericId = (int)$row['numeric_id'];
} else {
throw new \RuntimeException('Storage could neither be inserted nor be selected from the database: ' . $this->storageId);
}
}
}
}
/**
* @param string $storageId
* @return array
*/
public static function getStorageById($storageId) {
return self::getGlobalCache()->getStorageInfo($storageId);
}
/**
* Adjusts the storage id to use md5 if too long
* @param string $storageId storage id
* @return string unchanged $storageId if its length is less than 64 characters,
* else returns the md5 of $storageId
*/
public static function adjustStorageId($storageId) {
if (strlen($storageId) > 64) {
return md5($storageId);
}
return $storageId;
}
/**
* Get the numeric id for the storage
*
* @return int
*/
public function getNumericId() {
return $this->numericId;
}
/**
* Get the string id for the storage
*
* @param int $numericId
* @return string|null either the storage id string or null if the numeric id is not known
*/
public static function getStorageId(int $numericId): ?string {
$storage = self::getGlobalCache()->getStorageInfoByNumericId($numericId);
return $storage['id'] ?? null;
}
/**
* Get the numeric of the storage with the provided string id
*
* @param $storageId
* @return int|null either the numeric storage id or null if the storage id is not known
*/
public static function getNumericStorageId($storageId) {
$storageId = self::adjustStorageId($storageId);
if ($row = self::getStorageById($storageId)) {
return (int)$row['numeric_id'];
} else {
return null;
}
}
/**
* @return array [ available, last_checked ]
*/
public function getAvailability() {
if ($row = self::getStorageById($this->storageId)) {
return [
'available' => (int)$row['available'] === 1,
'last_checked' => $row['last_checked']
];
} else {
return [
'available' => true,
'last_checked' => time(),
];
}
}
/**
* @param bool $isAvailable
* @param int $delay amount of seconds to delay reconsidering that storage further
*/
public function setAvailability($isAvailable, int $delay = 0) {
$available = $isAvailable ? 1 : 0;
if (!$isAvailable) {
\OC::$server->get(LoggerInterface::class)->info('Storage with ' . $this->storageId . ' marked as unavailable', ['app' => 'lib']);
}
$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
$query->update('storages')
->set('available', $query->createNamedParameter($available))
->set('last_checked', $query->createNamedParameter(time() + $delay))
->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
$query->execute();
}
/**
* Check if a string storage id is known
*
* @param string $storageId
* @return bool
*/
public static function exists($storageId) {
return !is_null(self::getNumericStorageId($storageId));
}
/**
* remove the entry for the storage
*
* @param string $storageId
*/
public static function remove($storageId) {
$storageId = self::adjustStorageId($storageId);
$numericId = self::getNumericStorageId($storageId);
$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
$query->delete('storages')
->where($query->expr()->eq('id', $query->createNamedParameter($storageId)));
$query->execute();
if (!is_null($numericId)) {
$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
$query->delete('filecache')
->where($query->expr()->eq('storage', $query->createNamedParameter($numericId)));
$query->execute();
}
}
/**
* remove the entry for the storage by the mount id
*
* @param int $mountId
*/
public static function cleanByMountId(int $mountId) {
$db = \OC::$server->getDatabaseConnection();
try {
$db->beginTransaction();
$query = $db->getQueryBuilder();
$query->select('storage_id')
->from('mounts')
->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
$storageIds = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
$storageIds = array_unique($storageIds);
$query = $db->getQueryBuilder();
$query->delete('filecache')
->where($query->expr()->in('storage', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
$query->executeStatement();
$query = $db->getQueryBuilder();
$query->delete('storages')
->where($query->expr()->in('numeric_id', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
$query->executeStatement();
$query = $db->getQueryBuilder();
$query->delete('mounts')
->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
$query->executeStatement();
$db->commit();
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
}
}
@@ -0,0 +1,118 @@
<?php
/**
* @copyright Robin Appelman <robin@icewind.nl>
*
* @author Joas Schilling <coding@schilljs.com>
* @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 OC\Files\Cache;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* Handle the mapping between the string and numeric storage ids
*
* Each storage has 2 different ids
* a string id which is generated by the storage backend and reflects the configuration of the storage (e.g. 'smb://user@host/share')
* and a numeric storage id which is referenced in the file cache
*
* A mapping between the two storage ids is stored in the database and accessible through this class
*
* @package OC\Files\Cache
*/
class StorageGlobal {
/** @var IDBConnection */
private $connection;
/** @var array<string, array> */
private $cache = [];
/** @var array<int, array> */
private $numericIdCache = [];
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
/**
* @param string[] $storageIds
*/
public function loadForStorageIds(array $storageIds) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select(['id', 'numeric_id', 'available', 'last_checked'])
->from('storages')
->where($builder->expr()->in('id', $builder->createNamedParameter(array_values($storageIds), IQueryBuilder::PARAM_STR_ARRAY)));
$result = $query->execute();
while ($row = $result->fetch()) {
$this->cache[$row['id']] = $row;
}
$result->closeCursor();
}
/**
* @param string $storageId
* @return array|null
*/
public function getStorageInfo(string $storageId): ?array {
if (!isset($this->cache[$storageId])) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select(['id', 'numeric_id', 'available', 'last_checked'])
->from('storages')
->where($builder->expr()->eq('id', $builder->createNamedParameter($storageId)));
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
if ($row) {
$this->cache[$storageId] = $row;
$this->numericIdCache[(int)$row['numeric_id']] = $row;
}
}
return $this->cache[$storageId] ?? null;
}
/**
* @param int $numericId
* @return array|null
*/
public function getStorageInfoByNumericId(int $numericId): ?array {
if (!isset($this->numericIdCache[$numericId])) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select(['id', 'numeric_id', 'available', 'last_checked'])
->from('storages')
->where($builder->expr()->eq('numeric_id', $builder->createNamedParameter($numericId)));
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
if ($row) {
$this->numericIdCache[$numericId] = $row;
$this->cache[$row['id']] = $row;
}
}
return $this->numericIdCache[$numericId] ?? null;
}
public function clearCache() {
$this->cache = [];
}
}
@@ -0,0 +1,272 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Jagszent <daniel@jagszent.de>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.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 OC\Files\Cache;
use Doctrine\DBAL\Exception\DeadlockException;
use OC\Files\FileInfo;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Cache\IUpdater;
use OCP\Files\Storage\IStorage;
use Psr\Log\LoggerInterface;
/**
* Update the cache and propagate changes
*
*/
class Updater implements IUpdater {
/**
* @var bool
*/
protected $enabled = true;
/**
* @var \OC\Files\Storage\Storage
*/
protected $storage;
/**
* @var \OC\Files\Cache\Propagator
*/
protected $propagator;
/**
* @var Scanner
*/
protected $scanner;
/**
* @var Cache
*/
protected $cache;
private LoggerInterface $logger;
/**
* @param \OC\Files\Storage\Storage $storage
*/
public function __construct(\OC\Files\Storage\Storage $storage) {
$this->storage = $storage;
$this->propagator = $storage->getPropagator();
$this->scanner = $storage->getScanner();
$this->cache = $storage->getCache();
$this->logger = \OC::$server->get(LoggerInterface::class);
}
/**
* Disable updating the cache through this updater
*/
public function disable() {
$this->enabled = false;
}
/**
* Re-enable the updating of the cache through this updater
*/
public function enable() {
$this->enabled = true;
}
/**
* Get the propagator for etags and mtime for the view the updater works on
*
* @return Propagator
*/
public function getPropagator() {
return $this->propagator;
}
/**
* Propagate etag and mtime changes for the parent folders of $path up to the root of the filesystem
*
* @param string $path the path of the file to propagate the changes for
* @param int|null $time the timestamp to set as mtime for the parent folders, if left out the current time is used
*/
public function propagate($path, $time = null) {
if (Scanner::isPartialFile($path)) {
return;
}
$this->propagator->propagateChange($path, $time);
}
/**
* Update the cache for $path and update the size, etag and mtime of the parent folders
*
* @param string $path
* @param int $time
*/
public function update($path, $time = null) {
if (!$this->enabled or Scanner::isPartialFile($path)) {
return;
}
if (is_null($time)) {
$time = time();
}
$data = $this->scanner->scan($path, Scanner::SCAN_SHALLOW, -1, false);
if (
isset($data['oldSize']) && isset($data['size']) &&
!$data['encrypted'] // encryption is a pita and touches the cache itself
) {
$sizeDifference = $data['size'] - $data['oldSize'];
} else {
// scanner didn't provide size info, fallback to full size calculation
$sizeDifference = 0;
if ($this->cache instanceof Cache) {
$this->cache->correctFolderSize($path, $data);
}
}
$this->correctParentStorageMtime($path);
$this->propagator->propagateChange($path, $time, $sizeDifference);
}
/**
* Remove $path from the cache and update the size, etag and mtime of the parent folders
*
* @param string $path
*/
public function remove($path) {
if (!$this->enabled or Scanner::isPartialFile($path)) {
return;
}
$parent = dirname($path);
if ($parent === '.') {
$parent = '';
}
$entry = $this->cache->get($path);
$this->cache->remove($path);
$this->correctParentStorageMtime($path);
if ($entry instanceof ICacheEntry) {
$this->propagator->propagateChange($path, time(), -$entry->getSize());
} else {
$this->propagator->propagateChange($path, time());
if ($this->cache instanceof Cache) {
$this->cache->correctFolderSize($parent);
}
}
}
/**
* Rename a file or folder in the cache and update the size, etag and mtime of the parent folders
*
* @param IStorage $sourceStorage
* @param string $source
* @param string $target
*/
public function renameFromStorage(IStorage $sourceStorage, $source, $target) {
if (!$this->enabled or Scanner::isPartialFile($source) or Scanner::isPartialFile($target)) {
return;
}
$time = time();
$sourceCache = $sourceStorage->getCache();
$sourceUpdater = $sourceStorage->getUpdater();
$sourcePropagator = $sourceStorage->getPropagator();
$sourceInfo = $sourceCache->get($source);
if ($sourceInfo !== false) {
if ($this->cache->inCache($target)) {
$this->cache->remove($target);
}
if ($sourceStorage === $this->storage) {
$this->cache->move($source, $target);
} else {
$this->cache->moveFromCache($sourceCache, $source, $target);
}
$sourceExtension = pathinfo($source, PATHINFO_EXTENSION);
$targetExtension = pathinfo($target, PATHINFO_EXTENSION);
$targetIsTrash = preg_match("/d\d+/", $targetExtension);
if ($sourceExtension !== $targetExtension && $sourceInfo->getMimeType() !== FileInfo::MIMETYPE_FOLDER && !$targetIsTrash) {
// handle mime type change
$mimeType = $this->storage->getMimeType($target);
$fileId = $this->cache->getId($target);
$this->cache->update($fileId, ['mimetype' => $mimeType]);
}
}
if ($sourceCache instanceof Cache) {
$sourceCache->correctFolderSize($source);
}
if ($this->cache instanceof Cache) {
$this->cache->correctFolderSize($target);
}
if ($sourceUpdater instanceof Updater) {
$sourceUpdater->correctParentStorageMtime($source);
}
$this->correctParentStorageMtime($target);
$this->updateStorageMTimeOnly($target);
$sourcePropagator->propagateChange($source, $time);
$this->propagator->propagateChange($target, $time);
}
private function updateStorageMTimeOnly($internalPath) {
$fileId = $this->cache->getId($internalPath);
if ($fileId !== -1) {
$mtime = $this->storage->filemtime($internalPath);
if ($mtime !== false) {
$this->cache->update(
$fileId, [
'mtime' => null, // this magic tells it to not overwrite mtime
'storage_mtime' => $mtime
]
);
}
}
}
/**
* update the storage_mtime of the direct parent in the cache to the mtime from the storage
*
* @param string $internalPath
*/
private function correctParentStorageMtime($internalPath) {
$parentId = $this->cache->getParentId($internalPath);
$parent = dirname($internalPath);
if ($parentId != -1) {
$mtime = $this->storage->filemtime($parent);
if ($mtime !== false) {
try {
$this->cache->update($parentId, ['storage_mtime' => $mtime]);
} catch (DeadlockException $e) {
// ignore the failure.
// with failures concurrent updates, someone else would have already done it.
// in the worst case the `storage_mtime` isn't updated, which should at most only trigger an extra rescan
$this->logger->warning("Error while updating parent storage_mtime, should be safe to ignore", ['exception' => $e]);
}
}
}
}
}
@@ -0,0 +1,152 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Jagszent <daniel@jagszent.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @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 OC\Files\Cache;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Cache\IWatcher;
/**
* check the storage backends for updates and change the cache accordingly
*/
class Watcher implements IWatcher {
protected $watchPolicy = self::CHECK_ONCE;
protected $checkedPaths = [];
/**
* @var \OC\Files\Storage\Storage $storage
*/
protected $storage;
/**
* @var Cache $cache
*/
protected $cache;
/**
* @var Scanner $scanner ;
*/
protected $scanner;
/**
* @param \OC\Files\Storage\Storage $storage
*/
public function __construct(\OC\Files\Storage\Storage $storage) {
$this->storage = $storage;
$this->cache = $storage->getCache();
$this->scanner = $storage->getScanner();
}
/**
* @param int $policy either \OC\Files\Cache\Watcher::CHECK_NEVER, \OC\Files\Cache\Watcher::CHECK_ONCE, \OC\Files\Cache\Watcher::CHECK_ALWAYS
*/
public function setPolicy($policy) {
$this->watchPolicy = $policy;
}
/**
* @return int either \OC\Files\Cache\Watcher::CHECK_NEVER, \OC\Files\Cache\Watcher::CHECK_ONCE, \OC\Files\Cache\Watcher::CHECK_ALWAYS
*/
public function getPolicy() {
return $this->watchPolicy;
}
/**
* check $path for updates and update if needed
*
* @param string $path
* @param ICacheEntry|null $cachedEntry
* @return boolean true if path was updated
*/
public function checkUpdate($path, $cachedEntry = null) {
if (is_null($cachedEntry)) {
$cachedEntry = $this->cache->get($path);
}
if ($cachedEntry === false || $this->needsUpdate($path, $cachedEntry)) {
$this->update($path, $cachedEntry);
if ($cachedEntry === false) {
return true;
} else {
// storage backends can sometimes return false positives, only return true if the scanner actually found a change
$newEntry = $this->cache->get($path);
return $newEntry->getStorageMTime() > $cachedEntry->getStorageMTime();
}
} else {
return false;
}
}
/**
* Update the cache for changes to $path
*
* @param string $path
* @param ICacheEntry $cachedData
*/
public function update($path, $cachedData) {
if ($this->storage->is_dir($path)) {
$this->scanner->scan($path, Scanner::SCAN_SHALLOW);
} else {
$this->scanner->scanFile($path);
}
if (is_array($cachedData) && $cachedData['mimetype'] === 'httpd/unix-directory') {
$this->cleanFolder($path);
}
if ($this->cache instanceof Cache) {
$this->cache->correctFolderSize($path);
}
}
/**
* Check if the cache for $path needs to be updated
*
* @param string $path
* @param ICacheEntry $cachedData
* @return bool
*/
public function needsUpdate($path, $cachedData) {
if ($this->watchPolicy === self::CHECK_ALWAYS or ($this->watchPolicy === self::CHECK_ONCE and !in_array($path, $this->checkedPaths))) {
$this->checkedPaths[] = $path;
return $this->storage->hasUpdated($path, $cachedData['storage_mtime']);
}
return false;
}
/**
* remove deleted files in $path from the cache
*
* @param string $path
*/
public function cleanFolder($path) {
$cachedContent = $this->cache->getFolderContents($path);
foreach ($cachedContent as $entry) {
if (!$this->storage->file_exists($entry['path'])) {
$this->cache->remove($entry['path']);
}
}
}
}
@@ -0,0 +1,341 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Jagszent <daniel@jagszent.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 Thomas Müller <thomas.mueller@tmit.eu>
*
* @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 OC\Files\Cache\Wrapper;
use OC\Files\Cache\Cache;
use OC\Files\Search\SearchBinaryOperator;
use OC\Files\Search\SearchComparison;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
/**
* Jail to a subdirectory of the wrapped cache
*/
class CacheJail extends CacheWrapper {
/**
* @var string
*/
protected $root;
protected $unjailedRoot;
/**
* @param ?\OCP\Files\Cache\ICache $cache
* @param string $root
*/
public function __construct($cache, $root) {
parent::__construct($cache);
$this->root = $root;
if ($cache instanceof CacheJail) {
$this->unjailedRoot = $cache->getSourcePath($root);
} else {
$this->unjailedRoot = $root;
}
}
protected function getRoot() {
return $this->root;
}
/**
* Get the root path with any nested jails resolved
*
* @return string
*/
protected function getGetUnjailedRoot() {
return $this->unjailedRoot;
}
protected function getSourcePath($path) {
if ($path === '') {
return $this->getRoot();
} else {
return $this->getRoot() . '/' . ltrim($path, '/');
}
}
/**
* @param string $path
* @param null|string $root
* @return null|string the jailed path or null if the path is outside the jail
*/
protected function getJailedPath(string $path, string $root = null) {
if ($root === null) {
$root = $this->getRoot();
}
if ($root === '') {
return $path;
}
$rootLength = strlen($root) + 1;
if ($path === $root) {
return '';
} elseif (substr($path, 0, $rootLength) === $root . '/') {
return substr($path, $rootLength);
} else {
return null;
}
}
protected function formatCacheEntry($entry) {
if (isset($entry['path'])) {
$entry['path'] = $this->getJailedPath($entry['path']);
}
return $entry;
}
/**
* get the stored metadata of a file or folder
*
* @param string /int $file
* @return ICacheEntry|false
*/
public function get($file) {
if (is_string($file) or $file == '') {
$file = $this->getSourcePath($file);
}
return parent::get($file);
}
/**
* insert meta data for a new file or folder
*
* @param string $file
* @param array $data
*
* @return int file id
* @throws \RuntimeException
*/
public function insert($file, array $data) {
return $this->getCache()->insert($this->getSourcePath($file), $data);
}
/**
* update the metadata in the cache
*
* @param int $id
* @param array $data
*/
public function update($id, array $data) {
$this->getCache()->update($id, $data);
}
/**
* get the file id for a file
*
* @param string $file
* @return int
*/
public function getId($file) {
return $this->getCache()->getId($this->getSourcePath($file));
}
/**
* get the id of the parent folder of a file
*
* @param string $file
* @return int
*/
public function getParentId($file) {
return $this->getCache()->getParentId($this->getSourcePath($file));
}
/**
* check if a file is available in the cache
*
* @param string $file
* @return bool
*/
public function inCache($file) {
return $this->getCache()->inCache($this->getSourcePath($file));
}
/**
* remove a file or folder from the cache
*
* @param string $file
*/
public function remove($file) {
$this->getCache()->remove($this->getSourcePath($file));
}
/**
* Move a file or folder in the cache
*
* @param string $source
* @param string $target
*/
public function move($source, $target) {
$this->getCache()->move($this->getSourcePath($source), $this->getSourcePath($target));
}
/**
* Get the storage id and path needed for a move
*
* @param string $path
* @return array [$storageId, $internalPath]
*/
protected function getMoveInfo($path) {
return [$this->getNumericStorageId(), $this->getSourcePath($path)];
}
/**
* remove all entries for files that are stored on the storage from the cache
*/
public function clear() {
$this->getCache()->remove($this->getRoot());
}
/**
* @param string $file
*
* @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
*/
public function getStatus($file) {
return $this->getCache()->getStatus($this->getSourcePath($file));
}
/**
* update the folder size and the size of all parent folders
*
* @param string|boolean $path
* @param array $data (optional) meta data of the folder
*/
public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
if ($this->getCache() instanceof Cache) {
$this->getCache()->correctFolderSize($this->getSourcePath($path), $data, $isBackgroundScan);
}
}
/**
* get the size of a folder and set it in the cache
*
* @param string $path
* @param array|null|ICacheEntry $entry (optional) meta data of the folder
* @return int|float
*/
public function calculateFolderSize($path, $entry = null) {
if ($this->getCache() instanceof Cache) {
return $this->getCache()->calculateFolderSize($this->getSourcePath($path), $entry);
} else {
return 0;
}
}
/**
* get all file ids on the files on the storage
*
* @return int[]
*/
public function getAll() {
// not supported
return [];
}
/**
* find a folder in the cache which has not been fully scanned
*
* If multiply incomplete folders are in the cache, the one with the highest id will be returned,
* use the one with the highest id gives the best result with the background scanner, since that is most
* likely the folder where we stopped scanning previously
*
* @return string|false the path of the folder or false when no folder matched
*/
public function getIncomplete() {
// not supported
return false;
}
/**
* get the path of a file on this storage by it's id
*
* @param int $id
* @return string|null
*/
public function getPathById($id) {
$path = $this->getCache()->getPathById($id);
if ($path === null) {
return null;
}
return $this->getJailedPath($path);
}
/**
* Move a file or folder in the cache
*
* Note that this should make sure the entries are removed from the source cache
*
* @param \OCP\Files\Cache\ICache $sourceCache
* @param string $sourcePath
* @param string $targetPath
*/
public function moveFromCache(\OCP\Files\Cache\ICache $sourceCache, $sourcePath, $targetPath) {
if ($sourceCache === $this) {
return $this->move($sourcePath, $targetPath);
}
return $this->getCache()->moveFromCache($sourceCache, $sourcePath, $this->getSourcePath($targetPath));
}
public function getQueryFilterForStorage(): ISearchOperator {
return $this->addJailFilterQuery($this->getCache()->getQueryFilterForStorage());
}
protected function addJailFilterQuery(ISearchOperator $filter): ISearchOperator {
if ($this->getGetUnjailedRoot() !== '' && $this->getGetUnjailedRoot() !== '/') {
return new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND,
[
$filter,
new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR,
[
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', $this->getGetUnjailedRoot()),
new SearchComparison(ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE, 'path', SearchComparison::escapeLikeParameter($this->getGetUnjailedRoot()) . '/%'),
],
)
]
);
} else {
return $filter;
}
}
public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
if ($this->getGetUnjailedRoot() === '' || str_starts_with($rawEntry->getPath(), $this->getGetUnjailedRoot())) {
$rawEntry = $this->getCache()->getCacheEntryFromSearchResult($rawEntry);
if ($rawEntry) {
$jailedPath = $this->getJailedPath($rawEntry->getPath());
if ($jailedPath !== null) {
return $this->formatCacheEntry(clone $rawEntry) ?: null;
}
}
}
return null;
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @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 OC\Files\Cache\Wrapper;
class CachePermissionsMask extends CacheWrapper {
/**
* @var int
*/
protected $mask;
/**
* @param \OCP\Files\Cache\ICache $cache
* @param int $mask
*/
public function __construct($cache, $mask) {
parent::__construct($cache);
$this->mask = $mask;
}
protected function formatCacheEntry($entry) {
if (isset($entry['permissions'])) {
$entry['scan_permissions'] = $entry['permissions'];
$entry['permissions'] &= $this->mask;
}
return $entry;
}
}
@@ -0,0 +1,337 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Ari Selseng <ari@selseng.net>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Jagszent <daniel@jagszent.de>
* @author Joas Schilling <coding@schilljs.com>
* @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 OC\Files\Cache\Wrapper;
use OC\Files\Cache\Cache;
use OC\Files\Cache\QuerySearchHelper;
use OCP\Files\Cache\ICache;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchQuery;
use OCP\IDBConnection;
class CacheWrapper extends Cache {
/**
* @var \OCP\Files\Cache\ICache
*/
protected $cache;
/**
* @param \OCP\Files\Cache\ICache $cache
*/
public function __construct($cache) {
$this->cache = $cache;
if ($cache instanceof Cache) {
$this->mimetypeLoader = $cache->mimetypeLoader;
$this->connection = $cache->connection;
$this->querySearchHelper = $cache->querySearchHelper;
} else {
$this->mimetypeLoader = \OC::$server->get(IMimeTypeLoader::class);
$this->connection = \OC::$server->get(IDBConnection::class);
$this->querySearchHelper = \OC::$server->get(QuerySearchHelper::class);
}
}
protected function getCache() {
return $this->cache;
}
protected function hasEncryptionWrapper(): bool {
$cache = $this->getCache();
if ($cache instanceof Cache) {
return $cache->hasEncryptionWrapper();
} else {
return false;
}
}
/**
* Make it easy for wrappers to modify every returned cache entry
*
* @param ICacheEntry $entry
* @return ICacheEntry|false
*/
protected function formatCacheEntry($entry) {
return $entry;
}
/**
* get the stored metadata of a file or folder
*
* @param string|int $file
* @return ICacheEntry|false
*/
public function get($file) {
$result = $this->getCache()->get($file);
if ($result instanceof ICacheEntry) {
$result = $this->formatCacheEntry($result);
}
return $result;
}
/**
* get the metadata of all files stored in $folder
*
* @param string $folder
* @return ICacheEntry[]
*/
public function getFolderContents($folder) {
// can't do a simple $this->getCache()->.... call here since getFolderContentsById needs to be called on this
// and not the wrapped cache
$fileId = $this->getId($folder);
return $this->getFolderContentsById($fileId);
}
/**
* get the metadata of all files stored in $folder
*
* @param int $fileId the file id of the folder
* @return array
*/
public function getFolderContentsById($fileId) {
$results = $this->getCache()->getFolderContentsById($fileId);
return array_map([$this, 'formatCacheEntry'], $results);
}
/**
* insert or update meta data for a file or folder
*
* @param string $file
* @param array $data
*
* @return int file id
* @throws \RuntimeException
*/
public function put($file, array $data) {
if (($id = $this->getId($file)) > -1) {
$this->update($id, $data);
return $id;
} else {
return $this->insert($file, $data);
}
}
/**
* insert meta data for a new file or folder
*
* @param string $file
* @param array $data
*
* @return int file id
* @throws \RuntimeException
*/
public function insert($file, array $data) {
return $this->getCache()->insert($file, $data);
}
/**
* update the metadata in the cache
*
* @param int $id
* @param array $data
*/
public function update($id, array $data) {
$this->getCache()->update($id, $data);
}
/**
* get the file id for a file
*
* @param string $file
* @return int
*/
public function getId($file) {
return $this->getCache()->getId($file);
}
/**
* get the id of the parent folder of a file
*
* @param string $file
* @return int
*/
public function getParentId($file) {
return $this->getCache()->getParentId($file);
}
/**
* check if a file is available in the cache
*
* @param string $file
* @return bool
*/
public function inCache($file) {
return $this->getCache()->inCache($file);
}
/**
* remove a file or folder from the cache
*
* @param string $file
*/
public function remove($file) {
$this->getCache()->remove($file);
}
/**
* Move a file or folder in the cache
*
* @param string $source
* @param string $target
*/
public function move($source, $target) {
$this->getCache()->move($source, $target);
}
protected function getMoveInfo($path) {
/** @var Cache $cache */
$cache = $this->getCache();
return $cache->getMoveInfo($path);
}
public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
$this->getCache()->moveFromCache($sourceCache, $sourcePath, $targetPath);
}
/**
* remove all entries for files that are stored on the storage from the cache
*/
public function clear() {
$this->getCache()->clear();
}
/**
* @param string $file
*
* @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
*/
public function getStatus($file) {
return $this->getCache()->getStatus($file);
}
public function searchQuery(ISearchQuery $searchQuery) {
return current($this->querySearchHelper->searchInCaches($searchQuery, [$this]));
}
/**
* update the folder size and the size of all parent folders
*
* @param string|boolean $path
* @param array $data (optional) meta data of the folder
*/
public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
if ($this->getCache() instanceof Cache) {
$this->getCache()->correctFolderSize($path, $data, $isBackgroundScan);
}
}
/**
* get the size of a folder and set it in the cache
*
* @param string $path
* @param array|null|ICacheEntry $entry (optional) meta data of the folder
* @return int|float
*/
public function calculateFolderSize($path, $entry = null) {
if ($this->getCache() instanceof Cache) {
return $this->getCache()->calculateFolderSize($path, $entry);
} else {
return 0;
}
}
/**
* get all file ids on the files on the storage
*
* @return int[]
*/
public function getAll() {
return $this->getCache()->getAll();
}
/**
* find a folder in the cache which has not been fully scanned
*
* If multiple incomplete folders are in the cache, the one with the highest id will be returned,
* use the one with the highest id gives the best result with the background scanner, since that is most
* likely the folder where we stopped scanning previously
*
* @return string|false the path of the folder or false when no folder matched
*/
public function getIncomplete() {
return $this->getCache()->getIncomplete();
}
/**
* get the path of a file on this storage by it's id
*
* @param int $id
* @return string|null
*/
public function getPathById($id) {
return $this->getCache()->getPathById($id);
}
/**
* Returns the numeric storage id
*
* @return int
*/
public function getNumericStorageId() {
return $this->getCache()->getNumericStorageId();
}
/**
* get the storage id of the storage for a file and the internal path of the file
* unlike getPathById this does not limit the search to files on this storage and
* instead does a global search in the cache table
*
* @param int $id
* @return array first element holding the storage id, second the path
*/
public static function getById($id) {
return parent::getById($id);
}
public function getQueryFilterForStorage(): ISearchOperator {
return $this->getCache()->getQueryFilterForStorage();
}
public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
$rawEntry = $this->getCache()->getCacheEntryFromSearchResult($rawEntry);
if ($rawEntry) {
$entry = $this->formatCacheEntry(clone $rawEntry);
return $entry ?: null;
}
return null;
}
}
@@ -0,0 +1,44 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\Cache\Wrapper;
use OC\Files\Cache\Propagator;
use OC\Files\Storage\Wrapper\Jail;
class JailPropagator extends Propagator {
/**
* @var Jail
*/
protected $storage;
/**
* @param string $internalPath
* @param int $time
* @param int $sizeDifference
*/
public function propagateChange($internalPath, $time, $sizeDifference = 0) {
/** @var \OC\Files\Storage\Storage $storage */
[$storage, $sourceInternalPath] = $this->storage->resolvePath($internalPath);
$storage->getPropagator()->propagateChange($sourceInternalPath, $time, $sizeDifference);
}
}
@@ -0,0 +1,57 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\Config;
use OCP\Files\Config\ICachedMountFileInfo;
use OCP\IUser;
class CachedMountFileInfo extends CachedMountInfo implements ICachedMountFileInfo {
private string $internalPath;
public function __construct(
IUser $user,
int $storageId,
int $rootId,
string $mountPoint,
?int $mountId,
string $mountProvider,
string $rootInternalPath,
string $internalPath
) {
parent::__construct($user, $storageId, $rootId, $mountPoint, $mountProvider, $mountId, $rootInternalPath);
$this->internalPath = $internalPath;
}
public function getInternalPath(): string {
if ($this->getRootInternalPath()) {
return substr($this->internalPath, strlen($this->getRootInternalPath()) + 1);
} else {
return $this->internalPath;
}
}
public function getPath(): string {
return $this->getMountPoint() . $this->getInternalPath();
}
}
@@ -0,0 +1,141 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin Appelman <robin@icewind.nl>
* @author Semih Serhat Karakaya <karakayasemi@itu.edu.tr>
*
* @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 OC\Files\Config;
use OC\Files\Filesystem;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Node;
use OCP\IUser;
class CachedMountInfo implements ICachedMountInfo {
protected IUser $user;
protected int $storageId;
protected int $rootId;
protected string $mountPoint;
protected ?int $mountId;
protected string $rootInternalPath;
protected string $mountProvider;
protected string $key;
/**
* CachedMountInfo constructor.
*
* @param IUser $user
* @param int $storageId
* @param int $rootId
* @param string $mountPoint
* @param int|null $mountId
* @param string $rootInternalPath
*/
public function __construct(
IUser $user,
int $storageId,
int $rootId,
string $mountPoint,
string $mountProvider,
int $mountId = null,
string $rootInternalPath = ''
) {
$this->user = $user;
$this->storageId = $storageId;
$this->rootId = $rootId;
$this->mountPoint = $mountPoint;
$this->mountId = $mountId;
$this->rootInternalPath = $rootInternalPath;
if (strlen($mountProvider) > 128) {
throw new \Exception("Mount provider $mountProvider name exceeds the limit of 128 characters");
}
$this->mountProvider = $mountProvider;
$this->key = $rootId . '::' . $mountPoint;
}
/**
* @return IUser
*/
public function getUser(): IUser {
return $this->user;
}
/**
* @return int the numeric storage id of the mount
*/
public function getStorageId(): int {
return $this->storageId;
}
/**
* @return int the fileid of the root of the mount
*/
public function getRootId(): int {
return $this->rootId;
}
/**
* @return Node|null the root node of the mount
*/
public function getMountPointNode(): ?Node {
// TODO injection etc
Filesystem::initMountPoints($this->getUser()->getUID());
$userNode = \OC::$server->getUserFolder($this->getUser()->getUID());
$nodes = $userNode->getParent()->getById($this->getRootId());
if (count($nodes) > 0) {
return $nodes[0];
} else {
return null;
}
}
/**
* @return string the mount point of the mount for the user
*/
public function getMountPoint(): string {
return $this->mountPoint;
}
/**
* Get the id of the configured mount
*
* @return int|null mount id or null if not applicable
* @since 9.1.0
*/
public function getMountId(): ?int {
return $this->mountId;
}
/**
* Get the internal path (within the storage) of the root of the mount
*
* @return string
*/
public function getRootInternalPath(): string {
return $this->rootInternalPath;
}
public function getMountProvider(): string {
return $this->mountProvider;
}
public function getKey(): string {
return $this->key;
}
}
@@ -0,0 +1,98 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @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 OC\Files\Config;
use OCP\Files\Mount\IMountPoint;
use OCP\IUser;
class LazyStorageMountInfo extends CachedMountInfo {
private IMountPoint $mount;
/**
* CachedMountInfo constructor.
*
* @param IUser $user
* @param IMountPoint $mount
*/
public function __construct(IUser $user, IMountPoint $mount) {
$this->user = $user;
$this->mount = $mount;
$this->rootId = 0;
$this->storageId = 0;
$this->mountPoint = '';
$this->key = '';
}
/**
* @return int the numeric storage id of the mount
*/
public function getStorageId(): int {
if (!$this->storageId) {
$this->storageId = $this->mount->getNumericStorageId();
}
return parent::getStorageId();
}
/**
* @return int the fileid of the root of the mount
*/
public function getRootId(): int {
if (!$this->rootId) {
$this->rootId = $this->mount->getStorageRootId();
}
return parent::getRootId();
}
/**
* @return string the mount point of the mount for the user
*/
public function getMountPoint(): string {
if (!$this->mountPoint) {
$this->mountPoint = $this->mount->getMountPoint();
}
return parent::getMountPoint();
}
public function getMountId(): ?int {
return $this->mount->getMountId();
}
/**
* Get the internal path (within the storage) of the root of the mount
*
* @return string
*/
public function getRootInternalPath(): string {
return $this->mount->getInternalPath($this->mount->getMountPoint());
}
public function getMountProvider(): string {
return $this->mount->getMountProvider();
}
public function getKey(): string {
if (!$this->key) {
$this->key = $this->getRootId() . '::' . $this->getMountPoint();
}
return $this->key;
}
}
@@ -0,0 +1,258 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\Config;
use OC\Hooks\Emitter;
use OC\Hooks\EmitterTrait;
use OCP\Diagnostics\IEventLogger;
use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Config\IMountProvider;
use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Config\IRootMountProvider;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Storage\IStorageFactory;
use OCP\IUser;
class MountProviderCollection implements IMountProviderCollection, Emitter {
use EmitterTrait;
/**
* @var \OCP\Files\Config\IHomeMountProvider[]
*/
private $homeProviders = [];
/**
* @var \OCP\Files\Config\IMountProvider[]
*/
private $providers = [];
/** @var \OCP\Files\Config\IRootMountProvider[] */
private $rootProviders = [];
/**
* @var \OCP\Files\Storage\IStorageFactory
*/
private $loader;
/**
* @var \OCP\Files\Config\IUserMountCache
*/
private $mountCache;
/** @var callable[] */
private $mountFilters = [];
private IEventLogger $eventLogger;
/**
* @param \OCP\Files\Storage\IStorageFactory $loader
* @param IUserMountCache $mountCache
*/
public function __construct(
IStorageFactory $loader,
IUserMountCache $mountCache,
IEventLogger $eventLogger
) {
$this->loader = $loader;
$this->mountCache = $mountCache;
$this->eventLogger = $eventLogger;
}
private function getMountsFromProvider(IMountProvider $provider, IUser $user, IStorageFactory $loader): array {
$class = str_replace('\\', '_', get_class($provider));
$uid = $user->getUID();
$this->eventLogger->start('fs:setup:provider:' . $class, "Getting mounts from $class for $uid");
$mounts = $provider->getMountsForUser($user, $loader) ?? [];
$this->eventLogger->end('fs:setup:provider:' . $class);
return $mounts;
}
/**
* @param IUser $user
* @param IMountProvider[] $providers
* @return IMountPoint[]
*/
private function getUserMountsForProviders(IUser $user, array $providers): array {
$loader = $this->loader;
$mounts = array_map(function (IMountProvider $provider) use ($user, $loader) {
return $this->getMountsFromProvider($provider, $user, $loader);
}, $providers);
$mounts = array_reduce($mounts, function (array $mounts, array $providerMounts) {
return array_merge($mounts, $providerMounts);
}, []);
return $this->filterMounts($user, $mounts);
}
public function getMountsForUser(IUser $user): array {
return $this->getUserMountsForProviders($user, $this->providers);
}
public function getUserMountsForProviderClasses(IUser $user, array $mountProviderClasses): array {
$providers = array_filter(
$this->providers,
fn (IMountProvider $mountProvider) => (in_array(get_class($mountProvider), $mountProviderClasses))
);
return $this->getUserMountsForProviders($user, $providers);
}
public function addMountForUser(IUser $user, IMountManager $mountManager, callable $providerFilter = null) {
// shared mount provider gets to go last since it needs to know existing files
// to check for name collisions
$firstMounts = [];
if ($providerFilter) {
$providers = array_filter($this->providers, $providerFilter);
} else {
$providers = $this->providers;
}
$firstProviders = array_filter($providers, function (IMountProvider $provider) {
return (get_class($provider) !== 'OCA\Files_Sharing\MountProvider');
});
$lastProviders = array_filter($providers, function (IMountProvider $provider) {
return (get_class($provider) === 'OCA\Files_Sharing\MountProvider');
});
foreach ($firstProviders as $provider) {
$mounts = $this->getMountsFromProvider($provider, $user, $this->loader);
$firstMounts = array_merge($firstMounts, $mounts);
}
$firstMounts = $this->filterMounts($user, $firstMounts);
array_walk($firstMounts, [$mountManager, 'addMount']);
$lateMounts = [];
foreach ($lastProviders as $provider) {
$mounts = $this->getMountsFromProvider($provider, $user, $this->loader);
$lateMounts = array_merge($lateMounts, $mounts);
}
$lateMounts = $this->filterMounts($user, $lateMounts);
$this->eventLogger->start("fs:setup:add-mounts", "Add mounts to the filesystem");
array_walk($lateMounts, [$mountManager, 'addMount']);
$this->eventLogger->end("fs:setup:add-mounts");
return array_merge($lateMounts, $firstMounts);
}
/**
* Get the configured home mount for this user
*
* @param \OCP\IUser $user
* @return \OCP\Files\Mount\IMountPoint
* @since 9.1.0
*/
public function getHomeMountForUser(IUser $user) {
/** @var \OCP\Files\Config\IHomeMountProvider[] $providers */
$providers = array_reverse($this->homeProviders); // call the latest registered provider first to give apps an opportunity to overwrite builtin
foreach ($providers as $homeProvider) {
if ($mount = $homeProvider->getHomeMountForUser($user, $this->loader)) {
$mount->setMountPoint('/' . $user->getUID()); //make sure the mountpoint is what we expect
return $mount;
}
}
throw new \Exception('No home storage configured for user ' . $user);
}
/**
* Add a provider for mount points
*
* @param \OCP\Files\Config\IMountProvider $provider
*/
public function registerProvider(IMountProvider $provider) {
$this->providers[] = $provider;
$this->emit('\OC\Files\Config', 'registerMountProvider', [$provider]);
}
public function registerMountFilter(callable $filter) {
$this->mountFilters[] = $filter;
}
private function filterMounts(IUser $user, array $mountPoints) {
return array_filter($mountPoints, function (IMountPoint $mountPoint) use ($user) {
foreach ($this->mountFilters as $filter) {
if ($filter($mountPoint, $user) === false) {
return false;
}
}
return true;
});
}
/**
* Add a provider for home mount points
*
* @param \OCP\Files\Config\IHomeMountProvider $provider
* @since 9.1.0
*/
public function registerHomeProvider(IHomeMountProvider $provider) {
$this->homeProviders[] = $provider;
$this->emit('\OC\Files\Config', 'registerHomeMountProvider', [$provider]);
}
/**
* Get the mount cache which can be used to search for mounts without setting up the filesystem
*
* @return IUserMountCache
*/
public function getMountCache() {
return $this->mountCache;
}
public function registerRootProvider(IRootMountProvider $provider) {
$this->rootProviders[] = $provider;
}
/**
* Get all root mountpoints
*
* @return \OCP\Files\Mount\IMountPoint[]
* @since 20.0.0
*/
public function getRootMounts(): array {
$loader = $this->loader;
$mounts = array_map(function (IRootMountProvider $provider) use ($loader) {
return $provider->getRootMounts($loader);
}, $this->rootProviders);
$mounts = array_reduce($mounts, function (array $mounts, array $providerMounts) {
return array_merge($mounts, $providerMounts);
}, []);
if (count($mounts) === 0) {
throw new \Exception("No root mounts provided by any provider");
}
return $mounts;
}
public function clearProviders() {
$this->providers = [];
$this->homeProviders = [];
$this->rootProviders = [];
}
public function getProviders(): array {
return $this->providers;
}
}
@@ -0,0 +1,483 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Dariusz Olszewski <starypatyk@users.noreply.github.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @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 OC\Files\Config;
use OCP\Cache\CappedMemoryCache;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Diagnostics\IEventLogger;
use OCP\Files\Config\ICachedMountFileInfo;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\NotFoundException;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
/**
* Cache mounts points per user in the cache so we can easily look them up
*/
class UserMountCache implements IUserMountCache {
private IDBConnection $connection;
private IUserManager $userManager;
/**
* Cached mount info.
* @var CappedMemoryCache<ICachedMountInfo[]>
**/
private CappedMemoryCache $mountsForUsers;
private LoggerInterface $logger;
/** @var CappedMemoryCache<array> */
private CappedMemoryCache $cacheInfoCache;
private IEventLogger $eventLogger;
/**
* UserMountCache constructor.
*/
public function __construct(
IDBConnection $connection,
IUserManager $userManager,
LoggerInterface $logger,
IEventLogger $eventLogger
) {
$this->connection = $connection;
$this->userManager = $userManager;
$this->logger = $logger;
$this->eventLogger = $eventLogger;
$this->cacheInfoCache = new CappedMemoryCache();
$this->mountsForUsers = new CappedMemoryCache();
}
public function registerMounts(IUser $user, array $mounts, array $mountProviderClasses = null) {
$this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
/** @var array<string, ICachedMountInfo> $newMounts */
$newMounts = [];
foreach ($mounts as $mount) {
// filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
if ($mount->getStorageRootId() !== -1) {
$mountInfo = new LazyStorageMountInfo($user, $mount);
$newMounts[$mountInfo->getKey()] = $mountInfo;
}
}
$cachedMounts = $this->getMountsForUser($user);
if (is_array($mountProviderClasses)) {
$cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
// for existing mounts that didn't have a mount provider set
// we still want the ones that map to new mounts
if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountInfo->getKey()])) {
return true;
}
return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
});
}
$addedMounts = [];
$removedMounts = [];
foreach ($newMounts as $mountKey => $newMount) {
if (!isset($cachedMounts[$mountKey])) {
$addedMounts[] = $newMount;
}
}
foreach ($cachedMounts as $mountKey => $cachedMount) {
if (!isset($newMounts[$mountKey])) {
$removedMounts[] = $cachedMount;
}
}
$changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
if ($addedMounts || $removedMounts || $changedMounts) {
$this->connection->beginTransaction();
$userUID = $user->getUID();
try {
foreach ($addedMounts as $mount) {
$this->addToCache($mount);
/** @psalm-suppress InvalidArgument */
$this->mountsForUsers[$userUID][$mount->getKey()] = $mount;
}
foreach ($removedMounts as $mount) {
$this->removeFromCache($mount);
unset($this->mountsForUsers[$userUID][$mount->getKey()]);
}
foreach ($changedMounts as $mount) {
$this->updateCachedMount($mount);
/** @psalm-suppress InvalidArgument */
$this->mountsForUsers[$userUID][$mount->getKey()] = $mount;
}
$this->connection->commit();
} catch (\Throwable $e) {
$this->connection->rollBack();
throw $e;
}
}
$this->eventLogger->end('fs:setup:user:register');
}
/**
* @param array<string, ICachedMountInfo> $newMounts
* @param array<string, ICachedMountInfo> $cachedMounts
* @return ICachedMountInfo[]
*/
private function findChangedMounts(array $newMounts, array $cachedMounts) {
$changed = [];
foreach ($cachedMounts as $key => $cachedMount) {
if (isset($newMounts[$key])) {
$newMount = $newMounts[$key];
if (
$newMount->getStorageId() !== $cachedMount->getStorageId() ||
$newMount->getMountId() !== $cachedMount->getMountId() ||
$newMount->getMountProvider() !== $cachedMount->getMountProvider()
) {
$changed[] = $newMount;
}
}
}
return $changed;
}
private function addToCache(ICachedMountInfo $mount) {
if ($mount->getStorageId() !== -1) {
$this->connection->insertIfNotExist('*PREFIX*mounts', [
'storage_id' => $mount->getStorageId(),
'root_id' => $mount->getRootId(),
'user_id' => $mount->getUser()->getUID(),
'mount_point' => $mount->getMountPoint(),
'mount_id' => $mount->getMountId(),
'mount_provider_class' => $mount->getMountProvider(),
], ['root_id', 'user_id', 'mount_point']);
} else {
// in some cases this is legitimate, like orphaned shares
$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
}
}
private function updateCachedMount(ICachedMountInfo $mount) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->update('mounts')
->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
$query->execute();
}
private function removeFromCache(ICachedMountInfo $mount) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->delete('mounts')
->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)))
->andWhere($builder->expr()->eq('mount_point', $builder->createNamedParameter($mount->getMountPoint())));
$query->execute();
}
private function dbRowToMountInfo(array $row) {
$user = $this->userManager->get($row['user_id']);
if (is_null($user)) {
return null;
}
$mount_id = $row['mount_id'];
if (!is_null($mount_id)) {
$mount_id = (int)$mount_id;
}
return new CachedMountInfo(
$user,
(int)$row['storage_id'],
(int)$row['root_id'],
$row['mount_point'],
$row['mount_provider_class'] ?? '',
$mount_id,
$row['path'] ?? '',
);
}
/**
* @param IUser $user
* @return ICachedMountInfo[]
*/
public function getMountsForUser(IUser $user) {
$userUID = $user->getUID();
if (!isset($this->mountsForUsers[$userUID])) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
->from('mounts', 'm')
->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($userUID)));
$result = $query->execute();
$rows = $result->fetchAll();
$result->closeCursor();
$this->mountsForUsers[$userUID] = [];
/** @var array<string, ICachedMountInfo> $mounts */
foreach ($rows as $row) {
$mount = $this->dbRowToMountInfo($row);
if ($mount !== null) {
$this->mountsForUsers[$userUID][$mount->getKey()] = $mount;
}
}
}
return $this->mountsForUsers[$userUID];
}
/**
* @param int $numericStorageId
* @param string|null $user limit the results to a single user
* @return CachedMountInfo[]
*/
public function getMountsForStorageId($numericStorageId, $user = null) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
->from('mounts', 'm')
->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
if ($user) {
$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
}
$result = $query->execute();
$rows = $result->fetchAll();
$result->closeCursor();
return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
}
/**
* @param int $rootFileId
* @return CachedMountInfo[]
*/
public function getMountsForRootId($rootFileId) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
->from('mounts', 'm')
->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
$result = $query->execute();
$rows = $result->fetchAll();
$result->closeCursor();
return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
}
/**
* @param $fileId
* @return array{int, string, int}
* @throws \OCP\Files\NotFoundException
*/
private function getCacheInfoFromFileId($fileId): array {
if (!isset($this->cacheInfoCache[$fileId])) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select('storage', 'path', 'mimetype')
->from('filecache')
->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
if (is_array($row)) {
$this->cacheInfoCache[$fileId] = [
(int)$row['storage'],
(string)$row['path'],
(int)$row['mimetype']
];
} else {
throw new NotFoundException('File with id "' . $fileId . '" not found');
}
}
return $this->cacheInfoCache[$fileId];
}
/**
* @param int $fileId
* @param string|null $user optionally restrict the results to a single user
* @return ICachedMountFileInfo[]
* @since 9.0.0
*/
public function getMountsForFileId($fileId, $user = null) {
try {
[$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
} catch (NotFoundException $e) {
return [];
}
$builder = $this->connection->getQueryBuilder();
$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
->from('mounts', 'm')
->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($storageId, IQueryBuilder::PARAM_INT)));
if ($user) {
$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
}
$result = $query->execute();
$rows = $result->fetchAll();
$result->closeCursor();
// filter mounts that are from the same storage but a different directory
$filteredMounts = array_filter($rows, function (array $row) use ($internalPath, $fileId) {
if ($fileId === (int)$row['root_id']) {
return true;
}
$internalMountPath = $row['path'] ?? '';
return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
});
$filteredMounts = array_filter(array_map([$this, 'dbRowToMountInfo'], $filteredMounts));
return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
return new CachedMountFileInfo(
$mount->getUser(),
$mount->getStorageId(),
$mount->getRootId(),
$mount->getMountPoint(),
$mount->getMountId(),
$mount->getMountProvider(),
$mount->getRootInternalPath(),
$internalPath
);
}, $filteredMounts);
}
/**
* Remove all cached mounts for a user
*
* @param IUser $user
*/
public function removeUserMounts(IUser $user) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->delete('mounts')
->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
$query->execute();
}
public function removeUserStorageMount($storageId, $userId) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->delete('mounts')
->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
$query->execute();
}
public function remoteStorageMounts($storageId) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->delete('mounts')
->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
$query->execute();
}
/**
* @param array $users
* @return array
*/
public function getUsedSpaceForUsers(array $users) {
$builder = $this->connection->getQueryBuilder();
$slash = $builder->createNamedParameter('/');
$mountPoint = $builder->func()->concat(
$builder->func()->concat($slash, 'user_id'),
$slash
);
$userIds = array_map(function (IUser $user) {
return $user->getUID();
}, $users);
$query = $builder->select('m.user_id', 'f.size')
->from('mounts', 'm')
->innerJoin('m', 'filecache', 'f',
$builder->expr()->andX(
$builder->expr()->eq('m.storage_id', 'f.storage'),
$builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
))
->where($builder->expr()->eq('m.mount_point', $mountPoint))
->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
$result = $query->execute();
$results = [];
while ($row = $result->fetch()) {
$results[$row['user_id']] = $row['size'];
}
$result->closeCursor();
return $results;
}
public function clear(): void {
$this->cacheInfoCache = new CappedMemoryCache();
$this->mountsForUsers = new CappedMemoryCache();
}
public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
$mounts = $this->getMountsForUser($user);
$mountPoints = array_map(function (ICachedMountInfo $mount) {
return $mount->getMountPoint();
}, $mounts);
$mounts = array_combine($mountPoints, $mounts);
$current = rtrim($path, '/');
// walk up the directory tree until we find a path that has a mountpoint set
// the loop will return if a mountpoint is found or break if none are found
while (true) {
$mountPoint = $current . '/';
if (isset($mounts[$mountPoint])) {
return $mounts[$mountPoint];
} elseif ($current === '') {
break;
}
$current = dirname($current);
if ($current === '.' || $current === '/') {
$current = '';
}
}
throw new NotFoundException("No cached mount for path " . $path);
}
public function getMountsInPath(IUser $user, string $path): array {
$path = rtrim($path, '/') . '/';
$mounts = $this->getMountsForUser($user);
return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
});
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @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 OC\Files\Config;
use OC\User\Manager;
use OCP\Files\Config\IUserMountCache;
/**
* Listen to hooks and update the mount cache as needed
*/
class UserMountCacheListener {
/**
* @var IUserMountCache
*/
private $userMountCache;
/**
* UserMountCacheListener constructor.
*
* @param IUserMountCache $userMountCache
*/
public function __construct(IUserMountCache $userMountCache) {
$this->userMountCache = $userMountCache;
}
public function listen(Manager $manager) {
$manager->listen('\OC\User', 'postDelete', [$this->userMountCache, 'removeUserMounts']);
}
}
+421
View File
@@ -0,0 +1,421 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Piotr M <mrow4a@yahoo.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author tbartenstein <tbartenstein@users.noreply.github.com>
* @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 OC\Files;
use OC\Files\Mount\HomeMountPoint;
use OCA\Files_Sharing\External\Mount;
use OCA\Files_Sharing\ISharedMountPoint;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Mount\IMountPoint;
use OCP\IUser;
class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
private array|ICacheEntry $data;
/**
* @var string
*/
private $path;
/**
* @var \OC\Files\Storage\Storage $storage
*/
private $storage;
/**
* @var string
*/
private $internalPath;
/**
* @var \OCP\Files\Mount\IMountPoint
*/
private $mount;
private ?IUser $owner;
/**
* @var string[]
*/
private array $childEtags = [];
/**
* @var IMountPoint[]
*/
private array $subMounts = [];
private bool $subMountsUsed = false;
/**
* The size of the file/folder without any sub mount
*/
private int|float $rawSize = 0;
/**
* @param string|boolean $path
* @param Storage\Storage $storage
* @param string $internalPath
* @param array|ICacheEntry $data
* @param IMountPoint $mount
* @param ?IUser $owner
*/
public function __construct($path, $storage, $internalPath, $data, $mount, $owner = null) {
$this->path = $path;
$this->storage = $storage;
$this->internalPath = $internalPath;
$this->data = $data;
$this->mount = $mount;
$this->owner = $owner;
if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] !== 0) {
$this->rawSize = $this->data['unencrypted_size'];
} else {
$this->rawSize = $this->data['size'] ?? 0;
}
}
public function offsetSet($offset, $value): void {
if (is_null($offset)) {
throw new \TypeError('Null offset not supported');
}
$this->data[$offset] = $value;
}
public function offsetExists($offset): bool {
return isset($this->data[$offset]);
}
public function offsetUnset($offset): void {
unset($this->data[$offset]);
}
/**
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
return match ($offset) {
'type' => $this->getType(),
'etag' => $this->getEtag(),
'size' => $this->getSize(),
'mtime' => $this->getMTime(),
'permissions' => $this->getPermissions(),
default => $this->data[$offset] ?? null,
};
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
public function getStorage() {
return $this->storage;
}
/**
* @return string
*/
public function getInternalPath() {
return $this->internalPath;
}
/**
* Get FileInfo ID or null in case of part file
*
* @return int|null
*/
public function getId() {
return isset($this->data['fileid']) ? (int) $this->data['fileid'] : null;
}
/**
* @return string
*/
public function getMimetype() {
return $this->data['mimetype'];
}
/**
* @return string
*/
public function getMimePart() {
return $this->data['mimepart'];
}
/**
* @return string
*/
public function getName() {
return isset($this->data['name']) ? $this->data['name'] : basename($this->getPath());
}
/**
* @return string
*/
public function getEtag() {
$this->updateEntryfromSubMounts();
if (count($this->childEtags) > 0) {
$combinedEtag = $this->data['etag'] . '::' . implode('::', $this->childEtags);
return md5($combinedEtag);
} else {
return $this->data['etag'];
}
}
/**
* @param bool $includeMounts
* @return int|float
*/
public function getSize($includeMounts = true) {
if ($includeMounts) {
$this->updateEntryfromSubMounts();
if ($this->isEncrypted() && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) {
return $this->data['unencrypted_size'];
} else {
return isset($this->data['size']) ? 0 + $this->data['size'] : 0;
}
} else {
return $this->rawSize;
}
}
/**
* @return int
*/
public function getMTime() {
$this->updateEntryfromSubMounts();
return (int) $this->data['mtime'];
}
/**
* @return bool
*/
public function isEncrypted() {
return $this->data['encrypted'] ?? false;
}
/**
* Return the current version used for the HMAC in the encryption app
*/
public function getEncryptedVersion(): int {
return isset($this->data['encryptedVersion']) ? (int) $this->data['encryptedVersion'] : 1;
}
/**
* @return int
*/
public function getPermissions() {
return (int) $this->data['permissions'];
}
/**
* @return string \OCP\Files\FileInfo::TYPE_FILE|\OCP\Files\FileInfo::TYPE_FOLDER
*/
public function getType() {
if (!isset($this->data['type'])) {
$this->data['type'] = ($this->getMimetype() === self::MIMETYPE_FOLDER) ? self::TYPE_FOLDER : self::TYPE_FILE;
}
return $this->data['type'];
}
public function getData() {
return $this->data;
}
/**
* @param int $permissions
* @return bool
*/
protected function checkPermissions($permissions) {
return ($this->getPermissions() & $permissions) === $permissions;
}
/**
* @return bool
*/
public function isReadable() {
return $this->checkPermissions(\OCP\Constants::PERMISSION_READ);
}
/**
* @return bool
*/
public function isUpdateable() {
return $this->checkPermissions(\OCP\Constants::PERMISSION_UPDATE);
}
/**
* Check whether new files or folders can be created inside this folder
*
* @return bool
*/
public function isCreatable() {
return $this->checkPermissions(\OCP\Constants::PERMISSION_CREATE);
}
/**
* @return bool
*/
public function isDeletable() {
return $this->checkPermissions(\OCP\Constants::PERMISSION_DELETE);
}
/**
* @return bool
*/
public function isShareable() {
return $this->checkPermissions(\OCP\Constants::PERMISSION_SHARE);
}
/**
* Check if a file or folder is shared
*
* @return bool
*/
public function isShared() {
return $this->mount instanceof ISharedMountPoint;
}
public function isMounted() {
$isHome = $this->mount instanceof HomeMountPoint;
return !$isHome && !$this->isShared();
}
/**
* Get the mountpoint the file belongs to
*
* @return \OCP\Files\Mount\IMountPoint
*/
public function getMountPoint() {
return $this->mount;
}
/**
* Get the owner of the file
*
* @return ?IUser
*/
public function getOwner() {
return $this->owner;
}
/**
* @param IMountPoint[] $mounts
*/
public function setSubMounts(array $mounts) {
$this->subMounts = $mounts;
}
private function updateEntryfromSubMounts(): void {
if ($this->subMountsUsed) {
return;
}
$this->subMountsUsed = true;
foreach ($this->subMounts as $mount) {
$subStorage = $mount->getStorage();
if ($subStorage) {
$subCache = $subStorage->getCache('');
$rootEntry = $subCache->get('');
$this->addSubEntry($rootEntry, $mount->getMountPoint());
}
}
}
/**
* Add a cache entry which is the child of this folder
*
* Sets the size, etag and size to for cross-storage childs
*
* @param array|ICacheEntry $data cache entry for the child
* @param string $entryPath full path of the child entry
*/
public function addSubEntry($data, $entryPath) {
if (!$data) {
return;
}
$hasUnencryptedSize = isset($data['unencrypted_size']) && $data['unencrypted_size'] > 0;
if ($hasUnencryptedSize) {
$subSize = $data['unencrypted_size'];
} else {
$subSize = $data['size'] ?: 0;
}
$this->data['size'] += $subSize;
if ($hasUnencryptedSize) {
$this->data['unencrypted_size'] += $subSize;
}
if (isset($data['mtime'])) {
$this->data['mtime'] = max($this->data['mtime'], $data['mtime']);
}
if (isset($data['etag'])) {
// prefix the etag with the relative path of the subentry to propagate etag on mount moves
$relativeEntryPath = substr($entryPath, strlen($this->getPath()));
// attach the permissions to propagate etag on permission changes of submounts
$permissions = isset($data['permissions']) ? $data['permissions'] : 0;
$this->childEtags[] = $relativeEntryPath . '/' . $data['etag'] . $permissions;
}
}
/**
* @inheritdoc
*/
public function getChecksum() {
return $this->data['checksum'];
}
public function getExtension(): string {
return pathinfo($this->getName(), PATHINFO_EXTENSION);
}
public function getCreationTime(): int {
return (int) $this->data['creation_time'];
}
public function getUploadTime(): int {
return (int) $this->data['upload_time'];
}
public function getParentId(): int {
return $this->data['parent'] ?? -1;
}
/**
* @inheritDoc
* @return array<string, int|string|bool|float|string[]|int[]>
*/
public function getMetadata(): array {
return $this->data['metadata'] ?? [];
}
}
@@ -0,0 +1,766 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christopher Schäpers <kondou@ts.unde.re>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Florin Peter <github@florin-peter.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author korelstar <korelstar@users.noreply.github.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @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 Sam Tuke <mail@samtuke.com>
* @author Stephan Peijnik <speijnik@anexia-it.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 OC\Files;
use OC\Files\Mount\MountPoint;
use OC\User\NoUserException;
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\Node\FilesystemTornDownEvent;
use OCP\Files\Mount\IMountManager;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\IStorageFactory;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
class Filesystem {
private static ?Mount\Manager $mounts = null;
public static bool $loaded = false;
private static ?View $defaultInstance = null;
private static ?CappedMemoryCache $normalizedPathCache = null;
/** @var string[]|null */
private static ?array $blacklist = null;
/**
* classname which used for hooks handling
* used as signalclass in OC_Hooks::emit()
*/
public const CLASSNAME = 'OC_Filesystem';
/**
* signalname emitted before file renaming
*
* @param string $oldpath
* @param string $newpath
*/
public const signal_rename = 'rename';
/**
* signal emitted after file renaming
*
* @param string $oldpath
* @param string $newpath
*/
public const signal_post_rename = 'post_rename';
/**
* signal emitted before file/dir creation
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_create = 'create';
/**
* signal emitted after file/dir creation
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_post_create = 'post_create';
/**
* signal emits before file/dir copy
*
* @param string $oldpath
* @param string $newpath
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_copy = 'copy';
/**
* signal emits after file/dir copy
*
* @param string $oldpath
* @param string $newpath
*/
public const signal_post_copy = 'post_copy';
/**
* signal emits before file/dir save
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_write = 'write';
/**
* signal emits after file/dir save
*
* @param string $path
*/
public const signal_post_write = 'post_write';
/**
* signal emitted before file/dir update
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_update = 'update';
/**
* signal emitted after file/dir update
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_post_update = 'post_update';
/**
* signal emits when reading file/dir
*
* @param string $path
*/
public const signal_read = 'read';
/**
* signal emits when removing file/dir
*
* @param string $path
*/
public const signal_delete = 'delete';
/**
* parameters definitions for signals
*/
public const signal_param_path = 'path';
public const signal_param_oldpath = 'oldpath';
public const signal_param_newpath = 'newpath';
/**
* run - changing this flag to false in hook handler will cancel event
*/
public const signal_param_run = 'run';
public const signal_create_mount = 'create_mount';
public const signal_delete_mount = 'delete_mount';
public const signal_param_mount_type = 'mounttype';
public const signal_param_users = 'users';
private static ?\OC\Files\Storage\StorageFactory $loader = null;
private static bool $logWarningWhenAddingStorageWrapper = true;
/**
* @param bool $shouldLog
* @return bool previous value
* @internal
*/
public static function logWarningWhenAddingStorageWrapper(bool $shouldLog): bool {
$previousValue = self::$logWarningWhenAddingStorageWrapper;
self::$logWarningWhenAddingStorageWrapper = $shouldLog;
return $previousValue;
}
/**
* @param string $wrapperName
* @param callable $wrapper
* @param int $priority
*/
public static function addStorageWrapper($wrapperName, $wrapper, $priority = 50) {
if (self::$logWarningWhenAddingStorageWrapper) {
\OC::$server->getLogger()->warning("Storage wrapper '{wrapper}' was not registered via the 'OC_Filesystem - preSetup' hook which could cause potential problems.", [
'wrapper' => $wrapperName,
'app' => 'filesystem',
]);
}
$mounts = self::getMountManager()->getAll();
if (!self::getLoader()->addStorageWrapper($wrapperName, $wrapper, $priority, $mounts)) {
// do not re-wrap if storage with this name already existed
return;
}
}
/**
* Returns the storage factory
*
* @return IStorageFactory
*/
public static function getLoader() {
if (!self::$loader) {
self::$loader = \OC::$server->get(IStorageFactory::class);
}
return self::$loader;
}
/**
* Returns the mount manager
*/
public static function getMountManager(): Mount\Manager {
self::initMountManager();
assert(self::$mounts !== null);
return self::$mounts;
}
/**
* get the mountpoint of the storage object for a path
* ( note: because a storage is not always mounted inside the fakeroot, the
* returned mountpoint is relative to the absolute root of the filesystem
* and doesn't take the chroot into account )
*
* @param string $path
* @return string
*/
public static function getMountPoint($path) {
if (!self::$mounts) {
\OC_Util::setupFS();
}
$mount = self::$mounts->find($path);
return $mount->getMountPoint();
}
/**
* get a list of all mount points in a directory
*
* @param string $path
* @return string[]
*/
public static function getMountPoints($path) {
if (!self::$mounts) {
\OC_Util::setupFS();
}
$result = [];
$mounts = self::$mounts->findIn($path);
foreach ($mounts as $mount) {
$result[] = $mount->getMountPoint();
}
return $result;
}
/**
* get the storage mounted at $mountPoint
*
* @param string $mountPoint
* @return \OC\Files\Storage\Storage|null
*/
public static function getStorage($mountPoint) {
$mount = self::getMountManager()->find($mountPoint);
return $mount->getStorage();
}
/**
* @param string $id
* @return Mount\MountPoint[]
*/
public static function getMountByStorageId($id) {
return self::getMountManager()->findByStorageId($id);
}
/**
* @param int $id
* @return Mount\MountPoint[]
*/
public static function getMountByNumericId($id) {
return self::getMountManager()->findByNumericId($id);
}
/**
* resolve a path to a storage and internal path
*
* @param string $path
* @return array{?\OCP\Files\Storage\IStorage, string} an array consisting of the storage and the internal path
*/
public static function resolvePath($path): array {
$mount = self::getMountManager()->find($path);
return [$mount->getStorage(), rtrim($mount->getInternalPath($path), '/')];
}
public static function init(string|IUser|null $user, string $root): bool {
if (self::$defaultInstance) {
return false;
}
self::initInternal($root);
//load custom mount config
self::initMountPoints($user);
return true;
}
public static function initInternal(string $root): bool {
if (self::$defaultInstance) {
return false;
}
self::getLoader();
self::$defaultInstance = new View($root);
/** @var IEventDispatcher $eventDispatcher */
$eventDispatcher = \OC::$server->get(IEventDispatcher::class);
$eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
self::$defaultInstance = null;
self::$loaded = false;
});
self::initMountManager();
self::$loaded = true;
return true;
}
public static function initMountManager(): void {
if (!self::$mounts) {
self::$mounts = \OC::$server->get(IMountManager::class);
}
}
/**
* Initialize system and personal mount points for a user
*
* @throws \OC\User\NoUserException if the user is not available
*/
public static function initMountPoints(string|IUser|null $user = ''): void {
/** @var IUserManager $userManager */
$userManager = \OC::$server->get(IUserManager::class);
$userObject = ($user instanceof IUser) ? $user : $userManager->get($user);
if ($userObject) {
/** @var SetupManager $setupManager */
$setupManager = \OC::$server->get(SetupManager::class);
$setupManager->setupForUser($userObject);
} else {
throw new NoUserException();
}
}
/**
* Get the default filesystem view
*/
public static function getView(): ?View {
if (!self::$defaultInstance) {
/** @var IUserSession $session */
$session = \OC::$server->get(IUserSession::class);
$user = $session->getUser();
if ($user) {
$userDir = '/' . $user->getUID() . '/files';
self::initInternal($userDir);
}
}
return self::$defaultInstance;
}
/**
* tear down the filesystem, removing all storage providers
*/
public static function tearDown() {
\OC_Util::tearDownFS();
}
/**
* get the relative path of the root data directory for the current user
*
* @return ?string
*
* Returns path like /admin/files
*/
public static function getRoot() {
if (!self::$defaultInstance) {
return null;
}
return self::$defaultInstance->getRoot();
}
/**
* mount an \OC\Files\Storage\Storage in our virtual filesystem
*
* @param \OC\Files\Storage\Storage|string $class
* @param array $arguments
* @param string $mountpoint
*/
public static function mount($class, $arguments, $mountpoint) {
if (!self::$mounts) {
\OC_Util::setupFS();
}
$mount = new Mount\MountPoint($class, $mountpoint, $arguments, self::getLoader());
self::$mounts->addMount($mount);
}
/**
* return the path to a local version of the file
* we need this because we can't know if a file is stored local or not from
* outside the filestorage and for some purposes a local file is needed
*/
public static function getLocalFile(string $path): string|false {
return self::$defaultInstance->getLocalFile($path);
}
/**
* return path to file which reflects one visible in browser
*
* @param string $path
* @return string
*/
public static function getLocalPath($path) {
$datadir = \OC_User::getHome(\OC_User::getUser()) . '/files';
$newpath = $path;
if (strncmp($newpath, $datadir, strlen($datadir)) == 0) {
$newpath = substr($path, strlen($datadir));
}
return $newpath;
}
/**
* check if the requested path is valid
*
* @param string $path
* @return bool
*/
public static function isValidPath($path) {
$path = self::normalizePath($path);
if (!$path || $path[0] !== '/') {
$path = '/' . $path;
}
if (str_contains($path, '/../') || strrchr($path, '/') === '/..') {
return false;
}
return true;
}
/**
* @param string $filename
* @return bool
*/
public static function isFileBlacklisted($filename) {
$filename = self::normalizePath($filename);
if (self::$blacklist === null) {
self::$blacklist = \OC::$server->getConfig()->getSystemValue('blacklisted_files', ['.htaccess']);
}
$filename = strtolower(basename($filename));
return in_array($filename, self::$blacklist);
}
/**
* check if the directory should be ignored when scanning
* NOTE: the special directories . and .. would cause never ending recursion
*
* @param string $dir
* @return boolean
*/
public static function isIgnoredDir($dir) {
if ($dir === '.' || $dir === '..') {
return true;
}
return false;
}
/**
* following functions are equivalent to their php builtin equivalents for arguments/return values.
*/
public static function mkdir($path) {
return self::$defaultInstance->mkdir($path);
}
public static function rmdir($path) {
return self::$defaultInstance->rmdir($path);
}
public static function is_dir($path) {
return self::$defaultInstance->is_dir($path);
}
public static function is_file($path) {
return self::$defaultInstance->is_file($path);
}
public static function stat($path) {
return self::$defaultInstance->stat($path);
}
public static function filetype($path) {
return self::$defaultInstance->filetype($path);
}
public static function filesize($path) {
return self::$defaultInstance->filesize($path);
}
public static function readfile($path) {
return self::$defaultInstance->readfile($path);
}
public static function isCreatable($path) {
return self::$defaultInstance->isCreatable($path);
}
public static function isReadable($path) {
return self::$defaultInstance->isReadable($path);
}
public static function isUpdatable($path) {
return self::$defaultInstance->isUpdatable($path);
}
public static function isDeletable($path) {
return self::$defaultInstance->isDeletable($path);
}
public static function isSharable($path) {
return self::$defaultInstance->isSharable($path);
}
public static function file_exists($path) {
return self::$defaultInstance->file_exists($path);
}
public static function filemtime($path) {
return self::$defaultInstance->filemtime($path);
}
public static function touch($path, $mtime = null) {
return self::$defaultInstance->touch($path, $mtime);
}
/**
* @return string|false
*/
public static function file_get_contents($path) {
return self::$defaultInstance->file_get_contents($path);
}
public static function file_put_contents($path, $data) {
return self::$defaultInstance->file_put_contents($path, $data);
}
public static function unlink($path) {
return self::$defaultInstance->unlink($path);
}
public static function rename($source, $target) {
return self::$defaultInstance->rename($source, $target);
}
public static function copy($source, $target) {
return self::$defaultInstance->copy($source, $target);
}
public static function fopen($path, $mode) {
return self::$defaultInstance->fopen($path, $mode);
}
/**
* @param string $path
* @throws \OCP\Files\InvalidPathException
*/
public static function toTmpFile($path): string|false {
return self::$defaultInstance->toTmpFile($path);
}
public static function fromTmpFile($tmpFile, $path) {
return self::$defaultInstance->fromTmpFile($tmpFile, $path);
}
public static function getMimeType($path) {
return self::$defaultInstance->getMimeType($path);
}
public static function hash($type, $path, $raw = false) {
return self::$defaultInstance->hash($type, $path, $raw);
}
public static function free_space($path = '/') {
return self::$defaultInstance->free_space($path);
}
public static function search($query) {
return self::$defaultInstance->search($query);
}
/**
* @param string $query
*/
public static function searchByMime($query) {
return self::$defaultInstance->searchByMime($query);
}
/**
* @param string|int $tag name or tag id
* @param string $userId owner of the tags
* @return FileInfo[] array or file info
*/
public static function searchByTag($tag, $userId) {
return self::$defaultInstance->searchByTag($tag, $userId);
}
/**
* check if a file or folder has been updated since $time
*
* @param string $path
* @param int $time
* @return bool
*/
public static function hasUpdated($path, $time) {
return self::$defaultInstance->hasUpdated($path, $time);
}
/**
* Fix common problems with a file path
*
* @param string $path
* @param bool $stripTrailingSlash whether to strip the trailing slash
* @param bool $isAbsolutePath whether the given path is absolute
* @param bool $keepUnicode true to disable unicode normalization
* @psalm-taint-escape file
* @return string
*/
public static function normalizePath($path, $stripTrailingSlash = true, $isAbsolutePath = false, $keepUnicode = false) {
/**
* FIXME: This is a workaround for existing classes and files which call
* this function with another type than a valid string. This
* conversion should get removed as soon as all existing
* function calls have been fixed.
*/
$path = (string)$path;
if ($path === '') {
return '/';
}
if (is_null(self::$normalizedPathCache)) {
self::$normalizedPathCache = new CappedMemoryCache(2048);
}
$cacheKey = json_encode([$path, $stripTrailingSlash, $isAbsolutePath, $keepUnicode]);
if ($cacheKey && isset(self::$normalizedPathCache[$cacheKey])) {
return self::$normalizedPathCache[$cacheKey];
}
//normalize unicode if possible
if (!$keepUnicode) {
$path = \OC_Util::normalizeUnicode($path);
}
//add leading slash, if it is already there we strip it anyway
$path = '/' . $path;
$patterns = [
'#\\\\#s', // no windows style '\\' slashes
'#/\.(/\.)*/#s', // remove '/./'
'#\//+#s', // remove sequence of slashes
'#/\.$#s', // remove trailing '/.'
];
do {
$count = 0;
$path = preg_replace($patterns, '/', $path, -1, $count);
} while ($count > 0);
//remove trailing slash
if ($stripTrailingSlash && strlen($path) > 1) {
$path = rtrim($path, '/');
}
self::$normalizedPathCache[$cacheKey] = $path;
return $path;
}
/**
* get the filesystem info
*
* @param string $path
* @param bool|string $includeMountPoints whether to add mountpoint sizes,
* defaults to true
* @return \OC\Files\FileInfo|false False if file does not exist
*/
public static function getFileInfo($path, $includeMountPoints = true) {
return self::getView()->getFileInfo($path, $includeMountPoints);
}
/**
* change file metadata
*
* @param string $path
* @param array $data
* @return int
*
* returns the fileid of the updated file
*/
public static function putFileInfo($path, $data) {
return self::$defaultInstance->putFileInfo($path, $data);
}
/**
* get the content of a directory
*
* @param string $directory path under datadirectory
* @param string $mimetype_filter limit returned content to this mimetype or mimepart
* @return \OC\Files\FileInfo[]
*/
public static function getDirectoryContent($directory, $mimetype_filter = '') {
return self::$defaultInstance->getDirectoryContent($directory, $mimetype_filter);
}
/**
* Get the path of a file by id
*
* Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file
*
* @param int $id
* @throws NotFoundException
* @return string
*/
public static function getPath($id) {
return self::$defaultInstance->getPath($id);
}
/**
* Get the owner for a file or folder
*
* @param string $path
* @return string
*/
public static function getOwner($path) {
return self::$defaultInstance->getOwner($path);
}
/**
* get the ETag for a file or folder
*/
public static function getETag(string $path): string|false {
return self::$defaultInstance->getETag($path);
}
}
@@ -0,0 +1,72 @@
<?php
namespace OC\Files\Lock;
use OCP\Files\Lock\ILock;
use OCP\Files\Lock\ILockManager;
use OCP\Files\Lock\ILockProvider;
use OCP\Files\Lock\LockContext;
use OCP\PreConditionNotMetException;
class LockManager implements ILockManager {
private ?ILockProvider $lockProvider = null;
private ?LockContext $lockInScope = null;
public function registerLockProvider(ILockProvider $lockProvider): void {
if ($this->lockProvider) {
throw new PreConditionNotMetException('There is already a registered lock provider');
}
$this->lockProvider = $lockProvider;
}
public function isLockProviderAvailable(): bool {
return $this->lockProvider !== null;
}
public function runInScope(LockContext $lock, callable $callback): void {
if (!$this->lockProvider) {
$callback();
return;
}
if ($this->lockInScope) {
throw new PreConditionNotMetException('Could not obtain lock scope as already in use by ' . $this->lockInScope);
}
try {
$this->lockInScope = $lock;
$callback();
} finally {
$this->lockInScope = null;
}
}
public function getLockInScope(): ?LockContext {
return $this->lockInScope;
}
public function getLocks(int $fileId): array {
if (!$this->lockProvider) {
throw new PreConditionNotMetException('No lock provider available');
}
return $this->lockProvider->getLocks($fileId);
}
public function lock(LockContext $lockInfo): ILock {
if (!$this->lockProvider) {
throw new PreConditionNotMetException('No lock provider available');
}
return $this->lockProvider->lock($lockInfo);
}
public function unlock(LockContext $lockInfo): void {
if (!$this->lockProvider) {
throw new PreConditionNotMetException('No lock provider available');
}
$this->lockProvider->unlock($lockInfo);
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @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 OC\Files\Mount;
use OCP\Files\Config\IMountProvider;
use OCP\Files\Storage\IStorageFactory;
use OCP\IConfig;
use OCP\IUser;
/**
* Mount provider for custom cache storages
*/
class CacheMountProvider implements IMountProvider {
/**
* @var IConfig
*/
private $config;
/**
* ObjectStoreHomeMountProvider constructor.
*
* @param IConfig $config
*/
public function __construct(IConfig $config) {
$this->config = $config;
}
/**
* Get the cache mount for a user
*
* @param IUser $user
* @param IStorageFactory $loader
* @return \OCP\Files\Mount\IMountPoint[]
*/
public function getMountsForUser(IUser $user, IStorageFactory $loader) {
$cacheBaseDir = $this->config->getSystemValueString('cache_path', '');
if ($cacheBaseDir !== '') {
$cacheDir = rtrim($cacheBaseDir, '/') . '/' . $user->getUID();
if (!file_exists($cacheDir)) {
mkdir($cacheDir, 0770, true);
mkdir($cacheDir . '/uploads', 0770, true);
}
return [
new MountPoint('\OC\Files\Storage\Local', '/' . $user->getUID() . '/cache', ['datadir' => $cacheDir], $loader, null, null, self::class),
new MountPoint('\OC\Files\Storage\Local', '/' . $user->getUID() . '/uploads', ['datadir' => $cacheDir . '/uploads'], $loader, null, null, self::class)
];
} else {
return [];
}
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 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 OC\Files\Mount;
use OCP\Files\Storage\IStorageFactory;
use OCP\IUser;
class HomeMountPoint extends MountPoint {
private IUser $user;
public function __construct(
IUser $user,
$storage,
string $mountpoint,
array $arguments = null,
IStorageFactory $loader = null,
array $mountOptions = null,
int $mountId = null,
string $mountProvider = null
) {
parent::__construct($storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId, $mountProvider);
$this->user = $user;
}
public function getUser(): IUser {
return $this->user;
}
}
@@ -0,0 +1,43 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @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 OC\Files\Mount;
use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Storage\IStorageFactory;
use OCP\IUser;
/**
* Mount provider for regular posix home folders
*/
class LocalHomeMountProvider implements IHomeMountProvider {
/**
* Get the cache mount for a user
*
* @param IUser $user
* @param IStorageFactory $loader
* @return \OCP\Files\Mount\IMountPoint|null
*/
public function getHomeMountForUser(IUser $user, IStorageFactory $loader) {
$arguments = ['user' => $user];
return new HomeMountPoint($user, '\OC\Files\Storage\Home', '/' . $user->getUID(), $arguments, $loader, null, null, self::class);
}
}
@@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @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 Jonas <jonas@freesources.org>
*
* @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 OC\Files\Mount;
use OC\Files\Filesystem;
use OC\Files\SetupManager;
use OC\Files\SetupManagerFactory;
use OCP\Cache\CappedMemoryCache;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
class Manager implements IMountManager {
/** @var MountPoint[] */
private array $mounts = [];
/** @var CappedMemoryCache<IMountPoint> */
private CappedMemoryCache $pathCache;
/** @var CappedMemoryCache<IMountPoint[]> */
private CappedMemoryCache $inPathCache;
private SetupManager $setupManager;
public function __construct(SetupManagerFactory $setupManagerFactory) {
$this->pathCache = new CappedMemoryCache();
$this->inPathCache = new CappedMemoryCache();
$this->setupManager = $setupManagerFactory->create($this);
}
/**
* @param IMountPoint $mount
*/
public function addMount(IMountPoint $mount) {
$this->mounts[$mount->getMountPoint()] = $mount;
$this->pathCache->clear();
$this->inPathCache->clear();
}
/**
* @param string $mountPoint
*/
public function removeMount(string $mountPoint) {
$mountPoint = Filesystem::normalizePath($mountPoint);
if (\strlen($mountPoint) > 1) {
$mountPoint .= '/';
}
unset($this->mounts[$mountPoint]);
$this->pathCache->clear();
$this->inPathCache->clear();
}
/**
* @param string $mountPoint
* @param string $target
*/
public function moveMount(string $mountPoint, string $target) {
$this->mounts[$target] = $this->mounts[$mountPoint];
unset($this->mounts[$mountPoint]);
$this->pathCache->clear();
$this->inPathCache->clear();
}
/**
* Find the mount for $path
*
* @param string $path
* @return IMountPoint
*/
public function find(string $path): IMountPoint {
$this->setupManager->setupForPath($path);
$path = Filesystem::normalizePath($path);
if (isset($this->pathCache[$path])) {
return $this->pathCache[$path];
}
if (count($this->mounts) === 0) {
$this->setupManager->setupRoot();
if (count($this->mounts) === 0) {
throw new \Exception("No mounts even after explicitly setting up the root mounts");
}
}
$current = $path;
while (true) {
$mountPoint = $current . '/';
if (isset($this->mounts[$mountPoint])) {
$this->pathCache[$path] = $this->mounts[$mountPoint];
return $this->mounts[$mountPoint];
} elseif ($current === '') {
break;
}
$current = dirname($current);
if ($current === '.' || $current === '/') {
$current = '';
}
}
throw new NotFoundException("No mount for path " . $path . " existing mounts (" . count($this->mounts) ."): " . implode(",", array_keys($this->mounts)));
}
/**
* Find all mounts in $path
*
* @param string $path
* @return IMountPoint[]
*/
public function findIn(string $path): array {
$this->setupManager->setupForPath($path, true);
$path = $this->formatPath($path);
if (isset($this->inPathCache[$path])) {
return $this->inPathCache[$path];
}
$result = [];
$pathLength = \strlen($path);
$mountPoints = array_keys($this->mounts);
foreach ($mountPoints as $mountPoint) {
if (substr($mountPoint, 0, $pathLength) === $path && \strlen($mountPoint) > $pathLength) {
$result[] = $this->mounts[$mountPoint];
}
}
$this->inPathCache[$path] = $result;
return $result;
}
public function clear() {
$this->mounts = [];
$this->pathCache->clear();
$this->inPathCache->clear();
}
/**
* Find mounts by storage id
*
* @param string $id
* @return IMountPoint[]
*/
public function findByStorageId(string $id): array {
if (\strlen($id) > 64) {
$id = md5($id);
}
$result = [];
foreach ($this->mounts as $mount) {
if ($mount->getStorageId() === $id) {
$result[] = $mount;
}
}
return $result;
}
/**
* @return IMountPoint[]
*/
public function getAll(): array {
return $this->mounts;
}
/**
* Find mounts by numeric storage id
*
* @param int $id
* @return IMountPoint[]
*/
public function findByNumericId(int $id): array {
$result = [];
foreach ($this->mounts as $mount) {
if ($mount->getNumericStorageId() === $id) {
$result[] = $mount;
}
}
return $result;
}
/**
* @param string $path
* @return string
*/
private function formatPath(string $path): string {
$path = Filesystem::normalizePath($path);
if (\strlen($path) > 1) {
$path .= '/';
}
return $path;
}
public function getSetupManager(): SetupManager {
return $this->setupManager;
}
/**
* Return all mounts in a path from a specific mount provider
*
* @param string $path
* @param string[] $mountProviders
* @return MountPoint[]
*/
public function getMountsByMountProvider(string $path, array $mountProviders) {
$this->getSetupManager()->setupForProvider($path, $mountProviders);
if (in_array('', $mountProviders)) {
return $this->mounts;
} else {
return array_filter($this->mounts, function ($mount) use ($mountProviders) {
return in_array($mount->getMountProvider(), $mountProviders);
});
}
}
/**
* Return the mount matching a cached mount info (or mount file info)
*
* @param ICachedMountInfo $info
*
* @return IMountPoint|null
*/
public function getMountFromMountInfo(ICachedMountInfo $info): ?IMountPoint {
$this->setupManager->setupForPath($info->getMountPoint());
foreach ($this->mounts as $mount) {
if ($mount->getMountPoint() === $info->getMountPoint()) {
return $mount;
}
}
return null;
}
}
@@ -0,0 +1,316 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.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 OC\Files\Mount;
use OC\Files\Filesystem;
use OC\Files\Storage\Storage;
use OC\Files\Storage\StorageFactory;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Storage\IStorageFactory;
use Psr\Log\LoggerInterface;
class MountPoint implements IMountPoint {
/**
* @var \OC\Files\Storage\Storage|null $storage
*/
protected $storage = null;
protected $class;
protected $storageId;
protected $numericStorageId = null;
protected $rootId = null;
/**
* Configuration options for the storage backend
*
* @var array
*/
protected $arguments = [];
protected $mountPoint;
/**
* Mount specific options
*
* @var array
*/
protected $mountOptions = [];
/**
* @var \OC\Files\Storage\StorageFactory $loader
*/
private $loader;
/**
* Specified whether the storage is invalid after failing to
* instantiate it.
*
* @var bool
*/
private $invalidStorage = false;
/** @var int|null */
protected $mountId;
/** @var string */
protected $mountProvider;
/**
* @param string|\OC\Files\Storage\Storage $storage
* @param string $mountpoint
* @param array $arguments (optional) configuration for the storage backend
* @param \OCP\Files\Storage\IStorageFactory $loader
* @param array $mountOptions mount specific options
* @param int|null $mountId
* @param string|null $mountProvider
* @throws \Exception
*/
public function __construct(
$storage,
string $mountpoint,
array $arguments = null,
IStorageFactory $loader = null,
array $mountOptions = null,
int $mountId = null,
string $mountProvider = null
) {
if (is_null($arguments)) {
$arguments = [];
}
if (is_null($loader)) {
$this->loader = new StorageFactory();
} else {
$this->loader = $loader;
}
if (!is_null($mountOptions)) {
$this->mountOptions = $mountOptions;
}
$mountpoint = $this->formatPath($mountpoint);
$this->mountPoint = $mountpoint;
$this->mountId = $mountId;
if ($storage instanceof Storage) {
$this->class = get_class($storage);
$this->storage = $this->loader->wrap($this, $storage);
} else {
// Update old classes to new namespace
if (str_contains($storage, 'OC_Filestorage_')) {
$storage = '\OC\Files\Storage\\' . substr($storage, 15);
}
$this->class = $storage;
$this->arguments = $arguments;
}
if ($mountProvider) {
if (strlen($mountProvider) > 128) {
throw new \Exception("Mount provider $mountProvider name exceeds the limit of 128 characters");
}
}
$this->mountProvider = $mountProvider ?? '';
}
/**
* get complete path to the mount point, relative to data/
*
* @return string
*/
public function getMountPoint() {
return $this->mountPoint;
}
/**
* Sets the mount point path, relative to data/
*
* @param string $mountPoint new mount point
*/
public function setMountPoint($mountPoint) {
$this->mountPoint = $this->formatPath($mountPoint);
}
/**
* create the storage that is mounted
*/
private function createStorage() {
if ($this->invalidStorage) {
return;
}
if (class_exists($this->class)) {
try {
$class = $this->class;
// prevent recursion by setting the storage before applying wrappers
$this->storage = new $class($this->arguments);
$this->storage = $this->loader->wrap($this, $this->storage);
} catch (\Exception $exception) {
$this->storage = null;
$this->invalidStorage = true;
if ($this->mountPoint === '/') {
// the root storage could not be initialized, show the user!
throw new \Exception('The root storage could not be initialized. Please contact your local administrator.', $exception->getCode(), $exception);
} else {
\OC::$server->get(LoggerInterface::class)->error($exception->getMessage(), ['exception' => $exception]);
}
return;
}
} else {
\OC::$server->get(LoggerInterface::class)->error('Storage backend ' . $this->class . ' not found', ['app' => 'core']);
$this->invalidStorage = true;
return;
}
}
/**
* @return \OC\Files\Storage\Storage|null
*/
public function getStorage() {
if (is_null($this->storage)) {
$this->createStorage();
}
return $this->storage;
}
/**
* @return string|null
*/
public function getStorageId() {
if (!$this->storageId) {
$storage = $this->getStorage();
if (is_null($storage)) {
return null;
}
$this->storageId = $storage->getId();
if (strlen($this->storageId) > 64) {
$this->storageId = md5($this->storageId);
}
}
return $this->storageId;
}
/**
* @return int
*/
public function getNumericStorageId() {
if (is_null($this->numericStorageId)) {
$storage = $this->getStorage();
if (is_null($storage)) {
return -1;
}
$this->numericStorageId = $storage->getStorageCache()->getNumericId();
}
return $this->numericStorageId;
}
/**
* @param string $path
* @return string
*/
public function getInternalPath($path) {
$path = Filesystem::normalizePath($path, true, false, true);
if ($this->mountPoint === $path or $this->mountPoint . '/' === $path) {
$internalPath = '';
} else {
$internalPath = substr($path, strlen($this->mountPoint));
}
// substr returns false instead of an empty string, we always want a string
return (string)$internalPath;
}
/**
* @param string $path
* @return string
*/
private function formatPath($path) {
$path = Filesystem::normalizePath($path);
if (strlen($path) > 1) {
$path .= '/';
}
return $path;
}
/**
* @param callable $wrapper
*/
public function wrapStorage($wrapper) {
$storage = $this->getStorage();
// storage can be null if it couldn't be initialized
if ($storage != null) {
$this->storage = $wrapper($this->mountPoint, $storage, $this);
}
}
/**
* Get a mount option
*
* @param string $name Name of the mount option to get
* @param mixed $default Default value for the mount option
* @return mixed
*/
public function getOption($name, $default) {
return $this->mountOptions[$name] ?? $default;
}
/**
* Get all options for the mount
*
* @return array
*/
public function getOptions() {
return $this->mountOptions;
}
/**
* Get the file id of the root of the storage
*
* @return int
*/
public function getStorageRootId() {
if (is_null($this->rootId) || $this->rootId === -1) {
$storage = $this->getStorage();
// if we can't create the storage return -1 as root id, this is then handled the same as if the root isn't scanned yet
if ($storage === null) {
$this->rootId = -1;
} else {
$this->rootId = (int)$storage->getCache()->getId('');
}
}
return $this->rootId;
}
public function getMountId() {
return $this->mountId;
}
public function getMountType() {
return '';
}
public function getMountProvider(): string {
return $this->mountProvider;
}
}
@@ -0,0 +1,45 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @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 OC\Files\Mount;
use OCP\Files\Mount\IMovableMount;
/**
* Defines the mount point to be (re)moved by the user
*/
interface MoveableMount extends IMovableMount {
/**
* Move the mount point to $target
*
* @param string $target the target mount point
* @return bool
*/
public function moveMount($target);
/**
* Remove the mount points
*
* @return bool
*/
public function removeMount();
}
@@ -0,0 +1,140 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vlastimil Pecinka <pecinka@email.cz>
*
* @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 OC\Files\Mount;
use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Storage\IStorageFactory;
use OCP\IConfig;
use OCP\IUser;
use Psr\Log\LoggerInterface;
/**
* Mount provider for object store home storages
*/
class ObjectHomeMountProvider implements IHomeMountProvider {
/**
* @var IConfig
*/
private $config;
/**
* ObjectStoreHomeMountProvider constructor.
*
* @param IConfig $config
*/
public function __construct(IConfig $config) {
$this->config = $config;
}
/**
* Get the cache mount for a user
*
* @param IUser $user
* @param IStorageFactory $loader
* @return \OCP\Files\Mount\IMountPoint
*/
public function getHomeMountForUser(IUser $user, IStorageFactory $loader) {
$config = $this->getMultiBucketObjectStoreConfig($user);
if ($config === null) {
$config = $this->getSingleBucketObjectStoreConfig($user);
}
if ($config === null) {
return null;
}
return new HomeMountPoint($user, '\OC\Files\ObjectStore\HomeObjectStoreStorage', '/' . $user->getUID(), $config['arguments'], $loader, null, null, self::class);
}
/**
* @param IUser $user
* @return array|null
*/
private function getSingleBucketObjectStoreConfig(IUser $user) {
$config = $this->config->getSystemValue('objectstore');
if (!is_array($config)) {
return null;
}
// sanity checks
if (empty($config['class'])) {
\OC::$server->get(LoggerInterface::class)->error('No class given for objectstore', ['app' => 'files']);
}
if (!isset($config['arguments'])) {
$config['arguments'] = [];
}
// instantiate object store implementation
$config['arguments']['objectstore'] = new $config['class']($config['arguments']);
$config['arguments']['user'] = $user;
return $config;
}
/**
* @param IUser $user
* @return array|null
*/
private function getMultiBucketObjectStoreConfig(IUser $user) {
$config = $this->config->getSystemValue('objectstore_multibucket');
if (!is_array($config)) {
return null;
}
// sanity checks
if (empty($config['class'])) {
\OC::$server->get(LoggerInterface::class)->error('No class given for objectstore', ['app' => 'files']);
}
if (!isset($config['arguments'])) {
$config['arguments'] = [];
}
$bucket = $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'bucket', null);
if ($bucket === null) {
/*
* Use any provided bucket argument as prefix
* and add the mapping from username => bucket
*/
if (!isset($config['arguments']['bucket'])) {
$config['arguments']['bucket'] = '';
}
$mapper = new \OC\Files\ObjectStore\Mapper($user, $this->config);
$numBuckets = $config['arguments']['num_buckets'] ?? 64;
$config['arguments']['bucket'] .= $mapper->getBucket($numBuckets);
$this->config->setUserValue($user->getUID(), 'homeobjectstore', 'bucket', $config['arguments']['bucket']);
} else {
$config['arguments']['bucket'] = $bucket;
}
// instantiate object store implementation
$config['arguments']['objectstore'] = new $config['class']($config['arguments']);
$config['arguments']['user'] = $user;
return $config;
}
}
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020, Morris Jobke <hey@morrisjobke.de>
*
* @author Morris Jobke <hey@morrisjobke.de>
*
* @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 OC\Files\Mount;
use OC\Files\ObjectStore\AppdataPreviewObjectStoreStorage;
use OC\Files\ObjectStore\ObjectStoreStorage;
use OC\Files\Storage\Wrapper\Jail;
use OCP\Files\Config\IRootMountProvider;
use OCP\Files\Storage\IStorageFactory;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
/**
* Mount provider for object store app data folder for previews
*/
class ObjectStorePreviewCacheMountProvider implements IRootMountProvider {
private LoggerInterface $logger;
/** @var IConfig */
private $config;
public function __construct(LoggerInterface $logger, IConfig $config) {
$this->logger = $logger;
$this->config = $config;
}
/**
* @return MountPoint[]
* @throws \Exception
*/
public function getRootMounts(IStorageFactory $loader): array {
if (!is_array($this->config->getSystemValue('objectstore_multibucket'))) {
return [];
}
if ($this->config->getSystemValue('objectstore.multibucket.preview-distribution', false) !== true) {
return [];
}
$instanceId = $this->config->getSystemValueString('instanceid', '');
$mountPoints = [];
$directoryRange = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
$i = 0;
foreach ($directoryRange as $parent) {
foreach ($directoryRange as $child) {
$mountPoints[] = new MountPoint(
AppdataPreviewObjectStoreStorage::class,
'/appdata_' . $instanceId . '/preview/' . $parent . '/' . $child,
$this->getMultiBucketObjectStore($i),
$loader,
null,
null,
self::class
);
$i++;
}
}
$rootStorageArguments = $this->getMultiBucketObjectStoreForRoot();
$fakeRootStorage = new ObjectStoreStorage($rootStorageArguments);
$fakeRootStorageJail = new Jail([
'storage' => $fakeRootStorage,
'root' => '/appdata_' . $instanceId . '/preview',
]);
// add a fallback location to be able to fetch existing previews from the old bucket
$mountPoints[] = new MountPoint(
$fakeRootStorageJail,
'/appdata_' . $instanceId . '/preview/old-multibucket',
null,
$loader,
null,
null,
self::class
);
return $mountPoints;
}
protected function getMultiBucketObjectStore(int $number): array {
$config = $this->config->getSystemValue('objectstore_multibucket');
// sanity checks
if (empty($config['class'])) {
$this->logger->error('No class given for objectstore', ['app' => 'files']);
}
if (!isset($config['arguments'])) {
$config['arguments'] = [];
}
/*
* Use any provided bucket argument as prefix
* and add the mapping from parent/child => bucket
*/
if (!isset($config['arguments']['bucket'])) {
$config['arguments']['bucket'] = '';
}
$config['arguments']['bucket'] .= "-preview-$number";
// instantiate object store implementation
$config['arguments']['objectstore'] = new $config['class']($config['arguments']);
$config['arguments']['internal-id'] = $number;
return $config['arguments'];
}
protected function getMultiBucketObjectStoreForRoot(): array {
$config = $this->config->getSystemValue('objectstore_multibucket');
// sanity checks
if (empty($config['class'])) {
$this->logger->error('No class given for objectstore', ['app' => 'files']);
}
if (!isset($config['arguments'])) {
$config['arguments'] = [];
}
/*
* Use any provided bucket argument as prefix
* and add the mapping from parent/child => bucket
*/
if (!isset($config['arguments']['bucket'])) {
$config['arguments']['bucket'] = '';
}
$config['arguments']['bucket'] .= '0';
// instantiate object store implementation
$config['arguments']['objectstore'] = new $config['class']($config['arguments']);
return $config['arguments'];
}
}
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 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 OC\Files\Mount;
use OC;
use OC\Files\ObjectStore\ObjectStoreStorage;
use OC\Files\Storage\LocalRootStorage;
use OC_App;
use OCP\Files\Config\IRootMountProvider;
use OCP\Files\Storage\IStorageFactory;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
class RootMountProvider implements IRootMountProvider {
private IConfig $config;
private LoggerInterface $logger;
public function __construct(IConfig $config, LoggerInterface $logger) {
$this->config = $config;
$this->logger = $logger;
}
public function getRootMounts(IStorageFactory $loader): array {
$objectStore = $this->config->getSystemValue('objectstore', null);
$objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null);
if ($objectStoreMultiBucket) {
return [$this->getMultiBucketStoreRootMount($loader, $objectStoreMultiBucket)];
} elseif ($objectStore) {
return [$this->getObjectStoreRootMount($loader, $objectStore)];
} else {
return [$this->getLocalRootMount($loader)];
}
}
private function validateObjectStoreConfig(array &$config) {
if (empty($config['class'])) {
$this->logger->error('No class given for objectstore', ['app' => 'files']);
}
if (!isset($config['arguments'])) {
$config['arguments'] = [];
}
// instantiate object store implementation
$name = $config['class'];
if (str_starts_with($name, 'OCA\\') && substr_count($name, '\\') >= 2) {
$segments = explode('\\', $name);
OC_App::loadApp(strtolower($segments[1]));
}
}
private function getLocalRootMount(IStorageFactory $loader): MountPoint {
$configDataDirectory = $this->config->getSystemValue("datadirectory", OC::$SERVERROOT . "/data");
return new MountPoint(LocalRootStorage::class, '/', ['datadir' => $configDataDirectory], $loader, null, null, self::class);
}
private function getObjectStoreRootMount(IStorageFactory $loader, array $config): MountPoint {
$this->validateObjectStoreConfig($config);
$config['arguments']['objectstore'] = new $config['class']($config['arguments']);
// mount with plain / root object store implementation
$config['class'] = ObjectStoreStorage::class;
return new MountPoint($config['class'], '/', $config['arguments'], $loader, null, null, self::class);
}
private function getMultiBucketStoreRootMount(IStorageFactory $loader, array $config): MountPoint {
$this->validateObjectStoreConfig($config);
if (!isset($config['arguments']['bucket'])) {
$config['arguments']['bucket'] = '';
}
// put the root FS always in first bucket for multibucket configuration
$config['arguments']['bucket'] .= '0';
$config['arguments']['objectstore'] = new $config['class']($config['arguments']);
// mount with plain / root object store implementation
$config['class'] = ObjectStoreStorage::class;
return new MountPoint($config['class'], '/', $config['arguments'], $loader, null, null, self::class);
}
}
@@ -0,0 +1,161 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @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 OC\Files\Node;
use OCP\Files\GenericFileException;
use OCP\Files\NotPermittedException;
use OCP\Lock\LockedException;
class File extends Node implements \OCP\Files\File {
/**
* Creates a Folder that represents a non-existing path
*
* @param string $path path
* @return NonExistingFile non-existing node
*/
protected function createNonExistingNode($path) {
return new NonExistingFile($this->root, $this->view, $path);
}
/**
* @return string
* @throws NotPermittedException
* @throws GenericFileException
* @throws LockedException
*/
public function getContent() {
if ($this->checkPermissions(\OCP\Constants::PERMISSION_READ)) {
$content = $this->view->file_get_contents($this->path);
if ($content === false) {
throw new GenericFileException();
}
return $content;
} else {
throw new NotPermittedException();
}
}
/**
* @param string|resource $data
* @throws NotPermittedException
* @throws GenericFileException
* @throws LockedException
*/
public function putContent($data) {
if ($this->checkPermissions(\OCP\Constants::PERMISSION_UPDATE)) {
$this->sendHooks(['preWrite']);
if ($this->view->file_put_contents($this->path, $data) === false) {
throw new GenericFileException('file_put_contents failed');
}
$this->fileInfo = null;
$this->sendHooks(['postWrite']);
} else {
throw new NotPermittedException();
}
}
/**
* @param string $mode
* @return resource|false
* @throws NotPermittedException
* @throws LockedException
*/
public function fopen($mode) {
$preHooks = [];
$postHooks = [];
$requiredPermissions = \OCP\Constants::PERMISSION_READ;
switch ($mode) {
case 'r+':
case 'rb+':
case 'w+':
case 'wb+':
case 'x+':
case 'xb+':
case 'a+':
case 'ab+':
case 'w':
case 'wb':
case 'x':
case 'xb':
case 'a':
case 'ab':
$preHooks[] = 'preWrite';
$postHooks[] = 'postWrite';
$requiredPermissions |= \OCP\Constants::PERMISSION_UPDATE;
break;
}
if ($this->checkPermissions($requiredPermissions)) {
$this->sendHooks($preHooks);
$result = $this->view->fopen($this->path, $mode);
$this->sendHooks($postHooks);
return $result;
} else {
throw new NotPermittedException();
}
}
/**
* @throws NotPermittedException
* @throws \OCP\Files\InvalidPathException
* @throws \OCP\Files\NotFoundException
*/
public function delete() {
if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) {
$this->sendHooks(['preDelete']);
$fileInfo = $this->getFileInfo();
$this->view->unlink($this->path);
$nonExisting = new NonExistingFile($this->root, $this->view, $this->path, $fileInfo);
$this->sendHooks(['postDelete'], [$nonExisting]);
$this->fileInfo = null;
} else {
throw new NotPermittedException();
}
}
/**
* @param string $type
* @param bool $raw
* @return string
*/
public function hash($type, $raw = false) {
return $this->view->hash($type, $this->path, $raw);
}
/**
* @inheritdoc
*/
public function getChecksum() {
return $this->getFileInfo()->getChecksum();
}
public function getExtension(): string {
return $this->getFileInfo()->getExtension();
}
}
@@ -0,0 +1,459 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright Copyright (c) 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @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 OC\Files\Node;
use OC\Files\Cache\QuerySearchHelper;
use OC\Files\Search\SearchBinaryOperator;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchOrder;
use OC\Files\Search\SearchQuery;
use OC\Files\Utils\PathHelper;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\FileInfo;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Node as INode;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchOrder;
use OCP\Files\Search\ISearchQuery;
use OCP\IUserManager;
class Folder extends Node implements \OCP\Files\Folder {
/**
* Creates a Folder that represents a non-existing path
*
* @param string $path path
* @return NonExistingFolder non-existing node
*/
protected function createNonExistingNode($path) {
return new NonExistingFolder($this->root, $this->view, $path);
}
/**
* @param string $path path relative to the folder
* @return string
* @throws \OCP\Files\NotPermittedException
*/
public function getFullPath($path) {
$path = $this->normalizePath($path);
if (!$this->isValidPath($path)) {
throw new NotPermittedException('Invalid path "' . $path . '"');
}
return $this->path . $path;
}
/**
* @param string $path
* @return string|null
*/
public function getRelativePath($path) {
return PathHelper::getRelativePath($this->getPath(), $path);
}
/**
* check if a node is a (grand-)child of the folder
*
* @param \OC\Files\Node\Node $node
* @return bool
*/
public function isSubNode($node) {
return str_starts_with($node->getPath(), $this->path . '/');
}
/**
* get the content of this directory
*
* @return Node[]
* @throws \OCP\Files\NotFoundException
*/
public function getDirectoryListing() {
$folderContent = $this->view->getDirectoryContent($this->path, '', $this->getFileInfo(false));
return array_map(function (FileInfo $info) {
if ($info->getMimetype() === FileInfo::MIMETYPE_FOLDER) {
return new Folder($this->root, $this->view, $info->getPath(), $info, $this);
} else {
return new File($this->root, $this->view, $info->getPath(), $info, $this);
}
}, $folderContent);
}
protected function createNode(string $path, ?FileInfo $info = null, bool $infoHasSubMountsIncluded = true): INode {
if (is_null($info)) {
$isDir = $this->view->is_dir($path);
} else {
$isDir = $info->getType() === FileInfo::TYPE_FOLDER;
}
$parent = dirname($path) === $this->getPath() ? $this : null;
if ($isDir) {
return new Folder($this->root, $this->view, $path, $info, $parent, $infoHasSubMountsIncluded);
} else {
return new File($this->root, $this->view, $path, $info, $parent);
}
}
/**
* Get the node at $path
*
* @param string $path
* @return \OC\Files\Node\Node
* @throws \OCP\Files\NotFoundException
*/
public function get($path) {
return $this->root->get($this->getFullPath($path));
}
/**
* @param string $path
* @return bool
*/
public function nodeExists($path) {
try {
$this->get($path);
return true;
} catch (NotFoundException $e) {
return false;
}
}
/**
* @param string $path
* @return \OC\Files\Node\Folder
* @throws \OCP\Files\NotPermittedException
*/
public function newFolder($path) {
if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
$fullPath = $this->getFullPath($path);
$nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath);
$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
if (!$this->view->mkdir($fullPath)) {
throw new NotPermittedException('Could not create folder "' . $fullPath . '"');
}
$parent = dirname($fullPath) === $this->getPath() ? $this : null;
$node = new Folder($this->root, $this->view, $fullPath, null, $parent);
$this->sendHooks(['postWrite', 'postCreate'], [$node]);
return $node;
} else {
throw new NotPermittedException('No create permission for folder "' . $path . '"');
}
}
/**
* @param string $path
* @param string | resource | null $content
* @return \OC\Files\Node\File
* @throws \OCP\Files\NotPermittedException
*/
public function newFile($path, $content = null) {
if ($path === '') {
throw new NotPermittedException('Could not create as provided path is empty');
}
if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
$fullPath = $this->getFullPath($path);
$nonExisting = new NonExistingFile($this->root, $this->view, $fullPath);
$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
if ($content !== null) {
$result = $this->view->file_put_contents($fullPath, $content);
} else {
$result = $this->view->touch($fullPath);
}
if ($result === false) {
throw new NotPermittedException('Could not create path "' . $fullPath . '"');
}
$node = new File($this->root, $this->view, $fullPath, null, $this);
$this->sendHooks(['postWrite', 'postCreate'], [$node]);
return $node;
}
throw new NotPermittedException('No create permission for path "' . $path . '"');
}
private function queryFromOperator(ISearchOperator $operator, string $uid = null, int $limit = 0, int $offset = 0): ISearchQuery {
if ($uid === null) {
$user = null;
} else {
/** @var IUserManager $userManager */
$userManager = \OCP\Server::get(IUserManager::class);
$user = $userManager->get($uid);
}
return new SearchQuery($operator, $limit, $offset, [], $user);
}
/**
* search for files with the name matching $query
*
* @param string|ISearchQuery $query
* @return \OC\Files\Node\Node[]
*/
public function search($query) {
if (is_string($query)) {
$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%'));
}
// search is handled by a single query covering all caches that this folder contains
// this is done by collect
$limitToHome = $query->limitToHome();
if ($limitToHome && count(explode('/', $this->path)) !== 3) {
throw new \InvalidArgumentException('searching by owner is only allowed in the users home folder');
}
/** @var QuerySearchHelper $searchHelper */
$searchHelper = \OC::$server->get(QuerySearchHelper::class);
[$caches, $mountByMountPoint] = $searchHelper->getCachesAndMountPointsForSearch($this->root, $this->path, $limitToHome);
$resultsPerCache = $searchHelper->searchInCaches($query, $caches);
// loop through all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all
$files = array_merge(...array_map(function (array $results, string $relativeMountPoint) use ($mountByMountPoint) {
$mount = $mountByMountPoint[$relativeMountPoint];
return array_map(function (ICacheEntry $result) use ($relativeMountPoint, $mount) {
return $this->cacheEntryToFileInfo($mount, $relativeMountPoint, $result);
}, $results);
}, array_values($resultsPerCache), array_keys($resultsPerCache)));
// don't include this folder in the results
$files = array_filter($files, function (FileInfo $file) {
return $file->getPath() !== $this->getPath();
});
// since results were returned per-cache, they are no longer fully sorted
$order = $query->getOrder();
if ($order) {
usort($files, function (FileInfo $a, FileInfo $b) use ($order) {
foreach ($order as $orderField) {
$cmp = $orderField->sortFileInfo($a, $b);
if ($cmp !== 0) {
return $cmp;
}
}
return 0;
});
}
return array_map(function (FileInfo $file) {
return $this->createNode($file->getPath(), $file);
}, $files);
}
private function cacheEntryToFileInfo(IMountPoint $mount, string $appendRoot, ICacheEntry $cacheEntry): FileInfo {
$cacheEntry['internalPath'] = $cacheEntry['path'];
$cacheEntry['path'] = rtrim($appendRoot . $cacheEntry->getPath(), '/');
$subPath = $cacheEntry['path'] !== '' ? '/' . $cacheEntry['path'] : '';
return new \OC\Files\FileInfo($this->path . $subPath, $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
}
/**
* search for files by mimetype
*
* @param string $mimetype
* @return Node[]
*/
public function searchByMime($mimetype) {
if (!str_contains($mimetype, '/')) {
$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%'));
} else {
$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype));
}
return $this->search($query);
}
/**
* search for files by tag
*
* @param string|int $tag name or tag id
* @param string $userId owner of the tags
* @return Node[]
*/
public function searchByTag($tag, $userId) {
$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', $tag), $userId);
return $this->search($query);
}
public function searchBySystemTag(string $tagName, string $userId, int $limit = 0, int $offset = 0): array {
$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'systemtag', $tagName), $userId, $limit, $offset);
return $this->search($query);
}
/**
* @param int $id
* @return \OC\Files\Node\Node[]
*/
public function getById($id) {
return $this->root->getByIdInPath((int)$id, $this->getPath());
}
protected function getAppDataDirectoryName(): string {
$instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid');
return 'appdata_' . $instanceId;
}
/**
* In case the path we are currently in is inside the appdata_* folder,
* the original getById method does not work, because it can only look inside
* the user's mount points. But the user has no mount point for the root storage.
*
* So in that case we directly check the mount of the root if it contains
* the id. If it does we check if the path is inside the path we are working
* in.
*
* @param int $id
* @return array
*/
protected function getByIdInRootMount(int $id): array {
if (!method_exists($this->root, 'createNode')) {
// Always expected to be false. Being a method of Folder, this is
// always implemented. For it is an internal method and should not
// be exposed and made public, it is not part of an interface.
return [];
}
$mount = $this->root->getMount('');
$storage = $mount->getStorage();
$cacheEntry = $storage?->getCache($this->path)->get($id);
if (!$cacheEntry) {
return [];
}
$absolutePath = '/' . ltrim($cacheEntry->getPath(), '/');
$currentPath = rtrim($this->path, '/') . '/';
if (!str_starts_with($absolutePath, $currentPath)) {
return [];
}
return [$this->root->createNode(
$absolutePath, new \OC\Files\FileInfo(
$absolutePath,
$storage,
$cacheEntry->getPath(),
$cacheEntry,
$mount
))];
}
public function getFreeSpace() {
return $this->view->free_space($this->path);
}
public function delete() {
if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) {
$this->sendHooks(['preDelete']);
$fileInfo = $this->getFileInfo();
$this->view->rmdir($this->path);
$nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo);
$this->sendHooks(['postDelete'], [$nonExisting]);
} else {
throw new NotPermittedException('No delete permission for path "' . $this->path . '"');
}
}
/**
* Add a suffix to the name in case the file exists
*
* @param string $name
* @return string
* @throws NotPermittedException
*/
public function getNonExistingName($name) {
$uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view);
return trim($this->getRelativePath($uniqueName), '/');
}
/**
* @param int $limit
* @param int $offset
* @return INode[]
*/
public function getRecent($limit, $offset = 0) {
$filterOutNonEmptyFolder = new SearchBinaryOperator(
// filter out non empty folders
ISearchBinaryOperator::OPERATOR_OR,
[
new SearchBinaryOperator(
ISearchBinaryOperator::OPERATOR_NOT,
[
new SearchComparison(
ISearchComparison::COMPARE_EQUAL,
'mimetype',
FileInfo::MIMETYPE_FOLDER
),
]
),
new SearchComparison(
ISearchComparison::COMPARE_EQUAL,
'size',
0
),
]
);
$filterNonRecentFiles = new SearchComparison(
ISearchComparison::COMPARE_GREATER_THAN,
'mtime',
strtotime("-2 week")
);
if ($offset === 0 && $limit <= 100) {
$query = new SearchQuery(
new SearchBinaryOperator(
ISearchBinaryOperator::OPERATOR_AND,
[
$filterOutNonEmptyFolder,
$filterNonRecentFiles,
],
),
$limit,
$offset,
[
new SearchOrder(
ISearchOrder::DIRECTION_DESCENDING,
'mtime'
),
]
);
} else {
$query = new SearchQuery(
$filterOutNonEmptyFolder,
$limit,
$offset,
[
new SearchOrder(
ISearchOrder::DIRECTION_DESCENDING,
'mtime'
),
]
);
}
return $this->search($query);
}
}
@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\Node;
use OC\Files\Filesystem;
use OC\Files\View;
use OCP\EventDispatcher\GenericEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
use OCP\Files\Events\Node\BeforeNodeCreatedEvent;
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeReadEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\BeforeNodeTouchedEvent;
use OCP\Files\Events\Node\BeforeNodeWrittenEvent;
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeRenamedEvent;
use OCP\Files\Events\Node\NodeTouchedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\Files\FileInfo;
use OCP\Files\IRootFolder;
use OCP\Util;
class HookConnector {
/** @var IRootFolder */
private $root;
/** @var View */
private $view;
/** @var FileInfo[] */
private $deleteMetaCache = [];
/** @var IEventDispatcher */
private $dispatcher;
public function __construct(
IRootFolder $root,
View $view,
IEventDispatcher $dispatcher) {
$this->root = $root;
$this->view = $view;
$this->dispatcher = $dispatcher;
}
public function viewToNode() {
Util::connectHook('OC_Filesystem', 'write', $this, 'write');
Util::connectHook('OC_Filesystem', 'post_write', $this, 'postWrite');
Util::connectHook('OC_Filesystem', 'create', $this, 'create');
Util::connectHook('OC_Filesystem', 'post_create', $this, 'postCreate');
Util::connectHook('OC_Filesystem', 'delete', $this, 'delete');
Util::connectHook('OC_Filesystem', 'post_delete', $this, 'postDelete');
Util::connectHook('OC_Filesystem', 'rename', $this, 'rename');
Util::connectHook('OC_Filesystem', 'post_rename', $this, 'postRename');
Util::connectHook('OC_Filesystem', 'copy', $this, 'copy');
Util::connectHook('OC_Filesystem', 'post_copy', $this, 'postCopy');
Util::connectHook('OC_Filesystem', 'touch', $this, 'touch');
Util::connectHook('OC_Filesystem', 'post_touch', $this, 'postTouch');
Util::connectHook('OC_Filesystem', 'read', $this, 'read');
}
public function write($arguments) {
$node = $this->getNodeForPath($arguments['path']);
$this->root->emit('\OC\Files', 'preWrite', [$node]);
$this->dispatcher->dispatch('\OCP\Files::preWrite', new GenericEvent($node));
$event = new BeforeNodeWrittenEvent($node);
$this->dispatcher->dispatchTyped($event);
}
public function postWrite($arguments) {
$node = $this->getNodeForPath($arguments['path']);
$this->root->emit('\OC\Files', 'postWrite', [$node]);
$this->dispatcher->dispatch('\OCP\Files::postWrite', new GenericEvent($node));
$event = new NodeWrittenEvent($node);
$this->dispatcher->dispatchTyped($event);
}
public function create($arguments) {
$node = $this->getNodeForPath($arguments['path']);
$this->root->emit('\OC\Files', 'preCreate', [$node]);
$this->dispatcher->dispatch('\OCP\Files::preCreate', new GenericEvent($node));
$event = new BeforeNodeCreatedEvent($node);
$this->dispatcher->dispatchTyped($event);
}
public function postCreate($arguments) {
$node = $this->getNodeForPath($arguments['path']);
$this->root->emit('\OC\Files', 'postCreate', [$node]);
$this->dispatcher->dispatch('\OCP\Files::postCreate', new GenericEvent($node));
$event = new NodeCreatedEvent($node);
$this->dispatcher->dispatchTyped($event);
}
public function delete($arguments) {
$node = $this->getNodeForPath($arguments['path']);
$this->deleteMetaCache[$node->getPath()] = $node->getFileInfo();
$this->root->emit('\OC\Files', 'preDelete', [$node]);
$this->dispatcher->dispatch('\OCP\Files::preDelete', new GenericEvent($node));
$event = new BeforeNodeDeletedEvent($node, $arguments['run']);
$this->dispatcher->dispatchTyped($event);
}
public function postDelete($arguments) {
$node = $this->getNodeForPath($arguments['path']);
unset($this->deleteMetaCache[$node->getPath()]);
$this->root->emit('\OC\Files', 'postDelete', [$node]);
$this->dispatcher->dispatch('\OCP\Files::postDelete', new GenericEvent($node));
$event = new NodeDeletedEvent($node);
$this->dispatcher->dispatchTyped($event);
}
public function touch($arguments) {
$node = $this->getNodeForPath($arguments['path']);
$this->root->emit('\OC\Files', 'preTouch', [$node]);
$this->dispatcher->dispatch('\OCP\Files::preTouch', new GenericEvent($node));
$event = new BeforeNodeTouchedEvent($node);
$this->dispatcher->dispatchTyped($event);
}
public function postTouch($arguments) {
$node = $this->getNodeForPath($arguments['path']);
$this->root->emit('\OC\Files', 'postTouch', [$node]);
$this->dispatcher->dispatch('\OCP\Files::postTouch', new GenericEvent($node));
$event = new NodeTouchedEvent($node);
$this->dispatcher->dispatchTyped($event);
}
public function rename($arguments) {
$source = $this->getNodeForPath($arguments['oldpath']);
$target = $this->getNodeForPath($arguments['newpath']);
$this->root->emit('\OC\Files', 'preRename', [$source, $target]);
$this->dispatcher->dispatch('\OCP\Files::preRename', new GenericEvent([$source, $target]));
$event = new BeforeNodeRenamedEvent($source, $target, $arguments['run']);
$this->dispatcher->dispatchTyped($event);
}
public function postRename($arguments) {
$source = $this->getNodeForPath($arguments['oldpath']);
$target = $this->getNodeForPath($arguments['newpath']);
$this->root->emit('\OC\Files', 'postRename', [$source, $target]);
$this->dispatcher->dispatch('\OCP\Files::postRename', new GenericEvent([$source, $target]));
$event = new NodeRenamedEvent($source, $target);
$this->dispatcher->dispatchTyped($event);
}
public function copy($arguments) {
$source = $this->getNodeForPath($arguments['oldpath']);
$target = $this->getNodeForPath($arguments['newpath']);
$this->root->emit('\OC\Files', 'preCopy', [$source, $target]);
$this->dispatcher->dispatch('\OCP\Files::preCopy', new GenericEvent([$source, $target]));
$event = new BeforeNodeCopiedEvent($source, $target);
$this->dispatcher->dispatchTyped($event);
}
public function postCopy($arguments) {
$source = $this->getNodeForPath($arguments['oldpath']);
$target = $this->getNodeForPath($arguments['newpath']);
$this->root->emit('\OC\Files', 'postCopy', [$source, $target]);
$this->dispatcher->dispatch('\OCP\Files::postCopy', new GenericEvent([$source, $target]));
$event = new NodeCopiedEvent($source, $target);
$this->dispatcher->dispatchTyped($event);
}
public function read($arguments) {
$node = $this->getNodeForPath($arguments['path']);
$this->root->emit('\OC\Files', 'read', [$node]);
$this->dispatcher->dispatch('\OCP\Files::read', new GenericEvent([$node]));
$event = new BeforeNodeReadEvent($node);
$this->dispatcher->dispatchTyped($event);
}
private function getNodeForPath(string $path): Node {
$info = Filesystem::getView()->getFileInfo($path);
if (!$info) {
$fullPath = Filesystem::getView()->getAbsolutePath($path);
if (isset($this->deleteMetaCache[$fullPath])) {
$info = $this->deleteMetaCache[$fullPath];
} else {
$info = null;
}
if (Filesystem::is_dir($path)) {
return new NonExistingFolder($this->root, $this->view, $fullPath, $info);
} else {
return new NonExistingFile($this->root, $this->view, $fullPath, $info);
}
}
if ($info->getType() === FileInfo::TYPE_FILE) {
return new File($this->root, $this->view, $info->getPath(), $info);
} else {
return new Folder($this->root, $this->view, $info->getPath(), $info);
}
}
}
@@ -0,0 +1,586 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @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 OC\Files\Node;
use OC\Files\Filesystem;
use OC\Files\Utils\PathHelper;
use OCP\Constants;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotPermittedException;
/**
* Class LazyFolder
*
* This is a lazy wrapper around a folder. So only
* once it is needed this will get initialized.
*
* @package OC\Files\Node
*/
class LazyFolder implements Folder {
/** @var \Closure(): Folder */
private \Closure $folderClosure;
protected ?Folder $folder = null;
protected IRootFolder $rootFolder;
protected array $data;
/**
* @param IRootFolder $rootFolder
* @param \Closure(): Folder $folderClosure
* @param array $data
*/
public function __construct(IRootFolder $rootFolder, \Closure $folderClosure, array $data = []) {
$this->rootFolder = $rootFolder;
$this->folderClosure = $folderClosure;
$this->data = $data;
}
protected function getRootFolder(): IRootFolder {
return $this->rootFolder;
}
protected function getRealFolder(): Folder {
if ($this->folder === null) {
$this->folder = call_user_func($this->folderClosure);
}
return $this->folder;
}
/**
* Magic method to first get the real rootFolder and then
* call $method with $args on it
*
* @param $method
* @param $args
* @return mixed
*/
public function __call($method, $args) {
return call_user_func_array([$this->getRealFolder(), $method], $args);
}
/**
* @inheritDoc
*/
public function getUser() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function listen($scope, $method, callable $callback) {
$this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function removeListener($scope = null, $method = null, callable $callback = null) {
$this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function emit($scope, $method, $arguments = []) {
$this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function mount($storage, $mountPoint, $arguments = []) {
$this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getMount(string $mountPoint): IMountPoint {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @return IMountPoint[]
*/
public function getMountsIn(string $mountPoint): array {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getMountByStorageId($storageId) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getMountByNumericStorageId($numericId) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function unMount($mount) {
$this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function get($path) {
return $this->getRootFolder()->get($this->getFullPath($path));
}
/**
* @inheritDoc
*/
public function rename($targetPath) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function delete() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function copy($targetPath) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function touch($mtime = null) {
$this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getStorage() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getPath() {
if (isset($this->data['path'])) {
return $this->data['path'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getInternalPath() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getId() {
if (isset($this->data['fileid'])) {
return $this->data['fileid'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function stat() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getMTime() {
if (isset($this->data['mtime'])) {
return $this->data['mtime'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getSize($includeMounts = true): int|float {
if (isset($this->data['size'])) {
return $this->data['size'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getEtag() {
if (isset($this->data['etag'])) {
return $this->data['etag'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getPermissions() {
if (isset($this->data['permissions'])) {
return $this->data['permissions'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isReadable() {
if (isset($this->data['permissions'])) {
return ($this->data['permissions'] & Constants::PERMISSION_READ) == Constants::PERMISSION_READ;
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isUpdateable() {
if (isset($this->data['permissions'])) {
return ($this->data['permissions'] & Constants::PERMISSION_UPDATE) == Constants::PERMISSION_UPDATE;
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isDeletable() {
if (isset($this->data['permissions'])) {
return ($this->data['permissions'] & Constants::PERMISSION_DELETE) == Constants::PERMISSION_DELETE;
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isShareable() {
if (isset($this->data['permissions'])) {
return ($this->data['permissions'] & Constants::PERMISSION_SHARE) == Constants::PERMISSION_SHARE;
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getParent() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getName() {
if (isset($this->data['path'])) {
return basename($this->data['path']);
}
if (isset($this->data['name'])) {
return $this->data['name'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getUserFolder($userId) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getMimetype() {
if (isset($this->data['mimetype'])) {
return $this->data['mimetype'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getMimePart() {
if (isset($this->data['mimetype'])) {
[$part,] = explode('/', $this->data['mimetype']);
return $part;
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isEncrypted() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getType() {
if (isset($this->data['type'])) {
return $this->data['type'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isShared() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isMounted() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getMountPoint() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getOwner() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getChecksum() {
return $this->__call(__FUNCTION__, func_get_args());
}
public function getExtension(): string {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getFullPath($path) {
if (isset($this->data['path'])) {
$path = PathHelper::normalizePath($path);
if (!Filesystem::isValidPath($path)) {
throw new NotPermittedException('Invalid path "' . $path . '"');
}
return $this->data['path'] . $path;
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isSubNode($node) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getDirectoryListing() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function nodeExists($path) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function newFolder($path) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function newFile($path, $content = null) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function search($query) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function searchByMime($mimetype) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function searchByTag($tag, $userId) {
return $this->__call(__FUNCTION__, func_get_args());
}
public function searchBySystemTag(string $tagName, string $userId, int $limit = 0, int $offset = 0) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getById($id) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getFreeSpace() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function isCreatable() {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getNonExistingName($name) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function move($targetPath) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function lock($type) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function changeLock($targetType) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function unlock($type) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getRecent($limit, $offset = 0) {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getCreationTime(): int {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getUploadTime(): int {
return $this->__call(__FUNCTION__, func_get_args());
}
public function getRelativePath($path) {
return PathHelper::getRelativePath($this->getPath(), $path);
}
public function getParentId(): int {
if (isset($this->data['parent'])) {
return $this->data['parent'];
}
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
* @return array<string, int|string|bool|float|string[]|int[]>
*/
public function getMetadata(): array {
return $this->data['metadata'] ?? $this->__call(__FUNCTION__, func_get_args());
}
}
@@ -0,0 +1,62 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\Node;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Node as INode;
/**
* Class LazyRoot
*
* This is a lazy wrapper around the root. So only
* once it is needed this will get initialized.
*
* @package OC\Files\Node
*/
class LazyRoot extends LazyFolder implements IRootFolder {
public function __construct(\Closure $folderClosure, array $data = []) {
parent::__construct($this, $folderClosure, $data);
}
protected function getRootFolder(): IRootFolder {
$folder = $this->getRealFolder();
if (!$folder instanceof IRootFolder) {
throw new \Exception('Lazy root folder closure didn\'t return a root folder');
}
return $folder;
}
public function getUserFolder($userId) {
return $this->__call(__FUNCTION__, func_get_args());
}
public function getByIdInPath(int $id, string $path) {
return $this->__call(__FUNCTION__, func_get_args());
}
public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoint $mountPoint): INode {
return $this->getRootFolder()->getNodeFromCacheEntryAndMount($cacheEntry, $mountPoint);
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 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 OC\Files\Node;
use OCP\Constants;
use OCP\Files\File;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
use OCP\Files\NotFoundException;
use OCP\IUser;
use Psr\Log\LoggerInterface;
class LazyUserFolder extends LazyFolder {
private IUser $user;
private string $path;
private IMountManager $mountManager;
public function __construct(IRootFolder $rootFolder, IUser $user, IMountManager $mountManager) {
$this->user = $user;
$this->mountManager = $mountManager;
$this->path = '/' . $user->getUID() . '/files';
parent::__construct($rootFolder, function () use ($user): Folder {
try {
$node = $this->getRootFolder()->get($this->path);
if ($node instanceof File) {
$e = new \RuntimeException();
\OCP\Server::get(LoggerInterface::class)->error('User root storage is not a folder: ' . $this->path, [
'exception' => $e,
]);
throw $e;
}
return $node;
} catch (NotFoundException $e) {
if (!$this->getRootFolder()->nodeExists('/' . $user->getUID())) {
$this->getRootFolder()->newFolder('/' . $user->getUID());
}
return $this->getRootFolder()->newFolder($this->path);
}
}, [
'path' => $this->path,
// Sharing user root folder is not allowed
'permissions' => Constants::PERMISSION_ALL ^ Constants::PERMISSION_SHARE,
'type' => FileInfo::TYPE_FOLDER,
'mimetype' => FileInfo::MIMETYPE_FOLDER,
]);
}
public function get($path) {
return $this->getRootFolder()->get('/' . $this->user->getUID() . '/files/' . ltrim($path, '/'));
}
/**
* @param int $id
* @return \OCP\Files\Node[]
*/
public function getById($id) {
return $this->getRootFolder()->getByIdInPath((int)$id, $this->getPath());
}
public function getMountPoint() {
if ($this->folder !== null) {
return $this->folder->getMountPoint();
}
$mountPoint = $this->mountManager->find('/' . $this->user->getUID());
if (is_null($mountPoint)) {
throw new \Exception("No mountpoint for user folder");
}
return $mountPoint;
}
}
@@ -0,0 +1,509 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @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 OC\Files\Node;
use OC\Files\Filesystem;
use OC\Files\Mount\MoveableMount;
use OC\Files\Utils\PathHelper;
use OCP\EventDispatcher\GenericEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\FileInfo;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
use OCP\Files\Node as INode;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Lock\LockedException;
use OCP\PreConditionNotMetException;
// FIXME: this class really should be abstract (+1)
class Node implements INode {
/**
* @var \OC\Files\View $view
*/
protected $view;
protected IRootFolder $root;
/**
* @var string $path Absolute path to the node (e.g. /admin/files/folder/file)
*/
protected $path;
protected ?FileInfo $fileInfo;
protected ?INode $parent;
private bool $infoHasSubMountsIncluded;
/**
* @param \OC\Files\View $view
* @param \OCP\Files\IRootFolder $root
* @param string $path
* @param FileInfo $fileInfo
*/
public function __construct(IRootFolder $root, $view, $path, $fileInfo = null, ?INode $parent = null, bool $infoHasSubMountsIncluded = true) {
if (Filesystem::normalizePath($view->getRoot()) !== '/') {
throw new PreConditionNotMetException('The view passed to the node should not have any fake root set');
}
$this->view = $view;
$this->root = $root;
$this->path = $path;
$this->fileInfo = $fileInfo;
$this->parent = $parent;
$this->infoHasSubMountsIncluded = $infoHasSubMountsIncluded;
}
/**
* Creates a Node of the same type that represents a non-existing path
*
* @param string $path path
* @return Node non-existing node
* @throws \Exception
*/
protected function createNonExistingNode($path) {
throw new \Exception('Must be implemented by subclasses');
}
/**
* Returns the matching file info
*
* @return FileInfo
* @throws InvalidPathException
* @throws NotFoundException
*/
public function getFileInfo(bool $includeMountPoint = true) {
if (!$this->fileInfo) {
if (!Filesystem::isValidPath($this->path)) {
throw new InvalidPathException();
}
$fileInfo = $this->view->getFileInfo($this->path, $includeMountPoint);
$this->infoHasSubMountsIncluded = $includeMountPoint;
if ($fileInfo instanceof FileInfo) {
$this->fileInfo = $fileInfo;
} else {
throw new NotFoundException();
}
} elseif ($includeMountPoint && !$this->infoHasSubMountsIncluded && $this instanceof Folder) {
if ($this->fileInfo instanceof \OC\Files\FileInfo) {
$this->view->addSubMounts($this->fileInfo);
}
$this->infoHasSubMountsIncluded = true;
}
return $this->fileInfo;
}
/**
* @param string[] $hooks
*/
protected function sendHooks($hooks, array $args = null) {
$args = !empty($args) ? $args : [$this];
/** @var IEventDispatcher $dispatcher */
$dispatcher = \OC::$server->get(IEventDispatcher::class);
foreach ($hooks as $hook) {
if (method_exists($this->root, 'emit')) {
$this->root->emit('\OC\Files', $hook, $args);
}
if (in_array($hook, ['preWrite', 'postWrite', 'preCreate', 'postCreate', 'preTouch', 'postTouch', 'preDelete', 'postDelete'], true)) {
$event = new GenericEvent($args[0]);
} else {
$event = new GenericEvent($args);
}
$dispatcher->dispatch('\OCP\Files::' . $hook, $event);
}
}
/**
* @param int $permissions
* @return bool
* @throws InvalidPathException
* @throws NotFoundException
*/
protected function checkPermissions($permissions) {
return ($this->getPermissions() & $permissions) === $permissions;
}
public function delete() {
}
/**
* @param int $mtime
* @throws InvalidPathException
* @throws NotFoundException
* @throws NotPermittedException
*/
public function touch($mtime = null) {
if ($this->checkPermissions(\OCP\Constants::PERMISSION_UPDATE)) {
$this->sendHooks(['preTouch']);
$this->view->touch($this->path, $mtime);
$this->sendHooks(['postTouch']);
if ($this->fileInfo) {
if (is_null($mtime)) {
$mtime = time();
}
$this->fileInfo['mtime'] = $mtime;
}
} else {
throw new NotPermittedException();
}
}
public function getStorage() {
$storage = $this->getMountPoint()->getStorage();
if (!$storage) {
throw new \Exception("No storage for node");
}
return $storage;
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
/**
* @return string
*/
public function getInternalPath() {
return $this->getFileInfo(false)->getInternalPath();
}
/**
* @return int
* @throws InvalidPathException
* @throws NotFoundException
*/
public function getId() {
return $this->getFileInfo(false)->getId() ?? -1;
}
/**
* @return array
*/
public function stat() {
return $this->view->stat($this->path);
}
/**
* @return int
* @throws InvalidPathException
* @throws NotFoundException
*/
public function getMTime() {
return $this->getFileInfo()->getMTime();
}
/**
* @param bool $includeMounts
* @return int|float
* @throws InvalidPathException
* @throws NotFoundException
*/
public function getSize($includeMounts = true): int|float {
return $this->getFileInfo()->getSize($includeMounts);
}
/**
* @return string
* @throws InvalidPathException
* @throws NotFoundException
*/
public function getEtag() {
return $this->getFileInfo()->getEtag();
}
/**
* @return int
* @throws InvalidPathException
* @throws NotFoundException
*/
public function getPermissions() {
return $this->getFileInfo(false)->getPermissions();
}
/**
* @return bool
* @throws InvalidPathException
* @throws NotFoundException
*/
public function isReadable() {
return $this->getFileInfo(false)->isReadable();
}
/**
* @return bool
* @throws InvalidPathException
* @throws NotFoundException
*/
public function isUpdateable() {
return $this->getFileInfo(false)->isUpdateable();
}
/**
* @return bool
* @throws InvalidPathException
* @throws NotFoundException
*/
public function isDeletable() {
return $this->getFileInfo(false)->isDeletable();
}
/**
* @return bool
* @throws InvalidPathException
* @throws NotFoundException
*/
public function isShareable() {
return $this->getFileInfo(false)->isShareable();
}
/**
* @return bool
* @throws InvalidPathException
* @throws NotFoundException
*/
public function isCreatable() {
return $this->getFileInfo(false)->isCreatable();
}
public function getParent(): INode|IRootFolder {
if ($this->parent === null) {
$newPath = dirname($this->path);
if ($newPath === '' || $newPath === '.' || $newPath === '/') {
return $this->root;
}
// Manually fetch the parent if the current node doesn't have a file info yet
try {
$fileInfo = $this->getFileInfo();
} catch (NotFoundException) {
$this->parent = $this->root->get($newPath);
/** @var \OCP\Files\Folder $this->parent */
return $this->parent;
}
// gather the metadata we already know about our parent
$parentData = [
'path' => $newPath,
'fileid' => $fileInfo->getParentId(),
];
// and create lazy folder with it instead of always querying
$this->parent = new LazyFolder($this->root, function () use ($newPath) {
return $this->root->get($newPath);
}, $parentData);
}
return $this->parent;
}
/**
* @return string
*/
public function getName() {
return basename($this->path);
}
/**
* @param string $path
* @return string
*/
protected function normalizePath($path) {
return PathHelper::normalizePath($path);
}
/**
* check if the requested path is valid
*
* @param string $path
* @return bool
*/
public function isValidPath($path) {
return Filesystem::isValidPath($path);
}
public function isMounted() {
return $this->getFileInfo(false)->isMounted();
}
public function isShared() {
return $this->getFileInfo(false)->isShared();
}
public function getMimeType() {
return $this->getFileInfo(false)->getMimetype();
}
public function getMimePart() {
return $this->getFileInfo(false)->getMimePart();
}
public function getType() {
return $this->getFileInfo(false)->getType();
}
public function isEncrypted() {
return $this->getFileInfo(false)->isEncrypted();
}
public function getMountPoint() {
return $this->getFileInfo(false)->getMountPoint();
}
public function getOwner() {
return $this->getFileInfo(false)->getOwner();
}
public function getChecksum() {
}
public function getExtension(): string {
return $this->getFileInfo(false)->getExtension();
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @throws LockedException
*/
public function lock($type) {
$this->view->lockFile($this->path, $type);
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @throws LockedException
*/
public function changeLock($type) {
$this->view->changeLock($this->path, $type);
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @throws LockedException
*/
public function unlock($type) {
$this->view->unlockFile($this->path, $type);
}
/**
* @param string $targetPath
* @return INode
* @throws InvalidPathException
* @throws NotFoundException
* @throws NotPermittedException if copy not allowed or failed
*/
public function copy($targetPath) {
$targetPath = $this->normalizePath($targetPath);
$parent = $this->root->get(dirname($targetPath));
if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) {
$nonExisting = $this->createNonExistingNode($targetPath);
$this->sendHooks(['preCopy'], [$this, $nonExisting]);
$this->sendHooks(['preWrite'], [$nonExisting]);
if (!$this->view->copy($this->path, $targetPath)) {
throw new NotPermittedException('Could not copy ' . $this->path . ' to ' . $targetPath);
}
$targetNode = $this->root->get($targetPath);
$this->sendHooks(['postCopy'], [$this, $targetNode]);
$this->sendHooks(['postWrite'], [$targetNode]);
return $targetNode;
} else {
throw new NotPermittedException('No permission to copy to path ' . $targetPath);
}
}
/**
* @param string $targetPath
* @return INode
* @throws InvalidPathException
* @throws NotFoundException
* @throws NotPermittedException if move not allowed or failed
* @throws LockedException
*/
public function move($targetPath) {
$targetPath = $this->normalizePath($targetPath);
$parent = $this->root->get(dirname($targetPath));
if (
$parent instanceof Folder and
$this->isValidPath($targetPath) and
(
$parent->isCreatable() ||
($parent->getInternalPath() === '' && $parent->getMountPoint() instanceof MoveableMount)
)
) {
$nonExisting = $this->createNonExistingNode($targetPath);
$this->sendHooks(['preRename'], [$this, $nonExisting]);
$this->sendHooks(['preWrite'], [$nonExisting]);
if (!$this->view->rename($this->path, $targetPath)) {
throw new NotPermittedException('Could not move ' . $this->path . ' to ' . $targetPath);
}
$mountPoint = $this->getMountPoint();
if ($mountPoint) {
// update the cached fileinfo with the new (internal) path
/** @var \OC\Files\FileInfo $oldFileInfo */
$oldFileInfo = $this->getFileInfo();
$this->fileInfo = new \OC\Files\FileInfo($targetPath, $oldFileInfo->getStorage(), $mountPoint->getInternalPath($targetPath), $oldFileInfo->getData(), $mountPoint, $oldFileInfo->getOwner());
}
$targetNode = $this->root->get($targetPath);
$this->sendHooks(['postRename'], [$this, $targetNode]);
$this->sendHooks(['postWrite'], [$targetNode]);
$this->path = $targetPath;
return $targetNode;
} else {
throw new NotPermittedException('No permission to move to path ' . $targetPath);
}
}
public function getCreationTime(): int {
return $this->getFileInfo()->getCreationTime();
}
public function getUploadTime(): int {
return $this->getFileInfo()->getUploadTime();
}
public function getParentId(): int {
return $this->fileInfo->getParentId();
}
/**
* @inheritDoc
* @return array<string, int|string|bool|float|string[]|int[]>
*/
public function getMetadata(): array {
return $this->fileInfo->getMetadata();
}
}
@@ -0,0 +1,143 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @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 OC\Files\Node;
use OCP\Files\NotFoundException;
class NonExistingFile extends File {
/**
* @param string $newPath
* @throws \OCP\Files\NotFoundException
*/
public function rename($newPath) {
throw new NotFoundException();
}
public function delete() {
throw new NotFoundException();
}
public function copy($targetPath) {
throw new NotFoundException();
}
public function touch($mtime = null) {
throw new NotFoundException();
}
public function getId() {
if ($this->fileInfo) {
return parent::getId();
} else {
throw new NotFoundException();
}
}
public function stat() {
throw new NotFoundException();
}
public function getMTime() {
if ($this->fileInfo) {
return parent::getMTime();
} else {
throw new NotFoundException();
}
}
public function getSize($includeMounts = true): int|float {
if ($this->fileInfo) {
return parent::getSize($includeMounts);
} else {
throw new NotFoundException();
}
}
public function getEtag() {
if ($this->fileInfo) {
return parent::getEtag();
} else {
throw new NotFoundException();
}
}
public function getPermissions() {
if ($this->fileInfo) {
return parent::getPermissions();
} else {
throw new NotFoundException();
}
}
public function isReadable() {
if ($this->fileInfo) {
return parent::isReadable();
} else {
throw new NotFoundException();
}
}
public function isUpdateable() {
if ($this->fileInfo) {
return parent::isUpdateable();
} else {
throw new NotFoundException();
}
}
public function isDeletable() {
if ($this->fileInfo) {
return parent::isDeletable();
} else {
throw new NotFoundException();
}
}
public function isShareable() {
if ($this->fileInfo) {
return parent::isShareable();
} else {
throw new NotFoundException();
}
}
public function getContent() {
throw new NotFoundException();
}
public function putContent($data) {
throw new NotFoundException();
}
public function getMimeType() {
if ($this->fileInfo) {
return parent::getMimeType();
} else {
throw new NotFoundException();
}
}
public function fopen($mode) {
throw new NotFoundException();
}
}
@@ -0,0 +1,176 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.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 OC\Files\Node;
use OCP\Files\NotFoundException;
class NonExistingFolder extends Folder {
/**
* @param string $newPath
* @throws \OCP\Files\NotFoundException
*/
public function rename($newPath) {
throw new NotFoundException();
}
public function delete() {
throw new NotFoundException();
}
public function copy($targetPath) {
throw new NotFoundException();
}
public function touch($mtime = null) {
throw new NotFoundException();
}
public function getId() {
if ($this->fileInfo) {
return parent::getId();
} else {
throw new NotFoundException();
}
}
public function stat() {
throw new NotFoundException();
}
public function getMTime() {
if ($this->fileInfo) {
return parent::getMTime();
} else {
throw new NotFoundException();
}
}
public function getSize($includeMounts = true): int|float {
if ($this->fileInfo) {
return parent::getSize($includeMounts);
} else {
throw new NotFoundException();
}
}
public function getEtag() {
if ($this->fileInfo) {
return parent::getEtag();
} else {
throw new NotFoundException();
}
}
public function getPermissions() {
if ($this->fileInfo) {
return parent::getPermissions();
} else {
throw new NotFoundException();
}
}
public function isReadable() {
if ($this->fileInfo) {
return parent::isReadable();
} else {
throw new NotFoundException();
}
}
public function isUpdateable() {
if ($this->fileInfo) {
return parent::isUpdateable();
} else {
throw new NotFoundException();
}
}
public function isDeletable() {
if ($this->fileInfo) {
return parent::isDeletable();
} else {
throw new NotFoundException();
}
}
public function isShareable() {
if ($this->fileInfo) {
return parent::isShareable();
} else {
throw new NotFoundException();
}
}
public function get($path) {
throw new NotFoundException();
}
public function getDirectoryListing() {
throw new NotFoundException();
}
public function nodeExists($path) {
return false;
}
public function newFolder($path) {
throw new NotFoundException();
}
public function newFile($path, $content = null) {
throw new NotFoundException();
}
public function search($query) {
throw new NotFoundException();
}
public function searchByMime($mimetype) {
throw new NotFoundException();
}
public function searchByTag($tag, $userId) {
throw new NotFoundException();
}
public function searchBySystemTag(string $tagName, string $userId, int $limit = 0, int $offset = 0): array {
throw new NotFoundException();
}
public function getById($id) {
throw new NotFoundException();
}
public function getFreeSpace() {
throw new NotFoundException();
}
public function isCreatable() {
if ($this->fileInfo) {
return parent::isCreatable();
} else {
throw new NotFoundException();
}
}
}
@@ -0,0 +1,516 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Stefan Weil <sw@weilnetz.de>
* @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 OC\Files\Node;
use OC\Files\FileInfo;
use OC\Files\Mount\Manager;
use OC\Files\Mount\MountPoint;
use OC\Files\Utils\PathHelper;
use OC\Files\View;
use OC\Hooks\PublicEmitter;
use OC\User\NoUserException;
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Events\Node\FilesystemTornDownEvent;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Node as INode;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
/**
* Class Root
*
* Hooks available in scope \OC\Files
* - preWrite(\OCP\Files\Node $node)
* - postWrite(\OCP\Files\Node $node)
* - preCreate(\OCP\Files\Node $node)
* - postCreate(\OCP\Files\Node $node)
* - preDelete(\OCP\Files\Node $node)
* - postDelete(\OCP\Files\Node $node)
* - preTouch(\OC\FilesP\Node $node, int $mtime)
* - postTouch(\OCP\Files\Node $node)
* - preCopy(\OCP\Files\Node $source, \OCP\Files\Node $target)
* - postCopy(\OCP\Files\Node $source, \OCP\Files\Node $target)
* - preRename(\OCP\Files\Node $source, \OCP\Files\Node $target)
* - postRename(\OCP\Files\Node $source, \OCP\Files\Node $target)
*
* @package OC\Files\Node
*/
class Root extends Folder implements IRootFolder {
private Manager $mountManager;
private PublicEmitter $emitter;
private ?IUser $user;
private CappedMemoryCache $userFolderCache;
private IUserMountCache $userMountCache;
private LoggerInterface $logger;
private IUserManager $userManager;
private IEventDispatcher $eventDispatcher;
/**
* @param Manager $manager
* @param View $view
* @param IUser|null $user
*/
public function __construct(
$manager,
$view,
$user,
IUserMountCache $userMountCache,
LoggerInterface $logger,
IUserManager $userManager,
IEventDispatcher $eventDispatcher
) {
parent::__construct($this, $view, '');
$this->mountManager = $manager;
$this->user = $user;
$this->emitter = new PublicEmitter();
$this->userFolderCache = new CappedMemoryCache();
$this->userMountCache = $userMountCache;
$this->logger = $logger;
$this->userManager = $userManager;
$eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
$this->userFolderCache = new CappedMemoryCache();
});
}
/**
* Get the user for which the filesystem is setup
*
* @return \OC\User\User
*/
public function getUser() {
return $this->user;
}
/**
* @param string $scope
* @param string $method
* @param callable $callback
*/
public function listen($scope, $method, callable $callback) {
$this->emitter->listen($scope, $method, $callback);
}
/**
* @param string $scope optional
* @param string $method optional
* @param callable $callback optional
*/
public function removeListener($scope = null, $method = null, callable $callback = null) {
$this->emitter->removeListener($scope, $method, $callback);
}
/**
* @param string $scope
* @param string $method
* @param Node[] $arguments
*/
public function emit($scope, $method, $arguments = []) {
$this->emitter->emit($scope, $method, $arguments);
}
/**
* @param \OC\Files\Storage\Storage $storage
* @param string $mountPoint
* @param array $arguments
*/
public function mount($storage, $mountPoint, $arguments = []) {
$mount = new MountPoint($storage, $mountPoint, $arguments);
$this->mountManager->addMount($mount);
}
public function getMount(string $mountPoint): IMountPoint {
return $this->mountManager->find($mountPoint);
}
/**
* @param string $mountPoint
* @return \OC\Files\Mount\MountPoint[]
*/
public function getMountsIn(string $mountPoint): array {
return $this->mountManager->findIn($mountPoint);
}
/**
* @param string $storageId
* @return \OC\Files\Mount\MountPoint[]
*/
public function getMountByStorageId($storageId) {
return $this->mountManager->findByStorageId($storageId);
}
/**
* @param int $numericId
* @return MountPoint[]
*/
public function getMountByNumericStorageId($numericId) {
return $this->mountManager->findByNumericId($numericId);
}
/**
* @param \OC\Files\Mount\MountPoint $mount
*/
public function unMount($mount) {
$this->mountManager->remove($mount);
}
/**
* @param string $path
* @return Node
* @throws \OCP\Files\NotPermittedException
* @throws \OCP\Files\NotFoundException
*/
public function get($path) {
$path = $this->normalizePath($path);
if ($this->isValidPath($path)) {
$fullPath = $this->getFullPath($path);
$fileInfo = $this->view->getFileInfo($fullPath, false);
if ($fileInfo) {
return $this->createNode($fullPath, $fileInfo, false);
} else {
throw new NotFoundException($path);
}
} else {
throw new NotPermittedException();
}
}
//most operations can't be done on the root
/**
* @param string $targetPath
* @return Node
* @throws \OCP\Files\NotPermittedException
*/
public function rename($targetPath) {
throw new NotPermittedException();
}
public function delete() {
throw new NotPermittedException();
}
/**
* @param string $targetPath
* @return Node
* @throws \OCP\Files\NotPermittedException
*/
public function copy($targetPath) {
throw new NotPermittedException();
}
/**
* @param int $mtime
* @throws \OCP\Files\NotPermittedException
*/
public function touch($mtime = null) {
throw new NotPermittedException();
}
/**
* @return \OC\Files\Storage\Storage
* @throws \OCP\Files\NotFoundException
*/
public function getStorage() {
throw new NotFoundException();
}
/**
* @return string
*/
public function getPath() {
return '/';
}
/**
* @return string
*/
public function getInternalPath() {
return '';
}
/**
* @return int
*/
public function getId() {
return 0;
}
/**
* @return array
*/
public function stat() {
return [];
}
/**
* @return int
*/
public function getMTime() {
return 0;
}
/**
* @param bool $includeMounts
* @return int|float
*/
public function getSize($includeMounts = true): int|float {
return 0;
}
/**
* @return string
*/
public function getEtag() {
return '';
}
/**
* @return int
*/
public function getPermissions() {
return \OCP\Constants::PERMISSION_CREATE;
}
/**
* @return bool
*/
public function isReadable() {
return false;
}
/**
* @return bool
*/
public function isUpdateable() {
return false;
}
/**
* @return bool
*/
public function isDeletable() {
return false;
}
/**
* @return bool
*/
public function isShareable() {
return false;
}
/**
* @throws \OCP\Files\NotFoundException
*/
public function getParent(): INode|IRootFolder {
throw new NotFoundException();
}
/**
* @return string
*/
public function getName() {
return '';
}
/**
* Returns a view to user's files folder
*
* @param string $userId user ID
* @return \OCP\Files\Folder
* @throws NoUserException
* @throws NotPermittedException
*/
public function getUserFolder($userId) {
$userObject = $this->userManager->get($userId);
if (is_null($userObject)) {
$e = new NoUserException('Backends provided no user object');
$this->logger->error(
sprintf(
'Backends provided no user object for %s',
$userId
),
[
'app' => 'files',
'exception' => $e,
]
);
throw $e;
}
$userId = $userObject->getUID();
if (!$this->userFolderCache->hasKey($userId)) {
if ($this->mountManager->getSetupManager()->isSetupComplete($userObject)) {
try {
$folder = $this->get('/' . $userId . '/files');
if (!$folder instanceof \OCP\Files\Folder) {
throw new \Exception("User folder for $userId exists as a file");
}
} catch (NotFoundException $e) {
if (!$this->nodeExists('/' . $userId)) {
$this->newFolder('/' . $userId);
}
$folder = $this->newFolder('/' . $userId . '/files');
}
} else {
$folder = new LazyUserFolder($this, $userObject, $this->mountManager);
}
$this->userFolderCache->set($userId, $folder);
}
return $this->userFolderCache->get($userId);
}
public function getUserMountCache() {
return $this->userMountCache;
}
/**
* @param int $id
* @return Node[]
*/
public function getByIdInPath(int $id, string $path): array {
$mountCache = $this->getUserMountCache();
if (strpos($path, '/', 1) > 0) {
[, $user] = explode('/', $path);
} else {
$user = null;
}
$mountsContainingFile = $mountCache->getMountsForFileId($id, $user);
// if the mount isn't in the cache yet, perform a setup first, then try again
if (count($mountsContainingFile) === 0) {
$this->mountManager->getSetupManager()->setupForPath($path, true);
$mountsContainingFile = $mountCache->getMountsForFileId($id, $user);
}
// when a user has access through the same storage through multiple paths
// (such as an external storage that is both mounted for a user and shared to the user)
// the mount cache will only hold a single entry for the storage
// this can lead to issues as the different ways the user has access to a storage can have different permissions
//
// so instead of using the cached entries directly, we instead filter the current mounts by the rootid of the cache entry
$mountRootIds = array_map(function ($mount) {
return $mount->getRootId();
}, $mountsContainingFile);
$mountRootPaths = array_map(function ($mount) {
return $mount->getRootInternalPath();
}, $mountsContainingFile);
$mountProviders = array_unique(array_map(function ($mount) {
return $mount->getMountProvider();
}, $mountsContainingFile));
$mountRoots = array_combine($mountRootIds, $mountRootPaths);
$mounts = $this->mountManager->getMountsByMountProvider($path, $mountProviders);
$mountsContainingFile = array_filter($mounts, function ($mount) use ($mountRoots) {
return isset($mountRoots[$mount->getStorageRootId()]);
});
if (count($mountsContainingFile) === 0) {
if ($user === $this->getAppDataDirectoryName()) {
$folder = $this->get($path);
if ($folder instanceof Folder) {
return $folder->getByIdInRootMount($id);
} else {
throw new \Exception("getByIdInPath with non folder");
}
}
return [];
}
$nodes = array_map(function (IMountPoint $mount) use ($id, $mountRoots) {
$rootInternalPath = $mountRoots[$mount->getStorageRootId()];
$cacheEntry = $mount->getStorage()->getCache()->get($id);
if (!$cacheEntry) {
return null;
}
// cache jails will hide the "true" internal path
$internalPath = ltrim($rootInternalPath . '/' . $cacheEntry->getPath(), '/');
$pathRelativeToMount = substr($internalPath, strlen($rootInternalPath));
$pathRelativeToMount = ltrim($pathRelativeToMount, '/');
$absolutePath = rtrim($mount->getMountPoint() . $pathRelativeToMount, '/');
return $this->createNode($absolutePath, new FileInfo(
$absolutePath, $mount->getStorage(), $cacheEntry->getPath(), $cacheEntry, $mount,
\OC::$server->getUserManager()->get($mount->getStorage()->getOwner($pathRelativeToMount))
));
}, $mountsContainingFile);
$nodes = array_filter($nodes);
$folders = array_filter($nodes, function (Node $node) use ($path) {
return PathHelper::getRelativePath($path, $node->getPath()) !== null;
});
usort($folders, function ($a, $b) {
return $b->getPath() <=> $a->getPath();
});
return $folders;
}
public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoint $mountPoint): INode {
$path = $cacheEntry->getPath();
$fullPath = $mountPoint->getMountPoint() . $path;
// todo: LazyNode?
$info = new FileInfo($fullPath, $mountPoint->getStorage(), $path, $cacheEntry, $mountPoint);
$parentPath = dirname($fullPath);
$parent = new LazyFolder($this, function () use ($parentPath) {
$parent = $this->get($parentPath);
if ($parent instanceof \OCP\Files\Folder) {
return $parent;
} else {
throw new \Exception("parent $parentPath is not a folder");
}
}, [
'path' => $parentPath,
]);
$isDir = $info->getType() === FileInfo::TYPE_FOLDER;
$view = new View('');
if ($isDir) {
return new Folder($this, $view, $path, $info, $parent);
} else {
return new File($this, $view, $path, $info, $parent);
}
}
}
@@ -0,0 +1,64 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\Notify;
use OCP\Files\Notify\IChange;
class Change implements IChange {
/** @var int */
private $type;
/** @var string */
private $path;
/**
* Change constructor.
*
* @param int $type
* @param string $path
*/
public function __construct($type, $path) {
$this->type = $type;
$this->path = $path;
}
/**
* Get the type of the change
*
* @return int IChange::ADDED, IChange::REMOVED, IChange::MODIFIED or IChange::RENAMED
*/
public function getType() {
return $this->type;
}
/**
* Get the path of the file that was changed relative to the root of the storage
*
* Note, for rename changes this path is the old path for the file
*
* @return mixed
*/
public function getPath() {
return $this->path;
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\Notify;
use OCP\Files\Notify\IRenameChange;
class RenameChange extends Change implements IRenameChange {
/** @var string */
private $targetPath;
/**
* Change constructor.
*
* @param int $type
* @param string $path
* @param string $targetPath
*/
public function __construct($type, $path, $targetPath) {
parent::__construct($type, $path);
$this->targetPath = $targetPath;
}
/**
* Get the new path of the renamed file relative to the storage root
*
* @return string
*/
public function getTargetPath() {
return $this->targetPath;
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020, Morris Jobke <hey@morrisjobke.de>
*
* @author Morris Jobke <hey@morrisjobke.de>
*
* @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 OC\Files\ObjectStore;
class AppdataPreviewObjectStoreStorage extends ObjectStoreStorage {
/** @var string */
private $internalId;
public function __construct($params) {
if (!isset($params['internal-id'])) {
throw new \Exception('missing id in parameters');
}
$this->internalId = (string)$params['internal-id'];
parent::__construct($params);
}
public function getId() {
return 'object::appdata::preview:' . $this->internalId;
}
}
@@ -0,0 +1,136 @@
<?php
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\ObjectStore;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
use MicrosoftAzure\Storage\Blob\Models\CreateBlockBlobOptions;
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
use OCP\Files\ObjectStore\IObjectStore;
class Azure implements IObjectStore {
/** @var string */
private $containerName;
/** @var string */
private $accountName;
/** @var string */
private $accountKey;
/** @var BlobRestProxy|null */
private $blobClient = null;
/** @var string|null */
private $endpoint = null;
/** @var bool */
private $autoCreate = false;
/**
* @param array $parameters
*/
public function __construct($parameters) {
$this->containerName = $parameters['container'];
$this->accountName = $parameters['account_name'];
$this->accountKey = $parameters['account_key'];
if (isset($parameters['endpoint'])) {
$this->endpoint = $parameters['endpoint'];
}
if (isset($parameters['autocreate'])) {
$this->autoCreate = $parameters['autocreate'];
}
}
/**
* @return BlobRestProxy
*/
private function getBlobClient() {
if (!$this->blobClient) {
$protocol = $this->endpoint ? substr($this->endpoint, 0, strpos($this->endpoint, ':')) : 'https';
$connectionString = "DefaultEndpointsProtocol=" . $protocol . ";AccountName=" . $this->accountName . ";AccountKey=" . $this->accountKey;
if ($this->endpoint) {
$connectionString .= ';BlobEndpoint=' . $this->endpoint;
}
$this->blobClient = BlobRestProxy::createBlobService($connectionString);
if ($this->autoCreate) {
try {
$this->blobClient->createContainer($this->containerName);
} catch (ServiceException $e) {
if ($e->getCode() === 409) {
// already exists
} else {
throw $e;
}
}
}
}
return $this->blobClient;
}
/**
* @return string the container or bucket name where objects are stored
*/
public function getStorageId() {
return 'azure::blob::' . $this->containerName;
}
/**
* @param string $urn the unified resource name used to identify the object
* @return resource stream with the read data
* @throws \Exception when something goes wrong, message will be logged
*/
public function readObject($urn) {
$blob = $this->getBlobClient()->getBlob($this->containerName, $urn);
return $blob->getContentStream();
}
public function writeObject($urn, $stream, string $mimetype = null) {
$options = new CreateBlockBlobOptions();
if ($mimetype) {
$options->setContentType($mimetype);
}
$this->getBlobClient()->createBlockBlob($this->containerName, $urn, $stream, $options);
}
/**
* @param string $urn the unified resource name used to identify the object
* @return void
* @throws \Exception when something goes wrong, message will be logged
*/
public function deleteObject($urn) {
$this->getBlobClient()->deleteBlob($this->containerName, $urn);
}
public function objectExists($urn) {
try {
$this->getBlobClient()->getBlobMetadata($this->containerName, $urn);
return true;
} catch (ServiceException $e) {
if ($e->getCode() === 404) {
return false;
} else {
throw $e;
}
}
}
public function copyObject($from, $to) {
$this->getBlobClient()->copyBlob($this->containerName, $to, $this->containerName, $from);
}
}
@@ -0,0 +1,68 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\ObjectStore;
use OC\User\User;
use OCP\IUser;
class HomeObjectStoreStorage extends ObjectStoreStorage implements \OCP\Files\IHomeStorage {
/**
* The home user storage requires a user object to create a unique storage id
* @param array $params
*/
public function __construct($params) {
if (! isset($params['user']) || ! $params['user'] instanceof User) {
throw new \Exception('missing user object in parameters');
}
$this->user = $params['user'];
parent::__construct($params);
}
public function getId() {
return 'object::user:' . $this->user->getUID();
}
/**
* get the owner of a path
*
* @param string $path The path to get the owner
* @return false|string uid
*/
public function getOwner($path) {
if (is_object($this->user)) {
return $this->user->getUID();
}
return false;
}
/**
* @param string $path, optional
* @return \OC\User\User
*/
public function getUser($path = null): IUser {
return $this->user;
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\ObjectStore;
use OCP\IConfig;
use OCP\IUser;
/**
* Class Mapper
*
* @package OC\Files\ObjectStore
*
* Map a user to a bucket.
*/
class Mapper {
/** @var IUser */
private $user;
/** @var IConfig */
private $config;
/**
* Mapper constructor.
*
* @param IUser $user
* @param IConfig $config
*/
public function __construct(IUser $user, IConfig $config) {
$this->user = $user;
$this->config = $config;
}
/**
* @param int $numBuckets
* @return string
*/
public function getBucket($numBuckets = 64) {
// Get the bucket config and shift if provided.
// Allow us to prevent writing in old filled buckets
$config = $this->config->getSystemValue('objectstore_multibucket');
$minBucket = is_array($config) && isset($config['arguments']['min_bucket'])
? (int)$config['arguments']['min_bucket']
: 0;
$hash = md5($this->user->getUID());
$num = hexdec(substr($hash, 0, 4));
return (string)(($num % ($numBuckets - $minBucket)) + $minBucket);
}
}
@@ -0,0 +1,98 @@
<?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 Roeland Jago Douma <roeland@famdouma.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 OC\Files\ObjectStore;
use OC\Files\Cache\Scanner;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\FileInfo;
class ObjectStoreScanner extends Scanner {
public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
return [];
}
public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
return [];
}
protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float $oldSize, &$etagChanged = false) {
return 0;
}
public function backgroundScan() {
$lastPath = null;
// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
// we sort by path DESC to ensure that contents of a folder are handled before the parent folder
while (($path = $this->getIncomplete()) !== false && $path !== $lastPath) {
$this->runBackgroundScanJob(function () use ($path) {
$item = $this->cache->get($path);
if ($item && $item->getMimeType() !== FileInfo::MIMETYPE_FOLDER) {
$fh = $this->storage->fopen($path, 'r');
if ($fh) {
$stat = fstat($fh);
if ($stat['size']) {
$this->cache->update($item->getId(), ['size' => $stat['size']]);
}
}
}
}, $path);
// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
// to make this possible
$lastPath = $path;
}
}
/**
* Unlike the default Cache::getIncomplete this one sorts by path.
*
* This is needed since self::backgroundScan doesn't fix child entries when running on a parent folder.
* By sorting by path we ensure that we encounter the child entries first.
*
* @return false|string
* @throws \OCP\DB\Exception
*/
private function getIncomplete() {
$query = $this->connection->getQueryBuilder();
$query->select('path')
->from('filecache')
->where($query->expr()->eq('storage', $query->createNamedParameter($this->cache->getNumericStorageId(), IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->orderBy('path', 'DESC')
->setMaxResults(1);
$result = $query->executeQuery();
$path = $result->fetchOne();
$result->closeCursor();
if ($path === false) {
return false;
}
// Make sure Oracle does not continue with null for empty strings
return (string)$path;
}
}
@@ -0,0 +1,741 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Marcel Klehr <mklehr@gmx.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
*
* @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 OC\Files\ObjectStore;
use Aws\S3\Exception\S3Exception;
use Aws\S3\Exception\S3MultipartUploadException;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\CountWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Files\Cache\Cache;
use OC\Files\Cache\CacheEntry;
use OC\Files\Storage\PolyFill\CopyDirectory;
use OCP\Files\Cache\ICache;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\FileInfo;
use OCP\Files\GenericFileException;
use OCP\Files\NotFoundException;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload;
use OCP\Files\Storage\IChunkedFileWrite;
use OCP\Files\Storage\IStorage;
class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite {
use CopyDirectory;
/**
* @var \OCP\Files\ObjectStore\IObjectStore $objectStore
*/
protected $objectStore;
/**
* @var string $id
*/
protected $id;
/**
* @var \OC\User\User $user
*/
protected $user;
private $objectPrefix = 'urn:oid:';
private $logger;
private bool $handleCopiesAsOwned;
/** @var bool */
protected $validateWrites = true;
public function __construct($params) {
if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
$this->objectStore = $params['objectstore'];
} else {
throw new \Exception('missing IObjectStore instance');
}
if (isset($params['storageid'])) {
$this->id = 'object::store:' . $params['storageid'];
} else {
$this->id = 'object::store:' . $this->objectStore->getStorageId();
}
if (isset($params['objectPrefix'])) {
$this->objectPrefix = $params['objectPrefix'];
}
if (isset($params['validateWrites'])) {
$this->validateWrites = (bool)$params['validateWrites'];
}
$this->handleCopiesAsOwned = (bool)($params['handleCopiesAsOwned'] ?? false);
$this->logger = \OC::$server->getLogger();
}
public function mkdir($path, bool $force = false) {
$path = $this->normalizePath($path);
if (!$force && $this->file_exists($path)) {
$this->logger->warning("Tried to create an object store folder that already exists: $path");
return false;
}
$mTime = time();
$data = [
'mimetype' => 'httpd/unix-directory',
'size' => 0,
'mtime' => $mTime,
'storage_mtime' => $mTime,
'permissions' => \OCP\Constants::PERMISSION_ALL,
];
if ($path === '') {
//create root on the fly
$data['etag'] = $this->getETag('');
$this->getCache()->put('', $data);
return true;
} else {
// if parent does not exist, create it
$parent = $this->normalizePath(dirname($path));
$parentType = $this->filetype($parent);
if ($parentType === false) {
if (!$this->mkdir($parent)) {
// something went wrong
$this->logger->warning("Parent folder ($parent) doesn't exist and couldn't be created");
return false;
}
} elseif ($parentType === 'file') {
// parent is a file
$this->logger->warning("Parent ($parent) is a file");
return false;
}
// finally create the new dir
$mTime = time(); // update mtime
$data['mtime'] = $mTime;
$data['storage_mtime'] = $mTime;
$data['etag'] = $this->getETag($path);
$this->getCache()->put($path, $data);
return true;
}
}
/**
* @param string $path
* @return string
*/
private function normalizePath($path) {
$path = trim($path, '/');
//FIXME why do we sometimes get a path like 'files//username'?
$path = str_replace('//', '/', $path);
// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
if (!$path || $path === '.') {
$path = '';
}
return $path;
}
/**
* Object Stores use a NoopScanner because metadata is directly stored in
* the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
* @return \OC\Files\ObjectStore\ObjectStoreScanner
*/
public function getScanner($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($this->scanner)) {
$this->scanner = new ObjectStoreScanner($storage);
}
return $this->scanner;
}
public function getId() {
return $this->id;
}
public function rmdir($path) {
$path = $this->normalizePath($path);
$entry = $this->getCache()->get($path);
if (!$entry || $entry->getMimeType() !== ICacheEntry::DIRECTORY_MIMETYPE) {
return false;
}
return $this->rmObjects($entry);
}
private function rmObjects(ICacheEntry $entry): bool {
$children = $this->getCache()->getFolderContentsById($entry->getId());
foreach ($children as $child) {
if ($child->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
if (!$this->rmObjects($child)) {
return false;
}
} else {
if (!$this->rmObject($child)) {
return false;
}
}
}
$this->getCache()->remove($entry->getPath());
return true;
}
public function unlink($path) {
$path = $this->normalizePath($path);
$entry = $this->getCache()->get($path);
if ($entry instanceof ICacheEntry) {
if ($entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
return $this->rmObjects($entry);
} else {
return $this->rmObject($entry);
}
}
return false;
}
public function rmObject(ICacheEntry $entry): bool {
try {
$this->objectStore->deleteObject($this->getURN($entry->getId()));
} catch (\Exception $ex) {
if ($ex->getCode() !== 404) {
$this->logger->logException($ex, [
'app' => 'objectstore',
'message' => 'Could not delete object ' . $this->getURN($entry->getId()) . ' for ' . $entry->getPath(),
]);
return false;
}
//removing from cache is ok as it does not exist in the objectstore anyway
}
$this->getCache()->remove($entry->getPath());
return true;
}
public function stat($path) {
$path = $this->normalizePath($path);
$cacheEntry = $this->getCache()->get($path);
if ($cacheEntry instanceof CacheEntry) {
return $cacheEntry->getData();
} else {
if ($path === '') {
$this->mkdir('', true);
$cacheEntry = $this->getCache()->get($path);
if ($cacheEntry instanceof CacheEntry) {
return $cacheEntry->getData();
}
}
return false;
}
}
public function getPermissions($path) {
$stat = $this->stat($path);
if (is_array($stat) && isset($stat['permissions'])) {
return $stat['permissions'];
}
return parent::getPermissions($path);
}
/**
* Override this method if you need a different unique resource identifier for your object storage implementation.
* The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
* You may need a mapping table to store your URN if it cannot be generated from the fileid.
*
* @param int $fileId the fileid
* @return null|string the unified resource name used to identify the object
*/
public function getURN($fileId) {
if (is_numeric($fileId)) {
return $this->objectPrefix . $fileId;
}
return null;
}
public function opendir($path) {
$path = $this->normalizePath($path);
try {
$files = [];
$folderContents = $this->getCache()->getFolderContents($path);
foreach ($folderContents as $file) {
$files[] = $file['name'];
}
return IteratorDirectory::wrap($files);
} catch (\Exception $e) {
$this->logger->logException($e);
return false;
}
}
public function filetype($path) {
$path = $this->normalizePath($path);
$stat = $this->stat($path);
if ($stat) {
if ($stat['mimetype'] === 'httpd/unix-directory') {
return 'dir';
}
return 'file';
} else {
return false;
}
}
public function fopen($path, $mode) {
$path = $this->normalizePath($path);
if (strrpos($path, '.') !== false) {
$ext = substr($path, strrpos($path, '.'));
} else {
$ext = '';
}
switch ($mode) {
case 'r':
case 'rb':
$stat = $this->stat($path);
if (is_array($stat)) {
$filesize = $stat['size'] ?? 0;
// Reading 0 sized files is a waste of time
if ($filesize === 0) {
return fopen('php://memory', $mode);
}
try {
$handle = $this->objectStore->readObject($this->getURN($stat['fileid']));
if ($handle === false) {
return false; // keep backward compatibility
}
$streamStat = fstat($handle);
$actualSize = $streamStat['size'] ?? -1;
if ($actualSize > -1 && $actualSize !== $filesize) {
$this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]);
}
return $handle;
} catch (NotFoundException $e) {
$this->logger->logException($e, [
'app' => 'objectstore',
'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
]);
throw $e;
} catch (\Exception $ex) {
$this->logger->logException($ex, [
'app' => 'objectstore',
'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
]);
return false;
}
} else {
return false;
}
// no break
case 'w':
case 'wb':
case 'w+':
case 'wb+':
$dirName = dirname($path);
$parentExists = $this->is_dir($dirName);
if (!$parentExists) {
return false;
}
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
$handle = fopen($tmpFile, $mode);
return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
$this->writeBack($tmpFile, $path);
unlink($tmpFile);
});
case 'a':
case 'ab':
case 'r+':
case 'a+':
case 'x':
case 'x+':
case 'c':
case 'c+':
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
if ($this->file_exists($path)) {
$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);
unlink($tmpFile);
});
}
return false;
}
public function file_exists($path) {
$path = $this->normalizePath($path);
return (bool)$this->stat($path);
}
public function rename($source, $target) {
$source = $this->normalizePath($source);
$target = $this->normalizePath($target);
$this->remove($target);
$this->getCache()->move($source, $target);
$this->touch(dirname($target));
return true;
}
public function getMimeType($path) {
$path = $this->normalizePath($path);
return parent::getMimeType($path);
}
public function touch($path, $mtime = null) {
if (is_null($mtime)) {
$mtime = time();
}
$path = $this->normalizePath($path);
$dirName = dirname($path);
$parentExists = $this->is_dir($dirName);
if (!$parentExists) {
return false;
}
$stat = $this->stat($path);
if (is_array($stat)) {
// update existing mtime in db
$stat['mtime'] = $mtime;
$this->getCache()->update($stat['fileid'], $stat);
} else {
try {
//create a empty file, need to have at least on char to make it
// work with all object storage implementations
$this->file_put_contents($path, ' ');
$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
$stat = [
'etag' => $this->getETag($path),
'mimetype' => $mimeType,
'size' => 0,
'mtime' => $mtime,
'storage_mtime' => $mtime,
'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
];
$this->getCache()->put($path, $stat);
} catch (\Exception $ex) {
$this->logger->logException($ex, [
'app' => 'objectstore',
'message' => 'Could not create object for ' . $path,
]);
throw $ex;
}
}
return true;
}
public function writeBack($tmpFile, $path) {
$size = filesize($tmpFile);
$this->writeStream($path, fopen($tmpFile, 'r'), $size);
}
/**
* external changes are not supported, exclusive access to the object storage is assumed
*
* @param string $path
* @param int $time
* @return false
*/
public function hasUpdated($path, $time) {
return false;
}
public function needsPartFile() {
return false;
}
public function file_put_contents($path, $data) {
$handle = $this->fopen($path, 'w+');
if (!$handle) {
return false;
}
$result = fwrite($handle, $data);
fclose($handle);
return $result;
}
public function writeStream(string $path, $stream, int $size = null): int {
$stat = $this->stat($path);
if (empty($stat)) {
// create new file
$stat = [
'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
];
}
// update stat with new data
$mTime = time();
$stat['size'] = (int)$size;
$stat['mtime'] = $mTime;
$stat['storage_mtime'] = $mTime;
$mimetypeDetector = \OC::$server->getMimeTypeDetector();
$mimetype = $mimetypeDetector->detectPath($path);
$stat['mimetype'] = $mimetype;
$stat['etag'] = $this->getETag($path);
$stat['checksum'] = '';
$exists = $this->getCache()->inCache($path);
$uploadPath = $exists ? $path : $path . '.part';
if ($exists) {
$fileId = $stat['fileid'];
} else {
$fileId = $this->getCache()->put($uploadPath, $stat);
}
$urn = $this->getURN($fileId);
try {
//upload to object storage
if ($size === null) {
$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
$this->getCache()->update($fileId, [
'size' => $writtenSize,
]);
$size = $writtenSize;
});
$this->objectStore->writeObject($urn, $countStream, $mimetype);
if (is_resource($countStream)) {
fclose($countStream);
}
$stat['size'] = $size;
} else {
$this->objectStore->writeObject($urn, $stream, $mimetype);
if (is_resource($stream)) {
fclose($stream);
}
}
} catch (\Exception $ex) {
if (!$exists) {
/*
* Only remove the entry if we are dealing with a new file.
* Else people lose access to existing files
*/
$this->getCache()->remove($uploadPath);
$this->logger->logException($ex, [
'app' => 'objectstore',
'message' => 'Could not create object ' . $urn . ' for ' . $path,
]);
} else {
$this->logger->logException($ex, [
'app' => 'objectstore',
'message' => 'Could not update object ' . $urn . ' for ' . $path,
]);
}
throw $ex; // make this bubble up
}
if ($exists) {
// Always update the unencrypted size, for encryption the Encryption wrapper will update this afterwards anyways
$stat['unencrypted_size'] = $stat['size'];
$this->getCache()->update($fileId, $stat);
} else {
if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
$this->getCache()->move($uploadPath, $path);
} else {
$this->getCache()->remove($uploadPath);
throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
}
}
return $size;
}
public function getObjectStore(): IObjectStore {
return $this->objectStore;
}
public function copyFromStorage(
IStorage $sourceStorage,
$sourceInternalPath,
$targetInternalPath,
$preserveMtime = false
) {
if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
/** @var ObjectStoreStorage $sourceStorage */
if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
/** @var CacheEntry $sourceEntry */
$sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
$sourceEntryData = $sourceEntry->getData();
// $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
// user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
// unjailed storage.
if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
$sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
}
$this->copyInner($sourceStorage->getCache(), $sourceEntry, $targetInternalPath);
return true;
}
}
return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
}
public function copy($source, $target) {
$source = $this->normalizePath($source);
$target = $this->normalizePath($target);
$cache = $this->getCache();
$sourceEntry = $cache->get($source);
if (!$sourceEntry) {
throw new NotFoundException('Source object not found');
}
$this->copyInner($cache, $sourceEntry, $target);
return true;
}
private function copyInner(ICache $sourceCache, ICacheEntry $sourceEntry, string $to) {
$cache = $this->getCache();
if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
if ($cache->inCache($to)) {
$cache->remove($to);
}
$this->mkdir($to);
foreach ($sourceCache->getFolderContentsById($sourceEntry->getId()) as $child) {
$this->copyInner($sourceCache, $child, $to . '/' . $child->getName());
}
} else {
$this->copyFile($sourceEntry, $to);
}
}
private function copyFile(ICacheEntry $sourceEntry, string $to) {
$cache = $this->getCache();
$sourceUrn = $this->getURN($sourceEntry->getId());
if (!$cache instanceof Cache) {
throw new \Exception("Invalid source cache for object store copy");
}
$targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
$targetUrn = $this->getURN($targetId);
try {
$this->objectStore->copyObject($sourceUrn, $targetUrn);
if ($this->handleCopiesAsOwned) {
// Copied the file thus we gain all permissions as we are the owner now ! warning while this aligns with local storage it should not be used and instead fix local storage !
$cache->update($targetId, ['permissions' => \OCP\Constants::PERMISSION_ALL]);
}
} catch (\Exception $e) {
$cache->remove($to);
throw $e;
}
}
public function startChunkedWrite(string $targetPath): string {
if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
throw new GenericFileException('Object store does not support multipart upload');
}
$cacheEntry = $this->getCache()->get($targetPath);
$urn = $this->getURN($cacheEntry->getId());
return $this->objectStore->initiateMultipartUpload($urn);
}
/**
*
* @throws GenericFileException
*/
public function putChunkedWritePart(
string $targetPath,
string $writeToken,
string $chunkId,
$data,
$size = null
): ?array {
if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
throw new GenericFileException('Object store does not support multipart upload');
}
$cacheEntry = $this->getCache()->get($targetPath);
$urn = $this->getURN($cacheEntry->getId());
$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
$parts[$chunkId] = [
'PartNumber' => $chunkId,
'ETag' => trim($result->get('ETag'), '"'),
];
return $parts[$chunkId];
}
public function completeChunkedWrite(string $targetPath, string $writeToken): int {
if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
throw new GenericFileException('Object store does not support multipart upload');
}
$cacheEntry = $this->getCache()->get($targetPath);
$urn = $this->getURN($cacheEntry->getId());
$parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
$sortedParts = array_values($parts);
sort($sortedParts);
try {
$size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
$stat = $this->stat($targetPath);
$mtime = time();
if (is_array($stat)) {
$stat['size'] = $size;
$stat['mtime'] = $mtime;
$stat['mimetype'] = $this->getMimeType($targetPath);
$this->getCache()->update($stat['fileid'], $stat);
}
} catch (S3MultipartUploadException|S3Exception $e) {
$this->objectStore->abortMultipartUpload($urn, $writeToken);
$this->logger->logException($e, [
'app' => 'objectstore',
'message' => 'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
]);
throw new GenericFileException('Could not write chunked file');
}
return $size;
}
public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
throw new GenericFileException('Object store does not support multipart upload');
}
$cacheEntry = $this->getCache()->get($targetPath);
$urn = $this->getURN($cacheEntry->getId());
$this->objectStore->abortMultipartUpload($urn, $writeToken);
}
}
@@ -0,0 +1,113 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\ObjectStore;
use Aws\Result;
use Exception;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload;
class S3 implements IObjectStore, IObjectStoreMultiPartUpload {
use S3ConnectionTrait;
use S3ObjectTrait;
public function __construct($parameters) {
$parameters['primary_storage'] = true;
$this->parseParams($parameters);
}
/**
* @return string the container or bucket name where objects are stored
* @since 7.0.0
*/
public function getStorageId() {
return $this->id;
}
public function initiateMultipartUpload(string $urn): string {
$upload = $this->getConnection()->createMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $urn,
] + $this->getSSECParameters());
$uploadId = $upload->get('UploadId');
if ($uploadId === null) {
throw new Exception('No upload id returned');
}
return (string)$uploadId;
}
public function uploadMultipartPart(string $urn, string $uploadId, int $partId, $stream, $size): Result {
return $this->getConnection()->uploadPart([
'Body' => $stream,
'Bucket' => $this->bucket,
'Key' => $urn,
'ContentLength' => $size,
'PartNumber' => $partId,
'UploadId' => $uploadId,
] + $this->getSSECParameters());
}
public function getMultipartUploads(string $urn, string $uploadId): array {
$parts = [];
$isTruncated = true;
$partNumberMarker = 0;
while ($isTruncated) {
$result = $this->getConnection()->listParts([
'Bucket' => $this->bucket,
'Key' => $urn,
'UploadId' => $uploadId,
'MaxParts' => 1000,
'PartNumberMarker' => $partNumberMarker
] + $this->getSSECParameters());
$parts = array_merge($parts, $result->get('Parts') ?? []);
$isTruncated = $result->get('IsTruncated');
$partNumberMarker = $result->get('NextPartNumberMarker');
}
return $parts;
}
public function completeMultipartUpload(string $urn, string $uploadId, array $result): int {
$this->getConnection()->completeMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $urn,
'UploadId' => $uploadId,
'MultipartUpload' => ['Parts' => $result],
] + $this->getSSECParameters());
$stat = $this->getConnection()->headObject([
'Bucket' => $this->bucket,
'Key' => $urn,
] + $this->getSSECParameters());
return (int)$stat->get('ContentLength');
}
public function abortMultipartUpload($urn, $uploadId): void {
$this->getConnection()->abortMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $urn,
'UploadId' => $uploadId
]);
}
}
@@ -0,0 +1,271 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Florent <florent@coppint.com>
* @author James Letendre <James.Letendre@gmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author S. Cat <33800996+sparrowjack63@users.noreply.github.com>
* @author Stephen Cuppett <steve@cuppett.com>
* @author Jasper Weyne <jasperweyne@gmail.com>
*
* @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 OC\Files\ObjectStore;
use Aws\ClientResolver;
use Aws\Credentials\CredentialProvider;
use Aws\Credentials\Credentials;
use Aws\Exception\CredentialsException;
use Aws\S3\Exception\S3Exception;
use Aws\S3\S3Client;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\RejectedPromise;
use OCP\ICertificateManager;
use Psr\Log\LoggerInterface;
trait S3ConnectionTrait {
/** @var array */
protected $params;
/** @var S3Client */
protected $connection;
/** @var string */
protected $id;
/** @var string */
protected $bucket;
/** @var int */
protected $timeout;
/** @var string */
protected $proxy;
/** @var string */
protected $storageClass;
/** @var int */
protected $uploadPartSize;
/** @var int */
private $putSizeLimit;
/** @var int */
private $copySizeLimit;
private bool $useMultipartCopy = true;
protected $test;
protected function parseParams($params) {
if (empty($params['bucket'])) {
throw new \Exception("Bucket has to be configured.");
}
$this->id = 'amazon::' . $params['bucket'];
$this->test = isset($params['test']);
$this->bucket = $params['bucket'];
$this->proxy = $params['proxy'] ?? false;
$this->timeout = $params['timeout'] ?? 15;
$this->storageClass = !empty($params['storageClass']) ? $params['storageClass'] : 'STANDARD';
$this->uploadPartSize = $params['uploadPartSize'] ?? 524288000;
$this->putSizeLimit = $params['putSizeLimit'] ?? 104857600;
$this->copySizeLimit = $params['copySizeLimit'] ?? 5242880000;
$this->useMultipartCopy = (bool)($params['useMultipartCopy'] ?? true);
$params['region'] = empty($params['region']) ? 'eu-west-1' : $params['region'];
$params['hostname'] = empty($params['hostname']) ? 's3.' . $params['region'] . '.amazonaws.com' : $params['hostname'];
if (!isset($params['port']) || $params['port'] === '') {
$params['port'] = (isset($params['use_ssl']) && $params['use_ssl'] === false) ? 80 : 443;
}
$params['verify_bucket_exists'] = $params['verify_bucket_exists'] ?? true;
$this->params = $params;
}
public function getBucket() {
return $this->bucket;
}
public function getProxy() {
return $this->proxy;
}
/**
* Returns the connection
*
* @return S3Client connected client
* @throws \Exception if connection could not be made
*/
public function getConnection() {
if (!is_null($this->connection)) {
return $this->connection;
}
$scheme = (isset($this->params['use_ssl']) && $this->params['use_ssl'] === false) ? 'http' : 'https';
$base_url = $scheme . '://' . $this->params['hostname'] . ':' . $this->params['port'] . '/';
// Adding explicit credential provider to the beginning chain.
// Including default credential provider (skipping AWS shared config files).
$provider = CredentialProvider::memoize(
CredentialProvider::chain(
$this->paramCredentialProvider(),
CredentialProvider::defaultProvider(['use_aws_shared_config_files' => false])
)
);
$options = [
'version' => $this->params['version'] ?? 'latest',
'credentials' => $provider,
'endpoint' => $base_url,
'region' => $this->params['region'],
'use_path_style_endpoint' => isset($this->params['use_path_style']) ? $this->params['use_path_style'] : false,
'signature_provider' => \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider()),
'csm' => false,
'use_arn_region' => false,
'http' => ['verify' => $this->getCertificateBundlePath()],
'use_aws_shared_config_files' => false,
];
if ($this->getProxy()) {
$options['http']['proxy'] = $this->getProxy();
}
if (isset($this->params['legacy_auth']) && $this->params['legacy_auth']) {
$options['signature_version'] = 'v2';
}
$this->connection = new S3Client($options);
if (!$this->connection::isBucketDnsCompatible($this->bucket)) {
$logger = \OC::$server->get(LoggerInterface::class);
$logger->debug('Bucket "' . $this->bucket . '" This bucket name is not dns compatible, it may contain invalid characters.',
['app' => 'objectstore']);
}
if ($this->params['verify_bucket_exists'] && !$this->connection->doesBucketExist($this->bucket)) {
$logger = \OC::$server->get(LoggerInterface::class);
try {
$logger->info('Bucket "' . $this->bucket . '" does not exist - creating it.', ['app' => 'objectstore']);
if (!$this->connection::isBucketDnsCompatible($this->bucket)) {
throw new \Exception("The bucket will not be created because the name is not dns compatible, please correct it: " . $this->bucket);
}
$this->connection->createBucket(['Bucket' => $this->bucket]);
$this->testTimeout();
} catch (S3Exception $e) {
$logger->debug('Invalid remote storage.', [
'exception' => $e,
'app' => 'objectstore',
]);
throw new \Exception('Creation of bucket "' . $this->bucket . '" failed. ' . $e->getMessage());
}
}
// google cloud's s3 compatibility doesn't like the EncodingType parameter
if (strpos($base_url, 'storage.googleapis.com')) {
$this->connection->getHandlerList()->remove('s3.auto_encode');
}
return $this->connection;
}
/**
* when running the tests wait to let the buckets catch up
*/
private function testTimeout() {
if ($this->test) {
sleep($this->timeout);
}
}
public static function legacySignatureProvider($version, $service, $region) {
switch ($version) {
case 'v2':
case 's3':
return new S3Signature();
default:
return null;
}
}
/**
* This function creates a credential provider based on user parameter file
*/
protected function paramCredentialProvider(): callable {
return function () {
$key = empty($this->params['key']) ? null : $this->params['key'];
$secret = empty($this->params['secret']) ? null : $this->params['secret'];
if ($key && $secret) {
return Promise\promise_for(
new Credentials($key, $secret)
);
}
$msg = 'Could not find parameters set for credentials in config file.';
return new RejectedPromise(new CredentialsException($msg));
};
}
protected function getCertificateBundlePath(): ?string {
if ((int)($this->params['use_nextcloud_bundle'] ?? "0")) {
// since we store the certificate bundles on the primary storage, we can't get the bundle while setting up the primary storage
if (!isset($this->params['primary_storage'])) {
/** @var ICertificateManager $certManager */
$certManager = \OC::$server->get(ICertificateManager::class);
return $certManager->getAbsoluteBundlePath();
} else {
return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
}
} else {
return null;
}
}
protected function getSSECKey(): ?string {
if (isset($this->params['sse_c_key'])) {
return $this->params['sse_c_key'];
}
return null;
}
protected function getSSECParameters(bool $copy = false): array {
$key = $this->getSSECKey();
if ($key === null) {
return [];
}
$rawKey = base64_decode($key);
if ($copy) {
return [
'CopySourceSSECustomerAlgorithm' => 'AES256',
'CopySourceSSECustomerKey' => $rawKey,
'CopySourceSSECustomerKeyMD5' => md5($rawKey, true)
];
}
return [
'SSECustomerAlgorithm' => 'AES256',
'SSECustomerKey' => $rawKey,
'SSECustomerKeyMD5' => md5($rawKey, true)
];
}
}
@@ -0,0 +1,219 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Florent <florent@coppint.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\ObjectStore;
use Aws\S3\Exception\S3MultipartUploadException;
use Aws\S3\MultipartCopy;
use Aws\S3\MultipartUploader;
use Aws\S3\S3Client;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Utils;
use OC\Files\Stream\SeekableHttpStream;
use Psr\Http\Message\StreamInterface;
trait S3ObjectTrait {
/**
* Returns the connection
*
* @return S3Client connected client
* @throws \Exception if connection could not be made
*/
abstract protected function getConnection();
abstract protected function getCertificateBundlePath(): ?string;
abstract protected function getSSECParameters(bool $copy = false): array;
/**
* @param string $urn the unified resource name used to identify the object
*
* @return resource stream with the read data
* @throws \Exception when something goes wrong, message will be logged
* @since 7.0.0
*/
public function readObject($urn) {
$fh = SeekableHttpStream::open(function ($range) use ($urn) {
$command = $this->getConnection()->getCommand('GetObject', [
'Bucket' => $this->bucket,
'Key' => $urn,
'Range' => 'bytes=' . $range,
] + $this->getSSECParameters());
$request = \Aws\serialize($command);
$headers = [];
foreach ($request->getHeaders() as $key => $values) {
foreach ($values as $value) {
$headers[] = "$key: $value";
}
}
$opts = [
'http' => [
'protocol_version' => $request->getProtocolVersion(),
'header' => $headers,
]
];
$bundle = $this->getCertificateBundlePath();
if ($bundle) {
$opts['ssl'] = [
'cafile' => $bundle
];
}
if ($this->getProxy()) {
$opts['http']['proxy'] = $this->getProxy();
$opts['http']['request_fulluri'] = true;
}
$context = stream_context_create($opts);
return fopen($request->getUri(), 'r', false, $context);
});
if (!$fh) {
throw new \Exception("Failed to read object $urn");
}
return $fh;
}
/**
* Single object put helper
*
* @param string $urn the unified resource name used to identify the object
* @param StreamInterface $stream stream with the data to write
* @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0
* @throws \Exception when something goes wrong, message will be logged
*/
protected function writeSingle(string $urn, StreamInterface $stream, string $mimetype = null): void {
$this->getConnection()->putObject([
'Bucket' => $this->bucket,
'Key' => $urn,
'Body' => $stream,
'ACL' => 'private',
'ContentType' => $mimetype,
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters());
}
/**
* Multipart upload helper that tries to avoid orphaned fragments in S3
*
* @param string $urn the unified resource name used to identify the object
* @param StreamInterface $stream stream with the data to write
* @param string|null $mimetype the mimetype to set for the remove object
* @throws \Exception when something goes wrong, message will be logged
*/
protected function writeMultiPart(string $urn, StreamInterface $stream, string $mimetype = null): void {
$uploader = new MultipartUploader($this->getConnection(), $stream, [
'bucket' => $this->bucket,
'key' => $urn,
'part_size' => $this->uploadPartSize,
'params' => [
'ContentType' => $mimetype,
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters(),
]);
try {
$uploader->upload();
} catch (S3MultipartUploadException $e) {
// if anything goes wrong with multipart, make sure that you don´t poison and
// slow down s3 bucket with orphaned fragments
$uploadInfo = $e->getState()->getId();
if ($e->getState()->isInitiated() && (array_key_exists('UploadId', $uploadInfo))) {
$this->getConnection()->abortMultipartUpload($uploadInfo);
}
throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway("Error while uploading to S3 bucket", 0, $e);
}
}
/**
* @param string $urn the unified resource name used to identify the object
* @param resource $stream stream with the data to write
* @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0
* @throws \Exception when something goes wrong, message will be logged
* @since 7.0.0
*/
public function writeObject($urn, $stream, string $mimetype = null) {
$psrStream = Utils::streamFor($stream);
// ($psrStream->isSeekable() && $psrStream->getSize() !== null) evaluates to true for a On-Seekable stream
// so the optimisation does not apply
$buffer = new Psr7\Stream(fopen("php://memory", 'rwb+'));
Utils::copyToStream($psrStream, $buffer, $this->putSizeLimit);
$buffer->seek(0);
if ($buffer->getSize() < $this->putSizeLimit) {
// buffer is fully seekable, so use it directly for the small upload
$this->writeSingle($urn, $buffer, $mimetype);
} else {
$loadStream = new Psr7\AppendStream([$buffer, $psrStream]);
$this->writeMultiPart($urn, $loadStream, $mimetype);
}
}
/**
* @param string $urn the unified resource name used to identify the object
* @return void
* @throws \Exception when something goes wrong, message will be logged
* @since 7.0.0
*/
public function deleteObject($urn) {
$this->getConnection()->deleteObject([
'Bucket' => $this->bucket,
'Key' => $urn,
]);
}
public function objectExists($urn) {
return $this->getConnection()->doesObjectExist($this->bucket, $urn, $this->getSSECParameters());
}
public function copyObject($from, $to, array $options = []) {
$sourceMetadata = $this->getConnection()->headObject([
'Bucket' => $this->getBucket(),
'Key' => $from,
] + $this->getSSECParameters());
$size = (int)($sourceMetadata->get('Size') ?? $sourceMetadata->get('ContentLength'));
if ($this->useMultipartCopy && $size > $this->copySizeLimit) {
$copy = new MultipartCopy($this->getConnection(), [
"source_bucket" => $this->getBucket(),
"source_key" => $from
], array_merge([
"bucket" => $this->getBucket(),
"key" => $to,
"acl" => "private",
"params" => $this->getSSECParameters() + $this->getSSECParameters(true),
"source_metadata" => $sourceMetadata
], $options));
$copy->copy();
} else {
$this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to, 'private', array_merge([
'params' => $this->getSSECParameters() + $this->getSSECParameters(true)
], $options));
}
}
}
@@ -0,0 +1,222 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @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 OC\Files\ObjectStore;
use Aws\Credentials\CredentialsInterface;
use Aws\S3\S3Client;
use Aws\S3\S3UriParser;
use Aws\Signature\SignatureInterface;
use GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
/**
* Legacy Amazon S3 signature implementation
*/
class S3Signature implements SignatureInterface {
/** @var array Query string values that must be signed */
private $signableQueryString = [
'acl', 'cors', 'delete', 'lifecycle', 'location', 'logging',
'notification', 'partNumber', 'policy', 'requestPayment',
'response-cache-control', 'response-content-disposition',
'response-content-encoding', 'response-content-language',
'response-content-type', 'response-expires', 'restore', 'tagging',
'torrent', 'uploadId', 'uploads', 'versionId', 'versioning',
'versions', 'website'
];
/** @var array Sorted headers that must be signed */
private $signableHeaders = ['Content-MD5', 'Content-Type'];
/** @var \Aws\S3\S3UriParser S3 URI parser */
private $parser;
public function __construct() {
$this->parser = new S3UriParser();
// Ensure that the signable query string parameters are sorted
sort($this->signableQueryString);
}
public function signRequest(
RequestInterface $request,
CredentialsInterface $credentials
) {
$request = $this->prepareRequest($request, $credentials);
$stringToSign = $this->createCanonicalizedString($request);
$auth = 'AWS '
. $credentials->getAccessKeyId() . ':'
. $this->signString($stringToSign, $credentials);
return $request->withHeader('Authorization', $auth);
}
public function presign(
RequestInterface $request,
CredentialsInterface $credentials,
$expires,
array $options = []
) {
$query = [];
// URL encoding already occurs in the URI template expansion. Undo that
// and encode using the same encoding as GET object, PUT object, etc.
$uri = $request->getUri();
$path = S3Client::encodeKey(rawurldecode($uri->getPath()));
$request = $request->withUri($uri->withPath($path));
// Make sure to handle temporary credentials
if ($token = $credentials->getSecurityToken()) {
$request = $request->withHeader('X-Amz-Security-Token', $token);
$query['X-Amz-Security-Token'] = $token;
}
if ($expires instanceof \DateTime) {
$expires = $expires->getTimestamp();
} elseif (!is_numeric($expires)) {
$expires = strtotime($expires);
}
// Set query params required for pre-signed URLs
$query['AWSAccessKeyId'] = $credentials->getAccessKeyId();
$query['Expires'] = $expires;
$query['Signature'] = $this->signString(
$this->createCanonicalizedString($request, $expires),
$credentials
);
// Move X-Amz-* headers to the query string
foreach ($request->getHeaders() as $name => $header) {
$name = strtolower($name);
if (str_starts_with($name, 'x-amz-')) {
$query[$name] = implode(',', $header);
}
}
$queryString = http_build_query($query, null, '&', PHP_QUERY_RFC3986);
return $request->withUri($request->getUri()->withQuery($queryString));
}
/**
* @param RequestInterface $request
* @param CredentialsInterface $creds
*
* @return RequestInterface
*/
private function prepareRequest(
RequestInterface $request,
CredentialsInterface $creds
) {
$modify = [
'remove_headers' => ['X-Amz-Date'],
'set_headers' => ['Date' => gmdate(\DateTimeInterface::RFC2822)]
];
// Add the security token header if one is being used by the credentials
if ($token = $creds->getSecurityToken()) {
$modify['set_headers']['X-Amz-Security-Token'] = $token;
}
return Psr7\Utils::modifyRequest($request, $modify);
}
private function signString($string, CredentialsInterface $credentials) {
return base64_encode(
hash_hmac('sha1', $string, $credentials->getSecretKey(), true)
);
}
private function createCanonicalizedString(
RequestInterface $request,
$expires = null
) {
$buffer = $request->getMethod() . "\n";
// Add the interesting headers
foreach ($this->signableHeaders as $header) {
$buffer .= $request->getHeaderLine($header) . "\n";
}
$date = $expires ?: $request->getHeaderLine('date');
$buffer .= "{$date}\n"
. $this->createCanonicalizedAmzHeaders($request)
. $this->createCanonicalizedResource($request);
return $buffer;
}
private function createCanonicalizedAmzHeaders(RequestInterface $request) {
$headers = [];
foreach ($request->getHeaders() as $name => $header) {
$name = strtolower($name);
if (str_starts_with($name, 'x-amz-')) {
$value = implode(',', $header);
if (strlen($value) > 0) {
$headers[$name] = $name . ':' . $value;
}
}
}
if (!$headers) {
return '';
}
ksort($headers);
return implode("\n", $headers) . "\n";
}
private function createCanonicalizedResource(RequestInterface $request) {
$data = $this->parser->parse($request->getUri());
$buffer = '/';
if ($data['bucket']) {
$buffer .= $data['bucket'];
if (!empty($data['key']) || !$data['path_style']) {
$buffer .= '/' . $data['key'];
}
}
// Add sub resource parameters if present.
$query = $request->getUri()->getQuery();
if ($query) {
$params = Psr7\Query::parse($query);
$first = true;
foreach ($this->signableQueryString as $key) {
if (array_key_exists($key, $params)) {
$value = $params[$key];
$buffer .= $first ? '?' : '&';
$first = false;
$buffer .= $key;
// Don't add values for empty sub-resources
if (strlen($value)) {
$buffer .= "={$value}";
}
}
}
}
return $buffer;
}
}
@@ -0,0 +1,94 @@
<?php
/**
* @copyright Copyright (c) 2016 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 OC\Files\ObjectStore;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\Files\Storage\IStorage;
use function is_resource;
/**
* Object store that wraps a storage backend, mostly for testing purposes
*/
class StorageObjectStore implements IObjectStore {
/** @var IStorage */
private $storage;
/**
* @param IStorage $storage
*/
public function __construct(IStorage $storage) {
$this->storage = $storage;
}
/**
* @return string the container or bucket name where objects are stored
* @since 7.0.0
*/
public function getStorageId() {
$this->storage->getId();
}
/**
* @param string $urn the unified resource name used to identify the object
* @return resource stream with the read data
* @throws \Exception when something goes wrong, message will be logged
* @since 7.0.0
*/
public function readObject($urn) {
$handle = $this->storage->fopen($urn, 'r');
if (is_resource($handle)) {
return $handle;
}
throw new \Exception();
}
public function writeObject($urn, $stream, string $mimetype = null) {
$handle = $this->storage->fopen($urn, 'w');
if ($handle) {
stream_copy_to_stream($stream, $handle);
fclose($handle);
} else {
throw new \Exception();
}
}
/**
* @param string $urn the unified resource name used to identify the object
* @return void
* @throws \Exception when something goes wrong, message will be logged
* @since 7.0.0
*/
public function deleteObject($urn) {
$this->storage->unlink($urn);
}
public function objectExists($urn) {
return $this->storage->file_exists($urn);
}
public function copyObject($from, $to) {
$this->storage->copy($from, $to);
}
}
@@ -0,0 +1,155 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Adrian Brzezinski <adrian.brzezinski@eo.pl>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\ObjectStore;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Psr7\Utils;
use Icewind\Streams\RetryWrapper;
use OCP\Files\NotFoundException;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\Files\StorageAuthException;
use Psr\Log\LoggerInterface;
const SWIFT_SEGMENT_SIZE = 1073741824; // 1GB
class Swift implements IObjectStore {
/**
* @var array
*/
private $params;
/** @var SwiftFactory */
private $swiftFactory;
public function __construct($params, SwiftFactory $connectionFactory = null) {
$this->swiftFactory = $connectionFactory ?: new SwiftFactory(
\OC::$server->getMemCacheFactory()->createDistributed('swift::'),
$params,
\OC::$server->get(LoggerInterface::class)
);
$this->params = $params;
}
/**
* @return \OpenStack\ObjectStore\v1\Models\Container
* @throws StorageAuthException
* @throws \OCP\Files\StorageNotAvailableException
*/
private function getContainer() {
return $this->swiftFactory->getContainer();
}
/**
* @return string the container name where objects are stored
*/
public function getStorageId() {
if (isset($this->params['bucket'])) {
return $this->params['bucket'];
}
return $this->params['container'];
}
public function writeObject($urn, $stream, string $mimetype = null) {
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile('swiftwrite');
file_put_contents($tmpFile, $stream);
$handle = fopen($tmpFile, 'rb');
if (filesize($tmpFile) < SWIFT_SEGMENT_SIZE) {
$this->getContainer()->createObject([
'name' => $urn,
'stream' => Utils::streamFor($handle),
'contentType' => $mimetype,
]);
} else {
$this->getContainer()->createLargeObject([
'name' => $urn,
'stream' => Utils::streamFor($handle),
'segmentSize' => SWIFT_SEGMENT_SIZE,
'contentType' => $mimetype,
]);
}
}
/**
* @param string $urn the unified resource name used to identify the object
* @return resource stream with the read data
* @throws \Exception from openstack or GuzzleHttp libs when something goes wrong
* @throws NotFoundException if file does not exist
*/
public function readObject($urn) {
try {
$publicUri = $this->getContainer()->getObject($urn)->getPublicUri();
$tokenId = $this->swiftFactory->getCachedTokenId();
$response = (new Client())->request('GET', $publicUri,
[
'stream' => true,
'headers' => [
'X-Auth-Token' => $tokenId,
'Cache-Control' => 'no-cache',
],
]
);
} catch (BadResponseException $e) {
if ($e->getResponse() && $e->getResponse()->getStatusCode() === 404) {
throw new NotFoundException("object $urn not found in object store");
} else {
throw $e;
}
}
return RetryWrapper::wrap($response->getBody()->detach());
}
/**
* @param string $urn Unified Resource Name
* @return void
* @throws \Exception from openstack lib when something goes wrong
*/
public function deleteObject($urn) {
$this->getContainer()->getObject($urn)->delete();
}
/**
* @return void
* @throws \Exception from openstack lib when something goes wrong
*/
public function deleteContainer() {
$this->getContainer()->delete();
}
public function objectExists($urn) {
return $this->getContainer()->objectExists($urn);
}
public function copyObject($from, $to) {
$this->getContainer()->getObject($from)->copy([
'destination' => $this->getContainer()->name . '/' . $to
]);
}
}
@@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @author Adrian Brzezinski <adrian.brzezinski@eo.pl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julien Lutran <julien.lutran@corp.ovh.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Volker <skydiablo@gmx.net>
* @author William Pain <pain.william@gmail.com>
*
* @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 OC\Files\ObjectStore;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use OCP\Files\StorageAuthException;
use OCP\Files\StorageNotAvailableException;
use OCP\ICache;
use OpenStack\Common\Auth\Token;
use OpenStack\Common\Error\BadResponseError;
use OpenStack\Common\Transport\Utils as TransportUtils;
use OpenStack\Identity\v2\Models\Catalog;
use OpenStack\Identity\v2\Service as IdentityV2Service;
use OpenStack\Identity\v3\Service as IdentityV3Service;
use OpenStack\ObjectStore\v1\Models\Container;
use OpenStack\OpenStack;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerInterface;
class SwiftFactory {
private $cache;
private $params;
/** @var Container|null */
private $container = null;
private LoggerInterface $logger;
public const DEFAULT_OPTIONS = [
'autocreate' => false,
'urlType' => 'publicURL',
'catalogName' => 'swift',
'catalogType' => 'object-store'
];
public function __construct(ICache $cache, array $params, LoggerInterface $logger) {
$this->cache = $cache;
$this->params = $params;
$this->logger = $logger;
}
/**
* Gets currently cached token id
*
* @return string
* @throws StorageAuthException
*/
public function getCachedTokenId() {
if (!isset($this->params['cachedToken'])) {
throw new StorageAuthException('Unauthenticated ObjectStore connection');
}
// Is it V2 token?
if (isset($this->params['cachedToken']['token'])) {
return $this->params['cachedToken']['token']['id'];
}
return $this->params['cachedToken']['id'];
}
private function getCachedToken(string $cacheKey) {
$cachedTokenString = $this->cache->get($cacheKey . '/token');
if ($cachedTokenString) {
return json_decode($cachedTokenString, true);
} else {
return null;
}
}
private function cacheToken(Token $token, string $serviceUrl, string $cacheKey) {
if ($token instanceof \OpenStack\Identity\v3\Models\Token) {
// for v3 the catalog is cached as part of the token, so no need to cache $serviceUrl separately
$value = $token->export();
} else {
/** @var \OpenStack\Identity\v2\Models\Token $token */
$value = [
'serviceUrl' => $serviceUrl,
'token' => [
'issued_at' => $token->issuedAt->format('c'),
'expires' => $token->expires->format('c'),
'id' => $token->id,
'tenant' => $token->tenant
]
];
}
$this->params['cachedToken'] = $value;
$this->cache->set($cacheKey . '/token', json_encode($value));
}
/**
* @return OpenStack
* @throws StorageAuthException
*/
private function getClient() {
if (isset($this->params['bucket'])) {
$this->params['container'] = $this->params['bucket'];
}
if (!isset($this->params['container'])) {
$this->params['container'] = 'nextcloud';
}
if (isset($this->params['user']) && is_array($this->params['user'])) {
$userName = $this->params['user']['name'];
} else {
if (!isset($this->params['username']) && isset($this->params['user'])) {
$this->params['username'] = $this->params['user'];
}
$userName = $this->params['username'];
}
if (!isset($this->params['tenantName']) && isset($this->params['tenant'])) {
$this->params['tenantName'] = $this->params['tenant'];
}
if (isset($this->params['domain'])) {
$this->params['scope']['project']['name'] = $this->params['tenant'];
$this->params['scope']['project']['domain']['name'] = $this->params['domain'];
}
$this->params = array_merge(self::DEFAULT_OPTIONS, $this->params);
$cacheKey = $userName . '@' . $this->params['url'] . '/' . $this->params['container'];
$token = $this->getCachedToken($cacheKey);
$this->params['cachedToken'] = $token;
$httpClient = new Client([
'base_uri' => TransportUtils::normalizeUrl($this->params['url']),
'handler' => HandlerStack::create()
]);
if (isset($this->params['user']) && is_array($this->params['user']) && isset($this->params['user']['name'])) {
if (!isset($this->params['scope'])) {
throw new StorageAuthException('Scope has to be defined for V3 requests');
}
return $this->auth(IdentityV3Service::factory($httpClient), $cacheKey);
} else {
return $this->auth(SwiftV2CachingAuthService::factory($httpClient), $cacheKey);
}
}
/**
* @param IdentityV2Service|IdentityV3Service $authService
* @param string $cacheKey
* @return OpenStack
* @throws StorageAuthException
*/
private function auth($authService, string $cacheKey) {
$this->params['identityService'] = $authService;
$this->params['authUrl'] = $this->params['url'];
$cachedToken = $this->params['cachedToken'];
$hasValidCachedToken = false;
if (\is_array($cachedToken)) {
if ($authService instanceof IdentityV3Service) {
$token = $authService->generateTokenFromCache($cachedToken);
if (\is_null($token->catalog)) {
$this->logger->warning('Invalid cached token for swift, no catalog set: ' . json_encode($cachedToken));
} elseif ($token->hasExpired()) {
$this->logger->debug('Cached token for swift expired');
} else {
$hasValidCachedToken = true;
}
} else {
try {
/** @var \OpenStack\Identity\v2\Models\Token $token */
$token = $authService->model(\OpenStack\Identity\v2\Models\Token::class, $cachedToken['token']);
$now = new \DateTimeImmutable("now");
if ($token->expires > $now) {
$hasValidCachedToken = true;
$this->params['v2cachedToken'] = $token;
$this->params['v2serviceUrl'] = $cachedToken['serviceUrl'];
} else {
$this->logger->debug('Cached token for swift expired');
}
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
}
}
if (!$hasValidCachedToken) {
unset($this->params['cachedToken']);
try {
[$token, $serviceUrl] = $authService->authenticate($this->params);
$this->cacheToken($token, $serviceUrl, $cacheKey);
} catch (ConnectException $e) {
throw new StorageAuthException('Failed to connect to keystone, verify the keystone url', $e);
} catch (ClientException $e) {
$statusCode = $e->getResponse()->getStatusCode();
if ($statusCode === 404) {
throw new StorageAuthException('Keystone not found, verify the keystone url', $e);
} elseif ($statusCode === 412) {
throw new StorageAuthException('Precondition failed, verify the keystone url', $e);
} elseif ($statusCode === 401) {
throw new StorageAuthException('Authentication failed, verify the username, password and possibly tenant', $e);
} else {
throw new StorageAuthException('Unknown error', $e);
}
} catch (RequestException $e) {
throw new StorageAuthException('Connection reset while connecting to keystone, verify the keystone url', $e);
}
}
$client = new OpenStack($this->params);
return $client;
}
/**
* @return \OpenStack\ObjectStore\v1\Models\Container
* @throws StorageAuthException
* @throws StorageNotAvailableException
*/
public function getContainer() {
if (is_null($this->container)) {
$this->container = $this->createContainer();
}
return $this->container;
}
/**
* @return \OpenStack\ObjectStore\v1\Models\Container
* @throws StorageAuthException
* @throws StorageNotAvailableException
*/
private function createContainer() {
$client = $this->getClient();
$objectStoreService = $client->objectStoreV1();
$autoCreate = isset($this->params['autocreate']) && $this->params['autocreate'] === true;
try {
$container = $objectStoreService->getContainer($this->params['container']);
if ($autoCreate) {
$container->getMetadata();
}
return $container;
} catch (BadResponseError $ex) {
// if the container does not exist and autocreate is true try to create the container on the fly
if ($ex->getResponse()->getStatusCode() === 404 && $autoCreate) {
return $objectStoreService->createContainer([
'name' => $this->params['container']
]);
} else {
throw new StorageNotAvailableException('Invalid response while trying to get container info', StorageNotAvailableException::STATUS_ERROR, $ex);
}
} catch (ConnectException $e) {
/** @var RequestInterface $request */
$request = $e->getRequest();
$host = $request->getUri()->getHost() . ':' . $request->getUri()->getPort();
$this->logger->error("Can't connect to object storage server at $host", ['exception' => $e]);
throw new StorageNotAvailableException("Can't connect to object storage server at $host", StorageNotAvailableException::STATUS_ERROR, $e);
}
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\ObjectStore;
use OpenStack\Identity\v2\Service;
class SwiftV2CachingAuthService extends Service {
public function authenticate(array $options = []): array {
if (!empty($options['v2cachedToken'])) {
return [$options['v2cachedToken'], $options['v2serviceUrl']];
} else {
return parent::authenticate($options);
}
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @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 OC\Files\Search\QueryOptimizer;
use OC\Files\Search\SearchComparison;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
class PathPrefixOptimizer extends QueryOptimizerStep {
private bool $useHashEq = true;
public function inspectOperator(ISearchOperator $operator): void {
// normally any `path = "$path"` search filter would be generated as an `path_hash = md5($path)` sql query
// since the `path_hash` sql column usually provides much faster querying that selecting on the `path` sql column
//
// however, if we're already doing a filter on the `path` column in the form of `path LIKE "$prefix/%"`
// generating a `path = "$prefix"` sql query lets the database handle use the same column for both expressions and potentially use the same index
//
// If there is any operator in the query that matches this pattern, we change all `path = "$path"` instances to not the `path_hash` equality,
// otherwise mariadb has a tendency of ignoring the path_prefix index
if ($this->useHashEq && $this->isPathPrefixOperator($operator)) {
$this->useHashEq = false;
}
parent::inspectOperator($operator);
}
public function processOperator(ISearchOperator &$operator) {
if (!$this->useHashEq && $operator instanceof ISearchComparison && !$operator->getExtra() && $operator->getField() === 'path' && $operator->getType() === ISearchComparison::COMPARE_EQUAL) {
$operator->setQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, false);
}
parent::processOperator($operator);
}
private function isPathPrefixOperator(ISearchOperator $operator): bool {
if ($operator instanceof ISearchBinaryOperator && $operator->getType() === ISearchBinaryOperator::OPERATOR_OR && count($operator->getArguments()) == 2) {
$a = $operator->getArguments()[0];
$b = $operator->getArguments()[1];
if ($this->operatorPairIsPathPrefix($a, $b) || $this->operatorPairIsPathPrefix($b, $a)) {
return true;
}
}
return false;
}
private function operatorPairIsPathPrefix(ISearchOperator $like, ISearchOperator $equal): bool {
return (
$like instanceof ISearchComparison && $equal instanceof ISearchComparison &&
!$like->getExtra() && !$equal->getExtra() && $like->getField() === 'path' && $equal->getField() === 'path' &&
$like->getType() === ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE && $equal->getType() === ISearchComparison::COMPARE_EQUAL
&& $like->getValue() === SearchComparison::escapeLikeParameter($equal->getValue()) . '/%'
);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 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 OC\Files\Search\QueryOptimizer;
use OCP\Files\Search\ISearchOperator;
class QueryOptimizer {
/** @var QueryOptimizerStep[] */
private $steps = [];
public function __construct(
PathPrefixOptimizer $pathPrefixOptimizer
) {
$this->steps = [
$pathPrefixOptimizer
];
}
public function processOperator(ISearchOperator $operator) {
foreach ($this->steps as $step) {
$step->inspectOperator($operator);
}
foreach ($this->steps as $step) {
$step->processOperator($operator);
}
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 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 OC\Files\Search\QueryOptimizer;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchOperator;
class QueryOptimizerStep {
/**
* Allow optimizer steps to inspect the entire query before starting processing
*
* @param ISearchOperator $operator
* @return void
*/
public function inspectOperator(ISearchOperator $operator): void {
if ($operator instanceof ISearchBinaryOperator) {
foreach ($operator->getArguments() as $argument) {
$this->inspectOperator($argument);
}
}
}
/**
* Allow optimizer steps to modify query operators
*
* @param ISearchOperator $operator
* @return void
*/
public function processOperator(ISearchOperator &$operator) {
if ($operator instanceof ISearchBinaryOperator) {
foreach ($operator->getArguments() as $argument) {
$this->processOperator($argument);
}
}
}
}
@@ -0,0 +1,67 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\Search;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchOperator;
class SearchBinaryOperator implements ISearchBinaryOperator {
/** @var string */
private $type;
/** @var ISearchOperator[] */
private $arguments;
private $hints = [];
/**
* SearchBinaryOperator constructor.
*
* @param string $type
* @param ISearchOperator[] $arguments
*/
public function __construct($type, array $arguments) {
$this->type = $type;
$this->arguments = $arguments;
}
/**
* @return string
*/
public function getType() {
return $this->type;
}
/**
* @return ISearchOperator[]
*/
public function getArguments() {
return $this->arguments;
}
public function getQueryHint(string $name, $default) {
return $this->hints[$name] ?? $default;
}
public function setQueryHint(string $name, $value): void {
$this->hints[$name] = $value;
}
}
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @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 OC\Files\Search;
use OCP\Files\Search\ISearchComparison;
class SearchComparison implements ISearchComparison {
private array $hints = [];
public function __construct(
private string $type,
private string $field,
private \DateTime|int|string|bool $value,
private string $extra = ''
) {
}
/**
* @return string
*/
public function getType(): string {
return $this->type;
}
/**
* @return string
*/
public function getField(): string {
return $this->field;
}
/**
* @return \DateTime|int|string|bool
*/
public function getValue(): string|int|bool|\DateTime {
return $this->value;
}
/**
* @return string
* @since 28.0.0
*/
public function getExtra(): string {
return $this->extra;
}
public function getQueryHint(string $name, $default) {
return $this->hints[$name] ?? $default;
}
public function setQueryHint(string $name, $value): void {
$this->hints[$name] = $value;
}
public static function escapeLikeParameter(string $param): string {
return addcslashes($param, '\\_%');
}
}
@@ -0,0 +1,82 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @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 OC\Files\Search;
use OCP\Files\FileInfo;
use OCP\Files\Search\ISearchOrder;
class SearchOrder implements ISearchOrder {
public function __construct(
private string $direction,
private string $field,
private string $extra = ''
) {
}
/**
* @return string
*/
public function getDirection(): string {
return $this->direction;
}
/**
* @return string
*/
public function getField(): string {
return $this->field;
}
/**
* @return string
* @since 28.0.0
*/
public function getExtra(): string {
return $this->extra;
}
public function sortFileInfo(FileInfo $a, FileInfo $b): int {
$cmp = $this->sortFileInfoNoDirection($a, $b);
return $cmp * ($this->direction === ISearchOrder::DIRECTION_ASCENDING ? 1 : -1);
}
private function sortFileInfoNoDirection(FileInfo $a, FileInfo $b): int {
switch ($this->field) {
case 'name':
return $a->getName() <=> $b->getName();
case 'mimetype':
return $a->getMimetype() <=> $b->getMimetype();
case 'mtime':
return $a->getMtime() <=> $b->getMtime();
case 'size':
return $a->getSize() <=> $b->getSize();
case 'fileid':
return $a->getId() <=> $b->getId();
case 'permissions':
return $a->getPermissions() <=> $b->getPermissions();
default:
return 0;
}
}
}
@@ -0,0 +1,107 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\Search;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchOrder;
use OCP\Files\Search\ISearchQuery;
use OCP\IUser;
class SearchQuery implements ISearchQuery {
/** @var ISearchOperator */
private $searchOperation;
/** @var integer */
private $limit;
/** @var integer */
private $offset;
/** @var ISearchOrder[] */
private $order;
/** @var ?IUser */
private $user;
private $limitToHome;
/**
* SearchQuery constructor.
*
* @param ISearchOperator $searchOperation
* @param int $limit
* @param int $offset
* @param array $order
* @param ?IUser $user
* @param bool $limitToHome
*/
public function __construct(
ISearchOperator $searchOperation,
int $limit,
int $offset,
array $order,
?IUser $user = null,
bool $limitToHome = false
) {
$this->searchOperation = $searchOperation;
$this->limit = $limit;
$this->offset = $offset;
$this->order = $order;
$this->user = $user;
$this->limitToHome = $limitToHome;
}
/**
* @return ISearchOperator
*/
public function getSearchOperation() {
return $this->searchOperation;
}
/**
* @return int
*/
public function getLimit() {
return $this->limit;
}
/**
* @return int
*/
public function getOffset() {
return $this->offset;
}
/**
* @return ISearchOrder[]
*/
public function getOrder() {
return $this->order;
}
/**
* @return ?IUser
*/
public function getUser() {
return $this->user;
}
public function limitToHome(): bool {
return $this->limitToHome;
}
}
@@ -0,0 +1,601 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 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 OC\Files;
use OC\Files\Config\MountProviderCollection;
use OC\Files\Mount\HomeMountPoint;
use OC\Files\Mount\MountPoint;
use OC\Files\Storage\Common;
use OC\Files\Storage\Home;
use OC\Files\Storage\Storage;
use OC\Files\Storage\Wrapper\Availability;
use OC\Files\Storage\Wrapper\Encoding;
use OC\Files\Storage\Wrapper\PermissionsMask;
use OC\Files\Storage\Wrapper\Quota;
use OC\Lockdown\Filesystem\NullStorage;
use OC\Share\Share;
use OC\Share20\ShareDisableChecker;
use OC_App;
use OC_Hook;
use OC_Util;
use OCA\Files_External\Config\ConfigAdapter;
use OCA\Files_Sharing\External\Mount;
use OCA\Files_Sharing\ISharedMountPoint;
use OCA\Files_Sharing\SharedMount;
use OCP\Constants;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Config\IMountProvider;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Events\InvalidateMountCacheEvent;
use OCP\Files\Events\Node\FilesystemTornDownEvent;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\IStorage;
use OCP\Group\Events\UserAddedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Lockdown\ILockdownManager;
use OCP\Share\Events\ShareCreatedEvent;
use Psr\Log\LoggerInterface;
class SetupManager {
private bool $rootSetup = false;
// List of users for which at least one mount is setup
private array $setupUsers = [];
// List of users for which all mounts are setup
private array $setupUsersComplete = [];
/** @var array<string, string[]> */
private array $setupUserMountProviders = [];
private ICache $cache;
private bool $listeningForProviders;
private array $fullSetupRequired = [];
private bool $setupBuiltinWrappersDone = false;
public function __construct(
private IEventLogger $eventLogger,
private MountProviderCollection $mountProviderCollection,
private IMountManager $mountManager,
private IUserManager $userManager,
private IEventDispatcher $eventDispatcher,
private IUserMountCache $userMountCache,
private ILockdownManager $lockdownManager,
private IUserSession $userSession,
ICacheFactory $cacheFactory,
private LoggerInterface $logger,
private IConfig $config,
private ShareDisableChecker $shareDisableChecker,
) {
$this->cache = $cacheFactory->createDistributed('setupmanager::');
$this->listeningForProviders = false;
$this->setupListeners();
}
private function isSetupStarted(IUser $user): bool {
return in_array($user->getUID(), $this->setupUsers, true);
}
public function isSetupComplete(IUser $user): bool {
return in_array($user->getUID(), $this->setupUsersComplete, true);
}
private function setupBuiltinWrappers() {
if ($this->setupBuiltinWrappersDone) {
return;
}
$this->setupBuiltinWrappersDone = true;
// load all filesystem apps before, so no setup-hook gets lost
OC_App::loadApps(['filesystem']);
$prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
if ($mount->getOptions() && $storage->instanceOfStorage(Common::class)) {
$storage->setMountOptions($mount->getOptions());
}
return $storage;
});
$reSharingEnabled = Share::isResharingAllowed();
$user = $this->userSession->getUser();
$sharingEnabledForUser = $user ? !$this->shareDisableChecker->sharingDisabledForUser($user->getUID()) : true;
Filesystem::addStorageWrapper(
'sharing_mask',
function ($mountPoint, IStorage $storage, IMountPoint $mount) use ($reSharingEnabled, $sharingEnabledForUser) {
$sharingEnabledForMount = $mount->getOption('enable_sharing', true);
$isShared = $mount instanceof ISharedMountPoint;
if (!$sharingEnabledForMount || !$sharingEnabledForUser || (!$reSharingEnabled && $isShared)) {
return new PermissionsMask([
'storage' => $storage,
'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE,
]);
}
return $storage;
}
);
// install storage availability wrapper, before most other wrappers
Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
$externalMount = $mount instanceof ConfigAdapter || $mount instanceof Mount;
if ($externalMount && !$storage->isLocal()) {
return new Availability(['storage' => $storage]);
}
return $storage;
});
Filesystem::addStorageWrapper('oc_encoding', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
if ($mount->getOption('encoding_compatibility', false) && !$mount instanceof SharedMount) {
return new Encoding(['storage' => $storage]);
}
return $storage;
});
$quotaIncludeExternal = $this->config->getSystemValue('quota_include_external_storage', false);
Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage, IMountPoint $mount) use ($quotaIncludeExternal) {
// set up quota for home storages, even for other users
// which can happen when using sharing
if ($mount instanceof HomeMountPoint) {
$user = $mount->getUser();
return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) {
return OC_Util::getUserQuota($user);
}, 'root' => 'files', 'include_external_storage' => $quotaIncludeExternal]);
}
return $storage;
});
Filesystem::addStorageWrapper('readonly', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
/*
* Do not allow any operations that modify the storage
*/
if ($mount->getOption('readonly', false)) {
return new PermissionsMask([
'storage' => $storage,
'mask' => Constants::PERMISSION_ALL & ~(
Constants::PERMISSION_UPDATE |
Constants::PERMISSION_CREATE |
Constants::PERMISSION_DELETE
),
]);
}
return $storage;
});
Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
}
/**
* Setup the full filesystem for the specified user
*/
public function setupForUser(IUser $user): void {
if ($this->isSetupComplete($user)) {
return;
}
$this->setupUsersComplete[] = $user->getUID();
$this->eventLogger->start('fs:setup:user:full', 'Setup full filesystem for user');
if (!isset($this->setupUserMountProviders[$user->getUID()])) {
$this->setupUserMountProviders[$user->getUID()] = [];
}
$previouslySetupProviders = $this->setupUserMountProviders[$user->getUID()];
$this->setupForUserWith($user, function () use ($user) {
$this->mountProviderCollection->addMountForUser($user, $this->mountManager, function (
IMountProvider $provider
) use ($user) {
return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]);
});
});
$this->afterUserFullySetup($user, $previouslySetupProviders);
$this->eventLogger->end('fs:setup:user:full');
}
/**
* part of the user setup that is run only once per user
*/
private function oneTimeUserSetup(IUser $user) {
if ($this->isSetupStarted($user)) {
return;
}
$this->setupUsers[] = $user->getUID();
$this->setupRoot();
$this->eventLogger->start('fs:setup:user:onetime', 'Onetime filesystem for user');
$this->setupBuiltinWrappers();
$prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]);
Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
$userDir = '/' . $user->getUID() . '/files';
Filesystem::initInternal($userDir);
if ($this->lockdownManager->canAccessFilesystem()) {
$this->eventLogger->start('fs:setup:user:home', 'Setup home filesystem for user');
// home mounts are handled separate since we need to ensure this is mounted before we call the other mount providers
$homeMount = $this->mountProviderCollection->getHomeMountForUser($user);
$this->mountManager->addMount($homeMount);
if ($homeMount->getStorageRootId() === -1) {
$this->eventLogger->start('fs:setup:user:home:scan', 'Scan home filesystem for user');
$homeMount->getStorage()->mkdir('');
$homeMount->getStorage()->getScanner()->scan('');
$this->eventLogger->end('fs:setup:user:home:scan');
}
$this->eventLogger->end('fs:setup:user:home');
} else {
$this->mountManager->addMount(new MountPoint(
new NullStorage([]),
'/' . $user->getUID()
));
$this->mountManager->addMount(new MountPoint(
new NullStorage([]),
'/' . $user->getUID() . '/files'
));
$this->setupUsersComplete[] = $user->getUID();
}
$this->listenForNewMountProviders();
$this->eventLogger->end('fs:setup:user:onetime');
}
/**
* Final housekeeping after a user has been fully setup
*/
private function afterUserFullySetup(IUser $user, array $previouslySetupProviders): void {
$this->eventLogger->start('fs:setup:user:full:post', 'Housekeeping after user is setup');
$userRoot = '/' . $user->getUID() . '/';
$mounts = $this->mountManager->getAll();
$mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) {
return str_starts_with($mount->getMountPoint(), $userRoot);
});
$allProviders = array_map(function (IMountProvider $provider) {
return get_class($provider);
}, $this->mountProviderCollection->getProviders());
$newProviders = array_diff($allProviders, $previouslySetupProviders);
$mounts = array_filter($mounts, function (IMountPoint $mount) use ($previouslySetupProviders) {
return !in_array($mount->getMountProvider(), $previouslySetupProviders);
});
$this->userMountCache->registerMounts($user, $mounts, $newProviders);
$cacheDuration = $this->config->getSystemValueInt('fs_mount_cache_duration', 5 * 60);
if ($cacheDuration > 0) {
$this->cache->set($user->getUID(), true, $cacheDuration);
$this->fullSetupRequired[$user->getUID()] = false;
}
$this->eventLogger->end('fs:setup:user:full:post');
}
/**
* @param IUser $user
* @param IMountPoint $mounts
* @return void
* @throws \OCP\HintException
* @throws \OC\ServerNotAvailableException
*/
private function setupForUserWith(IUser $user, callable $mountCallback): void {
$this->oneTimeUserSetup($user);
if ($this->lockdownManager->canAccessFilesystem()) {
$mountCallback();
}
$this->eventLogger->start('fs:setup:user:post-init-mountpoint', 'post_initMountPoints legacy hook');
\OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', ['user' => $user->getUID()]);
$this->eventLogger->end('fs:setup:user:post-init-mountpoint');
$userDir = '/' . $user->getUID() . '/files';
$this->eventLogger->start('fs:setup:user:setup-hook', 'setup legacy hook');
OC_Hook::emit('OC_Filesystem', 'setup', ['user' => $user->getUID(), 'user_dir' => $userDir]);
$this->eventLogger->end('fs:setup:user:setup-hook');
}
/**
* Set up the root filesystem
*/
public function setupRoot(): void {
//setting up the filesystem twice can only lead to trouble
if ($this->rootSetup) {
return;
}
$this->setupBuiltinWrappers();
$this->rootSetup = true;
$this->eventLogger->start('fs:setup:root', 'Setup root filesystem');
$rootMounts = $this->mountProviderCollection->getRootMounts();
foreach ($rootMounts as $rootMountProvider) {
$this->mountManager->addMount($rootMountProvider);
}
$this->eventLogger->end('fs:setup:root');
}
/**
* Get the user to setup for a path or `null` if the root needs to be setup
*
* @param string $path
* @return IUser|null
*/
private function getUserForPath(string $path) {
if (str_starts_with($path, '/__groupfolders')) {
return null;
} elseif (substr_count($path, '/') < 2) {
if ($user = $this->userSession->getUser()) {
return $user;
} else {
return null;
}
} elseif (str_starts_with($path, '/appdata_' . \OC_Util::getInstanceId()) || str_starts_with($path, '/files_external/')) {
return null;
} else {
[, $userId] = explode('/', $path);
}
return $this->userManager->get($userId);
}
/**
* Set up the filesystem for the specified path
*/
public function setupForPath(string $path, bool $includeChildren = false): void {
$user = $this->getUserForPath($path);
if (!$user) {
$this->setupRoot();
return;
}
if ($this->isSetupComplete($user)) {
return;
}
if ($this->fullSetupRequired($user)) {
$this->setupForUser($user);
return;
}
// for the user's home folder, and includes children we need everything always
if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) {
$this->setupForUser($user);
return;
}
if (!isset($this->setupUserMountProviders[$user->getUID()])) {
$this->setupUserMountProviders[$user->getUID()] = [];
}
$setupProviders = &$this->setupUserMountProviders[$user->getUID()];
$currentProviders = [];
try {
$cachedMount = $this->userMountCache->getMountForPath($user, $path);
} catch (NotFoundException $e) {
$this->setupForUser($user);
return;
}
$this->oneTimeUserSetup($user);
$this->eventLogger->start('fs:setup:user:path', "Setup $path filesystem for user");
$this->eventLogger->start('fs:setup:user:path:find', "Find mountpoint for $path");
$mounts = [];
if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
$currentProviders[] = $cachedMount->getMountProvider();
if ($cachedMount->getMountProvider()) {
$setupProviders[] = $cachedMount->getMountProvider();
$mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]);
} else {
$this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup");
$this->eventLogger->end('fs:setup:user:path:find');
$this->setupForUser($user);
$this->eventLogger->end('fs:setup:user:path');
return;
}
}
if ($includeChildren) {
$subCachedMounts = $this->userMountCache->getMountsInPath($user, $path);
$this->eventLogger->end('fs:setup:user:path:find');
$needsFullSetup = array_reduce($subCachedMounts, function (bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) {
return $needsFullSetup || $cachedMountInfo->getMountProvider() === '';
}, false);
if ($needsFullSetup) {
$this->logger->debug("mount has no provider set, performing full setup");
$this->setupForUser($user);
$this->eventLogger->end('fs:setup:user:path');
return;
} else {
foreach ($subCachedMounts as $cachedMount) {
if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
$currentProviders[] = $cachedMount->getMountProvider();
$setupProviders[] = $cachedMount->getMountProvider();
$mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]));
}
}
}
} else {
$this->eventLogger->end('fs:setup:user:path:find');
}
if (count($mounts)) {
$this->userMountCache->registerMounts($user, $mounts, $currentProviders);
$this->setupForUserWith($user, function () use ($mounts) {
array_walk($mounts, [$this->mountManager, 'addMount']);
});
} elseif (!$this->isSetupStarted($user)) {
$this->oneTimeUserSetup($user);
}
$this->eventLogger->end('fs:setup:user:path');
}
private function fullSetupRequired(IUser $user): bool {
// we perform a "cached" setup only after having done the full setup recently
// this is also used to trigger a full setup after handling events that are likely
// to change the available mounts
if (!isset($this->fullSetupRequired[$user->getUID()])) {
$this->fullSetupRequired[$user->getUID()] = !$this->cache->get($user->getUID());
}
return $this->fullSetupRequired[$user->getUID()];
}
/**
* @param string $path
* @param string[] $providers
*/
public function setupForProvider(string $path, array $providers): void {
$user = $this->getUserForPath($path);
if (!$user) {
$this->setupRoot();
return;
}
if ($this->isSetupComplete($user)) {
return;
}
if ($this->fullSetupRequired($user)) {
$this->setupForUser($user);
return;
}
$this->eventLogger->start('fs:setup:user:providers', "Setup filesystem for " . implode(', ', $providers));
$this->oneTimeUserSetup($user);
// home providers are always used
$providers = array_filter($providers, function (string $provider) {
return !is_subclass_of($provider, IHomeMountProvider::class);
});
if (in_array('', $providers)) {
$this->setupForUser($user);
return;
}
$setupProviders = $this->setupUserMountProviders[$user->getUID()] ?? [];
$providers = array_diff($providers, $setupProviders);
if (count($providers) === 0) {
if (!$this->isSetupStarted($user)) {
$this->oneTimeUserSetup($user);
}
$this->eventLogger->end('fs:setup:user:providers');
return;
} else {
$this->setupUserMountProviders[$user->getUID()] = array_merge($setupProviders, $providers);
$mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, $providers);
}
$this->userMountCache->registerMounts($user, $mounts, $providers);
$this->setupForUserWith($user, function () use ($mounts) {
array_walk($mounts, [$this->mountManager, 'addMount']);
});
$this->eventLogger->end('fs:setup:user:providers');
}
public function tearDown() {
$this->setupUsers = [];
$this->setupUsersComplete = [];
$this->setupUserMountProviders = [];
$this->fullSetupRequired = [];
$this->rootSetup = false;
$this->mountManager->clear();
$this->eventDispatcher->dispatchTyped(new FilesystemTornDownEvent());
}
/**
* Get mounts from mount providers that are registered after setup
*/
private function listenForNewMountProviders() {
if (!$this->listeningForProviders) {
$this->listeningForProviders = true;
$this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function (
IMountProvider $provider
) {
foreach ($this->setupUsers as $userId) {
$user = $this->userManager->get($userId);
if ($user) {
$mounts = $provider->getMountsForUser($user, Filesystem::getLoader());
array_walk($mounts, [$this->mountManager, 'addMount']);
}
}
});
}
}
private function setupListeners() {
// note that this event handling is intentionally pessimistic
// clearing the cache to often is better than not enough
$this->eventDispatcher->addListener(UserAddedEvent::class, function (UserAddedEvent $event) {
$this->cache->remove($event->getUser()->getUID());
});
$this->eventDispatcher->addListener(UserRemovedEvent::class, function (UserRemovedEvent $event) {
$this->cache->remove($event->getUser()->getUID());
});
$this->eventDispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event) {
$this->cache->remove($event->getShare()->getSharedWith());
});
$this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event
) {
if ($user = $event->getUser()) {
$this->cache->remove($user->getUID());
} else {
$this->cache->clear();
}
});
$genericEvents = [
'OCA\Circles\Events\CreatingCircleEvent',
'OCA\Circles\Events\DestroyingCircleEvent',
'OCA\Circles\Events\AddingCircleMemberEvent',
'OCA\Circles\Events\RemovingCircleMemberEvent',
];
foreach ($genericEvents as $genericEvent) {
$this->eventDispatcher->addListener($genericEvent, function ($event) {
$this->cache->clear();
});
}
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 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 OC\Files;
use OC\Share20\ShareDisableChecker;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Mount\IMountManager;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Lockdown\ILockdownManager;
use Psr\Log\LoggerInterface;
class SetupManagerFactory {
private ?SetupManager $setupManager;
public function __construct(
private IEventLogger $eventLogger,
private IMountProviderCollection $mountProviderCollection,
private IUserManager $userManager,
private IEventDispatcher $eventDispatcher,
private IUserMountCache $userMountCache,
private ILockdownManager $lockdownManager,
private IUserSession $userSession,
private ICacheFactory $cacheFactory,
private LoggerInterface $logger,
private IConfig $config,
private ShareDisableChecker $shareDisableChecker,
) {
$this->setupManager = null;
}
public function create(IMountManager $mountManager): SetupManager {
if (!$this->setupManager) {
$this->setupManager = new SetupManager(
$this->eventLogger,
$this->mountProviderCollection,
$mountManager,
$this->userManager,
$this->eventDispatcher,
$this->userMountCache,
$this->lockdownManager,
$this->userSession,
$this->cacheFactory,
$this->logger,
$this->config,
$this->shareDisableChecker,
);
}
return $this->setupManager;
}
}
@@ -0,0 +1,229 @@
<?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 OC\Files\SimpleFS;
use Icewind\Streams\CallbackWrapper;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
class NewSimpleFile implements ISimpleFile {
private Folder $parentFolder;
private string $name;
private ?File $file = null;
/**
* File constructor.
*/
public function __construct(Folder $parentFolder, string $name) {
$this->parentFolder = $parentFolder;
$this->name = $name;
}
/**
* Get the name
*/
public function getName(): string {
return $this->name;
}
/**
* Get the size in bytes
*/
public function getSize(): int|float {
if ($this->file) {
return $this->file->getSize();
} else {
return 0;
}
}
/**
* Get the ETag
*/
public function getETag(): string {
if ($this->file) {
return $this->file->getEtag();
} else {
return '';
}
}
/**
* Get the last modification time
*/
public function getMTime(): int {
if ($this->file) {
return $this->file->getMTime();
} else {
return time();
}
}
/**
* Get the content
*
* @throws NotFoundException
* @throws NotPermittedException
*/
public function getContent(): string {
if ($this->file) {
$result = $this->file->getContent();
if ($result === false) {
$this->checkFile();
}
return $result;
} else {
return '';
}
}
/**
* Overwrite the file
*
* @param string|resource $data
* @throws NotPermittedException
* @throws NotFoundException
*/
public function putContent($data): void {
try {
if ($this->file) {
$this->file->putContent($data);
} else {
$this->file = $this->parentFolder->newFile($this->name, $data);
}
} catch (NotFoundException $e) {
$this->checkFile();
}
}
/**
* Sometimes there are some issues with the AppData. Most of them are from
* user error. But we should handle them gracefully anyway.
*
* If for some reason the current file can't be found. We remove it.
* Then traverse up and check all folders if they exists. This so that the
* next request will have a valid appdata structure again.
*
* @throws NotFoundException
*/
private function checkFile(): void {
if (!$this->file) {
throw new NotFoundException('File not set');
}
$cur = $this->file;
while ($cur->stat() === false) {
$parent = $cur->getParent();
try {
$cur->delete();
} catch (NotFoundException $e) {
// Just continue then
}
$cur = $parent;
}
if ($cur !== $this->file) {
throw new NotFoundException('File does not exist');
}
}
/**
* Delete the file
*
* @throws NotPermittedException
*/
public function delete(): void {
if ($this->file) {
$this->file->delete();
}
}
/**
* Get the MimeType
*
* @return string
*/
public function getMimeType(): string {
if ($this->file) {
return $this->file->getMimeType();
} else {
return 'text/plain';
}
}
/**
* {@inheritDoc}
*/
public function getExtension(): string {
if ($this->file) {
return $this->file->getExtension();
} else {
return \pathinfo($this->name, PATHINFO_EXTENSION);
}
}
/**
* Open the file as stream for reading, resulting resource can be operated as stream like the result from php's own fopen
*
* @return resource|false
* @throws \OCP\Files\NotPermittedException
* @since 14.0.0
*/
public function read() {
if ($this->file) {
return $this->file->fopen('r');
} else {
return fopen('php://temp', 'r');
}
}
/**
* Open the file as stream for writing, resulting resource can be operated as stream like the result from php's own fopen
*
* @return resource|bool
* @throws \OCP\Files\NotPermittedException
* @since 14.0.0
*/
public function write() {
if ($this->file) {
return $this->file->fopen('w');
} else {
$source = fopen('php://temp', 'w+');
return CallbackWrapper::wrap($source, null, null, null, null, function () use ($source) {
rewind($source);
$this->putContent($source);
});
}
}
}
@@ -0,0 +1,175 @@
<?php
/**
* @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\SimpleFS;
use OCP\Files\File;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
class SimpleFile implements ISimpleFile {
private File $file;
public function __construct(File $file) {
$this->file = $file;
}
/**
* Get the name
*/
public function getName(): string {
return $this->file->getName();
}
/**
* Get the size in bytes
*/
public function getSize(): int|float {
return $this->file->getSize();
}
/**
* Get the ETag
*/
public function getETag(): string {
return $this->file->getEtag();
}
/**
* Get the last modification time
*/
public function getMTime(): int {
return $this->file->getMTime();
}
/**
* Get the content
*
* @throws NotPermittedException
* @throws NotFoundException
*/
public function getContent(): string {
$result = $this->file->getContent();
if ($result === false) {
$this->checkFile();
}
return $result;
}
/**
* Overwrite the file
*
* @param string|resource $data
* @throws NotPermittedException
* @throws NotFoundException
*/
public function putContent($data): void {
try {
$this->file->putContent($data);
} catch (NotFoundException $e) {
$this->checkFile();
}
}
/**
* Sometimes there are some issues with the AppData. Most of them are from
* user error. But we should handle them gracefully anyway.
*
* If for some reason the current file can't be found. We remove it.
* Then traverse up and check all folders if they exists. This so that the
* next request will have a valid appdata structure again.
*
* @throws NotFoundException
*/
private function checkFile(): void {
$cur = $this->file;
while ($cur->stat() === false) {
$parent = $cur->getParent();
try {
$cur->delete();
} catch (NotFoundException $e) {
// Just continue then
}
$cur = $parent;
}
if ($cur !== $this->file) {
throw new NotFoundException('File does not exist');
}
}
/**
* Delete the file
*
* @throws NotPermittedException
*/
public function delete(): void {
$this->file->delete();
}
/**
* Get the MimeType
*/
public function getMimeType(): string {
return $this->file->getMimeType();
}
/**
* {@inheritDoc}
*/
public function getExtension(): string {
return $this->file->getExtension();
}
/**
* Open the file as stream for reading, resulting resource can be operated as stream like the result from php's own fopen
*
* @return resource|false
* @throws \OCP\Files\NotPermittedException
* @since 14.0.0
*/
public function read() {
return $this->file->fopen('r');
}
/**
* Open the file as stream for writing, resulting resource can be operated as stream like the result from php's own fopen
*
* @return resource|false
* @throws \OCP\Files\NotPermittedException
* @since 14.0.0
*/
public function write() {
return $this->file->fopen('w');
}
public function getId(): int {
return $this->file->getId();
}
}
@@ -0,0 +1,108 @@
<?php
/**
* @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\SimpleFS;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
class SimpleFolder implements ISimpleFolder {
/** @var Folder */
private $folder;
/**
* Folder constructor.
*
* @param Folder $folder
*/
public function __construct(Folder $folder) {
$this->folder = $folder;
}
public function getName(): string {
return $this->folder->getName();
}
public function getDirectoryListing(): array {
$listing = $this->folder->getDirectoryListing();
$fileListing = array_map(function (Node $file) {
if ($file instanceof File) {
return new SimpleFile($file);
}
return null;
}, $listing);
$fileListing = array_filter($fileListing);
return array_values($fileListing);
}
public function delete(): void {
$this->folder->delete();
}
public function fileExists(string $name): bool {
return $this->folder->nodeExists($name);
}
public function getFile(string $name): ISimpleFile {
$file = $this->folder->get($name);
if (!($file instanceof File)) {
throw new NotFoundException();
}
return new SimpleFile($file);
}
public function newFile(string $name, $content = null): ISimpleFile {
if ($content === null) {
// delay creating the file until it's written to
return new NewSimpleFile($this->folder, $name);
} else {
$file = $this->folder->newFile($name, $content);
return new SimpleFile($file);
}
}
public function getFolder(string $name): ISimpleFolder {
$folder = $this->folder->get($name);
if (!($folder instanceof Folder)) {
throw new NotFoundException();
}
return new SimpleFolder($folder);
}
public function newFolder(string $path): ISimpleFolder {
$folder = $this->folder->newFolder($path);
return new SimpleFolder($folder);
}
}
@@ -0,0 +1,910 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Greta Doci <gretadoci@gmail.com>
* @author hkjolhede <hkjolhede@gmail.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Martin Mattel <martin.mattel@diemattels.at>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @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 Roland Tapken <roland@bitarbeiter.net>
* @author Sam Tuke <mail@samtuke.com>
* @author scambra <sergio@entrecables.com>
* @author Stefan Weil <sw@weilnetz.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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 OC\Files\Storage;
use OC\Files\Cache\Cache;
use OC\Files\Cache\Propagator;
use OC\Files\Cache\Scanner;
use OC\Files\Cache\Updater;
use OC\Files\Cache\Watcher;
use OC\Files\Filesystem;
use OC\Files\Storage\Wrapper\Jail;
use OC\Files\Storage\Wrapper\Wrapper;
use OCP\Files\EmptyFileNameException;
use OCP\Files\FileNameTooLongException;
use OCP\Files\ForbiddenException;
use OCP\Files\GenericFileException;
use OCP\Files\InvalidCharacterInPathException;
use OCP\Files\InvalidDirectoryException;
use OCP\Files\InvalidPathException;
use OCP\Files\ReservedWordException;
use OCP\Files\Storage\ILockingStorage;
use OCP\Files\Storage\IStorage;
use OCP\Files\Storage\IWriteStreamStorage;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use Psr\Log\LoggerInterface;
/**
* Storage backend class for providing common filesystem operation methods
* which are not storage-backend specific.
*
* \OC\Files\Storage\Common is never used directly; it is extended by all other
* storage backends, where its methods may be overridden, and additional
* (backend-specific) methods are defined.
*
* Some \OC\Files\Storage\Common methods call functions which are first defined
* in classes which extend it, e.g. $this->stat() .
*/
abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
use LocalTempFileTrait;
protected $cache;
protected $scanner;
protected $watcher;
protected $propagator;
protected $storageCache;
protected $updater;
protected $mountOptions = [];
protected $owner = null;
/** @var ?bool */
private $shouldLogLocks = null;
/** @var ?LoggerInterface */
private $logger;
public function __construct($parameters) {
}
/**
* Remove a file or folder
*
* @param string $path
* @return bool
*/
protected function remove($path) {
if ($this->is_dir($path)) {
return $this->rmdir($path);
} elseif ($this->is_file($path)) {
return $this->unlink($path);
} else {
return false;
}
}
public function is_dir($path) {
return $this->filetype($path) === 'dir';
}
public function is_file($path) {
return $this->filetype($path) === 'file';
}
public function filesize($path): false|int|float {
if ($this->is_dir($path)) {
return 0; //by definition
} else {
$stat = $this->stat($path);
if (isset($stat['size'])) {
return $stat['size'];
} else {
return 0;
}
}
}
public function isReadable($path) {
// at least check whether it exists
// subclasses might want to implement this more thoroughly
return $this->file_exists($path);
}
public function isUpdatable($path) {
// at least check whether it exists
// subclasses might want to implement this more thoroughly
// a non-existing file/folder isn't updatable
return $this->file_exists($path);
}
public function isCreatable($path) {
if ($this->is_dir($path) && $this->isUpdatable($path)) {
return true;
}
return false;
}
public function isDeletable($path) {
if ($path === '' || $path === '/') {
return $this->isUpdatable($path);
}
$parent = dirname($path);
return $this->isUpdatable($parent) && $this->isUpdatable($path);
}
public function isSharable($path) {
return $this->isReadable($path);
}
public function getPermissions($path) {
$permissions = 0;
if ($this->isCreatable($path)) {
$permissions |= \OCP\Constants::PERMISSION_CREATE;
}
if ($this->isReadable($path)) {
$permissions |= \OCP\Constants::PERMISSION_READ;
}
if ($this->isUpdatable($path)) {
$permissions |= \OCP\Constants::PERMISSION_UPDATE;
}
if ($this->isDeletable($path)) {
$permissions |= \OCP\Constants::PERMISSION_DELETE;
}
if ($this->isSharable($path)) {
$permissions |= \OCP\Constants::PERMISSION_SHARE;
}
return $permissions;
}
public function filemtime($path) {
$stat = $this->stat($path);
if (isset($stat['mtime']) && $stat['mtime'] > 0) {
return $stat['mtime'];
} else {
return 0;
}
}
public function file_get_contents($path) {
$handle = $this->fopen($path, "r");
if (!$handle) {
return false;
}
$data = stream_get_contents($handle);
fclose($handle);
return $data;
}
public function file_put_contents($path, $data) {
$handle = $this->fopen($path, "w");
if (!$handle) {
return false;
}
$this->removeCachedFile($path);
$count = fwrite($handle, $data);
fclose($handle);
return $count;
}
public function rename($source, $target) {
$this->remove($target);
$this->removeCachedFile($source);
return $this->copy($source, $target) and $this->remove($source);
}
public function copy($source, $target) {
if ($this->is_dir($source)) {
$this->remove($target);
$dir = $this->opendir($source);
$this->mkdir($target);
while ($file = readdir($dir)) {
if (!Filesystem::isIgnoredDir($file)) {
if (!$this->copy($source . '/' . $file, $target . '/' . $file)) {
closedir($dir);
return false;
}
}
}
closedir($dir);
return true;
} else {
$sourceStream = $this->fopen($source, 'r');
$targetStream = $this->fopen($target, 'w');
[, $result] = \OC_Helper::streamCopy($sourceStream, $targetStream);
if (!$result) {
\OCP\Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
}
$this->removeCachedFile($target);
return $result;
}
}
public function getMimeType($path) {
if ($this->is_dir($path)) {
return 'httpd/unix-directory';
} elseif ($this->file_exists($path)) {
return \OC::$server->getMimeTypeDetector()->detectPath($path);
} else {
return false;
}
}
public function hash($type, $path, $raw = false) {
$fh = $this->fopen($path, 'rb');
$ctx = hash_init($type);
hash_update_stream($ctx, $fh);
fclose($fh);
return hash_final($ctx, $raw);
}
public function search($query) {
return $this->searchInDir($query);
}
public function getLocalFile($path) {
return $this->getCachedFile($path);
}
/**
* @param string $path
* @param string $target
*/
private function addLocalFolder($path, $target) {
$dh = $this->opendir($path);
if (is_resource($dh)) {
while (($file = readdir($dh)) !== false) {
if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
if ($this->is_dir($path . '/' . $file)) {
mkdir($target . '/' . $file);
$this->addLocalFolder($path . '/' . $file, $target . '/' . $file);
} else {
$tmp = $this->toTmpFile($path . '/' . $file);
rename($tmp, $target . '/' . $file);
}
}
}
}
}
/**
* @param string $query
* @param string $dir
* @return array
*/
protected function searchInDir($query, $dir = '') {
$files = [];
$dh = $this->opendir($dir);
if (is_resource($dh)) {
while (($item = readdir($dh)) !== false) {
if (\OC\Files\Filesystem::isIgnoredDir($item)) {
continue;
}
if (strstr(strtolower($item), strtolower($query)) !== false) {
$files[] = $dir . '/' . $item;
}
if ($this->is_dir($dir . '/' . $item)) {
$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
}
}
}
closedir($dh);
return $files;
}
/**
* Check if a file or folder has been updated since $time
*
* The method is only used to check if the cache needs to be updated. Storage backends that don't support checking
* the mtime should always return false here. As a result storage implementations that always return false expect
* exclusive access to the backend and will not pick up files that have been added in a way that circumvents
* Nextcloud filesystem.
*
* @param string $path
* @param int $time
* @return bool
*/
public function hasUpdated($path, $time) {
return $this->filemtime($path) > $time;
}
public function getCache($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($storage->cache)) {
$storage->cache = new Cache($storage);
}
return $storage->cache;
}
public function getScanner($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($storage->scanner)) {
$storage->scanner = new Scanner($storage);
}
return $storage->scanner;
}
public function getWatcher($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($this->watcher)) {
$this->watcher = new Watcher($storage);
$globalPolicy = \OC::$server->getConfig()->getSystemValue('filesystem_check_changes', Watcher::CHECK_NEVER);
$this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
}
return $this->watcher;
}
/**
* get a propagator instance for the cache
*
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
* @return \OC\Files\Cache\Propagator
*/
public function getPropagator($storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($storage->propagator)) {
$config = \OC::$server->getSystemConfig();
$storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getValue('instanceid')]);
}
return $storage->propagator;
}
public function getUpdater($storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($storage->updater)) {
$storage->updater = new Updater($storage);
}
return $storage->updater;
}
public function getStorageCache($storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($this->storageCache)) {
$this->storageCache = new \OC\Files\Cache\Storage($storage);
}
return $this->storageCache;
}
/**
* get the owner of a path
*
* @param string $path The path to get the owner
* @return string|false uid or false
*/
public function getOwner($path) {
if ($this->owner === null) {
$this->owner = \OC_User::getUser();
}
return $this->owner;
}
/**
* get the ETag for a file or folder
*
* @param string $path
* @return string
*/
public function getETag($path) {
return uniqid();
}
/**
* clean a path, i.e. remove all redundant '.' and '..'
* making sure that it can't point to higher than '/'
*
* @param string $path The path to clean
* @return string cleaned path
*/
public function cleanPath($path) {
if (strlen($path) == 0 or $path[0] != '/') {
$path = '/' . $path;
}
$output = [];
foreach (explode('/', $path) as $chunk) {
if ($chunk == '..') {
array_pop($output);
} elseif ($chunk == '.') {
} else {
$output[] = $chunk;
}
}
return implode('/', $output);
}
/**
* Test a storage for availability
*
* @return bool
*/
public function test() {
try {
if ($this->stat('')) {
return true;
}
\OC::$server->get(LoggerInterface::class)->info("External storage not available: stat() failed");
return false;
} catch (\Exception $e) {
\OC::$server->get(LoggerInterface::class)->warning(
"External storage not available: " . $e->getMessage(),
['exception' => $e]
);
return false;
}
}
/**
* get the free space in the storage
*
* @param string $path
* @return int|float|false
*/
public function free_space($path) {
return \OCP\Files\FileInfo::SPACE_UNKNOWN;
}
/**
* {@inheritdoc}
*/
public function isLocal() {
// the common implementation returns a temporary file by
// default, which is not local
return false;
}
/**
* Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
*
* @param string $class
* @return bool
*/
public function instanceOfStorage($class) {
if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') {
// FIXME Temporary fix to keep existing checks working
$class = '\OCA\Files_Sharing\SharedStorage';
}
return is_a($this, $class);
}
/**
* A custom storage implementation can return an url for direct download of a give file.
*
* For now the returned array can hold the parameter url - in future more attributes might follow.
*
* @param string $path
* @return array|false
*/
public function getDirectDownload($path) {
return [];
}
/**
* @inheritdoc
* @throws InvalidPathException
*/
public function verifyPath($path, $fileName) {
// verify empty and dot files
$trimmed = trim($fileName);
if ($trimmed === '') {
throw new EmptyFileNameException();
}
if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) {
throw new InvalidDirectoryException();
}
if (!\OC::$server->getDatabaseConnection()->supports4ByteText()) {
// verify database - e.g. mysql only 3-byte chars
if (preg_match('%(?:
\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)%xs', $fileName)) {
throw new InvalidCharacterInPathException();
}
}
// 255 characters is the limit on common file systems (ext/xfs)
// oc_filecache has a 250 char length limit for the filename
if (isset($fileName[250])) {
throw new FileNameTooLongException();
}
// NOTE: $path will remain unverified for now
$this->verifyPosixPath($fileName);
}
/**
* @param string $fileName
* @throws InvalidPathException
*/
protected function verifyPosixPath($fileName) {
$this->scanForInvalidCharacters($fileName, "\\/");
$fileName = trim($fileName);
$reservedNames = ['*'];
if (in_array($fileName, $reservedNames)) {
throw new ReservedWordException();
}
}
/**
* @param string $fileName
* @param string $invalidChars
* @throws InvalidPathException
*/
private function scanForInvalidCharacters($fileName, $invalidChars) {
foreach (str_split($invalidChars) as $char) {
if (str_contains($fileName, $char)) {
throw new InvalidCharacterInPathException();
}
}
$sanitizedFileName = filter_var($fileName, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW);
if ($sanitizedFileName !== $fileName) {
throw new InvalidCharacterInPathException();
}
}
/**
* @param array $options
*/
public function setMountOptions(array $options) {
$this->mountOptions = $options;
}
/**
* @param string $name
* @param mixed $default
* @return mixed
*/
public function getMountOption($name, $default = null) {
return $this->mountOptions[$name] ?? $default;
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @param bool $preserveMtime
* @return bool
*/
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
if ($sourceStorage === $this) {
return $this->copy($sourceInternalPath, $targetInternalPath);
}
if ($sourceStorage->is_dir($sourceInternalPath)) {
$dh = $sourceStorage->opendir($sourceInternalPath);
$result = $this->mkdir($targetInternalPath);
if (is_resource($dh)) {
$result = true;
while ($result and ($file = readdir($dh)) !== false) {
if (!Filesystem::isIgnoredDir($file)) {
$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
}
}
}
} else {
$source = $sourceStorage->fopen($sourceInternalPath, 'r');
$result = false;
if ($source) {
try {
$this->writeStream($targetInternalPath, $source);
$result = true;
} catch (\Exception $e) {
\OC::$server->get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
}
}
if ($result && $preserveMtime) {
$mtime = $sourceStorage->filemtime($sourceInternalPath);
$this->touch($targetInternalPath, is_int($mtime) ? $mtime : null);
}
if (!$result) {
// delete partially written target file
$this->unlink($targetInternalPath);
// delete cache entry that was created by fopen
$this->getCache()->remove($targetInternalPath);
}
}
return (bool)$result;
}
/**
* Check if a storage is the same as the current one, including wrapped storages
*
* @param IStorage $storage
* @return bool
*/
private function isSameStorage(IStorage $storage): bool {
while ($storage->instanceOfStorage(Wrapper::class)) {
/**
* @var Wrapper $sourceStorage
*/
$storage = $storage->getWrapperStorage();
}
return $storage === $this;
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if ($this->isSameStorage($sourceStorage)) {
// resolve any jailed paths
while ($sourceStorage->instanceOfStorage(Jail::class)) {
/**
* @var Jail $sourceStorage
*/
$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
$sourceStorage = $sourceStorage->getUnjailedStorage();
}
return $this->rename($sourceInternalPath, $targetInternalPath);
}
if (!$sourceStorage->isDeletable($sourceInternalPath)) {
return false;
}
$result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true);
if ($result) {
if ($sourceStorage->is_dir($sourceInternalPath)) {
$result = $sourceStorage->rmdir($sourceInternalPath);
} else {
$result = $sourceStorage->unlink($sourceInternalPath);
}
}
return $result;
}
/**
* @inheritdoc
*/
public function getMetaData($path) {
if (Filesystem::isFileBlacklisted($path)) {
throw new ForbiddenException('Invalid path: ' . $path, false);
}
$permissions = $this->getPermissions($path);
if (!$permissions & \OCP\Constants::PERMISSION_READ) {
//can't read, nothing we can do
return null;
}
$data = [];
$data['mimetype'] = $this->getMimeType($path);
$data['mtime'] = $this->filemtime($path);
if ($data['mtime'] === false) {
$data['mtime'] = time();
}
if ($data['mimetype'] == 'httpd/unix-directory') {
$data['size'] = -1; //unknown
} else {
$data['size'] = $this->filesize($path);
}
$data['etag'] = $this->getETag($path);
$data['storage_mtime'] = $data['mtime'];
$data['permissions'] = $permissions;
$data['name'] = basename($path);
return $data;
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
* @throws \OCP\Lock\LockedException
*/
public function acquireLock($path, $type, ILockingProvider $provider) {
$logger = $this->getLockLogger();
if ($logger) {
$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
$logger->info(
sprintf(
'acquire %s lock on "%s" on storage "%s"',
$typeString,
$path,
$this->getId()
),
[
'app' => 'locking',
]
);
}
try {
$provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path);
} catch (LockedException $e) {
$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
if ($logger) {
$logger->info($e->getMessage(), ['exception' => $e]);
}
throw $e;
}
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
* @throws \OCP\Lock\LockedException
*/
public function releaseLock($path, $type, ILockingProvider $provider) {
$logger = $this->getLockLogger();
if ($logger) {
$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
$logger->info(
sprintf(
'release %s lock on "%s" on storage "%s"',
$typeString,
$path,
$this->getId()
),
[
'app' => 'locking',
]
);
}
try {
$provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
} catch (LockedException $e) {
$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
if ($logger) {
$logger->info($e->getMessage(), ['exception' => $e]);
}
throw $e;
}
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
* @throws \OCP\Lock\LockedException
*/
public function changeLock($path, $type, ILockingProvider $provider) {
$logger = $this->getLockLogger();
if ($logger) {
$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
$logger->info(
sprintf(
'change lock on "%s" to %s on storage "%s"',
$path,
$typeString,
$this->getId()
),
[
'app' => 'locking',
]
);
}
try {
$provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
} catch (LockedException $e) {
$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
if ($logger) {
$logger->info($e->getMessage(), ['exception' => $e]);
}
throw $e;
}
}
private function getLockLogger(): ?LoggerInterface {
if (is_null($this->shouldLogLocks)) {
$this->shouldLogLocks = \OC::$server->getConfig()->getSystemValueBool('filelocking.debug', false);
$this->logger = $this->shouldLogLocks ? \OC::$server->get(LoggerInterface::class) : null;
}
return $this->logger;
}
/**
* @return array [ available, last_checked ]
*/
public function getAvailability() {
return $this->getStorageCache()->getAvailability();
}
/**
* @param bool $isAvailable
*/
public function setAvailability($isAvailable) {
$this->getStorageCache()->setAvailability($isAvailable);
}
/**
* @return bool
*/
public function needsPartFile() {
return true;
}
/**
* fallback implementation
*
* @param string $path
* @param resource $stream
* @param int $size
* @return int
*/
public function writeStream(string $path, $stream, int $size = null): int {
$target = $this->fopen($path, 'w');
if (!$target) {
throw new GenericFileException("Failed to open $path for writing");
}
try {
[$count, $result] = \OC_Helper::streamCopy($stream, $target);
if (!$result) {
throw new GenericFileException("Failed to copy stream");
}
} finally {
fclose($target);
fclose($stream);
}
return $count;
}
public function getDirectoryContent($directory): \Traversable {
$dh = $this->opendir($directory);
if (is_resource($dh)) {
$basePath = rtrim($directory, '/');
while (($file = readdir($dh)) !== false) {
if (!Filesystem::isIgnoredDir($file)) {
$childPath = $basePath . '/' . trim($file, '/');
$metadata = $this->getMetaData($childPath);
if ($metadata !== null) {
yield $metadata;
}
}
}
}
}
}
@@ -0,0 +1,81 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christopher Schäpers <kondou@ts.unde.re>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Felix Moeller <mail@felixmoeller.de>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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 OC\Files\Storage;
class CommonTest extends \OC\Files\Storage\Common {
/**
* underlying local storage used for missing functions
* @var \OC\Files\Storage\Local
*/
private $storage;
public function __construct($params) {
$this->storage = new \OC\Files\Storage\Local($params);
}
public function getId() {
return 'test::'.$this->storage->getId();
}
public function mkdir($path) {
return $this->storage->mkdir($path);
}
public function rmdir($path) {
return $this->storage->rmdir($path);
}
public function opendir($path) {
return $this->storage->opendir($path);
}
public function stat($path) {
return $this->storage->stat($path);
}
public function filetype($path) {
return @$this->storage->filetype($path);
}
public function isReadable($path) {
return $this->storage->isReadable($path);
}
public function isUpdatable($path) {
return $this->storage->isUpdatable($path);
}
public function file_exists($path) {
return $this->storage->file_exists($path);
}
public function unlink($path) {
return $this->storage->unlink($path);
}
public function fopen($path, $mode) {
return $this->storage->fopen($path, $mode);
}
public function free_space($path) {
return $this->storage->free_space($path);
}
public function touch($path, $mtime = null) {
return $this->storage->touch($path, $mtime);
}
}
@@ -0,0 +1,934 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Carlos Cerrillo <ccerrillo@gmail.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @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 Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
* @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 OC\Files\Storage;
use Exception;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Files\Filesystem;
use OC\MemCache\ArrayCache;
use OCP\AppFramework\Http;
use OCP\Constants;
use OCP\Diagnostics\IEventLogger;
use OCP\Files\FileInfo;
use OCP\Files\ForbiddenException;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\StorageInvalidException;
use OCP\Files\StorageNotAvailableException;
use OCP\Http\Client\IClientService;
use OCP\ICertificateManager;
use OCP\IConfig;
use OCP\Util;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Client;
use Sabre\DAV\Xml\Property\ResourceType;
use Sabre\HTTP\ClientException;
use Sabre\HTTP\ClientHttpException;
use Sabre\HTTP\RequestInterface;
/**
* Class DAV
*
* @package OC\Files\Storage
*/
class DAV extends Common {
/** @var string */
protected $password;
/** @var string */
protected $user;
/** @var string|null */
protected $authType;
/** @var string */
protected $host;
/** @var bool */
protected $secure;
/** @var string */
protected $root;
/** @var string */
protected $certPath;
/** @var bool */
protected $ready;
/** @var Client */
protected $client;
/** @var ArrayCache */
protected $statCache;
/** @var IClientService */
protected $httpClientService;
/** @var ICertificateManager */
protected $certManager;
protected LoggerInterface $logger;
protected IEventLogger $eventLogger;
protected IMimeTypeDetector $mimeTypeDetector;
/** @var int */
private $timeout;
protected const PROPFIND_PROPS = [
'{DAV:}getlastmodified',
'{DAV:}getcontentlength',
'{DAV:}getcontenttype',
'{http://owncloud.org/ns}permissions',
'{http://open-collaboration-services.org/ns}share-permissions',
'{DAV:}resourcetype',
'{DAV:}getetag',
'{DAV:}quota-available-bytes',
];
/**
* @param array $params
* @throws \Exception
*/
public function __construct($params) {
$this->statCache = new ArrayCache();
$this->httpClientService = \OC::$server->getHTTPClientService();
if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
$host = $params['host'];
//remove leading http[s], will be generated in createBaseUri()
if (str_starts_with($host, "https://")) {
$host = substr($host, 8);
} elseif (str_starts_with($host, "http://")) {
$host = substr($host, 7);
}
$this->host = $host;
$this->user = $params['user'];
$this->password = $params['password'];
if (isset($params['authType'])) {
$this->authType = $params['authType'];
}
if (isset($params['secure'])) {
if (is_string($params['secure'])) {
$this->secure = ($params['secure'] === 'true');
} else {
$this->secure = (bool)$params['secure'];
}
} else {
$this->secure = false;
}
if ($this->secure === true) {
// inject mock for testing
$this->certManager = \OC::$server->getCertificateManager();
}
$this->root = $params['root'] ?? '/';
$this->root = '/' . ltrim($this->root, '/');
$this->root = rtrim($this->root, '/') . '/';
} else {
throw new \Exception('Invalid webdav storage configuration');
}
$this->logger = \OC::$server->get(LoggerInterface::class);
$this->eventLogger = \OC::$server->get(IEventLogger::class);
// This timeout value will be used for the download and upload of files
$this->timeout = \OC::$server->get(IConfig::class)->getSystemValueInt('davstorage.request_timeout', 30);
$this->mimeTypeDetector = \OC::$server->getMimeTypeDetector();
}
protected function init() {
if ($this->ready) {
return;
}
$this->ready = true;
$settings = [
'baseUri' => $this->createBaseUri(),
'userName' => $this->user,
'password' => $this->password,
];
if ($this->authType !== null) {
$settings['authType'] = $this->authType;
}
$proxy = \OC::$server->getConfig()->getSystemValueString('proxy', '');
if ($proxy !== '') {
$settings['proxy'] = $proxy;
}
$this->client = new Client($settings);
$this->client->setThrowExceptions(true);
if ($this->secure === true) {
$certPath = $this->certManager->getAbsoluteBundlePath();
if (file_exists($certPath)) {
$this->certPath = $certPath;
}
if ($this->certPath) {
$this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
}
}
$lastRequestStart = 0;
$this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) {
$this->logger->debug("sending dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl(), ['app' => 'dav']);
$lastRequestStart = microtime(true);
$this->eventLogger->start('fs:storage:dav:request', "Sending dav request to external storage");
});
$this->client->on('afterRequest', function (RequestInterface $request) use (&$lastRequestStart) {
$elapsed = microtime(true) - $lastRequestStart;
$this->logger->debug("dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl() . " took " . round($elapsed * 1000, 1) . "ms", ['app' => 'dav']);
$this->eventLogger->end('fs:storage:dav:request');
});
}
/**
* Clear the stat cache
*/
public function clearStatCache() {
$this->statCache->clear();
}
/** {@inheritdoc} */
public function getId() {
return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
}
/** {@inheritdoc} */
public function createBaseUri() {
$baseUri = 'http';
if ($this->secure) {
$baseUri .= 's';
}
$baseUri .= '://' . $this->host . $this->root;
return $baseUri;
}
/** {@inheritdoc} */
public function mkdir($path) {
$this->init();
$path = $this->cleanPath($path);
$result = $this->simpleResponse('MKCOL', $path, null, 201);
if ($result) {
$this->statCache->set($path, true);
}
return $result;
}
/** {@inheritdoc} */
public function rmdir($path) {
$this->init();
$path = $this->cleanPath($path);
// FIXME: some WebDAV impl return 403 when trying to DELETE
// a non-empty folder
$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
$this->statCache->clear($path . '/');
$this->statCache->remove($path);
return $result;
}
/** {@inheritdoc} */
public function opendir($path) {
$this->init();
$path = $this->cleanPath($path);
try {
$content = $this->getDirectoryContent($path);
$files = [];
foreach ($content as $child) {
$files[] = $child['name'];
}
return IteratorDirectory::wrap($files);
} catch (\Exception $e) {
$this->convertException($e, $path);
}
return false;
}
/**
* Propfind call with cache handling.
*
* First checks if information is cached.
* If not, request it from the server then store to cache.
*
* @param string $path path to propfind
*
* @return array|boolean propfind response or false if the entry was not found
*
* @throws ClientHttpException
*/
protected function propfind($path) {
$path = $this->cleanPath($path);
$cachedResponse = $this->statCache->get($path);
// we either don't know it, or we know it exists but need more details
if (is_null($cachedResponse) || $cachedResponse === true) {
$this->init();
try {
$response = $this->client->propFind(
$this->encodePath($path),
self::PROPFIND_PROPS
);
$this->statCache->set($path, $response);
} catch (ClientHttpException $e) {
if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
$this->statCache->clear($path . '/');
$this->statCache->set($path, false);
return false;
}
$this->convertException($e, $path);
} catch (\Exception $e) {
$this->convertException($e, $path);
}
} else {
$response = $cachedResponse;
}
return $response;
}
/** {@inheritdoc} */
public function filetype($path) {
try {
$response = $this->propfind($path);
if ($response === false) {
return false;
}
$responseType = [];
if (isset($response["{DAV:}resourcetype"])) {
/** @var ResourceType[] $response */
$responseType = $response["{DAV:}resourcetype"]->getValue();
}
return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
} catch (\Exception $e) {
$this->convertException($e, $path);
}
return false;
}
/** {@inheritdoc} */
public function file_exists($path) {
try {
$path = $this->cleanPath($path);
$cachedState = $this->statCache->get($path);
if ($cachedState === false) {
// we know the file doesn't exist
return false;
} elseif (!is_null($cachedState)) {
return true;
}
// need to get from server
return ($this->propfind($path) !== false);
} catch (\Exception $e) {
$this->convertException($e, $path);
}
return false;
}
/** {@inheritdoc} */
public function unlink($path) {
$this->init();
$path = $this->cleanPath($path);
$result = $this->simpleResponse('DELETE', $path, null, 204);
$this->statCache->clear($path . '/');
$this->statCache->remove($path);
return $result;
}
/** {@inheritdoc} */
public function fopen($path, $mode) {
$this->init();
$path = $this->cleanPath($path);
switch ($mode) {
case 'r':
case 'rb':
try {
$response = $this->httpClientService
->newClient()
->get($this->createBaseUri() . $this->encodePath($path), [
'auth' => [$this->user, $this->password],
'stream' => true,
// set download timeout for users with slow connections or large files
'timeout' => $this->timeout
]);
} catch (\GuzzleHttp\Exception\ClientException $e) {
if ($e->getResponse() instanceof ResponseInterface
&& $e->getResponse()->getStatusCode() === 404) {
return false;
} else {
throw $e;
}
}
if ($response->getStatusCode() !== Http::STATUS_OK) {
if ($response->getStatusCode() === Http::STATUS_LOCKED) {
throw new \OCP\Lock\LockedException($path);
} else {
\OC::$server->get(LoggerInterface::class)->error('Guzzle get returned status code ' . $response->getStatusCode(), ['app' => 'webdav client']);
}
}
return $response->getBody();
case 'w':
case 'wb':
case 'a':
case 'ab':
case 'r+':
case 'w+':
case 'wb+':
case 'a+':
case 'x':
case 'x+':
case 'c':
case 'c+':
//emulate these
$tempManager = \OC::$server->getTempManager();
if (strrpos($path, '.') !== false) {
$ext = substr($path, strrpos($path, '.'));
} else {
$ext = '';
}
if ($this->file_exists($path)) {
if (!$this->isUpdatable($path)) {
return false;
}
if ($mode === 'w' or $mode === 'w+') {
$tmpFile = $tempManager->getTemporaryFile($ext);
} else {
$tmpFile = $this->getCachedFile($path);
}
} else {
if (!$this->isCreatable(dirname($path))) {
return false;
}
$tmpFile = $tempManager->getTemporaryFile($ext);
}
$handle = fopen($tmpFile, $mode);
return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
$this->writeBack($tmpFile, $path);
});
}
}
/**
* @param string $tmpFile
*/
public function writeBack($tmpFile, $path) {
$this->uploadFile($tmpFile, $path);
unlink($tmpFile);
}
/** {@inheritdoc} */
public function free_space($path) {
$this->init();
$path = $this->cleanPath($path);
try {
$response = $this->propfind($path);
if ($response === false) {
return FileInfo::SPACE_UNKNOWN;
}
if (isset($response['{DAV:}quota-available-bytes'])) {
return Util::numericToNumber($response['{DAV:}quota-available-bytes']);
} else {
return FileInfo::SPACE_UNKNOWN;
}
} catch (\Exception $e) {
return FileInfo::SPACE_UNKNOWN;
}
}
/** {@inheritdoc} */
public function touch($path, $mtime = null) {
$this->init();
if (is_null($mtime)) {
$mtime = time();
}
$path = $this->cleanPath($path);
// if file exists, update the mtime, else create a new empty file
if ($this->file_exists($path)) {
try {
$this->statCache->remove($path);
$this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
// non-owncloud clients might not have accepted the property, need to recheck it
$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
if ($response === false) {
return false;
}
if (isset($response['{DAV:}getlastmodified'])) {
$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
if ($remoteMtime !== $mtime) {
// server has not accepted the mtime
return false;
}
}
} catch (ClientHttpException $e) {
if ($e->getHttpStatus() === 501) {
return false;
}
$this->convertException($e, $path);
return false;
} catch (\Exception $e) {
$this->convertException($e, $path);
return false;
}
} else {
$this->file_put_contents($path, '');
}
return true;
}
/**
* @param string $path
* @param mixed $data
* @return int|float|false
*/
public function file_put_contents($path, $data) {
$path = $this->cleanPath($path);
$result = parent::file_put_contents($path, $data);
$this->statCache->remove($path);
return $result;
}
/**
* @param string $path
* @param string $target
*/
protected function uploadFile($path, $target) {
$this->init();
// invalidate
$target = $this->cleanPath($target);
$this->statCache->remove($target);
$source = fopen($path, 'r');
$this->httpClientService
->newClient()
->put($this->createBaseUri() . $this->encodePath($target), [
'body' => $source,
'auth' => [$this->user, $this->password],
// set upload timeout for users with slow connections or large files
'timeout' => $this->timeout
]);
$this->removeCachedFile($target);
}
/** {@inheritdoc} */
public function rename($source, $target) {
$this->init();
$source = $this->cleanPath($source);
$target = $this->cleanPath($target);
try {
// overwrite directory ?
if ($this->is_dir($target)) {
// needs trailing slash in destination
$target = rtrim($target, '/') . '/';
}
$this->client->request(
'MOVE',
$this->encodePath($source),
null,
[
'Destination' => $this->createBaseUri() . $this->encodePath($target),
]
);
$this->statCache->clear($source . '/');
$this->statCache->clear($target . '/');
$this->statCache->set($source, false);
$this->statCache->set($target, true);
$this->removeCachedFile($source);
$this->removeCachedFile($target);
return true;
} catch (\Exception $e) {
$this->convertException($e);
}
return false;
}
/** {@inheritdoc} */
public function copy($source, $target) {
$this->init();
$source = $this->cleanPath($source);
$target = $this->cleanPath($target);
try {
// overwrite directory ?
if ($this->is_dir($target)) {
// needs trailing slash in destination
$target = rtrim($target, '/') . '/';
}
$this->client->request(
'COPY',
$this->encodePath($source),
null,
[
'Destination' => $this->createBaseUri() . $this->encodePath($target),
]
);
$this->statCache->clear($target . '/');
$this->statCache->set($target, true);
$this->removeCachedFile($target);
return true;
} catch (\Exception $e) {
$this->convertException($e);
}
return false;
}
public function getMetaData($path) {
if (Filesystem::isFileBlacklisted($path)) {
throw new ForbiddenException('Invalid path: ' . $path, false);
}
$response = $this->propfind($path);
if (!$response) {
return null;
} else {
return $this->getMetaFromPropfind($path, $response);
}
}
private function getMetaFromPropfind(string $path, array $response): array {
if (isset($response['{DAV:}getetag'])) {
$etag = trim($response['{DAV:}getetag'], '"');
if (strlen($etag) > 40) {
$etag = md5($etag);
}
} else {
$etag = parent::getETag($path);
}
$responseType = [];
if (isset($response["{DAV:}resourcetype"])) {
/** @var ResourceType[] $response */
$responseType = $response["{DAV:}resourcetype"]->getValue();
}
$type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
if ($type === 'dir') {
$mimeType = 'httpd/unix-directory';
} elseif (isset($response['{DAV:}getcontenttype'])) {
$mimeType = $response['{DAV:}getcontenttype'];
} else {
$mimeType = $this->mimeTypeDetector->detectPath($path);
}
if (isset($response['{http://owncloud.org/ns}permissions'])) {
$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
} elseif ($type === 'dir') {
$permissions = Constants::PERMISSION_ALL;
} else {
$permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
}
$mtime = isset($response['{DAV:}getlastmodified']) ? strtotime($response['{DAV:}getlastmodified']) : null;
if ($type === 'dir') {
$size = -1;
} else {
$size = Util::numericToNumber($response['{DAV:}getcontentlength'] ?? 0);
}
return [
'name' => basename($path),
'mtime' => $mtime,
'storage_mtime' => $mtime,
'size' => $size,
'permissions' => $permissions,
'etag' => $etag,
'mimetype' => $mimeType,
];
}
/** {@inheritdoc} */
public function stat($path) {
$meta = $this->getMetaData($path);
if (!$meta) {
return false;
} else {
return $meta;
}
}
/** {@inheritdoc} */
public function getMimeType($path) {
$meta = $this->getMetaData($path);
if ($meta) {
return $meta['mimetype'];
} else {
return false;
}
}
/**
* @param string $path
* @return string
*/
public function cleanPath($path) {
if ($path === '') {
return $path;
}
$path = Filesystem::normalizePath($path);
// remove leading slash
return substr($path, 1);
}
/**
* URL encodes the given path but keeps the slashes
*
* @param string $path to encode
* @return string encoded path
*/
protected function encodePath($path) {
// slashes need to stay
return str_replace('%2F', '/', rawurlencode($path));
}
/**
* @param string $method
* @param string $path
* @param string|resource|null $body
* @param int $expected
* @return bool
* @throws StorageInvalidException
* @throws StorageNotAvailableException
*/
protected function simpleResponse($method, $path, $body, $expected) {
$path = $this->cleanPath($path);
try {
$response = $this->client->request($method, $this->encodePath($path), $body);
return $response['statusCode'] == $expected;
} catch (ClientHttpException $e) {
if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
$this->statCache->clear($path . '/');
$this->statCache->set($path, false);
return false;
}
$this->convertException($e, $path);
} catch (\Exception $e) {
$this->convertException($e, $path);
}
return false;
}
/**
* check if curl is installed
*/
public static function checkDependencies() {
return true;
}
/** {@inheritdoc} */
public function isUpdatable($path) {
return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
}
/** {@inheritdoc} */
public function isCreatable($path) {
return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
}
/** {@inheritdoc} */
public function isSharable($path) {
return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
}
/** {@inheritdoc} */
public function isDeletable($path) {
return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
}
/** {@inheritdoc} */
public function getPermissions($path) {
$stat = $this->getMetaData($path);
if ($stat) {
return $stat['permissions'];
} else {
return 0;
}
}
/** {@inheritdoc} */
public function getETag($path) {
$meta = $this->getMetaData($path);
if ($meta) {
return $meta['etag'];
} else {
return null;
}
}
/**
* @param string $permissionsString
* @return int
*/
protected function parsePermissions($permissionsString) {
$permissions = Constants::PERMISSION_READ;
if (str_contains($permissionsString, 'R')) {
$permissions |= Constants::PERMISSION_SHARE;
}
if (str_contains($permissionsString, 'D')) {
$permissions |= Constants::PERMISSION_DELETE;
}
if (str_contains($permissionsString, 'W')) {
$permissions |= Constants::PERMISSION_UPDATE;
}
if (str_contains($permissionsString, 'CK')) {
$permissions |= Constants::PERMISSION_CREATE;
$permissions |= Constants::PERMISSION_UPDATE;
}
return $permissions;
}
/**
* check if a file or folder has been updated since $time
*
* @param string $path
* @param int $time
* @throws \OCP\Files\StorageNotAvailableException
* @return bool
*/
public function hasUpdated($path, $time) {
$this->init();
$path = $this->cleanPath($path);
try {
// force refresh for $path
$this->statCache->remove($path);
$response = $this->propfind($path);
if ($response === false) {
if ($path === '') {
// if root is gone it means the storage is not available
throw new StorageNotAvailableException('root is gone');
}
return false;
}
if (isset($response['{DAV:}getetag'])) {
$cachedData = $this->getCache()->get($path);
$etag = trim($response['{DAV:}getetag'], '"');
if (($cachedData === false) || (!empty($etag) && ($cachedData['etag'] !== $etag))) {
return true;
} elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
return $sharePermissions !== $cachedData['permissions'];
} elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
return $permissions !== $cachedData['permissions'];
} else {
return false;
}
} elseif (isset($response['{DAV:}getlastmodified'])) {
$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
return $remoteMtime > $time;
} else {
// neither `getetag` nor `getlastmodified` is set
return false;
}
} catch (ClientHttpException $e) {
if ($e->getHttpStatus() === 405) {
if ($path === '') {
// if root is gone it means the storage is not available
throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
}
return false;
}
$this->convertException($e, $path);
return false;
} catch (\Exception $e) {
$this->convertException($e, $path);
return false;
}
}
/**
* Interpret the given exception and decide whether it is due to an
* unavailable storage, invalid storage or other.
* This will either throw StorageInvalidException, StorageNotAvailableException
* or do nothing.
*
* @param Exception $e sabre exception
* @param string $path optional path from the operation
*
* @throws StorageInvalidException if the storage is invalid, for example
* when the authentication expired or is invalid
* @throws StorageNotAvailableException if the storage is not available,
* which might be temporary
* @throws ForbiddenException if the action is not allowed
*/
protected function convertException(Exception $e, $path = '') {
\OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['app' => 'files_external', 'exception' => $e]);
if ($e instanceof ClientHttpException) {
if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
throw new \OCP\Lock\LockedException($path);
}
if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
// either password was changed or was invalid all along
throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
} elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
// ignore exception for MethodNotAllowed, false will be returned
return;
} elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
// The operation is forbidden. Fail somewhat gracefully
throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
}
throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
} elseif ($e instanceof ClientException) {
// connection timeout or refused, server could be temporarily down
throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
} elseif ($e instanceof \InvalidArgumentException) {
// parse error because the server returned HTML instead of XML,
// possibly temporarily down
throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
} elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
// rethrow
throw $e;
}
// TODO: only log for now, but in the future need to wrap/rethrow exception
}
public function getDirectoryContent($directory): \Traversable {
$this->init();
$directory = $this->cleanPath($directory);
try {
$responses = $this->client->propFind(
$this->encodePath($directory),
self::PROPFIND_PROPS,
1
);
if ($responses === false) {
return;
}
array_shift($responses); //the first entry is the current directory
if (!$this->statCache->hasKey($directory)) {
$this->statCache->set($directory, true);
}
foreach ($responses as $file => $response) {
$file = rawurldecode($file);
$file = substr($file, strlen($this->root));
$file = $this->cleanPath($file);
$this->statCache->set($file, $response);
yield $this->getMetaFromPropfind($file, $response);
}
} catch (\Exception $e) {
$this->convertException($e, $directory);
}
}
}
@@ -0,0 +1,214 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Lukas Reschke <lukas@statuscode.ch>
* @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 OC\Files\Storage;
use OC\Files\Cache\FailedCache;
use OCP\Files\Storage\IStorage;
use OCP\Files\StorageNotAvailableException;
use OCP\Lock\ILockingProvider;
/**
* Storage placeholder to represent a missing precondition, storage unavailable
*/
class FailedStorage extends Common {
/** @var \Exception */
protected $e;
/**
* @param array $params ['exception' => \Exception]
*/
public function __construct($params) {
$this->e = $params['exception'];
if (!$this->e) {
throw new \InvalidArgumentException('Missing "exception" argument in FailedStorage constructor');
}
}
public function getId() {
// we can't return anything sane here
return 'failedstorage';
}
public function mkdir($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function rmdir($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function opendir($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function is_dir($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function is_file($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function stat($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function filetype($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function filesize($path): false|int|float {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function isCreatable($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function isReadable($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function isUpdatable($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function isDeletable($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function isSharable($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function getPermissions($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function file_exists($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function filemtime($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function file_get_contents($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function file_put_contents($path, $data) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function unlink($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function rename($source, $target) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function copy($source, $target) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function fopen($path, $mode) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function getMimeType($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function hash($type, $path, $raw = false) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function free_space($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function search($query) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function touch($path, $mtime = null) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function getLocalFile($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function hasUpdated($path, $time) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function getETag($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function getDirectDownload($path) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function verifyPath($path, $fileName) {
return true;
}
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function acquireLock($path, $type, ILockingProvider $provider) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function releaseLock($path, $type, ILockingProvider $provider) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function changeLock($path, $type, ILockingProvider $provider) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function getAvailability() {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function setAvailability($isAvailable) {
throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
}
public function getCache($path = '', $storage = null) {
return new FailedCache();
}
}
@@ -0,0 +1,111 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.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 OC\Files\Storage;
use OC\Files\Cache\HomePropagator;
use OCP\IUser;
/**
* Specialized version of Local storage for home directory usage
*/
class Home extends Local implements \OCP\Files\IHomeStorage {
/**
* @var string
*/
protected $id;
/**
* @var \OC\User\User $user
*/
protected $user;
/**
* Construct a Home storage instance
*
* @param array $arguments array with "user" containing the
* storage owner
*/
public function __construct($arguments) {
$this->user = $arguments['user'];
$datadir = $this->user->getHome();
$this->id = 'home::' . $this->user->getUID();
parent::__construct(['datadir' => $datadir]);
}
public function getId() {
return $this->id;
}
/**
* @return \OC\Files\Cache\HomeCache
*/
public function getCache($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($this->cache)) {
$this->cache = new \OC\Files\Cache\HomeCache($storage);
}
return $this->cache;
}
/**
* get a propagator instance for the cache
*
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
* @return \OC\Files\Cache\Propagator
*/
public function getPropagator($storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($this->propagator)) {
$this->propagator = new HomePropagator($storage, \OC::$server->getDatabaseConnection());
}
return $this->propagator;
}
/**
* Returns the owner of this home storage
*
* @return \OC\User\User owner of this home storage
*/
public function getUser(): IUser {
return $this->user;
}
/**
* get the owner of a path
*
* @param string $path The path to get the owner
* @return string uid or false
*/
public function getOwner($path) {
return $this->user->getUID();
}
}
@@ -0,0 +1,653 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author aler9 <46489434+aler9@users.noreply.github.com>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Boris Rybalkin <ribalkin@gmail.com>
* @author Brice Maron <brice@bmaron.net>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Jakob Sack <mail@jakobsack.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Johannes Leuker <j.leuker@hosting.de>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Klaas Freitag <freitag@owncloud.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Martin Brugnara <martin@0x6d62.eu>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Sjors van der Pluijm <sjors@desjors.nl>
* @author Stefan Weil <sw@weilnetz.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
* @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 OC\Files\Storage;
use OC\Files\Filesystem;
use OC\Files\Storage\Wrapper\Encryption;
use OC\Files\Storage\Wrapper\Jail;
use OCP\Constants;
use OCP\Files\ForbiddenException;
use OCP\Files\GenericFileException;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\Storage\IStorage;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
use OCP\Util;
use Psr\Log\LoggerInterface;
/**
* for local filestore, we only have to map the paths
*/
class Local extends \OC\Files\Storage\Common {
protected $datadir;
protected $dataDirLength;
protected $realDataDir;
private IConfig $config;
private IMimeTypeDetector $mimeTypeDetector;
private $defUMask;
protected bool $unlinkOnTruncate;
protected bool $caseInsensitive = false;
public function __construct($arguments) {
if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) {
throw new \InvalidArgumentException('No data directory set for local storage');
}
$this->datadir = str_replace('//', '/', $arguments['datadir']);
// some crazy code uses a local storage on root...
if ($this->datadir === '/') {
$this->realDataDir = $this->datadir;
} else {
$realPath = realpath($this->datadir) ?: $this->datadir;
$this->realDataDir = rtrim($realPath, '/') . '/';
}
if (!str_ends_with($this->datadir, '/')) {
$this->datadir .= '/';
}
$this->dataDirLength = strlen($this->realDataDir);
$this->config = \OC::$server->get(IConfig::class);
$this->mimeTypeDetector = \OC::$server->get(IMimeTypeDetector::class);
$this->defUMask = $this->config->getSystemValue('localstorage.umask', 0022);
$this->caseInsensitive = $this->config->getSystemValueBool('localstorage.case_insensitive', false);
// support Write-Once-Read-Many file systems
$this->unlinkOnTruncate = $this->config->getSystemValueBool('localstorage.unlink_on_truncate', false);
if (isset($arguments['isExternal']) && $arguments['isExternal'] && !$this->stat('')) {
// data dir not accessible or available, can happen when using an external storage of type Local
// on an unmounted system mount point
throw new StorageNotAvailableException('Local storage path does not exist "' . $this->getSourcePath('') . '"');
}
}
public function __destruct() {
}
public function getId() {
return 'local::' . $this->datadir;
}
public function mkdir($path) {
$sourcePath = $this->getSourcePath($path);
$oldMask = umask($this->defUMask);
$result = @mkdir($sourcePath, 0777, true);
umask($oldMask);
return $result;
}
public function rmdir($path) {
if (!$this->isDeletable($path)) {
return false;
}
try {
$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->getSourcePath($path)),
\RecursiveIteratorIterator::CHILD_FIRST
);
/**
* RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
* This bug is fixed in PHP 5.5.9 or before
* See #8376
*/
$it->rewind();
while ($it->valid()) {
/**
* @var \SplFileInfo $file
*/
$file = $it->current();
clearstatcache(true, $this->getSourcePath($file));
if (in_array($file->getBasename(), ['.', '..'])) {
$it->next();
continue;
} elseif ($file->isFile() || $file->isLink()) {
unlink($file->getPathname());
} elseif ($file->isDir()) {
rmdir($file->getPathname());
}
$it->next();
}
clearstatcache(true, $this->getSourcePath($path));
return rmdir($this->getSourcePath($path));
} catch (\UnexpectedValueException $e) {
return false;
}
}
public function opendir($path) {
return opendir($this->getSourcePath($path));
}
public function is_dir($path) {
if ($this->caseInsensitive && !$this->file_exists($path)) {
return false;
}
if (str_ends_with($path, '/')) {
$path = substr($path, 0, -1);
}
return is_dir($this->getSourcePath($path));
}
public function is_file($path) {
if ($this->caseInsensitive && !$this->file_exists($path)) {
return false;
}
return is_file($this->getSourcePath($path));
}
public function stat($path) {
$fullPath = $this->getSourcePath($path);
clearstatcache(true, $fullPath);
if (!file_exists($fullPath)) {
return false;
}
$statResult = @stat($fullPath);
if (PHP_INT_SIZE === 4 && $statResult && !$this->is_dir($path)) {
$filesize = $this->filesize($path);
$statResult['size'] = $filesize;
$statResult[7] = $filesize;
}
if (is_array($statResult)) {
$statResult['full_path'] = $fullPath;
}
return $statResult;
}
/**
* @inheritdoc
*/
public function getMetaData($path) {
try {
$stat = $this->stat($path);
} catch (ForbiddenException $e) {
return null;
}
if (!$stat) {
return null;
}
$permissions = Constants::PERMISSION_SHARE;
$statPermissions = $stat['mode'];
$isDir = ($statPermissions & 0x4000) === 0x4000 && !($statPermissions & 0x8000);
if ($statPermissions & 0x0100) {
$permissions += Constants::PERMISSION_READ;
}
if ($statPermissions & 0x0080) {
$permissions += Constants::PERMISSION_UPDATE;
if ($isDir) {
$permissions += Constants::PERMISSION_CREATE;
}
}
if (!($path === '' || $path === '/')) { // deletable depends on the parents unix permissions
$parent = dirname($stat['full_path']);
if (is_writable($parent)) {
$permissions += Constants::PERMISSION_DELETE;
}
}
$data = [];
$data['mimetype'] = $isDir ? 'httpd/unix-directory' : $this->mimeTypeDetector->detectPath($path);
$data['mtime'] = $stat['mtime'];
if ($data['mtime'] === false) {
$data['mtime'] = time();
}
if ($isDir) {
$data['size'] = -1; //unknown
} else {
$data['size'] = $stat['size'];
}
$data['etag'] = $this->calculateEtag($path, $stat);
$data['storage_mtime'] = $data['mtime'];
$data['permissions'] = $permissions;
$data['name'] = basename($path);
return $data;
}
public function filetype($path) {
$filetype = filetype($this->getSourcePath($path));
if ($filetype == 'link') {
$filetype = filetype(realpath($this->getSourcePath($path)));
}
return $filetype;
}
public function filesize($path): false|int|float {
if (!$this->is_file($path)) {
return 0;
}
$fullPath = $this->getSourcePath($path);
if (PHP_INT_SIZE === 4) {
$helper = new \OC\LargeFileHelper;
return $helper->getFileSize($fullPath);
}
return filesize($fullPath);
}
public function isReadable($path) {
return is_readable($this->getSourcePath($path));
}
public function isUpdatable($path) {
return is_writable($this->getSourcePath($path));
}
public function file_exists($path) {
if ($this->caseInsensitive) {
$fullPath = $this->getSourcePath($path);
$content = scandir(dirname($fullPath), SCANDIR_SORT_NONE);
return is_array($content) && array_search(basename($fullPath), $content) !== false;
} else {
return file_exists($this->getSourcePath($path));
}
}
public function filemtime($path) {
$fullPath = $this->getSourcePath($path);
clearstatcache(true, $fullPath);
if (!$this->file_exists($path)) {
return false;
}
if (PHP_INT_SIZE === 4) {
$helper = new \OC\LargeFileHelper();
return $helper->getFileMtime($fullPath);
}
return filemtime($fullPath);
}
public function touch($path, $mtime = null) {
// sets the modification time of the file to the given value.
// If mtime is nil the current time is set.
// note that the access time of the file always changes to the current time.
if ($this->file_exists($path) and !$this->isUpdatable($path)) {
return false;
}
$oldMask = umask($this->defUMask);
if (!is_null($mtime)) {
$result = @touch($this->getSourcePath($path), $mtime);
} else {
$result = @touch($this->getSourcePath($path));
}
umask($oldMask);
if ($result) {
clearstatcache(true, $this->getSourcePath($path));
}
return $result;
}
public function file_get_contents($path) {
return file_get_contents($this->getSourcePath($path));
}
public function file_put_contents($path, $data) {
$oldMask = umask($this->defUMask);
if ($this->unlinkOnTruncate) {
$this->unlink($path);
}
$result = file_put_contents($this->getSourcePath($path), $data);
umask($oldMask);
return $result;
}
public function unlink($path) {
if ($this->is_dir($path)) {
return $this->rmdir($path);
} elseif ($this->is_file($path)) {
return unlink($this->getSourcePath($path));
} else {
return false;
}
}
private function checkTreeForForbiddenItems(string $path) {
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
foreach ($iterator as $file) {
/** @var \SplFileInfo $file */
if (Filesystem::isFileBlacklisted($file->getBasename())) {
throw new ForbiddenException('Invalid path: ' . $file->getPathname(), false);
}
}
}
public function rename($source, $target): bool {
$srcParent = dirname($source);
$dstParent = dirname($target);
if (!$this->isUpdatable($srcParent)) {
\OC::$server->get(LoggerInterface::class)->error('unable to rename, source directory is not writable : ' . $srcParent, ['app' => 'core']);
return false;
}
if (!$this->isUpdatable($dstParent)) {
\OC::$server->get(LoggerInterface::class)->error('unable to rename, destination directory is not writable : ' . $dstParent, ['app' => 'core']);
return false;
}
if (!$this->file_exists($source)) {
\OC::$server->get(LoggerInterface::class)->error('unable to rename, file does not exists : ' . $source, ['app' => 'core']);
return false;
}
if ($this->is_dir($target)) {
$this->rmdir($target);
} elseif ($this->is_file($target)) {
$this->unlink($target);
}
if ($this->is_dir($source)) {
$this->checkTreeForForbiddenItems($this->getSourcePath($source));
}
if (@rename($this->getSourcePath($source), $this->getSourcePath($target))) {
if ($this->caseInsensitive) {
if (mb_strtolower($target) === mb_strtolower($source) && !$this->file_exists($target)) {
return false;
}
}
return true;
}
return $this->copy($source, $target) && $this->unlink($source);
}
public function copy($source, $target) {
if ($this->is_dir($source)) {
return parent::copy($source, $target);
} else {
$oldMask = umask($this->defUMask);
if ($this->unlinkOnTruncate) {
$this->unlink($target);
}
$result = copy($this->getSourcePath($source), $this->getSourcePath($target));
umask($oldMask);
if ($this->caseInsensitive) {
if (mb_strtolower($target) === mb_strtolower($source) && !$this->file_exists($target)) {
return false;
}
}
return $result;
}
}
public function fopen($path, $mode) {
$sourcePath = $this->getSourcePath($path);
if (!file_exists($sourcePath) && $mode === 'r') {
return false;
}
$oldMask = umask($this->defUMask);
if (($mode === 'w' || $mode === 'w+') && $this->unlinkOnTruncate) {
$this->unlink($path);
}
$result = @fopen($sourcePath, $mode);
umask($oldMask);
return $result;
}
public function hash($type, $path, $raw = false) {
return hash_file($type, $this->getSourcePath($path), $raw);
}
public function free_space($path) {
$sourcePath = $this->getSourcePath($path);
// using !is_dir because $sourcePath might be a part file or
// non-existing file, so we'd still want to use the parent dir
// in such cases
if (!is_dir($sourcePath)) {
// disk_free_space doesn't work on files
$sourcePath = dirname($sourcePath);
}
$space = (function_exists('disk_free_space') && is_dir($sourcePath)) ? disk_free_space($sourcePath) : false;
if ($space === false || is_null($space)) {
return \OCP\Files\FileInfo::SPACE_UNKNOWN;
}
return Util::numericToNumber($space);
}
public function search($query) {
return $this->searchInDir($query);
}
public function getLocalFile($path) {
return $this->getSourcePath($path);
}
/**
* @param string $query
* @param string $dir
* @return array
*/
protected function searchInDir($query, $dir = '') {
$files = [];
$physicalDir = $this->getSourcePath($dir);
foreach (scandir($physicalDir) as $item) {
if (\OC\Files\Filesystem::isIgnoredDir($item)) {
continue;
}
$physicalItem = $physicalDir . '/' . $item;
if (strstr(strtolower($item), strtolower($query)) !== false) {
$files[] = $dir . '/' . $item;
}
if (is_dir($physicalItem)) {
$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
}
}
return $files;
}
/**
* 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 ($this->file_exists($path)) {
return $this->filemtime($path) > $time;
} else {
return true;
}
}
/**
* Get the source path (on disk) of a given path
*
* @param string $path
* @return string
* @throws ForbiddenException
*/
public function getSourcePath($path) {
if (Filesystem::isFileBlacklisted($path)) {
throw new ForbiddenException('Invalid path: ' . $path, false);
}
$fullPath = $this->datadir . $path;
$currentPath = $path;
$allowSymlinks = $this->config->getSystemValueBool('localstorage.allowsymlinks', false);
if ($allowSymlinks || $currentPath === '') {
return $fullPath;
}
$pathToResolve = $fullPath;
$realPath = realpath($pathToResolve);
while ($realPath === false) { // for non existing files check the parent directory
$currentPath = dirname($currentPath);
if ($currentPath === '' || $currentPath === '.') {
return $fullPath;
}
$realPath = realpath($this->datadir . $currentPath);
}
if ($realPath) {
$realPath = $realPath . '/';
}
if (substr($realPath, 0, $this->dataDirLength) === $this->realDataDir) {
return $fullPath;
}
\OC::$server->get(LoggerInterface::class)->error("Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ['app' => 'core']);
throw new ForbiddenException('Following symlinks is not allowed', false);
}
/**
* {@inheritdoc}
*/
public function isLocal() {
return true;
}
/**
* get the ETag for a file or folder
*
* @param string $path
* @return string
*/
public function getETag($path) {
return $this->calculateEtag($path, $this->stat($path));
}
private function calculateEtag(string $path, array $stat): string {
if ($stat['mode'] & 0x4000 && !($stat['mode'] & 0x8000)) { // is_dir & not socket
return parent::getETag($path);
} else {
if ($stat === false) {
return md5('');
}
$toHash = '';
if (isset($stat['mtime'])) {
$toHash .= $stat['mtime'];
}
if (isset($stat['ino'])) {
$toHash .= $stat['ino'];
}
if (isset($stat['dev'])) {
$toHash .= $stat['dev'];
}
if (isset($stat['size'])) {
$toHash .= $stat['size'];
}
return md5($toHash);
}
}
private function canDoCrossStorageMove(IStorage $sourceStorage) {
return $sourceStorage->instanceOfStorage(Local::class)
// Don't treat ACLStorageWrapper like local storage where copy can be done directly.
// Instead, use the slower recursive copying in php from Common::copyFromStorage with
// more permissions checks.
&& !$sourceStorage->instanceOfStorage('OCA\GroupFolders\ACL\ACLStorageWrapper')
// when moving encrypted files we have to handle keys and the target might not be encrypted
&& !$sourceStorage->instanceOfStorage(Encryption::class);
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @param bool $preserveMtime
* @return bool
*/
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
if ($this->canDoCrossStorageMove($sourceStorage)) {
if ($sourceStorage->instanceOfStorage(Jail::class)) {
/**
* @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
*/
$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
}
/**
* @var \OC\Files\Storage\Local $sourceStorage
*/
$rootStorage = new Local(['datadir' => '/']);
return $rootStorage->copy($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
} else {
return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
}
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if ($this->canDoCrossStorageMove($sourceStorage)) {
if ($sourceStorage->instanceOfStorage(Jail::class)) {
/**
* @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
*/
$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
}
/**
* @var \OC\Files\Storage\Local $sourceStorage
*/
$rootStorage = new Local(['datadir' => '/']);
return $rootStorage->rename($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
} else {
return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
}
}
public function writeStream(string $path, $stream, int $size = null): int {
/** @var int|false $result We consider here that returned size will never be a float because we write less than 4GB */
$result = $this->file_put_contents($path, $stream);
if (is_resource($stream)) {
fclose($stream);
}
if ($result === false) {
throw new GenericFileException("Failed write stream to $path");
} else {
return $result;
}
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @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 OC\Files\Storage;
use OC\Files\Cache\LocalRootScanner;
class LocalRootStorage extends Local {
public function getScanner($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
if (!isset($storage->scanner)) {
$storage->scanner = new LocalRootScanner($storage);
}
return $storage->scanner;
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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 OC\Files\Storage;
/**
* Storage backend class for providing common filesystem operation methods
* which are not storage-backend specific.
*
* \OC\Files\Storage\Common is never used directly; it is extended by all other
* storage backends, where its methods may be overridden, and additional
* (backend-specific) methods are defined.
*
* Some \OC\Files\Storage\Common methods call functions which are first defined
* in classes which extend it, e.g. $this->stat() .
*/
trait LocalTempFileTrait {
/** @var array<string,string|false> */
protected array $cachedFiles = [];
protected function getCachedFile(string $path): string|false {
if (!isset($this->cachedFiles[$path])) {
$this->cachedFiles[$path] = $this->toTmpFile($path);
}
return $this->cachedFiles[$path];
}
/**
* @param string $path
*/
protected function removeCachedFile($path) {
unset($this->cachedFiles[$path]);
}
protected function toTmpFile(string $path): string|false { //no longer in the storage api, still useful here
$source = $this->fopen($path, 'r');
if (!$source) {
return false;
}
if ($pos = strrpos($path, '.')) {
$extension = substr($path, $pos);
} else {
$extension = '';
}
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($extension);
$target = fopen($tmpFile, 'w');
\OC_Helper::streamCopy($source, $target);
fclose($target);
return $tmpFile;
}
}
@@ -0,0 +1,104 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Martin Mattel <martin.mattel@diemattels.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Stefan Weil <sw@weilnetz.de>
*
* @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 OC\Files\Storage\PolyFill;
trait CopyDirectory {
/**
* Check if a path is a directory
*
* @param string $path
* @return bool
*/
abstract public function is_dir($path);
/**
* Check if a file or folder exists
*
* @param string $path
* @return bool
*/
abstract public function file_exists($path);
/**
* Delete a file or folder
*
* @param string $path
* @return bool
*/
abstract public function unlink($path);
/**
* Open a directory handle for a folder
*
* @param string $path
* @return resource | bool
*/
abstract public function opendir($path);
/**
* Create a new folder
*
* @param string $path
* @return bool
*/
abstract public function mkdir($path);
public function copy($source, $target) {
if ($this->is_dir($source)) {
if ($this->file_exists($target)) {
$this->unlink($target);
}
$this->mkdir($target);
return $this->copyRecursive($source, $target);
} else {
return parent::copy($source, $target);
}
}
/**
* For adapters that don't support copying folders natively
*
* @param $source
* @param $target
* @return bool
*/
protected function copyRecursive($source, $target) {
$dh = $this->opendir($source);
$result = true;
while ($file = readdir($dh)) {
if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
if ($this->is_dir($source . '/' . $file)) {
$this->mkdir($target . '/' . $file);
$result = $this->copyRecursive($source . '/' . $file, $target . '/' . $file);
} else {
$result = parent::copy($source . '/' . $file, $target . '/' . $file);
}
if (!$result) {
break;
}
}
}
return $result;
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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 OC\Files\Storage;
use OCP\Lock\ILockingProvider;
/**
* Provide a common interface to all different storage options
*
* All paths passed to the storage are relative to the storage and should NOT have a leading slash.
*/
interface Storage extends \OCP\Files\Storage {
/**
* get a cache instance for the storage
*
* @param string $path
* @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache
* @return \OC\Files\Cache\Cache
*/
public function getCache($path = '', $storage = null);
/**
* get a scanner instance for the storage
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
* @return \OC\Files\Cache\Scanner
*/
public function getScanner($path = '', $storage = null);
/**
* get the user id of the owner of a file or folder
*
* @param string $path
* @return string
*/
public function getOwner($path);
/**
* get a watcher instance for the cache
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
* @return \OC\Files\Cache\Watcher
*/
public function getWatcher($path = '', $storage = null);
/**
* get a propagator instance for the cache
*
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
* @return \OC\Files\Cache\Propagator
*/
public function getPropagator($storage = null);
/**
* get a updater instance for the cache
*
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
* @return \OC\Files\Cache\Updater
*/
public function getUpdater($storage = null);
/**
* @return \OC\Files\Cache\Storage
*/
public function getStorageCache();
/**
* @param string $path
* @return array|null
*/
public function getMetaData($path);
/**
* @param string $path The path of the file to acquire the lock for
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
* @throws \OCP\Lock\LockedException
*/
public function acquireLock($path, $type, ILockingProvider $provider);
/**
* @param string $path The path of the file to release the lock for
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
* @throws \OCP\Lock\LockedException
*/
public function releaseLock($path, $type, ILockingProvider $provider);
/**
* @param string $path The path of the file to change the lock for
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
* @throws \OCP\Lock\LockedException
*/
public function changeLock($path, $type, ILockingProvider $provider);
/**
* Get the contents of a directory with metadata
*
* @param string $directory
* @return \Traversable an iterator, containing file metadata
*
* The metadata array will contain the following fields
*
* - name
* - mimetype
* - mtime
* - size
* - etag
* - storage_mtime
* - permissions
*/
public function getDirectoryContent($directory): \Traversable;
}
@@ -0,0 +1,107 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.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 OC\Files\Storage;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Storage\IStorageFactory;
class StorageFactory implements IStorageFactory {
/**
* @var array[] [$name=>['priority'=>$priority, 'wrapper'=>$callable] $storageWrappers
*/
private $storageWrappers = [];
/**
* allow modifier storage behaviour by adding wrappers around storages
*
* $callback should be a function of type (string $mountPoint, Storage $storage) => Storage
*
* @param string $wrapperName name of the wrapper
* @param callable $callback callback
* @param int $priority wrappers with the lower priority are applied last (meaning they get called first)
* @param \OCP\Files\Mount\IMountPoint[] $existingMounts existing mount points to apply the wrapper to
* @return bool true if the wrapper was added, false if there was already a wrapper with this
* name registered
*/
public function addStorageWrapper($wrapperName, $callback, $priority = 50, $existingMounts = []) {
if (isset($this->storageWrappers[$wrapperName])) {
return false;
}
// apply to existing mounts before registering it to prevent applying it double in MountPoint::createStorage
foreach ($existingMounts as $mount) {
$mount->wrapStorage($callback);
}
$this->storageWrappers[$wrapperName] = ['wrapper' => $callback, 'priority' => $priority];
return true;
}
/**
* Remove a storage wrapper by name.
* Note: internal method only to be used for cleanup
*
* @param string $wrapperName name of the wrapper
* @internal
*/
public function removeStorageWrapper($wrapperName) {
unset($this->storageWrappers[$wrapperName]);
}
/**
* Create an instance of a storage and apply the registered storage wrappers
*
* @param \OCP\Files\Mount\IMountPoint $mountPoint
* @param string $class
* @param array $arguments
* @return \OCP\Files\Storage
*/
public function getInstance(IMountPoint $mountPoint, $class, $arguments) {
return $this->wrap($mountPoint, new $class($arguments));
}
/**
* @param \OCP\Files\Mount\IMountPoint $mountPoint
* @param \OCP\Files\Storage $storage
* @return \OCP\Files\Storage
*/
public function wrap(IMountPoint $mountPoint, $storage) {
$wrappers = array_values($this->storageWrappers);
usort($wrappers, function ($a, $b) {
return $b['priority'] - $a['priority'];
});
/** @var callable[] $wrappers */
$wrappers = array_map(function ($wrapper) {
return $wrapper['wrapper'];
}, $wrappers);
foreach ($wrappers as $wrapper) {
$storage = $wrapper($mountPoint->getMountPoint(), $storage, $mountPoint);
if (!($storage instanceof \OCP\Files\Storage)) {
throw new \Exception('Invalid result from storage wrapper');
}
}
return $storage;
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.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 OC\Files\Storage;
/**
* local storage backend in temporary folder for testing purpose
*/
class Temporary extends Local {
public function __construct($arguments = null) {
parent::__construct(['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]);
}
public function cleanUp() {
\OC_Helper::rmdirr($this->datadir);
}
public function __destruct() {
parent::__destruct();
$this->cleanUp();
}
public function getDataDir() {
return $this->datadir;
}
}
@@ -0,0 +1,484 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.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 OC\Files\Storage\Wrapper;
use OCP\Files\Storage\IStorage;
use OCP\Files\StorageAuthException;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
/**
* Availability checker for storages
*
* Throws a StorageNotAvailableException for storages with known failures
*/
class Availability extends Wrapper {
public const RECHECK_TTL_SEC = 600; // 10 minutes
/** @var IConfig */
protected $config;
public function __construct($parameters) {
$this->config = $parameters['config'] ?? \OC::$server->getConfig();
parent::__construct($parameters);
}
public static function shouldRecheck($availability) {
if (!$availability['available']) {
// trigger a recheck if TTL reached
if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) {
return true;
}
}
return false;
}
/**
* Only called if availability === false
*
* @return bool
*/
private function updateAvailability() {
// reset availability to false so that multiple requests don't recheck concurrently
$this->setAvailability(false);
try {
$result = $this->test();
} catch (\Exception $e) {
$result = false;
}
$this->setAvailability($result);
return $result;
}
/**
* @return bool
*/
private function isAvailable() {
$availability = $this->getAvailability();
if (self::shouldRecheck($availability)) {
return $this->updateAvailability();
}
return $availability['available'];
}
/**
* @throws StorageNotAvailableException
*/
private function checkAvailability() {
if (!$this->isAvailable()) {
throw new StorageNotAvailableException();
}
}
/** {@inheritdoc} */
public function mkdir($path) {
$this->checkAvailability();
try {
return parent::mkdir($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function rmdir($path) {
$this->checkAvailability();
try {
return parent::rmdir($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function opendir($path) {
$this->checkAvailability();
try {
return parent::opendir($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function is_dir($path) {
$this->checkAvailability();
try {
return parent::is_dir($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function is_file($path) {
$this->checkAvailability();
try {
return parent::is_file($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function stat($path) {
$this->checkAvailability();
try {
return parent::stat($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function filetype($path) {
$this->checkAvailability();
try {
return parent::filetype($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function filesize($path): false|int|float {
$this->checkAvailability();
try {
return parent::filesize($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function isCreatable($path) {
$this->checkAvailability();
try {
return parent::isCreatable($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function isReadable($path) {
$this->checkAvailability();
try {
return parent::isReadable($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function isUpdatable($path) {
$this->checkAvailability();
try {
return parent::isUpdatable($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function isDeletable($path) {
$this->checkAvailability();
try {
return parent::isDeletable($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function isSharable($path) {
$this->checkAvailability();
try {
return parent::isSharable($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function getPermissions($path) {
$this->checkAvailability();
try {
return parent::getPermissions($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function file_exists($path) {
if ($path === '') {
return true;
}
$this->checkAvailability();
try {
return parent::file_exists($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function filemtime($path) {
$this->checkAvailability();
try {
return parent::filemtime($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function file_get_contents($path) {
$this->checkAvailability();
try {
return parent::file_get_contents($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function file_put_contents($path, $data) {
$this->checkAvailability();
try {
return parent::file_put_contents($path, $data);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function unlink($path) {
$this->checkAvailability();
try {
return parent::unlink($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function rename($source, $target) {
$this->checkAvailability();
try {
return parent::rename($source, $target);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function copy($source, $target) {
$this->checkAvailability();
try {
return parent::copy($source, $target);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function fopen($path, $mode) {
$this->checkAvailability();
try {
return parent::fopen($path, $mode);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function getMimeType($path) {
$this->checkAvailability();
try {
return parent::getMimeType($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function hash($type, $path, $raw = false) {
$this->checkAvailability();
try {
return parent::hash($type, $path, $raw);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function free_space($path) {
$this->checkAvailability();
try {
return parent::free_space($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function search($query) {
$this->checkAvailability();
try {
return parent::search($query);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function touch($path, $mtime = null) {
$this->checkAvailability();
try {
return parent::touch($path, $mtime);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function getLocalFile($path) {
$this->checkAvailability();
try {
return parent::getLocalFile($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function hasUpdated($path, $time) {
if (!$this->isAvailable()) {
return false;
}
try {
return parent::hasUpdated($path, $time);
} catch (StorageNotAvailableException $e) {
// set unavailable but don't rethrow
$this->setUnavailable(null);
return false;
}
}
/** {@inheritdoc} */
public function getOwner($path) {
try {
return parent::getOwner($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function getETag($path) {
$this->checkAvailability();
try {
return parent::getETag($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function getDirectDownload($path) {
$this->checkAvailability();
try {
return parent::getDirectDownload($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
$this->checkAvailability();
try {
return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
$this->checkAvailability();
try {
return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/** {@inheritdoc} */
public function getMetaData($path) {
$this->checkAvailability();
try {
return parent::getMetaData($path);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
/**
* @template T of StorageNotAvailableException|null
* @param T $e
* @psalm-return (T is null ? void : never)
* @throws StorageNotAvailableException
*/
protected function setUnavailable(?StorageNotAvailableException $e): void {
$delay = self::RECHECK_TTL_SEC;
if ($e instanceof StorageAuthException) {
$delay = max(
// 30min
$this->config->getSystemValueInt('external_storage.auth_availability_delay', 1800),
self::RECHECK_TTL_SEC
);
}
$this->getStorageCache()->setAvailability(false, $delay);
if ($e !== null) {
throw $e;
}
}
public function getDirectoryContent($directory): \Traversable {
$this->checkAvailability();
try {
return parent::getDirectoryContent($directory);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
}
}
}
@@ -0,0 +1,545 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
* @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 OC\Files\Storage\Wrapper;
use OC\Files\Filesystem;
use OCP\Cache\CappedMemoryCache;
use OCP\Files\Storage\IStorage;
use OCP\ICache;
/**
* Encoding wrapper that deals with file names that use unsupported encodings like NFD.
*
* When applied and a UTF-8 path name was given, the wrapper will first attempt to access
* the actual given name and then try its NFD form.
*/
class Encoding extends Wrapper {
/**
* @var ICache
*/
private $namesCache;
/**
* @param array $parameters
*/
public function __construct($parameters) {
$this->storage = $parameters['storage'];
$this->namesCache = new CappedMemoryCache();
}
/**
* Returns whether the given string is only made of ASCII characters
*
* @param string $str string
*
* @return bool true if the string is all ASCII, false otherwise
*/
private function isAscii($str) {
return !preg_match('/[\\x80-\\xff]+/', $str);
}
/**
* Checks whether the given path exists in NFC or NFD form after checking
* each form for each path section and returns the correct form.
* If no existing path found, returns the path as it was given.
*
* @param string $fullPath path to check
*
* @return string original or converted path
*/
private function findPathToUse($fullPath) {
$cachedPath = $this->namesCache[$fullPath];
if ($cachedPath !== null) {
return $cachedPath;
}
$sections = explode('/', $fullPath);
$path = '';
foreach ($sections as $section) {
$convertedPath = $this->findPathToUseLastSection($path, $section);
if ($convertedPath === null) {
// no point in continuing if the section was not found, use original path
return $fullPath;
}
$path = $convertedPath . '/';
}
$path = rtrim($path, '/');
return $path;
}
/**
* Checks whether the last path section of the given path exists in NFC or NFD form
* and returns the correct form. If no existing path found, returns null.
*
* @param string $basePath base path to check
* @param string $lastSection last section of the path to check for NFD/NFC variations
*
* @return string|null original or converted path, or null if none of the forms was found
*/
private function findPathToUseLastSection($basePath, $lastSection) {
$fullPath = $basePath . $lastSection;
if ($lastSection === '' || $this->isAscii($lastSection) || $this->storage->file_exists($fullPath)) {
$this->namesCache[$fullPath] = $fullPath;
return $fullPath;
}
// swap encoding
if (\Normalizer::isNormalized($lastSection, \Normalizer::FORM_C)) {
$otherFormPath = \Normalizer::normalize($lastSection, \Normalizer::FORM_D);
} else {
$otherFormPath = \Normalizer::normalize($lastSection, \Normalizer::FORM_C);
}
$otherFullPath = $basePath . $otherFormPath;
if ($this->storage->file_exists($otherFullPath)) {
$this->namesCache[$fullPath] = $otherFullPath;
return $otherFullPath;
}
// return original path, file did not exist at all
$this->namesCache[$fullPath] = $fullPath;
return null;
}
/**
* see https://www.php.net/manual/en/function.mkdir.php
*
* @param string $path
* @return bool
*/
public function mkdir($path) {
// note: no conversion here, method should not be called with non-NFC names!
$result = $this->storage->mkdir($path);
if ($result) {
$this->namesCache[$path] = $path;
}
return $result;
}
/**
* see https://www.php.net/manual/en/function.rmdir.php
*
* @param string $path
* @return bool
*/
public function rmdir($path) {
$result = $this->storage->rmdir($this->findPathToUse($path));
if ($result) {
unset($this->namesCache[$path]);
}
return $result;
}
/**
* see https://www.php.net/manual/en/function.opendir.php
*
* @param string $path
* @return resource|false
*/
public function opendir($path) {
$handle = $this->storage->opendir($this->findPathToUse($path));
return EncodingDirectoryWrapper::wrap($handle);
}
/**
* see https://www.php.net/manual/en/function.is_dir.php
*
* @param string $path
* @return bool
*/
public function is_dir($path) {
return $this->storage->is_dir($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.is_file.php
*
* @param string $path
* @return bool
*/
public function is_file($path) {
return $this->storage->is_file($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.stat.php
* only the following keys are required in the result: size and mtime
*
* @param string $path
* @return array|bool
*/
public function stat($path) {
return $this->storage->stat($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.filetype.php
*
* @param string $path
* @return string|bool
*/
public function filetype($path) {
return $this->storage->filetype($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.filesize.php
* The result for filesize when called on a folder is required to be 0
*/
public function filesize($path): false|int|float {
return $this->storage->filesize($this->findPathToUse($path));
}
/**
* check if a file can be created in $path
*
* @param string $path
* @return bool
*/
public function isCreatable($path) {
return $this->storage->isCreatable($this->findPathToUse($path));
}
/**
* check if a file can be read
*
* @param string $path
* @return bool
*/
public function isReadable($path) {
return $this->storage->isReadable($this->findPathToUse($path));
}
/**
* check if a file can be written to
*
* @param string $path
* @return bool
*/
public function isUpdatable($path) {
return $this->storage->isUpdatable($this->findPathToUse($path));
}
/**
* check if a file can be deleted
*
* @param string $path
* @return bool
*/
public function isDeletable($path) {
return $this->storage->isDeletable($this->findPathToUse($path));
}
/**
* check if a file can be shared
*
* @param string $path
* @return bool
*/
public function isSharable($path) {
return $this->storage->isSharable($this->findPathToUse($path));
}
/**
* get the full permissions of a path.
* Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php
*
* @param string $path
* @return int
*/
public function getPermissions($path) {
return $this->storage->getPermissions($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.file_exists.php
*
* @param string $path
* @return bool
*/
public function file_exists($path) {
return $this->storage->file_exists($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.filemtime.php
*
* @param string $path
* @return int|bool
*/
public function filemtime($path) {
return $this->storage->filemtime($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.file_get_contents.php
*
* @param string $path
* @return string|false
*/
public function file_get_contents($path) {
return $this->storage->file_get_contents($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.file_put_contents.php
*
* @param string $path
* @param mixed $data
* @return int|float|false
*/
public function file_put_contents($path, $data) {
return $this->storage->file_put_contents($this->findPathToUse($path), $data);
}
/**
* see https://www.php.net/manual/en/function.unlink.php
*
* @param string $path
* @return bool
*/
public function unlink($path) {
$result = $this->storage->unlink($this->findPathToUse($path));
if ($result) {
unset($this->namesCache[$path]);
}
return $result;
}
/**
* see https://www.php.net/manual/en/function.rename.php
*
* @param string $source
* @param string $target
* @return bool
*/
public function rename($source, $target) {
// second name always NFC
return $this->storage->rename($this->findPathToUse($source), $this->findPathToUse($target));
}
/**
* see https://www.php.net/manual/en/function.copy.php
*
* @param string $source
* @param string $target
* @return bool
*/
public function copy($source, $target) {
return $this->storage->copy($this->findPathToUse($source), $this->findPathToUse($target));
}
/**
* see https://www.php.net/manual/en/function.fopen.php
*
* @param string $path
* @param string $mode
* @return resource|bool
*/
public function fopen($path, $mode) {
$result = $this->storage->fopen($this->findPathToUse($path), $mode);
if ($result && $mode !== 'r' && $mode !== 'rb') {
unset($this->namesCache[$path]);
}
return $result;
}
/**
* get the mimetype for a file or folder
* The mimetype for a folder is required to be "httpd/unix-directory"
*
* @param string $path
* @return string|bool
*/
public function getMimeType($path) {
return $this->storage->getMimeType($this->findPathToUse($path));
}
/**
* see https://www.php.net/manual/en/function.hash.php
*
* @param string $type
* @param string $path
* @param bool $raw
* @return string|bool
*/
public function hash($type, $path, $raw = false) {
return $this->storage->hash($type, $this->findPathToUse($path), $raw);
}
/**
* see https://www.php.net/manual/en/function.free_space.php
*
* @param string $path
* @return int|float|bool
*/
public function free_space($path) {
return $this->storage->free_space($this->findPathToUse($path));
}
/**
* search for occurrences of $query in file names
*
* @param string $query
* @return array|bool
*/
public function search($query) {
return $this->storage->search($query);
}
/**
* see https://www.php.net/manual/en/function.touch.php
* If the backend does not support the operation, false should be returned
*
* @param string $path
* @param int $mtime
* @return bool
*/
public function touch($path, $mtime = null) {
return $this->storage->touch($this->findPathToUse($path), $mtime);
}
/**
* get the path to a local version of the file.
* The local version of the file can be temporary and doesn't have to be persistent across requests
*
* @param string $path
* @return string|false
*/
public function getLocalFile($path) {
return $this->storage->getLocalFile($this->findPathToUse($path));
}
/**
* check if a file or folder has been updated since $time
*
* @param string $path
* @param int $time
* @return bool
*
* hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed.
* returning true for other changes in the folder is optional
*/
public function hasUpdated($path, $time) {
return $this->storage->hasUpdated($this->findPathToUse($path), $time);
}
/**
* get a cache instance for the storage
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the cache
* @return \OC\Files\Cache\Cache
*/
public function getCache($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
return $this->storage->getCache($this->findPathToUse($path), $storage);
}
/**
* get a scanner instance for the storage
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
* @return \OC\Files\Cache\Scanner
*/
public function getScanner($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
return $this->storage->getScanner($this->findPathToUse($path), $storage);
}
/**
* get the ETag for a file or folder
*
* @param string $path
* @return string|false
*/
public function getETag($path) {
return $this->storage->getETag($this->findPathToUse($path));
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if ($sourceStorage === $this) {
return $this->copy($sourceInternalPath, $this->findPathToUse($targetInternalPath));
}
$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $this->findPathToUse($targetInternalPath));
if ($result) {
unset($this->namesCache[$targetInternalPath]);
}
return $result;
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if ($sourceStorage === $this) {
$result = $this->rename($sourceInternalPath, $this->findPathToUse($targetInternalPath));
if ($result) {
unset($this->namesCache[$sourceInternalPath]);
unset($this->namesCache[$targetInternalPath]);
}
return $result;
}
$result = $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $this->findPathToUse($targetInternalPath));
if ($result) {
unset($this->namesCache[$sourceInternalPath]);
unset($this->namesCache[$targetInternalPath]);
}
return $result;
}
public function getMetaData($path) {
$entry = $this->storage->getMetaData($this->findPathToUse($path));
$entry['name'] = trim(Filesystem::normalizePath($entry['name']), '/');
return $entry;
}
public function getDirectoryContent($directory): \Traversable {
$entries = $this->storage->getDirectoryContent($this->findPathToUse($directory));
foreach ($entries as $entry) {
$entry['name'] = trim(Filesystem::normalizePath($entry['name']), '/');
yield $entry;
}
}
}
@@ -0,0 +1,56 @@
<?php
/**
* @copyright Copyright (c) 2021, Nextcloud GmbH.
*
* @author Robin Appelman <robin@icewind.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 OC\Files\Storage\Wrapper;
use Icewind\Streams\DirectoryWrapper;
use OC\Files\Filesystem;
/**
* Normalize file names while reading directory entries
*/
class EncodingDirectoryWrapper extends DirectoryWrapper {
/**
* @psalm-suppress ImplementedReturnTypeMismatch Until return type is fixed upstream
* @return string|false
*/
public function dir_readdir() {
$file = readdir($this->source);
if ($file !== false && $file !== '.' && $file !== '..') {
$file = trim(Filesystem::normalizePath($file), '/');
}
return $file;
}
/**
* @param resource $source
* @param callable $filter
* @return resource|false
*/
public static function wrap($source) {
return self::wrapSource($source, [
'source' => $source,
]);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,534 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
*
* @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 OC\Files\Storage\Wrapper;
use OC\Files\Cache\Wrapper\CacheJail;
use OC\Files\Cache\Wrapper\JailPropagator;
use OC\Files\Filesystem;
use OCP\Files\Storage\IStorage;
use OCP\Files\Storage\IWriteStreamStorage;
use OCP\Lock\ILockingProvider;
/**
* Jail to a subdirectory of the wrapped storage
*
* This restricts access to a subfolder of the wrapped storage with the subfolder becoming the root folder new storage
*/
class Jail extends Wrapper {
/**
* @var string
*/
protected $rootPath;
/**
* @param array $arguments ['storage' => $storage, 'root' => $root]
*
* $storage: The storage that will be wrapper
* $root: The folder in the wrapped storage that will become the root folder of the wrapped storage
*/
public function __construct($arguments) {
parent::__construct($arguments);
$this->rootPath = $arguments['root'];
}
public function getUnjailedPath($path) {
return trim(Filesystem::normalizePath($this->rootPath . '/' . $path), '/');
}
/**
* This is separate from Wrapper::getWrapperStorage so we can get the jailed storage consistently even if the jail is inside another wrapper
*/
public function getUnjailedStorage() {
return $this->storage;
}
public function getJailedPath($path) {
$root = rtrim($this->rootPath, '/') . '/';
if ($path !== $this->rootPath && !str_starts_with($path, $root)) {
return null;
} else {
$path = substr($path, strlen($this->rootPath));
return trim($path, '/');
}
}
public function getId() {
return parent::getId();
}
/**
* see https://www.php.net/manual/en/function.mkdir.php
*
* @param string $path
* @return bool
*/
public function mkdir($path) {
return $this->getWrapperStorage()->mkdir($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.rmdir.php
*
* @param string $path
* @return bool
*/
public function rmdir($path) {
return $this->getWrapperStorage()->rmdir($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.opendir.php
*
* @param string $path
* @return resource|false
*/
public function opendir($path) {
return $this->getWrapperStorage()->opendir($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.is_dir.php
*
* @param string $path
* @return bool
*/
public function is_dir($path) {
return $this->getWrapperStorage()->is_dir($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.is_file.php
*
* @param string $path
* @return bool
*/
public function is_file($path) {
return $this->getWrapperStorage()->is_file($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.stat.php
* only the following keys are required in the result: size and mtime
*
* @param string $path
* @return array|bool
*/
public function stat($path) {
return $this->getWrapperStorage()->stat($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.filetype.php
*
* @param string $path
* @return bool
*/
public function filetype($path) {
return $this->getWrapperStorage()->filetype($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.filesize.php
* The result for filesize when called on a folder is required to be 0
*/
public function filesize($path): false|int|float {
return $this->getWrapperStorage()->filesize($this->getUnjailedPath($path));
}
/**
* check if a file can be created in $path
*
* @param string $path
* @return bool
*/
public function isCreatable($path) {
return $this->getWrapperStorage()->isCreatable($this->getUnjailedPath($path));
}
/**
* check if a file can be read
*
* @param string $path
* @return bool
*/
public function isReadable($path) {
return $this->getWrapperStorage()->isReadable($this->getUnjailedPath($path));
}
/**
* check if a file can be written to
*
* @param string $path
* @return bool
*/
public function isUpdatable($path) {
return $this->getWrapperStorage()->isUpdatable($this->getUnjailedPath($path));
}
/**
* check if a file can be deleted
*
* @param string $path
* @return bool
*/
public function isDeletable($path) {
return $this->getWrapperStorage()->isDeletable($this->getUnjailedPath($path));
}
/**
* check if a file can be shared
*
* @param string $path
* @return bool
*/
public function isSharable($path) {
return $this->getWrapperStorage()->isSharable($this->getUnjailedPath($path));
}
/**
* get the full permissions of a path.
* Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php
*
* @param string $path
* @return int
*/
public function getPermissions($path) {
return $this->getWrapperStorage()->getPermissions($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.file_exists.php
*
* @param string $path
* @return bool
*/
public function file_exists($path) {
return $this->getWrapperStorage()->file_exists($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.filemtime.php
*
* @param string $path
* @return int|bool
*/
public function filemtime($path) {
return $this->getWrapperStorage()->filemtime($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.file_get_contents.php
*
* @param string $path
* @return string|false
*/
public function file_get_contents($path) {
return $this->getWrapperStorage()->file_get_contents($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.file_put_contents.php
*
* @param string $path
* @param mixed $data
* @return int|float|false
*/
public function file_put_contents($path, $data) {
return $this->getWrapperStorage()->file_put_contents($this->getUnjailedPath($path), $data);
}
/**
* see https://www.php.net/manual/en/function.unlink.php
*
* @param string $path
* @return bool
*/
public function unlink($path) {
return $this->getWrapperStorage()->unlink($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.rename.php
*
* @param string $source
* @param string $target
* @return bool
*/
public function rename($source, $target) {
return $this->getWrapperStorage()->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target));
}
/**
* see https://www.php.net/manual/en/function.copy.php
*
* @param string $source
* @param string $target
* @return bool
*/
public function copy($source, $target) {
return $this->getWrapperStorage()->copy($this->getUnjailedPath($source), $this->getUnjailedPath($target));
}
/**
* see https://www.php.net/manual/en/function.fopen.php
*
* @param string $path
* @param string $mode
* @return resource|bool
*/
public function fopen($path, $mode) {
return $this->getWrapperStorage()->fopen($this->getUnjailedPath($path), $mode);
}
/**
* get the mimetype for a file or folder
* The mimetype for a folder is required to be "httpd/unix-directory"
*
* @param string $path
* @return string|bool
*/
public function getMimeType($path) {
return $this->getWrapperStorage()->getMimeType($this->getUnjailedPath($path));
}
/**
* see https://www.php.net/manual/en/function.hash.php
*
* @param string $type
* @param string $path
* @param bool $raw
* @return string|bool
*/
public function hash($type, $path, $raw = false) {
return $this->getWrapperStorage()->hash($type, $this->getUnjailedPath($path), $raw);
}
/**
* see https://www.php.net/manual/en/function.free_space.php
*
* @param string $path
* @return int|float|bool
*/
public function free_space($path) {
return $this->getWrapperStorage()->free_space($this->getUnjailedPath($path));
}
/**
* search for occurrences of $query in file names
*
* @param string $query
* @return array|bool
*/
public function search($query) {
return $this->getWrapperStorage()->search($query);
}
/**
* see https://www.php.net/manual/en/function.touch.php
* If the backend does not support the operation, false should be returned
*
* @param string $path
* @param int $mtime
* @return bool
*/
public function touch($path, $mtime = null) {
return $this->getWrapperStorage()->touch($this->getUnjailedPath($path), $mtime);
}
/**
* get the path to a local version of the file.
* The local version of the file can be temporary and doesn't have to be persistent across requests
*
* @param string $path
* @return string|false
*/
public function getLocalFile($path) {
return $this->getWrapperStorage()->getLocalFile($this->getUnjailedPath($path));
}
/**
* check if a file or folder has been updated since $time
*
* @param string $path
* @param int $time
* @return bool
*
* hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed.
* returning true for other changes in the folder is optional
*/
public function hasUpdated($path, $time) {
return $this->getWrapperStorage()->hasUpdated($this->getUnjailedPath($path), $time);
}
/**
* get a cache instance for the storage
*
* @param string $path
* @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache
* @return \OC\Files\Cache\Cache
*/
public function getCache($path = '', $storage = null) {
$sourceCache = $this->getWrapperStorage()->getCache($this->getUnjailedPath($path));
return new CacheJail($sourceCache, $this->rootPath);
}
/**
* get the user id of the owner of a file or folder
*
* @param string $path
* @return string
*/
public function getOwner($path) {
return $this->getWrapperStorage()->getOwner($this->getUnjailedPath($path));
}
/**
* get a watcher instance for the cache
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
* @return \OC\Files\Cache\Watcher
*/
public function getWatcher($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
return $this->getWrapperStorage()->getWatcher($this->getUnjailedPath($path), $storage);
}
/**
* get the ETag for a file or folder
*
* @param string $path
* @return string|false
*/
public function getETag($path) {
return $this->getWrapperStorage()->getETag($this->getUnjailedPath($path));
}
public function getMetaData($path) {
return $this->getWrapperStorage()->getMetaData($this->getUnjailedPath($path));
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
* @throws \OCP\Lock\LockedException
*/
public function acquireLock($path, $type, ILockingProvider $provider) {
$this->getWrapperStorage()->acquireLock($this->getUnjailedPath($path), $type, $provider);
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
*/
public function releaseLock($path, $type, ILockingProvider $provider) {
$this->getWrapperStorage()->releaseLock($this->getUnjailedPath($path), $type, $provider);
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
*/
public function changeLock($path, $type, ILockingProvider $provider) {
$this->getWrapperStorage()->changeLock($this->getUnjailedPath($path), $type, $provider);
}
/**
* Resolve the path for the source of the share
*
* @param string $path
* @return array
*/
public function resolvePath($path) {
return [$this->getWrapperStorage(), $this->getUnjailedPath($path)];
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if ($sourceStorage === $this) {
return $this->copy($sourceInternalPath, $targetInternalPath);
}
return $this->getWrapperStorage()->copyFromStorage($sourceStorage, $sourceInternalPath, $this->getUnjailedPath($targetInternalPath));
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if ($sourceStorage === $this) {
return $this->rename($sourceInternalPath, $targetInternalPath);
}
return $this->getWrapperStorage()->moveFromStorage($sourceStorage, $sourceInternalPath, $this->getUnjailedPath($targetInternalPath));
}
public function getPropagator($storage = null) {
if (isset($this->propagator)) {
return $this->propagator;
}
if (!$storage) {
$storage = $this;
}
$this->propagator = new JailPropagator($storage, \OC::$server->getDatabaseConnection());
return $this->propagator;
}
public function writeStream(string $path, $stream, int $size = null): int {
$storage = $this->getWrapperStorage();
if ($storage->instanceOfStorage(IWriteStreamStorage::class)) {
/** @var IWriteStreamStorage $storage */
return $storage->writeStream($this->getUnjailedPath($path), $stream, $size);
} else {
$target = $this->fopen($path, 'w');
[$count, $result] = \OC_Helper::streamCopy($stream, $target);
fclose($stream);
fclose($target);
return $count;
}
}
public function getDirectoryContent($directory): \Traversable {
return $this->getWrapperStorage()->getDirectoryContent($this->getUnjailedPath($directory));
}
}
@@ -0,0 +1,142 @@
<?php
namespace OC\Files\Storage\Wrapper;
use OCP\Cache\CappedMemoryCache;
use OCP\Files\Storage\IStorage;
use Psr\Clock\ClockInterface;
/**
* Wrapper that overwrites the mtime return by stat/getMetaData if the returned value
* is lower than when we last modified the file.
*
* This is useful because some storage servers can return an outdated mtime right after writes
*/
class KnownMtime extends Wrapper {
private CappedMemoryCache $knowMtimes;
private ClockInterface $clock;
public function __construct($arguments) {
parent::__construct($arguments);
$this->knowMtimes = new CappedMemoryCache();
$this->clock = $arguments['clock'];
}
public function file_put_contents($path, $data) {
$result = parent::file_put_contents($path, $data);
if ($result) {
$now = $this->clock->now()->getTimestamp();
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
}
return $result;
}
public function stat($path) {
$stat = parent::stat($path);
if ($stat) {
$this->applyKnownMtime($path, $stat);
}
return $stat;
}
public function getMetaData($path) {
$stat = parent::getMetaData($path);
if ($stat) {
$this->applyKnownMtime($path, $stat);
}
return $stat;
}
private function applyKnownMtime(string $path, array &$stat) {
if (isset($stat['mtime'])) {
$knownMtime = $this->knowMtimes->get($path) ?? 0;
$stat['mtime'] = max($stat['mtime'], $knownMtime);
}
}
public function filemtime($path) {
$knownMtime = $this->knowMtimes->get($path) ?? 0;
return max(parent::filemtime($path), $knownMtime);
}
public function mkdir($path) {
$result = parent::mkdir($path);
if ($result) {
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
}
return $result;
}
public function rmdir($path) {
$result = parent::rmdir($path);
if ($result) {
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
}
return $result;
}
public function unlink($path) {
$result = parent::unlink($path);
if ($result) {
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
}
return $result;
}
public function rename($source, $target) {
$result = parent::rename($source, $target);
if ($result) {
$this->knowMtimes->set($target, $this->clock->now()->getTimestamp());
$this->knowMtimes->set($source, $this->clock->now()->getTimestamp());
}
return $result;
}
public function copy($source, $target) {
$result = parent::copy($source, $target);
if ($result) {
$this->knowMtimes->set($target, $this->clock->now()->getTimestamp());
}
return $result;
}
public function fopen($path, $mode) {
$result = parent::fopen($path, $mode);
if ($result && $mode === 'w') {
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
}
return $result;
}
public function touch($path, $mtime = null) {
$result = parent::touch($path, $mtime);
if ($result) {
$this->knowMtimes->set($path, $mtime ?? $this->clock->now()->getTimestamp());
}
return $result;
}
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
$result = parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
if ($result) {
$this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp());
}
return $result;
}
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
$result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
if ($result) {
$this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp());
}
return $result;
}
public function writeStream(string $path, $stream, int $size = null): int {
$result = parent::writeStream($path, $stream, $size);
if ($result) {
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
}
return $result;
}
}
@@ -0,0 +1,164 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @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 Stefan Weil <sw@weilnetz.de>
*
* @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 OC\Files\Storage\Wrapper;
use OC\Files\Cache\Wrapper\CachePermissionsMask;
use OCP\Constants;
/**
* Mask the permissions of a storage
*
* This can be used to restrict update, create, delete and/or share permissions of a storage
*
* Note that the read permissions can't be masked
*/
class PermissionsMask extends Wrapper {
/**
* @var int the permissions bits we want to keep
*/
private $mask;
/**
* @param array $arguments ['storage' => $storage, 'mask' => $mask]
*
* $storage: The storage the permissions mask should be applied on
* $mask: The permission bits that should be kept, a combination of the \OCP\Constant::PERMISSION_ constants
*/
public function __construct($arguments) {
parent::__construct($arguments);
$this->mask = $arguments['mask'];
}
private function checkMask($permissions) {
return ($this->mask & $permissions) === $permissions;
}
public function isUpdatable($path) {
return $this->checkMask(Constants::PERMISSION_UPDATE) and parent::isUpdatable($path);
}
public function isCreatable($path) {
return $this->checkMask(Constants::PERMISSION_CREATE) and parent::isCreatable($path);
}
public function isDeletable($path) {
return $this->checkMask(Constants::PERMISSION_DELETE) and parent::isDeletable($path);
}
public function isSharable($path) {
return $this->checkMask(Constants::PERMISSION_SHARE) and parent::isSharable($path);
}
public function getPermissions($path) {
return $this->storage->getPermissions($path) & $this->mask;
}
public function rename($source, $target) {
//This is a rename of the transfer file to the original file
if (dirname($source) === dirname($target) && strpos($source, '.ocTransferId') > 0) {
return $this->checkMask(Constants::PERMISSION_CREATE) and parent::rename($source, $target);
}
return $this->checkMask(Constants::PERMISSION_UPDATE) and parent::rename($source, $target);
}
public function copy($source, $target) {
return $this->checkMask(Constants::PERMISSION_CREATE) and parent::copy($source, $target);
}
public function touch($path, $mtime = null) {
$permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
return $this->checkMask($permissions) and parent::touch($path, $mtime);
}
public function mkdir($path) {
return $this->checkMask(Constants::PERMISSION_CREATE) and parent::mkdir($path);
}
public function rmdir($path) {
return $this->checkMask(Constants::PERMISSION_DELETE) and parent::rmdir($path);
}
public function unlink($path) {
return $this->checkMask(Constants::PERMISSION_DELETE) and parent::unlink($path);
}
public function file_put_contents($path, $data) {
$permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
return $this->checkMask($permissions) ? parent::file_put_contents($path, $data) : false;
}
public function fopen($path, $mode) {
if ($mode === 'r' or $mode === 'rb') {
return parent::fopen($path, $mode);
} else {
$permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
return $this->checkMask($permissions) ? parent::fopen($path, $mode) : false;
}
}
/**
* get a cache instance for the storage
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the cache
* @return \OC\Files\Cache\Cache
*/
public function getCache($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
$sourceCache = parent::getCache($path, $storage);
return new CachePermissionsMask($sourceCache, $this->mask);
}
public function getMetaData($path) {
$data = parent::getMetaData($path);
if ($data && isset($data['permissions'])) {
$data['scan_permissions'] = $data['scan_permissions'] ?? $data['permissions'];
$data['permissions'] &= $this->mask;
}
return $data;
}
public function getScanner($path = '', $storage = null) {
if (!$storage) {
$storage = $this->storage;
}
return parent::getScanner($path, $storage);
}
public function getDirectoryContent($directory): \Traversable {
foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
$data['scan_permissions'] = $data['scan_permissions'] ?? $data['permissions'];
$data['permissions'] &= $this->mask;
yield $data;
}
}
}
@@ -0,0 +1,273 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author J0WI <J0WI@users.noreply.github.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Julius Härtl <jus@bitgrid.net>
* @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 Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
* @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 OC\Files\Storage\Wrapper;
use OC\Files\Filesystem;
use OC\SystemConfig;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\FileInfo;
use OCP\Files\Storage\IStorage;
class Quota extends Wrapper {
/** @var callable|null */
protected $quotaCallback;
/** @var int|float|null int on 64bits, float on 32bits for bigint */
protected int|float|null $quota;
protected string $sizeRoot;
private SystemConfig $config;
private bool $quotaIncludeExternalStorage;
/**
* @param array $parameters
*/
public function __construct($parameters) {
parent::__construct($parameters);
$this->quota = $parameters['quota'] ?? null;
$this->quotaCallback = $parameters['quotaCallback'] ?? null;
$this->sizeRoot = $parameters['root'] ?? '';
$this->quotaIncludeExternalStorage = $parameters['include_external_storage'] ?? false;
}
/**
* @return int|float quota value
*/
public function getQuota(): int|float {
if ($this->quota === null) {
$quotaCallback = $this->quotaCallback;
if ($quotaCallback === null) {
throw new \Exception("No quota or quota callback provider");
}
$this->quota = $quotaCallback();
}
return $this->quota;
}
private function hasQuota(): bool {
return $this->getQuota() !== FileInfo::SPACE_UNLIMITED;
}
/**
* @param string $path
* @param IStorage $storage
* @return int|float
*/
protected function getSize($path, $storage = null) {
if ($this->quotaIncludeExternalStorage) {
$rootInfo = Filesystem::getFileInfo('', 'ext');
if ($rootInfo) {
return $rootInfo->getSize(true);
}
return FileInfo::SPACE_NOT_COMPUTED;
} else {
$cache = is_null($storage) ? $this->getCache() : $storage->getCache();
$data = $cache->get($path);
if ($data instanceof ICacheEntry && isset($data['size'])) {
return $data['size'];
} else {
return FileInfo::SPACE_NOT_COMPUTED;
}
}
}
/**
* Get free space as limited by the quota
*
* @param string $path
* @return int|float|bool
*/
public function free_space($path) {
if (!$this->hasQuota()) {
return $this->storage->free_space($path);
}
if ($this->getQuota() < 0 || str_starts_with($path, 'cache') || str_starts_with($path, 'uploads')) {
return $this->storage->free_space($path);
} else {
$used = $this->getSize($this->sizeRoot);
if ($used < 0) {
return FileInfo::SPACE_NOT_COMPUTED;
} else {
$free = $this->storage->free_space($path);
$quotaFree = max($this->getQuota() - $used, 0);
// if free space is known
$free = $free >= 0 ? min($free, $quotaFree) : $quotaFree;
return $free;
}
}
}
/**
* see https://www.php.net/manual/en/function.file_put_contents.php
*
* @param string $path
* @param mixed $data
* @return int|float|false
*/
public function file_put_contents($path, $data) {
if (!$this->hasQuota()) {
return $this->storage->file_put_contents($path, $data);
}
$free = $this->free_space($path);
if ($free < 0 || strlen($data) < $free) {
return $this->storage->file_put_contents($path, $data);
} else {
return false;
}
}
/**
* see https://www.php.net/manual/en/function.copy.php
*
* @param string $source
* @param string $target
* @return bool
*/
public function copy($source, $target) {
if (!$this->hasQuota()) {
return $this->storage->copy($source, $target);
}
$free = $this->free_space($target);
if ($free < 0 || $this->getSize($source) < $free) {
return $this->storage->copy($source, $target);
} else {
return false;
}
}
/**
* see https://www.php.net/manual/en/function.fopen.php
*
* @param string $path
* @param string $mode
* @return resource|bool
*/
public function fopen($path, $mode) {
if (!$this->hasQuota()) {
return $this->storage->fopen($path, $mode);
}
$source = $this->storage->fopen($path, $mode);
// don't apply quota for part files
if (!$this->isPartFile($path)) {
$free = $this->free_space($path);
if ($source && (is_int($free) || is_float($free)) && $free >= 0 && $mode !== 'r' && $mode !== 'rb') {
// only apply quota for files, not metadata, trash or others
if ($this->shouldApplyQuota($path)) {
return \OC\Files\Stream\Quota::wrap($source, $free);
}
}
}
return $source;
}
/**
* Checks whether the given path is a part file
*
* @param string $path Path that may identify a .part file
* @return bool
* @note this is needed for reusing keys
*/
private function isPartFile($path) {
$extension = pathinfo($path, PATHINFO_EXTENSION);
return ($extension === 'part');
}
/**
* Only apply quota for files, not metadata, trash or others
*/
private function shouldApplyQuota(string $path): bool {
return str_starts_with(ltrim($path, '/'), 'files/');
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if (!$this->hasQuota()) {
return $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
}
$free = $this->free_space($targetInternalPath);
if ($free < 0 || $this->getSize($sourceInternalPath, $sourceStorage) < $free) {
return $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
} else {
return false;
}
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if (!$this->hasQuota()) {
return $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
}
$free = $this->free_space($targetInternalPath);
if ($free < 0 || $this->getSize($sourceInternalPath, $sourceStorage) < $free) {
return $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
} else {
return false;
}
}
public function mkdir($path) {
if (!$this->hasQuota()) {
return $this->storage->mkdir($path);
}
$free = $this->free_space($path);
if ($this->shouldApplyQuota($path) && $free == 0) {
return false;
}
return parent::mkdir($path);
}
public function touch($path, $mtime = null) {
if (!$this->hasQuota()) {
return $this->storage->touch($path, $mtime);
}
$free = $this->free_space($path);
if ($free == 0) {
return false;
}
return parent::touch($path, $mtime);
}
}
@@ -0,0 +1,657 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Joas Schilling <coding@schilljs.com>
* @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 Thomas Müller <thomas.mueller@tmit.eu>
* @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
* @author Vincent Petry <vincent@nextcloud.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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 OC\Files\Storage\Wrapper;
use OCP\Files\InvalidPathException;
use OCP\Files\Storage\ILockingStorage;
use OCP\Files\Storage\IStorage;
use OCP\Files\Storage\IWriteStreamStorage;
use OCP\Lock\ILockingProvider;
class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStreamStorage {
/**
* @var \OC\Files\Storage\Storage $storage
*/
protected $storage;
public $cache;
public $scanner;
public $watcher;
public $propagator;
public $updater;
/**
* @param array $parameters
*/
public function __construct($parameters) {
$this->storage = $parameters['storage'];
}
/**
* @return \OC\Files\Storage\Storage
*/
public function getWrapperStorage() {
return $this->storage;
}
/**
* Get the identifier for the storage,
* the returned id should be the same for every storage object that is created with the same parameters
* and two storage objects with the same id should refer to two storages that display the same files.
*
* @return string
*/
public function getId() {
return $this->getWrapperStorage()->getId();
}
/**
* see https://www.php.net/manual/en/function.mkdir.php
*
* @param string $path
* @return bool
*/
public function mkdir($path) {
return $this->getWrapperStorage()->mkdir($path);
}
/**
* see https://www.php.net/manual/en/function.rmdir.php
*
* @param string $path
* @return bool
*/
public function rmdir($path) {
return $this->getWrapperStorage()->rmdir($path);
}
/**
* see https://www.php.net/manual/en/function.opendir.php
*
* @param string $path
* @return resource|false
*/
public function opendir($path) {
return $this->getWrapperStorage()->opendir($path);
}
/**
* see https://www.php.net/manual/en/function.is_dir.php
*
* @param string $path
* @return bool
*/
public function is_dir($path) {
return $this->getWrapperStorage()->is_dir($path);
}
/**
* see https://www.php.net/manual/en/function.is_file.php
*
* @param string $path
* @return bool
*/
public function is_file($path) {
return $this->getWrapperStorage()->is_file($path);
}
/**
* see https://www.php.net/manual/en/function.stat.php
* only the following keys are required in the result: size and mtime
*
* @param string $path
* @return array|bool
*/
public function stat($path) {
return $this->getWrapperStorage()->stat($path);
}
/**
* see https://www.php.net/manual/en/function.filetype.php
*
* @param string $path
* @return string|bool
*/
public function filetype($path) {
return $this->getWrapperStorage()->filetype($path);
}
/**
* see https://www.php.net/manual/en/function.filesize.php
* The result for filesize when called on a folder is required to be 0
*/
public function filesize($path): false|int|float {
return $this->getWrapperStorage()->filesize($path);
}
/**
* check if a file can be created in $path
*
* @param string $path
* @return bool
*/
public function isCreatable($path) {
return $this->getWrapperStorage()->isCreatable($path);
}
/**
* check if a file can be read
*
* @param string $path
* @return bool
*/
public function isReadable($path) {
return $this->getWrapperStorage()->isReadable($path);
}
/**
* check if a file can be written to
*
* @param string $path
* @return bool
*/
public function isUpdatable($path) {
return $this->getWrapperStorage()->isUpdatable($path);
}
/**
* check if a file can be deleted
*
* @param string $path
* @return bool
*/
public function isDeletable($path) {
return $this->getWrapperStorage()->isDeletable($path);
}
/**
* check if a file can be shared
*
* @param string $path
* @return bool
*/
public function isSharable($path) {
return $this->getWrapperStorage()->isSharable($path);
}
/**
* get the full permissions of a path.
* Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php
*
* @param string $path
* @return int
*/
public function getPermissions($path) {
return $this->getWrapperStorage()->getPermissions($path);
}
/**
* see https://www.php.net/manual/en/function.file_exists.php
*
* @param string $path
* @return bool
*/
public function file_exists($path) {
return $this->getWrapperStorage()->file_exists($path);
}
/**
* see https://www.php.net/manual/en/function.filemtime.php
*
* @param string $path
* @return int|bool
*/
public function filemtime($path) {
return $this->getWrapperStorage()->filemtime($path);
}
/**
* see https://www.php.net/manual/en/function.file_get_contents.php
*
* @param string $path
* @return string|false
*/
public function file_get_contents($path) {
return $this->getWrapperStorage()->file_get_contents($path);
}
/**
* see https://www.php.net/manual/en/function.file_put_contents.php
*
* @param string $path
* @param mixed $data
* @return int|float|false
*/
public function file_put_contents($path, $data) {
return $this->getWrapperStorage()->file_put_contents($path, $data);
}
/**
* see https://www.php.net/manual/en/function.unlink.php
*
* @param string $path
* @return bool
*/
public function unlink($path) {
return $this->getWrapperStorage()->unlink($path);
}
/**
* see https://www.php.net/manual/en/function.rename.php
*
* @param string $source
* @param string $target
* @return bool
*/
public function rename($source, $target) {
return $this->getWrapperStorage()->rename($source, $target);
}
/**
* see https://www.php.net/manual/en/function.copy.php
*
* @param string $source
* @param string $target
* @return bool
*/
public function copy($source, $target) {
return $this->getWrapperStorage()->copy($source, $target);
}
/**
* see https://www.php.net/manual/en/function.fopen.php
*
* @param string $path
* @param string $mode
* @return resource|bool
*/
public function fopen($path, $mode) {
return $this->getWrapperStorage()->fopen($path, $mode);
}
/**
* get the mimetype for a file or folder
* The mimetype for a folder is required to be "httpd/unix-directory"
*
* @param string $path
* @return string|bool
*/
public function getMimeType($path) {
return $this->getWrapperStorage()->getMimeType($path);
}
/**
* see https://www.php.net/manual/en/function.hash.php
*
* @param string $type
* @param string $path
* @param bool $raw
* @return string|bool
*/
public function hash($type, $path, $raw = false) {
return $this->getWrapperStorage()->hash($type, $path, $raw);
}
/**
* see https://www.php.net/manual/en/function.free_space.php
*
* @param string $path
* @return int|float|bool
*/
public function free_space($path) {
return $this->getWrapperStorage()->free_space($path);
}
/**
* search for occurrences of $query in file names
*
* @param string $query
* @return array|bool
*/
public function search($query) {
return $this->getWrapperStorage()->search($query);
}
/**
* see https://www.php.net/manual/en/function.touch.php
* If the backend does not support the operation, false should be returned
*
* @param string $path
* @param int $mtime
* @return bool
*/
public function touch($path, $mtime = null) {
return $this->getWrapperStorage()->touch($path, $mtime);
}
/**
* get the path to a local version of the file.
* The local version of the file can be temporary and doesn't have to be persistent across requests
*
* @param string $path
* @return string|false
*/
public function getLocalFile($path) {
return $this->getWrapperStorage()->getLocalFile($path);
}
/**
* check if a file or folder has been updated since $time
*
* @param string $path
* @param int $time
* @return bool
*
* hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed.
* returning true for other changes in the folder is optional
*/
public function hasUpdated($path, $time) {
return $this->getWrapperStorage()->hasUpdated($path, $time);
}
/**
* get a cache instance for the storage
*
* @param string $path
* @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache
* @return \OC\Files\Cache\Cache
*/
public function getCache($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
return $this->getWrapperStorage()->getCache($path, $storage);
}
/**
* get a scanner instance for the storage
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
* @return \OC\Files\Cache\Scanner
*/
public function getScanner($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
return $this->getWrapperStorage()->getScanner($path, $storage);
}
/**
* get the user id of the owner of a file or folder
*
* @param string $path
* @return string
*/
public function getOwner($path) {
return $this->getWrapperStorage()->getOwner($path);
}
/**
* get a watcher instance for the cache
*
* @param string $path
* @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
* @return \OC\Files\Cache\Watcher
*/
public function getWatcher($path = '', $storage = null) {
if (!$storage) {
$storage = $this;
}
return $this->getWrapperStorage()->getWatcher($path, $storage);
}
public function getPropagator($storage = null) {
if (!$storage) {
$storage = $this;
}
return $this->getWrapperStorage()->getPropagator($storage);
}
public function getUpdater($storage = null) {
if (!$storage) {
$storage = $this;
}
return $this->getWrapperStorage()->getUpdater($storage);
}
/**
* @return \OC\Files\Cache\Storage
*/
public function getStorageCache() {
return $this->getWrapperStorage()->getStorageCache();
}
/**
* get the ETag for a file or folder
*
* @param string $path
* @return string|false
*/
public function getETag($path) {
return $this->getWrapperStorage()->getETag($path);
}
/**
* Returns true
*
* @return true
*/
public function test() {
return $this->getWrapperStorage()->test();
}
/**
* Returns the wrapped storage's value for isLocal()
*
* @return bool wrapped storage's isLocal() value
*/
public function isLocal() {
return $this->getWrapperStorage()->isLocal();
}
/**
* Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
*
* @param class-string<IStorage> $class
* @return bool
*/
public function instanceOfStorage($class) {
if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') {
// FIXME Temporary fix to keep existing checks working
$class = '\OCA\Files_Sharing\SharedStorage';
}
return is_a($this, $class) or $this->getWrapperStorage()->instanceOfStorage($class);
}
/**
* @psalm-template T of IStorage
* @psalm-param class-string<T> $class
* @psalm-return T|null
*/
public function getInstanceOfStorage(string $class) {
$storage = $this;
while ($storage instanceof Wrapper) {
if ($storage instanceof $class) {
break;
}
$storage = $storage->getWrapperStorage();
}
if (!($storage instanceof $class)) {
return null;
}
return $storage;
}
/**
* Pass any methods custom to specific storage implementations to the wrapped storage
*
* @param string $method
* @param array $args
* @return mixed
*/
public function __call($method, $args) {
return call_user_func_array([$this->getWrapperStorage(), $method], $args);
}
/**
* A custom storage implementation can return an url for direct download of a give file.
*
* For now the returned array can hold the parameter url - in future more attributes might follow.
*
* @param string $path
* @return array|bool
*/
public function getDirectDownload($path) {
return $this->getWrapperStorage()->getDirectDownload($path);
}
/**
* Get availability of the storage
*
* @return array [ available, last_checked ]
*/
public function getAvailability() {
return $this->getWrapperStorage()->getAvailability();
}
/**
* Set availability of the storage
*
* @param bool $isAvailable
*/
public function setAvailability($isAvailable) {
$this->getWrapperStorage()->setAvailability($isAvailable);
}
/**
* @param string $path the path of the target folder
* @param string $fileName the name of the file itself
* @return void
* @throws InvalidPathException
*/
public function verifyPath($path, $fileName) {
$this->getWrapperStorage()->verifyPath($path, $fileName);
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if ($sourceStorage === $this) {
return $this->copy($sourceInternalPath, $targetInternalPath);
}
return $this->getWrapperStorage()->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
}
/**
* @param IStorage $sourceStorage
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
*/
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
if ($sourceStorage === $this) {
return $this->rename($sourceInternalPath, $targetInternalPath);
}
return $this->getWrapperStorage()->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
}
public function getMetaData($path) {
return $this->getWrapperStorage()->getMetaData($path);
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
* @throws \OCP\Lock\LockedException
*/
public function acquireLock($path, $type, ILockingProvider $provider) {
if ($this->getWrapperStorage()->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->getWrapperStorage()->acquireLock($path, $type, $provider);
}
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
*/
public function releaseLock($path, $type, ILockingProvider $provider) {
if ($this->getWrapperStorage()->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->getWrapperStorage()->releaseLock($path, $type, $provider);
}
}
/**
* @param string $path
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param \OCP\Lock\ILockingProvider $provider
*/
public function changeLock($path, $type, ILockingProvider $provider) {
if ($this->getWrapperStorage()->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
$this->getWrapperStorage()->changeLock($path, $type, $provider);
}
}
/**
* @return bool
*/
public function needsPartFile() {
return $this->getWrapperStorage()->needsPartFile();
}
public function writeStream(string $path, $stream, int $size = null): int {
$storage = $this->getWrapperStorage();
if ($storage->instanceOfStorage(IWriteStreamStorage::class)) {
/** @var IWriteStreamStorage $storage */
return $storage->writeStream($path, $stream, $size);
} else {
$target = $this->fopen($path, 'w');
[$count, $result] = \OC_Helper::streamCopy($stream, $target);
fclose($stream);
fclose($target);
return $count;
}
}
public function getDirectoryContent($directory): \Traversable {
return $this->getWrapperStorage()->getDirectoryContent($directory);
}
}

Some files were not shown because too many files have changed in this diff Show More