Compare commits

...

2 Commits

Author SHA1 Message Date
Josh
6758b519c3 chore: fix lint/psalm
Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-02 09:21:21 -04:00
Josh
737ab787a0 refactor(preview): Modernize root storage class
Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-01 11:23:52 -04:00

View File

@@ -6,6 +6,7 @@ declare(strict_types=1);
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Preview\Storage;
use OC\Files\AppData\AppData;
@@ -14,61 +15,116 @@ use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFolder;
/**
* Preview storage root for Nextcloud.
*
* Implements preview folder access using all supported folder structures.
*/
class Root extends AppData {
private $isMultibucketPreviewDistributionEnabled = false;
public function __construct(IRootFolder $rootFolder, SystemConfig $systemConfig) {
/**
* @param IRootFolder $rootFolder
* @param SystemConfig $systemConfig
*/
public function __construct(
IRootFolder $rootFolder,
private SystemConfig $systemConfig,
) {
parent::__construct($rootFolder, $systemConfig, 'preview');
$this->isMultibucketPreviewDistributionEnabled = $systemConfig->getValue('objectstore.multibucket.preview-distribution', false) === true;
}
/**
* Retrieve the preview folder for a specified file.
*
* Search order:
* - Hashed folder structure
* - Legacy flat folder structure
* - Legacy single bucket folder structure (if enabled)
*
* If none of these locations exist, throws NotFoundException.
*
* @param string $name File identifier (fileId) as a string.
* @return ISimpleFolder Preview folder instance.
* @throws NotFoundException If no folder is found.
*/
public function getFolder(string $name): ISimpleFolder {
$internalFolder = self::getInternalFolder($name);
$searchTargets = $this->buildSearchTargets($name);
try {
return parent::getFolder($internalFolder);
} catch (NotFoundException $e) {
/*
* The new folder structure is not found.
* Lets try the old one
*/
}
try {
return parent::getFolder($name);
} catch (NotFoundException $e) {
/*
* The old folder structure is not found.
* Lets try the multibucket fallback if available
*/
if ($this->isMultibucketPreviewDistributionEnabled) {
return parent::getFolder('old-multibucket/' . $internalFolder);
foreach ($searchTargets as $target) {
try {
return parent::getFolder($target);
} catch (NotFoundException $e) {
// Optional: Add debug logging here
// Note: there is no point in rethrowing; don't know which structure should succeed.
continue;
}
// when there is no further fallback just throw the exception
throw $e;
}
throw new NotFoundException("Preview folder not found for name: $name");
}
/**
* Build a list of candidate folder paths to search for a preview folder.
*
* The order is:
* - Hashed folder structure (modern, default)
* - Legacy flat folder structure
* - Legacy single bucket folder structure (if enabled)
*
* @param string $name File identifier (fileId) as a string.
* @return string[] List of folder structure candidates to check for previews.
*/
private function buildSearchTargets(string $name): array {
$internalFolder = self::getInternalFolder($name);
$searchTargets = [ $internalFolder, $name ];
if ($this->systemConfig->getValue('objectstore.multibucket.preview-distribution', false)) {
$searchTargets[] = 'old-multibucket/' . $internalFolder;
// TODO: Consider moving legacy bucket fallback earlier as an optimization.
}
// TODO: Consider adding config flag to disable using legacy fallback strategies as an optimization.
return $searchTargets;
}
/**
* Create a new preview folder, using the default modern structure, for a specified file.
*
* @param string $name File identifier (fileId) as a string.
* @return ISimpleFolder Preview folder instance.
*/
public function newFolder(string $name): ISimpleFolder {
$internalFolder = self::getInternalFolder($name);
return parent::newFolder($internalFolder);
}
/*
* Do not allow directory listing on this special root
* since it gets to big and time consuming
/**
* Directory listing is disallowed for this root due to performance.
*
* @return array<ISimpleFolder> An empty array.
*/
public function getDirectoryListing(): array {
return [];
}
/**
* Utility function to get the internal (hashed) folder path for a given name.
* Uses the first 7 chars of the md5 hash as folder distribution.
*
* @param string $name File identifier (fileId) as a string.
* @return string Preview folder path for the specified file.
*/
public static function getInternalFolder(string $name): string {
return implode('/', str_split(substr(md5($name), 0, 7))) . '/' . $name;
$hash = substr(md5($name), 0, 7);
$folderPath = implode('/', str_split($hash)) . '/' . $name;
return $folderPath;
}
/**
* Get the numeric storage ID for this preview storage.
*
* @return int
*/
public function getStorageId(): int {
return $this->getAppDataRootFolder()->getStorage()->getCache()->getNumericStorageId();
return $this->getAppDataRootFolder()
->getStorage()
->getCache()
->getNumericStorageId();
}
}