Passed
Push — master ( 0059ba...f54c15 )
by Matias
18:33 queued 04:55
created

StaleImagesRemovalTask   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 165
Duplicated Lines 0 %

Test Coverage

Coverage 74.29%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 68
c 2
b 1
f 0
dl 0
loc 165
ccs 52
cts 70
cp 0.7429
rs 10
wmc 14

5 Methods

Rating   Name   Duplication   Size   Complexity  
A deleteImage() 0 8 1
A description() 0 2 1
A execute() 0 35 5
A __construct() 0 13 1
B staleImagesRemovalForUser() 0 59 6
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 2
	public function __construct(ImageMapper     $imageMapper,
73
	                            FaceMapper      $faceMapper,
74
	                            PersonMapper    $personMapper,
75
	                            FileService     $fileService,
76
	                            SettingsService $settingsService)
77
	{
78 2
		parent::__construct();
79
80 2
		$this->imageMapper     = $imageMapper;
81 2
		$this->faceMapper      = $faceMapper;
82 2
		$this->personMapper    = $personMapper;
83 2
		$this->fileService     = $fileService;
84 2
		$this->settingsService = $settingsService;
85 2
	}
86
87
	/**
88
	 * @inheritdoc
89
	 */
90 1
	public function description() {
91 1
		return "Crawl for stale images (either missing in filesystem or under .nomedia) and remove them from DB";
92
	}
93
94
	/**
95
	 * @inheritdoc
96
	 */
97 2
	public function execute(FaceRecognitionContext $context) {
98 2
		$this->setContext($context);
99
100
		// Check if we are called for one user only, or for all user in instance.
101 2
		$staleRemovedImages = 0;
102 2
		$eligable_users = array();
103 2
		if (is_null($this->context->user)) {
104 2
			$this->context->userManager->callForSeenUsers(function (IUser $user) use (&$eligable_users) {
105 2
				$eligable_users[] = $user->getUID();
106 2
			});
107
		} else {
108
			$eligable_users[] = $this->context->user->getUID();
109
		}
110
111 2
		foreach($eligable_users as $user) {
112 2
			if (!$this->settingsService->getNeedRemoveStaleImages($user)) {
113
				// Completely skip this task for this user, seems that we already did full scan for him
114 2
				$this->logDebug(sprintf('Skipping stale images removal for user %s as there is no need for it', $user));
115 2
				continue;
116
			}
117
118
			// Since method below can take long time, it is generator itself
119 1
			$generator = $this->staleImagesRemovalForUser($user, $this->settingsService->getCurrentFaceModel());
120 1
			foreach ($generator as $_) {
121 1
				yield;
122
			}
123 1
			$staleRemovedImages += $generator->getReturn();
124
125 1
			$this->settingsService->setNeedRemoveStaleImages(false, $user);
126
127 1
			yield;
128
		}
129
130 2
		$this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages'] = $staleRemovedImages;
131 2
		return true;
132
	}
133
134
	/**
135
	 * Gets all images in database for a given user. For each image, check if it
136
	 * actually present in filesystem (and there is no .nomedia for it) and removes
137
	 * it from database if it is not present.
138
	 *
139
	 * @param string $userId ID of the user for which to remove stale images for
140
	 * @param int $model Used model
141
	 * @return \Generator|int Returns generator during yielding and finally returns int,
142
	 * which represent number of stale images removed
143
	 */
144 1
	private function staleImagesRemovalForUser(string $userId, int $model) {
145
146 1
		$this->fileService->setupFS($userId);
147
148 1
		$this->logDebug(sprintf('Getting all images for user %s', $userId));
149 1
		$allImages = $this->imageMapper->findImages($userId, $model);
150 1
		$this->logDebug(sprintf('Found %d images for user %s', count($allImages), $userId));
151 1
		yield;
152
153
		// Find if we stopped somewhere abruptly before. If we are, we need to start from that point.
154
		// If there is value, we start from beggining. Important is that:
155
		// * There needs to be some (any!) ordering here, we used "id" for ordering key
156
		// * New images will be processed, or some might be checked more than once, and that is OK
157
		//   Important part is that we make continuous progess.
158
159 1
		$lastChecked = $this->settingsService->getLastStaleImageChecked($userId);
160 1
		$this->logDebug(sprintf('Last checked image id for user %s is %d', $userId, $lastChecked));
161 1
		yield;
162
163
		// Now filter by those above last checked and sort remaining images
164 1
		$allImages = array_filter($allImages, function ($i) use($lastChecked) {
165
			return $i->id > $lastChecked;
166 1
		});
167 1
		usort($allImages, function ($i1, $i2) {
168
			return $i1->id <=> $i2->id;
169 1
		});
170 1
		$this->logDebug(sprintf(
171 1
			'After filtering and sorting, there is %d remaining stale images to check for user %s',
172 1
			count($allImages), $userId));
173 1
		yield;
174
175
		// Now iterate and check remaining images
176 1
		$processed = 0;
177 1
		$imagesRemoved = 0;
178 1
		foreach ($allImages as $image) {
179
			$file = $this->fileService->getFileById($image->getFile(), $userId);
180
181
			// Delete image doesn't exist anymore in filesystem or it is under .nomedia
182
			if (($file === null) || (!$this->fileService->isAllowedNode($file)) ||
183
			    ($this->fileService->isUnderNoDetection($file))) {
184
				$this->deleteImage($image, $userId);
185
				$imagesRemoved++;
186
			}
187
188
			// Remember last processed image
189
			$this->settingsService->setLastStaleImageChecked($image->id, $userId);
190
191
			// Yield from time to time
192
			$processed++;
193
			if ($processed % 10 === 0) {
194
				$this->logDebug(sprintf('Processed %d/%d stale images for user %s', $processed, count($allImages), $userId));
195
				yield;
196
			}
197
		}
198
199
		// Remove this value when we are done, so next cleanup can start from 0
200 1
		$this->settingsService->setLastStaleImageChecked(0, $userId);
201
202 1
		return $imagesRemoved;
203
	}
204
205
	private function deleteImage(Image $image, string $userId) {
206
		$this->logInfo(sprintf('Removing stale image %d for user %s', $image->id, $userId));
207
		// note that invalidatePersons depends on existence of faces for a given image,
208
		// and we must invalidate before we delete faces!
209
		// TODO: this is same method as in Watcher, find where to unify them.
210
		$this->personMapper->invalidatePersons($image->id);
211
		$this->faceMapper->removeFromImage($image->id);
212
		$this->imageMapper->delete($image);
213
	}
214
}
215