Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 43 additions & 62 deletions core/BackgroundJobs/PreviewMigrationJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@
use OC\Preview\PreviewMigrationService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\IResult;
use OCP\Files\FileInfo;
use OCP\Files\IRootFolder;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IDBConnection;
use Override;
use Psr\Log\LoggerInterface;

Expand All @@ -27,7 +26,6 @@ public function __construct(
ITimeFactory $time,
private readonly IAppConfig $appConfig,
private readonly IConfig $config,
private readonly IDBConnection $connection,
private readonly IRootFolder $rootFolder,
private readonly PreviewMigrationService $migrationService,
private readonly LoggerInterface $logger,
Expand All @@ -45,79 +43,62 @@ protected function run(mixed $argument): void {
return;
}

$startTime = time();
while (true) {
$qb = $this->connection->getQueryBuilder();
$qb->select('path')
->from('filecache')
->where($qb->expr()->orX(
// Hierarchical preview folder structure
$qb->expr()->like('path', $qb->createNamedParameter($this->previewRootPath . '%/%/%/%/%/%/%/%/%')),
// Legacy flat preview folder structure
$qb->expr()->like('path', $qb->createNamedParameter($this->previewRootPath . '%/%.%'))
))->andWhere(
$qb->expr()->eq('storage', $qb->createNamedParameter($this->rootFolder->getMountPoint()->getNumericStorageId()))
)
->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId())
->setMaxResults(100);

$result = $qb->executeQuery();
$foundPreviews = $this->processQueryResult($result);

if (!$foundPreviews) {
break;
}
$storage = $this->rootFolder->getMountPoint()->getStorage();
if ($storage === null) {
$this->appConfig->setValueBool('core', 'previewMovedDone', true);
return;
}

// Stop if execution time is more than one hour.
if (time() - $startTime > 3600) {
return;
}
$cache = $storage->getCache();
$previewRootId = $cache->getId(rtrim($this->previewRootPath, '/'));
if ($previewRootId === -1) {
// No previews have ever been generated on this instance.
$this->appConfig->setValueBool('core', 'previewMovedDone', true);
return;
}

$this->appConfig->setValueBool('core', 'previewMovedDone', true);
}
$startTime = time();

private function processQueryResult(IResult $result): bool {
$foundPreview = false;
$fileIds = [];
$flatFileIds = [];
while ($row = $result->fetch()) {
$pathSplit = explode('/', $row['path']);
assert(count($pathSplit) >= 2);
$fileId = (int)$pathSplit[count($pathSplit) - 2];
if (count($pathSplit) === 11) {
// Hierarchical structure
if (!in_array($fileId, $fileIds)) {
$fileIds[] = $fileId;
}
} else {
// Flat structure
if (!in_array($fileId, $flatFileIds)) {
$flatFileIds[] = $fileId;
// Walk the preview folder tree via the `parent` column, which is indexed on
// every supported database platform.
//
// Depth from the preview root tells us which structure a leaf folder holds:
// - depth 1: legacy flat structure, e.g. preview/<fileid>/<size>.png
// - depth 8: hierarchical structure, e.g. preview/a/b/c/d/e/f/g/<fileid>/<size>.png
$foldersToVisit = [[$previewRootId, '', 0]];

while ($foldersToVisit !== []) {
[$folderId, $folderName, $depth] = array_pop($foldersToVisit);

// Collect the actual preview files here so migrateFileId() doesn't need to
// list this folder's contents a second time.
$previewEntries = [];
foreach ($cache->getFolderContentsById($folderId) as $entry) {
if ($entry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
$foldersToVisit[] = [$entry->getId(), $entry->getName(), $depth + 1];
} else {
$previewEntries[] = $entry;
}
}
$foundPreview = true;
}

foreach ($fileIds as $fileId) {
try {
$this->migrationService->migrateFileId($fileId, flatPath: false);
} catch (\Exception $e) {
$this->logger->error('Failed to migrate preview with fileId: ' . $fileId . ' (hierarchical file structure)', [
'exception' => $e,
]);
if ($previewEntries === [] || !ctype_digit($folderName)) {
continue;
}
}

foreach ($flatFileIds as $fileId) {
try {
$this->migrationService->migrateFileId($fileId, flatPath: true);
$this->migrationService->migrateFileId((int)$folderName, flatPath: $depth === 1, entries: $previewEntries);
} catch (\Exception $e) {
$this->logger->error('Failed to migrate preview with fileId: ' . $fileId . ' (legacy file structure)', [
$this->logger->error('Failed to migrate preview with fileId: ' . $folderName, [
'exception' => $e,
]);
}

// Stop if execution time is more than one hour.
if (time() - $startTime > 3600) {
return;
}
}
return $foundPreview;

$this->appConfig->setValueBool('core', 'previewMovedDone', true);
}
}
165 changes: 101 additions & 64 deletions lib/private/Preview/PreviewMigrationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use OC\Preview\Db\PreviewMapper;
use OC\Preview\Storage\StorageFactory;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IMimeTypeLoader;
Expand Down Expand Up @@ -44,44 +46,43 @@ public function __construct(
}

/**
* @param array<string|int, string[]> $previewFolders
* @param list<ICacheEntry|SimpleFile>|null $entries Preview file entries already fetched by the caller.
* @return Preview[]
*/
public function migrateFileId(int $fileId, bool $flatPath): array {
public function migrateFileId(int $fileId, bool $flatPath, ?array $entries = null): array {
$previews = [];
$internalPath = $this->getInternalFolder((string)$fileId, $flatPath);
try {
$folder = $this->appData->getFolder($internalPath);
} catch (NotFoundException) {
return [];

if ($entries === null) {
try {
$entries = $this->appData->getFolder($internalPath)->getDirectoryListing();
} catch (NotFoundException) {
return [];
}
}

/**
* @var list<array{file: SimpleFile, preview: Preview}> $previewFiles
* @var list<Preview> $previewsToInsert
*/
$previewFiles = [];
$previewsToInsert = [];

foreach ($folder->getDirectoryListing() as $previewFile) {
$path = $fileId . '/' . $previewFile->getName();
/** @var SimpleFile $previewFile */
foreach ($entries as $entry) {
$path = $fileId . '/' . $entry->getName();
$preview = Preview::fromPath($path, $this->mimeTypeDetector);
if ($preview === false) {
$this->logger->error('Unable to import old preview at path.');
continue;
}
$preview->generateId();
$preview->setSize($previewFile->getSize());
$preview->setMtime($previewFile->getMtime());
$preview->setOldFileId($previewFile->getId());
$preview->setSize($entry->getSize());
$preview->setMtime($entry->getMTime());
$preview->setOldFileId($entry->getId());
$preview->setEncrypted(false);

$previewFiles[] = [
'file' => $previewFile,
'preview' => $preview,
];
$previewsToInsert[] = $preview;
}

if (empty($previewFiles)) {
if (empty($previewsToInsert)) {
$this->deleteFolder($internalPath);

return $previews;
Expand All @@ -98,56 +99,51 @@ public function migrateFileId(int $fileId, bool $flatPath): array {
$cursor->closeCursor();

if ($result !== false) {
foreach ($previewFiles as $previewFile) {
/** @var Preview $preview */
$preview = $previewFile['preview'];
/** @var SimpleFile $file */
$file = $previewFile['file'];
$preview->setStorageId($result['storage']);
$preview->setEtag($result['etag']);
$preview->setSourceMimeType($this->mimeTypeLoader->getMimetypeById((int)$result['mimetype']));
$preview->generateId();
try {
$preview = $this->previewMapper->insert($preview);
} catch (Exception $e) {
if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
throw $e;
$oldFileIdsToDelete = [];
try {
foreach ($previewsToInsert as $preview) {
$preview->setStorageId($result['storage']);
$preview->setEtag($result['etag']);
$preview->setSourceMimeType($this->mimeTypeLoader->getMimetypeById((int)$result['mimetype']));
$preview->generateId();
try {
$preview = $this->previewMapper->insert($preview);
} catch (Exception $e) {
if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
throw $e;
}

// We already have this preview in the preview table, skip
$oldFileIdsToDelete[] = $preview->getOldFileId();
continue;
}

$delete = $this->connection->getQueryBuilder();
// We already have this preview in the preview table, skip
$delete->delete('filecache')
->where($delete->expr()->eq('fileid', $delete->createNamedParameter($file->getId())))
->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId())
->executeStatement();
continue;
}
try {
$this->storageFactory->migratePreview($preview);
// Do not delete the old file via a Node here, as that would also
// delete it from the file system; only its filecache row is stale.
} catch (\Exception $e) {
$this->previewMapper->delete($preview);
throw $e;
}

try {
$this->storageFactory->migratePreview($preview, $file);
$qb = $this->connection->getQueryBuilder();
$qb->delete('filecache')
->where($qb->expr()->eq('fileid', $qb->createNamedParameter($file->getId())))
->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId())
->executeStatement();
// Do not call $file->delete() as this will also delete the file from the file system
} catch (\Exception $e) {
$this->previewMapper->delete($preview);
throw $e;
$oldFileIdsToDelete[] = $preview->getOldFileId();
$previews[] = $preview;
}

$previews[] = $preview;
} finally {
$this->deleteOldFileCacheEntries($oldFileIdsToDelete);
}
} else {
// No matching fileId, delete preview
// No matching fileId, delete the orphaned preview files themselves.
try {
$folder = $this->appData->getFolder($internalPath);
$this->connection->beginTransaction();
foreach ($previewFiles as $previewFile) {
/** @var SimpleFile $file */
$file = $previewFile['file'];
foreach ($folder->getDirectoryListing() as $file) {
$file->delete();
}
$this->connection->commit();
} catch (NotFoundException) {
// Folder already gone, nothing to clean up.
} catch (Exception) {
$this->connection->rollback();
}
Expand All @@ -165,6 +161,23 @@ private static function getInternalFolder(string $name, bool $flatPath): string
return implode('/', str_split(substr(md5($name), 0, 7))) . '/' . $name;
}

/**
* @param list<int> $fileIds
*/
private function deleteOldFileCacheEntries(array $fileIds): void {
if ($fileIds === []) {
return;
}

foreach (array_chunk($fileIds, 1000) as $chunk) {
$qb = $this->connection->getQueryBuilder();
$qb->delete('filecache')
->where($qb->expr()->in('fileid', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)))
->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId())
->executeStatement();
}
}

private function deleteFolder(string $path): void {
$current = $path;

Expand All @@ -185,14 +198,38 @@ private function deleteFolder(string $path): void {
break;
}

try {
$folder = $this->appData->getFolder($current);
} catch (NotFoundException) {
break;
}
if (count($folder->getDirectoryListing()) !== 0) {
if ($this->folderHasChildren($rootFolderId, $this->previewRootPath . $current)) {
break;
}
}
}

private function folderHasChildren(int $storageId, string $path): bool {
$qb = $this->connection->getQueryBuilder();
$qb->select('fileid')
->from('filecache')
->where($qb->expr()->eq('path_hash', $qb->createNamedParameter(md5($path))))
->andWhere($qb->expr()->eq('storage', $qb->createNamedParameter($storageId)))
->setMaxResults(1);
$cursor = $qb->executeQuery();
$folderId = $cursor->fetchOne();
$cursor->closeCursor();

if ($folderId === false) {
// The folder itself is already gone, nothing to check.
return false;
}

$qb = $this->connection->getQueryBuilder();
$qb->select('fileid')
->from('filecache')
->where($qb->expr()->eq('parent', $qb->createNamedParameter((int)$folderId)))
->andWhere($qb->expr()->eq('storage', $qb->createNamedParameter($storageId)))
->setMaxResults(1);
$cursor = $qb->executeQuery();
$hasChild = $cursor->fetchOne() !== false;
$cursor->closeCursor();

return $hasChild;
}
}
3 changes: 1 addition & 2 deletions lib/private/Preview/Storage/IPreviewStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
namespace OC\Preview\Storage;

use Exception;
use OC\Files\SimpleFS\SimpleFile;
use OC\Preview\Db\Preview;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
Expand Down Expand Up @@ -42,7 +41,7 @@ public function deletePreview(Preview $preview): void;
* To remove at some point
* @throws Exception
*/
public function migratePreview(Preview $preview, SimpleFile $file): void;
public function migratePreview(Preview $preview): void;

/**
* @throws NotPermittedException
Expand Down
Loading
Loading