diff options
Diffstat (limited to 'lib/private/Files')
97 files changed, 3546 insertions, 3546 deletions
diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 3c871fdf4dc..329466e682d 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -9,6 +9,7 @@ namespace OC\Files\Cache; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OC\DB\Exceptions\DbalException; +use OC\DB\QueryBuilder\Sharded\ShardDefinition; use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchQuery; use OC\Files\Storage\Wrapper\Encryption; @@ -54,7 +55,7 @@ class Cache implements ICache { protected array $partial = []; protected string $storageId; protected Storage $storageCache; - protected IMimeTypeLoader$mimetypeLoader; + protected IMimeTypeLoader $mimetypeLoader; protected IDBConnection $connection; protected SystemConfig $systemConfig; protected LoggerInterface $logger; @@ -73,7 +74,7 @@ class Cache implements ICache { $this->storageId = md5($this->storageId); } if (!$dependencies) { - $dependencies = \OC::$server->get(CacheDependencies::class); + $dependencies = \OCP\Server::get(CacheDependencies::class); } $this->storageCache = new Storage($this->storage, true, $dependencies->getConnection()); $this->mimetypeLoader = $dependencies->getMimeTypeLoader(); @@ -87,9 +88,7 @@ class Cache implements ICache { protected function getQueryBuilder() { return new CacheQueryBuilder( - $this->connection, - $this->systemConfig, - $this->logger, + $this->connection->getQueryBuilder(), $this->metadataManager, ); } @@ -110,7 +109,7 @@ class Cache implements ICache { /** * get the stored metadata of a file or folder * - * @param string | int $file either the path of a file or folder or the file id for a file or folder + * @param string|int $file either the path of a file or folder or the file id for a file or folder * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache */ public function get($file) { @@ -122,25 +121,27 @@ class Cache implements ICache { // normalize file $file = $this->normalize($file); - $query->whereStorageId($this->getNumericStorageId()) - ->wherePath($file); + $query->wherePath($file); } else { //file id $query->whereFileId($file); } + $query->whereStorageId($this->getNumericStorageId()); - $result = $query->execute(); + $result = $query->executeQuery(); $data = $result->fetch(); $result->closeCursor(); - //merge partial data - if (!$data && is_string($file) && isset($this->partial[$file])) { - return $this->partial[$file]; - } elseif (!$data) { - return $data; - } else { + if ($data !== false) { $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray(); return self::cacheEntryFromData($data, $this->mimetypeLoader); + } else { + //merge partial data + if (is_string($file) && isset($this->partial[$file])) { + return $this->partial[$file]; + } } + + return false; } /** @@ -169,6 +170,9 @@ class Cache implements ICache { if ($data['storage_mtime'] == 0) { $data['storage_mtime'] = $data['mtime']; } + if (isset($data['f_permissions'])) { + $data['scan_permissions'] = $data['f_permissions']; + } $data['permissions'] = (int)$data['permissions']; if (isset($data['creation_time'])) { $data['creation_time'] = (int)$data['creation_time']; @@ -201,11 +205,12 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->selectFileCache() ->whereParent($fileId) + ->whereStorageId($this->getNumericStorageId()) ->orderBy('name', 'ASC'); $metadataQuery = $query->selectMetadata(); - $result = $query->execute(); + $result = $query->executeQuery(); $files = $result->fetchAll(); $result->closeCursor(); @@ -249,14 +254,14 @@ class Cache implements ICache { $file = $this->normalize($file); if (isset($this->partial[$file])) { //add any saved partial data - $data = array_merge($this->partial[$file], $data); + $data = array_merge($this->partial[$file]->getData(), $data); unset($this->partial[$file]); } $requiredFields = ['size', 'mtime', 'mimetype']; foreach ($requiredFields as $field) { if (!isset($data[$field])) { //data not complete save as partial and return - $this->partial[$file] = $data; + $this->partial[$file] = new CacheEntry($data); return -1; } } @@ -265,6 +270,9 @@ class Cache implements ICache { if (!isset($data['parent'])) { $data['parent'] = $this->getParentId($file); } + if ($data['parent'] === -1 && $file !== '') { + throw new \Exception('Parent folder not in filecache for ' . $file); + } $data['name'] = basename($file); [$values, $extensionValues] = $this->normalizeData($data); @@ -285,12 +293,13 @@ class Cache implements ICache { if (count($extensionValues)) { $query = $this->getQueryBuilder(); $query->insert('filecache_extended'); + $query->hintShardKey('storage', $storageId); $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)); foreach ($extensionValues as $column => $value) { $query->setValue($column, $query->createNamedParameter($value)); } - $query->execute(); + $query->executeStatement(); } $event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId); @@ -339,6 +348,7 @@ class Cache implements ICache { $query->update('filecache') ->whereFileId($id) + ->whereStorageId($this->getNumericStorageId()) ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) { return $query->expr()->orX( $query->expr()->neq($key, $query->createNamedParameter($value)), @@ -350,13 +360,14 @@ class Cache implements ICache { $query->set($key, $query->createNamedParameter($value)); } - $query->execute(); + $query->executeStatement(); } if (count($extensionValues)) { try { $query = $this->getQueryBuilder(); $query->insert('filecache_extended'); + $query->hintShardKey('storage', $this->getNumericStorageId()); $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)); foreach ($extensionValues as $column => $value) { @@ -368,6 +379,7 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->update('filecache_extended') ->whereFileId($id) + ->hintShardKey('storage', $this->getNumericStorageId()) ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) { return $query->expr()->orX( $query->expr()->neq($key, $query->createNamedParameter($value)), @@ -379,7 +391,7 @@ class Cache implements ICache { $query->set($key, $query->createNamedParameter($value)); } - $query->execute(); + $query->executeStatement(); } } @@ -461,7 +473,7 @@ class Cache implements ICache { ->whereStorageId($this->getNumericStorageId()) ->wherePath($file); - $result = $query->execute(); + $result = $query->executeQuery(); $id = $result->fetchOne(); $result->closeCursor(); @@ -514,13 +526,15 @@ class Cache implements ICache { if ($entry instanceof ICacheEntry) { $query = $this->getQueryBuilder(); $query->delete('filecache') + ->whereStorageId($this->getNumericStorageId()) ->whereFileId($entry->getId()); - $query->execute(); + $query->executeStatement(); $query = $this->getQueryBuilder(); $query->delete('filecache_extended') - ->whereFileId($entry->getId()); - $query->execute(); + ->whereFileId($entry->getId()) + ->hintShardKey('storage', $this->getNumericStorageId()); + $query->executeStatement(); if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) { $this->removeChildren($entry); @@ -563,11 +577,12 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->delete('filecache_extended') - ->where($query->expr()->in('fileid', $query->createParameter('childIds'))); + ->where($query->expr()->in('fileid', $query->createParameter('childIds'))) + ->hintShardKey('storage', $this->getNumericStorageId()); foreach (array_chunk($childIds, 1000) as $childIdChunk) { $query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY); - $query->execute(); + $query->executeStatement(); } /** @var ICacheEntry[] $childFolders */ @@ -585,6 +600,7 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->delete('filecache') + ->whereStorageId($this->getNumericStorageId()) ->whereParentInParameter('parentIds'); // Sorting before chunking allows the db to find the entries close to each @@ -592,7 +608,7 @@ class Cache implements ICache { sort($parentIds, SORT_NUMERIC); foreach (array_chunk($parentIds, 1000) as $parentIdChunk) { $query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY); - $query->execute(); + $query->executeStatement(); } foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) { @@ -647,7 +663,16 @@ class Cache implements ICache { $sourceData = $sourceCache->get($sourcePath); if (!$sourceData) { - throw new \Exception('Invalid source storage path: ' . $sourcePath); + throw new \Exception('Source path not found in cache: ' . $sourcePath); + } + + $shardDefinition = $this->connection->getShardDefinition('filecache'); + if ( + $shardDefinition + && $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId()) + ) { + $this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath); + return; } $sourceId = $sourceData['fileid']; @@ -671,7 +696,7 @@ class Cache implements ICache { $childChunks = array_chunk($childIds, 1000); - $query = $this->connection->getQueryBuilder(); + $query = $this->getQueryBuilder(); $fun = $query->func(); $newPathFunction = $fun->concat( @@ -679,12 +704,15 @@ class Cache implements ICache { $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash ); $query->update('filecache') - ->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT)) ->set('path_hash', $fun->md5($newPathFunction)) ->set('path', $newPathFunction) - ->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT))) + ->whereStorageId($sourceStorageId) ->andWhere($query->expr()->in('fileid', $query->createParameter('files'))); + if ($sourceStorageId !== $targetStorageId) { + $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT); + } + // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) { $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)); @@ -726,19 +754,23 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->update('filecache') - ->set('storage', $query->createNamedParameter($targetStorageId)) ->set('path', $query->createNamedParameter($targetPath)) ->set('path_hash', $query->createNamedParameter(md5($targetPath))) ->set('name', $query->createNamedParameter(basename($targetPath))) ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT)) + ->whereStorageId($sourceStorageId) ->whereFileId($sourceId); + if ($sourceStorageId !== $targetStorageId) { + $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT); + } + // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) { $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)); } - $query->execute(); + $query->executeStatement(); $this->connection->commit(); @@ -773,12 +805,12 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->delete('filecache') ->whereStorageId($this->getNumericStorageId()); - $query->execute(); + $query->executeStatement(); $query = $this->connection->getQueryBuilder(); $query->delete('storages') ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId))); - $query->execute(); + $query->executeStatement(); } /** @@ -803,7 +835,7 @@ class Cache implements ICache { ->whereStorageId($this->getNumericStorageId()) ->wherePath($file); - $result = $query->execute(); + $result = $query->executeQuery(); $size = $result->fetchOne(); $result->closeCursor(); @@ -837,7 +869,7 @@ class Cache implements ICache { * search for files by mimetype * * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image') - * where it will search for all mimetypes in the group ('image/*') + * where it will search for all mimetypes in the group ('image/*') * @return ICacheEntry[] an array of cache entries where the mimetype matches the search */ public function searchByMime($mimetype) { @@ -849,26 +881,30 @@ class Cache implements ICache { return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null)); } - public function searchQuery(ISearchQuery $searchQuery) { - return current($this->querySearchHelper->searchInCaches($searchQuery, [$this])); + public function searchQuery(ISearchQuery $query) { + return current($this->querySearchHelper->searchInCaches($query, [$this])); } /** * Re-calculate the folder size and the size of all parent folders * - * @param string|boolean $path - * @param array $data (optional) meta data of the folder + * @param array|ICacheEntry|null $data (optional) meta data of the folder */ - public function correctFolderSize($path, $data = null, $isBackgroundScan = false) { + public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void { $this->calculateFolderSize($path, $data); + if ($path !== '') { $parent = dirname($path); if ($parent === '.' || $parent === '/') { $parent = ''; } + if ($isBackgroundScan) { $parentData = $this->get($parent); - if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) { + if ($parentData !== false + && $parentData['size'] !== -1 + && $this->getIncompleteChildrenCount($parentData['fileid']) === 0 + ) { $this->correctFolderSize($parent, $parentData, $isBackgroundScan); } } else { @@ -889,9 +925,10 @@ class Cache implements ICache { $query->select($query->func()->count()) ->from('filecache') ->whereParent($fileId) - ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + ->whereStorageId($this->getNumericStorageId()) + ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))); - $result = $query->execute(); + $result = $query->executeQuery(); $size = (int)$result->fetchOne(); $result->closeCursor(); @@ -931,12 +968,13 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->select('size', 'unencrypted_size') ->from('filecache') + ->whereStorageId($this->getNumericStorageId()) ->whereParent($id); if ($ignoreUnknown) { $query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0))); } - $result = $query->execute(); + $result = $query->executeQuery(); $rows = $result->fetchAll(); $result->closeCursor(); @@ -977,8 +1015,8 @@ class Cache implements ICache { } // only set unencrypted size for a folder if any child entries have it set, or the folder is empty - $shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || $entry['unencrypted_size'] > 0; - if ($entry['size'] !== $totalSize || ($entry['unencrypted_size'] !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) { + $shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || ($entry['unencrypted_size'] ?? 0) > 0; + if ($entry['size'] !== $totalSize || (($entry['unencrypted_size'] ?? 0) !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) { if ($shouldWriteUnEncryptedSize) { // if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes if ($unencryptedMax === 0) { @@ -1010,7 +1048,7 @@ class Cache implements ICache { ->from('filecache') ->whereStorageId($this->getNumericStorageId()); - $result = $query->execute(); + $result = $query->executeQuery(); $files = $result->fetchAll(\PDO::FETCH_COLUMN); $result->closeCursor(); @@ -1029,28 +1067,19 @@ class Cache implements ICache { * @return string|false the path of the folder or false when no folder matched */ public function getIncomplete() { - // we select the fileid here first instead of directly selecting the path since this helps mariadb/mysql - // to use the correct index. - // The overhead of this should be minimal since the cost of selecting the path by id should be much lower - // than the cost of finding an item with size < 0 $query = $this->getQueryBuilder(); - $query->select('fileid') + $query->select('path') ->from('filecache') ->whereStorageId($this->getNumericStorageId()) - ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) ->orderBy('fileid', 'DESC') ->setMaxResults(1); - $result = $query->execute(); - $id = $result->fetchOne(); + $result = $query->executeQuery(); + $path = $result->fetchOne(); $result->closeCursor(); - if ($id === false) { - return false; - } - - $path = $this->getPathById($id); - return $path ?? false; + return $path === false ? false : (string)$path; } /** @@ -1066,7 +1095,7 @@ class Cache implements ICache { ->whereStorageId($this->getNumericStorageId()) ->whereFileId($id); - $result = $query->execute(); + $result = $query->executeQuery(); $path = $result->fetchOne(); $result->closeCursor(); @@ -1084,7 +1113,7 @@ class Cache implements ICache { * * @param int $id * @return array first element holding the storage id, second the path - * @deprecated use getPathById() instead + * @deprecated 17.0.0 use getPathById() instead */ public static function getById($id) { $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); @@ -1092,7 +1121,7 @@ class Cache implements ICache { ->from('filecache') ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(); $result->closeCursor(); @@ -1130,7 +1159,7 @@ class Cache implements ICache { */ public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int { if ($sourceEntry->getId() < 0) { - throw new \RuntimeException("Invalid source cache entry on copyFromCache"); + throw new \RuntimeException('Invalid source cache entry on copyFromCache'); } $data = $this->cacheEntryToArray($sourceEntry); @@ -1141,7 +1170,7 @@ class Cache implements ICache { $fileId = $this->put($targetPath, $data); if ($fileId <= 0) { - throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " "); + throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' '); } if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) { $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId()); @@ -1154,7 +1183,7 @@ class Cache implements ICache { } private function cacheEntryToArray(ICacheEntry $entry): array { - return [ + $data = [ 'size' => $entry->getSize(), 'mtime' => $entry->getMTime(), 'storage_mtime' => $entry->getStorageMTime(), @@ -1167,6 +1196,10 @@ class Cache implements ICache { 'upload_time' => $entry->getUploadTime(), 'metadata_etag' => $entry->getMetadataEtag(), ]; + if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) { + $data['permissions'] = $entry['scan_permissions']; + } + return $data; } public function getQueryFilterForStorage(): ISearchOperator { @@ -1180,4 +1213,72 @@ class Cache implements ICache { return null; } } + + private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath): void { + if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) { + $fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourceEntry->getPath()); + } else { + $fileIds = []; + } + $fileIds[] = $sourceEntry->getId(); + + $helper = $this->connection->getCrossShardMoveHelper(); + + $sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId()); + $targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId()); + + $cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds); + $extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds); + $metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds); + + // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark + $removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper(); + + $sourcePathLength = strlen($sourceEntry->getPath()); + foreach ($cacheItems as &$cacheItem) { + if ($cacheItem['path'] === $sourceEntry->getPath()) { + $cacheItem['path'] = $targetPath; + $cacheItem['parent'] = $this->getParentId($targetPath); + $cacheItem['name'] = basename($cacheItem['path']); + } else { + $cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash + } + $cacheItem['path_hash'] = md5($cacheItem['path']); + $cacheItem['storage'] = $this->getNumericStorageId(); + if ($removeEncryptedFlag) { + $cacheItem['encrypted'] = 0; + } + } + + $targetConnection->beginTransaction(); + + try { + $helper->saveItems($targetConnection, 'filecache', $cacheItems); + $helper->saveItems($targetConnection, 'filecache_extended', $extendedItems); + $helper->saveItems($targetConnection, 'files_metadata', $metadataItems); + } catch (\Exception $e) { + $targetConnection->rollback(); + throw $e; + } + + $sourceConnection->beginTransaction(); + + try { + $helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds); + $helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds); + $helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds); + } catch (\Exception $e) { + $targetConnection->rollback(); + $sourceConnection->rollBack(); + throw $e; + } + + try { + $sourceConnection->commit(); + } catch (\Exception $e) { + $targetConnection->rollback(); + throw $e; + } + $targetConnection->commit(); + } } diff --git a/lib/private/Files/Cache/CacheEntry.php b/lib/private/Files/Cache/CacheEntry.php index e9417c8012a..ab5bae316f4 100644 --- a/lib/private/Files/Cache/CacheEntry.php +++ b/lib/private/Files/Cache/CacheEntry.php @@ -110,6 +110,10 @@ class CacheEntry implements ICacheEntry { return $this->data['upload_time'] ?? null; } + public function getParentId(): int { + return $this->data['parent']; + } + public function getData() { return $this->data; } diff --git a/lib/private/Files/Cache/CacheQueryBuilder.php b/lib/private/Files/Cache/CacheQueryBuilder.php index 9bf5f970458..5492452273b 100644 --- a/lib/private/Files/Cache/CacheQueryBuilder.php +++ b/lib/private/Files/Cache/CacheQueryBuilder.php @@ -8,32 +8,27 @@ declare(strict_types=1); */ namespace OC\Files\Cache; -use OC\DB\QueryBuilder\QueryBuilder; -use OC\SystemConfig; +use OC\DB\QueryBuilder\ExtendedQueryBuilder; 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 { +class CacheQueryBuilder extends ExtendedQueryBuilder { private ?string $alias = null; public function __construct( - IDBConnection $connection, - SystemConfig $systemConfig, - LoggerInterface $logger, + IQueryBuilder $queryBuilder, private IFilesMetadataManager $filesMetadataManager, ) { - parent::__construct($connection, $systemConfig, $logger); + parent::__construct($queryBuilder); } public function selectTagUsage(): self { $this - ->select('systemtag.name', 'systemtag.id', 'systemtag.visibility', 'systemtag.editable') + ->select('systemtag.name', 'systemtag.id', 'systemtag.visibility', 'systemtag.editable', 'systemtag.etag', 'systemtag.color') ->selectAlias($this->createFunction('COUNT(filecache.fileid)'), 'number_files') ->selectAlias($this->createFunction('MAX(filecache.fileid)'), 'ref_file_id') ->from('filecache', 'filecache') @@ -53,7 +48,7 @@ class CacheQueryBuilder extends QueryBuilder { 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') + 'storage_mtime', 'encrypted', "$name.etag", "$name.permissions", 'checksum', 'unencrypted_size') ->from('filecache', $name); if ($joinExtendedCache) { diff --git a/lib/private/Files/Cache/FailedCache.php b/lib/private/Files/Cache/FailedCache.php index 8ba2ac491bf..44c1016ca8e 100644 --- a/lib/private/Files/Cache/FailedCache.php +++ b/lib/private/Files/Cache/FailedCache.php @@ -125,7 +125,7 @@ class FailedCache implements ICache { } public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int { - throw new \Exception("Invalid cache"); + throw new \Exception('Invalid cache'); } public function getQueryFilterForStorage(): ISearchOperator { diff --git a/lib/private/Files/Cache/FileAccess.php b/lib/private/Files/Cache/FileAccess.php index 5818017bd66..c3f3614f3ca 100644 --- a/lib/private/Files/Cache/FileAccess.php +++ b/lib/private/Files/Cache/FileAccess.php @@ -10,6 +10,7 @@ namespace OC\Files\Cache; use OC\FilesMetadata\FilesMetadataManager; use OC\SystemConfig; +use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\IFileAccess; use OCP\Files\IMimeTypeLoader; @@ -31,9 +32,7 @@ class FileAccess implements IFileAccess { private function getQuery(): CacheQueryBuilder { return new CacheQueryBuilder( - $this->connection, - $this->systemConfig, - $this->logger, + $this->connection->getQueryBuilder(), $this->metadataManager, ); } @@ -96,4 +95,128 @@ class FileAccess implements IFileAccess { $rows = $query->executeQuery()->fetchAll(); return $this->rowsToEntries($rows); } + + public function getByAncestorInStorage(int $storageId, int $folderId, int $fileIdCursor = 0, int $maxResults = 100, array $mimeTypeIds = [], bool $endToEndEncrypted = true, bool $serverSideEncrypted = true): \Generator { + $qb = $this->getQuery(); + $qb->select('path') + ->from('filecache') + ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($folderId, IQueryBuilder::PARAM_INT))); + $result = $qb->executeQuery(); + /** @var array{path:string}|false $root */ + $root = $result->fetch(); + $result->closeCursor(); + + if ($root === false) { + throw new Exception('Could not fetch storage root'); + } + + $qb = $this->getQuery(); + + $path = $root['path'] === '' ? '' : $root['path'] . '/'; + + $qb->selectDistinct('*') + ->from('filecache', 'f') + ->where($qb->expr()->like('f.path', $qb->createNamedParameter($this->connection->escapeLikeParameter($path) . '%'))) + ->andWhere($qb->expr()->eq('f.storage', $qb->createNamedParameter($storageId))) + ->andWhere($qb->expr()->gt('f.fileid', $qb->createNamedParameter($fileIdCursor, IQueryBuilder::PARAM_INT))); + + if (!$endToEndEncrypted) { + // End to end encrypted files are descendants of a folder with encrypted=1 + // Use a subquery to check the `encrypted` status of the parent folder + $subQuery = $this->getQuery()->select('p.encrypted') + ->from('filecache', 'p') + ->andWhere($qb->expr()->eq('p.fileid', 'f.parent')) + ->getSQL(); + + $qb->andWhere( + $qb->expr()->eq($qb->createFunction(sprintf('(%s)', $subQuery)), $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)) + ); + } + + if (!$serverSideEncrypted) { + // Server side encrypted files have encrypted=1 directly + $qb->andWhere($qb->expr()->eq('f.encrypted', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + } + + if (count($mimeTypeIds) > 0) { + $qb->andWhere($qb->expr()->in('f.mimetype', $qb->createNamedParameter($mimeTypeIds, IQueryBuilder::PARAM_INT_ARRAY))); + } + + if ($maxResults !== 0) { + $qb->setMaxResults($maxResults); + } + $qb->orderBy('f.fileid', 'ASC'); + $files = $qb->executeQuery(); + + while ( + /** @var array */ + $row = $files->fetch() + ) { + yield Cache::cacheEntryFromData($row, $this->mimeTypeLoader); + } + + $files->closeCursor(); + } + + public function getDistinctMounts(array $mountProviders = [], bool $onlyUserFilesMounts = true): \Generator { + $qb = $this->connection->getQueryBuilder(); + $qb->selectDistinct(['root_id', 'storage_id', 'mount_provider_class']) + ->from('mounts'); + if ($onlyUserFilesMounts) { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->like('mount_point', $qb->createNamedParameter('/%/files/%')), + $qb->expr()->in('mount_provider_class', $qb->createNamedParameter([ + \OC\Files\Mount\LocalHomeMountProvider::class, + \OC\Files\Mount\ObjectHomeMountProvider::class, + ], IQueryBuilder::PARAM_STR_ARRAY)) + ) + ); + } + if (count($mountProviders) > 0) { + $qb->andWhere($qb->expr()->in('mount_provider_class', $qb->createNamedParameter($mountProviders, IQueryBuilder::PARAM_STR_ARRAY))); + } + $qb->orderBy('root_id', 'ASC'); + $result = $qb->executeQuery(); + + while ( + /** @var array{storage_id:int, root_id:int,mount_provider_class:string} $row */ + $row = $result->fetch() + ) { + $storageId = (int)$row['storage_id']; + $rootId = (int)$row['root_id']; + $overrideRoot = $rootId; + // LocalHomeMountProvider is the default provider for user home directories + // ObjectHomeMountProvider is the home directory provider for when S3 primary storage is used + if ($onlyUserFilesMounts && in_array($row['mount_provider_class'], [ + \OC\Files\Mount\LocalHomeMountProvider::class, + \OC\Files\Mount\ObjectHomeMountProvider::class, + ], true)) { + // Only crawl files, not cache or trashbin + $qb = $this->getQuery(); + try { + $qb->select('fileid') + ->from('filecache') + ->where($qb->expr()->eq('storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($rootId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('path', $qb->createNamedParameter('files'))); + /** @var array|false $root */ + $root = $qb->executeQuery()->fetch(); + if ($root !== false) { + $overrideRoot = (int)$root['fileid']; + } + } catch (Exception $e) { + $this->logger->error('Could not fetch home storage files root for storage ' . $storageId, ['exception' => $e]); + continue; + } + } + // Reference to root_id is still necessary even if we have the overridden_root_id, because storage_id and root_id uniquely identify a mount + yield [ + 'storage_id' => $storageId, + 'root_id' => $rootId, + 'overridden_root' => $overrideRoot, + ]; + } + $result->closeCursor(); + } } diff --git a/lib/private/Files/Cache/Propagator.php b/lib/private/Files/Cache/Propagator.php index 5580dcf22a8..a6ba87896f4 100644 --- a/lib/private/Files/Cache/Propagator.php +++ b/lib/private/Files/Cache/Propagator.php @@ -13,6 +13,8 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\IPropagator; use OCP\Files\Storage\IReliableEtagStorage; use OCP\IDBConnection; +use OCP\Server; +use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; /** @@ -39,13 +41,15 @@ class Propagator implements IPropagator { */ private $ignore = []; + private ClockInterface $clock; + public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) { $this->storage = $storage; $this->connection = $connection; $this->ignore = $ignore; + $this->clock = Server::get(ClockInterface::class); } - /** * @param string $internalPath * @param int $time @@ -59,7 +63,9 @@ class Propagator implements IPropagator { } } - $storageId = (int)$this->storage->getStorageCache()->getNumericId(); + $time = min((int)$time, $this->clock->now()->getTimestamp()); + + $storageId = $this->storage->getStorageCache()->getNumericId(); $parents = $this->getParents($internalPath); @@ -79,7 +85,7 @@ class Propagator implements IPropagator { }, $parentHashes); $builder->update('filecache') - ->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT))) + ->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter($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)) { @@ -186,27 +192,27 @@ class Propagator implements IPropagator { $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))) + ->where($query->expr()->eq('storage', $query->createNamedParameter($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))); + ->where($query->expr()->eq('storage', $sizeQuery->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('path_hash', $sizeQuery->createParameter('hash'))) + ->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->createNamedParameter(-1, IQueryBuilder::PARAM_INT))); foreach ($this->batch as $item) { $query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT); $query->setParameter('hash', $item['hash']); - $query->execute(); + $query->executeStatement(); if ($item['size']) { $sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT); $sizeQuery->setParameter('hash', $item['hash']); - $sizeQuery->execute(); + $sizeQuery->executeStatement(); } } diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index c31d62e1a86..3ddcf1ca4e6 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -39,9 +40,7 @@ class QuerySearchHelper { protected function getQueryBuilder() { return new CacheQueryBuilder( - $this->connection, - $this->systemConfig, - $this->logger, + $this->connection->getQueryBuilder(), $this->filesMetadataManager, ); } @@ -56,7 +55,7 @@ class QuerySearchHelper { CacheQueryBuilder $query, ISearchQuery $searchQuery, array $caches, - ?IMetadataQuery $metadataQuery = null + ?IMetadataQuery $metadataQuery = null, ): void { $storageFilters = array_values(array_map(function (ICache $cache) { return $cache->getQueryFilterForStorage(); @@ -90,7 +89,7 @@ class QuerySearchHelper { $this->applySearchConstraints($query, $searchQuery, $caches); - $result = $query->execute(); + $result = $query->executeQuery(); $tags = $result->fetchAll(); $result->closeCursor(); return $tags; @@ -112,7 +111,6 @@ class QuerySearchHelper { $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())) @@ -170,7 +168,7 @@ class QuerySearchHelper { $this->applySearchConstraints($query, $searchQuery, $caches, $metadataQuery); - $result = $query->execute(); + $result = $query->executeQuery(); $files = $result->fetchAll(); $rawEntries = array_map(function (array $data) use ($metadataQuery) { @@ -197,7 +195,7 @@ class QuerySearchHelper { 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"); + throw new \InvalidArgumentException('This search operation requires the user to be set in the query'); } return $user; } diff --git a/lib/private/Files/Cache/Scanner.php b/lib/private/Files/Cache/Scanner.php index c85104ac4b9..b067f70b8cb 100644 --- a/lib/private/Files/Cache/Scanner.php +++ b/lib/private/Files/Cache/Scanner.php @@ -15,6 +15,7 @@ use OC\SystemConfig; use OCP\Files\Cache\IScanner; use OCP\Files\ForbiddenException; use OCP\Files\NotFoundException; +use OCP\Files\Storage\ILockingStorage; use OCP\Files\Storage\IReliableEtagStorage; use OCP\IDBConnection; use OCP\Lock\ILockingProvider; @@ -82,7 +83,7 @@ class Scanner extends BasicEmitter implements IScanner { * * @param bool $useTransactions */ - public function setUseTransactions($useTransactions) { + public function setUseTransactions($useTransactions): void { $this->useTransactions = $useTransactions; } @@ -107,9 +108,9 @@ class Scanner extends BasicEmitter implements IScanner { * @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 array|CacheEntry|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 + * @param array|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 */ @@ -121,139 +122,130 @@ class Scanner extends BasicEmitter implements IScanner { 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 (self::isPartialFile($file)) { + return null; + } + + // acquire a lock + if ($lock) { + if ($this->storage->instanceOfStorage(ILockingStorage::class)) { + $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->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider); + if ($this->storage->instanceOfStorage(ILockingStorage::class)) { + $this->storage->releaseLock($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; + } - return null; - } + try { + if ($data === null) { + $this->removeFromCache($file); + } else { + // 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]); + } - 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); + } - $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 === null) { + return null; } - // 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; - } - } + $parentId = $parentData['fileid']; + } + if ($parent) { + $data['parent'] = $parentId; + } - // we only updated unencrypted_size if it's already set - if ($cacheData['unencrypted_size'] === 0) { - unset($data['unencrypted_size']); + $cacheData = $cacheData ?? $this->cache->get($file); + if ($reuseExisting && $cacheData !== false && 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']; } - - // Only update metadata that has changed - // i.e. get all the values in $data that are not present in the cache already - $newData = $this->array_diff_assoc_multi($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; + if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) { + $data['etag'] = $etag; } - } 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']; + // we only updated unencrypted_size if it's already set + if (isset($cacheData['unencrypted_size']) && $cacheData['unencrypted_size'] === 0) { + unset($data['unencrypted_size']); } - // 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]); + /** + * Only update metadata that has changed. + * i.e. get all the values in $data that are not present in the cache already + * + * We need the OC implementation for usage of "getData" method below. + * @var \OC\Files\Cache\CacheEntry $cacheData + */ + $newData = $this->array_diff_assoc_multi($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 { - $this->removeFromCache($file); + unset($data['unencrypted_size']); + $newData = $data; + $fileId = -1; } - } catch (\Exception $e) { - if ($lock) { - if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) { - $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider); - } + if (!empty($newData)) { + // Reset the checksum if the data has changed + $newData['checksum'] = ''; + $newData['parent'] = $parentId; + $data['fileid'] = $this->addToCache($file, $newData, $fileId); } - 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 ($cacheData !== false) { + $data['oldSize'] = $cacheData['size'] ?? 0; + $data['encrypted'] = $cacheData['encrypted'] ?? false; } - } - if ($data && !isset($data['encrypted'])) { - $data['encrypted'] = false; + // 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]); + } + } + } finally { + // release the acquired lock + if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) { + $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider); } - return $data; } - return null; + return $data; } protected function removeFromCache($path) { @@ -318,29 +310,26 @@ class Scanner extends BasicEmitter implements IScanner { 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); - } + + if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) { + $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; + $data = $this->scanFile($path, $reuse, -1, lock: $lock); + + if ($data !== null && $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); - } + if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) { + $this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider); + $this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider); } } return $data; @@ -362,23 +351,23 @@ class Scanner extends BasicEmitter implements IScanner { * */ protected function array_diff_assoc_multi(array $array1, array $array2) { - + $result = []; foreach ($array1 as $key => $value) { - + // if $array2 doesn't have the same key, that's a result if (!array_key_exists($key, $array2)) { $result[$key] = $value; continue; } - + // if $array2's value for the same key is different, that's a result if ($array2[$key] !== $value && !is_array($value)) { $result[$key] = $value; continue; } - + if (is_array($value)) { $nestedDiff = $this->array_diff_assoc_multi($value, $array2[$key]); if (!empty($nestedDiff)) { @@ -394,9 +383,9 @@ class Scanner extends BasicEmitter implements IScanner { * Get the children currently in the cache * * @param int $folderId - * @return array[] + * @return array<string, \OCP\Files\Cache\ICacheEntry> */ - protected function getExistingChildren($folderId) { + protected function getExistingChildren($folderId): array { $existingChildren = []; $children = $this->cache->getFolderContentsById($folderId); foreach ($children as $child) { @@ -488,7 +477,7 @@ class Scanner extends BasicEmitter implements IScanner { $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']); + \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; diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index 32502cb8258..e1d3c42a8a2 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -12,6 +13,7 @@ use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOperator; use OCP\Files\Search\ISearchOrder; +use OCP\FilesMetadata\IFilesMetadataManager; use OCP\FilesMetadata\IMetadataQuery; /** @@ -64,7 +66,7 @@ class SearchBuilder { 'owner' => 'string', ]; - /** @var array<string, int> */ + /** @var array<string, int|string> */ protected static $paramTypeMap = [ 'string' => IQueryBuilder::PARAM_STR, 'integer' => IQueryBuilder::PARAM_INT, @@ -80,13 +82,10 @@ class SearchBuilder { public const TAG_FAVORITE = '_$!<Favorite>!$_'; - /** @var IMimeTypeLoader */ - private $mimetypeLoader; - public function __construct( - IMimeTypeLoader $mimetypeLoader + private IMimeTypeLoader $mimetypeLoader, + private IFilesMetadataManager $filesMetadataManager, ) { - $this->mimetypeLoader = $mimetypeLoader; } /** @@ -110,7 +109,7 @@ class SearchBuilder { public function searchOperatorArrayToDBExprArray( IQueryBuilder $builder, array $operators, - ?IMetadataQuery $metadataQuery = null + ?IMetadataQuery $metadataQuery = null, ) { return array_filter(array_map(function ($operator) use ($builder, $metadataQuery) { return $this->searchOperatorToDBExpr($builder, $operator, $metadataQuery); @@ -120,7 +119,7 @@ class SearchBuilder { public function searchOperatorToDBExpr( IQueryBuilder $builder, ISearchOperator $operator, - ?IMetadataQuery $metadataQuery = null + ?IMetadataQuery $metadataQuery = null, ) { $expr = $builder->expr(); @@ -156,7 +155,7 @@ class SearchBuilder { IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap, - ?IMetadataQuery $metadataQuery = null + ?IMetadataQuery $metadataQuery = null, ) { if ($comparison->getExtra()) { [$field, $value, $type, $paramType] = $this->getExtraOperatorField($comparison, $metadataQuery); @@ -285,12 +284,19 @@ class SearchBuilder { private function getExtraOperatorField(ISearchComparison $operator, IMetadataQuery $metadataQuery): array { - $paramType = self::$fieldTypes[$operator->getField()]; $field = $operator->getField(); $value = $operator->getValue(); $type = $operator->getType(); - switch($operator->getExtra()) { + $knownMetadata = $this->filesMetadataManager->getKnownMetadata(); + $isIndex = $knownMetadata->isIndex($field); + $paramType = $knownMetadata->getType($field) === 'int' ? 'integer' : 'string'; + + if (!$isIndex) { + throw new \InvalidArgumentException('Cannot search non indexed metadata key'); + } + + switch ($operator->getExtra()) { case IMetadataQuery::EXTRA: $metadataQuery->joinIndex($field); // join index table if not joined yet $field = $metadataQuery->getMetadataValueField($field); diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php index 0929907fcff..1a3bda58e6a 100644 --- a/lib/private/Files/Cache/Storage.php +++ b/lib/private/Files/Cache/Storage.php @@ -80,7 +80,7 @@ class Storage { * 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 + * else returns the md5 of $storageId */ public static function adjustStorageId($storageId) { if (strlen($storageId) > 64) { @@ -149,7 +149,7 @@ class Storage { 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']); + \OCP\Server::get(LoggerInterface::class)->info('Storage with ' . $this->storageId . ' marked as unavailable', ['app' => 'lib']); } $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); @@ -157,7 +157,7 @@ class Storage { ->set('available', $query->createNamedParameter($available)) ->set('last_checked', $query->createNamedParameter(time() + $delay)) ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId))); - $query->execute(); + $query->executeStatement(); } /** @@ -182,13 +182,13 @@ class Storage { $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); $query->delete('storages') ->where($query->expr()->eq('id', $query->createNamedParameter($storageId))); - $query->execute(); + $query->executeStatement(); if (!is_null($numericId)) { $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); $query->delete('filecache') ->where($query->expr()->eq('storage', $query->createNamedParameter($numericId))); - $query->execute(); + $query->executeStatement(); } } @@ -213,6 +213,7 @@ class Storage { $query = $db->getQueryBuilder(); $query->delete('filecache') ->where($query->expr()->in('storage', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY))); + $query->runAcrossAllShards(); $query->executeStatement(); $query = $db->getQueryBuilder(); diff --git a/lib/private/Files/Cache/StorageGlobal.php b/lib/private/Files/Cache/StorageGlobal.php index b55c0fcdb91..bab31b1db91 100644 --- a/lib/private/Files/Cache/StorageGlobal.php +++ b/lib/private/Files/Cache/StorageGlobal.php @@ -21,16 +21,14 @@ use OCP\IDBConnection; * @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; + public function __construct( + private IDBConnection $connection, + ) { } /** @@ -42,7 +40,7 @@ class StorageGlobal { ->from('storages') ->where($builder->expr()->in('id', $builder->createNamedParameter(array_values($storageIds), IQueryBuilder::PARAM_STR_ARRAY))); - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { $this->cache[$row['id']] = $row; } @@ -60,7 +58,7 @@ class StorageGlobal { ->from('storages') ->where($builder->expr()->eq('id', $builder->createNamedParameter($storageId))); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(); $result->closeCursor(); @@ -83,7 +81,7 @@ class StorageGlobal { ->from('storages') ->where($builder->expr()->eq('numeric_id', $builder->createNamedParameter($numericId))); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(); $result->closeCursor(); diff --git a/lib/private/Files/Cache/Updater.php b/lib/private/Files/Cache/Updater.php index e8c6d32599e..03681036aa2 100644 --- a/lib/private/Files/Cache/Updater.php +++ b/lib/private/Files/Cache/Updater.php @@ -9,6 +9,8 @@ namespace OC\Files\Cache; use Doctrine\DBAL\Exception\DeadlockException; use OC\Files\FileInfo; +use OC\Files\ObjectStore\ObjectStoreStorage; +use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Cache\IUpdater; use OCP\Files\Storage\IStorage; @@ -114,7 +116,7 @@ class Updater implements IUpdater { } // encryption is a pita and touches the cache itself - if (isset($data['encrypted']) && !!$data['encrypted']) { + if (isset($data['encrypted']) && (bool)$data['encrypted']) { $sizeDifference = null; } @@ -157,13 +159,51 @@ class Updater implements IUpdater { } /** - * Rename a file or folder in the cache and update the size, etag and mtime of the parent folders + * Rename a file or folder in the cache. * * @param IStorage $sourceStorage * @param string $source * @param string $target */ public function renameFromStorage(IStorage $sourceStorage, $source, $target) { + $this->copyOrRenameFromStorage($sourceStorage, $source, $target, function (ICache $sourceCache) use ($sourceStorage, $source, $target) { + // Remove existing cache entry to no reuse the fileId. + 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); + } + }); + } + + /** + * Copy a file or folder in the cache. + */ + public function copyFromStorage(IStorage $sourceStorage, string $source, string $target): void { + $this->copyOrRenameFromStorage($sourceStorage, $source, $target, function (ICache $sourceCache, ICacheEntry $sourceInfo) use ($target) { + $parent = dirname($target); + if ($parent === '.') { + $parent = ''; + } + $parentInCache = $this->cache->inCache($parent); + if (!$parentInCache) { + $parentData = $this->scanner->scan($parent, Scanner::SCAN_SHALLOW, -1, false); + $parentInCache = $parentData !== null; + } + if ($parentInCache) { + $this->cache->copyFromCache($sourceCache, $sourceInfo, $target); + } + }); + } + + /** + * Utility to copy or rename a file or folder in the cache and update the size, etag and mtime of the parent folders + */ + private function copyOrRenameFromStorage(IStorage $sourceStorage, string $source, string $target, callable $operation): void { if (!$this->enabled or Scanner::isPartialFile($source) or Scanner::isPartialFile($target)) { return; } @@ -176,27 +216,25 @@ class Updater implements IUpdater { $sourceInfo = $sourceCache->get($source); - if ($sourceInfo !== false) { - if ($this->cache->inCache($target)) { - $this->cache->remove($target); - } + $sourceExtension = pathinfo($source, PATHINFO_EXTENSION); + $targetExtension = pathinfo($target, PATHINFO_EXTENSION); + $targetIsTrash = preg_match("/^d\d+$/", $targetExtension); - if ($sourceStorage === $this->storage) { - $this->cache->move($source, $target); - } else { - $this->cache->moveFromCache($sourceCache, $source, $target); + if ($sourceInfo !== false) { + if (!$this->storage->instanceOfStorage(ObjectStoreStorage::class)) { + $operation($sourceCache, $sourceInfo); } - $sourceExtension = pathinfo($source, PATHINFO_EXTENSION); - $targetExtension = pathinfo($target, PATHINFO_EXTENSION); - $targetIsTrash = preg_match("/d\d+/", $targetExtension); + $isDir = $sourceInfo->getMimeType() === FileInfo::MIMETYPE_FOLDER; + } else { + $isDir = $this->storage->is_dir($target); + } - 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 ($sourceExtension !== $targetExtension && !$isDir && !$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) { @@ -246,7 +284,7 @@ class Updater implements IUpdater { // 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]); + $this->logger->warning('Error while updating parent storage_mtime, should be safe to ignore', ['exception' => $e]); } } } diff --git a/lib/private/Files/Cache/Watcher.php b/lib/private/Files/Cache/Watcher.php index 2e42b716695..f1de5d3cfb8 100644 --- a/lib/private/Files/Cache/Watcher.php +++ b/lib/private/Files/Cache/Watcher.php @@ -33,6 +33,9 @@ class Watcher implements IWatcher { */ protected $scanner; + /** @var callable[] */ + protected $onUpdate = []; + /** * @param \OC\Files\Storage\Storage $storage */ @@ -100,6 +103,9 @@ class Watcher implements IWatcher { if ($this->cache instanceof Cache) { $this->cache->correctFolderSize($path); } + foreach ($this->onUpdate as $callback) { + $callback($path); + } } /** @@ -112,7 +118,7 @@ class Watcher implements IWatcher { 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 $cachedData['storage_mtime'] === null || $this->storage->hasUpdated($path, $cachedData['storage_mtime']); } return false; } @@ -130,4 +136,11 @@ class Watcher implements IWatcher { } } } + + /** + * register a callback to be called whenever the watcher triggers and update + */ + public function onUpdate(callable $callback): void { + $this->onUpdate[] = $callback; + } } diff --git a/lib/private/Files/Cache/Wrapper/CacheJail.php b/lib/private/Files/Cache/Wrapper/CacheJail.php index ea0f992114a..5bc4ee8529d 100644 --- a/lib/private/Files/Cache/Wrapper/CacheJail.php +++ b/lib/private/Files/Cache/Wrapper/CacheJail.php @@ -21,27 +21,29 @@ use OCP\Files\Search\ISearchOperator; * Jail to a subdirectory of the wrapped cache */ class CacheJail extends CacheWrapper { - /** - * @var string - */ - protected $root; - protected $unjailedRoot; + + protected string $unjailedRoot; public function __construct( ?ICache $cache, - string $root, + protected string $root, ?CacheDependencies $dependencies = null, ) { parent::__construct($cache, $dependencies); - $this->root = $root; - if ($cache instanceof CacheJail) { - $this->unjailedRoot = $cache->getSourcePath($root); - } else { - $this->unjailedRoot = $root; + $this->unjailedRoot = $root; + $parent = $cache; + while ($parent instanceof CacheWrapper) { + if ($parent instanceof CacheJail) { + $this->unjailedRoot = $parent->getSourcePath($this->unjailedRoot); + } + $parent = $parent->getCache(); } } + /** + * @return string + */ protected function getRoot() { return $this->root; } @@ -51,11 +53,14 @@ class CacheJail extends CacheWrapper { * * @return string */ - protected function getGetUnjailedRoot() { + public function getGetUnjailedRoot() { return $this->unjailedRoot; } - protected function getSourcePath($path) { + /** + * @return string + */ + protected function getSourcePath(string $path) { if ($path === '') { return $this->getRoot(); } else { @@ -95,7 +100,7 @@ class CacheJail extends CacheWrapper { /** * get the stored metadata of a file or folder * - * @param string /int $file + * @param string|int $file * @return ICacheEntry|false */ public function get($file) { @@ -206,12 +211,12 @@ class CacheJail extends CacheWrapper { /** * update the folder size and the size of all parent folders * - * @param string|boolean $path - * @param array $data (optional) meta data of the folder + * @param array|ICacheEntry|null $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); + public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void { + $cache = $this->getCache(); + if ($cache instanceof Cache) { + $cache->correctFolderSize($this->getSourcePath($path), $data, $isBackgroundScan); } } @@ -223,8 +228,9 @@ class CacheJail extends CacheWrapper { * @return int|float */ public function calculateFolderSize($path, $entry = null) { - if ($this->getCache() instanceof Cache) { - return $this->getCache()->calculateFolderSize($this->getSourcePath($path), $entry); + $cache = $this->getCache(); + if ($cache instanceof Cache) { + return $cache->calculateFolderSize($this->getSourcePath($path), $entry); } else { return 0; } diff --git a/lib/private/Files/Cache/Wrapper/CacheWrapper.php b/lib/private/Files/Cache/Wrapper/CacheWrapper.php index 17f1031d1cc..f2f1036d6a3 100644 --- a/lib/private/Files/Cache/Wrapper/CacheWrapper.php +++ b/lib/private/Files/Cache/Wrapper/CacheWrapper.php @@ -214,19 +214,19 @@ class CacheWrapper extends Cache { return $this->getCache()->getStatus($file); } - public function searchQuery(ISearchQuery $searchQuery) { - return current($this->querySearchHelper->searchInCaches($searchQuery, [$this])); + public function searchQuery(ISearchQuery $query) { + return current($this->querySearchHelper->searchInCaches($query, [$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 + * @param array|ICacheEntry|null $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); + public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void { + $cache = $this->getCache(); + if ($cache instanceof Cache) { + $cache->correctFolderSize($path, $data, $isBackgroundScan); } } @@ -238,8 +238,9 @@ class CacheWrapper extends Cache { * @return int|float */ public function calculateFolderSize($path, $entry = null) { - if ($this->getCache() instanceof Cache) { - return $this->getCache()->calculateFolderSize($path, $entry); + $cache = $this->getCache(); + if ($cache instanceof Cache) { + return $cache->calculateFolderSize($path, $entry); } else { return 0; } diff --git a/lib/private/Files/Cache/Wrapper/JailPropagator.php b/lib/private/Files/Cache/Wrapper/JailPropagator.php index 19ca4a13ece..d6409b7875e 100644 --- a/lib/private/Files/Cache/Wrapper/JailPropagator.php +++ b/lib/private/Files/Cache/Wrapper/JailPropagator.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Files/Cache/Wrapper/JailWatcher.php b/lib/private/Files/Cache/Wrapper/JailWatcher.php index 9bd7da57233..b1ae516654a 100644 --- a/lib/private/Files/Cache/Wrapper/JailWatcher.php +++ b/lib/private/Files/Cache/Wrapper/JailWatcher.php @@ -55,4 +55,7 @@ class JailWatcher extends Watcher { $this->watcher->cleanFolder($this->getSourcePath($path)); } + public function onUpdate(callable $callback): void { + $this->watcher->onUpdate($callback); + } } diff --git a/lib/private/Files/Config/CachedMountFileInfo.php b/lib/private/Files/Config/CachedMountFileInfo.php index 41dbec87ef5..69bd4e9301e 100644 --- a/lib/private/Files/Config/CachedMountFileInfo.php +++ b/lib/private/Files/Config/CachedMountFileInfo.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -19,7 +20,7 @@ class CachedMountFileInfo extends CachedMountInfo implements ICachedMountFileInf ?int $mountId, string $mountProvider, string $rootInternalPath, - string $internalPath + string $internalPath, ) { parent::__construct($user, $storageId, $rootId, $mountPoint, $mountProvider, $mountId, $rootInternalPath); $this->internalPath = $internalPath; diff --git a/lib/private/Files/Config/CachedMountInfo.php b/lib/private/Files/Config/CachedMountInfo.php index 80423dcae40..79dd6c6ea1d 100644 --- a/lib/private/Files/Config/CachedMountInfo.php +++ b/lib/private/Files/Config/CachedMountInfo.php @@ -39,7 +39,7 @@ class CachedMountInfo implements ICachedMountInfo { string $mountPoint, string $mountProvider, ?int $mountId = null, - string $rootInternalPath = '' + string $rootInternalPath = '', ) { $this->user = $user; $this->storageId = $storageId; diff --git a/lib/private/Files/Config/MountProviderCollection.php b/lib/private/Files/Config/MountProviderCollection.php index 0e103690b6b..9d63184e05f 100644 --- a/lib/private/Files/Config/MountProviderCollection.php +++ b/lib/private/Files/Config/MountProviderCollection.php @@ -24,60 +24,43 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { use EmitterTrait; /** - * @var \OCP\Files\Config\IHomeMountProvider[] + * @var list<IHomeMountProvider> */ - private $homeProviders = []; + private array $homeProviders = []; /** - * @var \OCP\Files\Config\IMountProvider[] + * @var list<IMountProvider> */ - private $providers = []; + private array $providers = []; - /** @var \OCP\Files\Config\IRootMountProvider[] */ - private $rootProviders = []; + /** @var list<IRootMountProvider> */ + private array $rootProviders = []; - /** - * @var \OCP\Files\Storage\IStorageFactory - */ - private $loader; - - /** - * @var \OCP\Files\Config\IUserMountCache - */ - private $mountCache; - - /** @var callable[] */ - private $mountFilters = []; + /** @var list<callable> */ + private array $mountFilters = []; - private IEventLogger $eventLogger; - - /** - * @param \OCP\Files\Storage\IStorageFactory $loader - * @param IUserMountCache $mountCache - */ public function __construct( - IStorageFactory $loader, - IUserMountCache $mountCache, - IEventLogger $eventLogger + private IStorageFactory $loader, + private IUserMountCache $mountCache, + private IEventLogger $eventLogger, ) { - $this->loader = $loader; - $this->mountCache = $mountCache; - $this->eventLogger = $eventLogger; } + /** + * @return list<IMountPoint> + */ private function getMountsFromProvider(IMountProvider $provider, IUser $user, IStorageFactory $loader): array { $class = str_replace('\\', '_', get_class($provider)); $uid = $user->getUID(); $this->eventLogger->start('fs:setup:provider:' . $class, "Getting mounts from $class for $uid"); $mounts = $provider->getMountsForUser($user, $loader) ?? []; $this->eventLogger->end('fs:setup:provider:' . $class); - return $mounts; + return array_values($mounts); } /** - * @param IUser $user - * @param IMountProvider[] $providers - * @return IMountPoint[] + * @param list<IMountProvider> $providers + * @return list<IMountPoint> */ private function getUserMountsForProviders(IUser $user, array $providers): array { $loader = $this->loader; @@ -90,10 +73,16 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { return $this->filterMounts($user, $mounts); } + /** + * @return list<IMountPoint> + */ public function getMountsForUser(IUser $user): array { return $this->getUserMountsForProviders($user, $this->providers); } + /** + * @return list<IMountPoint> + */ public function getUserMountsForProviderClasses(IUser $user, array $mountProviderClasses): array { $providers = array_filter( $this->providers, @@ -102,7 +91,10 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { return $this->getUserMountsForProviders($user, $providers); } - public function addMountForUser(IUser $user, IMountManager $mountManager, ?callable $providerFilter = null) { + /** + * @return list<IMountPoint> + */ + public function addMountForUser(IUser $user, IMountManager $mountManager, ?callable $providerFilter = null): array { // shared mount provider gets to go last since it needs to know existing files // to check for name collisions $firstMounts = []; @@ -131,22 +123,19 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { } $lateMounts = $this->filterMounts($user, $lateMounts); - $this->eventLogger->start("fs:setup:add-mounts", "Add mounts to the filesystem"); + $this->eventLogger->start('fs:setup:add-mounts', 'Add mounts to the filesystem'); array_walk($lateMounts, [$mountManager, 'addMount']); - $this->eventLogger->end("fs:setup:add-mounts"); + $this->eventLogger->end('fs:setup:add-mounts'); - return array_merge($lateMounts, $firstMounts); + return array_values(array_merge($lateMounts, $firstMounts)); } /** * Get the configured home mount for this user * - * @param \OCP\IUser $user - * @return \OCP\Files\Mount\IMountPoint * @since 9.1.0 */ - public function getHomeMountForUser(IUser $user) { - /** @var \OCP\Files\Config\IHomeMountProvider[] $providers */ + public function getHomeMountForUser(IUser $user): IMountPoint { $providers = array_reverse($this->homeProviders); // call the latest registered provider first to give apps an opportunity to overwrite builtin foreach ($providers as $homeProvider) { if ($mount = $homeProvider->getHomeMountForUser($user, $this->loader)) { @@ -159,34 +148,36 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { /** * Add a provider for mount points - * - * @param \OCP\Files\Config\IMountProvider $provider */ - public function registerProvider(IMountProvider $provider) { + public function registerProvider(IMountProvider $provider): void { $this->providers[] = $provider; $this->emit('\OC\Files\Config', 'registerMountProvider', [$provider]); } - public function registerMountFilter(callable $filter) { + public function registerMountFilter(callable $filter): void { $this->mountFilters[] = $filter; } - private function filterMounts(IUser $user, array $mountPoints) { - return array_filter($mountPoints, function (IMountPoint $mountPoint) use ($user) { + /** + * @param list<IMountPoint> $mountPoints + * @return list<IMountPoint> + */ + private function filterMounts(IUser $user, array $mountPoints): array { + return array_values(array_filter($mountPoints, function (IMountPoint $mountPoint) use ($user) { foreach ($this->mountFilters as $filter) { if ($filter($mountPoint, $user) === false) { return false; } } return true; - }); + })); } /** * Add a provider for home mount points * - * @param \OCP\Files\Config\IHomeMountProvider $provider + * @param IHomeMountProvider $provider * @since 9.1.0 */ public function registerHomeProvider(IHomeMountProvider $provider) { @@ -196,21 +187,19 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { /** * Get the mount cache which can be used to search for mounts without setting up the filesystem - * - * @return IUserMountCache */ - public function getMountCache() { + public function getMountCache(): IUserMountCache { return $this->mountCache; } - public function registerRootProvider(IRootMountProvider $provider) { + public function registerRootProvider(IRootMountProvider $provider): void { $this->rootProviders[] = $provider; } /** * Get all root mountpoints * - * @return \OCP\Files\Mount\IMountPoint[] + * @return list<IMountPoint> * @since 20.0.0 */ public function getRootMounts(): array { @@ -223,19 +212,36 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { }, []); if (count($mounts) === 0) { - throw new \Exception("No root mounts provided by any provider"); + throw new \Exception('No root mounts provided by any provider'); } - return $mounts; + return array_values($mounts); } - public function clearProviders() { + public function clearProviders(): void { $this->providers = []; $this->homeProviders = []; $this->rootProviders = []; } + /** + * @return list<IMountProvider> + */ public function getProviders(): array { return $this->providers; } + + /** + * @return list<IHomeMountProvider> + */ + public function getHomeProviders(): array { + return $this->homeProviders; + } + + /** + * @return list<IRootMountProvider> + */ + public function getRootProviders(): array { + return $this->rootProviders; + } } diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 0afdf9cdcc2..3e53a67a044 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -11,6 +11,10 @@ use OC\User\LazyUser; use OCP\Cache\CappedMemoryCache; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Diagnostics\IEventLogger; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Config\Event\UserMountAddedEvent; +use OCP\Files\Config\Event\UserMountRemovedEvent; +use OCP\Files\Config\Event\UserMountUpdatedEvent; use OCP\Files\Config\ICachedMountFileInfo; use OCP\Files\Config\ICachedMountInfo; use OCP\Files\Config\IUserMountCache; @@ -24,8 +28,6 @@ use Psr\Log\LoggerInterface; * Cache mounts points per user in the cache so we can easily look them up */ class UserMountCache implements IUserMountCache { - private IDBConnection $connection; - private IUserManager $userManager; /** * Cached mount info. @@ -37,24 +39,19 @@ class UserMountCache implements IUserMountCache { * @var CappedMemoryCache<string> **/ private CappedMemoryCache $internalPathCache; - private LoggerInterface $logger; /** @var CappedMemoryCache<array> */ private CappedMemoryCache $cacheInfoCache; - private IEventLogger $eventLogger; /** * UserMountCache constructor. */ public function __construct( - IDBConnection $connection, - IUserManager $userManager, - LoggerInterface $logger, - IEventLogger $eventLogger + private IDBConnection $connection, + private IUserManager $userManager, + private LoggerInterface $logger, + private IEventLogger $eventLogger, + private IEventDispatcher $eventDispatcher, ) { - $this->connection = $connection; - $this->userManager = $userManager; - $this->logger = $logger; - $this->eventLogger = $eventLogger; $this->cacheInfoCache = new CappedMemoryCache(); $this->internalPathCache = new CappedMemoryCache(); $this->mountsForUsers = new CappedMemoryCache(); @@ -106,24 +103,39 @@ class UserMountCache implements IUserMountCache { $userUID = $user->getUID(); try { foreach ($addedMounts as $mount) { + $this->logger->debug("Adding mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]); $this->addToCache($mount); /** @psalm-suppress InvalidArgument */ $this->mountsForUsers[$userUID][$mount->getKey()] = $mount; } foreach ($removedMounts as $mount) { + $this->logger->debug("Removing mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]); $this->removeFromCache($mount); unset($this->mountsForUsers[$userUID][$mount->getKey()]); } - foreach ($changedMounts as $mount) { - $this->updateCachedMount($mount); + foreach ($changedMounts as $mountPair) { + $newMount = $mountPair[1]; + $this->logger->debug("Updating mount '{$newMount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $newMount->getMountProvider()]); + $this->updateCachedMount($newMount); /** @psalm-suppress InvalidArgument */ - $this->mountsForUsers[$userUID][$mount->getKey()] = $mount; + $this->mountsForUsers[$userUID][$newMount->getKey()] = $newMount; } $this->connection->commit(); } catch (\Throwable $e) { $this->connection->rollBack(); throw $e; } + + // Only fire events after all mounts have already been adjusted in the database. + foreach ($addedMounts as $mount) { + $this->eventDispatcher->dispatchTyped(new UserMountAddedEvent($mount)); + } + foreach ($removedMounts as $mount) { + $this->eventDispatcher->dispatchTyped(new UserMountRemovedEvent($mount)); + } + foreach ($changedMounts as $mountPair) { + $this->eventDispatcher->dispatchTyped(new UserMountUpdatedEvent($mountPair[0], $mountPair[1])); + } } $this->eventLogger->end('fs:setup:user:register'); } @@ -131,19 +143,19 @@ class UserMountCache implements IUserMountCache { /** * @param array<string, ICachedMountInfo> $newMounts * @param array<string, ICachedMountInfo> $cachedMounts - * @return ICachedMountInfo[] + * @return list<list{0: ICachedMountInfo, 1: ICachedMountInfo}> Pairs of old and new mounts */ - private function findChangedMounts(array $newMounts, array $cachedMounts) { + private function findChangedMounts(array $newMounts, array $cachedMounts): array { $changed = []; foreach ($cachedMounts as $key => $cachedMount) { if (isset($newMounts[$key])) { $newMount = $newMounts[$key]; if ( - $newMount->getStorageId() !== $cachedMount->getStorageId() || - $newMount->getMountId() !== $cachedMount->getMountId() || - $newMount->getMountProvider() !== $cachedMount->getMountProvider() + $newMount->getStorageId() !== $cachedMount->getStorageId() + || $newMount->getMountId() !== $cachedMount->getMountId() + || $newMount->getMountProvider() !== $cachedMount->getMountProvider() ) { - $changed[] = $newMount; + $changed[] = [$cachedMount, $newMount]; } } } @@ -177,7 +189,7 @@ class UserMountCache implements IUserMountCache { ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID()))) ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT))); - $query->execute(); + $query->executeStatement(); } private function removeFromCache(ICachedMountInfo $mount) { @@ -187,7 +199,7 @@ class UserMountCache implements IUserMountCache { ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID()))) ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT))) ->andWhere($builder->expr()->eq('mount_point', $builder->createNamedParameter($mount->getMountPoint()))); - $query->execute(); + $query->executeStatement(); } /** @@ -237,9 +249,9 @@ class UserMountCache implements IUserMountCache { $builder = $this->connection->getQueryBuilder(); $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'mount_provider_class') ->from('mounts', 'm') - ->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($userUID))); + ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userUID))); - $result = $query->execute(); + $result = $query->executeQuery(); $rows = $result->fetchAll(); $result->closeCursor(); @@ -264,7 +276,7 @@ class UserMountCache implements IUserMountCache { $builder = $this->connection->getQueryBuilder(); $query = $builder->select('path') ->from('filecache') - ->where($builder->expr()->eq('fileid', $builder->createPositionalParameter($info->getRootId()))); + ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($info->getRootId()))); return $query->executeQuery()->fetchOne() ?: ''; } @@ -278,13 +290,13 @@ class UserMountCache implements IUserMountCache { $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class') ->from('mounts', 'm') ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) - ->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT))); + ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($numericStorageId, IQueryBuilder::PARAM_INT))); if ($user) { - $query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user))); + $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user))); } - $result = $query->execute(); + $result = $query->executeQuery(); $rows = $result->fetchAll(); $result->closeCursor(); @@ -300,9 +312,9 @@ class UserMountCache implements IUserMountCache { $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class') ->from('mounts', 'm') ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) - ->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT))); + ->where($builder->expr()->eq('root_id', $builder->createNamedParameter($rootFileId, IQueryBuilder::PARAM_INT))); - $result = $query->execute(); + $result = $query->executeQuery(); $rows = $result->fetchAll(); $result->closeCursor(); @@ -321,7 +333,7 @@ class UserMountCache implements IUserMountCache { ->from('filecache') ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(); $result->closeCursor(); @@ -362,9 +374,9 @@ class UserMountCache implements IUserMountCache { return $internalMountPath === '' || str_starts_with($internalPath, $internalMountPath . '/'); }); - $filteredMounts = array_filter($filteredMounts, function (ICachedMountInfo $mount) { + $filteredMounts = array_values(array_filter($filteredMounts, function (ICachedMountInfo $mount) { return $this->userManager->userExists($mount->getUser()->getUID()); - }); + })); return array_map(function (ICachedMountInfo $mount) use ($internalPath) { return new CachedMountFileInfo( @@ -390,7 +402,7 @@ class UserMountCache implements IUserMountCache { $query = $builder->delete('mounts') ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID()))); - $query->execute(); + $query->executeStatement(); } public function removeUserStorageMount($storageId, $userId) { @@ -399,7 +411,7 @@ class UserMountCache implements IUserMountCache { $query = $builder->delete('mounts') ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId))) ->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))); - $query->execute(); + $query->executeStatement(); } public function remoteStorageMounts($storageId) { @@ -407,7 +419,7 @@ class UserMountCache implements IUserMountCache { $query = $builder->delete('mounts') ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))); - $query->execute(); + $query->executeStatement(); } /** @@ -438,7 +450,7 @@ class UserMountCache implements IUserMountCache { ->where($builder->expr()->eq('m.mount_point', $mountPoint)) ->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY))); - $result = $query->execute(); + $result = $query->executeQuery(); $results = []; while ($row = $result->fetch()) { @@ -477,7 +489,7 @@ class UserMountCache implements IUserMountCache { } } - throw new NotFoundException("No cached mount for path " . $path); + throw new NotFoundException('No cached mount for path ' . $path); } public function getMountsInPath(IUser $user, string $path): array { diff --git a/lib/private/Files/Conversion/ConversionManager.php b/lib/private/Files/Conversion/ConversionManager.php new file mode 100644 index 00000000000..2c98a4c6404 --- /dev/null +++ b/lib/private/Files/Conversion/ConversionManager.php @@ -0,0 +1,181 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Files\Conversion; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\ForbiddenException; +use OC\SystemConfig; +use OCP\Files\Conversion\IConversionManager; +use OCP\Files\Conversion\IConversionProvider; +use OCP\Files\File; +use OCP\Files\GenericFileException; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\ITempManager; +use OCP\L10N\IFactory; +use OCP\PreConditionNotMetException; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +class ConversionManager implements IConversionManager { + /** @var string[] */ + private array $preferredApps = [ + 'richdocuments', + ]; + + /** @var list<IConversionProvider> */ + private array $preferredProviders = []; + + /** @var list<IConversionProvider> */ + private array $providers = []; + + private IL10N $l10n; + public function __construct( + private Coordinator $coordinator, + private ContainerInterface $serverContainer, + private IRootFolder $rootFolder, + private ITempManager $tempManager, + private LoggerInterface $logger, + private SystemConfig $config, + IFactory $l10nFactory, + ) { + $this->l10n = $l10nFactory->get('files'); + } + + public function hasProviders(): bool { + $context = $this->coordinator->getRegistrationContext(); + return !empty($context->getFileConversionProviders()); + } + + public function getProviders(): array { + $providers = []; + foreach ($this->getRegisteredProviders() as $provider) { + $providers = array_merge($providers, $provider->getSupportedMimeTypes()); + } + return $providers; + } + + public function convert(File $file, string $targetMimeType, ?string $destination = null): string { + if (!$this->hasProviders()) { + throw new PreConditionNotMetException($this->l10n->t('No file conversion providers available')); + } + + // Operate in mebibytes + $fileSize = $file->getSize() / (1024 * 1024); + $threshold = $this->config->getValue('max_file_conversion_filesize', 100); + if ($fileSize > $threshold) { + throw new GenericFileException($this->l10n->t('File is too large to convert')); + } + + $fileMimeType = $file->getMimetype(); + $validProvider = $this->getValidProvider($fileMimeType, $targetMimeType); + + if ($validProvider !== null) { + // Get the target extension given by the provider + $targetExtension = ''; + foreach ($validProvider->getSupportedMimeTypes() as $mimeProvider) { + if ($mimeProvider->getTo() === $targetMimeType) { + $targetExtension = $mimeProvider->getExtension(); + break; + } + } + // If destination not provided, we use the same path + // as the original file, but with the new extension + if ($destination === null) { + $basename = pathinfo($file->getPath(), PATHINFO_FILENAME); + $parent = $file->getParent(); + $destination = $parent->getFullPath($basename . '.' . $targetExtension); + } + + // If destination doesn't match the target extension, we throw an error + if (pathinfo($destination, PATHINFO_EXTENSION) !== $targetExtension) { + throw new GenericFileException($this->l10n->t('Destination does not match conversion extension')); + } + + // Check destination before converting + $this->checkDestination($destination); + + // Convert the file and write it to the destination + $convertedFile = $validProvider->convertFile($file, $targetMimeType); + $convertedFile = $this->writeToDestination($destination, $convertedFile); + return $convertedFile->getPath(); + } + + throw new RuntimeException($this->l10n->t('Could not convert file')); + } + + /** + * @return list<IConversionProvider> + */ + private function getRegisteredProviders(): array { + $context = $this->coordinator->getRegistrationContext(); + foreach ($context->getFileConversionProviders() as $providerRegistration) { + $class = $providerRegistration->getService(); + $appId = $providerRegistration->getAppId(); + + try { + if (in_array($appId, $this->preferredApps)) { + $this->preferredProviders[$class] = $this->serverContainer->get($class); + continue; + } + + $this->providers[$class] = $this->serverContainer->get($class); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface|Throwable $e) { + $this->logger->error('Failed to load file conversion provider ' . $class, [ + 'exception' => $e, + ]); + } + } + + return array_values(array_merge([], $this->preferredProviders, $this->providers)); + } + + private function checkDestination(string $destination): void { + if (!$this->rootFolder->nodeExists(dirname($destination))) { + throw new ForbiddenException($this->l10n->t('Destination does not exist')); + } + + $folder = $this->rootFolder->get(dirname($destination)); + if (!$folder->isCreatable()) { + throw new ForbiddenException($this->l10n->t('Destination is not creatable')); + } + } + + private function writeToDestination(string $destination, mixed $content): File { + $this->checkDestination($destination); + + if ($this->rootFolder->nodeExists($destination)) { + $file = $this->rootFolder->get($destination); + $parent = $file->getParent(); + + // Folder permissions is already checked in checkDestination method + $newName = $parent->getNonExistingName(basename($destination)); + $destination = $parent->getFullPath($newName); + } + + return $this->rootFolder->newFile($destination, $content); + } + + private function getValidProvider(string $fileMimeType, string $targetMimeType): ?IConversionProvider { + foreach ($this->getRegisteredProviders() as $provider) { + foreach ($provider->getSupportedMimeTypes() as $mimeProvider) { + if ($mimeProvider->getFrom() === $fileMimeType && $mimeProvider->getTo() === $targetMimeType) { + return $provider; + } + } + } + + return null; + } +} diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php index c3dcb531342..0679dc1ae72 100644 --- a/lib/private/Files/FileInfo.php +++ b/lib/private/Files/FileInfo.php @@ -95,11 +95,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { unset($this->data[$offset]); } - /** - * @return mixed - */ - #[\ReturnTypeWillChange] - public function offsetGet($offset) { + public function offsetGet(mixed $offset): mixed { return match ($offset) { 'type' => $this->getType(), 'etag' => $this->getEtag(), @@ -134,7 +130,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * @return int|null */ public function getId() { - return isset($this->data['fileid']) ? (int) $this->data['fileid'] : null; + return isset($this->data['fileid']) ? (int)$this->data['fileid'] : null; } /** @@ -164,7 +160,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * @return string */ public function getEtag() { - $this->updateEntryfromSubMounts(); + $this->updateEntryFromSubMounts(); if (count($this->childEtags) > 0) { $combinedEtag = $this->data['etag'] . '::' . implode('::', $this->childEtags); return md5($combinedEtag); @@ -179,7 +175,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { */ public function getSize($includeMounts = true) { if ($includeMounts) { - $this->updateEntryfromSubMounts(); + $this->updateEntryFromSubMounts(); if ($this->isEncrypted() && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) { return $this->data['unencrypted_size']; @@ -195,8 +191,8 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * @return int */ public function getMTime() { - $this->updateEntryfromSubMounts(); - return (int) $this->data['mtime']; + $this->updateEntryFromSubMounts(); + return (int)$this->data['mtime']; } /** @@ -210,14 +206,14 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * Return the current version used for the HMAC in the encryption app */ public function getEncryptedVersion(): int { - return isset($this->data['encryptedVersion']) ? (int) $this->data['encryptedVersion'] : 1; + return isset($this->data['encryptedVersion']) ? (int)$this->data['encryptedVersion'] : 1; } /** * @return int */ public function getPermissions() { - return (int) $this->data['permissions']; + return (int)$this->data['permissions']; } /** @@ -318,7 +314,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { $this->subMounts = $mounts; } - private function updateEntryfromSubMounts(): void { + private function updateEntryFromSubMounts(): void { if ($this->subMountsUsed) { return; } @@ -379,11 +375,11 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { } public function getCreationTime(): int { - return (int) $this->data['creation_time']; + return (int)$this->data['creation_time']; } public function getUploadTime(): int { - return (int) $this->data['upload_time']; + return (int)$this->data['upload_time']; } public function getParentId(): int { diff --git a/lib/private/Files/FilenameValidator.php b/lib/private/Files/FilenameValidator.php new file mode 100644 index 00000000000..a78c6d3cc3c --- /dev/null +++ b/lib/private/Files/FilenameValidator.php @@ -0,0 +1,335 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Files; + +use OCP\Files\EmptyFileNameException; +use OCP\Files\FileNameTooLongException; +use OCP\Files\IFilenameValidator; +use OCP\Files\InvalidCharacterInPathException; +use OCP\Files\InvalidDirectoryException; +use OCP\Files\InvalidPathException; +use OCP\Files\ReservedWordException; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\L10N\IFactory; +use Psr\Log\LoggerInterface; + +/** + * @since 30.0.0 + */ +class FilenameValidator implements IFilenameValidator { + + public const INVALID_FILE_TYPE = 100; + + private IL10N $l10n; + + /** + * @var list<string> + */ + private array $forbiddenNames = []; + + /** + * @var list<string> + */ + private array $forbiddenBasenames = []; + /** + * @var list<string> + */ + private array $forbiddenCharacters = []; + + /** + * @var list<string> + */ + private array $forbiddenExtensions = []; + + public function __construct( + IFactory $l10nFactory, + private IDBConnection $database, + private IConfig $config, + private LoggerInterface $logger, + ) { + $this->l10n = $l10nFactory->get('core'); + } + + /** + * Get a list of reserved filenames that must not be used + * This list should be checked case-insensitive, all names are returned lowercase. + * @return list<string> + * @since 30.0.0 + */ + public function getForbiddenExtensions(): array { + if (empty($this->forbiddenExtensions)) { + $forbiddenExtensions = $this->getConfigValue('forbidden_filename_extensions', ['.filepart']); + + // Always forbid .part files as they are used internally + $forbiddenExtensions[] = '.part'; + + $this->forbiddenExtensions = array_values($forbiddenExtensions); + } + return $this->forbiddenExtensions; + } + + /** + * Get a list of forbidden filename extensions that must not be used + * This list should be checked case-insensitive, all names are returned lowercase. + * @return list<string> + * @since 30.0.0 + */ + public function getForbiddenFilenames(): array { + if (empty($this->forbiddenNames)) { + $forbiddenNames = $this->getConfigValue('forbidden_filenames', ['.htaccess']); + + // Handle legacy config option + // TODO: Drop with Nextcloud 34 + $legacyForbiddenNames = $this->getConfigValue('blacklisted_files', []); + if (!empty($legacyForbiddenNames)) { + $this->logger->warning('System config option "blacklisted_files" is deprecated and will be removed in Nextcloud 34, use "forbidden_filenames" instead.'); + } + $forbiddenNames = array_merge($legacyForbiddenNames, $forbiddenNames); + + // Ensure we are having a proper string list + $this->forbiddenNames = array_values($forbiddenNames); + } + return $this->forbiddenNames; + } + + /** + * Get a list of forbidden file basenames that must not be used + * This list should be checked case-insensitive, all names are returned lowercase. + * @return list<string> + * @since 30.0.0 + */ + public function getForbiddenBasenames(): array { + if (empty($this->forbiddenBasenames)) { + $forbiddenBasenames = $this->getConfigValue('forbidden_filename_basenames', []); + // Ensure we are having a proper string list + $this->forbiddenBasenames = array_values($forbiddenBasenames); + } + return $this->forbiddenBasenames; + } + + /** + * Get a list of characters forbidden in filenames + * + * Note: Characters in the range [0-31] are always forbidden, + * even if not inside this list (see OCP\Files\Storage\IStorage::verifyPath). + * + * @return list<string> + * @since 30.0.0 + */ + public function getForbiddenCharacters(): array { + if (empty($this->forbiddenCharacters)) { + // Get always forbidden characters + $forbiddenCharacters = str_split(\OCP\Constants::FILENAME_INVALID_CHARS); + + // Get admin defined invalid characters + $additionalChars = $this->config->getSystemValue('forbidden_filename_characters', []); + if (!is_array($additionalChars)) { + $this->logger->error('Invalid system config value for "forbidden_filename_characters" is ignored.'); + $additionalChars = []; + } + $forbiddenCharacters = array_merge($forbiddenCharacters, $additionalChars); + + // Handle legacy config option + // TODO: Drop with Nextcloud 34 + $legacyForbiddenCharacters = $this->config->getSystemValue('forbidden_chars', []); + if (!is_array($legacyForbiddenCharacters)) { + $this->logger->error('Invalid system config value for "forbidden_chars" is ignored.'); + $legacyForbiddenCharacters = []; + } + if (!empty($legacyForbiddenCharacters)) { + $this->logger->warning('System config option "forbidden_chars" is deprecated and will be removed in Nextcloud 34, use "forbidden_filename_characters" instead.'); + } + $forbiddenCharacters = array_merge($legacyForbiddenCharacters, $forbiddenCharacters); + + $this->forbiddenCharacters = array_values($forbiddenCharacters); + } + return $this->forbiddenCharacters; + } + + /** + * @inheritdoc + */ + public function isFilenameValid(string $filename): bool { + try { + $this->validateFilename($filename); + } catch (\OCP\Files\InvalidPathException) { + return false; + } + return true; + } + + /** + * @inheritdoc + */ + public function validateFilename(string $filename): void { + $trimmed = trim($filename); + if ($trimmed === '') { + throw new EmptyFileNameException(); + } + + // the special directories . and .. would cause never ending recursion + // we check the trimmed name here to ensure unexpected trimming will not cause severe issues + if ($trimmed === '.' || $trimmed === '..') { + throw new InvalidDirectoryException($this->l10n->t('Dot files are not allowed')); + } + + // 255 characters is the limit on common file systems (ext/xfs) + // oc_filecache has a 250 char length limit for the filename + if (isset($filename[250])) { + throw new FileNameTooLongException(); + } + + if (!$this->database->supports4ByteText()) { + // verify database - e.g. mysql only 3-byte chars + if (preg_match('%(?: + \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 + | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 + | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 +)%xs', $filename)) { + throw new InvalidCharacterInPathException(); + } + } + + $this->checkForbiddenName($filename); + + $this->checkForbiddenExtension($filename); + + $this->checkForbiddenCharacters($filename); + } + + /** + * Check if the filename is forbidden + * @param string $path Path to check the filename + * @return bool True if invalid name, False otherwise + */ + public function isForbidden(string $path): bool { + // We support paths here as this function is also used in some storage internals + $filename = basename($path); + $filename = mb_strtolower($filename); + + if ($filename === '') { + return false; + } + + // Check for forbidden filenames + $forbiddenNames = $this->getForbiddenFilenames(); + if (in_array($filename, $forbiddenNames)) { + return true; + } + + // Filename is not forbidden + return false; + } + + public function sanitizeFilename(string $name, ?string $charReplacement = null): string { + $forbiddenCharacters = $this->getForbiddenCharacters(); + + if ($charReplacement === null) { + $charReplacement = array_diff(['_', '-', ' '], $forbiddenCharacters); + $charReplacement = reset($charReplacement) ?: ''; + } + if (mb_strlen($charReplacement) !== 1) { + throw new \InvalidArgumentException('No or invalid character replacement given'); + } + + $nameLowercase = mb_strtolower($name); + foreach ($this->getForbiddenExtensions() as $extension) { + if (str_ends_with($nameLowercase, $extension)) { + $name = substr($name, 0, strlen($name) - strlen($extension)); + } + } + + $basename = strlen($name) > 1 + ? substr($name, 0, strpos($name, '.', 1) ?: null) + : $name; + if (in_array(mb_strtolower($basename), $this->getForbiddenBasenames())) { + $name = str_replace($basename, $this->l10n->t('%1$s (renamed)', [$basename]), $name); + } + + if ($name === '') { + $name = $this->l10n->t('renamed file'); + } + + if (in_array(mb_strtolower($name), $this->getForbiddenFilenames())) { + $name = $this->l10n->t('%1$s (renamed)', [$name]); + } + + $name = str_replace($forbiddenCharacters, $charReplacement, $name); + return $name; + } + + protected function checkForbiddenName(string $filename): void { + $filename = mb_strtolower($filename); + if ($this->isForbidden($filename)) { + throw new ReservedWordException($this->l10n->t('"%1$s" is a forbidden file or folder name.', [$filename])); + } + + // Check for forbidden basenames - basenames are the part of the file until the first dot + // (except if the dot is the first character as this is then part of the basename "hidden files") + $basename = substr($filename, 0, strpos($filename, '.', 1) ?: null); + $forbiddenNames = $this->getForbiddenBasenames(); + if (in_array($basename, $forbiddenNames)) { + throw new ReservedWordException($this->l10n->t('"%1$s" is a forbidden prefix for file or folder names.', [$filename])); + } + } + + + /** + * Check if a filename contains any of the forbidden characters + * @param string $filename + * @throws InvalidCharacterInPathException + */ + protected function checkForbiddenCharacters(string $filename): void { + $sanitizedFileName = filter_var($filename, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW); + if ($sanitizedFileName !== $filename) { + throw new InvalidCharacterInPathException(); + } + + foreach ($this->getForbiddenCharacters() as $char) { + if (str_contains($filename, $char)) { + throw new InvalidCharacterInPathException($this->l10n->t('"%1$s" is not allowed inside a file or folder name.', [$char])); + } + } + } + + /** + * Check if a filename has a forbidden filename extension + * @param string $filename The filename to validate + * @throws InvalidPathException + */ + protected function checkForbiddenExtension(string $filename): void { + $filename = mb_strtolower($filename); + // Check for forbidden filename extensions + $forbiddenExtensions = $this->getForbiddenExtensions(); + foreach ($forbiddenExtensions as $extension) { + if (str_ends_with($filename, $extension)) { + if (str_starts_with($extension, '.')) { + throw new InvalidPathException($this->l10n->t('"%1$s" is a forbidden file type.', [$extension]), self::INVALID_FILE_TYPE); + } else { + throw new InvalidPathException($this->l10n->t('Filenames must not end with "%1$s".', [$extension])); + } + } + } + } + + /** + * Helper to get lower case list from config with validation + * @return string[] + */ + private function getConfigValue(string $key, array $fallback): array { + $values = $this->config->getSystemValue($key, $fallback); + if (!is_array($values)) { + $this->logger->error('Invalid system config value for "' . $key . '" is ignored.'); + $values = $fallback; + } + + return array_map(mb_strtolower(...), $values); + } +}; diff --git a/lib/private/Files/Filesystem.php b/lib/private/Files/Filesystem.php index db7420c3c4c..8fe56cf060c 100644 --- a/lib/private/Files/Filesystem.php +++ b/lib/private/Files/Filesystem.php @@ -8,6 +8,7 @@ namespace OC\Files; use OC\Files\Mount\MountPoint; +use OC\Files\Storage\StorageFactory; use OC\User\NoUserException; use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; @@ -178,7 +179,9 @@ class Filesystem { } $mounts = self::getMountManager()->getAll(); - if (!self::getLoader()->addStorageWrapper($wrapperName, $wrapper, $priority, $mounts)) { + /** @var StorageFactory $loader */ + $loader = self::getLoader(); + if (!$loader->addStorageWrapper($wrapperName, $wrapper, $priority, $mounts)) { // do not re-wrap if storage with this name already existed return; } @@ -674,7 +677,7 @@ class Filesystem { * * @param string $path * @param bool|string $includeMountPoints whether to add mountpoint sizes, - * defaults to true + * defaults to true * @return \OC\Files\FileInfo|false False if file does not exist */ public static function getFileInfo($path, $includeMountPoints = true) { diff --git a/lib/private/Files/Mount/HomeMountPoint.php b/lib/private/Files/Mount/HomeMountPoint.php index 0aa4b54ddf0..5a648f08c89 100644 --- a/lib/private/Files/Mount/HomeMountPoint.php +++ b/lib/private/Files/Mount/HomeMountPoint.php @@ -22,7 +22,7 @@ class HomeMountPoint extends MountPoint { ?IStorageFactory $loader = null, ?array $mountOptions = null, ?int $mountId = null, - ?string $mountProvider = null + ?string $mountProvider = null, ) { parent::__construct($storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId, $mountProvider); $this->user = $user; diff --git a/lib/private/Files/Mount/Manager.php b/lib/private/Files/Mount/Manager.php index c2267af3c96..55de488c726 100644 --- a/lib/private/Files/Mount/Manager.php +++ b/lib/private/Files/Mount/Manager.php @@ -84,7 +84,7 @@ class Manager implements IMountManager { if (count($this->mounts) === 0) { $this->setupManager->setupRoot(); if (count($this->mounts) === 0) { - throw new \Exception("No mounts even after explicitly setting up the root mounts"); + throw new \Exception('No mounts even after explicitly setting up the root mounts'); } } @@ -104,7 +104,7 @@ class Manager implements IMountManager { } } - throw new NotFoundException("No mount for path " . $path . " existing mounts (" . count($this->mounts) ."): " . implode(",", array_keys($this->mounts))); + throw new NotFoundException('No mount for path ' . $path . ' existing mounts (' . count($this->mounts) . '): ' . implode(',', array_keys($this->mounts))); } /** diff --git a/lib/private/Files/Mount/MountPoint.php b/lib/private/Files/Mount/MountPoint.php index cb3a3e8a22d..bab2dc8e4bd 100644 --- a/lib/private/Files/Mount/MountPoint.php +++ b/lib/private/Files/Mount/MountPoint.php @@ -75,7 +75,7 @@ class MountPoint implements IMountPoint { ?IStorageFactory $loader = null, ?array $mountOptions = null, ?int $mountId = null, - ?string $mountProvider = null + ?string $mountProvider = null, ) { if (is_null($arguments)) { $arguments = []; @@ -198,7 +198,7 @@ class MountPoint implements IMountPoint { if (is_null($storage)) { return -1; } - $this->numericStorageId = $storage->getStorageCache()->getNumericId(); + $this->numericStorageId = $storage->getCache()->getNumericStorageId(); } return $this->numericStorageId; } diff --git a/lib/private/Files/Mount/ObjectHomeMountProvider.php b/lib/private/Files/Mount/ObjectHomeMountProvider.php index 99c52108fa8..4b088f2c808 100644 --- a/lib/private/Files/Mount/ObjectHomeMountProvider.php +++ b/lib/private/Files/Mount/ObjectHomeMountProvider.php @@ -7,117 +7,39 @@ */ namespace OC\Files\Mount; +use OC\Files\ObjectStore\HomeObjectStoreStorage; +use OC\Files\ObjectStore\PrimaryObjectStoreConfig; use OCP\Files\Config\IHomeMountProvider; +use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorageFactory; -use OCP\IConfig; use OCP\IUser; -use Psr\Log\LoggerInterface; /** * Mount provider for object store home storages */ class ObjectHomeMountProvider implements IHomeMountProvider { - /** - * @var IConfig - */ - private $config; - - /** - * ObjectStoreHomeMountProvider constructor. - * - * @param IConfig $config - */ - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + private PrimaryObjectStoreConfig $objectStoreConfig, + ) { } /** - * Get the cache mount for a user + * Get the home mount for a user * * @param IUser $user * @param IStorageFactory $loader - * @return \OCP\Files\Mount\IMountPoint + * @return ?IMountPoint */ - public function getHomeMountForUser(IUser $user, IStorageFactory $loader) { - $config = $this->getMultiBucketObjectStoreConfig($user); - if ($config === null) { - $config = $this->getSingleBucketObjectStoreConfig($user); - } - - if ($config === null) { + public function getHomeMountForUser(IUser $user, IStorageFactory $loader): ?IMountPoint { + $objectStoreConfig = $this->objectStoreConfig->getObjectStoreConfigForUser($user); + if ($objectStoreConfig === null) { return null; } + $arguments = array_merge($objectStoreConfig['arguments'], [ + 'objectstore' => $this->objectStoreConfig->buildObjectStore($objectStoreConfig), + 'user' => $user, + ]); - return new HomeMountPoint($user, '\OC\Files\ObjectStore\HomeObjectStoreStorage', '/' . $user->getUID(), $config['arguments'], $loader, null, null, self::class); - } - - /** - * @param IUser $user - * @return array|null - */ - private function getSingleBucketObjectStoreConfig(IUser $user) { - $config = $this->config->getSystemValue('objectstore'); - if (!is_array($config)) { - return null; - } - - // sanity checks - if (empty($config['class'])) { - \OC::$server->get(LoggerInterface::class)->error('No class given for objectstore', ['app' => 'files']); - } - if (!isset($config['arguments'])) { - $config['arguments'] = []; - } - // instantiate object store implementation - $config['arguments']['objectstore'] = new $config['class']($config['arguments']); - - $config['arguments']['user'] = $user; - - return $config; - } - - /** - * @param IUser $user - * @return array|null - */ - private function getMultiBucketObjectStoreConfig(IUser $user) { - $config = $this->config->getSystemValue('objectstore_multibucket'); - if (!is_array($config)) { - return null; - } - - // sanity checks - if (empty($config['class'])) { - \OC::$server->get(LoggerInterface::class)->error('No class given for objectstore', ['app' => 'files']); - } - if (!isset($config['arguments'])) { - $config['arguments'] = []; - } - - $bucket = $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'bucket', null); - - if ($bucket === null) { - /* - * Use any provided bucket argument as prefix - * and add the mapping from username => bucket - */ - if (!isset($config['arguments']['bucket'])) { - $config['arguments']['bucket'] = ''; - } - $mapper = new \OC\Files\ObjectStore\Mapper($user, $this->config); - $numBuckets = $config['arguments']['num_buckets'] ?? 64; - $config['arguments']['bucket'] .= $mapper->getBucket($numBuckets); - - $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'bucket', $config['arguments']['bucket']); - } else { - $config['arguments']['bucket'] = $bucket; - } - - // instantiate object store implementation - $config['arguments']['objectstore'] = new $config['class']($config['arguments']); - - $config['arguments']['user'] = $user; - - return $config; + return new HomeMountPoint($user, HomeObjectStoreStorage::class, '/' . $user->getUID(), $arguments, $loader, null, null, self::class); } } diff --git a/lib/private/Files/Mount/RootMountProvider.php b/lib/private/Files/Mount/RootMountProvider.php index 7ad7a7f059c..5e0c924ad38 100644 --- a/lib/private/Files/Mount/RootMountProvider.php +++ b/lib/private/Files/Mount/RootMountProvider.php @@ -10,79 +10,41 @@ namespace OC\Files\Mount; use OC; use OC\Files\ObjectStore\ObjectStoreStorage; +use OC\Files\ObjectStore\PrimaryObjectStoreConfig; use OC\Files\Storage\LocalRootStorage; -use OC_App; use OCP\Files\Config\IRootMountProvider; use OCP\Files\Storage\IStorageFactory; use OCP\IConfig; -use Psr\Log\LoggerInterface; class RootMountProvider implements IRootMountProvider { + private PrimaryObjectStoreConfig $objectStoreConfig; private IConfig $config; - private LoggerInterface $logger; - public function __construct(IConfig $config, LoggerInterface $logger) { + public function __construct(PrimaryObjectStoreConfig $objectStoreConfig, IConfig $config) { + $this->objectStoreConfig = $objectStoreConfig; $this->config = $config; - $this->logger = $logger; } public function getRootMounts(IStorageFactory $loader): array { - $objectStore = $this->config->getSystemValue('objectstore', null); - $objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null); + $objectStoreConfig = $this->objectStoreConfig->getObjectStoreConfigForRoot(); - if ($objectStoreMultiBucket) { - return [$this->getMultiBucketStoreRootMount($loader, $objectStoreMultiBucket)]; - } elseif ($objectStore) { - return [$this->getObjectStoreRootMount($loader, $objectStore)]; + if ($objectStoreConfig) { + return [$this->getObjectStoreRootMount($loader, $objectStoreConfig)]; } else { return [$this->getLocalRootMount($loader)]; } } - private function validateObjectStoreConfig(array &$config) { - if (empty($config['class'])) { - $this->logger->error('No class given for objectstore', ['app' => 'files']); - } - if (!isset($config['arguments'])) { - $config['arguments'] = []; - } - - // instantiate object store implementation - $name = $config['class']; - if (str_starts_with($name, 'OCA\\') && substr_count($name, '\\') >= 2) { - $segments = explode('\\', $name); - OC_App::loadApp(strtolower($segments[1])); - } - } - private function getLocalRootMount(IStorageFactory $loader): MountPoint { - $configDataDirectory = $this->config->getSystemValue("datadirectory", OC::$SERVERROOT . "/data"); + $configDataDirectory = $this->config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data'); return new MountPoint(LocalRootStorage::class, '/', ['datadir' => $configDataDirectory], $loader, null, null, self::class); } - private function getObjectStoreRootMount(IStorageFactory $loader, array $config): MountPoint { - $this->validateObjectStoreConfig($config); - - $config['arguments']['objectstore'] = new $config['class']($config['arguments']); - // mount with plain / root object store implementation - $config['class'] = ObjectStoreStorage::class; - - return new MountPoint($config['class'], '/', $config['arguments'], $loader, null, null, self::class); - } - - private function getMultiBucketStoreRootMount(IStorageFactory $loader, array $config): MountPoint { - $this->validateObjectStoreConfig($config); - - if (!isset($config['arguments']['bucket'])) { - $config['arguments']['bucket'] = ''; - } - // put the root FS always in first bucket for multibucket configuration - $config['arguments']['bucket'] .= '0'; - - $config['arguments']['objectstore'] = new $config['class']($config['arguments']); - // mount with plain / root object store implementation - $config['class'] = ObjectStoreStorage::class; + private function getObjectStoreRootMount(IStorageFactory $loader, array $objectStoreConfig): MountPoint { + $arguments = array_merge($objectStoreConfig['arguments'], [ + 'objectstore' => $this->objectStoreConfig->buildObjectStore($objectStoreConfig), + ]); - return new MountPoint($config['class'], '/', $config['arguments'], $loader, null, null, self::class); + return new MountPoint(ObjectStoreStorage::class, '/', $arguments, $loader, null, null, self::class); } } diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 51b205d545b..7453b553119 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -12,6 +13,7 @@ use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchOrder; use OC\Files\Search\SearchQuery; use OC\Files\Utils\PathHelper; +use OC\User\LazyUser; use OCP\Files\Cache\ICacheEntry; use OCP\Files\FileInfo; use OCP\Files\Mount\IMountPoint; @@ -26,6 +28,9 @@ use OCP\Files\Search\ISearchQuery; use OCP\IUserManager; class Folder extends Node implements \OCP\Files\Folder { + + private ?IUserManager $userManager = null; + /** * Creates a Folder that represents a non-existing path * @@ -99,26 +104,15 @@ class Folder extends Node implements \OCP\Files\Folder { } } - /** - * Get the node at $path - * - * @param string $path - * @return \OC\Files\Node\Node - * @throws \OCP\Files\NotFoundException - */ public function get($path) { return $this->root->get($this->getFullPath($path)); } - /** - * @param string $path - * @return bool - */ public function nodeExists($path) { try { $this->get($path); return true; - } catch (NotFoundException $e) { + } catch (NotFoundException|NotPermittedException) { return false; } } @@ -133,8 +127,21 @@ class Folder extends Node implements \OCP\Files\Folder { $fullPath = $this->getFullPath($path); $nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath); $this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]); - if (!$this->view->mkdir($fullPath) && !$this->view->is_dir($fullPath)) { - throw new NotPermittedException('Could not create folder "' . $fullPath . '"'); + if (!$this->view->mkdir($fullPath)) { + // maybe another concurrent process created the folder already + if (!$this->view->is_dir($fullPath)) { + throw new NotPermittedException('Could not create folder "' . $fullPath . '"'); + } else { + // we need to ensure we don't return before the concurrent request has finished updating the cache + $tries = 5; + while (!$this->view->getFileInfo($fullPath)) { + if ($tries < 1) { + throw new NotPermittedException('Could not create folder "' . $fullPath . '", folder exists but unable to get cache entry'); + } + usleep(5 * 1000); + $tries--; + } + } } $parent = dirname($fullPath) === $this->getPath() ? $this : null; $node = new Folder($this->root, $this->view, $fullPath, null, $parent); @@ -218,9 +225,9 @@ class Folder extends Node implements \OCP\Files\Folder { }, array_values($resultsPerCache), array_keys($resultsPerCache))); // don't include this folder in the results - $files = array_filter($files, function (FileInfo $file) { + $files = array_values(array_filter($files, function (FileInfo $file) { return $file->getPath() !== $this->getPath(); - }); + })); // since results were returned per-cache, they are no longer fully sorted $order = $query->getOrder(); @@ -245,7 +252,26 @@ class Folder extends Node implements \OCP\Files\Folder { $cacheEntry['internalPath'] = $cacheEntry['path']; $cacheEntry['path'] = rtrim($appendRoot . $cacheEntry->getPath(), '/'); $subPath = $cacheEntry['path'] !== '' ? '/' . $cacheEntry['path'] : ''; - return new \OC\Files\FileInfo($this->path . $subPath, $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount); + $storage = $mount->getStorage(); + + $owner = null; + $ownerId = $storage->getOwner($cacheEntry['internalPath']); + if ($ownerId !== false) { + // Cache the user manager (for performance) + if ($this->userManager === null) { + $this->userManager = \OCP\Server::get(IUserManager::class); + } + $owner = new LazyUser($ownerId, $this->userManager); + } + + return new \OC\Files\FileInfo( + $this->path . $subPath, + $storage, + $cacheEntry['internalPath'], + $cacheEntry, + $mount, + $owner, + ); } /** @@ -289,7 +315,7 @@ class Folder extends Node implements \OCP\Files\Folder { } public function getFirstNodeById(int $id): ?\OCP\Files\Node { - return current($this->getById($id)) ?: null; + return $this->root->getFirstNodeByIdInPath($id, $this->getPath()); } public function getAppDataDirectoryName(): string { @@ -399,7 +425,7 @@ class Folder extends Node implements \OCP\Files\Folder { $filterNonRecentFiles = new SearchComparison( ISearchComparison::COMPARE_GREATER_THAN, 'mtime', - strtotime("-2 week") + strtotime('-2 week') ); if ($offset === 0 && $limit <= 100) { $query = new SearchQuery( @@ -435,4 +461,12 @@ class Folder extends Node implements \OCP\Files\Folder { return $this->search($query); } + + public function verifyPath($fileName, $readonly = false): void { + $this->view->verifyPath( + $this->getPath(), + $fileName, + $readonly, + ); + } } diff --git a/lib/private/Files/Node/HookConnector.php b/lib/private/Files/Node/HookConnector.php index 8fd2ffa3369..1149951174c 100644 --- a/lib/private/Files/Node/HookConnector.php +++ b/lib/private/Files/Node/HookConnector.php @@ -39,7 +39,7 @@ class HookConnector { private IRootFolder $root, private View $view, private IEventDispatcher $dispatcher, - private LoggerInterface $logger + private LoggerInterface $logger, ) { } @@ -171,7 +171,7 @@ class HookConnector { public function copy($arguments) { $source = $this->getNodeForPath($arguments['oldpath']); - $target = $this->getNodeForPath($arguments['newpath']); + $target = $this->getNodeForPath($arguments['newpath'], $source instanceof Folder); $this->root->emit('\OC\Files', 'preCopy', [$source, $target]); $this->dispatcher->dispatch('\OCP\Files::preCopy', new GenericEvent([$source, $target])); @@ -203,7 +203,7 @@ class HookConnector { $this->dispatcher->dispatchTyped($event); } - private function getNodeForPath(string $path): Node { + private function getNodeForPath(string $path, bool $isDir = false): Node { $info = Filesystem::getView()->getFileInfo($path); if (!$info) { $fullPath = Filesystem::getView()->getAbsolutePath($path); @@ -212,7 +212,7 @@ class HookConnector { } else { $info = null; } - if (Filesystem::is_dir($path)) { + if ($isDir || Filesystem::is_dir($path)) { return new NonExistingFolder($this->root, $this->view, $fullPath, $info); } else { return new NonExistingFile($this->root, $this->view, $fullPath, $info); diff --git a/lib/private/Files/Node/LazyFolder.php b/lib/private/Files/Node/LazyFolder.php index 83ed7e534a7..37b1efa0fad 100644 --- a/lib/private/Files/Node/LazyFolder.php +++ b/lib/private/Files/Node/LazyFolder.php @@ -134,9 +134,6 @@ class LazyFolder implements Folder { $this->__call(__FUNCTION__, func_get_args()); } - /** - * @inheritDoc - */ public function get($path) { return $this->getRootFolder()->get($this->getFullPath($path)); } @@ -422,9 +419,6 @@ class LazyFolder implements Folder { return $this->__call(__FUNCTION__, func_get_args()); } - /** - * @inheritDoc - */ public function nodeExists($path) { return $this->__call(__FUNCTION__, func_get_args()); } @@ -567,4 +561,8 @@ class LazyFolder implements Folder { public function getMetadata(): array { return $this->data['metadata'] ?? $this->__call(__FUNCTION__, func_get_args()); } + + public function verifyPath($fileName, $readonly = false): void { + $this->__call(__FUNCTION__, func_get_args()); + } } diff --git a/lib/private/Files/Node/LazyUserFolder.php b/lib/private/Files/Node/LazyUserFolder.php index f9908b9b7b6..77479c2fa5e 100644 --- a/lib/private/Files/Node/LazyUserFolder.php +++ b/lib/private/Files/Node/LazyUserFolder.php @@ -59,7 +59,7 @@ class LazyUserFolder extends LazyFolder { } $mountPoint = $this->mountManager->find('/' . $this->user->getUID()); if (is_null($mountPoint)) { - throw new \Exception("No mountpoint for user folder"); + throw new \Exception('No mountpoint for user folder'); } return $mountPoint; } diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php index e7f533e73a9..5dbdc4054bf 100644 --- a/lib/private/Files/Node/Node.php +++ b/lib/private/Files/Node/Node.php @@ -158,7 +158,7 @@ class Node implements INode { public function getStorage() { $storage = $this->getMountPoint()->getStorage(); if (!$storage) { - throw new \Exception("No storage for node"); + throw new \Exception('No storage for node'); } return $storage; } @@ -432,11 +432,14 @@ class Node implements INode { $targetPath = $this->normalizePath($targetPath); $parent = $this->root->get(dirname($targetPath)); if ( - $parent instanceof Folder and - $this->isValidPath($targetPath) and - ( - $parent->isCreatable() || - ($parent->getInternalPath() === '' && $parent->getMountPoint() instanceof MoveableMount) + ($parent instanceof Folder) + && $this->isValidPath($targetPath) + && ( + $parent->isCreatable() + || ( + $parent->getInternalPath() === '' + && ($parent->getMountPoint() instanceof MoveableMount) + ) ) ) { $nonExisting = $this->createNonExistingNode($targetPath); diff --git a/lib/private/Files/Node/NonExistingFile.php b/lib/private/Files/Node/NonExistingFile.php index d154876432e..66ec2e6c040 100644 --- a/lib/private/Files/Node/NonExistingFile.php +++ b/lib/private/Files/Node/NonExistingFile.php @@ -38,6 +38,14 @@ class NonExistingFile extends File { } } + public function getInternalPath() { + if ($this->fileInfo) { + return parent::getInternalPath(); + } else { + return $this->getParent()->getMountPoint()->getInternalPath($this->getPath()); + } + } + public function stat() { throw new NotFoundException(); } diff --git a/lib/private/Files/Node/NonExistingFolder.php b/lib/private/Files/Node/NonExistingFolder.php index 5650c99fe73..4489fdaf010 100644 --- a/lib/private/Files/Node/NonExistingFolder.php +++ b/lib/private/Files/Node/NonExistingFolder.php @@ -38,6 +38,14 @@ class NonExistingFolder extends Folder { } } + public function getInternalPath() { + if ($this->fileInfo) { + return parent::getInternalPath(); + } else { + return $this->getParent()->getMountPoint()->getInternalPath($this->getPath()); + } + } + public function stat() { throw new NotFoundException(); } diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index 5fb4013cfc9..76afca9dee8 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -28,6 +28,7 @@ use OCP\ICache; use OCP\ICacheFactory; use OCP\IUser; use OCP\IUserManager; +use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -170,12 +171,6 @@ class Root extends Folder implements IRootFolder { $this->mountManager->remove($mount); } - /** - * @param string $path - * @return Node - * @throws \OCP\Files\NotPermittedException - * @throws \OCP\Files\NotFoundException - */ public function get($path) { $path = $this->normalizePath($path); if ($this->isValidPath($path)) { @@ -389,13 +384,17 @@ class Root extends Folder implements IRootFolder { // scope the cache by user, so we don't return nodes for different users if ($this->user) { $cachedPath = $this->pathByIdCache->get($this->user->getUID() . '::' . $id); - if ($cachedPath && str_starts_with($path, $cachedPath)) { + if ($cachedPath && str_starts_with($cachedPath, $path)) { // getting the node by path is significantly cheaper than finding it by id - $node = $this->get($cachedPath); - // by validating that the cached path still has the requested fileid we can work around the need to invalidate the cached path - // if the cached path is invalid or a different file now we fall back to the uncached logic - if ($node && $node->getId() === $id) { - return $node; + try { + $node = $this->get($cachedPath); + // by validating that the cached path still has the requested fileid we can work around the need to invalidate the cached path + // if the cached path is invalid or a different file now we fall back to the uncached logic + if ($node && $node->getId() === $id) { + return $node; + } + } catch (NotFoundException|NotPermittedException) { + // The file may be moved but the old path still in cache } } } @@ -416,7 +415,7 @@ class Root extends Folder implements IRootFolder { */ public function getByIdInPath(int $id, string $path): array { $mountCache = $this->getUserMountCache(); - if (strpos($path, '/', 1) > 0) { + if ($path !== '' && strpos($path, '/', 1) > 0) { [, $user] = explode('/', $path); } else { $user = null; @@ -459,7 +458,7 @@ class Root extends Folder implements IRootFolder { if ($folder instanceof Folder) { return $folder->getByIdInRootMount($id); } else { - throw new \Exception("getByIdInPath with non folder"); + throw new \Exception('getByIdInPath with non folder'); } } return []; @@ -477,9 +476,23 @@ class Root extends Folder implements IRootFolder { $pathRelativeToMount = substr($internalPath, strlen($rootInternalPath)); $pathRelativeToMount = ltrim($pathRelativeToMount, '/'); $absolutePath = rtrim($mount->getMountPoint() . $pathRelativeToMount, '/'); + $storage = $mount->getStorage(); + if ($storage === null) { + return null; + } + $ownerId = $storage->getOwner($pathRelativeToMount); + if ($ownerId !== false) { + $owner = Server::get(IUserManager::class)->get($ownerId); + } else { + $owner = null; + } return $this->createNode($absolutePath, new FileInfo( - $absolutePath, $mount->getStorage(), $cacheEntry->getPath(), $cacheEntry, $mount, - \OC::$server->getUserManager()->get($mount->getStorage()->getOwner($pathRelativeToMount)) + $absolutePath, + $storage, + $cacheEntry->getPath(), + $cacheEntry, + $mount, + $owner, )); }, $mountsContainingFile); @@ -513,9 +526,9 @@ class Root extends Folder implements IRootFolder { $isDir = $info->getType() === FileInfo::TYPE_FOLDER; $view = new View(''); if ($isDir) { - return new Folder($this, $view, $path, $info, $parent); + return new Folder($this, $view, $fullPath, $info, $parent); } else { - return new File($this, $view, $path, $info, $parent); + return new File($this, $view, $fullPath, $info, $parent); } } } diff --git a/lib/private/Files/Notify/Change.php b/lib/private/Files/Notify/Change.php index d2448e5deec..c8eccd11ae2 100644 --- a/lib/private/Files/Notify/Change.php +++ b/lib/private/Files/Notify/Change.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Files/Notify/RenameChange.php b/lib/private/Files/Notify/RenameChange.php index 98fd7099a58..28554ceaa26 100644 --- a/lib/private/Files/Notify/RenameChange.php +++ b/lib/private/Files/Notify/RenameChange.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Files/ObjectStore/AppdataPreviewObjectStoreStorage.php b/lib/private/Files/ObjectStore/AppdataPreviewObjectStoreStorage.php index 66fa74172d3..aaaee044bac 100644 --- a/lib/private/Files/ObjectStore/AppdataPreviewObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/AppdataPreviewObjectStoreStorage.php @@ -12,15 +12,15 @@ class AppdataPreviewObjectStoreStorage extends ObjectStoreStorage { private string $internalId; /** - * @param array $params + * @param array $parameters * @throws \Exception */ - public function __construct($params) { - if (!isset($params['internal-id'])) { + public function __construct(array $parameters) { + if (!isset($parameters['internal-id'])) { throw new \Exception('missing id in parameters'); } - $this->internalId = (string)$params['internal-id']; - parent::__construct($params); + $this->internalId = (string)$parameters['internal-id']; + parent::__construct($parameters); } public function getId(): string { diff --git a/lib/private/Files/ObjectStore/Azure.php b/lib/private/Files/ObjectStore/Azure.php index 55400d4131c..2729bb3c037 100644 --- a/lib/private/Files/ObjectStore/Azure.php +++ b/lib/private/Files/ObjectStore/Azure.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -21,13 +22,13 @@ class Azure implements IObjectStore { private $blobClient = null; /** @var string|null */ private $endpoint = null; - /** @var bool */ + /** @var bool */ private $autoCreate = false; /** * @param array $parameters */ - public function __construct($parameters) { + public function __construct(array $parameters) { $this->containerName = $parameters['container']; $this->accountName = $parameters['account_name']; $this->accountKey = $parameters['account_key']; @@ -45,7 +46,7 @@ class Azure implements IObjectStore { private function getBlobClient() { if (!$this->blobClient) { $protocol = $this->endpoint ? substr($this->endpoint, 0, strpos($this->endpoint, ':')) : 'https'; - $connectionString = "DefaultEndpointsProtocol=" . $protocol . ";AccountName=" . $this->accountName . ";AccountKey=" . $this->accountKey; + $connectionString = 'DefaultEndpointsProtocol=' . $protocol . ';AccountName=' . $this->accountName . ';AccountKey=' . $this->accountKey; if ($this->endpoint) { $connectionString .= ';BlobEndpoint=' . $this->endpoint; } diff --git a/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php b/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php index b543d223f4c..4e2d10705fe 100644 --- a/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php @@ -17,28 +17,22 @@ class HomeObjectStoreStorage extends ObjectStoreStorage implements IHomeStorage /** * The home user storage requires a user object to create a unique storage id * - * @param array $params + * @param array $parameters * @throws Exception */ - public function __construct($params) { - if (! isset($params['user']) || ! $params['user'] instanceof IUser) { + public function __construct(array $parameters) { + if (! isset($parameters['user']) || ! $parameters['user'] instanceof IUser) { throw new Exception('missing user object in parameters'); } - $this->user = $params['user']; - parent::__construct($params); + $this->user = $parameters['user']; + parent::__construct($parameters); } public function getId(): string { return 'object::user:' . $this->user->getUID(); } - /** - * get the owner of a path - * - * @param string $path The path to get the owner - * @return string uid - */ - public function getOwner($path): string { + public function getOwner(string $path): string|false { return $this->user->getUID(); } diff --git a/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php b/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php new file mode 100644 index 00000000000..369182b069d --- /dev/null +++ b/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Files\ObjectStore; + +class InvalidObjectStoreConfigurationException extends \Exception { + +} diff --git a/lib/private/Files/ObjectStore/ObjectStoreScanner.php b/lib/private/Files/ObjectStore/ObjectStoreScanner.php index d8a77d36dee..5c3992b8458 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreScanner.php +++ b/lib/private/Files/ObjectStore/ObjectStoreScanner.php @@ -13,11 +13,11 @@ use OCP\Files\FileInfo; class ObjectStoreScanner extends Scanner { public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) { - return []; + return null; } public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) { - return []; + return null; } protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float $oldSize, &$etagChanged = false) { @@ -61,7 +61,7 @@ class ObjectStoreScanner extends Scanner { $query->select('path') ->from('filecache') ->where($query->expr()->eq('storage', $query->createNamedParameter($this->cache->getNumericStorageId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) ->orderBy('path', 'DESC') ->setMaxResults(1); diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index 389f744eab4..9ab11f8a3df 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -17,10 +17,12 @@ use OC\Files\Cache\CacheEntry; use OC\Files\Storage\PolyFill\CopyDirectory; use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Cache\IScanner; use OCP\Files\FileInfo; use OCP\Files\GenericFileException; use OCP\Files\NotFoundException; use OCP\Files\ObjectStore\IObjectStore; +use OCP\Files\ObjectStore\IObjectStoreMetaData; use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload; use OCP\Files\Storage\IChunkedFileWrite; use OCP\Files\Storage\IStorage; @@ -37,34 +39,35 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil private bool $handleCopiesAsOwned; protected bool $validateWrites = true; + private bool $preserveCacheItemsOnDelete = false; /** - * @param array $params + * @param array $parameters * @throws \Exception */ - public function __construct($params) { - if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) { - $this->objectStore = $params['objectstore']; + public function __construct(array $parameters) { + if (isset($parameters['objectstore']) && $parameters['objectstore'] instanceof IObjectStore) { + $this->objectStore = $parameters['objectstore']; } else { throw new \Exception('missing IObjectStore instance'); } - if (isset($params['storageid'])) { - $this->id = 'object::store:' . $params['storageid']; + if (isset($parameters['storageid'])) { + $this->id = 'object::store:' . $parameters['storageid']; } else { $this->id = 'object::store:' . $this->objectStore->getStorageId(); } - if (isset($params['objectPrefix'])) { - $this->objectPrefix = $params['objectPrefix']; + if (isset($parameters['objectPrefix'])) { + $this->objectPrefix = $parameters['objectPrefix']; } - if (isset($params['validateWrites'])) { - $this->validateWrites = (bool)$params['validateWrites']; + if (isset($parameters['validateWrites'])) { + $this->validateWrites = (bool)$parameters['validateWrites']; } - $this->handleCopiesAsOwned = (bool)($params['handleCopiesAsOwned'] ?? false); + $this->handleCopiesAsOwned = (bool)($parameters['handleCopiesAsOwned'] ?? false); $this->logger = \OCP\Server::get(LoggerInterface::class); } - public function mkdir($path, bool $force = false) { + public function mkdir(string $path, bool $force = false, array $metadata = []): bool { $path = $this->normalizePath($path); if (!$force && $this->file_exists($path)) { $this->logger->warning("Tried to create an object store folder that already exists: $path"); @@ -74,7 +77,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil $mTime = time(); $data = [ 'mimetype' => 'httpd/unix-directory', - 'size' => 0, + 'size' => $metadata['size'] ?? 0, 'mtime' => $mTime, 'storage_mtime' => $mTime, 'permissions' => \OCP\Constants::PERMISSION_ALL, @@ -109,11 +112,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } } - /** - * @param string $path - * @return string - */ - private function normalizePath($path) { + private function normalizePath(string $path): string { $path = trim($path, '/'); //FIXME why do we sometimes get a path like 'files//username'? $path = str_replace('//', '/', $path); @@ -129,26 +128,23 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil /** * Object Stores use a NoopScanner because metadata is directly stored in * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere. - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner - * @return \OC\Files\ObjectStore\ObjectStoreScanner */ - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } if (!isset($this->scanner)) { $this->scanner = new ObjectStoreScanner($storage); } + /** @var \OC\Files\ObjectStore\ObjectStoreScanner */ return $this->scanner; } - public function getId() { + public function getId(): string { return $this->id; } - public function rmdir($path) { + public function rmdir(string $path): bool { $path = $this->normalizePath($path); $entry = $this->getCache()->get($path); @@ -173,12 +169,14 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } } - $this->getCache()->remove($entry->getPath()); + if (!$this->preserveCacheItemsOnDelete) { + $this->getCache()->remove($entry->getPath()); + } return true; } - public function unlink($path) { + public function unlink(string $path): bool { $path = $this->normalizePath($path); $entry = $this->getCache()->get($path); @@ -208,11 +206,13 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } //removing from cache is ok as it does not exist in the objectstore anyway } - $this->getCache()->remove($entry->getPath()); + if (!$this->preserveCacheItemsOnDelete) { + $this->getCache()->remove($entry->getPath()); + } return true; } - public function stat($path) { + public function stat(string $path): array|false { $path = $this->normalizePath($path); $cacheEntry = $this->getCache()->get($path); if ($cacheEntry instanceof CacheEntry) { @@ -229,7 +229,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } } - public function getPermissions($path) { + public function getPermissions(string $path): int { $stat = $this->stat($path); if (is_array($stat) && isset($stat['permissions'])) { @@ -244,17 +244,13 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users. * You may need a mapping table to store your URN if it cannot be generated from the fileid. * - * @param int $fileId the fileid - * @return null|string the unified resource name used to identify the object + * @return string the unified resource name used to identify the object */ - public function getURN($fileId) { - if (is_numeric($fileId)) { - return $this->objectPrefix . $fileId; - } - return null; + public function getURN(int $fileId): string { + return $this->objectPrefix . $fileId; } - public function opendir($path) { + public function opendir(string $path) { $path = $this->normalizePath($path); try { @@ -271,7 +267,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } } - public function filetype($path) { + public function filetype(string $path): string|false { $path = $this->normalizePath($path); $stat = $this->stat($path); if ($stat) { @@ -284,7 +280,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $path = $this->normalizePath($path); if (strrpos($path, '.') !== false) { @@ -376,12 +372,12 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil return false; } - public function file_exists($path) { + public function file_exists(string $path): bool { $path = $this->normalizePath($path); return (bool)$this->stat($path); } - public function rename($source, $target) { + public function rename(string $source, string $target): bool { $source = $this->normalizePath($source); $target = $this->normalizePath($target); $this->remove($target); @@ -390,12 +386,12 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil return true; } - public function getMimeType($path) { + public function getMimeType(string $path): string|false { $path = $this->normalizePath($path); return parent::getMimeType($path); } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { if (is_null($mtime)) { $mtime = time(); } @@ -417,16 +413,6 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil //create a empty file, need to have at least on char to make it // work with all object storage implementations $this->file_put_contents($path, ' '); - $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path); - $stat = [ - 'etag' => $this->getETag($path), - 'mimetype' => $mimeType, - 'size' => 0, - 'mtime' => $mtime, - 'storage_mtime' => $mtime, - 'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, - ]; - $this->getCache()->put($path, $stat); } catch (\Exception $ex) { $this->logger->error( 'Could not create object for ' . $path, @@ -441,37 +427,34 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil return true; } - public function writeBack($tmpFile, $path) { + public function writeBack(string $tmpFile, string $path) { $size = filesize($tmpFile); $this->writeStream($path, fopen($tmpFile, 'r'), $size); } - /** - * external changes are not supported, exclusive access to the object storage is assumed - * - * @param string $path - * @param int $time - * @return false - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { return false; } - public function needsPartFile() { + public function needsPartFile(): bool { return false; } - public function file_put_contents($path, $data) { - $handle = $this->fopen($path, 'w+'); - if (!$handle) { - return false; - } - $result = fwrite($handle, $data); - fclose($handle); - return $result; + public function file_put_contents(string $path, mixed $data): int { + $fh = fopen('php://temp', 'w+'); + fwrite($fh, $data); + rewind($fh); + return $this->writeStream($path, $fh, strlen($data)); } public function writeStream(string $path, $stream, ?int $size = null): int { + if ($size === null) { + $stats = fstat($stream); + if (is_array($stats) && isset($stats['size'])) { + $size = $stats['size']; + } + } + $stat = $this->stat($path); if (empty($stat)) { // create new file @@ -487,6 +470,14 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil $mimetypeDetector = \OC::$server->getMimeTypeDetector(); $mimetype = $mimetypeDetector->detectPath($path); + $metadata = [ + 'mimetype' => $mimetype, + 'original-storage' => $this->getId(), + 'original-path' => $path, + ]; + if ($size) { + $metadata['size'] = $size; + } $stat['mimetype'] = $mimetype; $stat['etag'] = $this->getETag($path); @@ -498,30 +489,37 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil if ($exists) { $fileId = $stat['fileid']; } else { + $parent = $this->normalizePath(dirname($path)); + if (!$this->is_dir($parent)) { + throw new \InvalidArgumentException("trying to upload a file ($path) inside a non-directory ($parent)"); + } $fileId = $this->getCache()->put($uploadPath, $stat); } $urn = $this->getURN($fileId); try { //upload to object storage - if ($size === null) { - $countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) { + + $totalWritten = 0; + $countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, $size, $exists, &$totalWritten) { + if (is_null($size) && !$exists) { $this->getCache()->update($fileId, [ 'size' => $writtenSize, ]); - $size = $writtenSize; - }); - $this->objectStore->writeObject($urn, $countStream, $mimetype); - if (is_resource($countStream)) { - fclose($countStream); } - $stat['size'] = $size; + $totalWritten = $writtenSize; + }); + + if ($this->objectStore instanceof IObjectStoreMetaData) { + $this->objectStore->writeObjectWithMetaData($urn, $countStream, $metadata); } else { - $this->objectStore->writeObject($urn, $stream, $mimetype); - if (is_resource($stream)) { - fclose($stream); - } + $this->objectStore->writeObject($urn, $countStream, $metadata['mimetype']); + } + if (is_resource($countStream)) { + fclose($countStream); } + + $stat['size'] = $totalWritten; } catch (\Exception $ex) { if (!$exists) { /* @@ -545,7 +543,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil ] ); } - throw $ex; // make this bubble up + throw new GenericFileException('Error while writing stream to object store', 0, $ex); } if ($exists) { @@ -561,7 +559,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } } - return $size; + return $totalWritten; } public function getObjectStore(): IObjectStore { @@ -570,10 +568,10 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil public function copyFromStorage( IStorage $sourceStorage, - $sourceInternalPath, - $targetInternalPath, - $preserveMtime = false - ) { + string $sourceInternalPath, + string $targetInternalPath, + bool $preserveMtime = false, + ): bool { if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { /** @var ObjectStoreStorage $sourceStorage */ if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) { @@ -594,7 +592,90 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } - public function copy($source, $target) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, ?ICacheEntry $sourceCacheEntry = null): bool { + $sourceCache = $sourceStorage->getCache(); + if ( + $sourceStorage->instanceOfStorage(ObjectStoreStorage::class) + && $sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId() + ) { + if ($this->getCache()->get($targetInternalPath)) { + $this->unlink($targetInternalPath); + $this->getCache()->remove($targetInternalPath); + } + $this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath); + // Do not import any data when source and target bucket are identical. + return true; + } + if (!$sourceCacheEntry) { + $sourceCacheEntry = $sourceCache->get($sourceInternalPath); + } + + $this->copyObjects($sourceStorage, $sourceCache, $sourceCacheEntry); + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(true); + } + if ($sourceCacheEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) { + $sourceStorage->rmdir($sourceInternalPath); + } else { + $sourceStorage->unlink($sourceInternalPath); + } + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(false); + } + if ($this->getCache()->get($targetInternalPath)) { + $this->unlink($targetInternalPath); + $this->getCache()->remove($targetInternalPath); + } + $this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath); + + return true; + } + + /** + * Copy the object(s) of a file or folder into this storage, without touching the cache + */ + private function copyObjects(IStorage $sourceStorage, ICache $sourceCache, ICacheEntry $sourceCacheEntry) { + $copiedFiles = []; + try { + foreach ($this->getAllChildObjects($sourceCache, $sourceCacheEntry) as $file) { + $sourceStream = $sourceStorage->fopen($file->getPath(), 'r'); + if (!$sourceStream) { + throw new \Exception("Failed to open source file {$file->getPath()} ({$file->getId()})"); + } + $this->objectStore->writeObject($this->getURN($file->getId()), $sourceStream, $file->getMimeType()); + if (is_resource($sourceStream)) { + fclose($sourceStream); + } + $copiedFiles[] = $file->getId(); + } + } catch (\Exception $e) { + foreach ($copiedFiles as $fileId) { + try { + $this->objectStore->deleteObject($this->getURN($fileId)); + } catch (\Exception $e) { + // ignore + } + } + throw $e; + } + } + + /** + * @return \Iterator<ICacheEntry> + */ + private function getAllChildObjects(ICache $cache, ICacheEntry $entry): \Iterator { + if ($entry->getMimeType() === FileInfo::MIMETYPE_FOLDER) { + foreach ($cache->getFolderContentsById($entry->getId()) as $child) { + yield from $this->getAllChildObjects($cache, $child); + } + } else { + yield $entry; + } + } + + public function copy(string $source, string $target): bool { $source = $this->normalizePath($source); $target = $this->normalizePath($target); @@ -616,7 +697,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil if ($cache->inCache($to)) { $cache->remove($to); } - $this->mkdir($to); + $this->mkdir($to, false, ['size' => $sourceEntry->getSize()]); foreach ($sourceCache->getFolderContentsById($sourceEntry->getId()) as $child) { $this->copyInner($sourceCache, $child, $to . '/' . $child->getName()); @@ -632,7 +713,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil $sourceUrn = $this->getURN($sourceEntry->getId()); if (!$cache instanceof Cache) { - throw new \Exception("Invalid source cache for object store copy"); + throw new \Exception('Invalid source cache for object store copy'); } $targetId = $cache->copyFromCache($cache, $sourceEntry, $to); @@ -662,7 +743,6 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } /** - * * @throws GenericFileException */ public function putChunkedWritePart( @@ -670,7 +750,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil string $writeToken, string $chunkId, $data, - $size = null + $size = null, ): ?array { if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) { throw new GenericFileException('Object store does not support multipart upload'); @@ -728,4 +808,8 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil $urn = $this->getURN($cacheEntry->getId()); $this->objectStore->abortMultipartUpload($urn, $writeToken); } + + public function setPreserveCacheOnDelete(bool $preserve) { + $this->preserveCacheItemsOnDelete = $preserve; + } } diff --git a/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php b/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php new file mode 100644 index 00000000000..ffc33687340 --- /dev/null +++ b/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php @@ -0,0 +1,225 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OC\Files\ObjectStore; + +use OCP\App\IAppManager; +use OCP\Files\ObjectStore\IObjectStore; +use OCP\IConfig; +use OCP\IUser; + +/** + * @psalm-type ObjectStoreConfig array{class: class-string<IObjectStore>, arguments: array{multibucket: bool, ...}} + */ +class PrimaryObjectStoreConfig { + public function __construct( + private readonly IConfig $config, + private readonly IAppManager $appManager, + ) { + } + + /** + * @param ObjectStoreConfig $config + */ + public function buildObjectStore(array $config): IObjectStore { + return new $config['class']($config['arguments']); + } + + /** + * @return ?ObjectStoreConfig + */ + public function getObjectStoreConfigForRoot(): ?array { + if (!$this->hasObjectStore()) { + return null; + } + + $config = $this->getObjectStoreConfiguration('root'); + + if ($config['arguments']['multibucket']) { + if (!isset($config['arguments']['bucket'])) { + $config['arguments']['bucket'] = ''; + } + + // put the root FS always in first bucket for multibucket configuration + $config['arguments']['bucket'] .= '0'; + } + return $config; + } + + /** + * @return ?ObjectStoreConfig + */ + public function getObjectStoreConfigForUser(IUser $user): ?array { + if (!$this->hasObjectStore()) { + return null; + } + + $store = $this->getObjectStoreForUser($user); + $config = $this->getObjectStoreConfiguration($store); + + if ($config['arguments']['multibucket']) { + $config['arguments']['bucket'] = $this->getBucketForUser($user, $config); + } + return $config; + } + + /** + * @param string $name + * @return ObjectStoreConfig + */ + public function getObjectStoreConfiguration(string $name): array { + $configs = $this->getObjectStoreConfigs(); + $name = $this->resolveAlias($name); + if (!isset($configs[$name])) { + throw new \Exception("Object store configuration for '$name' not found"); + } + if (is_string($configs[$name])) { + throw new \Exception("Object store configuration for '{$configs[$name]}' not found"); + } + return $configs[$name]; + } + + public function resolveAlias(string $name): string { + $configs = $this->getObjectStoreConfigs(); + + while (isset($configs[$name]) && is_string($configs[$name])) { + $name = $configs[$name]; + } + return $name; + } + + public function hasObjectStore(): bool { + $objectStore = $this->config->getSystemValue('objectstore', null); + $objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null); + return $objectStore || $objectStoreMultiBucket; + } + + public function hasMultipleObjectStorages(): bool { + $objectStore = $this->config->getSystemValue('objectstore', []); + return isset($objectStore['default']); + } + + /** + * @return ?array<string, ObjectStoreConfig|string> + * @throws InvalidObjectStoreConfigurationException + */ + public function getObjectStoreConfigs(): ?array { + $objectStore = $this->config->getSystemValue('objectstore', null); + $objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null); + + // new-style multibucket config uses the same 'objectstore' key but sets `'multibucket' => true`, transparently upgrade older style config + if ($objectStoreMultiBucket) { + $objectStoreMultiBucket['arguments']['multibucket'] = true; + return [ + 'default' => 'server1', + 'server1' => $this->validateObjectStoreConfig($objectStoreMultiBucket), + 'root' => 'server1', + ]; + } elseif ($objectStore) { + if (!isset($objectStore['default'])) { + $objectStore = [ + 'default' => 'server1', + 'root' => 'server1', + 'server1' => $objectStore, + ]; + } + if (!isset($objectStore['root'])) { + $objectStore['root'] = 'default'; + } + + if (!is_string($objectStore['default'])) { + throw new InvalidObjectStoreConfigurationException('The \'default\' object storage configuration is required to be a reference to another configuration.'); + } + return array_map($this->validateObjectStoreConfig(...), $objectStore); + } else { + return null; + } + } + + /** + * @param array|string $config + * @return string|ObjectStoreConfig + */ + private function validateObjectStoreConfig(array|string $config): array|string { + if (is_string($config)) { + return $config; + } + if (!isset($config['class'])) { + throw new InvalidObjectStoreConfigurationException('No class configured for object store'); + } + if (!isset($config['arguments'])) { + $config['arguments'] = []; + } + $class = $config['class']; + $arguments = $config['arguments']; + if (!is_array($arguments)) { + throw new InvalidObjectStoreConfigurationException('Configured object store arguments are not an array'); + } + if (!isset($arguments['multibucket'])) { + $arguments['multibucket'] = false; + } + if (!is_bool($arguments['multibucket'])) { + throw new InvalidObjectStoreConfigurationException('arguments.multibucket must be a boolean in object store configuration'); + } + + if (!is_string($class)) { + throw new InvalidObjectStoreConfigurationException('Configured class for object store is not a string'); + } + + if (str_starts_with($class, 'OCA\\') && substr_count($class, '\\') >= 2) { + [$appId] = explode('\\', $class); + $this->appManager->loadApp(strtolower($appId)); + } + + if (!is_a($class, IObjectStore::class, true)) { + throw new InvalidObjectStoreConfigurationException('Configured class for object store is not an object store'); + } + return [ + 'class' => $class, + 'arguments' => $arguments, + ]; + } + + public function getBucketForUser(IUser $user, array $config): string { + $bucket = $this->getSetBucketForUser($user); + + if ($bucket === null) { + /* + * Use any provided bucket argument as prefix + * and add the mapping from username => bucket + */ + if (!isset($config['arguments']['bucket'])) { + $config['arguments']['bucket'] = ''; + } + $mapper = new Mapper($user, $this->config); + $numBuckets = $config['arguments']['num_buckets'] ?? 64; + $bucket = $config['arguments']['bucket'] . $mapper->getBucket($numBuckets); + + $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'bucket', $bucket); + } + + return $bucket; + } + + public function getSetBucketForUser(IUser $user): ?string { + return $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'bucket', null); + } + + public function getObjectStoreForUser(IUser $user): string { + if ($this->hasMultipleObjectStorages()) { + $value = $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'objectstore', null); + if ($value === null) { + $value = $this->resolveAlias('default'); + $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'objectstore', $value); + } + return $value; + } else { + return 'default'; + } + } +} diff --git a/lib/private/Files/ObjectStore/S3.php b/lib/private/Files/ObjectStore/S3.php index 72c19d951e4..72e1751e23d 100644 --- a/lib/private/Files/ObjectStore/S3.php +++ b/lib/private/Files/ObjectStore/S3.php @@ -1,20 +1,23 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OC\Files\ObjectStore; use Aws\Result; use Exception; use OCP\Files\ObjectStore\IObjectStore; +use OCP\Files\ObjectStore\IObjectStoreMetaData; use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload; -class S3 implements IObjectStore, IObjectStoreMultiPartUpload { +class S3 implements IObjectStore, IObjectStoreMultiPartUpload, IObjectStoreMetaData { use S3ConnectionTrait; use S3ObjectTrait; - public function __construct($parameters) { + public function __construct(array $parameters) { $parameters['primary_storage'] = true; $this->parseParams($parameters); } @@ -61,7 +64,7 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload { 'Key' => $urn, 'UploadId' => $uploadId, 'MaxParts' => 1000, - 'PartNumberMarker' => $partNumberMarker + 'PartNumberMarker' => $partNumberMarker, ] + $this->getSSECParameters()); $parts = array_merge($parts, $result->get('Parts') ?? []); $isTruncated = $result->get('IsTruncated'); @@ -89,7 +92,51 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload { $this->getConnection()->abortMultipartUpload([ 'Bucket' => $this->bucket, 'Key' => $urn, - 'UploadId' => $uploadId + 'UploadId' => $uploadId, ]); } + + private function parseS3Metadata(array $metadata): array { + $result = []; + foreach ($metadata as $key => $value) { + if (str_starts_with($key, 'x-amz-meta-')) { + $result[substr($key, strlen('x-amz-meta-'))] = $value; + } + } + return $result; + } + + public function getObjectMetaData(string $urn): array { + $object = $this->getConnection()->headObject([ + 'Bucket' => $this->bucket, + 'Key' => $urn + ] + $this->getSSECParameters())->toArray(); + return [ + 'mtime' => $object['LastModified'], + 'etag' => trim($object['ETag'], '"'), + 'size' => (int)($object['Size'] ?? $object['ContentLength']), + ] + $this->parseS3Metadata($object['Metadata'] ?? []); + } + + public function listObjects(string $prefix = ''): \Iterator { + $results = $this->getConnection()->getPaginator('ListObjectsV2', [ + 'Bucket' => $this->bucket, + 'Prefix' => $prefix, + ] + $this->getSSECParameters()); + + foreach ($results as $result) { + if (is_array($result['Contents'])) { + foreach ($result['Contents'] as $object) { + yield [ + 'urn' => basename($object['Key']), + 'metadata' => [ + 'mtime' => $object['LastModified'], + 'etag' => trim($object['ETag'], '"'), + 'size' => (int)($object['Size'] ?? $object['ContentLength']), + ], + ]; + } + } + } + } } diff --git a/lib/private/Files/ObjectStore/S3ConfigTrait.php b/lib/private/Files/ObjectStore/S3ConfigTrait.php index 3a399e6413d..5b086db8f77 100644 --- a/lib/private/Files/ObjectStore/S3ConfigTrait.php +++ b/lib/private/Files/ObjectStore/S3ConfigTrait.php @@ -18,9 +18,13 @@ trait S3ConfigTrait { /** Maximum number of concurrent multipart uploads */ protected int $concurrency; + /** Timeout, in seconds, for the connection to S3 server, not for the + * request. */ + protected float $connectTimeout; + protected int $timeout; - protected string $proxy; + protected string|false $proxy; protected string $storageClass; diff --git a/lib/private/Files/ObjectStore/S3ConnectionTrait.php b/lib/private/Files/ObjectStore/S3ConnectionTrait.php index c7a5a8a1add..67b82a44ab7 100644 --- a/lib/private/Files/ObjectStore/S3ConnectionTrait.php +++ b/lib/private/Files/ObjectStore/S3ConnectionTrait.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,9 +12,11 @@ use Aws\Credentials\Credentials; use Aws\Exception\CredentialsException; use Aws\S3\Exception\S3Exception; use Aws\S3\S3Client; -use GuzzleHttp\Promise; +use GuzzleHttp\Promise\Create; use GuzzleHttp\Promise\RejectedPromise; +use OCP\Files\StorageNotAvailableException; use OCP\ICertificateManager; +use OCP\Server; use Psr\Log\LoggerInterface; trait S3ConnectionTrait { @@ -27,7 +30,7 @@ trait S3ConnectionTrait { protected function parseParams($params) { if (empty($params['bucket'])) { - throw new \Exception("Bucket has to be configured."); + throw new \Exception('Bucket has to be configured.'); } $this->id = 'amazon::' . $params['bucket']; @@ -37,6 +40,7 @@ trait S3ConnectionTrait { // Default to 5 like the S3 SDK does $this->concurrency = $params['concurrency'] ?? 5; $this->proxy = $params['proxy'] ?? false; + $this->connectTimeout = $params['connect_timeout'] ?? 5; $this->timeout = $params['timeout'] ?? 15; $this->storageClass = !empty($params['storageClass']) ? $params['storageClass'] : 'STANDARD'; $this->uploadPartSize = $params['uploadPartSize'] ?? 524288000; @@ -98,8 +102,15 @@ trait S3ConnectionTrait { 'signature_provider' => \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider()), 'csm' => false, 'use_arn_region' => false, - 'http' => ['verify' => $this->getCertificateBundlePath()], + 'http' => [ + 'verify' => $this->getCertificateBundlePath(), + 'connect_timeout' => $this->connectTimeout, + ], 'use_aws_shared_config_files' => false, + 'retries' => [ + 'mode' => 'standard', + 'max_attempts' => 5, + ], ]; if ($this->params['s3-accelerate']) { @@ -116,35 +127,38 @@ trait S3ConnectionTrait { } $this->connection = new S3Client($options); - if (!$this->connection::isBucketDnsCompatible($this->bucket)) { - $logger = \OC::$server->get(LoggerInterface::class); - $logger->debug('Bucket "' . $this->bucket . '" This bucket name is not dns compatible, it may contain invalid characters.', - ['app' => 'objectstore']); - } + try { + $logger = Server::get(LoggerInterface::class); + if (!$this->connection::isBucketDnsCompatible($this->bucket)) { + $logger->debug('Bucket "' . $this->bucket . '" This bucket name is not dns compatible, it may contain invalid characters.', + ['app' => 'objectstore']); + } - if ($this->params['verify_bucket_exists'] && !$this->connection->doesBucketExist($this->bucket)) { - $logger = \OC::$server->get(LoggerInterface::class); - try { - $logger->info('Bucket "' . $this->bucket . '" does not exist - creating it.', ['app' => 'objectstore']); - if (!$this->connection::isBucketDnsCompatible($this->bucket)) { - throw new \Exception("The bucket will not be created because the name is not dns compatible, please correct it: " . $this->bucket); - } - $this->connection->createBucket(['Bucket' => $this->bucket]); - $this->testTimeout(); - } catch (S3Exception $e) { - $logger->debug('Invalid remote storage.', [ - 'exception' => $e, - 'app' => 'objectstore', - ]); - if ($e->getAwsErrorCode() !== "BucketAlreadyOwnedByYou") { - throw new \Exception('Creation of bucket "' . $this->bucket . '" failed. ' . $e->getMessage()); + if ($this->params['verify_bucket_exists'] && !$this->connection->doesBucketExist($this->bucket)) { + try { + $logger->info('Bucket "' . $this->bucket . '" does not exist - creating it.', ['app' => 'objectstore']); + if (!$this->connection::isBucketDnsCompatible($this->bucket)) { + throw new StorageNotAvailableException('The bucket will not be created because the name is not dns compatible, please correct it: ' . $this->bucket); + } + $this->connection->createBucket(['Bucket' => $this->bucket]); + $this->testTimeout(); + } catch (S3Exception $e) { + $logger->debug('Invalid remote storage.', [ + 'exception' => $e, + 'app' => 'objectstore', + ]); + if ($e->getAwsErrorCode() !== 'BucketAlreadyOwnedByYou') { + throw new StorageNotAvailableException('Creation of bucket "' . $this->bucket . '" failed. ' . $e->getMessage()); + } } } - } - // google cloud's s3 compatibility doesn't like the EncodingType parameter - if (strpos($base_url, 'storage.googleapis.com')) { - $this->connection->getHandlerList()->remove('s3.auto_encode'); + // google cloud's s3 compatibility doesn't like the EncodingType parameter + if (strpos($base_url, 'storage.googleapis.com')) { + $this->connection->getHandlerList()->remove('s3.auto_encode'); + } + } catch (S3Exception $e) { + throw new StorageNotAvailableException('S3 service is unable to handle request: ' . $e->getMessage()); } return $this->connection; @@ -176,10 +190,12 @@ trait S3ConnectionTrait { return function () { $key = empty($this->params['key']) ? null : $this->params['key']; $secret = empty($this->params['secret']) ? null : $this->params['secret']; + $sessionToken = empty($this->params['session_token']) ? null : $this->params['session_token']; if ($key && $secret) { - return Promise\promise_for( - new Credentials($key, $secret) + return Create::promiseFor( + // a null sessionToken match the default signature of the constructor + new Credentials($key, $secret, $sessionToken) ); } @@ -189,11 +205,11 @@ trait S3ConnectionTrait { } protected function getCertificateBundlePath(): ?string { - if ((int)($this->params['use_nextcloud_bundle'] ?? "0")) { + if ((int)($this->params['use_nextcloud_bundle'] ?? '0')) { // since we store the certificate bundles on the primary storage, we can't get the bundle while setting up the primary storage if (!isset($this->params['primary_storage'])) { /** @var ICertificateManager $certManager */ - $certManager = \OC::$server->get(ICertificateManager::class); + $certManager = Server::get(ICertificateManager::class); return $certManager->getAbsoluteBundlePath(); } else { return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt'; @@ -204,7 +220,7 @@ trait S3ConnectionTrait { } protected function getSSECKey(): ?string { - if (isset($this->params['sse_c_key'])) { + if (isset($this->params['sse_c_key']) && !empty($this->params['sse_c_key'])) { return $this->params['sse_c_key']; } diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index 5d00c184ca7..89405de2e8e 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -6,6 +6,8 @@ */ namespace OC\Files\ObjectStore; +use Aws\Command; +use Aws\Exception\MultipartUploadException; use Aws\S3\Exception\S3MultipartUploadException; use Aws\S3\MultipartCopy; use Aws\S3\MultipartUploader; @@ -77,24 +79,42 @@ trait S3ObjectTrait { return $fh; } + private function buildS3Metadata(array $metadata): array { + $result = []; + foreach ($metadata as $key => $value) { + $result['x-amz-meta-' . $key] = $value; + } + return $result; + } /** * Single object put helper * * @param string $urn the unified resource name used to identify the object * @param StreamInterface $stream stream with the data to write - * @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0 + * @param array $metaData the metadata to set for the object * @throws \Exception when something goes wrong, message will be logged */ - protected function writeSingle(string $urn, StreamInterface $stream, ?string $mimetype = null): void { - $this->getConnection()->putObject([ + protected function writeSingle(string $urn, StreamInterface $stream, array $metaData): void { + $mimetype = $metaData['mimetype'] ?? null; + unset($metaData['mimetype']); + unset($metaData['size']); + + $args = [ 'Bucket' => $this->bucket, 'Key' => $urn, 'Body' => $stream, 'ACL' => 'private', 'ContentType' => $mimetype, + 'Metadata' => $this->buildS3Metadata($metaData), 'StorageClass' => $this->storageClass, - ] + $this->getSSECParameters()); + ] + $this->getSSECParameters(); + + if ($size = $stream->getSize()) { + $args['ContentLength'] = $size; + } + + $this->getConnection()->putObject($args); } @@ -103,57 +123,116 @@ trait S3ObjectTrait { * * @param string $urn the unified resource name used to identify the object * @param StreamInterface $stream stream with the data to write - * @param string|null $mimetype the mimetype to set for the remove object + * @param array $metaData the metadata to set for the object * @throws \Exception when something goes wrong, message will be logged */ - protected function writeMultiPart(string $urn, StreamInterface $stream, ?string $mimetype = null): void { - $uploader = new MultipartUploader($this->getConnection(), $stream, [ - 'bucket' => $this->bucket, - 'concurrency' => $this->concurrency, - 'key' => $urn, - 'part_size' => $this->uploadPartSize, - 'params' => [ - 'ContentType' => $mimetype, - 'StorageClass' => $this->storageClass, - ] + $this->getSSECParameters(), - ]); + protected function writeMultiPart(string $urn, StreamInterface $stream, array $metaData): void { + $mimetype = $metaData['mimetype'] ?? null; + unset($metaData['mimetype']); + unset($metaData['size']); + + $attempts = 0; + $uploaded = false; + $concurrency = $this->concurrency; + $exception = null; + $state = null; + $size = $stream->getSize(); + $totalWritten = 0; + + // retry multipart upload once with concurrency at half on failure + while (!$uploaded && $attempts <= 1) { + $uploader = new MultipartUploader($this->getConnection(), $stream, [ + 'bucket' => $this->bucket, + 'concurrency' => $concurrency, + 'key' => $urn, + 'part_size' => $this->uploadPartSize, + 'state' => $state, + 'params' => [ + 'ContentType' => $mimetype, + 'Metadata' => $this->buildS3Metadata($metaData), + 'StorageClass' => $this->storageClass, + ] + $this->getSSECParameters(), + 'before_upload' => function (Command $command) use (&$totalWritten) { + $totalWritten += $command['ContentLength']; + }, + 'before_complete' => function ($_command) use (&$totalWritten, $size, &$uploader, &$attempts) { + if ($size !== null && $totalWritten != $size) { + $e = new \Exception('Incomplete multi part upload, expected ' . $size . ' bytes, wrote ' . $totalWritten); + throw new MultipartUploadException($uploader->getState(), $e); + } + }, + ]); + + try { + $uploader->upload(); + $uploaded = true; + } catch (S3MultipartUploadException $e) { + $exception = $e; + $attempts++; + + if ($concurrency > 1) { + $concurrency = round($concurrency / 2); + } + + if ($stream->isSeekable()) { + $stream->rewind(); + } + } catch (MultipartUploadException $e) { + $exception = $e; + break; + } + } - try { - $uploader->upload(); - } catch (S3MultipartUploadException $e) { + if (!$uploaded) { // if anything goes wrong with multipart, make sure that you don´t poison and // slow down s3 bucket with orphaned fragments - $uploadInfo = $e->getState()->getId(); - if ($e->getState()->isInitiated() && (array_key_exists('UploadId', $uploadInfo))) { + $uploadInfo = $exception->getState()->getId(); + if ($exception->getState()->isInitiated() && (array_key_exists('UploadId', $uploadInfo))) { $this->getConnection()->abortMultipartUpload($uploadInfo); } - throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway("Error while uploading to S3 bucket", 0, $e); + + throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway('Error while uploading to S3 bucket', 0, $exception); } } - - /** - * @param string $urn the unified resource name used to identify the object - * @param resource $stream stream with the data to write - * @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0 - * @throws \Exception when something goes wrong, message will be logged - * @since 7.0.0 - */ public function writeObject($urn, $stream, ?string $mimetype = null) { - $psrStream = Utils::streamFor($stream); - - // ($psrStream->isSeekable() && $psrStream->getSize() !== null) evaluates to true for a On-Seekable stream - // so the optimisation does not apply - $buffer = new Psr7\Stream(fopen("php://memory", 'rwb+')); - Utils::copyToStream($psrStream, $buffer, $this->putSizeLimit); - $buffer->seek(0); - if ($buffer->getSize() < $this->putSizeLimit) { - // buffer is fully seekable, so use it directly for the small upload - $this->writeSingle($urn, $buffer, $mimetype); + $metaData = []; + if ($mimetype) { + $metaData['mimetype'] = $mimetype; + } + $this->writeObjectWithMetaData($urn, $stream, $metaData); + } + + public function writeObjectWithMetaData(string $urn, $stream, array $metaData): void { + $canSeek = fseek($stream, 0, SEEK_CUR) === 0; + $psrStream = Utils::streamFor($stream, [ + 'size' => $metaData['size'] ?? null, + ]); + + + $size = $psrStream->getSize(); + if ($size === null || !$canSeek) { + // The s3 single-part upload requires the size to be known for the stream. + // So for input streams that don't have a known size, we need to copy (part of) + // the input into a temporary stream so the size can be determined + $buffer = new Psr7\Stream(fopen('php://temp', 'rw+')); + Utils::copyToStream($psrStream, $buffer, $this->putSizeLimit); + $buffer->seek(0); + if ($buffer->getSize() < $this->putSizeLimit) { + // buffer is fully seekable, so use it directly for the small upload + $this->writeSingle($urn, $buffer, $metaData); + } else { + $loadStream = new Psr7\AppendStream([$buffer, $psrStream]); + $this->writeMultiPart($urn, $loadStream, $metaData); + } } else { - $loadStream = new Psr7\AppendStream([$buffer, $psrStream]); - $this->writeMultiPart($urn, $loadStream, $mimetype); + if ($size < $this->putSizeLimit) { + $this->writeSingle($urn, $psrStream, $metaData); + } else { + $this->writeMultiPart($urn, $psrStream, $metaData); + } } + $psrStream->close(); } /** @@ -183,14 +262,14 @@ trait S3ObjectTrait { if ($this->useMultipartCopy && $size > $this->copySizeLimit) { $copy = new MultipartCopy($this->getConnection(), [ - "source_bucket" => $this->getBucket(), - "source_key" => $from + 'source_bucket' => $this->getBucket(), + 'source_key' => $from ], array_merge([ - "bucket" => $this->getBucket(), - "key" => $to, - "acl" => "private", - "params" => $this->getSSECParameters() + $this->getSSECParameters(true), - "source_metadata" => $sourceMetadata + 'bucket' => $this->getBucket(), + 'key' => $to, + 'acl' => 'private', + 'params' => $this->getSSECParameters() + $this->getSSECParameters(true), + 'source_metadata' => $sourceMetadata ], $options)); $copy->copy(); } else { diff --git a/lib/private/Files/ObjectStore/S3Signature.php b/lib/private/Files/ObjectStore/S3Signature.php index fd24a34b090..b80382ff67d 100644 --- a/lib/private/Files/ObjectStore/S3Signature.php +++ b/lib/private/Files/ObjectStore/S3Signature.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -41,7 +42,7 @@ class S3Signature implements SignatureInterface { public function signRequest( RequestInterface $request, - CredentialsInterface $credentials + CredentialsInterface $credentials, ) { $request = $this->prepareRequest($request, $credentials); $stringToSign = $this->createCanonicalizedString($request); @@ -56,7 +57,7 @@ class S3Signature implements SignatureInterface { RequestInterface $request, CredentialsInterface $credentials, $expires, - array $options = [] + array $options = [], ) { $query = []; // URL encoding already occurs in the URI template expansion. Undo that @@ -93,20 +94,20 @@ class S3Signature implements SignatureInterface { } } - $queryString = http_build_query($query, null, '&', PHP_QUERY_RFC3986); + $queryString = http_build_query($query, '', '&', PHP_QUERY_RFC3986); return $request->withUri($request->getUri()->withQuery($queryString)); } /** - * @param RequestInterface $request + * @param RequestInterface $request * @param CredentialsInterface $creds * * @return RequestInterface */ private function prepareRequest( RequestInterface $request, - CredentialsInterface $creds + CredentialsInterface $creds, ) { $modify = [ 'remove_headers' => ['X-Amz-Date'], @@ -129,7 +130,7 @@ class S3Signature implements SignatureInterface { private function createCanonicalizedString( RequestInterface $request, - $expires = null + $expires = null, ) { $buffer = $request->getMethod() . "\n"; diff --git a/lib/private/Files/ObjectStore/StorageObjectStore.php b/lib/private/Files/ObjectStore/StorageObjectStore.php index 5e7125e18a6..888602a62e4 100644 --- a/lib/private/Files/ObjectStore/StorageObjectStore.php +++ b/lib/private/Files/ObjectStore/StorageObjectStore.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -27,8 +28,8 @@ class StorageObjectStore implements IObjectStore { * @return string the container or bucket name where objects are stored * @since 7.0.0 */ - public function getStorageId() { - $this->storage->getId(); + public function getStorageId(): string { + return $this->storage->getId(); } /** diff --git a/lib/private/Files/ObjectStore/SwiftFactory.php b/lib/private/Files/ObjectStore/SwiftFactory.php index 0db5c9762d2..118724159e5 100644 --- a/lib/private/Files/ObjectStore/SwiftFactory.php +++ b/lib/private/Files/ObjectStore/SwiftFactory.php @@ -170,7 +170,7 @@ class SwiftFactory { try { /** @var \OpenStack\Identity\v2\Models\Token $token */ $token = $authService->model(\OpenStack\Identity\v2\Models\Token::class, $cachedToken['token']); - $now = new \DateTimeImmutable("now"); + $now = new \DateTimeImmutable('now'); if ($token->expires > $now) { $hasValidCachedToken = true; $this->params['v2cachedToken'] = $token; @@ -194,13 +194,13 @@ class SwiftFactory { } catch (ClientException $e) { $statusCode = $e->getResponse()->getStatusCode(); if ($statusCode === 404) { - throw new StorageAuthException('Keystone not found, verify the keystone url', $e); + throw new StorageAuthException('Keystone not found while connecting to object storage, verify the keystone url', $e); } elseif ($statusCode === 412) { - throw new StorageAuthException('Precondition failed, verify the keystone url', $e); + throw new StorageAuthException('Precondition failed while connecting to object storage, verify the keystone url', $e); } elseif ($statusCode === 401) { - throw new StorageAuthException('Authentication failed, verify the username, password and possibly tenant', $e); + throw new StorageAuthException('Authentication failed while connecting to object storage, verify the username, password and possibly tenant', $e); } else { - throw new StorageAuthException('Unknown error', $e); + throw new StorageAuthException('Unknown error while connecting to object storage', $e); } } catch (RequestException $e) { throw new StorageAuthException('Connection reset while connecting to keystone, verify the keystone url', $e); diff --git a/lib/private/Files/Search/QueryOptimizer/FlattenNestedBool.php b/lib/private/Files/Search/QueryOptimizer/FlattenNestedBool.php index 2bde6f0bbdb..bb7bef2ed63 100644 --- a/lib/private/Files/Search/QueryOptimizer/FlattenNestedBool.php +++ b/lib/private/Files/Search/QueryOptimizer/FlattenNestedBool.php @@ -14,8 +14,8 @@ class FlattenNestedBool extends QueryOptimizerStep { public function processOperator(ISearchOperator &$operator) { if ( $operator instanceof SearchBinaryOperator && ( - $operator->getType() === ISearchBinaryOperator::OPERATOR_OR || - $operator->getType() === ISearchBinaryOperator::OPERATOR_AND + $operator->getType() === ISearchBinaryOperator::OPERATOR_OR + || $operator->getType() === ISearchBinaryOperator::OPERATOR_AND ) ) { $newArguments = []; diff --git a/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php b/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php index cb04351534a..7e99c04f197 100644 --- a/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php +++ b/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php @@ -16,11 +16,11 @@ class FlattenSingleArgumentBinaryOperation extends ReplacingOptimizerStep { public function processOperator(ISearchOperator &$operator): bool { parent::processOperator($operator); if ( - $operator instanceof ISearchBinaryOperator && - count($operator->getArguments()) === 1 && - ( - $operator->getType() === ISearchBinaryOperator::OPERATOR_OR || - $operator->getType() === ISearchBinaryOperator::OPERATOR_AND + $operator instanceof ISearchBinaryOperator + && count($operator->getArguments()) === 1 + && ( + $operator->getType() === ISearchBinaryOperator::OPERATOR_OR + || $operator->getType() === ISearchBinaryOperator::OPERATOR_AND ) ) { $operator = $operator->getArguments()[0]; diff --git a/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php b/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php index 3f2b36823ab..6df35c9c9a2 100644 --- a/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php +++ b/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php @@ -18,8 +18,8 @@ use OCP\Files\Search\ISearchOperator; class OrEqualsToIn extends ReplacingOptimizerStep { public function processOperator(ISearchOperator &$operator): bool { if ( - $operator instanceof ISearchBinaryOperator && - $operator->getType() === ISearchBinaryOperator::OPERATOR_OR + $operator instanceof ISearchBinaryOperator + && $operator->getType() === ISearchBinaryOperator::OPERATOR_OR ) { $groups = $this->groupEqualsComparisonsByField($operator->getArguments()); $newParts = array_map(function (array $group) { @@ -32,7 +32,7 @@ class OrEqualsToIn extends ReplacingOptimizerStep { $value = $comparison->getValue(); return $value; }, $group); - $in = new SearchComparison(ISearchComparison::COMPARE_IN, $field, $values); + $in = new SearchComparison(ISearchComparison::COMPARE_IN, $field, $values, $group[0]->getExtra()); $pathEqHash = array_reduce($group, function ($pathEqHash, ISearchComparison $comparison) { return $comparison->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true) && $pathEqHash; }, true); diff --git a/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php b/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php index 9d50746d5d1..2994a9365a7 100644 --- a/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php +++ b/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php @@ -52,9 +52,9 @@ class PathPrefixOptimizer extends QueryOptimizerStep { private function operatorPairIsPathPrefix(ISearchOperator $like, ISearchOperator $equal): bool { return ( - $like instanceof ISearchComparison && $equal instanceof ISearchComparison && - !$like->getExtra() && !$equal->getExtra() && $like->getField() === 'path' && $equal->getField() === 'path' && - $like->getType() === ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE && $equal->getType() === ISearchComparison::COMPARE_EQUAL + $like instanceof ISearchComparison && $equal instanceof ISearchComparison + && !$like->getExtra() && !$equal->getExtra() && $like->getField() === 'path' && $equal->getField() === 'path' + && $like->getType() === ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE && $equal->getType() === ISearchComparison::COMPARE_EQUAL && $like->getValue() === SearchComparison::escapeLikeParameter($equal->getValue()) . '/%' ); } diff --git a/lib/private/Files/Search/QueryOptimizer/SplitLargeIn.php b/lib/private/Files/Search/QueryOptimizer/SplitLargeIn.php index 5c7655b49c1..8aee1975708 100644 --- a/lib/private/Files/Search/QueryOptimizer/SplitLargeIn.php +++ b/lib/private/Files/Search/QueryOptimizer/SplitLargeIn.php @@ -18,13 +18,13 @@ use OCP\Files\Search\ISearchOperator; class SplitLargeIn extends ReplacingOptimizerStep { public function processOperator(ISearchOperator &$operator): bool { if ( - $operator instanceof ISearchComparison && - $operator->getType() === ISearchComparison::COMPARE_IN && - count($operator->getValue()) > 1000 + $operator instanceof ISearchComparison + && $operator->getType() === ISearchComparison::COMPARE_IN + && count($operator->getValue()) > 1000 ) { $chunks = array_chunk($operator->getValue(), 1000); $chunkComparisons = array_map(function (array $values) use ($operator) { - return new SearchComparison(ISearchComparison::COMPARE_IN, $operator->getField(), $values); + return new SearchComparison(ISearchComparison::COMPARE_IN, $operator->getField(), $values, $operator->getExtra()); }, $chunks); $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $chunkComparisons); diff --git a/lib/private/Files/Search/SearchBinaryOperator.php b/lib/private/Files/Search/SearchBinaryOperator.php index 59a1ba2dfaf..49f599933f4 100644 --- a/lib/private/Files/Search/SearchBinaryOperator.php +++ b/lib/private/Files/Search/SearchBinaryOperator.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Files/Search/SearchComparison.php b/lib/private/Files/Search/SearchComparison.php index e3a8f800506..c1f0176afd9 100644 --- a/lib/private/Files/Search/SearchComparison.php +++ b/lib/private/Files/Search/SearchComparison.php @@ -20,7 +20,7 @@ class SearchComparison implements ISearchComparison { private string $field, /** @var ParamValue $value */ private \DateTime|int|string|bool|array $value, - private string $extra = '' + private string $extra = '', ) { } diff --git a/lib/private/Files/Search/SearchOrder.php b/lib/private/Files/Search/SearchOrder.php index f3f62b933a1..5a036653f4e 100644 --- a/lib/private/Files/Search/SearchOrder.php +++ b/lib/private/Files/Search/SearchOrder.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -12,7 +13,7 @@ class SearchOrder implements ISearchOrder { public function __construct( private string $direction, private string $field, - private string $extra = '' + private string $extra = '', ) { } diff --git a/lib/private/Files/Search/SearchQuery.php b/lib/private/Files/Search/SearchQuery.php index e7cb031da3e..592749cf4a0 100644 --- a/lib/private/Files/Search/SearchQuery.php +++ b/lib/private/Files/Search/SearchQuery.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,13 +12,13 @@ use OCP\Files\Search\ISearchQuery; use OCP\IUser; class SearchQuery implements ISearchQuery { - /** @var ISearchOperator */ + /** @var ISearchOperator */ private $searchOperation; - /** @var integer */ + /** @var integer */ private $limit; - /** @var integer */ + /** @var integer */ private $offset; - /** @var ISearchOrder[] */ + /** @var ISearchOrder[] */ private $order; /** @var ?IUser */ private $user; @@ -39,7 +40,7 @@ class SearchQuery implements ISearchQuery { int $offset, array $order, ?IUser $user = null, - bool $limitToHome = false + bool $limitToHome = false, ) { $this->searchOperation = $searchOperation; $this->limit = $limit; diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index 53befe57e36..b92c608a81d 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -21,20 +21,21 @@ use OC\Files\Storage\Wrapper\Quota; use OC\Lockdown\Filesystem\NullStorage; use OC\Share\Share; use OC\Share20\ShareDisableChecker; -use OC_App; use OC_Hook; -use OC_Util; -use OCA\Files_External\Config\ConfigAdapter; +use OCA\Files_External\Config\ExternalMountPoint; use OCA\Files_Sharing\External\Mount; use OCA\Files_Sharing\ISharedMountPoint; use OCA\Files_Sharing\SharedMount; +use OCP\App\IAppManager; use OCP\Constants; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\ICachedMountInfo; use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IMountProvider; +use OCP\Files\Config\IRootMountProvider; use OCP\Files\Config\IUserMountCache; +use OCP\Files\Events\BeforeFileSystemSetupEvent; use OCP\Files\Events\InvalidateMountCacheEvent; use OCP\Files\Events\Node\FilesystemTornDownEvent; use OCP\Files\Mount\IMountManager; @@ -80,6 +81,7 @@ class SetupManager { private LoggerInterface $logger, private IConfig $config, private ShareDisableChecker $shareDisableChecker, + private IAppManager $appManager, ) { $this->cache = $cacheFactory->createDistributed('setupmanager::'); $this->listeningForProviders = false; @@ -103,12 +105,13 @@ class SetupManager { $this->setupBuiltinWrappersDone = true; // load all filesystem apps before, so no setup-hook gets lost - OC_App::loadApps(['filesystem']); + $this->appManager->loadApps(['filesystem']); $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false); Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) { - if ($mount->getOptions() && $storage->instanceOfStorage(Common::class)) { - $storage->setMountOptions($mount->getOptions()); + if ($storage->instanceOfStorage(Common::class)) { + $options = array_merge($mount->getOptions(), ['mount_point' => $mountPoint]); + $storage->setMountOptions($options); } return $storage; }); @@ -133,7 +136,7 @@ class SetupManager { // install storage availability wrapper, before most other wrappers Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage, IMountPoint $mount) { - $externalMount = $mount instanceof ConfigAdapter || $mount instanceof Mount; + $externalMount = $mount instanceof ExternalMountPoint || $mount instanceof Mount; if ($externalMount && !$storage->isLocal()) { return new Availability(['storage' => $storage]); } @@ -154,7 +157,7 @@ class SetupManager { if ($mount instanceof HomeMountPoint) { $user = $mount->getUser(); return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) { - return OC_Util::getUserQuota($user); + return $user->getQuotaBytes(); }, 'root' => 'files', 'include_external_storage' => $quotaIncludeExternal]); } @@ -169,9 +172,9 @@ class SetupManager { return new PermissionsMask([ 'storage' => $storage, 'mask' => Constants::PERMISSION_ALL & ~( - Constants::PERMISSION_UPDATE | - Constants::PERMISSION_CREATE | - Constants::PERMISSION_DELETE + Constants::PERMISSION_UPDATE + | Constants::PERMISSION_CREATE + | Constants::PERMISSION_DELETE ), ]); } @@ -200,7 +203,7 @@ class SetupManager { $this->setupForUserWith($user, function () use ($user) { $this->mountProviderCollection->addMountForUser($user, $this->mountManager, function ( - IMountProvider $provider + IMountProvider $provider, ) use ($user) { return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]); }); @@ -226,8 +229,12 @@ class SetupManager { $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false); + // TODO remove hook OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]); + $event = new BeforeFileSystemSetupEvent($user); + $this->eventDispatcher->dispatchTyped($event); + Filesystem::logWarningWhenAddingStorageWrapper($prevLogging); $userDir = '/' . $user->getUID() . '/files'; @@ -274,14 +281,18 @@ class SetupManager { $mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) { return str_starts_with($mount->getMountPoint(), $userRoot); }); - $allProviders = array_map(function (IMountProvider $provider) { + $allProviders = array_map(function (IMountProvider|IHomeMountProvider|IRootMountProvider $provider) { return get_class($provider); - }, $this->mountProviderCollection->getProviders()); + }, array_merge( + $this->mountProviderCollection->getProviders(), + $this->mountProviderCollection->getHomeProviders(), + $this->mountProviderCollection->getRootProviders(), + )); $newProviders = array_diff($allProviders, $previouslySetupProviders); $mounts = array_filter($mounts, function (IMountPoint $mount) use ($previouslySetupProviders) { return !in_array($mount->getMountProvider(), $previouslySetupProviders); }); - $this->userMountCache->registerMounts($user, $mounts, $newProviders); + $this->registerMounts($user, $mounts, $newProviders); $cacheDuration = $this->config->getSystemValueInt('fs_mount_cache_duration', 5 * 60); if ($cacheDuration > 0) { @@ -381,7 +392,7 @@ class SetupManager { } // for the user's home folder, and includes children we need everything always - if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) { + if (rtrim($path) === '/' . $user->getUID() . '/files' && $includeChildren) { $this->setupForUser($user); return; } @@ -411,7 +422,7 @@ class SetupManager { $setupProviders[] = $cachedMount->getMountProvider(); $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]); } else { - $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup"); + $this->logger->debug('mount at ' . $cachedMount->getMountPoint() . ' has no provider set, performing full setup'); $this->eventLogger->end('fs:setup:user:path:find'); $this->setupForUser($user); $this->eventLogger->end('fs:setup:user:path'); @@ -428,7 +439,7 @@ class SetupManager { }, false); if ($needsFullSetup) { - $this->logger->debug("mount has no provider set, performing full setup"); + $this->logger->debug('mount has no provider set, performing full setup'); $this->setupForUser($user); $this->eventLogger->end('fs:setup:user:path'); return; @@ -446,7 +457,7 @@ class SetupManager { } if (count($mounts)) { - $this->userMountCache->registerMounts($user, $mounts, $currentProviders); + $this->registerMounts($user, $mounts, $currentProviders); $this->setupForUserWith($user, function () use ($mounts) { array_walk($mounts, [$this->mountManager, 'addMount']); }); @@ -490,7 +501,7 @@ class SetupManager { return; } - $this->eventLogger->start('fs:setup:user:providers', "Setup filesystem for " . implode(', ', $providers)); + $this->eventLogger->start('fs:setup:user:providers', 'Setup filesystem for ' . implode(', ', $providers)); $this->oneTimeUserSetup($user); @@ -517,7 +528,7 @@ class SetupManager { $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, $providers); } - $this->userMountCache->registerMounts($user, $mounts, $providers); + $this->registerMounts($user, $mounts, $providers); $this->setupForUserWith($user, function () use ($mounts) { array_walk($mounts, [$this->mountManager, 'addMount']); }); @@ -541,7 +552,7 @@ class SetupManager { if (!$this->listeningForProviders) { $this->listeningForProviders = true; $this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function ( - IMountProvider $provider + IMountProvider $provider, ) { foreach ($this->setupUsers as $userId) { $user = $this->userManager->get($userId); @@ -567,7 +578,7 @@ class SetupManager { $this->eventDispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event) { $this->cache->remove($event->getShare()->getSharedWith()); }); - $this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event + $this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event, ) { if ($user = $event->getUser()) { $this->cache->remove($user->getUID()); @@ -589,4 +600,10 @@ class SetupManager { }); } } + + private function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null): void { + if ($this->lockdownManager->canAccessFilesystem()) { + $this->userMountCache->registerMounts($user, $mounts, $mountProviderClasses); + } + } } diff --git a/lib/private/Files/SetupManagerFactory.php b/lib/private/Files/SetupManagerFactory.php index 6ae38c6fdbc..d2fe978fa9e 100644 --- a/lib/private/Files/SetupManagerFactory.php +++ b/lib/private/Files/SetupManagerFactory.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace OC\Files; use OC\Share20\ShareDisableChecker; +use OCP\App\IAppManager; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\IMountProviderCollection; @@ -36,6 +37,7 @@ class SetupManagerFactory { private LoggerInterface $logger, private IConfig $config, private ShareDisableChecker $shareDisableChecker, + private IAppManager $appManager, ) { $this->setupManager = null; } @@ -55,6 +57,7 @@ class SetupManagerFactory { $this->logger, $this->config, $this->shareDisableChecker, + $this->appManager, ); } return $this->setupManager; diff --git a/lib/private/Files/SimpleFS/SimpleFile.php b/lib/private/Files/SimpleFS/SimpleFile.php index cbb3af4db29..d9c1b47d2f1 100644 --- a/lib/private/Files/SimpleFS/SimpleFile.php +++ b/lib/private/Files/SimpleFS/SimpleFile.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,9 +7,11 @@ namespace OC\Files\SimpleFS; use OCP\Files\File; +use OCP\Files\GenericFileException; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Lock\LockedException; class SimpleFile implements ISimpleFile { private File $file; @@ -48,8 +51,10 @@ class SimpleFile implements ISimpleFile { /** * Get the content * - * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException * @throws NotFoundException + * @throws NotPermittedException */ public function getContent(): string { $result = $this->file->getContent(); @@ -65,8 +70,10 @@ class SimpleFile implements ISimpleFile { * Overwrite the file * * @param string|resource $data - * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException * @throws NotFoundException + * @throws NotPermittedException */ public function putContent($data): void { try { diff --git a/lib/private/Files/SimpleFS/SimpleFolder.php b/lib/private/Files/SimpleFS/SimpleFolder.php index 0da552e402b..62f3db25e9b 100644 --- a/lib/private/Files/SimpleFS/SimpleFolder.php +++ b/lib/private/Files/SimpleFS/SimpleFolder.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index e613e435f03..2dc359169d7 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -13,21 +13,28 @@ use OC\Files\Cache\Propagator; use OC\Files\Cache\Scanner; use OC\Files\Cache\Updater; use OC\Files\Cache\Watcher; +use OC\Files\FilenameValidator; use OC\Files\Filesystem; +use OC\Files\ObjectStore\ObjectStoreStorage; +use OC\Files\Storage\Wrapper\Encryption; use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\Wrapper; -use OCP\Files\EmptyFileNameException; -use OCP\Files\FileNameTooLongException; +use OCP\Files; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\IPropagator; +use OCP\Files\Cache\IScanner; +use OCP\Files\Cache\IUpdater; +use OCP\Files\Cache\IWatcher; use OCP\Files\ForbiddenException; use OCP\Files\GenericFileException; -use OCP\Files\InvalidCharacterInPathException; -use OCP\Files\InvalidDirectoryException; +use OCP\Files\IFilenameValidator; use OCP\Files\InvalidPathException; -use OCP\Files\ReservedWordException; +use OCP\Files\Storage\IConstructableStorage; use OCP\Files\Storage\ILockingStorage; use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IWriteStreamStorage; use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; use OCP\Server; @@ -44,85 +51,75 @@ use Psr\Log\LoggerInterface; * Some \OC\Files\Storage\Common methods call functions which are first defined * in classes which extend it, e.g. $this->stat() . */ -abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { +abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage, IConstructableStorage { use LocalTempFileTrait; - protected $cache; - protected $scanner; - protected $watcher; - protected $propagator; + protected ?Cache $cache = null; + protected ?Scanner $scanner = null; + protected ?Watcher $watcher = null; + protected ?Propagator $propagator = null; protected $storageCache; - protected $updater; + protected ?Updater $updater = null; - protected $mountOptions = []; + protected array $mountOptions = []; protected $owner = null; - /** @var ?bool */ - private $shouldLogLocks = null; - /** @var ?LoggerInterface */ - private $logger; + private ?bool $shouldLogLocks = null; + private ?LoggerInterface $logger = null; + private ?IFilenameValidator $filenameValidator = null; - public function __construct($parameters) { + public function __construct(array $parameters) { } - /** - * Remove a file or folder - * - * @param string $path - * @return bool - */ - protected function remove($path) { - if ($this->is_dir($path)) { - return $this->rmdir($path); - } elseif ($this->is_file($path)) { - return $this->unlink($path); - } else { - return false; + protected function remove(string $path): bool { + if ($this->file_exists($path)) { + if ($this->is_dir($path)) { + return $this->rmdir($path); + } elseif ($this->is_file($path)) { + return $this->unlink($path); + } } + return false; } - public function is_dir($path) { + public function is_dir(string $path): bool { return $this->filetype($path) === 'dir'; } - public function is_file($path) { + public function is_file(string $path): bool { return $this->filetype($path) === 'file'; } - public function filesize($path): false|int|float { + public function filesize(string $path): int|float|false { if ($this->is_dir($path)) { return 0; //by definition } else { $stat = $this->stat($path); - if (isset($stat['size'])) { - return $stat['size']; - } else { - return 0; - } + return isset($stat['size']) ? $stat['size'] : 0; } } - public function isReadable($path) { + public function isReadable(string $path): bool { // at least check whether it exists // subclasses might want to implement this more thoroughly return $this->file_exists($path); } - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { // at least check whether it exists // subclasses might want to implement this more thoroughly // a non-existing file/folder isn't updatable return $this->file_exists($path); } - public function isCreatable($path) { + public function isCreatable(string $path): bool { if ($this->is_dir($path) && $this->isUpdatable($path)) { return true; } return false; } - public function isDeletable($path) { + public function isDeletable(string $path): bool { if ($path === '' || $path === '/') { return $this->isUpdatable($path); } @@ -130,11 +127,11 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $this->isUpdatable($parent) && $this->isUpdatable($path); } - public function isSharable($path) { + public function isSharable(string $path): bool { return $this->isReadable($path); } - public function getPermissions($path) { + public function getPermissions(string $path): int { $permissions = 0; if ($this->isCreatable($path)) { $permissions |= \OCP\Constants::PERMISSION_CREATE; @@ -154,7 +151,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $permissions; } - public function filemtime($path) { + public function filemtime(string $path): int|false { $stat = $this->stat($path); if (isset($stat['mtime']) && $stat['mtime'] > 0) { return $stat['mtime']; @@ -163,8 +160,8 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } } - public function file_get_contents($path) { - $handle = $this->fopen($path, "r"); + public function file_get_contents(string $path): string|false { + $handle = $this->fopen($path, 'r'); if (!$handle) { return false; } @@ -173,8 +170,8 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $data; } - public function file_put_contents($path, $data) { - $handle = $this->fopen($path, "w"); + public function file_put_contents(string $path, mixed $data): int|float|false { + $handle = $this->fopen($path, 'w'); if (!$handle) { return false; } @@ -184,19 +181,19 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $count; } - public function rename($source, $target) { + public function rename(string $source, string $target): bool { $this->remove($target); $this->removeCachedFile($source); return $this->copy($source, $target) and $this->remove($source); } - public function copy($source, $target) { + public function copy(string $source, string $target): bool { if ($this->is_dir($source)) { $this->remove($target); $dir = $this->opendir($source); $this->mkdir($target); - while ($file = readdir($dir)) { + while (($file = readdir($dir)) !== false) { if (!Filesystem::isIgnoredDir($file)) { if (!$this->copy($source . '/' . $file, $target . '/' . $file)) { closedir($dir); @@ -209,16 +206,16 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } else { $sourceStream = $this->fopen($source, 'r'); $targetStream = $this->fopen($target, 'w'); - [, $result] = \OC_Helper::streamCopy($sourceStream, $targetStream); + [, $result] = Files::streamCopy($sourceStream, $targetStream, true); if (!$result) { - \OCP\Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target"); + Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target"); } $this->removeCachedFile($target); return $result; } } - public function getMimeType($path) { + public function getMimeType(string $path): string|false { if ($this->is_dir($path)) { return 'httpd/unix-directory'; } elseif ($this->file_exists($path)) { @@ -228,31 +225,26 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } } - public function hash($type, $path, $raw = false) { + public function hash(string $type, string $path, bool $raw = false): string|false { $fh = $this->fopen($path, 'rb'); + if (!$fh) { + return false; + } $ctx = hash_init($type); hash_update_stream($ctx, $fh); fclose($fh); return hash_final($ctx, $raw); } - public function search($query) { - return $this->searchInDir($query); - } - - public function getLocalFile($path) { + public function getLocalFile(string $path): string|false { return $this->getCachedFile($path); } - /** - * @param string $path - * @param string $target - */ - private function addLocalFolder($path, $target) { + private function addLocalFolder(string $path, string $target): void { $dh = $this->opendir($path); if (is_resource($dh)) { while (($file = readdir($dh)) !== false) { - if (!\OC\Files\Filesystem::isIgnoredDir($file)) { + if (!Filesystem::isIgnoredDir($file)) { if ($this->is_dir($path . '/' . $file)) { mkdir($target . '/' . $file); $this->addLocalFolder($path . '/' . $file, $target . '/' . $file); @@ -265,17 +257,12 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } } - /** - * @param string $query - * @param string $dir - * @return array - */ - protected function searchInDir($query, $dir = '') { + protected function searchInDir(string $query, string $dir = ''): array { $files = []; $dh = $this->opendir($dir); if (is_resource($dh)) { while (($item = readdir($dh)) !== false) { - if (\OC\Files\Filesystem::isIgnoredDir($item)) { + if (Filesystem::isIgnoredDir($item)) { continue; } if (strstr(strtolower($item), strtolower($query)) !== false) { @@ -291,18 +278,15 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } /** + * @inheritDoc * Check if a file or folder has been updated since $time * * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking * the mtime should always return false here. As a result storage implementations that always return false expect * exclusive access to the backend and will not pick up files that have been added in a way that circumvents * Nextcloud filesystem. - * - * @param string $path - * @param int $time - * @return bool */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { return $this->filemtime($path) > $time; } @@ -314,76 +298,78 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $dependencies; } - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): ICache { if (!$storage) { $storage = $this; } + /** @var self $storage */ if (!isset($storage->cache)) { $storage->cache = new Cache($storage, $this->getCacheDependencies()); } return $storage->cache; } - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } + if (!$storage->instanceOfStorage(self::class)) { + throw new \InvalidArgumentException('Storage is not of the correct class'); + } if (!isset($storage->scanner)) { $storage->scanner = new Scanner($storage); } return $storage->scanner; } - public function getWatcher($path = '', $storage = null) { + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { if (!$storage) { $storage = $this; } if (!isset($this->watcher)) { $this->watcher = new Watcher($storage); - $globalPolicy = \OC::$server->getConfig()->getSystemValue('filesystem_check_changes', Watcher::CHECK_NEVER); + $globalPolicy = Server::get(IConfig::class)->getSystemValueInt('filesystem_check_changes', Watcher::CHECK_NEVER); $this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy)); } return $this->watcher; } - /** - * get a propagator instance for the cache - * - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher - * @return \OC\Files\Cache\Propagator - */ - public function getPropagator($storage = null) { + public function getPropagator(?IStorage $storage = null): IPropagator { if (!$storage) { $storage = $this; } + if (!$storage->instanceOfStorage(self::class)) { + throw new \InvalidArgumentException('Storage is not of the correct class'); + } + /** @var self $storage */ if (!isset($storage->propagator)) { - $config = \OC::$server->getSystemConfig(); - $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getValue('instanceid')]); + $config = Server::get(IConfig::class); + $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getSystemValueString('instanceid')]); } return $storage->propagator; } - public function getUpdater($storage = null) { + public function getUpdater(?IStorage $storage = null): IUpdater { if (!$storage) { $storage = $this; } + if (!$storage->instanceOfStorage(self::class)) { + throw new \InvalidArgumentException('Storage is not of the correct class'); + } + /** @var self $storage */ if (!isset($storage->updater)) { $storage->updater = new Updater($storage); } return $storage->updater; } - public function getStorageCache($storage = null) { - return $this->getCache($storage)->getStorageCache(); + public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage { + /** @var Cache $cache */ + $cache = $this->getCache(storage: $storage); + return $cache->getStorageCache(); } - /** - * get the owner of a path - * - * @param string $path The path to get the owner - * @return string|false uid or false - */ - public function getOwner($path) { + public function getOwner(string $path): string|false { if ($this->owner === null) { $this->owner = \OC_User::getUser(); } @@ -391,13 +377,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $this->owner; } - /** - * get the ETag for a file or folder - * - * @param string $path - * @return string - */ - public function getETag($path) { + public function getETag(string $path): string|false { return uniqid(); } @@ -408,8 +388,8 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { * @param string $path The path to clean * @return string cleaned path */ - public function cleanPath($path) { - if (strlen($path) == 0 or $path[0] != '/') { + public function cleanPath(string $path): string { + if (strlen($path) == 0 || $path[0] != '/') { $path = '/' . $path; } @@ -427,39 +407,28 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { /** * Test a storage for availability - * - * @return bool */ - public function test() { + public function test(): bool { try { if ($this->stat('')) { return true; } - \OC::$server->get(LoggerInterface::class)->info("External storage not available: stat() failed"); + Server::get(LoggerInterface::class)->info('External storage not available: stat() failed'); return false; } catch (\Exception $e) { - \OC::$server->get(LoggerInterface::class)->warning( - "External storage not available: " . $e->getMessage(), + Server::get(LoggerInterface::class)->warning( + 'External storage not available: ' . $e->getMessage(), ['exception' => $e] ); return false; } } - /** - * get the free space in the storage - * - * @param string $path - * @return int|float|false - */ - public function free_space($path) { + public function free_space(string $path): int|float|false { return \OCP\Files\FileInfo::SPACE_UNKNOWN; } - /** - * {@inheritdoc} - */ - public function isLocal() { + public function isLocal(): bool { // the common implementation returns a temporary file by // default, which is not local return false; @@ -467,11 +436,8 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { /** * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class - * - * @param string $class - * @return bool */ - public function instanceOfStorage($class) { + public function instanceOfStorage(string $class): bool { if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') { // FIXME Temporary fix to keep existing checks working $class = '\OCA\Files_Sharing\SharedStorage'; @@ -483,107 +449,49 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { * A custom storage implementation can return an url for direct download of a give file. * * For now the returned array can hold the parameter url - in future more attributes might follow. - * - * @param string $path - * @return array|false */ - public function getDirectDownload($path) { + public function getDirectDownload(string $path): array|false { return []; } - /** - * @inheritdoc - * @throws InvalidPathException - */ - public function verifyPath($path, $fileName) { - // verify empty and dot files - $trimmed = trim($fileName); - if ($trimmed === '') { - throw new EmptyFileNameException(); - } - - if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) { - throw new InvalidDirectoryException(); - } + public function verifyPath(string $path, string $fileName): void { + $this->getFilenameValidator() + ->validateFilename($fileName); - if (!\OC::$server->getDatabaseConnection()->supports4ByteText()) { - // verify database - e.g. mysql only 3-byte chars - if (preg_match('%(?: - \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 - | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 - | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 -)%xs', $fileName)) { - throw new InvalidCharacterInPathException(); + // verify also the path is valid + if ($path && $path !== '/' && $path !== '.') { + try { + $this->verifyPath(dirname($path), basename($path)); + } catch (InvalidPathException $e) { + // Ignore invalid file type exceptions on directories + if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) { + $l = \OCP\Util::getL10N('lib'); + throw new InvalidPathException($l->t('Invalid parent path'), previous: $e); + } } } - - // 255 characters is the limit on common file systems (ext/xfs) - // oc_filecache has a 250 char length limit for the filename - if (isset($fileName[250])) { - throw new FileNameTooLongException(); - } - - // NOTE: $path will remain unverified for now - $this->verifyPosixPath($fileName); } /** - * @param string $fileName - * @throws InvalidPathException + * Get the filename validator + * (cached for performance) */ - protected function verifyPosixPath($fileName) { - $invalidChars = \OCP\Util::getForbiddenFileNameChars(); - $this->scanForInvalidCharacters($fileName, $invalidChars); - - $fileName = trim($fileName); - $reservedNames = ['*']; - if (in_array($fileName, $reservedNames)) { - throw new ReservedWordException(); + protected function getFilenameValidator(): IFilenameValidator { + if ($this->filenameValidator === null) { + $this->filenameValidator = Server::get(IFilenameValidator::class); } + return $this->filenameValidator; } - /** - * @param string $fileName - * @param string[] $invalidChars - * @throws InvalidPathException - */ - private function scanForInvalidCharacters(string $fileName, array $invalidChars) { - foreach ($invalidChars as $char) { - if (str_contains($fileName, $char)) { - throw new InvalidCharacterInPathException(); - } - } - - $sanitizedFileName = filter_var($fileName, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW); - if ($sanitizedFileName !== $fileName) { - throw new InvalidCharacterInPathException(); - } - } - - /** - * @param array $options - */ - public function setMountOptions(array $options) { + public function setMountOptions(array $options): void { $this->mountOptions = $options; } - /** - * @param string $name - * @param mixed $default - * @return mixed - */ - public function getMountOption($name, $default = null) { + public function getMountOption(string $name, mixed $default = null): mixed { return $this->mountOptions[$name] ?? $default; } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @param bool $preserveMtime - * @return bool - */ - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool { if ($sourceStorage === $this) { return $this->copy($sourceInternalPath, $targetInternalPath); } @@ -593,7 +501,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { $result = $this->mkdir($targetInternalPath); if (is_resource($dh)) { $result = true; - while ($result and ($file = readdir($dh)) !== false) { + while ($result && ($file = readdir($dh)) !== false) { if (!Filesystem::isIgnoredDir($file)) { $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file); } @@ -607,7 +515,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { $this->writeStream($targetInternalPath, $source); $result = true; } catch (\Exception $e) { - \OC::$server->get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]); + Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]); } } @@ -628,9 +536,6 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { /** * Check if a storage is the same as the current one, including wrapped storages - * - * @param IStorage $storage - * @return bool */ private function isSameStorage(IStorage $storage): bool { while ($storage->instanceOfStorage(Wrapper::class)) { @@ -643,14 +548,11 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $storage === $this; } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { - if ($this->isSameStorage($sourceStorage)) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + if ( + !$sourceStorage->instanceOfStorage(Encryption::class) + && $this->isSameStorage($sourceStorage) + ) { // resolve any jailed paths while ($sourceStorage->instanceOfStorage(Jail::class)) { /** @@ -669,19 +571,27 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true); if ($result) { - if ($sourceStorage->is_dir($sourceInternalPath)) { - $result = $sourceStorage->rmdir($sourceInternalPath); - } else { - $result = $sourceStorage->unlink($sourceInternalPath); + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(true); + } + try { + if ($sourceStorage->is_dir($sourceInternalPath)) { + $result = $sourceStorage->rmdir($sourceInternalPath); + } else { + $result = $sourceStorage->unlink($sourceInternalPath); + } + } finally { + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(false); + } } } return $result; } - /** - * @inheritdoc - */ - public function getMetaData($path) { + public function getMetaData(string $path): ?array { if (Filesystem::isFileBlacklisted($path)) { throw new ForbiddenException('Invalid path: ' . $path, false); } @@ -711,13 +621,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $data; } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function acquireLock($path, $type, ILockingProvider $provider) { + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { $logger = $this->getLockLogger(); if ($logger) { $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; @@ -744,13 +648,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function releaseLock($path, $type, ILockingProvider $provider) { + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { $logger = $this->getLockLogger(); if ($logger) { $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; @@ -777,13 +675,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function changeLock($path, $type, ILockingProvider $provider) { + public function changeLock(string $path, int $type, ILockingProvider $provider): void { $logger = $this->getLockLogger(); if ($logger) { $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; @@ -812,8 +704,8 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { private function getLockLogger(): ?LoggerInterface { if (is_null($this->shouldLogLocks)) { - $this->shouldLogLocks = \OC::$server->getConfig()->getSystemValueBool('filelocking.debug', false); - $this->logger = $this->shouldLogLocks ? \OC::$server->get(LoggerInterface::class) : null; + $this->shouldLogLocks = Server::get(IConfig::class)->getSystemValueBool('filelocking.debug', false); + $this->logger = $this->shouldLogLocks ? Server::get(LoggerInterface::class) : null; } return $this->logger; } @@ -821,54 +713,31 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { /** * @return array [ available, last_checked ] */ - public function getAvailability() { + public function getAvailability(): array { return $this->getStorageCache()->getAvailability(); } - /** - * @param bool $isAvailable - */ - public function setAvailability($isAvailable) { + public function setAvailability(bool $isAvailable): void { $this->getStorageCache()->setAvailability($isAvailable); } - /** - * Allow setting the storage owner - * - * This can be used for storages that do not have a dedicated owner, where we want to - * pass the user that we setup the mountpoint for along to the storage layer - * - * @param string|null $user - * @return void - */ public function setOwner(?string $user): void { $this->owner = $user; } - /** - * @return bool - */ - public function needsPartFile() { + public function needsPartFile(): bool { return true; } - /** - * fallback implementation - * - * @param string $path - * @param resource $stream - * @param int $size - * @return int - */ public function writeStream(string $path, $stream, ?int $size = null): int { $target = $this->fopen($path, 'w'); if (!$target) { throw new GenericFileException("Failed to open $path for writing"); } try { - [$count, $result] = \OC_Helper::streamCopy($stream, $target); + [$count, $result] = Files::streamCopy($stream, $target, true); if (!$result) { - throw new GenericFileException("Failed to copy stream"); + throw new GenericFileException('Failed to copy stream'); } } finally { fclose($target); @@ -877,7 +746,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { return $count; } - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { $dh = $this->opendir($directory); if ($dh === false) { diff --git a/lib/private/Files/Storage/CommonTest.php b/lib/private/Files/Storage/CommonTest.php index b92e493bc2c..da796130899 100644 --- a/lib/private/Files/Storage/CommonTest.php +++ b/lib/private/Files/Storage/CommonTest.php @@ -14,47 +14,47 @@ class CommonTest extends \OC\Files\Storage\Common { */ private $storage; - public function __construct($params) { - $this->storage = new \OC\Files\Storage\Local($params); + public function __construct(array $parameters) { + $this->storage = new \OC\Files\Storage\Local($parameters); } - public function getId() { - return 'test::'.$this->storage->getId(); + public function getId(): string { + return 'test::' . $this->storage->getId(); } - public function mkdir($path) { + public function mkdir(string $path): bool { return $this->storage->mkdir($path); } - public function rmdir($path) { + public function rmdir(string $path): bool { return $this->storage->rmdir($path); } - public function opendir($path) { + public function opendir(string $path) { return $this->storage->opendir($path); } - public function stat($path) { + public function stat(string $path): array|false { return $this->storage->stat($path); } - public function filetype($path) { + public function filetype(string $path): string|false { return @$this->storage->filetype($path); } - public function isReadable($path) { + public function isReadable(string $path): bool { return $this->storage->isReadable($path); } - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { return $this->storage->isUpdatable($path); } - public function file_exists($path) { + public function file_exists(string $path): bool { return $this->storage->file_exists($path); } - public function unlink($path) { + public function unlink(string $path): bool { return $this->storage->unlink($path); } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { return $this->storage->fopen($path, $mode); } - public function free_space($path) { + public function free_space(string $path): int|float|false { return $this->storage->free_space($path); } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { return $this->storage->touch($path, $mtime); } } diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index 63ef1399a69..2d166b5438d 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -20,9 +20,11 @@ use OCP\Files\ForbiddenException; use OCP\Files\IMimeTypeDetector; use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; +use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\ICertificateManager; use OCP\IConfig; +use OCP\Server; use OCP\Util; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; @@ -81,31 +83,31 @@ class DAV extends Common { ]; /** - * @param array $params + * @param array $parameters * @throws \Exception */ - public function __construct($params) { + public function __construct(array $parameters) { $this->statCache = new ArrayCache(); - $this->httpClientService = \OC::$server->get(IClientService::class); - if (isset($params['host']) && isset($params['user']) && isset($params['password'])) { - $host = $params['host']; + $this->httpClientService = Server::get(IClientService::class); + if (isset($parameters['host']) && isset($parameters['user']) && isset($parameters['password'])) { + $host = $parameters['host']; //remove leading http[s], will be generated in createBaseUri() - if (str_starts_with($host, "https://")) { + if (str_starts_with($host, 'https://')) { $host = substr($host, 8); - } elseif (str_starts_with($host, "http://")) { + } elseif (str_starts_with($host, 'http://')) { $host = substr($host, 7); } $this->host = $host; - $this->user = $params['user']; - $this->password = $params['password']; - if (isset($params['authType'])) { - $this->authType = $params['authType']; + $this->user = $parameters['user']; + $this->password = $parameters['password']; + if (isset($parameters['authType'])) { + $this->authType = $parameters['authType']; } - if (isset($params['secure'])) { - if (is_string($params['secure'])) { - $this->secure = ($params['secure'] === 'true'); + if (isset($parameters['secure'])) { + if (is_string($parameters['secure'])) { + $this->secure = ($parameters['secure'] === 'true'); } else { - $this->secure = (bool)$params['secure']; + $this->secure = (bool)$parameters['secure']; } } else { $this->secure = false; @@ -114,20 +116,20 @@ class DAV extends Common { // inject mock for testing $this->certManager = \OC::$server->getCertificateManager(); } - $this->root = $params['root'] ?? '/'; + $this->root = rawurldecode($parameters['root'] ?? '/'); $this->root = '/' . ltrim($this->root, '/'); $this->root = rtrim($this->root, '/') . '/'; } else { throw new \Exception('Invalid webdav storage configuration'); } - $this->logger = \OC::$server->get(LoggerInterface::class); - $this->eventLogger = \OC::$server->get(IEventLogger::class); + $this->logger = Server::get(LoggerInterface::class); + $this->eventLogger = Server::get(IEventLogger::class); // This timeout value will be used for the download and upload of files - $this->timeout = \OC::$server->get(IConfig::class)->getSystemValueInt('davstorage.request_timeout', 30); + $this->timeout = Server::get(IConfig::class)->getSystemValueInt('davstorage.request_timeout', IClient::DEFAULT_REQUEST_TIMEOUT); $this->mimeTypeDetector = \OC::$server->getMimeTypeDetector(); } - protected function init() { + protected function init(): void { if ($this->ready) { return; } @@ -142,7 +144,7 @@ class DAV extends Common { $settings['authType'] = $this->authType; } - $proxy = \OC::$server->getConfig()->getSystemValueString('proxy', ''); + $proxy = Server::get(IConfig::class)->getSystemValueString('proxy', ''); if ($proxy !== '') { $settings['proxy'] = $proxy; } @@ -162,13 +164,13 @@ class DAV extends Common { $lastRequestStart = 0; $this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) { - $this->logger->debug("sending dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl(), ['app' => 'dav']); + $this->logger->debug('sending dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl(), ['app' => 'dav']); $lastRequestStart = microtime(true); - $this->eventLogger->start('fs:storage:dav:request', "Sending dav request to external storage"); + $this->eventLogger->start('fs:storage:dav:request', 'Sending dav request to external storage'); }); $this->client->on('afterRequest', function (RequestInterface $request) use (&$lastRequestStart) { $elapsed = microtime(true) - $lastRequestStart; - $this->logger->debug("dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl() . " took " . round($elapsed * 1000, 1) . "ms", ['app' => 'dav']); + $this->logger->debug('dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl() . ' took ' . round($elapsed * 1000, 1) . 'ms', ['app' => 'dav']); $this->eventLogger->end('fs:storage:dav:request'); }); } @@ -176,27 +178,24 @@ class DAV extends Common { /** * Clear the stat cache */ - public function clearStatCache() { + public function clearStatCache(): void { $this->statCache->clear(); } - /** {@inheritdoc} */ - public function getId() { + public function getId(): string { return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root; } - /** {@inheritdoc} */ - public function createBaseUri() { + public function createBaseUri(): string { $baseUri = 'http'; if ($this->secure) { $baseUri .= 's'; } - $baseUri .= '://' . $this->host . $this->root; + $baseUri .= '://' . $this->host . $this->encodePath($this->root); return $baseUri; } - /** {@inheritdoc} */ - public function mkdir($path) { + public function mkdir(string $path): bool { $this->init(); $path = $this->cleanPath($path); $result = $this->simpleResponse('MKCOL', $path, null, 201); @@ -206,8 +205,7 @@ class DAV extends Common { return $result; } - /** {@inheritdoc} */ - public function rmdir($path) { + public function rmdir(string $path): bool { $this->init(); $path = $this->cleanPath($path); // FIXME: some WebDAV impl return 403 when trying to DELETE @@ -218,8 +216,7 @@ class DAV extends Common { return $result; } - /** {@inheritdoc} */ - public function opendir($path) { + public function opendir(string $path) { $this->init(); $path = $this->cleanPath($path); try { @@ -243,16 +240,17 @@ class DAV extends Common { * * @param string $path path to propfind * - * @return array|boolean propfind response or false if the entry was not found + * @return array|false propfind response or false if the entry was not found * * @throws ClientHttpException */ - protected function propfind($path) { + protected function propfind(string $path): array|false { $path = $this->cleanPath($path); $cachedResponse = $this->statCache->get($path); // we either don't know it, or we know it exists but need more details if (is_null($cachedResponse) || $cachedResponse === true) { $this->init(); + $response = false; try { $response = $this->client->propFind( $this->encodePath($path), @@ -263,9 +261,9 @@ class DAV extends Common { if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) { $this->statCache->clear($path . '/'); $this->statCache->set($path, false); - return false; + } else { + $this->convertException($e, $path); } - $this->convertException($e, $path); } catch (\Exception $e) { $this->convertException($e, $path); } @@ -275,27 +273,25 @@ class DAV extends Common { return $response; } - /** {@inheritdoc} */ - public function filetype($path) { + public function filetype(string $path): string|false { try { $response = $this->propfind($path); if ($response === false) { return false; } $responseType = []; - if (isset($response["{DAV:}resourcetype"])) { + if (isset($response['{DAV:}resourcetype'])) { /** @var ResourceType[] $response */ - $responseType = $response["{DAV:}resourcetype"]->getValue(); + $responseType = $response['{DAV:}resourcetype']->getValue(); } - return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; + return (count($responseType) > 0 && $responseType[0] == '{DAV:}collection') ? 'dir' : 'file'; } catch (\Exception $e) { $this->convertException($e, $path); } return false; } - /** {@inheritdoc} */ - public function file_exists($path) { + public function file_exists(string $path): bool { try { $path = $this->cleanPath($path); $cachedState = $this->statCache->get($path); @@ -313,8 +309,7 @@ class DAV extends Common { return false; } - /** {@inheritdoc} */ - public function unlink($path) { + public function unlink(string $path): bool { $this->init(); $path = $this->cleanPath($path); $result = $this->simpleResponse('DELETE', $path, null, 204); @@ -323,8 +318,7 @@ class DAV extends Common { return $result; } - /** {@inheritdoc} */ - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $this->init(); $path = $this->cleanPath($path); switch ($mode) { @@ -352,11 +346,17 @@ class DAV extends Common { if ($response->getStatusCode() === Http::STATUS_LOCKED) { throw new \OCP\Lock\LockedException($path); } else { - \OC::$server->get(LoggerInterface::class)->error('Guzzle get returned status code ' . $response->getStatusCode(), ['app' => 'webdav client']); + $this->logger->error('Guzzle get returned status code ' . $response->getStatusCode(), ['app' => 'webdav client']); } } - return $response->getBody(); + $content = $response->getBody(); + + if ($content === null || is_string($content)) { + return false; + } + + return $content; case 'w': case 'wb': case 'a': @@ -380,7 +380,7 @@ class DAV extends Common { if (!$this->isUpdatable($path)) { return false; } - if ($mode === 'w' or $mode === 'w+') { + if ($mode === 'w' || $mode === 'w+') { $tmpFile = $tempManager->getTemporaryFile($ext); } else { $tmpFile = $this->getCachedFile($path); @@ -396,18 +396,16 @@ class DAV extends Common { $this->writeBack($tmpFile, $path); }); } + + return false; } - /** - * @param string $tmpFile - */ - public function writeBack($tmpFile, $path) { + public function writeBack(string $tmpFile, string $path): void { $this->uploadFile($tmpFile, $path); unlink($tmpFile); } - /** {@inheritdoc} */ - public function free_space($path) { + public function free_space(string $path): int|float|false { $this->init(); $path = $this->cleanPath($path); try { @@ -425,8 +423,7 @@ class DAV extends Common { } } - /** {@inheritdoc} */ - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { $this->init(); if (is_null($mtime)) { $mtime = time(); @@ -463,23 +460,14 @@ class DAV extends Common { return true; } - /** - * @param string $path - * @param mixed $data - * @return int|float|false - */ - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { $path = $this->cleanPath($path); $result = parent::file_put_contents($path, $data); $this->statCache->remove($path); return $result; } - /** - * @param string $path - * @param string $target - */ - protected function uploadFile($path, $target) { + protected function uploadFile(string $path, string $target): void { $this->init(); // invalidate @@ -499,8 +487,7 @@ class DAV extends Common { $this->removeCachedFile($target); } - /** {@inheritdoc} */ - public function rename($source, $target) { + public function rename(string $source, string $target): bool { $this->init(); $source = $this->cleanPath($source); $target = $this->cleanPath($target); @@ -531,8 +518,7 @@ class DAV extends Common { return false; } - /** {@inheritdoc} */ - public function copy($source, $target) { + public function copy(string $source, string $target): bool { $this->init(); $source = $this->cleanPath($source); $target = $this->cleanPath($target); @@ -560,7 +546,7 @@ class DAV extends Common { return false; } - public function getMetaData($path) { + public function getMetaData(string $path): ?array { if (Filesystem::isFileBlacklisted($path)) { throw new ForbiddenException('Invalid path: ' . $path, false); } @@ -582,11 +568,11 @@ class DAV extends Common { } $responseType = []; - if (isset($response["{DAV:}resourcetype"])) { + if (isset($response['{DAV:}resourcetype'])) { /** @var ResourceType[] $response */ - $responseType = $response["{DAV:}resourcetype"]->getValue(); + $responseType = $response['{DAV:}resourcetype']->getValue(); } - $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; + $type = (count($responseType) > 0 && $responseType[0] == '{DAV:}collection') ? 'dir' : 'file'; if ($type === 'dir') { $mimeType = 'httpd/unix-directory'; } elseif (isset($response['{DAV:}getcontenttype'])) { @@ -622,31 +608,18 @@ class DAV extends Common { ]; } - /** {@inheritdoc} */ - public function stat($path) { + public function stat(string $path): array|false { $meta = $this->getMetaData($path); - if (!$meta) { - return false; - } else { - return $meta; - } + return $meta ?: false; + } - /** {@inheritdoc} */ - public function getMimeType($path) { + public function getMimeType(string $path): string|false { $meta = $this->getMetaData($path); - if ($meta) { - return $meta['mimetype']; - } else { - return false; - } + return $meta ? $meta['mimetype'] : false; } - /** - * @param string $path - * @return string - */ - public function cleanPath($path) { + public function cleanPath(string $path): string { if ($path === '') { return $path; } @@ -661,21 +634,17 @@ class DAV extends Common { * @param string $path to encode * @return string encoded path */ - protected function encodePath($path) { + protected function encodePath(string $path): string { // slashes need to stay return str_replace('%2F', '/', rawurlencode($path)); } /** - * @param string $method - * @param string $path - * @param string|resource|null $body - * @param int $expected * @return bool * @throws StorageInvalidException * @throws StorageNotAvailableException */ - protected function simpleResponse($method, $path, $body, $expected) { + protected function simpleResponse(string $method, string $path, ?string $body, int $expected): bool { $path = $this->cleanPath($path); try { $response = $this->client->request($method, $this->encodePath($path), $body); @@ -697,55 +666,37 @@ class DAV extends Common { /** * check if curl is installed */ - public static function checkDependencies() { + public static function checkDependencies(): bool { return true; } - /** {@inheritdoc} */ - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE); } - /** {@inheritdoc} */ - public function isCreatable($path) { + public function isCreatable(string $path): bool { return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE); } - /** {@inheritdoc} */ - public function isSharable($path) { + public function isSharable(string $path): bool { return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); } - /** {@inheritdoc} */ - public function isDeletable($path) { + public function isDeletable(string $path): bool { return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE); } - /** {@inheritdoc} */ - public function getPermissions($path) { + public function getPermissions(string $path): int { $stat = $this->getMetaData($path); - if ($stat) { - return $stat['permissions']; - } else { - return 0; - } + return $stat ? $stat['permissions'] : 0; } - /** {@inheritdoc} */ - public function getETag($path) { + public function getETag(string $path): string|false { $meta = $this->getMetaData($path); - if ($meta) { - return $meta['etag']; - } else { - return null; - } + return $meta ? $meta['etag'] : false; } - /** - * @param string $permissionsString - * @return int - */ - protected function parsePermissions($permissionsString) { + protected function parsePermissions(string $permissionsString): int { $permissions = Constants::PERMISSION_READ; if (str_contains($permissionsString, 'R')) { $permissions |= Constants::PERMISSION_SHARE; @@ -763,15 +714,7 @@ class DAV extends Common { return $permissions; } - /** - * check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @throws \OCP\Files\StorageNotAvailableException - * @return bool - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { $this->init(); $path = $this->cleanPath($path); try { @@ -832,13 +775,13 @@ class DAV extends Common { * @param string $path optional path from the operation * * @throws StorageInvalidException if the storage is invalid, for example - * when the authentication expired or is invalid + * when the authentication expired or is invalid * @throws StorageNotAvailableException if the storage is not available, - * which might be temporary + * which might be temporary * @throws ForbiddenException if the action is not allowed */ - protected function convertException(Exception $e, $path = '') { - \OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['app' => 'files_external', 'exception' => $e]); + protected function convertException(Exception $e, string $path = ''): void { + $this->logger->debug($e->getMessage(), ['app' => 'files_external', 'exception' => $e]); if ($e instanceof ClientHttpException) { if ($e->getHttpStatus() === Http::STATUS_LOCKED) { throw new \OCP\Lock\LockedException($path); @@ -869,7 +812,7 @@ class DAV extends Common { // TODO: only log for now, but in the future need to wrap/rethrow exception } - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { $this->init(); $directory = $this->cleanPath($directory); try { diff --git a/lib/private/Files/Storage/FailedStorage.php b/lib/private/Files/Storage/FailedStorage.php index 87a6e7f5041..a8288de48d0 100644 --- a/lib/private/Files/Storage/FailedStorage.php +++ b/lib/private/Files/Storage/FailedStorage.php @@ -20,177 +20,172 @@ class FailedStorage extends Common { protected $e; /** - * @param array $params ['exception' => \Exception] + * @param array $parameters ['exception' => \Exception] */ - public function __construct($params) { - $this->e = $params['exception']; + public function __construct(array $parameters) { + $this->e = $parameters['exception']; if (!$this->e) { throw new \InvalidArgumentException('Missing "exception" argument in FailedStorage constructor'); } } - public function getId() { + public function getId(): string { // we can't return anything sane here return 'failedstorage'; } - public function mkdir($path) { + public function mkdir(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function rmdir($path) { + public function rmdir(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function opendir($path) { + public function opendir(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function is_dir($path) { + public function is_dir(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function is_file($path) { + public function is_file(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function stat($path) { + public function stat(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function filetype($path) { + public function filetype(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function filesize($path): false|int|float { + public function filesize(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function isCreatable($path) { + public function isCreatable(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function isReadable($path) { + public function isReadable(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function isUpdatable($path) { + public function isUpdatable(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function isDeletable($path) { + public function isDeletable(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function isSharable($path) { + public function isSharable(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function getPermissions($path) { + public function getPermissions(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function file_exists($path) { + public function file_exists(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function filemtime($path) { + public function filemtime(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function file_get_contents($path) { + public function file_get_contents(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function unlink($path) { + public function unlink(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function rename($source, $target) { + public function rename(string $source, string $target): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function copy($source, $target) { + public function copy(string $source, string $target): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function getMimeType($path) { + public function getMimeType(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function hash($type, $path, $raw = false) { + public function hash(string $type, string $path, bool $raw = false): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function free_space($path) { + public function free_space(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function search($query) { + public function touch(string $path, ?int $mtime = null): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function touch($path, $mtime = null) { + public function getLocalFile(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function getLocalFile($path) { + public function hasUpdated(string $path, int $time): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function hasUpdated($path, $time) { + public function getETag(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function getETag($path) { + public function getDirectDownload(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function getDirectDownload($path) { - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); - } - - public function verifyPath($path, $fileName) { - return true; + public function verifyPath(string $path, string $fileName): void { } - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function acquireLock($path, $type, ILockingProvider $provider) { + public function acquireLock(string $path, int $type, ILockingProvider $provider): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function releaseLock($path, $type, ILockingProvider $provider) { + public function releaseLock(string $path, int $type, ILockingProvider $provider): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function changeLock($path, $type, ILockingProvider $provider) { + public function changeLock(string $path, int $type, ILockingProvider $provider): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function getAvailability() { + public function getAvailability(): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function setAvailability($isAvailable) { + public function setAvailability(bool $isAvailable): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): FailedCache { return new FailedCache(); } } diff --git a/lib/private/Files/Storage/Home.php b/lib/private/Files/Storage/Home.php index 9a336d2efcc..91b8071ac30 100644 --- a/lib/private/Files/Storage/Home.php +++ b/lib/private/Files/Storage/Home.php @@ -8,6 +8,9 @@ namespace OC\Files\Storage; use OC\Files\Cache\HomePropagator; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\IPropagator; +use OCP\Files\Storage\IStorage; use OCP\IUser; /** @@ -27,25 +30,22 @@ class Home extends Local implements \OCP\Files\IHomeStorage { /** * Construct a Home storage instance * - * @param array $arguments array with "user" containing the - * storage owner + * @param array $parameters array with "user" containing the + * storage owner */ - public function __construct($arguments) { - $this->user = $arguments['user']; + public function __construct(array $parameters) { + $this->user = $parameters['user']; $datadir = $this->user->getHome(); $this->id = 'home::' . $this->user->getUID(); parent::__construct(['datadir' => $datadir]); } - public function getId() { + public function getId(): string { return $this->id; } - /** - * @return \OC\Files\Cache\HomeCache - */ - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): ICache { if (!$storage) { $storage = $this; } @@ -55,13 +55,7 @@ class Home extends Local implements \OCP\Files\IHomeStorage { return $this->cache; } - /** - * get a propagator instance for the cache - * - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher - * @return \OC\Files\Cache\Propagator - */ - public function getPropagator($storage = null) { + public function getPropagator(?IStorage $storage = null): IPropagator { if (!$storage) { $storage = $this; } @@ -72,22 +66,11 @@ class Home extends Local implements \OCP\Files\IHomeStorage { } - /** - * Returns the owner of this home storage - * - * @return \OC\User\User owner of this home storage - */ public function getUser(): IUser { return $this->user; } - /** - * get the owner of a path - * - * @param string $path The path to get the owner - * @return string uid or false - */ - public function getOwner($path) { + public function getOwner(string $path): string|false { return $this->user->getUID(); } } diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index ac3696118f7..260f9218a88 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -17,6 +17,7 @@ use OCP\Files\IMimeTypeDetector; use OCP\Files\Storage\IStorage; use OCP\Files\StorageNotAvailableException; use OCP\IConfig; +use OCP\Server; use OCP\Util; use Psr\Log\LoggerInterface; @@ -40,11 +41,11 @@ class Local extends \OC\Files\Storage\Common { protected bool $caseInsensitive = false; - public function __construct($arguments) { - if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) { + public function __construct(array $parameters) { + if (!isset($parameters['datadir']) || !is_string($parameters['datadir'])) { throw new \InvalidArgumentException('No data directory set for local storage'); } - $this->datadir = str_replace('//', '/', $arguments['datadir']); + $this->datadir = str_replace('//', '/', $parameters['datadir']); // some crazy code uses a local storage on root... if ($this->datadir === '/') { $this->realDataDir = $this->datadir; @@ -56,15 +57,15 @@ class Local extends \OC\Files\Storage\Common { $this->datadir .= '/'; } $this->dataDirLength = strlen($this->realDataDir); - $this->config = \OC::$server->get(IConfig::class); - $this->mimeTypeDetector = \OC::$server->get(IMimeTypeDetector::class); + $this->config = Server::get(IConfig::class); + $this->mimeTypeDetector = Server::get(IMimeTypeDetector::class); $this->defUMask = $this->config->getSystemValue('localstorage.umask', 0022); $this->caseInsensitive = $this->config->getSystemValueBool('localstorage.case_insensitive', false); // support Write-Once-Read-Many file systems $this->unlinkOnTruncate = $this->config->getSystemValueBool('localstorage.unlink_on_truncate', false); - if (isset($arguments['isExternal']) && $arguments['isExternal'] && !$this->stat('')) { + if (isset($parameters['isExternal']) && $parameters['isExternal'] && !$this->stat('')) { // data dir not accessible or available, can happen when using an external storage of type Local // on an unmounted system mount point throw new StorageNotAvailableException('Local storage path does not exist "' . $this->getSourcePath('') . '"'); @@ -74,11 +75,11 @@ class Local extends \OC\Files\Storage\Common { public function __destruct() { } - public function getId() { + public function getId(): string { return 'local::' . $this->datadir; } - public function mkdir($path) { + public function mkdir(string $path): bool { $sourcePath = $this->getSourcePath($path); $oldMask = umask($this->defUMask); $result = @mkdir($sourcePath, 0777, true); @@ -86,7 +87,7 @@ class Local extends \OC\Files\Storage\Common { return $result; } - public function rmdir($path) { + public function rmdir(string $path): bool { if (!$this->isDeletable($path)) { return false; } @@ -106,7 +107,7 @@ class Local extends \OC\Files\Storage\Common { * @var \SplFileInfo $file */ $file = $it->current(); - clearstatcache(true, $this->getSourcePath($file)); + clearstatcache(true, $file->getRealPath()); if (in_array($file->getBasename(), ['.', '..'])) { $it->next(); continue; @@ -117,6 +118,7 @@ class Local extends \OC\Files\Storage\Common { } $it->next(); } + unset($it); // Release iterator and thereby its potential directory lock (e.g. in case of VirtualBox shared folders) clearstatcache(true, $this->getSourcePath($path)); return rmdir($this->getSourcePath($path)); } catch (\UnexpectedValueException $e) { @@ -124,11 +126,11 @@ class Local extends \OC\Files\Storage\Common { } } - public function opendir($path) { + public function opendir(string $path) { return opendir($this->getSourcePath($path)); } - public function is_dir($path) { + public function is_dir(string $path): bool { if ($this->caseInsensitive && !$this->file_exists($path)) { return false; } @@ -138,14 +140,14 @@ class Local extends \OC\Files\Storage\Common { return is_dir($this->getSourcePath($path)); } - public function is_file($path) { + public function is_file(string $path): bool { if ($this->caseInsensitive && !$this->file_exists($path)) { return false; } return is_file($this->getSourcePath($path)); } - public function stat($path) { + public function stat(string $path): array|false { $fullPath = $this->getSourcePath($path); clearstatcache(true, $fullPath); if (!file_exists($fullPath)) { @@ -163,10 +165,7 @@ class Local extends \OC\Files\Storage\Common { return $statResult; } - /** - * @inheritdoc - */ - public function getMetaData($path) { + public function getMetaData(string $path): ?array { try { $stat = $this->stat($path); } catch (ForbiddenException $e) { @@ -215,7 +214,7 @@ class Local extends \OC\Files\Storage\Common { return $data; } - public function filetype($path) { + public function filetype(string $path): string|false { $filetype = filetype($this->getSourcePath($path)); if ($filetype == 'link') { $filetype = filetype(realpath($this->getSourcePath($path))); @@ -223,7 +222,7 @@ class Local extends \OC\Files\Storage\Common { return $filetype; } - public function filesize($path): false|int|float { + public function filesize(string $path): int|float|false { if (!$this->is_file($path)) { return 0; } @@ -235,15 +234,15 @@ class Local extends \OC\Files\Storage\Common { return filesize($fullPath); } - public function isReadable($path) { + public function isReadable(string $path): bool { return is_readable($this->getSourcePath($path)); } - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { return is_writable($this->getSourcePath($path)); } - public function file_exists($path) { + public function file_exists(string $path): bool { if ($this->caseInsensitive) { $fullPath = $this->getSourcePath($path); $parentPath = dirname($fullPath); @@ -257,7 +256,7 @@ class Local extends \OC\Files\Storage\Common { } } - public function filemtime($path) { + public function filemtime(string $path): int|false { $fullPath = $this->getSourcePath($path); clearstatcache(true, $fullPath); if (!$this->file_exists($path)) { @@ -270,11 +269,11 @@ class Local extends \OC\Files\Storage\Common { return filemtime($fullPath); } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { // sets the modification time of the file to the given value. // If mtime is nil the current time is set. // note that the access time of the file always changes to the current time. - if ($this->file_exists($path) and !$this->isUpdatable($path)) { + if ($this->file_exists($path) && !$this->isUpdatable($path)) { return false; } $oldMask = umask($this->defUMask); @@ -291,11 +290,11 @@ class Local extends \OC\Files\Storage\Common { return $result; } - public function file_get_contents($path) { + public function file_get_contents(string $path): string|false { return file_get_contents($this->getSourcePath($path)); } - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { $oldMask = umask($this->defUMask); if ($this->unlinkOnTruncate) { $this->unlink($path); @@ -305,7 +304,7 @@ class Local extends \OC\Files\Storage\Common { return $result; } - public function unlink($path) { + public function unlink(string $path): bool { if ($this->is_dir($path)) { return $this->rmdir($path); } elseif ($this->is_file($path)) { @@ -315,7 +314,7 @@ class Local extends \OC\Files\Storage\Common { } } - private function checkTreeForForbiddenItems(string $path) { + private function checkTreeForForbiddenItems(string $path): void { $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path)); foreach ($iterator as $file) { /** @var \SplFileInfo $file */ @@ -325,29 +324,31 @@ class Local extends \OC\Files\Storage\Common { } } - public function rename($source, $target): bool { + public function rename(string $source, string $target): bool { $srcParent = dirname($source); $dstParent = dirname($target); if (!$this->isUpdatable($srcParent)) { - \OC::$server->get(LoggerInterface::class)->error('unable to rename, source directory is not writable : ' . $srcParent, ['app' => 'core']); + Server::get(LoggerInterface::class)->error('unable to rename, source directory is not writable : ' . $srcParent, ['app' => 'core']); return false; } if (!$this->isUpdatable($dstParent)) { - \OC::$server->get(LoggerInterface::class)->error('unable to rename, destination directory is not writable : ' . $dstParent, ['app' => 'core']); + Server::get(LoggerInterface::class)->error('unable to rename, destination directory is not writable : ' . $dstParent, ['app' => 'core']); return false; } if (!$this->file_exists($source)) { - \OC::$server->get(LoggerInterface::class)->error('unable to rename, file does not exists : ' . $source, ['app' => 'core']); + Server::get(LoggerInterface::class)->error('unable to rename, file does not exists : ' . $source, ['app' => 'core']); return false; } - if ($this->is_dir($target)) { - $this->rmdir($target); - } elseif ($this->is_file($target)) { - $this->unlink($target); + if ($this->file_exists($target)) { + if ($this->is_dir($target)) { + $this->rmdir($target); + } elseif ($this->is_file($target)) { + $this->unlink($target); + } } if ($this->is_dir($source)) { @@ -366,7 +367,7 @@ class Local extends \OC\Files\Storage\Common { return $this->copy($source, $target) && $this->unlink($source); } - public function copy($source, $target) { + public function copy(string $source, string $target): bool { if ($this->is_dir($source)) { return parent::copy($source, $target); } else { @@ -385,7 +386,7 @@ class Local extends \OC\Files\Storage\Common { } } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $sourcePath = $this->getSourcePath($path); if (!file_exists($sourcePath) && $mode === 'r') { return false; @@ -399,11 +400,11 @@ class Local extends \OC\Files\Storage\Common { return $result; } - public function hash($type, $path, $raw = false) { + public function hash(string $type, string $path, bool $raw = false): string|false { return hash_file($type, $this->getSourcePath($path), $raw); } - public function free_space($path) { + public function free_space(string $path): int|float|false { $sourcePath = $this->getSourcePath($path); // using !is_dir because $sourcePath might be a part file or // non-existing file, so we'd still want to use the parent dir @@ -419,20 +420,15 @@ class Local extends \OC\Files\Storage\Common { return Util::numericToNumber($space); } - public function search($query) { + public function search(string $query): array { return $this->searchInDir($query); } - public function getLocalFile($path) { + public function getLocalFile(string $path): string|false { return $this->getSourcePath($path); } - /** - * @param string $query - * @param string $dir - * @return array - */ - protected function searchInDir($query, $dir = '') { + protected function searchInDir(string $query, string $dir = ''): array { $files = []; $physicalDir = $this->getSourcePath($dir); foreach (scandir($physicalDir) as $item) { @@ -451,14 +447,7 @@ class Local extends \OC\Files\Storage\Common { return $files; } - /** - * check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @return bool - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { if ($this->file_exists($path)) { return $this->filemtime($path) > $time; } else { @@ -469,11 +458,9 @@ class Local extends \OC\Files\Storage\Common { /** * Get the source path (on disk) of a given path * - * @param string $path - * @return string * @throws ForbiddenException */ - public function getSourcePath($path) { + public function getSourcePath(string $path): string { if (Filesystem::isFileBlacklisted($path)) { throw new ForbiddenException('Invalid path: ' . $path, false); } @@ -501,28 +488,19 @@ class Local extends \OC\Files\Storage\Common { return $fullPath; } - \OC::$server->get(LoggerInterface::class)->error("Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ['app' => 'core']); + Server::get(LoggerInterface::class)->error("Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ['app' => 'core']); throw new ForbiddenException('Following symlinks is not allowed', false); } - /** - * {@inheritdoc} - */ - public function isLocal() { + public function isLocal(): bool { return true; } - /** - * get the ETag for a file or folder - * - * @param string $path - * @return string - */ - public function getETag($path) { + public function getETag(string $path): string|false { return $this->calculateEtag($path, $this->stat($path)); } - private function calculateEtag(string $path, array $stat): string { + private function calculateEtag(string $path, array $stat): string|false { if ($stat['mode'] & 0x4000 && !($stat['mode'] & 0x8000)) { // is_dir & not socket return parent::getETag($path); } else { @@ -548,8 +526,8 @@ class Local extends \OC\Files\Storage\Common { } } - private function canDoCrossStorageMove(IStorage $sourceStorage) { - /** @psalm-suppress UndefinedClass */ + private function canDoCrossStorageMove(IStorage $sourceStorage): bool { + /** @psalm-suppress UndefinedClass,InvalidArgument */ return $sourceStorage->instanceOfStorage(Local::class) // Don't treat ACLStorageWrapper like local storage where copy can be done directly. // Instead, use the slower recursive copying in php from Common::copyFromStorage with @@ -561,20 +539,15 @@ class Local extends \OC\Files\Storage\Common { && !$sourceStorage->instanceOfStorage(Encryption::class); } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @param bool $preserveMtime - * @return bool - */ - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool { if ($this->canDoCrossStorageMove($sourceStorage)) { - if ($sourceStorage->instanceOfStorage(Jail::class)) { + // resolve any jailed paths + while ($sourceStorage->instanceOfStorage(Jail::class)) { /** * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage */ $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath); + $sourceStorage = $sourceStorage->getUnjailedStorage(); } /** * @var \OC\Files\Storage\Local $sourceStorage @@ -586,19 +559,15 @@ class Local extends \OC\Files\Storage\Common { } } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($this->canDoCrossStorageMove($sourceStorage)) { - if ($sourceStorage->instanceOfStorage(Jail::class)) { + // resolve any jailed paths + while ($sourceStorage->instanceOfStorage(Jail::class)) { /** * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage */ $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath); + $sourceStorage = $sourceStorage->getUnjailedStorage(); } /** * @var \OC\Files\Storage\Local $sourceStorage diff --git a/lib/private/Files/Storage/LocalRootStorage.php b/lib/private/Files/Storage/LocalRootStorage.php index ae0575fe043..2e0645e092a 100644 --- a/lib/private/Files/Storage/LocalRootStorage.php +++ b/lib/private/Files/Storage/LocalRootStorage.php @@ -9,15 +9,14 @@ declare(strict_types=1); namespace OC\Files\Storage; use OC\Files\Cache\LocalRootScanner; +use OCP\Files\Cache\IScanner; +use OCP\Files\Storage\IStorage; class LocalRootStorage extends Local { - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } - if (!isset($storage->scanner)) { - $storage->scanner = new LocalRootScanner($storage); - } - return $storage->scanner; + return $storage->scanner ?? ($storage->scanner = new LocalRootScanner($storage)); } } diff --git a/lib/private/Files/Storage/LocalTempFileTrait.php b/lib/private/Files/Storage/LocalTempFileTrait.php index 26fbcc238fd..fffc3e789f3 100644 --- a/lib/private/Files/Storage/LocalTempFileTrait.php +++ b/lib/private/Files/Storage/LocalTempFileTrait.php @@ -7,6 +7,8 @@ */ namespace OC\Files\Storage; +use OCP\Files; + /** * Storage backend class for providing common filesystem operation methods * which are not storage-backend specific. @@ -29,10 +31,7 @@ trait LocalTempFileTrait { return $this->cachedFiles[$path]; } - /** - * @param string $path - */ - protected function removeCachedFile($path) { + protected function removeCachedFile(string $path): void { unset($this->cachedFiles[$path]); } @@ -48,7 +47,7 @@ trait LocalTempFileTrait { } $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($extension); $target = fopen($tmpFile, 'w'); - \OC_Helper::streamCopy($source, $target); + Files::streamCopy($source, $target); fclose($target); return $tmpFile; } diff --git a/lib/private/Files/Storage/PolyFill/CopyDirectory.php b/lib/private/Files/Storage/PolyFill/CopyDirectory.php index 4b3e367da78..2f6167ef85e 100644 --- a/lib/private/Files/Storage/PolyFill/CopyDirectory.php +++ b/lib/private/Files/Storage/PolyFill/CopyDirectory.php @@ -10,45 +10,32 @@ namespace OC\Files\Storage\PolyFill; trait CopyDirectory { /** * Check if a path is a directory - * - * @param string $path - * @return bool */ - abstract public function is_dir($path); + abstract public function is_dir(string $path): bool; /** * Check if a file or folder exists - * - * @param string $path - * @return bool */ - abstract public function file_exists($path); + abstract public function file_exists(string $path): bool; /** * Delete a file or folder - * - * @param string $path - * @return bool */ - abstract public function unlink($path); + abstract public function unlink(string $path): bool; /** * Open a directory handle for a folder * - * @param string $path - * @return resource | bool + * @return resource|false */ - abstract public function opendir($path); + abstract public function opendir(string $path); /** * Create a new folder - * - * @param string $path - * @return bool */ - abstract public function mkdir($path); + abstract public function mkdir(string $path): bool; - public function copy($source, $target) { + public function copy(string $source, string $target): bool { if ($this->is_dir($source)) { if ($this->file_exists($target)) { $this->unlink($target); @@ -62,15 +49,11 @@ trait CopyDirectory { /** * For adapters that don't support copying folders natively - * - * @param $source - * @param $target - * @return bool */ - protected function copyRecursive($source, $target) { + protected function copyRecursive(string $source, string $target): bool { $dh = $this->opendir($source); $result = true; - while ($file = readdir($dh)) { + while (($file = readdir($dh)) !== false) { if (!\OC\Files\Filesystem::isIgnoredDir($file)) { if ($this->is_dir($source . '/' . $file)) { $this->mkdir($target . '/' . $file); diff --git a/lib/private/Files/Storage/Storage.php b/lib/private/Files/Storage/Storage.php index 44b1a52cfc0..aa17c12b309 100644 --- a/lib/private/Files/Storage/Storage.php +++ b/lib/private/Files/Storage/Storage.php @@ -5,109 +5,40 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\Files\Storage; -use OCP\Lock\ILockingProvider; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\IPropagator; +use OCP\Files\Cache\IScanner; +use OCP\Files\Cache\IUpdater; +use OCP\Files\Cache\IWatcher; +use OCP\Files\Storage\ILockingStorage; +use OCP\Files\Storage\IStorage; /** * Provide a common interface to all different storage options * * All paths passed to the storage are relative to the storage and should NOT have a leading slash. */ -interface Storage extends \OCP\Files\Storage { - /** - * get a cache instance for the storage - * - * @param string $path - * @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache - * @return \OC\Files\Cache\Cache - */ - public function getCache($path = '', $storage = null); +interface Storage extends IStorage, ILockingStorage { + public function getCache(string $path = '', ?IStorage $storage = null): ICache; - /** - * get a scanner instance for the storage - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner - * @return \OC\Files\Cache\Scanner - */ - public function getScanner($path = '', $storage = null); + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner; + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher; - /** - * get the user id of the owner of a file or folder - * - * @param string $path - * @return string - */ - public function getOwner($path); + public function getPropagator(?IStorage $storage = null): IPropagator; - /** - * get a watcher instance for the cache - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher - * @return \OC\Files\Cache\Watcher - */ - public function getWatcher($path = '', $storage = null); + public function getUpdater(?IStorage $storage = null): IUpdater; - /** - * get a propagator instance for the cache - * - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher - * @return \OC\Files\Cache\Propagator - */ - public function getPropagator($storage = null); + public function getStorageCache(): \OC\Files\Cache\Storage; - /** - * get a updater instance for the cache - * - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher - * @return \OC\Files\Cache\Updater - */ - public function getUpdater($storage = null); - - /** - * @return \OC\Files\Cache\Storage - */ - public function getStorageCache(); - - /** - * @param string $path - * @return array|null - */ - public function getMetaData($path); - - /** - * @param string $path The path of the file to acquire the lock for - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function acquireLock($path, $type, ILockingProvider $provider); - - /** - * @param string $path The path of the file to release the lock for - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function releaseLock($path, $type, ILockingProvider $provider); - - /** - * @param string $path The path of the file to change the lock for - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function changeLock($path, $type, ILockingProvider $provider); + public function getMetaData(string $path): ?array; /** * Get the contents of a directory with metadata * - * @param string $directory - * @return \Traversable an iterator, containing file metadata - * * The metadata array will contain the following fields * * - name @@ -118,5 +49,5 @@ interface Storage extends \OCP\Files\Storage { * - storage_mtime * - permissions */ - public function getDirectoryContent($directory): \Traversable; + public function getDirectoryContent(string $directory): \Traversable; } diff --git a/lib/private/Files/Storage/StorageFactory.php b/lib/private/Files/Storage/StorageFactory.php index 18fca1d28e3..603df7fe007 100644 --- a/lib/private/Files/Storage/StorageFactory.php +++ b/lib/private/Files/Storage/StorageFactory.php @@ -8,7 +8,10 @@ namespace OC\Files\Storage; use OCP\Files\Mount\IMountPoint; +use OCP\Files\Storage\IConstructableStorage; +use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IStorageFactory; +use Psr\Log\LoggerInterface; class StorageFactory implements IStorageFactory { /** @@ -16,19 +19,7 @@ class StorageFactory implements IStorageFactory { */ private $storageWrappers = []; - /** - * allow modifier storage behaviour by adding wrappers around storages - * - * $callback should be a function of type (string $mountPoint, Storage $storage) => Storage - * - * @param string $wrapperName name of the wrapper - * @param callable $callback callback - * @param int $priority wrappers with the lower priority are applied last (meaning they get called first) - * @param \OCP\Files\Mount\IMountPoint[] $existingMounts existing mount points to apply the wrapper to - * @return bool true if the wrapper was added, false if there was already a wrapper with this - * name registered - */ - public function addStorageWrapper($wrapperName, $callback, $priority = 50, $existingMounts = []) { + public function addStorageWrapper(string $wrapperName, callable $callback, int $priority = 50, array $existingMounts = []): bool { if (isset($this->storageWrappers[$wrapperName])) { return false; } @@ -46,31 +37,23 @@ class StorageFactory implements IStorageFactory { * Remove a storage wrapper by name. * Note: internal method only to be used for cleanup * - * @param string $wrapperName name of the wrapper * @internal */ - public function removeStorageWrapper($wrapperName) { + public function removeStorageWrapper(string $wrapperName): void { unset($this->storageWrappers[$wrapperName]); } /** * Create an instance of a storage and apply the registered storage wrappers - * - * @param \OCP\Files\Mount\IMountPoint $mountPoint - * @param string $class - * @param array $arguments - * @return \OCP\Files\Storage */ - public function getInstance(IMountPoint $mountPoint, $class, $arguments) { + public function getInstance(IMountPoint $mountPoint, string $class, array $arguments): IStorage { + if (!is_a($class, IConstructableStorage::class, true)) { + \OCP\Server::get(LoggerInterface::class)->warning('Building a storage not implementing IConstructableStorage is deprecated since 31.0.0', ['class' => $class]); + } return $this->wrap($mountPoint, new $class($arguments)); } - /** - * @param \OCP\Files\Mount\IMountPoint $mountPoint - * @param \OCP\Files\Storage $storage - * @return \OCP\Files\Storage - */ - public function wrap(IMountPoint $mountPoint, $storage) { + public function wrap(IMountPoint $mountPoint, IStorage $storage): IStorage { $wrappers = array_values($this->storageWrappers); usort($wrappers, function ($a, $b) { return $b['priority'] - $a['priority']; @@ -81,7 +64,7 @@ class StorageFactory implements IStorageFactory { }, $wrappers); foreach ($wrappers as $wrapper) { $storage = $wrapper($mountPoint->getMountPoint(), $storage, $mountPoint); - if (!($storage instanceof \OCP\Files\Storage)) { + if (!($storage instanceof IStorage)) { throw new \Exception('Invalid result from storage wrapper'); } } diff --git a/lib/private/Files/Storage/Temporary.php b/lib/private/Files/Storage/Temporary.php index 95ae84cb7d9..ecf8a1315a9 100644 --- a/lib/private/Files/Storage/Temporary.php +++ b/lib/private/Files/Storage/Temporary.php @@ -7,16 +7,20 @@ */ namespace OC\Files\Storage; +use OCP\Files; +use OCP\ITempManager; +use OCP\Server; + /** * local storage backend in temporary folder for testing purpose */ class Temporary extends Local { - public function __construct($arguments = null) { - parent::__construct(['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]); + public function __construct(array $parameters = []) { + parent::__construct(['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]); } - public function cleanUp() { - \OC_Helper::rmdirr($this->datadir); + public function cleanUp(): void { + Files::rmdirr($this->datadir); } public function __destruct() { @@ -24,7 +28,7 @@ class Temporary extends Local { $this->cleanUp(); } - public function getDataDir() { + public function getDataDir(): array|string { return $this->datadir; } } diff --git a/lib/private/Files/Storage/Wrapper/Availability.php b/lib/private/Files/Storage/Wrapper/Availability.php index 149cbbaa33b..32c51a1b25e 100644 --- a/lib/private/Files/Storage/Wrapper/Availability.php +++ b/lib/private/Files/Storage/Wrapper/Availability.php @@ -23,12 +23,12 @@ class Availability extends Wrapper { /** @var IConfig */ protected $config; - public function __construct($parameters) { - $this->config = $parameters['config'] ?? \OC::$server->getConfig(); + public function __construct(array $parameters) { + $this->config = $parameters['config'] ?? \OCP\Server::get(IConfig::class); parent::__construct($parameters); } - public static function shouldRecheck($availability) { + public static function shouldRecheck($availability): bool { if (!$availability['available']) { // trigger a recheck if TTL reached if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) { @@ -40,10 +40,8 @@ class Availability extends Wrapper { /** * Only called if availability === false - * - * @return bool */ - private function updateAvailability() { + private function updateAvailability(): bool { // reset availability to false so that multiple requests don't recheck concurrently $this->setAvailability(false); try { @@ -55,10 +53,7 @@ class Availability extends Wrapper { return $result; } - /** - * @return bool - */ - private function isAvailable() { + private function isAvailable(): bool { $availability = $this->getAvailability(); if (self::shouldRecheck($availability)) { return $this->updateAvailability(); @@ -69,297 +64,137 @@ class Availability extends Wrapper { /** * @throws StorageNotAvailableException */ - private function checkAvailability() { + private function checkAvailability(): void { if (!$this->isAvailable()) { throw new StorageNotAvailableException(); } } - /** {@inheritdoc} */ - public function mkdir($path) { + /** + * Handles availability checks and delegates method calls dynamically + */ + private function handleAvailability(string $method, mixed ...$args): mixed { $this->checkAvailability(); try { - return parent::mkdir($path); + return call_user_func_array([parent::class, $method], $args); } catch (StorageNotAvailableException $e) { $this->setUnavailable($e); + return false; } } - /** {@inheritdoc} */ - public function rmdir($path) { - $this->checkAvailability(); - try { - return parent::rmdir($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function mkdir(string $path): bool { + return $this->handleAvailability('mkdir', $path); } - /** {@inheritdoc} */ - public function opendir($path) { - $this->checkAvailability(); - try { - return parent::opendir($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function rmdir(string $path): bool { + return $this->handleAvailability('rmdir', $path); } - /** {@inheritdoc} */ - public function is_dir($path) { - $this->checkAvailability(); - try { - return parent::is_dir($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function opendir(string $path) { + return $this->handleAvailability('opendir', $path); } - /** {@inheritdoc} */ - public function is_file($path) { - $this->checkAvailability(); - try { - return parent::is_file($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function is_dir(string $path): bool { + return $this->handleAvailability('is_dir', $path); } - /** {@inheritdoc} */ - public function stat($path) { - $this->checkAvailability(); - try { - return parent::stat($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function is_file(string $path): bool { + return $this->handleAvailability('is_file', $path); } - /** {@inheritdoc} */ - public function filetype($path) { - $this->checkAvailability(); - try { - return parent::filetype($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function stat(string $path): array|false { + return $this->handleAvailability('stat', $path); } - /** {@inheritdoc} */ - public function filesize($path): false|int|float { - $this->checkAvailability(); - try { - return parent::filesize($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function filetype(string $path): string|false { + return $this->handleAvailability('filetype', $path); } - /** {@inheritdoc} */ - public function isCreatable($path) { - $this->checkAvailability(); - try { - return parent::isCreatable($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function filesize(string $path): int|float|false { + return $this->handleAvailability('filesize', $path); } - /** {@inheritdoc} */ - public function isReadable($path) { - $this->checkAvailability(); - try { - return parent::isReadable($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function isCreatable(string $path): bool { + return $this->handleAvailability('isCreatable', $path); } - /** {@inheritdoc} */ - public function isUpdatable($path) { - $this->checkAvailability(); - try { - return parent::isUpdatable($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function isReadable(string $path): bool { + return $this->handleAvailability('isReadable', $path); } - /** {@inheritdoc} */ - public function isDeletable($path) { - $this->checkAvailability(); - try { - return parent::isDeletable($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function isUpdatable(string $path): bool { + return $this->handleAvailability('isUpdatable', $path); } - /** {@inheritdoc} */ - public function isSharable($path) { - $this->checkAvailability(); - try { - return parent::isSharable($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function isDeletable(string $path): bool { + return $this->handleAvailability('isDeletable', $path); } - /** {@inheritdoc} */ - public function getPermissions($path) { - $this->checkAvailability(); - try { - return parent::getPermissions($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function isSharable(string $path): bool { + return $this->handleAvailability('isSharable', $path); } - /** {@inheritdoc} */ - public function file_exists($path) { - if ($path === '') { - return true; - } - $this->checkAvailability(); - try { - return parent::file_exists($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function getPermissions(string $path): int { + return $this->handleAvailability('getPermissions', $path); } - /** {@inheritdoc} */ - public function filemtime($path) { - $this->checkAvailability(); - try { - return parent::filemtime($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); + public function file_exists(string $path): bool { + if ($path === '') { + return true; } + return $this->handleAvailability('file_exists', $path); } - /** {@inheritdoc} */ - public function file_get_contents($path) { - $this->checkAvailability(); - try { - return parent::file_get_contents($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function filemtime(string $path): int|false { + return $this->handleAvailability('filemtime', $path); } - /** {@inheritdoc} */ - public function file_put_contents($path, $data) { - $this->checkAvailability(); - try { - return parent::file_put_contents($path, $data); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function file_get_contents(string $path): string|false { + return $this->handleAvailability('file_get_contents', $path); } - /** {@inheritdoc} */ - public function unlink($path) { - $this->checkAvailability(); - try { - return parent::unlink($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function file_put_contents(string $path, mixed $data): int|float|false { + return $this->handleAvailability('file_put_contents', $path, $data); } - /** {@inheritdoc} */ - public function rename($source, $target) { - $this->checkAvailability(); - try { - return parent::rename($source, $target); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function unlink(string $path): bool { + return $this->handleAvailability('unlink', $path); } - /** {@inheritdoc} */ - public function copy($source, $target) { - $this->checkAvailability(); - try { - return parent::copy($source, $target); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function rename(string $source, string $target): bool { + return $this->handleAvailability('rename', $source, $target); } - /** {@inheritdoc} */ - public function fopen($path, $mode) { - $this->checkAvailability(); - try { - return parent::fopen($path, $mode); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function copy(string $source, string $target): bool { + return $this->handleAvailability('copy', $source, $target); } - /** {@inheritdoc} */ - public function getMimeType($path) { - $this->checkAvailability(); - try { - return parent::getMimeType($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function fopen(string $path, string $mode) { + return $this->handleAvailability('fopen', $path, $mode); } - /** {@inheritdoc} */ - public function hash($type, $path, $raw = false) { - $this->checkAvailability(); - try { - return parent::hash($type, $path, $raw); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function getMimeType(string $path): string|false { + return $this->handleAvailability('getMimeType', $path); } - /** {@inheritdoc} */ - public function free_space($path) { - $this->checkAvailability(); - try { - return parent::free_space($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function hash(string $type, string $path, bool $raw = false): string|false { + return $this->handleAvailability('hash', $type, $path, $raw); } - /** {@inheritdoc} */ - public function search($query) { - $this->checkAvailability(); - try { - return parent::search($query); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function free_space(string $path): int|float|false { + return $this->handleAvailability('free_space', $path); } - /** {@inheritdoc} */ - public function touch($path, $mtime = null) { - $this->checkAvailability(); - try { - return parent::touch($path, $mtime); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function touch(string $path, ?int $mtime = null): bool { + return $this->handleAvailability('touch', $path, $mtime); } - /** {@inheritdoc} */ - public function getLocalFile($path) { - $this->checkAvailability(); - try { - return parent::getLocalFile($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function getLocalFile(string $path): string|false { + return $this->handleAvailability('getLocalFile', $path); } - /** {@inheritdoc} */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { if (!$this->isAvailable()) { return false; } @@ -372,62 +207,38 @@ class Availability extends Wrapper { } } - /** {@inheritdoc} */ - public function getOwner($path) { + public function getOwner(string $path): string|false { try { return parent::getOwner($path); } catch (StorageNotAvailableException $e) { $this->setUnavailable($e); + return false; } } - /** {@inheritdoc} */ - public function getETag($path) { - $this->checkAvailability(); - try { - return parent::getETag($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function getETag(string $path): string|false { + return $this->handleAvailability('getETag', $path); } - /** {@inheritdoc} */ - public function getDirectDownload($path) { - $this->checkAvailability(); - try { - return parent::getDirectDownload($path); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function getDirectDownload(string $path): array|false { + return $this->handleAvailability('getDirectDownload', $path); } - /** {@inheritdoc} */ - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { - $this->checkAvailability(); - try { - return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath); } - /** {@inheritdoc} */ - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { - $this->checkAvailability(); - try { - return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); - } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); - } + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + return $this->handleAvailability('moveFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath); } - /** {@inheritdoc} */ - public function getMetaData($path) { + public function getMetaData(string $path): ?array { $this->checkAvailability(); try { return parent::getMetaData($path); } catch (StorageNotAvailableException $e) { $this->setUnavailable($e); + return null; } } @@ -454,12 +265,13 @@ class Availability extends Wrapper { - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { $this->checkAvailability(); try { return parent::getDirectoryContent($directory); } catch (StorageNotAvailableException $e) { $this->setUnavailable($e); + return new \EmptyIterator(); } } } diff --git a/lib/private/Files/Storage/Wrapper/Encoding.php b/lib/private/Files/Storage/Wrapper/Encoding.php index 11f21eb828e..92e20cfb3df 100644 --- a/lib/private/Files/Storage/Wrapper/Encoding.php +++ b/lib/private/Files/Storage/Wrapper/Encoding.php @@ -9,6 +9,7 @@ namespace OC\Files\Storage\Wrapper; use OC\Files\Filesystem; use OCP\Cache\CappedMemoryCache; +use OCP\Files\Cache\IScanner; use OCP\Files\Storage\IStorage; use OCP\ICache; @@ -27,19 +28,15 @@ class Encoding extends Wrapper { /** * @param array $parameters */ - public function __construct($parameters) { + public function __construct(array $parameters) { $this->storage = $parameters['storage']; $this->namesCache = new CappedMemoryCache(); } /** * Returns whether the given string is only made of ASCII characters - * - * @param string $str string - * - * @return bool true if the string is all ASCII, false otherwise */ - private function isAscii($str) { + private function isAscii(string $str): bool { return !preg_match('/[\\x80-\\xff]+/', $str); } @@ -48,11 +45,9 @@ class Encoding extends Wrapper { * each form for each path section and returns the correct form. * If no existing path found, returns the path as it was given. * - * @param string $fullPath path to check - * * @return string original or converted path */ - private function findPathToUse($fullPath) { + private function findPathToUse(string $fullPath): string { $cachedPath = $this->namesCache[$fullPath]; if ($cachedPath !== null) { return $cachedPath; @@ -76,12 +71,11 @@ class Encoding extends Wrapper { * Checks whether the last path section of the given path exists in NFC or NFD form * and returns the correct form. If no existing path found, returns null. * - * @param string $basePath base path to check * @param string $lastSection last section of the path to check for NFD/NFC variations * * @return string|null original or converted path, or null if none of the forms was found */ - private function findPathToUseLastSection($basePath, $lastSection) { + private function findPathToUseLastSection(string $basePath, string $lastSection): ?string { $fullPath = $basePath . $lastSection; if ($lastSection === '' || $this->isAscii($lastSection) || $this->storage->file_exists($fullPath)) { $this->namesCache[$fullPath] = $fullPath; @@ -105,13 +99,7 @@ class Encoding extends Wrapper { return null; } - /** - * see https://www.php.net/manual/en/function.mkdir.php - * - * @param string $path - * @return bool - */ - public function mkdir($path) { + public function mkdir(string $path): bool { // note: no conversion here, method should not be called with non-NFC names! $result = $this->storage->mkdir($path); if ($result) { @@ -120,13 +108,7 @@ class Encoding extends Wrapper { return $result; } - /** - * see https://www.php.net/manual/en/function.rmdir.php - * - * @param string $path - * @return bool - */ - public function rmdir($path) { + public function rmdir(string $path): bool { $result = $this->storage->rmdir($this->findPathToUse($path)); if ($result) { unset($this->namesCache[$path]); @@ -134,175 +116,72 @@ class Encoding extends Wrapper { return $result; } - /** - * see https://www.php.net/manual/en/function.opendir.php - * - * @param string $path - * @return resource|false - */ - public function opendir($path) { + public function opendir(string $path) { $handle = $this->storage->opendir($this->findPathToUse($path)); return EncodingDirectoryWrapper::wrap($handle); } - /** - * see https://www.php.net/manual/en/function.is_dir.php - * - * @param string $path - * @return bool - */ - public function is_dir($path) { + public function is_dir(string $path): bool { return $this->storage->is_dir($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.is_file.php - * - * @param string $path - * @return bool - */ - public function is_file($path) { + public function is_file(string $path): bool { return $this->storage->is_file($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.stat.php - * only the following keys are required in the result: size and mtime - * - * @param string $path - * @return array|bool - */ - public function stat($path) { + public function stat(string $path): array|false { return $this->storage->stat($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.filetype.php - * - * @param string $path - * @return string|bool - */ - public function filetype($path) { + public function filetype(string $path): string|false { return $this->storage->filetype($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.filesize.php - * The result for filesize when called on a folder is required to be 0 - */ - public function filesize($path): false|int|float { + public function filesize(string $path): int|float|false { return $this->storage->filesize($this->findPathToUse($path)); } - /** - * check if a file can be created in $path - * - * @param string $path - * @return bool - */ - public function isCreatable($path) { + public function isCreatable(string $path): bool { return $this->storage->isCreatable($this->findPathToUse($path)); } - /** - * check if a file can be read - * - * @param string $path - * @return bool - */ - public function isReadable($path) { + public function isReadable(string $path): bool { return $this->storage->isReadable($this->findPathToUse($path)); } - /** - * check if a file can be written to - * - * @param string $path - * @return bool - */ - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { return $this->storage->isUpdatable($this->findPathToUse($path)); } - /** - * check if a file can be deleted - * - * @param string $path - * @return bool - */ - public function isDeletable($path) { + public function isDeletable(string $path): bool { return $this->storage->isDeletable($this->findPathToUse($path)); } - /** - * check if a file can be shared - * - * @param string $path - * @return bool - */ - public function isSharable($path) { + public function isSharable(string $path): bool { return $this->storage->isSharable($this->findPathToUse($path)); } - /** - * get the full permissions of a path. - * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php - * - * @param string $path - * @return int - */ - public function getPermissions($path) { + public function getPermissions(string $path): int { return $this->storage->getPermissions($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.file_exists.php - * - * @param string $path - * @return bool - */ - public function file_exists($path) { + public function file_exists(string $path): bool { return $this->storage->file_exists($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.filemtime.php - * - * @param string $path - * @return int|bool - */ - public function filemtime($path) { + public function filemtime(string $path): int|false { return $this->storage->filemtime($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.file_get_contents.php - * - * @param string $path - * @return string|false - */ - public function file_get_contents($path) { + public function file_get_contents(string $path): string|false { return $this->storage->file_get_contents($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.file_put_contents.php - * - * @param string $path - * @param mixed $data - * @return int|float|false - */ - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { return $this->storage->file_put_contents($this->findPathToUse($path), $data); } - /** - * see https://www.php.net/manual/en/function.unlink.php - * - * @param string $path - * @return bool - */ - public function unlink($path) { + public function unlink(string $path): bool { $result = $this->storage->unlink($this->findPathToUse($path)); if ($result) { unset($this->namesCache[$path]); @@ -310,37 +189,16 @@ class Encoding extends Wrapper { return $result; } - /** - * see https://www.php.net/manual/en/function.rename.php - * - * @param string $source - * @param string $target - * @return bool - */ - public function rename($source, $target) { + public function rename(string $source, string $target): bool { // second name always NFC return $this->storage->rename($this->findPathToUse($source), $this->findPathToUse($target)); } - /** - * see https://www.php.net/manual/en/function.copy.php - * - * @param string $source - * @param string $target - * @return bool - */ - public function copy($source, $target) { + public function copy(string $source, string $target): bool { return $this->storage->copy($this->findPathToUse($source), $this->findPathToUse($target)); } - /** - * see https://www.php.net/manual/en/function.fopen.php - * - * @param string $path - * @param string $mode - * @return resource|bool - */ - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $result = $this->storage->fopen($this->findPathToUse($path), $mode); if ($result && $mode !== 'r' && $mode !== 'rb') { unset($this->namesCache[$path]); @@ -348,131 +206,49 @@ class Encoding extends Wrapper { return $result; } - /** - * get the mimetype for a file or folder - * The mimetype for a folder is required to be "httpd/unix-directory" - * - * @param string $path - * @return string|bool - */ - public function getMimeType($path) { + public function getMimeType(string $path): string|false { return $this->storage->getMimeType($this->findPathToUse($path)); } - /** - * see https://www.php.net/manual/en/function.hash.php - * - * @param string $type - * @param string $path - * @param bool $raw - * @return string|bool - */ - public function hash($type, $path, $raw = false) { + public function hash(string $type, string $path, bool $raw = false): string|false { return $this->storage->hash($type, $this->findPathToUse($path), $raw); } - /** - * see https://www.php.net/manual/en/function.free_space.php - * - * @param string $path - * @return int|float|bool - */ - public function free_space($path) { + public function free_space(string $path): int|float|false { return $this->storage->free_space($this->findPathToUse($path)); } - /** - * search for occurrences of $query in file names - * - * @param string $query - * @return array|bool - */ - public function search($query) { - return $this->storage->search($query); - } - - /** - * see https://www.php.net/manual/en/function.touch.php - * If the backend does not support the operation, false should be returned - * - * @param string $path - * @param int $mtime - * @return bool - */ - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { return $this->storage->touch($this->findPathToUse($path), $mtime); } - /** - * get the path to a local version of the file. - * The local version of the file can be temporary and doesn't have to be persistent across requests - * - * @param string $path - * @return string|false - */ - public function getLocalFile($path) { + public function getLocalFile(string $path): string|false { return $this->storage->getLocalFile($this->findPathToUse($path)); } - /** - * check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @return bool - * - * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed. - * returning true for other changes in the folder is optional - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { return $this->storage->hasUpdated($this->findPathToUse($path), $time); } - /** - * get a cache instance for the storage - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the cache - * @return \OC\Files\Cache\Cache - */ - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\ICache { if (!$storage) { $storage = $this; } return $this->storage->getCache($this->findPathToUse($path), $storage); } - /** - * get a scanner instance for the storage - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner - * @return \OC\Files\Cache\Scanner - */ - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } return $this->storage->getScanner($this->findPathToUse($path), $storage); } - /** - * get the ETag for a file or folder - * - * @param string $path - * @return string|false - */ - public function getETag($path) { + public function getETag(string $path): string|false { return $this->storage->getETag($this->findPathToUse($path)); } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($sourceStorage === $this) { return $this->copy($sourceInternalPath, $this->findPathToUse($targetInternalPath)); } @@ -484,13 +260,7 @@ class Encoding extends Wrapper { return $result; } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($sourceStorage === $this) { $result = $this->rename($sourceInternalPath, $this->findPathToUse($targetInternalPath)); if ($result) { @@ -508,13 +278,15 @@ class Encoding extends Wrapper { return $result; } - public function getMetaData($path) { + public function getMetaData(string $path): ?array { $entry = $this->storage->getMetaData($this->findPathToUse($path)); - $entry['name'] = trim(Filesystem::normalizePath($entry['name']), '/'); + if ($entry !== null) { + $entry['name'] = trim(Filesystem::normalizePath($entry['name']), '/'); + } return $entry; } - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { $entries = $this->storage->getDirectoryContent($this->findPathToUse($directory)); foreach ($entries as $entry) { $entry['name'] = trim(Filesystem::normalizePath($entry['name']), '/'); diff --git a/lib/private/Files/Storage/Wrapper/EncodingDirectoryWrapper.php b/lib/private/Files/Storage/Wrapper/EncodingDirectoryWrapper.php index 26b32d9aa62..0a90b49f0f1 100644 --- a/lib/private/Files/Storage/Wrapper/EncodingDirectoryWrapper.php +++ b/lib/private/Files/Storage/Wrapper/EncodingDirectoryWrapper.php @@ -13,11 +13,7 @@ use OC\Files\Filesystem; * Normalize file names while reading directory entries */ class EncodingDirectoryWrapper extends DirectoryWrapper { - /** - * @psalm-suppress ImplementedReturnTypeMismatch Until return type is fixed upstream - * @return string|false - */ - public function dir_readdir() { + public function dir_readdir(): string|false { $file = readdir($this->source); if ($file !== false && $file !== '.' && $file !== '..') { $file = trim(Filesystem::normalizePath($file), '/'); @@ -28,7 +24,6 @@ class EncodingDirectoryWrapper extends DirectoryWrapper { /** * @param resource $source - * @param callable $filter * @return resource|false */ public static function wrap($source) { diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index 5c7862e8226..58bd4dfddcf 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -8,20 +8,22 @@ namespace OC\Files\Storage\Wrapper; use OC\Encryption\Exceptions\ModuleDoesNotExistsException; -use OC\Encryption\Update; use OC\Encryption\Util; use OC\Files\Cache\CacheEntry; use OC\Files\Filesystem; use OC\Files\Mount\Manager; use OC\Files\ObjectStore\ObjectStoreStorage; +use OC\Files\Storage\Common; use OC\Files\Storage\LocalTempFileTrait; use OC\Memcache\ArrayCache; use OCP\Cache\CappedMemoryCache; -use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\Encryption\Exceptions\InvalidHeaderException; use OCP\Encryption\IFile; use OCP\Encryption\IManager; use OCP\Encryption\Keys\IStorage; +use OCP\Files; use OCP\Files\Cache\ICacheEntry; +use OCP\Files\GenericFileException; use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage; use Psr\Log\LoggerInterface; @@ -29,90 +31,42 @@ use Psr\Log\LoggerInterface; class Encryption extends Wrapper { use LocalTempFileTrait; - /** @var string */ - private $mountPoint; - - /** @var \OC\Encryption\Util */ - private $util; - - /** @var \OCP\Encryption\IManager */ - private $encryptionManager; - - private LoggerInterface $logger; - - /** @var string */ - private $uid; - - /** @var array */ - protected $unencryptedSize; - - /** @var \OCP\Encryption\IFile */ - private $fileHelper; - - /** @var IMountPoint */ - private $mount; - - /** @var IStorage */ - private $keyStorage; - - /** @var Update */ - private $update; - - /** @var Manager */ - private $mountManager; - - /** @var array remember for which path we execute the repair step to avoid recursions */ - private $fixUnencryptedSizeOf = []; - - /** @var ArrayCache */ - private $arrayCache; - + private string $mountPoint; + protected array $unencryptedSize = []; + private IMountPoint $mount; + /** for which path we execute the repair step to avoid recursions */ + private array $fixUnencryptedSizeOf = []; /** @var CappedMemoryCache<bool> */ private CappedMemoryCache $encryptedPaths; - - private $enabled = true; + private bool $enabled = true; /** * @param array $parameters */ public function __construct( - $parameters, - ?IManager $encryptionManager = null, - ?Util $util = null, - ?LoggerInterface $logger = null, - ?IFile $fileHelper = null, - $uid = null, - ?IStorage $keyStorage = null, - ?Update $update = null, - ?Manager $mountManager = null, - ?ArrayCache $arrayCache = null + array $parameters, + private IManager $encryptionManager, + private Util $util, + private LoggerInterface $logger, + private IFile $fileHelper, + private ?string $uid, + private IStorage $keyStorage, + private Manager $mountManager, + private ArrayCache $arrayCache, ) { $this->mountPoint = $parameters['mountPoint']; $this->mount = $parameters['mount']; - $this->encryptionManager = $encryptionManager; - $this->util = $util; - $this->logger = $logger; - $this->uid = $uid; - $this->fileHelper = $fileHelper; - $this->keyStorage = $keyStorage; - $this->unencryptedSize = []; - $this->update = $update; - $this->mountManager = $mountManager; - $this->arrayCache = $arrayCache; $this->encryptedPaths = new CappedMemoryCache(); parent::__construct($parameters); } - /** - * see https://www.php.net/manual/en/function.filesize.php - * The result for filesize when called on a folder is required to be 0 - */ - public function filesize($path): false|int|float { + public function filesize(string $path): int|float|false { $fullPath = $this->getFullPath($path); $info = $this->getCache()->get($path); if ($info === false) { - return false; + /* Pass call to wrapped storage, it may be a special file like a part file */ + return $this->storage->filesize($path); } if (isset($this->unencryptedSize[$fullPath])) { $size = $this->unencryptedSize[$fullPath]; @@ -150,11 +104,6 @@ class Encryption extends Wrapper { return $this->storage->filesize($path); } - /** - * @param string $path - * @param array $data - * @return array - */ private function modifyMetaData(string $path, array $data): array { $fullPath = $this->getFullPath($path); $info = $this->getCache()->get($path); @@ -178,7 +127,7 @@ class Encryption extends Wrapper { return $data; } - public function getMetaData($path) { + public function getMetaData(string $path): ?array { $data = $this->storage->getMetaData($path); if (is_null($data)) { return null; @@ -186,24 +135,18 @@ class Encryption extends Wrapper { return $this->modifyMetaData($path, $data); } - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { $parent = rtrim($directory, '/'); foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) { yield $this->modifyMetaData($parent . '/' . $data['name'], $data); } } - /** - * see https://www.php.net/manual/en/function.file_get_contents.php - * - * @param string $path - * @return string|false - */ - public function file_get_contents($path) { + public function file_get_contents(string $path): string|false { $encryptionModule = $this->getEncryptionModule($path); if ($encryptionModule) { - $handle = $this->fopen($path, "r"); + $handle = $this->fopen($path, 'r'); if (!$handle) { return false; } @@ -214,14 +157,7 @@ class Encryption extends Wrapper { return $this->storage->file_get_contents($path); } - /** - * see https://www.php.net/manual/en/function.file_put_contents.php - * - * @param string $path - * @param mixed $data - * @return int|false - */ - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { // file put content will always be translated to a stream write $handle = $this->fopen($path, 'w'); if (is_resource($handle)) { @@ -233,13 +169,7 @@ class Encryption extends Wrapper { return false; } - /** - * see https://www.php.net/manual/en/function.unlink.php - * - * @param string $path - * @return bool - */ - public function unlink($path) { + public function unlink(string $path): bool { $fullPath = $this->getFullPath($path); if ($this->util->isExcluded($fullPath)) { return $this->storage->unlink($path); @@ -253,21 +183,14 @@ class Encryption extends Wrapper { return $this->storage->unlink($path); } - /** - * see https://www.php.net/manual/en/function.rename.php - * - * @param string $source - * @param string $target - * @return bool - */ - public function rename($source, $target) { + public function rename(string $source, string $target): bool { $result = $this->storage->rename($source, $target); - if ($result && + if ($result // versions always use the keys from the original file, so we can skip // this step for versions - $this->isVersion($target) === false && - $this->encryptionManager->isEnabled()) { + && $this->isVersion($target) === false + && $this->encryptionManager->isEnabled()) { $sourcePath = $this->getFullPath($source); if (!$this->util->isExcluded($sourcePath)) { $targetPath = $this->getFullPath($target); @@ -285,18 +208,12 @@ class Encryption extends Wrapper { return $result; } - /** - * see https://www.php.net/manual/en/function.rmdir.php - * - * @param string $path - * @return bool - */ - public function rmdir($path) { + public function rmdir(string $path): bool { $result = $this->storage->rmdir($path); $fullPath = $this->getFullPath($path); - if ($result && - $this->util->isExcluded($fullPath) === false && - $this->encryptionManager->isEnabled() + if ($result + && $this->util->isExcluded($fullPath) === false + && $this->encryptionManager->isEnabled() ) { $this->keyStorage->deleteAllFileKeys($fullPath); } @@ -304,20 +221,14 @@ class Encryption extends Wrapper { return $result; } - /** - * check if a file can be read - * - * @param string $path - * @return bool - */ - public function isReadable($path) { + public function isReadable(string $path): bool { $isReadable = true; $metaData = $this->getMetaData($path); if ( - !$this->is_dir($path) && - isset($metaData['encrypted']) && - $metaData['encrypted'] === true + !$this->is_dir($path) + && isset($metaData['encrypted']) + && $metaData['encrypted'] === true ) { $fullPath = $this->getFullPath($path); $module = $this->getEncryptionModule($path); @@ -327,13 +238,7 @@ class Encryption extends Wrapper { return $this->storage->isReadable($path) && $isReadable; } - /** - * see https://www.php.net/manual/en/function.copy.php - * - * @param string $source - * @param string $target - */ - public function copy($source, $target): bool { + public function copy(string $source, string $target): bool { $sourcePath = $this->getFullPath($source); if ($this->util->isExcluded($sourcePath)) { @@ -346,16 +251,7 @@ class Encryption extends Wrapper { return $this->copyFromStorage($this, $source, $target); } - /** - * see https://www.php.net/manual/en/function.fopen.php - * - * @param string $path - * @param string $mode - * @return resource|bool - * @throws GenericEncryptionException - * @throws ModuleDoesNotExistsException - */ - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { // check if the file is stored in the array cache, this means that we // copy a file over to the versions folder, in this case we don't want to // decrypt it @@ -408,10 +304,8 @@ class Encryption extends Wrapper { // if we update a encrypted file with a un-encrypted one we change the db flag if ($targetIsEncrypted && $encryptionEnabled === false) { $cache = $this->storage->getCache(); - if ($cache) { - $entry = $cache->get($path); - $cache->update($entry->getId(), ['encrypted' => 0]); - } + $entry = $cache->get($path); + $cache->update($entry->getId(), ['encrypted' => 0]); } if ($encryptionEnabled) { // if $encryptionModuleId is empty, the default module will be used @@ -426,7 +320,7 @@ class Encryption extends Wrapper { if (!empty($encryptionModuleId)) { $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); $shouldEncrypt = true; - } elseif (empty($encryptionModuleId) && $info['encrypted'] === true) { + } elseif ($info !== false && $info['encrypted'] === true) { // we come from a old installation. No header and/or no module defined // but the file is encrypted. In this case we need to use the // OC_DEFAULT_MODULE to read the file @@ -452,6 +346,16 @@ class Encryption extends Wrapper { if ($shouldEncrypt === true && $encryptionModule !== null) { $this->encryptedPaths->set($this->util->stripPartialFileExtension($path), true); $headerSize = $this->getHeaderSize($path); + if ($mode === 'r' && $headerSize === 0) { + $firstBlock = $this->readFirstBlock($path); + if (!$firstBlock) { + throw new InvalidHeaderException("Unable to get header block for $path"); + } elseif (!str_starts_with($firstBlock, Util::HEADER_START)) { + throw new InvalidHeaderException("Unable to get header size for $path, file doesn't start with encryption header"); + } else { + throw new InvalidHeaderException("Unable to get header size for $path, even though file does start with encryption header"); + } + } $source = $this->storage->fopen($path, $mode); if (!is_resource($source)) { return false; @@ -469,7 +373,7 @@ class Encryption extends Wrapper { /** - * perform some plausibility checks if the the unencrypted size is correct. + * perform some plausibility checks if the unencrypted size is correct. * If not, we calculate the correct unencrypted size and return it * * @param string $path internal path relative to the storage root @@ -481,9 +385,9 @@ class Encryption extends Wrapper { $size = $this->storage->filesize($path); $result = $unencryptedSize; - if ($unencryptedSize < 0 || - ($size > 0 && $unencryptedSize === $size) || - $unencryptedSize > $size + if ($unencryptedSize < 0 + || ($size > 0 && $unencryptedSize === $size) + || $unencryptedSize > $size ) { // check if we already calculate the unencrypted size for the // given path to avoid recursions @@ -507,10 +411,8 @@ class Encryption extends Wrapper { * @param string $path internal path relative to the storage root * @param int $size size of the physical file * @param int $unencryptedSize size of the unencrypted file - * - * @return int calculated unencrypted size */ - protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int { + protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int|float { $headerSize = $this->getHeaderSize($path); $header = $this->getHeader($path); $encryptionModule = $this->getEncryptionModule($path); @@ -579,12 +481,10 @@ class Encryption extends Wrapper { // write to cache if applicable $cache = $this->storage->getCache(); - if ($cache) { - $entry = $cache->get($path); - $cache->update($entry['fileid'], [ - 'unencrypted_size' => $newUnencryptedSize - ]); - } + $entry = $cache->get($path); + $cache->update($entry['fileid'], [ + 'unencrypted_size' => $newUnencryptedSize + ]); return $newUnencryptedSize; } @@ -598,7 +498,7 @@ class Encryption extends Wrapper { * This is required as stream_read only returns smaller chunks of data when the stream fetches from a * remote storage over the internet and it does not care about the given $blockSize. * - * @param handle the stream to read from + * @param resource $handle the stream to read from * @param int $blockSize Length of requested data block in bytes * @return string Data fetched from stream. */ @@ -616,19 +516,12 @@ class Encryption extends Wrapper { return $data; } - /** - * @param Storage\IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @param bool $preserveMtime - * @return bool - */ public function moveFromStorage( Storage\IStorage $sourceStorage, - $sourceInternalPath, - $targetInternalPath, - $preserveMtime = true - ) { + string $sourceInternalPath, + string $targetInternalPath, + $preserveMtime = true, + ): bool { if ($sourceStorage === $this) { return $this->rename($sourceInternalPath, $targetInternalPath); } @@ -645,31 +538,34 @@ class Encryption extends Wrapper { $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true); if ($result) { - if ($sourceStorage->is_dir($sourceInternalPath)) { - $result &= $sourceStorage->rmdir($sourceInternalPath); - } else { - $result &= $sourceStorage->unlink($sourceInternalPath); + $setPreserveCacheOnDelete = $sourceStorage->instanceOfStorage(ObjectStoreStorage::class) && !$this->instanceOfStorage(ObjectStoreStorage::class); + if ($setPreserveCacheOnDelete) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(true); + } + try { + if ($sourceStorage->is_dir($sourceInternalPath)) { + $result = $sourceStorage->rmdir($sourceInternalPath); + } else { + $result = $sourceStorage->unlink($sourceInternalPath); + } + } finally { + if ($setPreserveCacheOnDelete) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(false); + } } } return $result; } - - /** - * @param Storage\IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @param bool $preserveMtime - * @param bool $isRename - * @return bool - */ public function copyFromStorage( Storage\IStorage $sourceStorage, - $sourceInternalPath, - $targetInternalPath, + string $sourceInternalPath, + string $targetInternalPath, $preserveMtime = false, - $isRename = false - ) { + $isRename = false, + ): bool { // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed: // - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage // - copy the file cache update from $this->copyBetweenStorage to this method @@ -681,20 +577,14 @@ class Encryption extends Wrapper { /** * Update the encrypted cache version in the database - * - * @param Storage\IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @param bool $isRename - * @param bool $keepEncryptionVersion */ private function updateEncryptedVersion( Storage\IStorage $sourceStorage, - $sourceInternalPath, - $targetInternalPath, - $isRename, - $keepEncryptionVersion - ) { + string $sourceInternalPath, + string $targetInternalPath, + bool $isRename, + bool $keepEncryptionVersion, + ): void { $isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath); $cacheInformation = [ 'encrypted' => $isEncrypted, @@ -734,26 +624,19 @@ class Encryption extends Wrapper { /** * copy file between two storages - * - * @param Storage\IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @param bool $preserveMtime - * @param bool $isRename - * @return bool * @throws \Exception */ private function copyBetweenStorage( Storage\IStorage $sourceStorage, - $sourceInternalPath, - $targetInternalPath, - $preserveMtime, - $isRename - ) { + string $sourceInternalPath, + string $targetInternalPath, + bool $preserveMtime, + bool $isRename, + ): bool { // for versions we have nothing to do, because versions should always use the // key from the original file. Just create a 1:1 copy and done - if ($this->isVersion($targetInternalPath) || - $this->isVersion($sourceInternalPath)) { + if ($this->isVersion($targetInternalPath) + || $this->isVersion($sourceInternalPath)) { // remember that we try to create a version so that we can detect it during // fopen($sourceInternalPath) and by-pass the encryption in order to // create a 1:1 copy of the file @@ -776,9 +659,8 @@ class Encryption extends Wrapper { // first copy the keys that we reuse the existing file key on the target location // and don't create a new one which would break versions for example. - $mount = $this->mountManager->findByStorageId($sourceStorage->getId()); - if (count($mount) === 1) { - $mountPoint = $mount[0]->getMountPoint(); + if ($sourceStorage->instanceOfStorage(Common::class) && $sourceStorage->getMountOption('mount_point')) { + $mountPoint = $sourceStorage->getMountOption('mount_point'); $source = $mountPoint . '/' . $sourceInternalPath; $target = $this->getFullPath($targetInternalPath); $this->copyKeys($source, $target); @@ -794,9 +676,9 @@ class Encryption extends Wrapper { $result = true; } if (is_resource($dh)) { - while ($result and ($file = readdir($dh)) !== false) { + while ($result && ($file = readdir($dh)) !== false) { if (!Filesystem::isIgnoredDir($file)) { - $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename); + $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, $preserveMtime, $isRename); } } } @@ -804,12 +686,16 @@ class Encryption extends Wrapper { try { $source = $sourceStorage->fopen($sourceInternalPath, 'r'); $target = $this->fopen($targetInternalPath, 'w'); - [, $result] = \OC_Helper::streamCopy($source, $target); + if ($source === false || $target === false) { + $result = false; + } else { + [, $result] = Files::streamCopy($source, $target, true); + } } finally { - if (is_resource($source)) { + if (isset($source) && $source !== false) { fclose($source); } - if (is_resource($target)) { + if (isset($target) && $target !== false) { fclose($target); } } @@ -828,7 +714,7 @@ class Encryption extends Wrapper { return (bool)$result; } - public function getLocalFile($path) { + public function getLocalFile(string $path): string|false { if ($this->encryptionManager->isEnabled()) { $cachedFile = $this->getCachedFile($path); if (is_string($cachedFile)) { @@ -838,14 +724,14 @@ class Encryption extends Wrapper { return $this->storage->getLocalFile($path); } - public function isLocal() { + public function isLocal(): bool { if ($this->encryptionManager->isEnabled()) { return false; } return $this->storage->isLocal(); } - public function stat($path) { + public function stat(string $path): array|false { $stat = $this->storage->stat($path); if (!$stat) { return false; @@ -857,8 +743,11 @@ class Encryption extends Wrapper { return $stat; } - public function hash($type, $path, $raw = false) { + public function hash(string $type, string $path, bool $raw = false): string|false { $fh = $this->fopen($path, 'rb'); + if ($fh === false) { + return false; + } $ctx = hash_init($type); hash_update_stream($ctx, $fh); fclose($fh); @@ -871,21 +760,21 @@ class Encryption extends Wrapper { * @param string $path relative to mount point * @return string full path including mount point */ - protected function getFullPath($path) { + protected function getFullPath(string $path): string { return Filesystem::normalizePath($this->mountPoint . '/' . $path); } /** * read first block of encrypted file, typically this will contain the * encryption header - * - * @param string $path - * @return string */ - protected function readFirstBlock($path) { + protected function readFirstBlock(string $path): string { $firstBlock = ''; if ($this->storage->is_file($path)) { $handle = $this->storage->fopen($path, 'r'); + if ($handle === false) { + return ''; + } $firstBlock = fread($handle, $this->util->getHeaderSize()); fclose($handle); } @@ -894,11 +783,8 @@ class Encryption extends Wrapper { /** * return header size of given file - * - * @param string $path - * @return int */ - protected function getHeaderSize($path) { + protected function getHeaderSize(string $path): int { $headerSize = 0; $realFile = $this->util->stripPartialFileExtension($path); if ($this->storage->is_file($realFile)) { @@ -915,11 +801,8 @@ class Encryption extends Wrapper { /** * read header from file - * - * @param string $path - * @return array */ - protected function getHeader($path) { + protected function getHeader(string $path): array { $realFile = $this->util->stripPartialFileExtension($path); $exists = $this->storage->is_file($realFile); if ($exists) { @@ -951,12 +834,10 @@ class Encryption extends Wrapper { /** * read encryption module needed to read/write the file located at $path * - * @param string $path - * @return null|\OCP\Encryption\IEncryptionModule * @throws ModuleDoesNotExistsException * @throws \Exception */ - protected function getEncryptionModule($path) { + protected function getEncryptionModule(string $path): ?\OCP\Encryption\IEncryptionModule { $encryptionModule = null; $header = $this->getHeader($path); $encryptionModuleId = $this->util->getEncryptionModuleId($header); @@ -972,11 +853,7 @@ class Encryption extends Wrapper { return $encryptionModule; } - /** - * @param string $path - * @param int $unencryptedSize - */ - public function updateUnencryptedSize($path, $unencryptedSize) { + public function updateUnencryptedSize(string $path, int|float $unencryptedSize): void { $this->unencryptedSize[$path] = $unencryptedSize; } @@ -985,9 +862,8 @@ class Encryption extends Wrapper { * * @param string $source path relative to data/ * @param string $target path relative to data/ - * @return bool */ - protected function copyKeys($source, $target) { + protected function copyKeys(string $source, string $target): bool { if (!$this->util->isExcluded($source)) { return $this->keyStorage->copyKeys($source, $target); } @@ -997,22 +873,16 @@ class Encryption extends Wrapper { /** * check if path points to a files version - * - * @param $path - * @return bool */ - protected function isVersion($path) { + protected function isVersion(string $path): bool { $normalized = Filesystem::normalizePath($path); return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/'; } /** * check if the given storage should be encrypted or not - * - * @param $path - * @return bool */ - protected function shouldEncrypt($path) { + protected function shouldEncrypt(string $path): bool { $fullPath = $this->getFullPath($path); $mountPointConfig = $this->mount->getOption('encrypt', true); if ($mountPointConfig === false) { @@ -1035,7 +905,10 @@ class Encryption extends Wrapper { public function writeStream(string $path, $stream, ?int $size = null): int { // always fall back to fopen $target = $this->fopen($path, 'w'); - [$count, $result] = \OC_Helper::streamCopy($stream, $target); + if ($target === false) { + throw new GenericFileException("Failed to open $path for writing"); + } + [$count, $result] = Files::streamCopy($stream, $target, true); fclose($stream); fclose($target); @@ -1054,11 +927,20 @@ class Encryption extends Wrapper { /** * Allow temporarily disabling the wrapper - * - * @param bool $enabled - * @return void */ public function setEnabled(bool $enabled): void { $this->enabled = $enabled; } + + /** + * Check if the on-disk data for a file has a valid encrypted header + * + * @param string $path + * @return bool + */ + public function hasValidHeader(string $path): bool { + $firstBlock = $this->readFirstBlock($path); + $header = $this->util->parseRawHeader($firstBlock); + return (count($header) > 0); + } } diff --git a/lib/private/Files/Storage/Wrapper/Jail.php b/lib/private/Files/Storage/Wrapper/Jail.php index 2e8306dc705..38b113cef88 100644 --- a/lib/private/Files/Storage/Wrapper/Jail.php +++ b/lib/private/Files/Storage/Wrapper/Jail.php @@ -11,6 +11,10 @@ use OC\Files\Cache\Wrapper\CacheJail; use OC\Files\Cache\Wrapper\JailPropagator; use OC\Files\Cache\Wrapper\JailWatcher; use OC\Files\Filesystem; +use OCP\Files; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\IPropagator; +use OCP\Files\Cache\IWatcher; use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IWriteStreamStorage; use OCP\Lock\ILockingProvider; @@ -27,29 +31,29 @@ class Jail extends Wrapper { protected $rootPath; /** - * @param array $arguments ['storage' => $storage, 'root' => $root] + * @param array $parameters ['storage' => $storage, 'root' => $root] * * $storage: The storage that will be wrapper * $root: The folder in the wrapped storage that will become the root folder of the wrapped storage */ - public function __construct($arguments) { - parent::__construct($arguments); - $this->rootPath = $arguments['root']; + public function __construct(array $parameters) { + parent::__construct($parameters); + $this->rootPath = $parameters['root']; } - public function getUnjailedPath($path) { + public function getUnjailedPath(string $path): string { return trim(Filesystem::normalizePath($this->rootPath . '/' . $path), '/'); } /** * This is separate from Wrapper::getWrapperStorage so we can get the jailed storage consistently even if the jail is inside another wrapper */ - public function getUnjailedStorage() { + public function getUnjailedStorage(): IStorage { return $this->storage; } - public function getJailedPath($path) { + public function getJailedPath(string $path): ?string { $root = rtrim($this->rootPath, '/') . '/'; if ($path !== $this->rootPath && !str_starts_with($path, $root)) { @@ -60,427 +64,178 @@ class Jail extends Wrapper { } } - public function getId() { + public function getId(): string { return parent::getId(); } - /** - * see https://www.php.net/manual/en/function.mkdir.php - * - * @param string $path - * @return bool - */ - public function mkdir($path) { + public function mkdir(string $path): bool { return $this->getWrapperStorage()->mkdir($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.rmdir.php - * - * @param string $path - * @return bool - */ - public function rmdir($path) { + public function rmdir(string $path): bool { return $this->getWrapperStorage()->rmdir($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.opendir.php - * - * @param string $path - * @return resource|false - */ - public function opendir($path) { + public function opendir(string $path) { return $this->getWrapperStorage()->opendir($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.is_dir.php - * - * @param string $path - * @return bool - */ - public function is_dir($path) { + public function is_dir(string $path): bool { return $this->getWrapperStorage()->is_dir($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.is_file.php - * - * @param string $path - * @return bool - */ - public function is_file($path) { + public function is_file(string $path): bool { return $this->getWrapperStorage()->is_file($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.stat.php - * only the following keys are required in the result: size and mtime - * - * @param string $path - * @return array|bool - */ - public function stat($path) { + public function stat(string $path): array|false { return $this->getWrapperStorage()->stat($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.filetype.php - * - * @param string $path - * @return bool - */ - public function filetype($path) { + public function filetype(string $path): string|false { return $this->getWrapperStorage()->filetype($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.filesize.php - * The result for filesize when called on a folder is required to be 0 - */ - public function filesize($path): false|int|float { + public function filesize(string $path): int|float|false { return $this->getWrapperStorage()->filesize($this->getUnjailedPath($path)); } - /** - * check if a file can be created in $path - * - * @param string $path - * @return bool - */ - public function isCreatable($path) { + public function isCreatable(string $path): bool { return $this->getWrapperStorage()->isCreatable($this->getUnjailedPath($path)); } - /** - * check if a file can be read - * - * @param string $path - * @return bool - */ - public function isReadable($path) { + public function isReadable(string $path): bool { return $this->getWrapperStorage()->isReadable($this->getUnjailedPath($path)); } - /** - * check if a file can be written to - * - * @param string $path - * @return bool - */ - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { return $this->getWrapperStorage()->isUpdatable($this->getUnjailedPath($path)); } - /** - * check if a file can be deleted - * - * @param string $path - * @return bool - */ - public function isDeletable($path) { + public function isDeletable(string $path): bool { return $this->getWrapperStorage()->isDeletable($this->getUnjailedPath($path)); } - /** - * check if a file can be shared - * - * @param string $path - * @return bool - */ - public function isSharable($path) { + public function isSharable(string $path): bool { return $this->getWrapperStorage()->isSharable($this->getUnjailedPath($path)); } - /** - * get the full permissions of a path. - * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php - * - * @param string $path - * @return int - */ - public function getPermissions($path) { + public function getPermissions(string $path): int { return $this->getWrapperStorage()->getPermissions($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.file_exists.php - * - * @param string $path - * @return bool - */ - public function file_exists($path) { + public function file_exists(string $path): bool { return $this->getWrapperStorage()->file_exists($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.filemtime.php - * - * @param string $path - * @return int|bool - */ - public function filemtime($path) { + public function filemtime(string $path): int|false { return $this->getWrapperStorage()->filemtime($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.file_get_contents.php - * - * @param string $path - * @return string|false - */ - public function file_get_contents($path) { + public function file_get_contents(string $path): string|false { return $this->getWrapperStorage()->file_get_contents($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.file_put_contents.php - * - * @param string $path - * @param mixed $data - * @return int|float|false - */ - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { return $this->getWrapperStorage()->file_put_contents($this->getUnjailedPath($path), $data); } - /** - * see https://www.php.net/manual/en/function.unlink.php - * - * @param string $path - * @return bool - */ - public function unlink($path) { + public function unlink(string $path): bool { return $this->getWrapperStorage()->unlink($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.rename.php - * - * @param string $source - * @param string $target - * @return bool - */ - public function rename($source, $target) { + public function rename(string $source, string $target): bool { return $this->getWrapperStorage()->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target)); } - /** - * see https://www.php.net/manual/en/function.copy.php - * - * @param string $source - * @param string $target - * @return bool - */ - public function copy($source, $target) { + public function copy(string $source, string $target): bool { return $this->getWrapperStorage()->copy($this->getUnjailedPath($source), $this->getUnjailedPath($target)); } - /** - * see https://www.php.net/manual/en/function.fopen.php - * - * @param string $path - * @param string $mode - * @return resource|bool - */ - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { return $this->getWrapperStorage()->fopen($this->getUnjailedPath($path), $mode); } - /** - * get the mimetype for a file or folder - * The mimetype for a folder is required to be "httpd/unix-directory" - * - * @param string $path - * @return string|bool - */ - public function getMimeType($path) { + public function getMimeType(string $path): string|false { return $this->getWrapperStorage()->getMimeType($this->getUnjailedPath($path)); } - /** - * see https://www.php.net/manual/en/function.hash.php - * - * @param string $type - * @param string $path - * @param bool $raw - * @return string|bool - */ - public function hash($type, $path, $raw = false) { + public function hash(string $type, string $path, bool $raw = false): string|false { return $this->getWrapperStorage()->hash($type, $this->getUnjailedPath($path), $raw); } - /** - * see https://www.php.net/manual/en/function.free_space.php - * - * @param string $path - * @return int|float|bool - */ - public function free_space($path) { + public function free_space(string $path): int|float|false { return $this->getWrapperStorage()->free_space($this->getUnjailedPath($path)); } - /** - * search for occurrences of $query in file names - * - * @param string $query - * @return array|bool - */ - public function search($query) { - return $this->getWrapperStorage()->search($query); - } - - /** - * see https://www.php.net/manual/en/function.touch.php - * If the backend does not support the operation, false should be returned - * - * @param string $path - * @param int $mtime - * @return bool - */ - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { return $this->getWrapperStorage()->touch($this->getUnjailedPath($path), $mtime); } - /** - * get the path to a local version of the file. - * The local version of the file can be temporary and doesn't have to be persistent across requests - * - * @param string $path - * @return string|false - */ - public function getLocalFile($path) { + public function getLocalFile(string $path): string|false { return $this->getWrapperStorage()->getLocalFile($this->getUnjailedPath($path)); } - /** - * check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @return bool - * - * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed. - * returning true for other changes in the folder is optional - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { return $this->getWrapperStorage()->hasUpdated($this->getUnjailedPath($path), $time); } - /** - * get a cache instance for the storage - * - * @param string $path - * @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache - * @return \OC\Files\Cache\Cache - */ - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): ICache { $sourceCache = $this->getWrapperStorage()->getCache($this->getUnjailedPath($path)); return new CacheJail($sourceCache, $this->rootPath); } - /** - * get the user id of the owner of a file or folder - * - * @param string $path - * @return string - */ - public function getOwner($path) { + public function getOwner(string $path): string|false { return $this->getWrapperStorage()->getOwner($this->getUnjailedPath($path)); } - /** - * get a watcher instance for the cache - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher - * @return \OC\Files\Cache\Watcher - */ - public function getWatcher($path = '', $storage = null) { + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { $sourceWatcher = $this->getWrapperStorage()->getWatcher($this->getUnjailedPath($path), $this->getWrapperStorage()); return new JailWatcher($sourceWatcher, $this->rootPath); } - /** - * get the ETag for a file or folder - * - * @param string $path - * @return string|false - */ - public function getETag($path) { + public function getETag(string $path): string|false { return $this->getWrapperStorage()->getETag($this->getUnjailedPath($path)); } - public function getMetaData($path) { + public function getMetaData(string $path): ?array { return $this->getWrapperStorage()->getMetaData($this->getUnjailedPath($path)); } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function acquireLock($path, $type, ILockingProvider $provider) { + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { $this->getWrapperStorage()->acquireLock($this->getUnjailedPath($path), $type, $provider); } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - */ - public function releaseLock($path, $type, ILockingProvider $provider) { + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { $this->getWrapperStorage()->releaseLock($this->getUnjailedPath($path), $type, $provider); } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - */ - public function changeLock($path, $type, ILockingProvider $provider) { + public function changeLock(string $path, int $type, ILockingProvider $provider): void { $this->getWrapperStorage()->changeLock($this->getUnjailedPath($path), $type, $provider); } /** * Resolve the path for the source of the share - * - * @param string $path - * @return array */ - public function resolvePath($path) { + public function resolvePath(string $path): array { return [$this->getWrapperStorage(), $this->getUnjailedPath($path)]; } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($sourceStorage === $this) { return $this->copy($sourceInternalPath, $targetInternalPath); } return $this->getWrapperStorage()->copyFromStorage($sourceStorage, $sourceInternalPath, $this->getUnjailedPath($targetInternalPath)); } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($sourceStorage === $this) { return $this->rename($sourceInternalPath, $targetInternalPath); } return $this->getWrapperStorage()->moveFromStorage($sourceStorage, $sourceInternalPath, $this->getUnjailedPath($targetInternalPath)); } - public function getPropagator($storage = null) { + public function getPropagator(?IStorage $storage = null): IPropagator { if (isset($this->propagator)) { return $this->propagator; } @@ -499,14 +254,14 @@ class Jail extends Wrapper { return $storage->writeStream($this->getUnjailedPath($path), $stream, $size); } else { $target = $this->fopen($path, 'w'); - [$count, $result] = \OC_Helper::streamCopy($stream, $target); + $count = Files::streamCopy($stream, $target); fclose($stream); fclose($target); return $count; } } - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { return $this->getWrapperStorage()->getDirectoryContent($this->getUnjailedPath($directory)); } } diff --git a/lib/private/Files/Storage/Wrapper/KnownMtime.php b/lib/private/Files/Storage/Wrapper/KnownMtime.php index a7360ae1346..657c6c9250c 100644 --- a/lib/private/Files/Storage/Wrapper/KnownMtime.php +++ b/lib/private/Files/Storage/Wrapper/KnownMtime.php @@ -20,13 +20,13 @@ class KnownMtime extends Wrapper { private CappedMemoryCache $knowMtimes; private ClockInterface $clock; - public function __construct($arguments) { - parent::__construct($arguments); + public function __construct(array $parameters) { + parent::__construct($parameters); $this->knowMtimes = new CappedMemoryCache(); - $this->clock = $arguments['clock']; + $this->clock = $parameters['clock']; } - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { $result = parent::file_put_contents($path, $data); if ($result) { $now = $this->clock->now()->getTimestamp(); @@ -35,7 +35,7 @@ class KnownMtime extends Wrapper { return $result; } - public function stat($path) { + public function stat(string $path): array|false { $stat = parent::stat($path); if ($stat) { $this->applyKnownMtime($path, $stat); @@ -43,7 +43,7 @@ class KnownMtime extends Wrapper { return $stat; } - public function getMetaData($path) { + public function getMetaData(string $path): ?array { $stat = parent::getMetaData($path); if ($stat) { $this->applyKnownMtime($path, $stat); @@ -51,19 +51,19 @@ class KnownMtime extends Wrapper { return $stat; } - private function applyKnownMtime(string $path, array &$stat) { + private function applyKnownMtime(string $path, array &$stat): void { if (isset($stat['mtime'])) { $knownMtime = $this->knowMtimes->get($path) ?? 0; $stat['mtime'] = max($stat['mtime'], $knownMtime); } } - public function filemtime($path) { + public function filemtime(string $path): int|false { $knownMtime = $this->knowMtimes->get($path) ?? 0; return max(parent::filemtime($path), $knownMtime); } - public function mkdir($path) { + public function mkdir(string $path): bool { $result = parent::mkdir($path); if ($result) { $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); @@ -71,7 +71,7 @@ class KnownMtime extends Wrapper { return $result; } - public function rmdir($path) { + public function rmdir(string $path): bool { $result = parent::rmdir($path); if ($result) { $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); @@ -79,7 +79,7 @@ class KnownMtime extends Wrapper { return $result; } - public function unlink($path) { + public function unlink(string $path): bool { $result = parent::unlink($path); if ($result) { $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); @@ -87,7 +87,7 @@ class KnownMtime extends Wrapper { return $result; } - public function rename($source, $target) { + public function rename(string $source, string $target): bool { $result = parent::rename($source, $target); if ($result) { $this->knowMtimes->set($target, $this->clock->now()->getTimestamp()); @@ -96,7 +96,7 @@ class KnownMtime extends Wrapper { return $result; } - public function copy($source, $target) { + public function copy(string $source, string $target): bool { $result = parent::copy($source, $target); if ($result) { $this->knowMtimes->set($target, $this->clock->now()->getTimestamp()); @@ -104,7 +104,7 @@ class KnownMtime extends Wrapper { return $result; } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $result = parent::fopen($path, $mode); if ($result && $mode === 'w') { $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); @@ -112,7 +112,7 @@ class KnownMtime extends Wrapper { return $result; } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { $result = parent::touch($path, $mtime); if ($result) { $this->knowMtimes->set($path, $mtime ?? $this->clock->now()->getTimestamp()); @@ -120,7 +120,7 @@ class KnownMtime extends Wrapper { return $result; } - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { $result = parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); if ($result) { $this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp()); @@ -128,7 +128,7 @@ class KnownMtime extends Wrapper { return $result; } - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { $result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); if ($result) { $this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp()); diff --git a/lib/private/Files/Storage/Wrapper/PermissionsMask.php b/lib/private/Files/Storage/Wrapper/PermissionsMask.php index c502de41a86..684040146ba 100644 --- a/lib/private/Files/Storage/Wrapper/PermissionsMask.php +++ b/lib/private/Files/Storage/Wrapper/PermissionsMask.php @@ -9,6 +9,7 @@ namespace OC\Files\Storage\Wrapper; use OC\Files\Cache\Wrapper\CachePermissionsMask; use OCP\Constants; +use OCP\Files\Storage\IStorage; /** * Mask the permissions of a storage @@ -24,41 +25,41 @@ class PermissionsMask extends Wrapper { private $mask; /** - * @param array $arguments ['storage' => $storage, 'mask' => $mask] + * @param array $parameters ['storage' => $storage, 'mask' => $mask] * * $storage: The storage the permissions mask should be applied on * $mask: The permission bits that should be kept, a combination of the \OCP\Constant::PERMISSION_ constants */ - public function __construct($arguments) { - parent::__construct($arguments); - $this->mask = $arguments['mask']; + public function __construct(array $parameters) { + parent::__construct($parameters); + $this->mask = $parameters['mask']; } - private function checkMask($permissions) { + private function checkMask(int $permissions): bool { return ($this->mask & $permissions) === $permissions; } - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { return $this->checkMask(Constants::PERMISSION_UPDATE) and parent::isUpdatable($path); } - public function isCreatable($path) { + public function isCreatable(string $path): bool { return $this->checkMask(Constants::PERMISSION_CREATE) and parent::isCreatable($path); } - public function isDeletable($path) { + public function isDeletable(string $path): bool { return $this->checkMask(Constants::PERMISSION_DELETE) and parent::isDeletable($path); } - public function isSharable($path) { + public function isSharable(string $path): bool { return $this->checkMask(Constants::PERMISSION_SHARE) and parent::isSharable($path); } - public function getPermissions($path) { + public function getPermissions(string $path): int { return $this->storage->getPermissions($path) & $this->mask; } - public function rename($source, $target) { + public function rename(string $source, string $target): bool { //This is a rename of the transfer file to the original file if (dirname($source) === dirname($target) && strpos($source, '.ocTransferId') > 0) { return $this->checkMask(Constants::PERMISSION_CREATE) and parent::rename($source, $target); @@ -66,33 +67,33 @@ class PermissionsMask extends Wrapper { return $this->checkMask(Constants::PERMISSION_UPDATE) and parent::rename($source, $target); } - public function copy($source, $target) { + public function copy(string $source, string $target): bool { return $this->checkMask(Constants::PERMISSION_CREATE) and parent::copy($source, $target); } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { $permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE; return $this->checkMask($permissions) and parent::touch($path, $mtime); } - public function mkdir($path) { + public function mkdir(string $path): bool { return $this->checkMask(Constants::PERMISSION_CREATE) and parent::mkdir($path); } - public function rmdir($path) { + public function rmdir(string $path): bool { return $this->checkMask(Constants::PERMISSION_DELETE) and parent::rmdir($path); } - public function unlink($path) { + public function unlink(string $path): bool { return $this->checkMask(Constants::PERMISSION_DELETE) and parent::unlink($path); } - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { $permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE; return $this->checkMask($permissions) ? parent::file_put_contents($path, $data) : false; } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { if ($mode === 'r' or $mode === 'rb') { return parent::fopen($path, $mode); } else { @@ -101,14 +102,7 @@ class PermissionsMask extends Wrapper { } } - /** - * get a cache instance for the storage - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the cache - * @return \OC\Files\Cache\Cache - */ - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\ICache { if (!$storage) { $storage = $this; } @@ -116,7 +110,7 @@ class PermissionsMask extends Wrapper { return new CachePermissionsMask($sourceCache, $this->mask); } - public function getMetaData($path) { + public function getMetaData(string $path): ?array { $data = parent::getMetaData($path); if ($data && isset($data['permissions'])) { @@ -126,14 +120,14 @@ class PermissionsMask extends Wrapper { return $data; } - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\IScanner { if (!$storage) { $storage = $this->storage; } return parent::getScanner($path, $storage); } - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) { $data['scan_permissions'] = $data['scan_permissions'] ?? $data['permissions']; $data['permissions'] &= $this->mask; diff --git a/lib/private/Files/Storage/Wrapper/Quota.php b/lib/private/Files/Storage/Wrapper/Quota.php index 8c6799fdd2d..35a265f8c8e 100644 --- a/lib/private/Files/Storage/Wrapper/Quota.php +++ b/lib/private/Files/Storage/Wrapper/Quota.php @@ -21,11 +21,12 @@ class Quota extends Wrapper { protected string $sizeRoot; private SystemConfig $config; private bool $quotaIncludeExternalStorage; + private bool $enabled = true; /** * @param array $parameters */ - public function __construct($parameters) { + public function __construct(array $parameters) { parent::__construct($parameters); $this->quota = $parameters['quota'] ?? null; $this->quotaCallback = $parameters['quotaCallback'] ?? null; @@ -33,14 +34,11 @@ class Quota extends Wrapper { $this->quotaIncludeExternalStorage = $parameters['include_external_storage'] ?? false; } - /** - * @return int|float quota value - */ public function getQuota(): int|float { if ($this->quota === null) { $quotaCallback = $this->quotaCallback; if ($quotaCallback === null) { - throw new \Exception("No quota or quota callback provider"); + throw new \Exception('No quota or quota callback provider'); } $this->quota = $quotaCallback(); } @@ -49,15 +47,13 @@ class Quota extends Wrapper { } private function hasQuota(): bool { + if (!$this->enabled) { + return false; + } return $this->getQuota() !== FileInfo::SPACE_UNLIMITED; } - /** - * @param string $path - * @param IStorage $storage - * @return int|float - */ - protected function getSize($path, $storage = null) { + protected function getSize(string $path, ?IStorage $storage = null): int|float { if ($this->quotaIncludeExternalStorage) { $rootInfo = Filesystem::getFileInfo('', 'ext'); if ($rootInfo) { @@ -75,13 +71,7 @@ class Quota extends Wrapper { } } - /** - * Get free space as limited by the quota - * - * @param string $path - * @return int|float|bool - */ - public function free_space($path) { + public function free_space(string $path): int|float|false { if (!$this->hasQuota()) { return $this->storage->free_space($path); } @@ -101,14 +91,7 @@ class Quota extends Wrapper { } } - /** - * see https://www.php.net/manual/en/function.file_put_contents.php - * - * @param string $path - * @param mixed $data - * @return int|float|false - */ - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { if (!$this->hasQuota()) { return $this->storage->file_put_contents($path, $data); } @@ -120,14 +103,7 @@ class Quota extends Wrapper { } } - /** - * see https://www.php.net/manual/en/function.copy.php - * - * @param string $source - * @param string $target - * @return bool - */ - public function copy($source, $target) { + public function copy(string $source, string $target): bool { if (!$this->hasQuota()) { return $this->storage->copy($source, $target); } @@ -139,14 +115,7 @@ class Quota extends Wrapper { } } - /** - * see https://www.php.net/manual/en/function.fopen.php - * - * @param string $path - * @param string $mode - * @return resource|bool - */ - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { if (!$this->hasQuota()) { return $this->storage->fopen($path, $mode); } @@ -170,10 +139,9 @@ class Quota extends Wrapper { * Checks whether the given path is a part file * * @param string $path Path that may identify a .part file - * @return bool * @note this is needed for reusing keys */ - private function isPartFile($path) { + private function isPartFile(string $path): bool { $extension = pathinfo($path, PATHINFO_EXTENSION); return ($extension === 'part'); @@ -182,17 +150,11 @@ class Quota extends Wrapper { /** * Only apply quota for files, not metadata, trash or others */ - private function shouldApplyQuota(string $path): bool { + protected function shouldApplyQuota(string $path): bool { return str_starts_with(ltrim($path, '/'), 'files/'); } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if (!$this->hasQuota()) { return $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } @@ -204,13 +166,7 @@ class Quota extends Wrapper { } } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if (!$this->hasQuota()) { return $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } @@ -222,7 +178,7 @@ class Quota extends Wrapper { } } - public function mkdir($path) { + public function mkdir(string $path): bool { if (!$this->hasQuota()) { return $this->storage->mkdir($path); } @@ -234,7 +190,7 @@ class Quota extends Wrapper { return parent::mkdir($path); } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { if (!$this->hasQuota()) { return $this->storage->touch($path, $mtime); } @@ -245,4 +201,8 @@ class Quota extends Wrapper { return parent::touch($path, $mtime); } + + public function enableQuota(bool $enabled): void { + $this->enabled = $enabled; + } } diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index 29acc9ad1c2..7af11dd5ef7 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -8,7 +8,13 @@ namespace OC\Files\Storage\Wrapper; use OC\Files\Storage\FailedStorage; -use OCP\Files\InvalidPathException; +use OC\Files\Storage\Storage; +use OCP\Files; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\IPropagator; +use OCP\Files\Cache\IScanner; +use OCP\Files\Cache\IUpdater; +use OCP\Files\Cache\IWatcher; use OCP\Files\Storage\ILockingStorage; use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IWriteStreamStorage; @@ -31,16 +37,13 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea /** * @param array $parameters */ - public function __construct($parameters) { + public function __construct(array $parameters) { $this->storage = $parameters['storage']; } - /** - * @return \OC\Files\Storage\Storage - */ - public function getWrapperStorage() { + public function getWrapperStorage(): Storage { if (!$this->storage) { - $message = "storage wrapper " . get_class($this) . " doesn't have a wrapped storage set"; + $message = 'storage wrapper ' . get_class($this) . " doesn't have a wrapped storage set"; $logger = Server::get(LoggerInterface::class); $logger->error($message); $this->storage = new FailedStorage(['exception' => new \Exception($message)]); @@ -48,430 +51,178 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea return $this->storage; } - /** - * Get the identifier for the storage, - * the returned id should be the same for every storage object that is created with the same parameters - * and two storage objects with the same id should refer to two storages that display the same files. - * - * @return string - */ - public function getId() { + public function getId(): string { return $this->getWrapperStorage()->getId(); } - /** - * see https://www.php.net/manual/en/function.mkdir.php - * - * @param string $path - * @return bool - */ - public function mkdir($path) { + public function mkdir(string $path): bool { return $this->getWrapperStorage()->mkdir($path); } - /** - * see https://www.php.net/manual/en/function.rmdir.php - * - * @param string $path - * @return bool - */ - public function rmdir($path) { + public function rmdir(string $path): bool { return $this->getWrapperStorage()->rmdir($path); } - /** - * see https://www.php.net/manual/en/function.opendir.php - * - * @param string $path - * @return resource|false - */ - public function opendir($path) { + public function opendir(string $path) { return $this->getWrapperStorage()->opendir($path); } - /** - * see https://www.php.net/manual/en/function.is_dir.php - * - * @param string $path - * @return bool - */ - public function is_dir($path) { + public function is_dir(string $path): bool { return $this->getWrapperStorage()->is_dir($path); } - /** - * see https://www.php.net/manual/en/function.is_file.php - * - * @param string $path - * @return bool - */ - public function is_file($path) { + public function is_file(string $path): bool { return $this->getWrapperStorage()->is_file($path); } - /** - * see https://www.php.net/manual/en/function.stat.php - * only the following keys are required in the result: size and mtime - * - * @param string $path - * @return array|bool - */ - public function stat($path) { + public function stat(string $path): array|false { return $this->getWrapperStorage()->stat($path); } - /** - * see https://www.php.net/manual/en/function.filetype.php - * - * @param string $path - * @return string|bool - */ - public function filetype($path) { + public function filetype(string $path): string|false { return $this->getWrapperStorage()->filetype($path); } - /** - * see https://www.php.net/manual/en/function.filesize.php - * The result for filesize when called on a folder is required to be 0 - */ - public function filesize($path): false|int|float { + public function filesize(string $path): int|float|false { return $this->getWrapperStorage()->filesize($path); } - /** - * check if a file can be created in $path - * - * @param string $path - * @return bool - */ - public function isCreatable($path) { + public function isCreatable(string $path): bool { return $this->getWrapperStorage()->isCreatable($path); } - /** - * check if a file can be read - * - * @param string $path - * @return bool - */ - public function isReadable($path) { + public function isReadable(string $path): bool { return $this->getWrapperStorage()->isReadable($path); } - /** - * check if a file can be written to - * - * @param string $path - * @return bool - */ - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { return $this->getWrapperStorage()->isUpdatable($path); } - /** - * check if a file can be deleted - * - * @param string $path - * @return bool - */ - public function isDeletable($path) { + public function isDeletable(string $path): bool { return $this->getWrapperStorage()->isDeletable($path); } - /** - * check if a file can be shared - * - * @param string $path - * @return bool - */ - public function isSharable($path) { + public function isSharable(string $path): bool { return $this->getWrapperStorage()->isSharable($path); } - /** - * get the full permissions of a path. - * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php - * - * @param string $path - * @return int - */ - public function getPermissions($path) { + public function getPermissions(string $path): int { return $this->getWrapperStorage()->getPermissions($path); } - /** - * see https://www.php.net/manual/en/function.file_exists.php - * - * @param string $path - * @return bool - */ - public function file_exists($path) { + public function file_exists(string $path): bool { return $this->getWrapperStorage()->file_exists($path); } - /** - * see https://www.php.net/manual/en/function.filemtime.php - * - * @param string $path - * @return int|bool - */ - public function filemtime($path) { + public function filemtime(string $path): int|false { return $this->getWrapperStorage()->filemtime($path); } - /** - * see https://www.php.net/manual/en/function.file_get_contents.php - * - * @param string $path - * @return string|false - */ - public function file_get_contents($path) { + public function file_get_contents(string $path): string|false { return $this->getWrapperStorage()->file_get_contents($path); } - /** - * see https://www.php.net/manual/en/function.file_put_contents.php - * - * @param string $path - * @param mixed $data - * @return int|float|false - */ - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { return $this->getWrapperStorage()->file_put_contents($path, $data); } - /** - * see https://www.php.net/manual/en/function.unlink.php - * - * @param string $path - * @return bool - */ - public function unlink($path) { + public function unlink(string $path): bool { return $this->getWrapperStorage()->unlink($path); } - /** - * see https://www.php.net/manual/en/function.rename.php - * - * @param string $source - * @param string $target - * @return bool - */ - public function rename($source, $target) { + public function rename(string $source, string $target): bool { return $this->getWrapperStorage()->rename($source, $target); } - /** - * see https://www.php.net/manual/en/function.copy.php - * - * @param string $source - * @param string $target - * @return bool - */ - public function copy($source, $target) { + public function copy(string $source, string $target): bool { return $this->getWrapperStorage()->copy($source, $target); } - /** - * see https://www.php.net/manual/en/function.fopen.php - * - * @param string $path - * @param string $mode - * @return resource|bool - */ - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { return $this->getWrapperStorage()->fopen($path, $mode); } - /** - * get the mimetype for a file or folder - * The mimetype for a folder is required to be "httpd/unix-directory" - * - * @param string $path - * @return string|bool - */ - public function getMimeType($path) { + public function getMimeType(string $path): string|false { return $this->getWrapperStorage()->getMimeType($path); } - /** - * see https://www.php.net/manual/en/function.hash.php - * - * @param string $type - * @param string $path - * @param bool $raw - * @return string|bool - */ - public function hash($type, $path, $raw = false) { + public function hash(string $type, string $path, bool $raw = false): string|false { return $this->getWrapperStorage()->hash($type, $path, $raw); } - /** - * see https://www.php.net/manual/en/function.free_space.php - * - * @param string $path - * @return int|float|bool - */ - public function free_space($path) { + public function free_space(string $path): int|float|false { return $this->getWrapperStorage()->free_space($path); } - /** - * search for occurrences of $query in file names - * - * @param string $query - * @return array|bool - */ - public function search($query) { - return $this->getWrapperStorage()->search($query); - } - - /** - * see https://www.php.net/manual/en/function.touch.php - * If the backend does not support the operation, false should be returned - * - * @param string $path - * @param int $mtime - * @return bool - */ - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { return $this->getWrapperStorage()->touch($path, $mtime); } - /** - * get the path to a local version of the file. - * The local version of the file can be temporary and doesn't have to be persistent across requests - * - * @param string $path - * @return string|false - */ - public function getLocalFile($path) { + public function getLocalFile(string $path): string|false { return $this->getWrapperStorage()->getLocalFile($path); } - /** - * check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @return bool - * - * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed. - * returning true for other changes in the folder is optional - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { return $this->getWrapperStorage()->hasUpdated($path, $time); } - /** - * get a cache instance for the storage - * - * @param string $path - * @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache - * @return \OC\Files\Cache\Cache - */ - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): ICache { if (!$storage) { $storage = $this; } return $this->getWrapperStorage()->getCache($path, $storage); } - /** - * get a scanner instance for the storage - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner - * @return \OC\Files\Cache\Scanner - */ - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } return $this->getWrapperStorage()->getScanner($path, $storage); } - - /** - * get the user id of the owner of a file or folder - * - * @param string $path - * @return string - */ - public function getOwner($path) { + public function getOwner(string $path): string|false { return $this->getWrapperStorage()->getOwner($path); } - /** - * get a watcher instance for the cache - * - * @param string $path - * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher - * @return \OC\Files\Cache\Watcher - */ - public function getWatcher($path = '', $storage = null) { + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { if (!$storage) { $storage = $this; } return $this->getWrapperStorage()->getWatcher($path, $storage); } - public function getPropagator($storage = null) { + public function getPropagator(?IStorage $storage = null): IPropagator { if (!$storage) { $storage = $this; } return $this->getWrapperStorage()->getPropagator($storage); } - public function getUpdater($storage = null) { + public function getUpdater(?IStorage $storage = null): IUpdater { if (!$storage) { $storage = $this; } return $this->getWrapperStorage()->getUpdater($storage); } - /** - * @return \OC\Files\Cache\Storage - */ - public function getStorageCache() { + public function getStorageCache(): \OC\Files\Cache\Storage { return $this->getWrapperStorage()->getStorageCache(); } - /** - * get the ETag for a file or folder - * - * @param string $path - * @return string|false - */ - public function getETag($path) { + public function getETag(string $path): string|false { return $this->getWrapperStorage()->getETag($path); } - /** - * Returns true - * - * @return true - */ - public function test() { + public function test(): bool { return $this->getWrapperStorage()->test(); } - /** - * Returns the wrapped storage's value for isLocal() - * - * @return bool wrapped storage's isLocal() value - */ - public function isLocal() { + public function isLocal(): bool { return $this->getWrapperStorage()->isLocal(); } - /** - * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class - * - * @param class-string<IStorage> $class - * @return bool - */ - public function instanceOfStorage($class) { + public function instanceOfStorage(string $class): bool { if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') { // FIXME Temporary fix to keep existing checks working $class = '\OCA\Files_Sharing\SharedStorage'; @@ -484,7 +235,7 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea * @psalm-param class-string<T> $class * @psalm-return T|null */ - public function getInstanceOfStorage(string $class) { + public function getInstanceOfStorage(string $class): ?IStorage { $storage = $this; while ($storage instanceof Wrapper) { if ($storage instanceof $class) { @@ -501,61 +252,29 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea /** * Pass any methods custom to specific storage implementations to the wrapped storage * - * @param string $method - * @param array $args * @return mixed */ - public function __call($method, $args) { + public function __call(string $method, array $args) { return call_user_func_array([$this->getWrapperStorage(), $method], $args); } - /** - * A custom storage implementation can return an url for direct download of a give file. - * - * For now the returned array can hold the parameter url - in future more attributes might follow. - * - * @param string $path - * @return array|bool - */ - public function getDirectDownload($path) { + public function getDirectDownload(string $path): array|false { return $this->getWrapperStorage()->getDirectDownload($path); } - /** - * Get availability of the storage - * - * @return array [ available, last_checked ] - */ - public function getAvailability() { + public function getAvailability(): array { return $this->getWrapperStorage()->getAvailability(); } - /** - * Set availability of the storage - * - * @param bool $isAvailable - */ - public function setAvailability($isAvailable) { + public function setAvailability(bool $isAvailable): void { $this->getWrapperStorage()->setAvailability($isAvailable); } - /** - * @param string $path the path of the target folder - * @param string $fileName the name of the file itself - * @return void - * @throws InvalidPathException - */ - public function verifyPath($path, $fileName) { + public function verifyPath(string $path, string $fileName): void { $this->getWrapperStorage()->verifyPath($path, $fileName); } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($sourceStorage === $this) { return $this->copy($sourceInternalPath, $targetInternalPath); } @@ -563,13 +282,7 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea return $this->getWrapperStorage()->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($sourceStorage === $this) { return $this->rename($sourceInternalPath, $targetInternalPath); } @@ -577,48 +290,29 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea return $this->getWrapperStorage()->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } - public function getMetaData($path) { + public function getMetaData(string $path): ?array { return $this->getWrapperStorage()->getMetaData($path); } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function acquireLock($path, $type, ILockingProvider $provider) { + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { if ($this->getWrapperStorage()->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) { $this->getWrapperStorage()->acquireLock($path, $type, $provider); } } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - */ - public function releaseLock($path, $type, ILockingProvider $provider) { + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { if ($this->getWrapperStorage()->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) { $this->getWrapperStorage()->releaseLock($path, $type, $provider); } } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - */ - public function changeLock($path, $type, ILockingProvider $provider) { + public function changeLock(string $path, int $type, ILockingProvider $provider): void { if ($this->getWrapperStorage()->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) { $this->getWrapperStorage()->changeLock($path, $type, $provider); } } - /** - * @return bool - */ - public function needsPartFile() { + public function needsPartFile(): bool { return $this->getWrapperStorage()->needsPartFile(); } @@ -629,18 +323,18 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea return $storage->writeStream($path, $stream, $size); } else { $target = $this->fopen($path, 'w'); - [$count, $result] = \OC_Helper::streamCopy($stream, $target); + $count = Files::streamCopy($stream, $target); fclose($stream); fclose($target); return $count; } } - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { return $this->getWrapperStorage()->getDirectoryContent($directory); } - public function isWrapperOf(IStorage $storage) { + public function isWrapperOf(IStorage $storage): bool { $wrapped = $this->getWrapperStorage(); if ($wrapped === $storage) { return true; diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php index 32c0021cd23..ef147ec421f 100644 --- a/lib/private/Files/Stream/Encryption.php +++ b/lib/private/Files/Stream/Encryption.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -9,81 +11,42 @@ namespace OC\Files\Stream; use Icewind\Streams\Wrapper; use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException; +use OC\Encryption\File; +use OC\Encryption\Util; +use OC\Files\Storage\Storage; +use OCP\Encryption\IEncryptionModule; use function is_array; use function stream_context_create; class Encryption extends Wrapper { - /** @var \OC\Encryption\Util */ - protected $util; - - /** @var \OC\Encryption\File */ - protected $file; - - /** @var \OCP\Encryption\IEncryptionModule */ - protected $encryptionModule; - - /** @var \OC\Files\Storage\Storage */ - protected $storage; - - /** @var \OC\Files\Storage\Wrapper\Encryption */ - protected $encryptionStorage; - - /** @var string */ - protected $internalPath; - - /** @var string */ - protected $cache; - - /** @var integer */ - protected $size; - - /** @var integer */ - protected $position; - - /** @var integer */ - protected $unencryptedSize; - - /** @var integer */ - protected $headerSize; - - /** @var integer */ - protected $unencryptedBlockSize; - - /** @var array */ - protected $header; - - /** @var string */ - protected $fullPath; - - /** @var bool */ - protected $signed; - + protected Util $util; + protected File $file; + protected IEncryptionModule $encryptionModule; + protected Storage $storage; + protected \OC\Files\Storage\Wrapper\Encryption $encryptionStorage; + protected string $internalPath; + protected string $cache; + protected ?int $size = null; + protected int $position; + protected ?int $unencryptedSize = null; + protected int $headerSize; + protected int $unencryptedBlockSize; + protected array $header; + protected string $fullPath; + protected bool $signed; /** * header data returned by the encryption module, will be written to the file * in case of a write operation - * - * @var array */ - protected $newHeader; - + protected array $newHeader; /** * user who perform the read/write operation null for public access - * - * @var string */ - protected $uid; - - /** @var bool */ - protected $readOnly; - - /** @var bool */ - protected $writeFlag; - - /** @var array */ - protected $expectedContextProperties; - - /** @var bool */ - protected $fileUpdated; + protected ?string $uid; + protected bool $readOnly; + protected bool $writeFlag; + protected array $expectedContextProperties; + protected bool $fileUpdated; public function __construct() { $this->expectedContextProperties = [ @@ -113,11 +76,11 @@ class Encryption extends Wrapper { * @param string $fullPath relative to data/ * @param array $header * @param string $uid - * @param \OCP\Encryption\IEncryptionModule $encryptionModule - * @param \OC\Files\Storage\Storage $storage + * @param IEncryptionModule $encryptionModule + * @param Storage $storage * @param \OC\Files\Storage\Wrapper\Encryption $encStorage - * @param \OC\Encryption\Util $util - * @param \OC\Encryption\File $file + * @param Util $util + * @param File $file * @param string $mode * @param int|float $size * @param int|float $unencryptedSize @@ -128,19 +91,24 @@ class Encryption extends Wrapper { * * @throws \BadMethodCallException */ - public static function wrap($source, $internalPath, $fullPath, array $header, + public static function wrap( + $source, + $internalPath, + $fullPath, + array $header, $uid, - \OCP\Encryption\IEncryptionModule $encryptionModule, - \OC\Files\Storage\Storage $storage, + IEncryptionModule $encryptionModule, + Storage $storage, \OC\Files\Storage\Wrapper\Encryption $encStorage, - \OC\Encryption\Util $util, - \OC\Encryption\File $file, + Util $util, + File $file, $mode, $size, $unencryptedSize, $headerSize, $signed, - $wrapper = Encryption::class) { + $wrapper = Encryption::class, + ) { $context = stream_context_create([ 'ocencryption' => [ 'source' => $source, @@ -344,8 +312,8 @@ class Encryption extends Wrapper { // for seekable streams the pointer is moved back to the beginning of the encrypted block // flush will start writing there when the position moves to another block - $positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) * - $this->util->getBlockSize() + $this->headerSize; + $positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) + * $this->util->getBlockSize() + $this->headerSize; $resultFseek = $this->parentStreamSeek($positionInFile); // only allow writes on seekable streams, or at the end of the encrypted stream @@ -368,8 +336,8 @@ class Encryption extends Wrapper { // if $data doesn't fit the current block, the fill the current block and reiterate // after the block is filled, it is flushed and $data is updatedxxx } else { - $this->cache = substr($this->cache, 0, $blockPosition) . - substr($data, 0, $this->unencryptedBlockSize - $blockPosition); + $this->cache = substr($this->cache, 0, $blockPosition) + . substr($data, 0, $this->unencryptedBlockSize - $blockPosition); $this->flush(); $this->position += ($this->unencryptedBlockSize - $blockPosition); $length += ($this->unencryptedBlockSize - $blockPosition); diff --git a/lib/private/Files/Stream/Quota.php b/lib/private/Files/Stream/Quota.php index d8e7ae6dff0..cc737910fd8 100644 --- a/lib/private/Files/Stream/Quota.php +++ b/lib/private/Files/Stream/Quota.php @@ -23,7 +23,7 @@ class Quota extends Wrapper { /** * @param resource $stream * @param int $limit - * @return bool|resource + * @return resource|false */ public static function wrap($stream, $limit) { $context = stream_context_create([ diff --git a/lib/private/Files/Stream/SeekableHttpStream.php b/lib/private/Files/Stream/SeekableHttpStream.php index 02ed1470fbd..6ce0a880e8d 100644 --- a/lib/private/Files/Stream/SeekableHttpStream.php +++ b/lib/private/Files/Stream/SeekableHttpStream.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -90,7 +91,7 @@ class SeekableHttpStream implements File { continue 2; } } - throw new \Exception("Failed to get source stream from stream wrapper of " . get_class($responseHead)); + throw new \Exception('Failed to get source stream from stream wrapper of ' . get_class($responseHead)); } $rangeHeaders = array_values(array_filter($responseHead, function ($v) { diff --git a/lib/private/Files/Template/TemplateManager.php b/lib/private/Files/Template/TemplateManager.php index 8362298b831..80ef5a14786 100644 --- a/lib/private/Files/Template/TemplateManager.php +++ b/lib/private/Files/Template/TemplateManager.php @@ -18,6 +18,8 @@ use OCP\Files\GenericFileException; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotFoundException; +use OCP\Files\Template\BeforeGetTemplatesEvent; +use OCP\Files\Template\Field; use OCP\Files\Template\FileCreatedFromTemplateEvent; use OCP\Files\Template\ICustomTemplateProvider; use OCP\Files\Template\ITemplateManager; @@ -62,7 +64,7 @@ class TemplateManager implements ITemplateManager { IPreview $previewManager, IConfig $config, IFactory $l10nFactory, - LoggerInterface $logger + LoggerInterface $logger, ) { $this->serverContainer = $serverContainer; $this->eventDispatcher = $eventDispatcher; @@ -117,20 +119,34 @@ class TemplateManager implements ITemplateManager { } public function listTemplates(): array { - return array_map(function (TemplateFileCreator $entry) { + return array_values(array_map(function (TemplateFileCreator $entry) { return array_merge($entry->jsonSerialize(), [ 'templates' => $this->getTemplateFiles($entry) ]); - }, $this->listCreators()); + }, $this->listCreators())); + } + + public function listTemplateFields(int $fileId): array { + foreach ($this->listCreators() as $creator) { + $fields = $this->getTemplateFields($creator, $fileId); + if (empty($fields)) { + continue; + } + + return $fields; + } + + return []; } /** * @param string $filePath * @param string $templateId + * @param array $templateFields * @return array * @throws GenericFileException */ - public function createFromTemplate(string $filePath, string $templateId = '', string $templateType = 'user'): array { + public function createFromTemplate(string $filePath, string $templateId = '', string $templateType = 'user', array $templateFields = []): array { $userFolder = $this->rootFolder->getUserFolder($this->userId); try { $userFolder->get($filePath); @@ -142,11 +158,9 @@ class TemplateManager implements ITemplateManager { throw new GenericFileException($this->l10n->t('Invalid path')); } $folder = $userFolder->get(dirname($filePath)); - $targetFile = $folder->newFile(basename($filePath)); $template = null; if ($templateType === 'user' && $templateId !== '') { $template = $userFolder->get($templateId); - $template->copy($targetFile->getPath()); } else { $matchingProvider = array_filter($this->getRegisteredProviders(), function (ICustomTemplateProvider $provider) use ($templateType) { return $templateType === get_class($provider); @@ -154,10 +168,12 @@ class TemplateManager implements ITemplateManager { $provider = array_shift($matchingProvider); if ($provider) { $template = $provider->getCustomTemplate($templateId); - $template->copy($targetFile->getPath()); } } - $this->eventDispatcher->dispatchTyped(new FileCreatedFromTemplateEvent($template, $targetFile)); + + $targetFile = $folder->newFile(basename($filePath), ($template instanceof File ? $template->fopen('rb') : null)); + + $this->eventDispatcher->dispatchTyped(new FileCreatedFromTemplateEvent($template, $targetFile, $templateFields)); return $this->formatFile($userFolder->get($filePath)); } catch (\Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); @@ -171,14 +187,34 @@ class TemplateManager implements ITemplateManager { * @throws \OCP\Files\NotPermittedException * @throws \OC\User\NoUserException */ - private function getTemplateFolder(): Node { + private function getTemplateFolder(): Folder { if ($this->getTemplatePath() !== '') { - return $this->rootFolder->getUserFolder($this->userId)->get($this->getTemplatePath()); + $path = $this->rootFolder->getUserFolder($this->userId)->get($this->getTemplatePath()); + if ($path instanceof Folder) { + return $path; + } } throw new NotFoundException(); } + /** + * @return list<Template> + */ private function getTemplateFiles(TemplateFileCreator $type): array { + $templates = array_merge( + $this->getProviderTemplates($type), + $this->getUserTemplates($type) + ); + + $this->eventDispatcher->dispatchTyped(new BeforeGetTemplatesEvent($templates, false)); + + return $templates; + } + + /** + * @return list<Template> + */ + private function getProviderTemplates(TemplateFileCreator $type): array { $templates = []; foreach ($this->getRegisteredProviders() as $provider) { foreach ($type->getMimetypes() as $mimetype) { @@ -187,11 +223,22 @@ class TemplateManager implements ITemplateManager { } } } + + return $templates; + } + + /** + * @return list<Template> + */ + private function getUserTemplates(TemplateFileCreator $type): array { + $templates = []; + try { $userTemplateFolder = $this->getTemplateFolder(); } catch (\Exception $e) { return $templates; } + foreach ($type->getMimetypes() as $mimetype) { foreach ($userTemplateFolder->searchByMime($mimetype) as $templateFile) { $template = new Template( @@ -207,6 +254,30 @@ class TemplateManager implements ITemplateManager { return $templates; } + /* + * @return list<Field> + */ + private function getTemplateFields(TemplateFileCreator $type, int $fileId): array { + $providerTemplates = $this->getProviderTemplates($type); + $userTemplates = $this->getUserTemplates($type); + + $matchedTemplates = array_filter( + array_merge($providerTemplates, $userTemplates), + function (Template $template) use ($fileId) { + return $template->jsonSerialize()['fileid'] === $fileId; + }); + + if (empty($matchedTemplates)) { + return []; + } + + $this->eventDispatcher->dispatchTyped(new BeforeGetTemplatesEvent($matchedTemplates, true)); + + return array_values(array_map(function (Template $template) { + return $template->jsonSerialize()['fields'] ?? []; + }, $matchedTemplates)); + } + /** * @param Node|File $file * @return array diff --git a/lib/private/Files/Type/Detection.php b/lib/private/Files/Type/Detection.php index b1e4c098e54..6af6ce1a0b1 100644 --- a/lib/private/Files/Type/Detection.php +++ b/lib/private/Files/Type/Detection.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace OC\Files\Type; use OCP\Files\IMimeTypeDetector; +use OCP\IBinaryFinder; +use OCP\ITempManager; use OCP\IURLGenerator; use Psr\Log\LoggerInterface; @@ -22,13 +24,17 @@ use Psr\Log\LoggerInterface; class Detection implements IMimeTypeDetector { private const CUSTOM_MIMETYPEMAPPING = 'mimetypemapping.json'; private const CUSTOM_MIMETYPEALIASES = 'mimetypealiases.json'; + private const CUSTOM_MIMETYPENAMES = 'mimetypenames.json'; - protected array $mimetypes = []; + /** @var array<list{string, string|null}> */ + protected array $mimeTypes = []; protected array $secureMimeTypes = []; - protected array $mimetypeIcons = []; + protected array $mimeTypeIcons = []; /** @var array<string,string> */ protected array $mimeTypeAlias = []; + /** @var array<string,string> */ + protected array $mimeTypesNames = []; public function __construct( private IURLGenerator $urlGenerator, @@ -39,40 +45,47 @@ class Detection implements IMimeTypeDetector { } /** - * Add an extension -> mimetype mapping + * Add an extension -> MIME type mapping * - * $mimetype is the assumed correct mime type + * $mimeType is the assumed correct mime type * The optional $secureMimeType is an alternative to send to send * to avoid potential XSS. * * @param string $extension - * @param string $mimetype + * @param string $mimeType * @param string|null $secureMimeType */ - public function registerType(string $extension, - string $mimetype, + public function registerType( + string $extension, + string $mimeType, ?string $secureMimeType = null): void { - $this->mimetypes[$extension] = [$mimetype, $secureMimeType]; - $this->secureMimeTypes[$mimetype] = $secureMimeType ?: $mimetype; + // Make sure the extension is a string + // https://github.com/nextcloud/server/issues/42902 + $this->mimeTypes[$extension] = [$mimeType, $secureMimeType]; + $this->secureMimeTypes[$mimeType] = $secureMimeType ?? $mimeType; } /** - * Add an array of extension -> mimetype mappings + * Add an array of extension -> MIME type mappings * - * The mimetype value is in itself an array where the first index is - * the assumed correct mimetype and the second is either a secure alternative + * The mimeType value is in itself an array where the first index is + * the assumed correct mimeType and the second is either a secure alternative * or null if the correct is considered secure. * * @param array $types */ public function registerTypeArray(array $types): void { - $this->mimetypes = array_merge($this->mimetypes, $types); + // Register the types, + foreach ($types as $extension => $mimeType) { + $this->registerType((string)$extension, $mimeType[0], $mimeType[1] ?? null); + } - // Update the alternative mimetypes to avoid having to look them up each time. - foreach ($this->mimetypes as $extension => $mimeType) { - if (str_starts_with($extension, '_comment')) { + // Update the alternative mimeTypes to avoid having to look them up each time. + foreach ($this->mimeTypes as $extension => $mimeType) { + if (str_starts_with((string)$extension, '_comment')) { continue; } + $this->secureMimeTypes[$mimeType[0]] = $mimeType[1] ?? $mimeType[0]; if (isset($mimeType[1])) { $this->secureMimeTypes[$mimeType[1]] = $mimeType[1]; @@ -93,7 +106,7 @@ class Detection implements IMimeTypeDetector { } /** - * Add the mimetype aliases if they are not yet present + * Add the MIME type aliases if they are not yet present */ private function loadAliases(): void { if (!empty($this->mimeTypeAlias)) { @@ -119,29 +132,48 @@ class Detection implements IMimeTypeDetector { } /** - * Add mimetype mappings if they are not yet present + * Add MIME type mappings if they are not yet present */ private function loadMappings(): void { - if (!empty($this->mimetypes)) { + if (!empty($this->mimeTypes)) { return; } - $mimetypeMapping = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypemapping.dist.json'), true); - $mimetypeMapping = $this->loadCustomDefinitions(self::CUSTOM_MIMETYPEMAPPING, $mimetypeMapping); + $mimeTypeMapping = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypemapping.dist.json'), true); + $mimeTypeMapping = $this->loadCustomDefinitions(self::CUSTOM_MIMETYPEMAPPING, $mimeTypeMapping); - $this->registerTypeArray($mimetypeMapping); + $this->registerTypeArray($mimeTypeMapping); } /** - * @return array + * @return array<list{string, string|null}> */ public function getAllMappings(): array { $this->loadMappings(); - return $this->mimetypes; + return $this->mimeTypes; + } + + private function loadNamings(): void { + if (!empty($this->mimeTypesNames)) { + return; + } + + $mimeTypeMapping = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypenames.dist.json'), true); + $mimeTypeMapping = $this->loadCustomDefinitions(self::CUSTOM_MIMETYPENAMES, $mimeTypeMapping); + + $this->mimeTypesNames = $mimeTypeMapping; + } + + /** + * @return array<string,string> + */ + public function getAllNamings(): array { + $this->loadNamings(); + return $this->mimeTypesNames; } /** - * detect mimetype only based on filename, content of file is not used + * detect MIME type only based on filename, content of file is not used * * @param string $path * @return string @@ -163,8 +195,8 @@ class Detection implements IMimeTypeDetector { $extension = strrchr($fileName, '.'); if ($extension !== false) { $extension = strtolower($extension); - $extension = substr($extension, 1); //remove leading . - return $this->mimetypes[$extension][0] ?? 'application/octet-stream'; + $extension = substr($extension, 1); // remove leading . + return $this->mimeTypes[$extension][0] ?? 'application/octet-stream'; } } @@ -172,7 +204,8 @@ class Detection implements IMimeTypeDetector { } /** - * detect mimetype only based on the content of file + * Detect MIME type only based on the content of file. + * * @param string $path * @return string * @since 18.0.0 @@ -185,14 +218,10 @@ class Detection implements IMimeTypeDetector { return 'httpd/unix-directory'; } - if (function_exists('finfo_open') - && function_exists('finfo_file') - && $finfo = finfo_open(FILEINFO_MIME)) { - $info = @finfo_file($finfo, $path); - finfo_close($finfo); - if ($info) { - $info = strtolower($info); - $mimeType = str_contains($info, ';') ? substr($info, 0, strpos($info, ';')) : $info; + if (class_exists(finfo::class)) { + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mimeType = @$finfo->file($path); + if ($mimeType) { $mimeType = $this->getSecureMimeType($mimeType); if ($mimeType !== 'application/octet-stream') { return $mimeType; @@ -208,7 +237,7 @@ class Detection implements IMimeTypeDetector { if (function_exists('mime_content_type')) { // use mime magic extension if available $mimeType = mime_content_type($path); - if ($mimeType !== false) { + if ($mimeType) { $mimeType = $this->getSecureMimeType($mimeType); if ($mimeType !== 'application/octet-stream') { return $mimeType; @@ -216,28 +245,30 @@ class Detection implements IMimeTypeDetector { } } - if (\OC_Helper::canExecute('file')) { + $binaryFinder = \OCP\Server::get(IBinaryFinder::class); + $program = $binaryFinder->findBinaryPath('file'); + if ($program !== false) { // it looks like we have a 'file' command, // lets see if it does have mime support $path = escapeshellarg($path); - $fp = popen("test -f $path && file -b --mime-type $path", 'r'); - $mimeType = fgets($fp); - pclose($fp); - - if ($mimeType !== false) { - //trim the newline - $mimeType = trim($mimeType); - $mimeType = $this->getSecureMimeType($mimeType); - if ($mimeType !== 'application/octet-stream') { + $fp = popen("test -f $path && $program -b --mime-type $path", 'r'); + if ($fp !== false) { + $mimeType = fgets($fp); + pclose($fp); + if ($mimeType) { + //trim the newline + $mimeType = trim($mimeType); + $mimeType = $this->getSecureMimeType($mimeType); return $mimeType; } } } + return 'application/octet-stream'; } /** - * detect mimetype based on both filename and content + * Detect MIME type based on both filename and content * * @param string $path * @return string @@ -253,29 +284,31 @@ class Detection implements IMimeTypeDetector { } /** - * detect mimetype based on the content of a string + * Detect MIME type based on the content of a string * * @param string $data * @return string */ public function detectString($data): string { - if (function_exists('finfo_open') && function_exists('finfo_file')) { - $finfo = finfo_open(FILEINFO_MIME); - $info = finfo_buffer($finfo, $data); - return str_contains($info, ';') ? substr($info, 0, strpos($info, ';')) : $info; + if (class_exists(finfo::class)) { + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mimeType = $finfo->buffer($data); + if ($mimeType) { + return $mimeType; + } } - $tmpFile = \OC::$server->getTempManager()->getTemporaryFile(); + $tmpFile = \OCP\Server::get(ITempManager::class)->getTemporaryFile(); $fh = fopen($tmpFile, 'wb'); fwrite($fh, $data, 8024); fclose($fh); - $mime = $this->detect($tmpFile); + $mimeType = $this->detect($tmpFile); unset($tmpFile); - return $mime; + return $mimeType; } /** - * Get a secure mimetype that won't expose potential XSS. + * Get a secure MIME type that won't expose potential XSS. * * @param string $mimeType * @return string @@ -288,57 +321,56 @@ class Detection implements IMimeTypeDetector { /** * Get path to the icon of a file type - * @param string $mimetype the MIME type + * @param string $mimeType the MIME type * @return string the url */ - public function mimeTypeIcon($mimetype): string { + public function mimeTypeIcon($mimeType): string { $this->loadAliases(); - while (isset($this->mimeTypeAlias[$mimetype])) { - $mimetype = $this->mimeTypeAlias[$mimetype]; + while (isset($this->mimeTypeAlias[$mimeType])) { + $mimeType = $this->mimeTypeAlias[$mimeType]; } - if (isset($this->mimetypeIcons[$mimetype])) { - return $this->mimetypeIcons[$mimetype]; + if (isset($this->mimeTypeIcons[$mimeType])) { + return $this->mimeTypeIcons[$mimeType]; } // Replace slash and backslash with a minus - $icon = str_replace(['/', '\\'], '-', $mimetype); + $icon = str_replace(['/', '\\'], '-', $mimeType); // Is it a dir? - if ($mimetype === 'dir') { - $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder.svg'); - return $this->mimetypeIcons[$mimetype]; + if ($mimeType === 'dir') { + $this->mimeTypeIcons[$mimeType] = $this->urlGenerator->imagePath('core', 'filetypes/folder.svg'); + return $this->mimeTypeIcons[$mimeType]; } - if ($mimetype === 'dir-shared') { - $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder-shared.svg'); - return $this->mimetypeIcons[$mimetype]; + if ($mimeType === 'dir-shared') { + $this->mimeTypeIcons[$mimeType] = $this->urlGenerator->imagePath('core', 'filetypes/folder-shared.svg'); + return $this->mimeTypeIcons[$mimeType]; } - if ($mimetype === 'dir-external') { - $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder-external.svg'); - return $this->mimetypeIcons[$mimetype]; + if ($mimeType === 'dir-external') { + $this->mimeTypeIcons[$mimeType] = $this->urlGenerator->imagePath('core', 'filetypes/folder-external.svg'); + return $this->mimeTypeIcons[$mimeType]; } // Icon exists? try { - $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/' . $icon . '.svg'); - return $this->mimetypeIcons[$mimetype]; + $this->mimeTypeIcons[$mimeType] = $this->urlGenerator->imagePath('core', 'filetypes/' . $icon . '.svg'); + return $this->mimeTypeIcons[$mimeType]; } catch (\RuntimeException $e) { // Specified image not found } // Try only the first part of the filetype - if (strpos($icon, '-')) { $mimePart = substr($icon, 0, strpos($icon, '-')); try { - $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/' . $mimePart . '.svg'); - return $this->mimetypeIcons[$mimetype]; + $this->mimeTypeIcons[$mimeType] = $this->urlGenerator->imagePath('core', 'filetypes/' . $mimePart . '.svg'); + return $this->mimeTypeIcons[$mimeType]; } catch (\RuntimeException $e) { - // Image for the first part of the mimetype not found + // Image for the first part of the MIME type not found } } - $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/file.svg'); - return $this->mimetypeIcons[$mimetype]; + $this->mimeTypeIcons[$mimeType] = $this->urlGenerator->imagePath('core', 'filetypes/file.svg'); + return $this->mimeTypeIcons[$mimeType]; } } diff --git a/lib/private/Files/Type/Loader.php b/lib/private/Files/Type/Loader.php index 247acf0141a..5fbe4139759 100644 --- a/lib/private/Files/Type/Loader.php +++ b/lib/private/Files/Type/Loader.php @@ -21,8 +21,6 @@ use OCP\IDBConnection; class Loader implements IMimeTypeLoader { use TTransactional; - private IDBConnection $dbConnection; - /** @psalm-var array<int, string> */ protected array $mimetypes; @@ -32,8 +30,9 @@ class Loader implements IMimeTypeLoader { /** * @param IDBConnection $dbConnection */ - public function __construct(IDBConnection $dbConnection) { - $this->dbConnection = $dbConnection; + public function __construct( + private IDBConnection $dbConnection, + ) { $this->mimetypes = []; $this->mimetypeIds = []; } @@ -115,7 +114,7 @@ class Loader implements IMimeTypeLoader { throw new \Exception("Database threw an unique constraint on inserting a new mimetype, but couldn't return the ID for this very mimetype"); } - $mimetypeId = (int) $id; + $mimetypeId = (int)$id; } $this->mimetypes[$mimetypeId] = $mimetype; @@ -136,8 +135,8 @@ class Loader implements IMimeTypeLoader { $result->closeCursor(); foreach ($results as $row) { - $this->mimetypes[(int) $row['id']] = $row['mimetype']; - $this->mimetypeIds[$row['mimetype']] = (int) $row['id']; + $this->mimetypes[(int)$row['id']] = $row['mimetype']; + $this->mimetypeIds[$row['mimetype']] = (int)$row['id']; } } @@ -161,6 +160,6 @@ class Loader implements IMimeTypeLoader { $update->func()->lower('name'), $update->createNamedParameter('%' . $this->dbConnection->escapeLikeParameter('.' . $ext)) )); - return $update->execute(); + return $update->executeStatement(); } } diff --git a/lib/private/Files/Utils/PathHelper.php b/lib/private/Files/Utils/PathHelper.php index a6ae029b957..db1294bcc10 100644 --- a/lib/private/Files/Utils/PathHelper.php +++ b/lib/private/Files/Utils/PathHelper.php @@ -37,6 +37,8 @@ class PathHelper { if ($path === '' or $path === '/') { return '/'; } + // No null bytes + $path = str_replace(chr(0), '', $path); //no windows style slashes $path = str_replace('\\', '/', $path); //add leading slash diff --git a/lib/private/Files/Utils/Scanner.php b/lib/private/Files/Utils/Scanner.php index bcc54dea0dc..576cb66b3cf 100644 --- a/lib/private/Files/Utils/Scanner.php +++ b/lib/private/Files/Utils/Scanner.php @@ -23,11 +23,13 @@ use OCP\Files\Events\FileScannedEvent; use OCP\Files\Events\FolderScannedEvent; use OCP\Files\Events\NodeAddedToCache; use OCP\Files\Events\NodeRemovedFromCache; +use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; use OCP\Files\StorageNotAvailableException; use OCP\IDBConnection; use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; use Psr\Log\LoggerInterface; /** @@ -85,7 +87,7 @@ class Scanner extends PublicEmitter { * get all storages for $dir * * @param string $dir - * @return \OC\Files\Mount\MountPoint[] + * @return array<string, IMountPoint> */ protected function getMounts($dir) { //TODO: move to the node based fileapi once that's done @@ -96,8 +98,9 @@ class Scanner extends PublicEmitter { $mounts = $mountManager->findIn($dir); $mounts[] = $mountManager->find($dir); $mounts = array_reverse($mounts); //start with the mount of $dir + $mountPoints = array_map(fn ($mount) => $mount->getMountPoint(), $mounts); - return $mounts; + return array_combine($mountPoints, $mounts); } /** @@ -106,6 +109,7 @@ class Scanner extends PublicEmitter { * @param \OC\Files\Mount\MountPoint $mount */ protected function attachListener($mount) { + /** @var \OC\Files\Cache\Scanner $scanner */ $scanner = $mount->getStorage()->getScanner(); $scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function ($path) use ($mount) { $this->emit('\OC\Files\Utils\Scanner', 'scanFile', [$mount->getMountPoint() . $path]); @@ -145,6 +149,7 @@ class Scanner extends PublicEmitter { continue; } + /** @var \OC\Files\Cache\Scanner $scanner */ $scanner = $storage->getScanner(); $this->attachListener($mount); @@ -200,7 +205,10 @@ class Scanner extends PublicEmitter { foreach (['', 'files'] as $path) { if (!$storage->isCreatable($path)) { $fullPath = $storage->getSourcePath($path); - if (!$storage->is_dir($path) && $storage->getCache()->inCache($path)) { + if (isset($mounts[$mount->getMountPoint() . $path . '/'])) { + // /<user>/files is overwritten by a mountpoint, so this check is irrelevant + break; + } elseif (!$storage->is_dir($path) && $storage->getCache()->inCache($path)) { throw new NotFoundException("User folder $fullPath exists in cache but not on disk"); } elseif ($storage->is_dir($path)) { $ownerUid = fileowner($fullPath); @@ -221,6 +229,7 @@ class Scanner extends PublicEmitter { continue; } $relativePath = $mount->getInternalPath($dir); + /** @var \OC\Files\Cache\Scanner $scanner */ $scanner = $storage->getScanner(); $scanner->setUseTransactions(false); $this->attachListener($mount); @@ -252,7 +261,15 @@ class Scanner extends PublicEmitter { try { $propagator = $storage->getPropagator(); $propagator->beginBatch(); - $scanner->scan($relativePath, $recursive, \OC\Files\Cache\Scanner::REUSE_ETAG | \OC\Files\Cache\Scanner::REUSE_SIZE); + try { + $scanner->scan($relativePath, $recursive, \OC\Files\Cache\Scanner::REUSE_ETAG | \OC\Files\Cache\Scanner::REUSE_SIZE); + } catch (LockedException $e) { + if (is_string($e->getReadablePath()) && str_starts_with($e->getReadablePath(), 'scanner::')) { + throw new LockedException("scanner::$dir", $e, $e->getExistingLock()); + } else { + throw $e; + } + } $cache = $storage->getCache(); if ($cache instanceof Cache) { // only re-calculate for the root folder we scanned, anything below that is taken care of by the scanner diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 0150a3e117a..a852f453963 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -10,12 +10,14 @@ namespace OC\Files; use Icewind\Streams\CallbackWrapper; use OC\Files\Mount\MoveableMount; use OC\Files\Storage\Storage; +use OC\Files\Storage\Wrapper\Quota; use OC\Share\Share; use OC\User\LazyUser; use OC\User\Manager as UserManager; use OC\User\User; use OCA\Files_Sharing\SharedMount; use OCP\Constants; +use OCP\Files; use OCP\Files\Cache\ICacheEntry; use OCP\Files\ConnectionLostException; use OCP\Files\EmptyFileNameException; @@ -24,10 +26,13 @@ use OCP\Files\ForbiddenException; use OCP\Files\InvalidCharacterInPathException; use OCP\Files\InvalidDirectoryException; use OCP\Files\InvalidPathException; +use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; use OCP\Files\ReservedWordException; use OCP\IUser; +use OCP\IUserManager; +use OCP\L10N\IFactory; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; use OCP\Server; @@ -221,7 +226,7 @@ class View { $relPath = '/' . $pathParts[3]; $this->lockFile($relPath, ILockingProvider::LOCK_SHARED, true); \OC_Hook::emit( - Filesystem::CLASSNAME, "umount", + Filesystem::CLASSNAME, 'umount', [Filesystem::signal_param_path => $relPath] ); $this->changeLock($relPath, ILockingProvider::LOCK_EXCLUSIVE, true); @@ -229,7 +234,7 @@ class View { $this->changeLock($relPath, ILockingProvider::LOCK_SHARED, true); if ($result) { \OC_Hook::emit( - Filesystem::CLASSNAME, "post_umount", + Filesystem::CLASSNAME, 'post_umount', [Filesystem::signal_param_path => $relPath] ); } @@ -272,6 +277,12 @@ class View { } } + protected function copyUpdate(Storage $sourceStorage, Storage $targetStorage, string $sourceInternalPath, string $targetInternalPath): void { + if ($this->updaterEnabled) { + $targetStorage->getUpdater()->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + } + } + /** * @param string $path * @return bool|mixed @@ -598,13 +609,13 @@ class View { $this->lockFile($path, ILockingProvider::LOCK_SHARED); $exists = $this->file_exists($path); - $run = true; if ($this->shouldEmitHooks($path)) { + $run = true; $this->emit_file_hooks_pre($exists, $path, $run); - } - if (!$run) { - $this->unlockFile($path, ILockingProvider::LOCK_SHARED); - return false; + if (!$run) { + $this->unlockFile($path, ILockingProvider::LOCK_SHARED); + return false; + } } try { @@ -619,7 +630,7 @@ class View { [$storage, $internalPath] = $this->resolvePath($path); $target = $storage->fopen($internalPath, 'w'); if ($target) { - [, $result] = \OC_Helper::streamCopy($data, $target); + [, $result] = Files::streamCopy($data, $target, true); fclose($target); fclose($data); @@ -688,18 +699,24 @@ class View { * * @param string $source source path * @param string $target target path + * @param array $options * * @return bool|mixed * @throws LockedException */ - public function rename($source, $target) { + public function rename($source, $target, array $options = []) { + $checkSubMounts = $options['checkSubMounts'] ?? true; + $absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source)); $absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target)); if (str_starts_with($absolutePath2, $absolutePath1 . '/')) { - throw new ForbiddenException("Moving a folder into a child folder is forbidden", false); + throw new ForbiddenException('Moving a folder into a child folder is forbidden', false); } + /** @var IMountManager $mountManager */ + $mountManager = \OC::$server->get(IMountManager::class); + $targetParts = explode('/', $absolutePath2); $targetUser = $targetParts[1] ?? null; $result = false; @@ -716,6 +733,12 @@ class View { return false; } + try { + $this->verifyPath(dirname($target), basename($target)); + } catch (InvalidPathException) { + return false; + } + $this->lockFile($source, ILockingProvider::LOCK_SHARED, true); try { $this->lockFile($target, ILockingProvider::LOCK_SHARED, true); @@ -739,8 +762,6 @@ class View { } } if ($run) { - $this->verifyPath(dirname($target), basename($target)); - $manager = Filesystem::getMountManager(); $mount1 = $this->getMount($source); $mount2 = $this->getMount($target); @@ -753,24 +774,28 @@ class View { try { $this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true); + if ($checkSubMounts) { + $movedMounts = $mountManager->findIn($this->getAbsolutePath($source)); + } else { + $movedMounts = []; + } + if ($internalPath1 === '') { - if ($mount1 instanceof MoveableMount) { - $sourceParentMount = $this->getMount(dirname($source)); - if ($sourceParentMount === $mount2 && $this->targetIsNotShared($targetUser, $absolutePath2)) { - /** - * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1 - */ - $sourceMountPoint = $mount1->getMountPoint(); - $result = $mount1->moveMount($absolutePath2); - $manager->moveMount($sourceMountPoint, $mount1->getMountPoint()); - } else { - $result = false; - } - } else { - $result = false; - } + $sourceParentMount = $this->getMount(dirname($source)); + $movedMounts[] = $mount1; + $this->validateMountMove($movedMounts, $sourceParentMount, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2)); + /** + * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1 + */ + $sourceMountPoint = $mount1->getMountPoint(); + $result = $mount1->moveMount($absolutePath2); + $manager->moveMount($sourceMountPoint, $mount1->getMountPoint()); + // moving a file/folder within the same mount point } elseif ($storage1 === $storage2) { + if (count($movedMounts) > 0) { + $this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2)); + } if ($storage1) { $result = $storage1->rename($internalPath1, $internalPath2); } else { @@ -778,6 +803,9 @@ class View { } // moving a file/folder between storages (from $storage1 to $storage2) } else { + if (count($movedMounts) > 0) { + $this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2)); + } $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2); } @@ -828,6 +856,56 @@ class View { } /** + * @throws ForbiddenException + */ + private function validateMountMove(array $mounts, IMountPoint $sourceMount, IMountPoint $targetMount, bool $targetIsShared): void { + $targetPath = $this->getRelativePath($targetMount->getMountPoint()); + if ($targetPath) { + $targetPath = trim($targetPath, '/'); + } else { + $targetPath = $targetMount->getMountPoint(); + } + + $l = \OC::$server->get(IFactory::class)->get('files'); + foreach ($mounts as $mount) { + $sourcePath = $this->getRelativePath($mount->getMountPoint()); + if ($sourcePath) { + $sourcePath = trim($sourcePath, '/'); + } else { + $sourcePath = $mount->getMountPoint(); + } + + if (!$mount instanceof MoveableMount) { + throw new ForbiddenException($l->t('Storage %s cannot be moved', [$sourcePath]), false); + } + + if ($targetIsShared) { + if ($sourceMount instanceof SharedMount) { + throw new ForbiddenException($l->t('Moving a share (%s) into a shared folder is not allowed', [$sourcePath]), false); + } else { + throw new ForbiddenException($l->t('Moving a storage (%s) into a shared folder is not allowed', [$sourcePath]), false); + } + } + + if ($sourceMount !== $targetMount) { + if ($sourceMount instanceof SharedMount) { + if ($targetMount instanceof SharedMount) { + throw new ForbiddenException($l->t('Moving a share (%s) into another share (%s) is not allowed', [$sourcePath, $targetPath]), false); + } else { + throw new ForbiddenException($l->t('Moving a share (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false); + } + } else { + if ($targetMount instanceof SharedMount) { + throw new ForbiddenException($l->t('Moving a storage (%s) into a share (%s) is not allowed', [$sourcePath, $targetPath]), false); + } else { + throw new ForbiddenException($l->t('Moving a storage (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false); + } + } + } + } + } + + /** * Copy a file/folder from the source path to target path * * @param string $source source path @@ -860,7 +938,7 @@ class View { try { $exists = $this->file_exists($target); - if ($this->shouldEmitHooks()) { + if ($this->shouldEmitHooks($target)) { \OC_Hook::emit( Filesystem::CLASSNAME, Filesystem::signal_copy, @@ -893,12 +971,14 @@ class View { $result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2); } - $this->writeUpdate($storage2, $internalPath2); + if ($result) { + $this->copyUpdate($storage1, $storage2, $internalPath1, $internalPath2); + } $this->changeLock($target, ILockingProvider::LOCK_SHARED); $lockTypePath2 = ILockingProvider::LOCK_SHARED; - if ($this->shouldEmitHooks() && $result !== false) { + if ($this->shouldEmitHooks($target) && $result !== false) { \OC_Hook::emit( Filesystem::CLASSNAME, Filesystem::signal_post_copy, @@ -1326,7 +1406,7 @@ class View { * * @param string $path * @param bool|string $includeMountPoints true to add mountpoint sizes, - * 'ext' to add only ext storage mount point sizes. Defaults to true. + * 'ext' to add only ext storage mount point sizes. Defaults to true. * @return \OC\Files\FileInfo|false False if file does not exist */ public function getFileInfo($path, $includeMountPoints = true) { @@ -1334,9 +1414,6 @@ class View { if (!Filesystem::isValidPath($path)) { return false; } - if (Cache\Scanner::isPartialFile($path)) { - return $this->getPartFileInfo($path); - } $relativePath = $path; $path = Filesystem::normalizePath($this->fakeRoot . '/' . $path); @@ -1347,15 +1424,23 @@ class View { $data = $this->getCacheEntry($storage, $internalPath, $relativePath); if (!$data instanceof ICacheEntry) { + if (Cache\Scanner::isPartialFile($relativePath)) { + return $this->getPartFileInfo($relativePath); + } + return false; } if ($mount instanceof MoveableMount && $internalPath === '') { $data['permissions'] |= \OCP\Constants::PERMISSION_DELETE; } + if ($internalPath === '' && $data['name']) { + $data['name'] = basename($path); + } + $ownerId = $storage->getOwner($internalPath); $owner = null; - if ($ownerId !== null && $ownerId !== false) { + if ($ownerId !== false) { // ownerId might be null if files are accessed with an access token without file system access $owner = $this->getUserObjectForOwner($ownerId); } @@ -1383,8 +1468,7 @@ class View { public function addSubMounts(FileInfo $info, $extOnly = false): void { $mounts = Filesystem::getMountManager()->findIn($info->getPath()); $info->setSubMounts(array_filter($mounts, function (IMountPoint $mount) use ($extOnly) { - $subStorage = $mount->getStorage(); - return !($extOnly && $subStorage instanceof \OCA\Files_Sharing\SharedStorage); + return !($extOnly && $mount instanceof SharedMount); })); } @@ -1441,7 +1525,12 @@ class View { if ($sharingDisabled) { $content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE; } - $owner = $this->getUserObjectForOwner($storage->getOwner($content['path'])); + $ownerId = $storage->getOwner($content['path']); + if ($ownerId !== false) { + $owner = $this->getUserObjectForOwner($ownerId); + } else { + $owner = null; + } return new FileInfo($path . '/' . $content['name'], $storage, $content['path'], $content, $mount, $owner); }, $contents); $files = array_combine($fileNames, $fileInfos); @@ -1487,6 +1576,32 @@ class View { if ($pos = strpos($relativePath, '/')) { //mountpoint inside subfolder add size to the correct folder $entryName = substr($relativePath, 0, $pos); + + // Create parent folders if the mountpoint is inside a subfolder that doesn't exist yet + if (!isset($files[$entryName])) { + try { + [$storage, ] = $this->resolvePath($path . '/' . $entryName); + // make sure we can create the mountpoint folder, even if the user has a quota of 0 + if ($storage->instanceOfStorage(Quota::class)) { + $storage->enableQuota(false); + } + + if ($this->mkdir($path . '/' . $entryName) !== false) { + $info = $this->getFileInfo($path . '/' . $entryName); + if ($info !== false) { + $files[$entryName] = $info; + } + } + + if ($storage->instanceOfStorage(Quota::class)) { + $storage->enableQuota(true); + } + } catch (\Exception $e) { + // Creating the parent folder might not be possible, for example due to a lack of permissions. + $this->logger->debug('Failed to create non-existent parent', ['exception' => $e, 'path' => $path . '/' . $entryName]); + } + } + if (isset($files[$entryName])) { $files[$entryName]->addSubEntry($rootEntry, $mountPoint); } @@ -1509,7 +1624,12 @@ class View { $rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE; } - $owner = $this->getUserObjectForOwner($subStorage->getOwner('')); + $ownerId = $subStorage->getOwner(''); + if ($ownerId !== false) { + $owner = $this->getUserObjectForOwner($ownerId); + } else { + $owner = null; + } $files[$rootEntry->getName()] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner); } } @@ -1626,7 +1746,12 @@ class View { $internalPath = $result['path']; $path = $mountPoint . $result['path']; $result['path'] = substr($mountPoint . $result['path'], $rootLength); - $owner = $userManager->get($storage->getOwner($internalPath)); + $ownerId = $storage->getOwner($internalPath); + if ($ownerId !== false) { + $owner = $userManager->get($ownerId); + } else { + $owner = null; + } $files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner); } } @@ -1645,7 +1770,12 @@ class View { $internalPath = $result['path']; $result['path'] = rtrim($relativeMountPoint . $result['path'], '/'); $path = rtrim($mountPoint . $internalPath, '/'); - $owner = $userManager->get($storage->getOwner($internalPath)); + $ownerId = $storage->getOwner($internalPath); + if ($ownerId !== false) { + $owner = $userManager->get($ownerId); + } else { + $owner = null; + } $files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner); } } @@ -1658,11 +1788,9 @@ class View { /** * Get the owner for a file or folder * - * @param string $path - * @return string the user id of the owner * @throws NotFoundException */ - public function getOwner($path) { + public function getOwner(string $path): string { $info = $this->getFileInfo($path); if (!$info) { throw new NotFoundException($path . ' not found while trying to get owner'); @@ -1700,43 +1828,25 @@ class View { * @return string * @throws NotFoundException */ - public function getPath($id, ?int $storageId = null) { + public function getPath($id, ?int $storageId = null): string { $id = (int)$id; - $manager = Filesystem::getMountManager(); - $mounts = $manager->findIn($this->fakeRoot); - $mounts[] = $manager->find($this->fakeRoot); - $mounts = array_filter($mounts); - // reverse the array, so we start with the storage this view is in - // which is the most likely to contain the file we're looking for - $mounts = array_reverse($mounts); - - // put non-shared mounts in front of the shared mount - // this prevents unneeded recursion into shares - usort($mounts, function (IMountPoint $a, IMountPoint $b) { - return $a instanceof SharedMount && (!$b instanceof SharedMount) ? 1 : -1; - }); + $rootFolder = Server::get(Files\IRootFolder::class); - if (!is_null($storageId)) { - $mounts = array_filter($mounts, function (IMountPoint $mount) use ($storageId) { - return $mount->getNumericStorageId() === $storageId; - }); + $node = $rootFolder->getFirstNodeByIdInPath($id, $this->getRoot()); + if ($node) { + if ($storageId === null || $storageId === $node->getStorage()->getCache()->getNumericStorageId()) { + return $this->getRelativePath($node->getPath()) ?? ''; + } + } else { + throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id)); } - foreach ($mounts as $mount) { - /** - * @var \OC\Files\Mount\MountPoint $mount - */ - if ($mount->getStorage()) { - $cache = $mount->getStorage()->getCache(); - $internalPath = $cache->getPathById($id); - if (is_string($internalPath)) { - $fullPath = $mount->getMountPoint() . $internalPath; - if (!is_null($path = $this->getRelativePath($fullPath))) { - return $path; - } - } + foreach ($rootFolder->getByIdInPath($id, $this->getRoot()) as $node) { + if ($storageId === $node->getStorage()->getCache()->getNumericStorageId()) { + return $this->getRelativePath($node->getPath()) ?? ''; } } + throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id)); } @@ -1776,7 +1886,8 @@ class View { }, $providers)); foreach ($shares as $share) { - if (str_starts_with($targetPath, $share->getNode()->getPath())) { + $sharedPath = $share->getNode()->getPath(); + if ($targetPath === $sharedPath || str_starts_with($targetPath, $sharedPath . '/')) { $this->logger->debug( 'It is not allowed to move one mount point into a shared folder', ['app' => 'files']); @@ -1794,7 +1905,12 @@ class View { $mount = $this->getMount($path); $storage = $mount->getStorage(); $internalPath = $mount->getInternalPath($this->getAbsolutePath($path)); - $owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath)); + $ownerId = $storage->getOwner($internalPath); + if ($ownerId !== false) { + $owner = Server::get(IUserManager::class)->get($ownerId); + } else { + $owner = null; + } return new FileInfo( $this->getAbsolutePath($path), $storage, @@ -1817,22 +1933,39 @@ class View { /** * @param string $path * @param string $fileName + * @param bool $readonly Check only if the path is allowed for read-only access * @throws InvalidPathException */ - public function verifyPath($path, $fileName): void { + public function verifyPath($path, $fileName, $readonly = false): void { + // All of the view's functions disallow '..' in the path so we can short cut if the path is invalid + if (!Filesystem::isValidPath($path ?: '/')) { + $l = \OCP\Util::getL10N('lib'); + throw new InvalidPathException($l->t('Path contains invalid segments')); + } + + // Short cut for read-only validation + if ($readonly) { + $validator = Server::get(FilenameValidator::class); + if ($validator->isForbidden($fileName)) { + $l = \OCP\Util::getL10N('lib'); + throw new InvalidPathException($l->t('Filename is a reserved word')); + } + return; + } + try { /** @type \OCP\Files\Storage $storage */ [$storage, $internalPath] = $this->resolvePath($path); $storage->verifyPath($internalPath, $fileName); } catch (ReservedWordException $ex) { $l = \OCP\Util::getL10N('lib'); - throw new InvalidPathException($l->t('File name is a reserved word')); + throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename is a reserved word')); } catch (InvalidCharacterInPathException $ex) { $l = \OCP\Util::getL10N('lib'); - throw new InvalidPathException($l->t('File name contains at least one invalid character')); + throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename contains at least one invalid character')); } catch (FileNameTooLongException $ex) { $l = \OCP\Util::getL10N('lib'); - throw new InvalidPathException($l->t('File name is too long')); + throw new InvalidPathException($l->t('Filename is too long')); } catch (InvalidDirectoryException $ex) { $l = \OCP\Util::getL10N('lib'); throw new InvalidPathException($l->t('Dot files are not allowed')); @@ -1874,7 +2007,7 @@ class View { * * @param string $absolutePath absolute path * @param bool $useParentMount true to return parent mount instead of whatever - * is mounted directly on the given path, false otherwise + * is mounted directly on the given path, false otherwise * @return IMountPoint mount point for which to apply locks */ private function getMountForLock(string $absolutePath, bool $useParentMount = false): IMountPoint { @@ -1920,9 +2053,9 @@ class View { ); } } catch (LockedException $e) { - // rethrow with the a human-readable path + // rethrow with the human-readable path throw new LockedException( - $this->getPathRelativeToFiles($absolutePath), + $path, $e, $e->getExistingLock() ); @@ -1960,20 +2093,12 @@ class View { ); } } catch (LockedException $e) { - try { - // rethrow with the a human-readable path - throw new LockedException( - $this->getPathRelativeToFiles($absolutePath), - $e, - $e->getExistingLock() - ); - } catch (\InvalidArgumentException $ex) { - throw new LockedException( - $absolutePath, - $ex, - $e->getExistingLock() - ); - } + // rethrow with the a human-readable path + throw new LockedException( + $path, + $e, + $e->getExistingLock() + ); } return true; @@ -2088,7 +2213,7 @@ class View { * @param string $absolutePath absolute path which is under "files" * * @return string path relative to "files" with trimmed slashes or null - * if the path was NOT relative to files + * if the path was NOT relative to files * * @throws \InvalidArgumentException if the given path was not under "files" * @since 8.1.0 |