appData = $appDataFactory->get('preview'); $this->setTimeSensitivity(self::TIME_INSENSITIVE); $this->setInterval(24 * 60 * 60); } protected function run(mixed $argument): void { if ($this->appConfig->getValueBool('core', 'previewMovedDone')) { return; } $emptyHierarchicalPreviewFolders = false; $startTime = time(); while (true) { // Check new hierarchical preview folders first if (!$emptyHierarchicalPreviewFolders) { $qb = $this->connection->getQueryBuilder(); $qb->select('*') ->from('filecache') ->where($qb->expr()->like('path', $qb->createNamedParameter('appdata_%/preview/%/%/%/%/%/%/%/%/%'))) ->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId()) ->setMaxResults(100); $result = $qb->executeQuery(); while ($row = $result->fetch()) { $pathSplit = explode('/', $row['path']); assert(count($pathSplit) >= 2); $fileId = $pathSplit[count($pathSplit) - 2]; $this->processPreviews($fileId, false); } } // And then the flat preview folder (legacy) $emptyHierarchicalPreviewFolders = true; $qb = $this->connection->getQueryBuilder(); $qb->select('*') ->from('filecache') ->where($qb->expr()->like('path', $qb->createNamedParameter('appdata_%/preview/%/%.%'))) ->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId()) ->setMaxResults(100); $result = $qb->executeQuery(); $foundOldPreview = false; while ($row = $result->fetch()) { $pathSplit = explode('/', $row['path']); assert(count($pathSplit) >= 2); $fileId = $pathSplit[count($pathSplit) - 2]; array_pop($pathSplit); $this->processPreviews($fileId, true); $foundOldPreview = true; } if (!$foundOldPreview) { break; } // Stop if execution time is more than one hour. if (time() - $startTime > 3600) { return; } } try { // Delete any leftover preview directory $this->appData->getFolder('.')->delete(); } catch (NotFoundException) { // ignore } $this->appConfig->setValueBool('core', 'previewMovedDone', true); } /** * @param array $previewFolders */ private function processPreviews(int|string $fileId, bool $simplePaths): void { $internalPath = $this->getInternalFolder((string)$fileId, $simplePaths); $folder = $this->appData->getFolder($internalPath); /** * @var list $previewFiles */ $previewFiles = []; foreach ($folder->getDirectoryListing() as $previewFile) { /** @var SimpleFile $previewFile */ $preview = Preview::fromPath($fileId . '/' . $previewFile->getName()); $preview->setSize($previewFile->getSize()); $preview->setMtime($previewFile->getMtime()); $preview->setOldFileId($previewFile->getId()); $preview->setEncrypted(false); $previewFiles[] = [ 'file' => $previewFile, 'preview' => $preview, ]; } $qb = $this->connection->getQueryBuilder(); $qb->select('*') ->from('filecache') ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId))) ->setMaxResults(1); $result = $qb->executeQuery(); $result = $result->fetchAll(); if (count($result) > 0) { foreach ($previewFiles as $previewFile) { $preview = $previewFile['preview']; /** @var SimpleFile $file */ $file = $previewFile['file']; $preview->setStorageId($result[0]['storage']); $preview->setEtag($result[0]['etag']); try { $preview = $this->previewMapper->insert($preview); } catch (Exception $e) { // We already have this preview in the preview table, skip continue; } try { $this->storageFactory->migratePreview($preview, $file); $qb->delete('filecache') ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($file->getId()))) ->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; } } } $this->deleteFolder($internalPath, $folder); } public static function getInternalFolder(string $name, bool $simplePaths): string { if ($simplePaths) { return '/' . $name; } return implode('/', str_split(substr(md5($name), 0, 7))) . '/' . $name; } private function deleteFolder(string $path, ISimpleFolder $folder): void { $folder->delete(); $current = $path; while (true) { $current = dirname($current); if ($current === '/' || $current === '.' || $current === '') { break; } $folder = $this->appData->getFolder($current); if (count($folder->getDirectoryListing()) !== 0) { break; } $folder->delete(); } } }