Passed
Push — master ( 476755...eac637 )
by Blizzz
14:31 queued 12s
created

MigrateBackgroundImages::readUserIdsToProcess()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 12
c 1
b 0
f 0
nc 5
nop 0
dl 0
loc 16
rs 9.8666
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2022 Arthur Schiwon <[email protected]>
7
 *
8
 * @author Arthur Schiwon <[email protected]>
9
 *
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OCA\Theming\Jobs;
28
29
use OCA\Theming\AppInfo\Application;
30
use OCP\AppFramework\Utility\ITimeFactory;
31
use OCP\BackgroundJob\IJobList;
32
use OCP\BackgroundJob\QueuedJob;
33
use OCP\Files\AppData\IAppDataFactory;
34
use OCP\Files\IAppData;
35
use OCP\Files\NotFoundException;
36
use OCP\Files\NotPermittedException;
37
use OCP\Files\SimpleFS\ISimpleFolder;
38
use OCP\IDBConnection;
39
use Psr\Log\LoggerInterface;
40
41
class MigrateBackgroundImages extends QueuedJob {
42
	public const TIME_SENSITIVE = 0;
43
44
	public const STAGE_PREPARE = 'prepare';
45
	public const STAGE_EXECUTE = 'execute';
46
	// will be saved in appdata/theming/global/
47
	protected const STATE_FILE_NAME = '25_dashboard_to_theming_migration_users.json';
48
49
	private IAppDataFactory $appDataFactory;
50
	private IJobList $jobList;
51
	private IDBConnection $dbc;
52
	private IAppData $appData;
53
	private LoggerInterface $logger;
54
55
	public function __construct(
56
		ITimeFactory $time,
57
		IAppDataFactory $appDataFactory,
58
		IJobList $jobList,
59
		IDBConnection $dbc,
60
		IAppData $appData,
61
		LoggerInterface $logger
62
	) {
63
		parent::__construct($time);
64
		$this->appDataFactory = $appDataFactory;
65
		$this->jobList = $jobList;
66
		$this->dbc = $dbc;
67
		$this->appData = $appData;
68
		$this->logger = $logger;
69
	}
70
71
	protected function run($argument): void {
72
		if (!isset($argument['stage'])) {
73
			// not executed in 25.0.0?!
74
			$argument['stage'] = self::STAGE_PREPARE;
75
		}
76
77
		switch ($argument['stage']) {
78
			case self::STAGE_PREPARE:
79
				$this->runPreparation();
80
				break;
81
			case self::STAGE_EXECUTE:
82
				$this->runMigration();
83
				break;
84
			default:
85
				break;
86
		}
87
	}
88
89
	protected function runPreparation(): void {
90
		try {
91
			$selector = $this->dbc->getQueryBuilder();
92
			$result = $selector->select('userid')
93
				->from('preferences')
94
				->where($selector->expr()->eq('appid', $selector->createNamedParameter('theming')))
95
				->andWhere($selector->expr()->eq('configkey', $selector->createNamedParameter('background')))
96
				->andWhere($selector->expr()->eq('configvalue', $selector->createNamedParameter('custom')))
97
				->executeQuery();
98
99
			$userIds = $result->fetchAll(\PDO::FETCH_COLUMN);
100
			$this->storeUserIdsToProcess($userIds);
101
		} catch (\Throwable $t) {
102
			$this->jobList->add(self::class, self::STAGE_PREPARE);
103
			throw $t;
104
		}
105
		$this->jobList->add(self::class, self::STAGE_EXECUTE);
106
	}
107
108
	/**
109
	 * @throws NotPermittedException
110
	 * @throws NotFoundException
111
	 */
112
	protected function runMigration(): void {
113
		$allUserIds = $this->readUserIdsToProcess();
114
		$notSoFastMode = count($allUserIds) > 5000;
115
		$dashboardData = $this->appDataFactory->get('dashboard');
116
117
		$userIds = $notSoFastMode ? array_slice($allUserIds, 0, 5000) : $allUserIds;
118
		foreach ($userIds as $userId) {
119
			try {
120
				// migration
121
				$file = $dashboardData->getFolder($userId)->getFile('background.jpg');
122
				$targetDir = $this->getUserFolder($userId);
123
124
				if (!$targetDir->fileExists('background.jpg')) {
125
					$targetDir->newFile('background.jpg', $file->getContent());
126
				}
127
				$file->delete();
128
			} catch (NotFoundException|NotPermittedException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
129
			}
130
		}
131
132
		if ($notSoFastMode) {
133
			$remainingUserIds = array_slice($allUserIds, 5000);
134
			$this->storeUserIdsToProcess($remainingUserIds);
135
			$this->jobList->add(self::class, ['stage' => self::STAGE_EXECUTE]);
136
		} else {
137
			$this->deleteStateFile();
138
		}
139
	}
140
141
	/**
142
	 * @throws NotPermittedException
143
	 * @throws NotFoundException
144
	 */
145
	protected function readUserIdsToProcess(): array {
146
		$globalFolder = $this->appData->getFolder('global');
147
		if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
148
			$file = $globalFolder->getFile(self::STATE_FILE_NAME);
149
			try {
150
				$userIds = \json_decode($file->getContent(), true);
151
			} catch (NotFoundException $e) {
152
				$userIds = [];
153
			}
154
			if ($userIds === null) {
155
				$userIds = [];
156
			}
157
		} else {
158
			$userIds = [];
159
		}
160
		return $userIds;
161
	}
162
163
	/**
164
	 * @throws NotFoundException
165
	 */
166
	protected function storeUserIdsToProcess(array $userIds): void {
167
		$storableUserIds = \json_encode($userIds);
168
		$globalFolder = $this->appData->getFolder('global');
169
		try {
170
			if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
171
				$file = $globalFolder->getFile(self::STATE_FILE_NAME);
172
			} else {
173
				$file = $globalFolder->newFile(self::STATE_FILE_NAME);
174
			}
175
			$file->putContent($storableUserIds);
176
		} catch (NotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
177
		} catch (NotPermittedException $e) {
178
			$this->logger->warning('Lacking permissions to create {file}',
179
				[
180
					'app' => 'theming',
181
					'file' => self::STATE_FILE_NAME,
182
					'exception' => $e,
183
				]
184
			);
185
		}
186
	}
187
188
	/**
189
	 * @throws NotFoundException
190
	 */
191
	protected function deleteStateFile(): void {
192
		$globalFolder = $this->appData->getFolder('global');
193
		if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
194
			$file = $globalFolder->getFile(self::STATE_FILE_NAME);
195
			try {
196
				$file->delete();
197
			} catch (NotPermittedException $e) {
198
				$this->logger->info('Could not delete {file} due to permissions. It is safe to delete manually inside data -> appdata -> theming -> global.',
199
					[
200
						'app' => 'theming',
201
						'file' => $file->getName(),
202
						'exception' => $e,
203
					]
204
				);
205
			}
206
		}
207
	}
208
209
	/**
210
	 * Get the root location for users theming data
211
	 */
212
	protected function getUserFolder(string $userId): ISimpleFolder {
213
		$themingData = $this->appDataFactory->get(Application::APP_ID);
214
215
		try {
216
			$rootFolder = $themingData->getFolder('users');
217
		} catch (NotFoundException $e) {
218
			$rootFolder = $themingData->newFolder('users');
219
		}
220
221
		try {
222
			return $rootFolder->getFolder($userId);
223
		} catch (NotFoundException $e) {
224
			return $rootFolder->newFolder($userId);
225
		}
226
	}
227
}
228