From 01807e48bf7b46d39ff65f2e6741284644f81042 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Sun, 10 May 2026 12:19:43 +0200 Subject: [PATCH] fix(appstore): catch GenericFileException when reading cache file in Fetcher When the appstore cache file exists but file_get_contents returns false (e.g. empty or corrupted file), a GenericFileException is thrown but was not caught, crashing the apps settings page. Recreate the file and proceed to fetch fresh data, same as when the file is absent. Signed-off-by: Anna Larch AI-Assisted-By: Claude Sonnet 4.6 --- lib/private/App/AppStore/Fetcher/Fetcher.php | 7 +- .../lib/App/AppStore/Fetcher/FetcherBase.php | 81 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php index 29adf98a23c30..7b2d1ba92883e 100644 --- a/lib/private/App/AppStore/Fetcher/Fetcher.php +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -12,6 +12,7 @@ use OC\Files\AppData\Factory; use OCP\AppFramework\Http; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\GenericFileException; use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Http\Client\IClientService; @@ -167,8 +168,12 @@ public function get($allowUnstable = false): array { } } } catch (NotFoundException $e) { - // File does not already exists + // File does not already exist $file = $rootFolder->newFile($this->fileName); + } catch (GenericFileException $e) { + // Cache file exists but could not be read (I/O error or OS-level permission failure) + // $file is already set from the getFile() call above; putContent() will overwrite it + $this->logger->warning('Could not read appstore cache file, it will be refreshed', ['app' => 'appstoreFetcher', 'exception' => $e]); } // Refresh the file content diff --git a/tests/lib/App/AppStore/Fetcher/FetcherBase.php b/tests/lib/App/AppStore/Fetcher/FetcherBase.php index 55252a33dc054..6c89e05564368 100644 --- a/tests/lib/App/AppStore/Fetcher/FetcherBase.php +++ b/tests/lib/App/AppStore/Fetcher/FetcherBase.php @@ -13,6 +13,7 @@ use OC\Files\AppData\AppData; use OC\Files\AppData\Factory; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\GenericFileException; use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Files\SimpleFS\ISimpleFile; @@ -684,4 +685,84 @@ public function testFetchAfterUpgradeNoETag(): void { ]; $this->assertSame($expected, $this->fetcher->get()); } + + public function testGetWithUnreadableCacheFileRecreatesAndFetches(): void { + $this->config + ->method('getSystemValueString') + ->willReturnCallback(function ($var, $default) { + if ($var === 'appstoreurl') { + return 'https://apps.nextcloud.com/api/v1'; + } elseif ($var === 'version') { + return '11.0.0.2'; + } + return $default; + }); + $this->config->method('getSystemValueBool') + ->willReturnArgument(1); + + $folder = $this->createMock(ISimpleFolder::class); + $corruptedFile = $this->createMock(ISimpleFile::class); + $freshFile = $this->createMock(ISimpleFile::class); + $this->appData + ->expects($this->once()) + ->method('getFolder') + ->with('/') + ->willReturn($folder); + $folder + ->expects($this->once()) + ->method('getFile') + ->with($this->fileName) + ->willReturn($corruptedFile); + $corruptedFile + ->expects($this->once()) + ->method('getContent') + ->willThrowException(new GenericFileException()); + $folder + ->expects($this->once()) + ->method('newFile') + ->with($this->fileName) + ->willReturn($freshFile); + $client = $this->createMock(IClient::class); + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $response = $this->createMock(IResponse::class); + $client + ->expects($this->once()) + ->method('get') + ->with($this->endpoint) + ->willReturn($response); + $response + ->expects($this->once()) + ->method('getBody') + ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]'); + $response->method('getHeader') + ->with($this->equalTo('ETag')) + ->willReturn('"myETag"'); + $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1502,"ncversion":"11.0.0.2","ETag":"\"myETag\""}'; + $freshFile + ->expects($this->once()) + ->method('putContent') + ->with($fileData); + $freshFile + ->expects($this->once()) + ->method('getContent') + ->willReturn($fileData); + $this->timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn(1502); + + $expected = [ + [ + 'id' => 'MyNewApp', + 'foo' => 'foo', + ], + [ + 'id' => 'bar', + ], + ]; + $this->assertSame($expected, $this->fetcher->get()); + } }