2025-08-20 11:34:07 -04:00
|
|
|
|
<?php
|
|
|
|
|
|
|
2025-08-21 10:42:34 -04:00
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
|
|
|
|
* SPDX-FileContributor: Carl Schwan
|
|
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-08-20 11:34:07 -04:00
|
|
|
|
namespace OC\Preview\Storage;
|
|
|
|
|
|
|
2025-08-21 10:42:34 -04:00
|
|
|
|
use LogicException;
|
|
|
|
|
|
use OC;
|
|
|
|
|
|
use OC\Files\SimpleFS\SimpleFile;
|
2025-08-20 11:34:07 -04:00
|
|
|
|
use OC\Preview\Db\Preview;
|
2025-09-25 08:25:47 -04:00
|
|
|
|
use OC\Preview\Db\PreviewMapper;
|
|
|
|
|
|
use OCP\DB\Exception;
|
2026-03-02 10:20:06 -05:00
|
|
|
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
2025-09-30 07:44:34 -04:00
|
|
|
|
use OCP\Files\IMimeTypeDetector;
|
2026-02-09 14:13:44 -05:00
|
|
|
|
use OCP\Files\IMimeTypeLoader;
|
2026-02-10 11:44:13 -05:00
|
|
|
|
use OCP\Files\IRootFolder;
|
2025-09-30 09:56:31 -04:00
|
|
|
|
use OCP\Files\NotFoundException;
|
|
|
|
|
|
use OCP\Files\NotPermittedException;
|
2025-09-25 08:25:47 -04:00
|
|
|
|
use OCP\IAppConfig;
|
2025-08-21 10:42:34 -04:00
|
|
|
|
use OCP\IConfig;
|
2025-09-25 08:25:47 -04:00
|
|
|
|
use OCP\IDBConnection;
|
2025-09-26 08:22:38 -04:00
|
|
|
|
use Override;
|
2025-09-30 07:44:34 -04:00
|
|
|
|
use Psr\Log\LoggerInterface;
|
2025-09-25 08:25:47 -04:00
|
|
|
|
use RecursiveDirectoryIterator;
|
|
|
|
|
|
use RecursiveIteratorIterator;
|
2025-08-20 11:34:07 -04:00
|
|
|
|
|
|
|
|
|
|
class LocalPreviewStorage implements IPreviewStorage {
|
2026-03-02 10:20:06 -05:00
|
|
|
|
private const SCAN_BATCH_SIZE = 1000;
|
|
|
|
|
|
|
2025-08-21 10:42:34 -04:00
|
|
|
|
public function __construct(
|
|
|
|
|
|
private readonly IConfig $config,
|
2025-09-25 08:25:47 -04:00
|
|
|
|
private readonly PreviewMapper $previewMapper,
|
|
|
|
|
|
private readonly IAppConfig $appConfig,
|
|
|
|
|
|
private readonly IDBConnection $connection,
|
2025-09-30 07:44:34 -04:00
|
|
|
|
private readonly IMimeTypeDetector $mimeTypeDetector,
|
|
|
|
|
|
private readonly LoggerInterface $logger,
|
2026-02-09 14:13:44 -05:00
|
|
|
|
private readonly IMimeTypeLoader $mimeTypeLoader,
|
2026-02-10 11:44:13 -05:00
|
|
|
|
private readonly IRootFolder $rootFolder,
|
2025-08-21 10:42:34 -04:00
|
|
|
|
) {
|
2025-08-20 11:34:07 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 08:22:38 -04:00
|
|
|
|
#[Override]
|
2025-09-30 09:56:31 -04:00
|
|
|
|
public function writePreview(Preview $preview, mixed $stream): int {
|
2025-08-20 11:34:07 -04:00
|
|
|
|
$previewPath = $this->constructPath($preview);
|
2025-09-30 09:56:31 -04:00
|
|
|
|
$this->createParentFiles($previewPath);
|
2025-09-11 08:52:34 -04:00
|
|
|
|
return file_put_contents($previewPath, $stream);
|
2025-08-20 11:34:07 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 08:22:38 -04:00
|
|
|
|
#[Override]
|
2025-09-16 05:34:41 -04:00
|
|
|
|
public function readPreview(Preview $preview): mixed {
|
2025-09-30 09:56:31 -04:00
|
|
|
|
$previewPath = $this->constructPath($preview);
|
|
|
|
|
|
$resource = @fopen($previewPath, 'r');
|
|
|
|
|
|
if ($resource === false) {
|
|
|
|
|
|
throw new NotFoundException('Unable to open preview stream at ' . $previewPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
return $resource;
|
2025-08-20 11:34:07 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 08:22:38 -04:00
|
|
|
|
#[Override]
|
2025-09-16 05:34:41 -04:00
|
|
|
|
public function deletePreview(Preview $preview): void {
|
2025-09-30 09:56:31 -04:00
|
|
|
|
$previewPath = $this->constructPath($preview);
|
|
|
|
|
|
if (!@unlink($previewPath) && is_file($previewPath)) {
|
|
|
|
|
|
throw new NotPermittedException('Unable to delete preview at ' . $previewPath);
|
|
|
|
|
|
}
|
2025-09-10 04:51:41 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 11:44:13 -05:00
|
|
|
|
public function getRootFolder(): string {
|
|
|
|
|
|
return $this->config->getSystemValueString('datadirectory', OC::$SERVERROOT . '/data');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 04:51:41 -04:00
|
|
|
|
public function getPreviewRootFolder(): string {
|
2026-02-10 11:44:13 -05:00
|
|
|
|
return $this->getRootFolder() . '/' . $this->rootFolder->getAppDataDirectoryName() . '/preview/';
|
2025-08-20 11:34:07 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function constructPath(Preview $preview): string {
|
2025-09-30 09:56:31 -04:00
|
|
|
|
return $this->getPreviewRootFolder() . implode('/', str_split(substr(md5((string)$preview->getFileId()), 0, 7))) . '/' . $preview->getFileId() . '/' . $preview->getName();
|
2025-08-20 11:34:07 -04:00
|
|
|
|
}
|
2025-08-21 10:42:34 -04:00
|
|
|
|
|
2025-09-30 09:56:31 -04:00
|
|
|
|
private function createParentFiles(string $path): void {
|
2025-09-25 08:25:47 -04:00
|
|
|
|
$dirname = dirname($path);
|
2026-02-05 09:22:54 -05:00
|
|
|
|
if (!is_dir($dirname)) {
|
|
|
|
|
|
mkdir($dirname, recursive: true);
|
|
|
|
|
|
}
|
2025-09-30 09:56:31 -04:00
|
|
|
|
if (!is_dir($dirname)) {
|
|
|
|
|
|
throw new NotPermittedException("Unable to create directory '$dirname'");
|
|
|
|
|
|
}
|
2025-08-21 10:42:34 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 08:22:38 -04:00
|
|
|
|
#[Override]
|
2025-08-21 10:42:34 -04:00
|
|
|
|
public function migratePreview(Preview $preview, SimpleFile $file): void {
|
2025-09-11 08:52:34 -04:00
|
|
|
|
// legacy flat directory
|
2025-09-30 09:56:31 -04:00
|
|
|
|
$sourcePath = $this->getPreviewRootFolder() . $preview->getFileId() . '/' . $preview->getName();
|
2025-09-11 08:52:34 -04:00
|
|
|
|
if (!file_exists($sourcePath)) {
|
|
|
|
|
|
return;
|
2025-08-21 10:42:34 -04:00
|
|
|
|
}
|
2025-09-10 04:51:41 -04:00
|
|
|
|
|
2025-09-11 08:52:34 -04:00
|
|
|
|
$destinationPath = $this->constructPath($preview);
|
2025-08-21 10:42:34 -04:00
|
|
|
|
if (file_exists($destinationPath)) {
|
2025-09-10 04:51:41 -04:00
|
|
|
|
@unlink($sourcePath); // We already have a new preview, just delete the old one
|
2025-08-21 10:42:34 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-11 08:52:34 -04:00
|
|
|
|
|
|
|
|
|
|
$this->createParentFiles($destinationPath);
|
2025-08-28 08:25:55 -04:00
|
|
|
|
$ok = rename($sourcePath, $destinationPath);
|
2025-08-21 10:42:34 -04:00
|
|
|
|
if (!$ok) {
|
2025-09-30 07:44:34 -04:00
|
|
|
|
throw new LogicException('Failed to move ' . $sourcePath . ' to ' . $destinationPath);
|
2025-08-21 10:42:34 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-25 08:25:47 -04:00
|
|
|
|
|
2025-09-26 08:22:38 -04:00
|
|
|
|
#[Override]
|
2025-09-25 08:25:47 -04:00
|
|
|
|
public function scan(): int {
|
|
|
|
|
|
$checkForFileCache = !$this->appConfig->getValueBool('core', 'previewMovedDone');
|
|
|
|
|
|
|
2026-02-10 11:44:13 -05:00
|
|
|
|
if (!file_exists($this->getPreviewRootFolder())) {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
2026-03-02 10:20:06 -05:00
|
|
|
|
|
2025-09-25 08:25:47 -04:00
|
|
|
|
$scanner = new RecursiveDirectoryIterator($this->getPreviewRootFolder());
|
|
|
|
|
|
$previewsFound = 0;
|
2026-03-02 10:20:06 -05:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Use an associative array keyed by path for O(1) lookup instead of
|
|
|
|
|
|
* the O(n) in_array() the original code used.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @var array<string, true> $skipPaths
|
|
|
|
|
|
*/
|
|
|
|
|
|
$skipPaths = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Pending previews grouped by fileId. A single original file can have
|
|
|
|
|
|
* many preview variants (different sizes/formats), so we group them to
|
|
|
|
|
|
* issue one filecache lookup per original file rather than one per
|
|
|
|
|
|
* preview variant.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @var array<int, list<array{preview: Preview, filePath: string, realPath: string}>> $pendingByFileId
|
|
|
|
|
|
*/
|
|
|
|
|
|
$pendingByFileId = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* path_hash => realPath for legacy filecache entries that need to be
|
|
|
|
|
|
* cleaned up. Only populated when $checkForFileCache is true.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @var array<string, string> $pendingPathHashes
|
|
|
|
|
|
*/
|
|
|
|
|
|
$pendingPathHashes = [];
|
|
|
|
|
|
$pendingCount = 0;
|
|
|
|
|
|
|
2025-09-25 08:25:47 -04:00
|
|
|
|
foreach (new RecursiveIteratorIterator($scanner) as $file) {
|
2026-03-02 10:20:06 -05:00
|
|
|
|
if (!$file->isFile()) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$filePath = $file->getPathname();
|
|
|
|
|
|
if (isset($skipPaths[$filePath])) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$preview = Preview::fromPath($filePath, $this->mimeTypeDetector);
|
|
|
|
|
|
if ($preview === false) {
|
|
|
|
|
|
$this->logger->error('Unable to parse preview information for ' . $file->getRealPath());
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$preview->setSize($file->getSize());
|
|
|
|
|
|
$preview->setMtime($file->getMtime());
|
|
|
|
|
|
$preview->setEncrypted(false);
|
|
|
|
|
|
|
|
|
|
|
|
$realPath = $file->getRealPath();
|
|
|
|
|
|
$pendingByFileId[$preview->getFileId()][] = [
|
|
|
|
|
|
'preview' => $preview,
|
|
|
|
|
|
'filePath' => $filePath,
|
|
|
|
|
|
'realPath' => $realPath,
|
|
|
|
|
|
];
|
|
|
|
|
|
$pendingCount++;
|
|
|
|
|
|
|
|
|
|
|
|
if ($checkForFileCache) {
|
|
|
|
|
|
$relativePath = str_replace($this->getRootFolder() . '/', '', $realPath);
|
|
|
|
|
|
$pendingPathHashes[md5($relativePath)] = $realPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($pendingCount >= self::SCAN_BATCH_SIZE) {
|
|
|
|
|
|
$this->connection->beginTransaction();
|
2025-09-25 08:25:47 -04:00
|
|
|
|
try {
|
2026-03-02 10:20:06 -05:00
|
|
|
|
$previewsFound += $this->processScanBatch($pendingByFileId, $pendingPathHashes, $checkForFileCache, $skipPaths);
|
|
|
|
|
|
$this->connection->commit();
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
$this->connection->rollBack();
|
|
|
|
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
|
|
|
|
throw $e;
|
|
|
|
|
|
}
|
|
|
|
|
|
$pendingByFileId = [];
|
|
|
|
|
|
$pendingPathHashes = [];
|
|
|
|
|
|
$pendingCount = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-25 08:25:47 -04:00
|
|
|
|
|
2026-03-02 10:20:06 -05:00
|
|
|
|
if ($pendingCount > 0) {
|
|
|
|
|
|
$this->connection->beginTransaction();
|
|
|
|
|
|
try {
|
|
|
|
|
|
$previewsFound += $this->processScanBatch($pendingByFileId, $pendingPathHashes, $checkForFileCache, $skipPaths);
|
|
|
|
|
|
$this->connection->commit();
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
$this->connection->rollBack();
|
|
|
|
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
|
|
|
|
throw $e;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-25 08:25:47 -04:00
|
|
|
|
|
2026-03-02 10:20:06 -05:00
|
|
|
|
return $previewsFound;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Process one batch of preview files collected during scan().
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array<int, list<array{preview: Preview, filePath: string, realPath: string}>> $pendingByFileId
|
|
|
|
|
|
* @param array<string, string> $pendingPathHashes path_hash => realPath
|
|
|
|
|
|
* @param array<string, true> $skipPaths Modified in place: newly-moved paths are added so the outer iterator skips them.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function processScanBatch(
|
|
|
|
|
|
array $pendingByFileId,
|
|
|
|
|
|
array $pendingPathHashes,
|
|
|
|
|
|
bool $checkForFileCache,
|
|
|
|
|
|
array &$skipPaths,
|
|
|
|
|
|
): int {
|
|
|
|
|
|
$filecacheByFileId = $this->fetchFilecacheByFileIds(array_keys($pendingByFileId));
|
|
|
|
|
|
$legacyByPathHash = [];
|
|
|
|
|
|
if ($checkForFileCache && $pendingPathHashes !== []) {
|
|
|
|
|
|
$legacyByPathHash = $this->fetchFilecacheByPathHashes(array_keys($pendingPathHashes));
|
|
|
|
|
|
}
|
2025-09-25 08:25:47 -04:00
|
|
|
|
|
2026-03-02 10:20:06 -05:00
|
|
|
|
$previewsFound = 0;
|
|
|
|
|
|
foreach ($pendingByFileId as $fileId => $items) {
|
|
|
|
|
|
if (!isset($filecacheByFileId[$fileId])) {
|
|
|
|
|
|
// Original file has been deleted – clean up all its previews.
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$this->logger->warning('Original file ' . $fileId . ' was not found. Deleting preview at ' . $item['realPath']);
|
|
|
|
|
|
@unlink($item['realPath']);
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$filecacheRow = $filecacheByFileId[$fileId];
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$preview = $item['preview'];
|
|
|
|
|
|
|
|
|
|
|
|
if ($checkForFileCache) {
|
|
|
|
|
|
$relativePath = str_replace($this->getRootFolder() . '/', '', $item['realPath']);
|
|
|
|
|
|
$pathHash = md5($relativePath);
|
|
|
|
|
|
if (isset($legacyByPathHash[$pathHash])) {
|
|
|
|
|
|
$legacyRow = $legacyByPathHash[$pathHash];
|
|
|
|
|
|
$qb = $this->connection->getTypedQueryBuilder();
|
|
|
|
|
|
$qb->delete('filecache')
|
|
|
|
|
|
->where($qb->expr()->eq('fileid', $qb->createNamedParameter($legacyRow['fileid'])))
|
|
|
|
|
|
->andWhere($qb->expr()->eq('storage', $qb->createNamedParameter($legacyRow['storage'])))
|
|
|
|
|
|
->executeStatement();
|
|
|
|
|
|
$this->deleteParentsFromFileCache((int)$legacyRow['parent'], (int)$legacyRow['storage']);
|
2025-09-30 07:44:34 -04:00
|
|
|
|
}
|
2026-03-02 10:20:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$preview->setStorageId((int)$filecacheRow['storage']);
|
|
|
|
|
|
$preview->setEtag($filecacheRow['etag']);
|
|
|
|
|
|
$preview->setSourceMimetype($this->mimeTypeLoader->getMimetypeById((int)$filecacheRow['mimetype']));
|
|
|
|
|
|
$preview->generateId();
|
|
|
|
|
|
|
|
|
|
|
|
$this->connection->beginTransaction();
|
|
|
|
|
|
try {
|
|
|
|
|
|
$this->previewMapper->insert($preview);
|
|
|
|
|
|
$this->connection->commit();
|
2025-09-25 08:25:47 -04:00
|
|
|
|
} catch (Exception $e) {
|
2026-03-02 10:20:06 -05:00
|
|
|
|
$this->connection->rollBack();
|
2025-09-25 08:25:47 -04:00
|
|
|
|
if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
|
|
|
|
|
|
throw $e;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-02 10:20:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Move old flat preview to new nested directory format.
|
|
|
|
|
|
$dirName = str_replace($this->getPreviewRootFolder(), '', $item['filePath']);
|
|
|
|
|
|
if (preg_match('/[0-9a-e]\/[0-9a-e]\/[0-9a-e]\/[0-9a-e]\/[0-9a-e]\/[0-9a-e]\/[0-9a-e]\/[0-9]+/', $dirName) !== 1) {
|
|
|
|
|
|
$previewPath = $this->constructPath($preview);
|
|
|
|
|
|
$this->createParentFiles($previewPath);
|
|
|
|
|
|
$ok = rename($item['realPath'], $previewPath);
|
|
|
|
|
|
if (!$ok) {
|
|
|
|
|
|
throw new LogicException('Failed to move ' . $item['realPath'] . ' to ' . $previewPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Mark the destination so the outer iterator skips it if it encounters the path later.
|
|
|
|
|
|
$skipPaths[$previewPath] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 08:25:47 -04:00
|
|
|
|
$previewsFound++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $previewsFound;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 10:20:06 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Bulk-fetch filecache rows for a set of fileIds.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int[] $fileIds
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function fetchFilecacheByFileIds(array $fileIds): array {
|
|
|
|
|
|
if (empty($fileIds)) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$result = [];
|
|
|
|
|
|
$qb = $this->connection->getTypedQueryBuilder();
|
|
|
|
|
|
$qb->selectColumns('fileid', 'storage', 'etag', 'mimetype')
|
|
|
|
|
|
->from('filecache');
|
|
|
|
|
|
foreach (array_chunk($fileIds, 1000) as $chunk) {
|
|
|
|
|
|
$qb->andWhere(
|
|
|
|
|
|
$qb->expr()->in('fileid', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
$rows = $qb->runAcrossAllShards()
|
|
|
|
|
|
->executeQuery();
|
|
|
|
|
|
while ($row = $rows->fetchAssociative()) {
|
|
|
|
|
|
$result[(int)$row['fileid']] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
$rows->closeCursor();
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Bulk-fetch filecache rows for a set of path_hashes (legacy migration).
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string[] $pathHashes
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function fetchFilecacheByPathHashes(array $pathHashes): array {
|
|
|
|
|
|
if (empty($pathHashes)) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$result = [];
|
|
|
|
|
|
$qb = $this->connection->getTypedQueryBuilder();
|
|
|
|
|
|
$qb->selectColumns('fileid', 'storage', 'etag', 'mimetype', 'parent', 'path_hash')
|
|
|
|
|
|
->from('filecache');
|
|
|
|
|
|
foreach (array_chunk($pathHashes, 1000) as $chunk) {
|
|
|
|
|
|
$qb->andWhere(
|
|
|
|
|
|
$qb->expr()->in('path_hash', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY))
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
$rows = $qb->runAcrossAllShards()
|
|
|
|
|
|
->executeQuery();
|
|
|
|
|
|
while ($row = $rows->fetchAssociative()) {
|
|
|
|
|
|
$result[$row['path_hash']] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
$rows->closeCursor();
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 11:44:13 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Recursive method that deletes the folder and its parent folders if it's not
|
|
|
|
|
|
* empty.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function deleteParentsFromFileCache(int $folderId, int $storageId): void {
|
2026-03-02 10:20:06 -05:00
|
|
|
|
$qb = $this->connection->getTypedQueryBuilder();
|
|
|
|
|
|
$result = $qb->selectColumns('fileid', 'path', 'storage', 'parent')
|
2025-09-25 08:25:47 -04:00
|
|
|
|
->from('filecache')
|
2026-02-10 11:44:13 -05:00
|
|
|
|
->where($qb->expr()->eq('parent', $qb->createNamedParameter($folderId)))
|
2025-09-25 08:25:47 -04:00
|
|
|
|
->setMaxResults(1)
|
|
|
|
|
|
->runAcrossAllShards()
|
2026-03-02 10:20:06 -05:00
|
|
|
|
->executeQuery();
|
|
|
|
|
|
$row = $result->fetchAssociative();
|
|
|
|
|
|
$result->closeCursor();
|
2025-09-25 08:25:47 -04:00
|
|
|
|
|
2026-03-02 10:20:06 -05:00
|
|
|
|
if ($row !== false) {
|
2026-02-10 11:44:13 -05:00
|
|
|
|
// there are other files in the directory, don't delete yet
|
2025-09-25 08:25:47 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 11:44:13 -05:00
|
|
|
|
// Get new parent
|
2026-03-02 10:20:06 -05:00
|
|
|
|
$qb = $this->connection->getTypedQueryBuilder();
|
|
|
|
|
|
$result = $qb->selectColumns('fileid', 'path', 'parent')
|
2026-02-10 11:44:13 -05:00
|
|
|
|
->from('filecache')
|
|
|
|
|
|
->where($qb->expr()->eq('fileid', $qb->createNamedParameter($folderId)))
|
|
|
|
|
|
->andWhere($qb->expr()->eq('storage', $qb->createNamedParameter($storageId)))
|
|
|
|
|
|
->setMaxResults(1)
|
2026-03-02 10:20:06 -05:00
|
|
|
|
->executeQuery();
|
|
|
|
|
|
$row = $result->fetchAssociative();
|
|
|
|
|
|
$result->closeCursor();
|
|
|
|
|
|
if ($row !== false) {
|
|
|
|
|
|
$parentFolderId = (int)$row['parent'];
|
2025-09-25 08:25:47 -04:00
|
|
|
|
|
2026-03-02 10:20:06 -05:00
|
|
|
|
$qb = $this->connection->getTypedQueryBuilder();
|
2026-02-10 11:44:13 -05:00
|
|
|
|
$qb->delete('filecache')
|
|
|
|
|
|
->where($qb->expr()->eq('fileid', $qb->createNamedParameter($folderId)))
|
|
|
|
|
|
->andWhere($qb->expr()->eq('storage', $qb->createNamedParameter($storageId)))
|
|
|
|
|
|
->executeStatement();
|
2025-09-25 08:25:47 -04:00
|
|
|
|
|
2026-02-10 11:44:13 -05:00
|
|
|
|
$this->deleteParentsFromFileCache($parentFolderId, $storageId);
|
2025-09-25 08:25:47 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-20 11:34:07 -04:00
|
|
|
|
}
|