diff --git a/lib/private/Files/Cache/Updater.php b/lib/private/Files/Cache/Updater.php index b3a97aeda7c5d..1170341f71aed 100644 --- a/lib/private/Files/Cache/Updater.php +++ b/lib/private/Files/Cache/Updater.php @@ -123,6 +123,16 @@ public function remove(string $path): void { #[Override] public function renameFromStorage(IStorage $sourceStorage, string $source, string $target): void { $this->copyOrRenameFromStorage($sourceStorage, $source, $target, function (ICache $sourceCache) use ($sourceStorage, $source, $target): void { + $parent = dirname($target); + if ($parent === '.') { + $parent = ''; + } + if (!$this->cache->inCache($parent)) { + // scan the parent first, moving the entry below a parent that is not in + // the cache would hide it from folder listings until the next scan + $this->scanner->scan($parent, Scanner::SCAN_SHALLOW, -1, false); + } + // Remove existing cache entry to no reuse the fileId. if ($this->cache->inCache($target)) { $this->cache->remove($target); @@ -181,6 +191,12 @@ private function copyOrRenameFromStorage(IStorage $sourceStorage, string $source $isDir = $sourceInfo->getMimeType() === FileInfo::MIMETYPE_FOLDER; } else { + if (!$this->storage->instanceOfStorage(ObjectStoreStorage::class) && !$this->cache->inCache($target)) { + // the source was not in the cache, so the operation could not transfer + // an entry to the target. Scan the target to not leave it invisible + // until the next scan + $this->scanner->scan($target, Scanner::SCAN_SHALLOW, -1, false); + } $isDir = $this->storage->is_dir($target); } diff --git a/lib/private/legacy/OC_Hook.php b/lib/private/legacy/OC_Hook.php index 0def846b7cfd8..9d0a7bf34c7d2 100644 --- a/lib/private/legacy/OC_Hook.php +++ b/lib/private/legacy/OC_Hook.php @@ -85,7 +85,9 @@ public static function emit($signalClass, $signalName, $params = []) { foreach (self::$registered[$signalClass][$signalName] as $i) { try { call_user_func([ $i['class'], $i['name'] ], $params); - } catch (Exception $e) { + } catch (Throwable $e) { + // a failing hook handler must not break the operation that emitted + // the signal, this includes Errors like TypeError self::$thrownExceptions[] = $e; Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); if ($e instanceof HintException) { diff --git a/tests/lib/Files/Cache/UpdaterTest.php b/tests/lib/Files/Cache/UpdaterTest.php index a2db97c9a7e44..f4b51a58e6633 100644 --- a/tests/lib/Files/Cache/UpdaterTest.php +++ b/tests/lib/Files/Cache/UpdaterTest.php @@ -256,6 +256,48 @@ public function testMoveCrossStorage(): void { $this->assertEquals($cached['fileid'], $cachedTarget['fileid']); } + public function testMoveCrossStorageMissingTargetParent(): void { + $storage2 = new Temporary([]); + $cache2 = $storage2->getCache(); + Filesystem::mount($storage2, [], '/bar'); + $this->storage->file_put_contents('foo.txt', 'qwerty'); + $this->updater->update('foo.txt'); + $cached = $this->cache->get('foo.txt'); + + // the target parent exists on disk but is missing from the cache + $storage2->mkdir('sub'); + $this->assertFalse($cache2->inCache('sub')); + + $storage2->moveFromStorage($this->storage, 'foo.txt', 'sub/bar.txt'); + $storage2->getUpdater()->renameFromStorage($this->storage, 'foo.txt', 'sub/bar.txt'); + + $this->assertFalse($this->cache->inCache('foo.txt')); + $this->assertTrue($cache2->inCache('sub')); + $this->assertTrue($cache2->inCache('sub/bar.txt')); + + // the moved entry is attached to the scanned parent instead of being orphaned + $cachedTarget = $cache2->get('sub/bar.txt'); + $this->assertEquals($cache2->getId('sub'), $cachedTarget['parent']); + $this->assertEquals($cached['fileid'], $cachedTarget['fileid']); + } + + public function testMoveCrossStorageSourceNotInCache(): void { + $storage2 = new Temporary([]); + $cache2 = $storage2->getCache(); + Filesystem::mount($storage2, [], '/bar'); + $this->storage->file_put_contents('foo.txt', 'qwerty'); + // the source was never scanned into the cache + $this->assertFalse($this->cache->inCache('foo.txt')); + + $storage2->moveFromStorage($this->storage, 'foo.txt', 'bar.txt'); + $storage2->getUpdater()->renameFromStorage($this->storage, 'foo.txt', 'bar.txt'); + + // the target still gets a cache entry + $this->assertTrue($cache2->inCache('bar.txt')); + $cachedTarget = $cache2->get('bar.txt'); + $this->assertEquals(6, $cachedTarget['size']); + } + public function testMoveFolderCrossStorage(): void { $storage2 = new Temporary([]); $cache2 = $storage2->getCache(); diff --git a/tests/lib/LegacyHookTest.php b/tests/lib/LegacyHookTest.php new file mode 100644 index 0000000000000..c606340265b92 --- /dev/null +++ b/tests/lib/LegacyHookTest.php @@ -0,0 +1,52 @@ +assertTrue(\OC_Hook::emit('LegacyHookTest', 'error')); + + $this->assertCount(1, \OC_Hook::$thrownExceptions); + $this->assertInstanceOf(\TypeError::class, \OC_Hook::$thrownExceptions[0]); + } + + public function testEmitRethrowsHintException(): void { + \OC_Hook::connect('LegacyHookTest', 'hint', self::class, 'throwHintException'); + + $this->expectException(HintException::class); + \OC_Hook::emit('LegacyHookTest', 'hint'); + } +}