aboutsummaryrefslogtreecommitdiffstats
path: root/tests/lib/Files/Cache/ScannerTest.php
diff options
context:
space:
mode:
Diffstat (limited to 'tests/lib/Files/Cache/ScannerTest.php')
-rw-r--r--tests/lib/Files/Cache/ScannerTest.php453
1 files changed, 453 insertions, 0 deletions
diff --git a/tests/lib/Files/Cache/ScannerTest.php b/tests/lib/Files/Cache/ScannerTest.php
new file mode 100644
index 00000000000..123c13893f7
--- /dev/null
+++ b/tests/lib/Files/Cache/ScannerTest.php
@@ -0,0 +1,453 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Files\Cache;
+
+use OC;
+use OC\Files\Cache\Cache;
+use OC\Files\Cache\CacheEntry;
+use OC\Files\Cache\Scanner;
+use OC\Files\Storage\Storage;
+use OC\Files\Storage\Temporary;
+use OCP\Files\Cache\IScanner;
+use OCP\IDBConnection;
+use OCP\Server;
+use Test\TestCase;
+
+/**
+ * Class ScannerTest
+ *
+ * @group DB
+ *
+ * @package Test\Files\Cache
+ */
+class ScannerTest extends TestCase {
+ private Storage $storage;
+ private Scanner $scanner;
+ private Cache $cache;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->storage = new Temporary([]);
+ $this->scanner = new Scanner($this->storage);
+ $this->cache = new Cache($this->storage);
+ }
+
+ protected function tearDown(): void {
+ $this->cache->clear();
+
+ parent::tearDown();
+ }
+
+ public function testFile(): void {
+ $data = "dummy file data\n";
+ $this->storage->file_put_contents('foo.txt', $data);
+ $this->scanner->scanFile('foo.txt');
+
+ $this->assertEquals($this->cache->inCache('foo.txt'), true);
+ $cachedData = $this->cache->get('foo.txt');
+ $this->assertEquals($cachedData['size'], strlen($data));
+ $this->assertEquals($cachedData['mimetype'], 'text/plain');
+ $this->assertNotEquals($cachedData['parent'], -1); //parent folders should be scanned automatically
+
+ $data = file_get_contents(OC::$SERVERROOT . '/core/img/logo/logo.png');
+ $this->storage->file_put_contents('foo.png', $data);
+ $this->scanner->scanFile('foo.png');
+
+ $this->assertEquals($this->cache->inCache('foo.png'), true);
+ $cachedData = $this->cache->get('foo.png');
+ $this->assertEquals($cachedData['size'], strlen($data));
+ $this->assertEquals($cachedData['mimetype'], 'image/png');
+ }
+
+ public function testFile4Byte(): void {
+ $data = "dummy file data\n";
+ $this->storage->file_put_contents('foo🙈.txt', $data);
+
+ if (Server::get(IDBConnection::class)->supports4ByteText()) {
+ $this->assertNotNull($this->scanner->scanFile('foo🙈.txt'));
+ $this->assertTrue($this->cache->inCache('foo🙈.txt'), true);
+
+ $cachedData = $this->cache->get('foo🙈.txt');
+ $this->assertEquals(strlen($data), $cachedData['size']);
+ $this->assertEquals('text/plain', $cachedData['mimetype']);
+ $this->assertNotEquals(-1, $cachedData['parent']); //parent folders should be scanned automatically
+ } else {
+ $this->assertNull($this->scanner->scanFile('foo🙈.txt'));
+ $this->assertFalse($this->cache->inCache('foo🙈.txt'), true);
+ }
+ }
+
+ public function testFileInvalidChars(): void {
+ $data = "dummy file data\n";
+ $this->storage->file_put_contents("foo\nbar.txt", $data);
+
+ $this->assertNull($this->scanner->scanFile("foo\nbar.txt"));
+ $this->assertFalse($this->cache->inCache("foo\nbar.txt"), true);
+ }
+
+ private function fillTestFolders() {
+ $textData = "dummy file data\n";
+ $imgData = file_get_contents(OC::$SERVERROOT . '/core/img/logo/logo.png');
+ $this->storage->mkdir('folder');
+ $this->storage->file_put_contents('foo.txt', $textData);
+ $this->storage->file_put_contents('foo.png', $imgData);
+ $this->storage->file_put_contents('folder/bar.txt', $textData);
+ }
+
+ public function testFolder(): void {
+ $this->fillTestFolders();
+
+ $this->scanner->scan('');
+ $this->assertEquals($this->cache->inCache(''), true);
+ $this->assertEquals($this->cache->inCache('foo.txt'), true);
+ $this->assertEquals($this->cache->inCache('foo.png'), true);
+ $this->assertEquals($this->cache->inCache('folder'), true);
+ $this->assertEquals($this->cache->inCache('folder/bar.txt'), true);
+
+ $cachedDataText = $this->cache->get('foo.txt');
+ $cachedDataText2 = $this->cache->get('foo.txt');
+ $cachedDataImage = $this->cache->get('foo.png');
+ $cachedDataFolder = $this->cache->get('');
+ $cachedDataFolder2 = $this->cache->get('folder');
+
+ $this->assertEquals($cachedDataImage['parent'], $cachedDataText['parent']);
+ $this->assertEquals($cachedDataFolder['fileid'], $cachedDataImage['parent']);
+ $this->assertEquals($cachedDataFolder['size'], $cachedDataImage['size'] + $cachedDataText['size'] + $cachedDataText2['size']);
+ $this->assertEquals($cachedDataFolder2['size'], $cachedDataText2['size']);
+ }
+
+ public function testShallow(): void {
+ $this->fillTestFolders();
+
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW);
+ $this->assertEquals($this->cache->inCache(''), true);
+ $this->assertEquals($this->cache->inCache('foo.txt'), true);
+ $this->assertEquals($this->cache->inCache('foo.png'), true);
+ $this->assertEquals($this->cache->inCache('folder'), true);
+ $this->assertEquals($this->cache->inCache('folder/bar.txt'), false);
+
+ $cachedDataFolder = $this->cache->get('');
+ $cachedDataFolder2 = $this->cache->get('folder');
+
+ $this->assertEquals(-1, $cachedDataFolder['size']);
+ $this->assertEquals(-1, $cachedDataFolder2['size']);
+
+ $this->scanner->scan('folder', IScanner::SCAN_SHALLOW);
+
+ $cachedDataFolder2 = $this->cache->get('folder');
+
+ $this->assertNotEquals($cachedDataFolder2['size'], -1);
+
+ $this->cache->correctFolderSize('folder');
+
+ $cachedDataFolder = $this->cache->get('');
+ $this->assertNotEquals($cachedDataFolder['size'], -1);
+ }
+
+ public function testBackgroundScan(): void {
+ $this->fillTestFolders();
+ $this->storage->mkdir('folder2');
+ $this->storage->file_put_contents('folder2/bar.txt', 'foobar');
+
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW);
+ $this->assertFalse($this->cache->inCache('folder/bar.txt'));
+ $this->assertFalse($this->cache->inCache('folder/2bar.txt'));
+ $cachedData = $this->cache->get('');
+ $this->assertEquals(-1, $cachedData['size']);
+
+ $this->scanner->backgroundScan();
+
+ $this->assertTrue($this->cache->inCache('folder/bar.txt'));
+ $this->assertTrue($this->cache->inCache('folder/bar.txt'));
+
+ $cachedData = $this->cache->get('');
+ $this->assertnotEquals(-1, $cachedData['size']);
+
+ $this->assertFalse($this->cache->getIncomplete());
+ }
+
+ public function testBackgroundScanOnlyRecurseIncomplete(): void {
+ $this->fillTestFolders();
+ $this->storage->mkdir('folder2');
+ $this->storage->file_put_contents('folder2/bar.txt', 'foobar');
+
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW);
+ $this->assertFalse($this->cache->inCache('folder/bar.txt'));
+ $this->assertFalse($this->cache->inCache('folder/2bar.txt'));
+ $this->assertFalse($this->cache->inCache('folder2/bar.txt'));
+ $this->cache->put('folder2', ['size' => 1]); // mark as complete
+
+ $cachedData = $this->cache->get('');
+ $this->assertEquals(-1, $cachedData['size']);
+
+ $this->scanner->scan('', IScanner::SCAN_RECURSIVE_INCOMPLETE, IScanner::REUSE_ETAG | IScanner::REUSE_SIZE);
+
+ $this->assertTrue($this->cache->inCache('folder/bar.txt'));
+ $this->assertTrue($this->cache->inCache('folder/bar.txt'));
+ $this->assertFalse($this->cache->inCache('folder2/bar.txt'));
+
+ $cachedData = $this->cache->get('');
+ $this->assertNotEquals(-1, $cachedData['size']);
+
+ $this->assertFalse($this->cache->getIncomplete());
+ }
+
+ public function testBackgroundScanNestedIncompleteFolders(): void {
+ $this->storage->mkdir('folder');
+ $this->scanner->backgroundScan();
+
+ $this->storage->mkdir('folder/subfolder1');
+ $this->storage->mkdir('folder/subfolder2');
+
+ $this->storage->mkdir('folder/subfolder1/subfolder3');
+ $this->cache->put('folder', ['size' => -1]);
+ $this->cache->put('folder/subfolder1', ['size' => -1]);
+
+ // do a scan to get the folders into the cache.
+ $this->scanner->backgroundScan();
+
+ $this->assertTrue($this->cache->inCache('folder/subfolder1/subfolder3'));
+
+ $this->storage->file_put_contents('folder/subfolder1/bar1.txt', 'foobar');
+ $this->storage->file_put_contents('folder/subfolder1/subfolder3/bar3.txt', 'foobar');
+ $this->storage->file_put_contents('folder/subfolder2/bar2.txt', 'foobar');
+
+ //mark folders as incomplete.
+ $this->cache->put('folder/subfolder1', ['size' => -1]);
+ $this->cache->put('folder/subfolder2', ['size' => -1]);
+ $this->cache->put('folder/subfolder1/subfolder3', ['size' => -1]);
+
+ $this->scanner->backgroundScan();
+
+ $this->assertTrue($this->cache->inCache('folder/subfolder1/bar1.txt'));
+ $this->assertTrue($this->cache->inCache('folder/subfolder2/bar2.txt'));
+ $this->assertTrue($this->cache->inCache('folder/subfolder1/subfolder3/bar3.txt'));
+
+ //check if folder sizes are correct.
+ $this->assertEquals(18, $this->cache->get('folder')['size']);
+ $this->assertEquals(12, $this->cache->get('folder/subfolder1')['size']);
+ $this->assertEquals(6, $this->cache->get('folder/subfolder1/subfolder3')['size']);
+ $this->assertEquals(6, $this->cache->get('folder/subfolder2')['size']);
+ }
+
+ public function testReuseExisting(): void {
+ $this->fillTestFolders();
+
+ $this->scanner->scan('');
+ $oldData = $this->cache->get('');
+ $this->storage->unlink('folder/bar.txt');
+ $this->cache->put('folder', ['mtime' => $this->storage->filemtime('folder'), 'storage_mtime' => $this->storage->filemtime('folder')]);
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW, IScanner::REUSE_SIZE);
+ $newData = $this->cache->get('');
+ $this->assertIsString($oldData['etag']);
+ $this->assertIsString($newData['etag']);
+ $this->assertNotSame($oldData['etag'], $newData['etag']);
+ $this->assertEquals($oldData['size'], $newData['size']);
+
+ $oldData = $newData;
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW, IScanner::REUSE_ETAG);
+ $newData = $this->cache->get('');
+ $this->assertSame($oldData['etag'], $newData['etag']);
+ $this->assertEquals(-1, $newData['size']);
+
+ $this->scanner->scan('', IScanner::SCAN_RECURSIVE);
+ $oldData = $this->cache->get('');
+ $this->assertNotEquals(-1, $oldData['size']);
+ $this->scanner->scanFile('', IScanner::REUSE_ETAG + IScanner::REUSE_SIZE);
+ $newData = $this->cache->get('');
+ $this->assertSame($oldData['etag'], $newData['etag']);
+ $this->assertEquals($oldData['size'], $newData['size']);
+
+ $this->scanner->scan('', IScanner::SCAN_RECURSIVE, IScanner::REUSE_ETAG + IScanner::REUSE_SIZE);
+ $newData = $this->cache->get('');
+ $this->assertSame($oldData['etag'], $newData['etag']);
+ $this->assertEquals($oldData['size'], $newData['size']);
+
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW, IScanner::REUSE_ETAG + IScanner::REUSE_SIZE);
+ $newData = $this->cache->get('');
+ $this->assertSame($oldData['etag'], $newData['etag']);
+ $this->assertEquals($oldData['size'], $newData['size']);
+ }
+
+ public function testRemovedFile(): void {
+ $this->fillTestFolders();
+
+ $this->scanner->scan('');
+ $this->assertTrue($this->cache->inCache('foo.txt'));
+ $this->storage->unlink('foo.txt');
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW);
+ $this->assertFalse($this->cache->inCache('foo.txt'));
+ }
+
+ public function testRemovedFolder(): void {
+ $this->fillTestFolders();
+
+ $this->scanner->scan('');
+ $this->assertTrue($this->cache->inCache('folder/bar.txt'));
+ $this->storage->rmdir('/folder');
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW);
+ $this->assertFalse($this->cache->inCache('folder'));
+ $this->assertFalse($this->cache->inCache('folder/bar.txt'));
+ }
+
+ public function testScanRemovedFile(): void {
+ $this->fillTestFolders();
+
+ $this->scanner->scan('');
+ $this->assertTrue($this->cache->inCache('folder/bar.txt'));
+ $this->storage->unlink('folder/bar.txt');
+ $this->scanner->scanFile('folder/bar.txt');
+ $this->assertFalse($this->cache->inCache('folder/bar.txt'));
+ }
+
+ public function testETagRecreation(): void {
+ $this->fillTestFolders();
+
+ $this->scanner->scan('folder/bar.txt');
+
+ // manipulate etag to simulate an empty etag
+ $this->scanner->scan('', IScanner::SCAN_SHALLOW, IScanner::REUSE_ETAG);
+ /** @var CacheEntry $data0 */
+ $data0 = $this->cache->get('folder/bar.txt');
+ $this->assertIsString($data0['etag']);
+ $data1 = $this->cache->get('folder');
+ $this->assertIsString($data1['etag']);
+ $data2 = $this->cache->get('');
+ $this->assertIsString($data2['etag']);
+ $data0['etag'] = '';
+ $this->cache->put('folder/bar.txt', $data0->getData());
+
+ // rescan
+ $this->scanner->scan('folder/bar.txt', IScanner::SCAN_SHALLOW, IScanner::REUSE_ETAG);
+
+ // verify cache content
+ $newData0 = $this->cache->get('folder/bar.txt');
+ $this->assertIsString($newData0['etag']);
+ $this->assertNotEmpty($newData0['etag']);
+ }
+
+ public function testRepairParent(): void {
+ $this->fillTestFolders();
+ $this->scanner->scan('');
+ $this->assertTrue($this->cache->inCache('folder/bar.txt'));
+ $oldFolderId = $this->cache->getId('folder');
+
+ // delete the folder without removing the children
+ $query = Server::get(IDBConnection::class)->getQueryBuilder();
+ $query->delete('filecache')
+ ->where($query->expr()->eq('fileid', $query->createNamedParameter($oldFolderId)));
+ $query->execute();
+
+ $cachedData = $this->cache->get('folder/bar.txt');
+ $this->assertEquals($oldFolderId, $cachedData['parent']);
+ $this->assertFalse($this->cache->inCache('folder'));
+
+ $this->scanner->scan('');
+
+ $this->assertTrue($this->cache->inCache('folder'));
+ $newFolderId = $this->cache->getId('folder');
+ $this->assertNotEquals($oldFolderId, $newFolderId);
+
+ $cachedData = $this->cache->get('folder/bar.txt');
+ $this->assertEquals($newFolderId, $cachedData['parent']);
+ }
+
+ public function testRepairParentShallow(): void {
+ $this->fillTestFolders();
+ $this->scanner->scan('');
+ $this->assertTrue($this->cache->inCache('folder/bar.txt'));
+ $oldFolderId = $this->cache->getId('folder');
+
+ // delete the folder without removing the children
+ $query = Server::get(IDBConnection::class)->getQueryBuilder();
+ $query->delete('filecache')
+ ->where($query->expr()->eq('fileid', $query->createNamedParameter($oldFolderId)));
+ $query->execute();
+
+ $cachedData = $this->cache->get('folder/bar.txt');
+ $this->assertEquals($oldFolderId, $cachedData['parent']);
+ $this->assertFalse($this->cache->inCache('folder'));
+
+ $this->scanner->scan('folder', IScanner::SCAN_SHALLOW);
+
+ $this->assertTrue($this->cache->inCache('folder'));
+ $newFolderId = $this->cache->getId('folder');
+ $this->assertNotEquals($oldFolderId, $newFolderId);
+
+ $cachedData = $this->cache->get('folder/bar.txt');
+ $this->assertEquals($newFolderId, $cachedData['parent']);
+ }
+
+ /**
+ *
+ * @param string $path
+ * @param bool $expected
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestIsPartialFile')]
+ public function testIsPartialFile($path, $expected): void {
+ $this->assertSame($expected,
+ $this->scanner->isPartialFile($path)
+ );
+ }
+
+ public static function dataTestIsPartialFile(): array {
+ return [
+ ['foo.txt.part', true],
+ ['/sub/folder/foo.txt.part', true],
+ ['/sub/folder.part/foo.txt', true],
+ ['foo.txt', false],
+ ['/sub/folder/foo.txt', false],
+ ];
+ }
+
+ public function testNoETagUnscannedFolder(): void {
+ $this->fillTestFolders();
+
+ $this->scanner->scan('');
+
+ $oldFolderEntry = $this->cache->get('folder');
+ // create a new file in a folder by keeping the mtime unchanged, but mark the folder as unscanned
+ $this->storage->file_put_contents('folder/new.txt', 'foo');
+ $this->storage->touch('folder', $oldFolderEntry->getMTime());
+ $this->cache->update($oldFolderEntry->getId(), ['size' => -1]);
+
+ $this->scanner->scan('');
+
+ $this->cache->inCache('folder/new.txt');
+
+ $newFolderEntry = $this->cache->get('folder');
+ $this->assertNotEquals($newFolderEntry->getEtag(), $oldFolderEntry->getEtag());
+ }
+
+ public function testNoETagUnscannedSubFolder(): void {
+ $this->fillTestFolders();
+ $this->storage->mkdir('folder/sub');
+
+ $this->scanner->scan('');
+
+ $oldFolderEntry1 = $this->cache->get('folder');
+ $oldFolderEntry2 = $this->cache->get('folder/sub');
+ // create a new file in a folder by keeping the mtime unchanged, but mark the folder as unscanned
+ $this->storage->file_put_contents('folder/sub/new.txt', 'foo');
+ $this->storage->touch('folder/sub', $oldFolderEntry1->getMTime());
+
+ // we only mark the direct parent as unscanned, which is the current "notify" behavior
+ $this->cache->update($oldFolderEntry2->getId(), ['size' => -1]);
+
+ $this->scanner->scan('');
+
+ $this->cache->inCache('folder/new.txt');
+
+ $newFolderEntry1 = $this->cache->get('folder');
+ $this->assertNotEquals($newFolderEntry1->getEtag(), $oldFolderEntry1->getEtag());
+ $newFolderEntry2 = $this->cache->get('folder/sub');
+ $this->assertNotEquals($newFolderEntry2->getEtag(), $oldFolderEntry2->getEtag());
+ }
+}