Passed
Push — master ( a86ea0...028966 )
by Matias
05:02
created

StaleImagesRemovalTask::execute()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 16
nc 4
nop 1
dl 0
loc 29
ccs 17
cts 17
cp 1
crap 5
rs 9.4222
c 1
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017-2020 Matias De lellis <[email protected]>
4
 * @copyright Copyright (c) 2018, Branko Kokanovic <[email protected]>
5
 *
6
 * @author Branko Kokanovic <[email protected]>
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 *
23
 */
24
namespace OCA\FaceRecognition\BackgroundJob\Tasks;
25
26
use OCP\IUser;
27
28
use OCP\Files\File;
29
use OCP\Files\Folder;
30
use OCP\Files\Node;
31
32
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionBackgroundTask;
33
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext;
34
35
use OCA\FaceRecognition\Db\Image;
36
use OCA\FaceRecognition\Db\ImageMapper;
37
use OCA\FaceRecognition\Db\FaceMapper;
38
use OCA\FaceRecognition\Db\PersonMapper;
39
40
use OCA\FaceRecognition\Service\FileService;
41
use OCA\FaceRecognition\Service\SettingsService;
42
43
/**
44
 * Task that, for each user, crawls for all images in database,
45
 * checks if they actually exist and removes them if they don't.
46
 * It should be executed rarely.
47
 */
48
class StaleImagesRemovalTask extends FaceRecognitionBackgroundTask {
49
50
	/** @var ImageMapper Image mapper */
51
	private $imageMapper;
52
53
	/** @var FaceMapper Face mapper */
54
	private $faceMapper;
55
56
	/** @var PersonMapper Person mapper */
57
	private $personMapper;
58
59
	/** @var FileService  File service*/
60
	private $fileService;
61
62
	/** @var SettingsService */
63
	private $settingsService;
64
65
	/**
66
	 * @param ImageMapper $imageMapper Image mapper
67
	 * @param FaceMapper $faceMapper Face mapper
68
	 * @param PersonMapper $personMapper Person mapper
69
	 * @param FileService $fileService File Service
70
	 * @param SettingsService $settingsService Settings Service
71
	 */
72 3
	public function __construct(ImageMapper     $imageMapper,
73
	                            FaceMapper      $faceMapper,
74
	                            PersonMapper    $personMapper,
75
	                            FileService     $fileService,
76
	                            SettingsService $settingsService)
77
	{
78 3
		parent::__construct();
79
80 3
		$this->imageMapper     = $imageMapper;
81 3
		$this->faceMapper      = $faceMapper;
82 3
		$this->personMapper    = $personMapper;
83 3
		$this->fileService     = $fileService;
84 3
		$this->settingsService = $settingsService;
85
	}
86
87
	/**
88
	 * @inheritdoc
89
	 */
90 2
	public function description() {
91 2
		return "Crawl for stale images (either missing in filesystem or under .nomedia) and remove them from DB";
92
	}
93
94
	/**
95
	 * @inheritdoc
96
	 */
97 3
	public function execute(FaceRecognitionContext $context) {
98 3
		$this->setContext($context);
99
100 3
		$staleRemovedImages = 0;
101
102 3
		$eligable_users = $this->context->getEligibleUsers();
103 3
		foreach($eligable_users as $user) {
104 3
			if (!$this->context->isRunningInSyncMode() &&
105 3
			    !$this->settingsService->getNeedRemoveStaleImages($user)) {
106
				// Completely skip this task for this user, seems that we already did full scan for him
107 3
				$this->logDebug(sprintf('Skipping stale images removal for user %s as there is no need for it', $user));
108 3
				continue;
109
			}
110
111
			// Since method below can take long time, it is generator itself
112 2
			$generator = $this->staleImagesRemovalForUser($user, $this->settingsService->getCurrentFaceModel());
113 2
			foreach ($generator as $_) {
114 2
				yield;
115
			}
116 2
			$staleRemovedImages += $generator->getReturn();
117
118 2
			$this->settingsService->setNeedRemoveStaleImages(false, $user);
119
120 2
			yield;
121
		}
122
123
		// NOTE: Dont remove, it is used within the Integration tests
124 3
		$this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages'] = $staleRemovedImages;
125 3
		return true;
126
	}
127
128
	/**
129
	 * Gets all images in database for a given user. For each image, check if it
130
	 * actually present in filesystem (and there is no .nomedia for it) and removes
131
	 * it from database if it is not present.
132
	 *
133
	 * @param string $userId ID of the user for which to remove stale images for
134
	 * @param int $model Used model
135
	 * @return \Generator|int Returns generator during yielding and finally returns int,
136
	 * which represent number of stale images removed
137
	 */
138 2
	private function staleImagesRemovalForUser(string $userId, int $model) {
139
140 2
		$this->fileService->setupFS($userId);
141
142 2
		$this->logDebug(sprintf('Getting all images for user %s', $userId));
143 2
		$allImages = $this->imageMapper->findImages($userId, $model);
144 2
		$this->logDebug(sprintf('Found %d images for user %s', count($allImages), $userId));
145 2
		yield;
146
147
		// Find if we stopped somewhere abruptly before. If we are, we need to start from that point.
148
		// If there is value, we start from beggining. Important is that:
149
		// * There needs to be some (any!) ordering here, we used "id" for ordering key
150
		// * New images will be processed, or some might be checked more than once, and that is OK
151
		//   Important part is that we make continuous progess.
152
153 2
		$lastChecked = $this->settingsService->getLastStaleImageChecked($userId);
154 2
		$this->logDebug(sprintf('Last checked image id for user %s is %d', $userId, $lastChecked));
155 2
		yield;
156
157
		// Now filter by those above last checked and sort remaining images
158 2
		$allImages = array_filter($allImages, function ($i) use($lastChecked) {
159 2
			return $i->id > $lastChecked;
160 2
		});
161 2
		usort($allImages, function ($i1, $i2) {
162 1
			return $i1->id <=> $i2->id;
163 2
		});
164 2
		$this->logDebug(sprintf(
165 2
			'After filtering and sorting, there is %d remaining stale images to check for user %s',
166 2
			count($allImages), $userId));
167 2
		yield;
168
169
		// Now iterate and check remaining images
170 2
		$processed = 0;
171 2
		$imagesRemoved = 0;
172 2
		foreach ($allImages as $image) {
173 2
			$file = $this->fileService->getFileById($image->getFile(), $userId);
174
175
			// Delete image doesn't exist anymore in filesystem or it is under .nomedia
176 2
			if (($file === null) || (!$this->fileService->isAllowedNode($file)) ||
177 2
			    ($this->fileService->isUnderNoDetection($file))) {
178 2
				$this->deleteImage($image, $userId);
179 2
				$imagesRemoved++;
180
			}
181
182
			// Remember last processed image
183 2
			$this->settingsService->setLastStaleImageChecked($image->id, $userId);
184
185
			// Yield from time to time
186 2
			$processed++;
187 2
			if ($processed % 10 === 0) {
188
				$this->logDebug(sprintf('Processed %d/%d stale images for user %s', $processed, count($allImages), $userId));
189
				yield;
190
			}
191
		}
192
193
		// Remove this value when we are done, so next cleanup can start from 0
194 2
		$this->settingsService->setLastStaleImageChecked(0, $userId);
195
196 2
		return $imagesRemoved;
197
	}
198
199 2
	private function deleteImage(Image $image, string $userId): void {
200 2
		$this->logInfo(sprintf('Removing stale image %d for user %s', $image->id, $userId));
201
		// note that invalidatePersons depends on existence of faces for a given image,
202
		// and we must invalidate before we delete faces!
203
		// TODO: this is same method as in Watcher, find where to unify them.
204 2
		$this->personMapper->invalidatePersons($image->id);
205 2
		$this->faceMapper->removeFromImage($image->id);
206 2
		$this->imageMapper->delete($image);
207
	}
208
}
209