MigrateBackgroundImages.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\Theming\Jobs;
  8. use OCA\Theming\AppInfo\Application;
  9. use OCP\AppFramework\Utility\ITimeFactory;
  10. use OCP\BackgroundJob\IJobList;
  11. use OCP\BackgroundJob\QueuedJob;
  12. use OCP\DB\QueryBuilder\IQueryBuilder;
  13. use OCP\Files\AppData\IAppDataFactory;
  14. use OCP\Files\IAppData;
  15. use OCP\Files\NotFoundException;
  16. use OCP\Files\NotPermittedException;
  17. use OCP\Files\SimpleFS\ISimpleFolder;
  18. use OCP\IDBConnection;
  19. use Psr\Log\LoggerInterface;
  20. class MigrateBackgroundImages extends QueuedJob {
  21. public const TIME_SENSITIVE = 0;
  22. public const STAGE_PREPARE = 'prepare';
  23. public const STAGE_EXECUTE = 'execute';
  24. // will be saved in appdata/theming/global/
  25. protected const STATE_FILE_NAME = '25_dashboard_to_theming_migration_users.json';
  26. private IAppDataFactory $appDataFactory;
  27. private IJobList $jobList;
  28. private IDBConnection $dbc;
  29. private IAppData $appData;
  30. private LoggerInterface $logger;
  31. public function __construct(
  32. ITimeFactory $time,
  33. IAppDataFactory $appDataFactory,
  34. IJobList $jobList,
  35. IDBConnection $dbc,
  36. IAppData $appData,
  37. LoggerInterface $logger
  38. ) {
  39. parent::__construct($time);
  40. $this->appDataFactory = $appDataFactory;
  41. $this->jobList = $jobList;
  42. $this->dbc = $dbc;
  43. $this->appData = $appData;
  44. $this->logger = $logger;
  45. }
  46. protected function run(mixed $argument): void {
  47. if (!is_array($argument) || !isset($argument['stage'])) {
  48. throw new \Exception('Job '.self::class.' called with wrong argument');
  49. }
  50. switch ($argument['stage']) {
  51. case self::STAGE_PREPARE:
  52. $this->runPreparation();
  53. break;
  54. case self::STAGE_EXECUTE:
  55. $this->runMigration();
  56. break;
  57. default:
  58. break;
  59. }
  60. }
  61. protected function runPreparation(): void {
  62. try {
  63. $selector = $this->dbc->getQueryBuilder();
  64. $result = $selector->select('userid')
  65. ->from('preferences')
  66. ->where($selector->expr()->eq('appid', $selector->createNamedParameter('theming')))
  67. ->andWhere($selector->expr()->eq('configkey', $selector->createNamedParameter('background')))
  68. ->andWhere($selector->expr()->eq('configvalue', $selector->createNamedParameter('custom', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
  69. ->executeQuery();
  70. $userIds = $result->fetchAll(\PDO::FETCH_COLUMN);
  71. $this->storeUserIdsToProcess($userIds);
  72. } catch (\Throwable $t) {
  73. $this->jobList->add(self::class, ['stage' => self::STAGE_PREPARE]);
  74. throw $t;
  75. }
  76. $this->jobList->add(self::class, ['stage' => self::STAGE_EXECUTE]);
  77. }
  78. /**
  79. * @throws NotPermittedException
  80. * @throws NotFoundException
  81. */
  82. protected function runMigration(): void {
  83. $allUserIds = $this->readUserIdsToProcess();
  84. $notSoFastMode = count($allUserIds) > 5000;
  85. $dashboardData = $this->appDataFactory->get('dashboard');
  86. $userIds = $notSoFastMode ? array_slice($allUserIds, 0, 5000) : $allUserIds;
  87. foreach ($userIds as $userId) {
  88. try {
  89. // migration
  90. $file = $dashboardData->getFolder($userId)->getFile('background.jpg');
  91. $targetDir = $this->getUserFolder($userId);
  92. if (!$targetDir->fileExists('background.jpg')) {
  93. $targetDir->newFile('background.jpg', $file->getContent());
  94. }
  95. $file->delete();
  96. } catch (NotFoundException|NotPermittedException $e) {
  97. }
  98. }
  99. if ($notSoFastMode) {
  100. $remainingUserIds = array_slice($allUserIds, 5000);
  101. $this->storeUserIdsToProcess($remainingUserIds);
  102. $this->jobList->add(self::class, ['stage' => self::STAGE_EXECUTE]);
  103. } else {
  104. $this->deleteStateFile();
  105. }
  106. }
  107. /**
  108. * @throws NotPermittedException
  109. * @throws NotFoundException
  110. */
  111. protected function readUserIdsToProcess(): array {
  112. $globalFolder = $this->appData->getFolder('global');
  113. if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
  114. $file = $globalFolder->getFile(self::STATE_FILE_NAME);
  115. try {
  116. $userIds = \json_decode($file->getContent(), true);
  117. } catch (NotFoundException $e) {
  118. $userIds = [];
  119. }
  120. if ($userIds === null) {
  121. $userIds = [];
  122. }
  123. } else {
  124. $userIds = [];
  125. }
  126. return $userIds;
  127. }
  128. /**
  129. * @throws NotFoundException
  130. */
  131. protected function storeUserIdsToProcess(array $userIds): void {
  132. $storableUserIds = \json_encode($userIds);
  133. $globalFolder = $this->appData->getFolder('global');
  134. try {
  135. if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
  136. $file = $globalFolder->getFile(self::STATE_FILE_NAME);
  137. } else {
  138. $file = $globalFolder->newFile(self::STATE_FILE_NAME);
  139. }
  140. $file->putContent($storableUserIds);
  141. } catch (NotFoundException $e) {
  142. } catch (NotPermittedException $e) {
  143. $this->logger->warning('Lacking permissions to create {file}',
  144. [
  145. 'app' => 'theming',
  146. 'file' => self::STATE_FILE_NAME,
  147. 'exception' => $e,
  148. ]
  149. );
  150. }
  151. }
  152. /**
  153. * @throws NotFoundException
  154. */
  155. protected function deleteStateFile(): void {
  156. $globalFolder = $this->appData->getFolder('global');
  157. if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
  158. $file = $globalFolder->getFile(self::STATE_FILE_NAME);
  159. try {
  160. $file->delete();
  161. } catch (NotPermittedException $e) {
  162. $this->logger->info('Could not delete {file} due to permissions. It is safe to delete manually inside data -> appdata -> theming -> global.',
  163. [
  164. 'app' => 'theming',
  165. 'file' => $file->getName(),
  166. 'exception' => $e,
  167. ]
  168. );
  169. }
  170. }
  171. }
  172. /**
  173. * Get the root location for users theming data
  174. */
  175. protected function getUserFolder(string $userId): ISimpleFolder {
  176. $themingData = $this->appDataFactory->get(Application::APP_ID);
  177. try {
  178. $rootFolder = $themingData->getFolder('users');
  179. } catch (NotFoundException $e) {
  180. $rootFolder = $themingData->newFolder('users');
  181. }
  182. try {
  183. return $rootFolder->getFolder($userId);
  184. } catch (NotFoundException $e) {
  185. return $rootFolder->newFolder($userId);
  186. }
  187. }
  188. }