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
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);
}
}