Passed
Push — mem-fixes ( 0dd6bf )
by Matias
04:24
created

StaleImagesRemovalTask   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 168
Duplicated Lines 0 %

Test Coverage

Coverage 97.33%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 75
c 2
b 1
f 0
dl 0
loc 168
ccs 73
cts 75
cp 0.9733
rs 10
wmc 14

5 Methods

Rating   Name   Duplication   Size   Complexity  
A description() 0 2 1
A execute() 0 38 5
A __construct() 0 11 1
A deleteImage() 0 8 1
B staleImagesRemovalForUser() 0 59 6
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017, 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\IConfig;
27
use OCP\IUser;
28
29
use OCP\Files\File;
30
use OCP\Files\Folder;
31
use OCP\Files\Node;
32
33
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionBackgroundTask;
34
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext;
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
use OCA\FaceRecognition\Migration\AddDefaultFaceModel;
40
use OCA\FaceRecognition\Service\FileService;
41
42
/**
43
 * Task that, for each user, crawls for all images in database,
44
 * checks if they actually exist and removes them if they don't.
45
 * It should be executed rarely.
46
 */
47
class StaleImagesRemovalTask extends FaceRecognitionBackgroundTask {
48
	const STALE_IMAGES_REMOVAL_NEEDED_KEY = "stale_images_removal_needed";
49
	const STALE_IMAGES_LAST_CHECKED_KEY = "stale_images_last_checked";
50
51
	/** @var IConfig Config */
52
	private $config;
53
54
	/** @var ImageMapper Image mapper */
55
	private $imageMapper;
56
57
	/** @var FaceMapper Face mapper */
58
	private $faceMapper;
59
60
	/** @var PersonMapper Person mapper */
61
	private $personMapper;
62
63
	/** @var FileService */
64
	private $fileService;
65
66
	/**
67
	 * @param IConfig $config Config
68
	 * @param ImageMapper $imageMapper Image mapper
69
	 * @param FaceMapper $faceMapper Face mapper
70
	 * @param PersonMapper $personMapper Person mapper
71
	 * @param FileService $fileService File Service
72
	 */
73 3
	public function __construct(IConfig      $config,
74
	                            ImageMapper  $imageMapper,
75
	                            FaceMapper   $faceMapper,
76
	                            PersonMapper $personMapper,
77
	                            FileService  $fileService) {
78 3
		parent::__construct();
79 3
		$this->config = $config;
80 3
		$this->imageMapper  = $imageMapper;
81 3
		$this->faceMapper   = $faceMapper;
82 3
		$this->personMapper = $personMapper;
83 3
		$this->fileService  = $fileService;
84 3
	}
85
86
	/**
87
	 * @inheritdoc
88
	 */
89 2
	public function description() {
90 2
		return "Crawl for stale images (either missing in filesystem or under .nomedia) and remove them from DB";
91
	}
92
93
	/**
94
	 * @inheritdoc
95
	 */
96 3
	public function execute(FaceRecognitionContext $context) {
97 3
		$this->setContext($context);
98
99 3
		$model = intval($this->config->getAppValue('facerecognition', 'model', AddDefaultFaceModel::DEFAULT_FACE_MODEL_ID));
100
101
		// Check if we are called for one user only, or for all user in instance.
102 3
		$staleRemovedImages = 0;
103 3
		$eligable_users = array();
104 3
		if (is_null($this->context->user)) {
105 3
			$this->context->userManager->callForSeenUsers(function (IUser $user) use (&$eligable_users) {
106 3
				$eligable_users[] = $user->getUID();
107 3
			});
108
		} else {
109
			$eligable_users[] = $this->context->user->getUID();
110
		}
111
112 3
		foreach($eligable_users as $user) {
113 3
			$staleImagesRemovalNeeded = $this->config->getUserValue(
114 3
				$user, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_REMOVAL_NEEDED_KEY, 'false');
115 3
			if ($staleImagesRemovalNeeded === 'false') {
116
				// Completely skip this task for this user, seems that we already did full scan for him
117 3
				$this->logDebug(sprintf('Skipping stale images removal for user %s as there is no need for it', $user));
118 3
				continue;
119
			}
120
121
			// Since method below can take long time, it is generator itself
122 2
			$generator = $this->staleImagesRemovalForUser($user, $model);
123 2
			foreach ($generator as $_) {
124 2
				yield;
125
			}
126 2
			$staleRemovedImages += $generator->getReturn();
127
128 2
			$this->config->setUserValue($user, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_REMOVAL_NEEDED_KEY, 'false');
129 2
			yield;
130
		}
131
132 3
		$this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages'] = $staleRemovedImages;
133 3
		return true;
134
	}
135
136
	/**
137
	 * Gets all images in database for a given user. For each image, check if it
138
	 * actually present in filesystem (and there is no .nomedia for it) and removes
139
	 * it from database if it is not present.
140
	 *
141
	 * @param string $userId ID of the user for which to remove stale images for
142
	 * @param int $model Used model
143
	 * @return \Generator|int Returns generator during yielding and finally returns int,
144
	 * which represent number of stale images removed
145
	 */
146 2
	private function staleImagesRemovalForUser(string $userId, int $model) {
147
148 2
		$this->fileService->setupFS($userId);
149
150 2
		$this->logDebug(sprintf('Getting all images for user %s', $userId));
151 2
		$allImages = $this->imageMapper->findImages($userId, $model);
152 2
		$this->logDebug(sprintf('Found %d images for user %s', count($allImages), $userId));
153 2
		yield;
154
155
		// Find if we stopped somewhere abruptly before. If we are, we need to start from that point.
156
		// If there is value, we start from beggining. Important is that:
157
		// * There needs to be some (any!) ordering here, we used "id" for ordering key
158
		// * New images will be processed, or some might be checked more than once, and that is OK
159
		//   Important part is that we make continuous progess.
160 2
		$lastChecked = intval($this->config->getUserValue(
161 2
			$userId, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_LAST_CHECKED_KEY, '0'));
162 2
		$this->logDebug(sprintf('Last checked image id for user %s is %d', $userId, $lastChecked));
163 2
		yield;
164
165
		// Now filter by those above last checked and sort remaining images
166 2
		$allImages = array_filter($allImages, function ($i) use($lastChecked) {
167 2
			return $i->id > $lastChecked;
168 2
		});
169 2
		usort($allImages, function ($i1, $i2) {
170 1
			return $i1->id <=> $i2->id;
171 2
		});
172 2
		$this->logDebug(sprintf(
173 2
			'After filtering and sorting, there is %d remaining stale images to check for user %s',
174 2
			count($allImages), $userId));
175 2
		yield;
176
177
		// Now iterate and check remaining images
178 2
		$processed = 0;
179 2
		$imagesRemoved = 0;
180 2
		foreach ($allImages as $image) {
181 2
			$file = $this->fileService->getFileById($image->getFile(), $userId);
182
183
			// Delete image doesn't exist anymore in filesystem or it is under .nomedia
184 2
			if (($file === null) || (!$this->fileService->isAllowedNode($file)) ||
185 2
			    ($this->fileService->isUnderNoDetection($file))) {
186 2
				$this->deleteImage($image, $userId);
187 2
				$imagesRemoved++;
188
			}
189
190
			// Remember last processed image
191 2
			$this->config->setUserValue(
192 2
				$userId, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_LAST_CHECKED_KEY, $image->id);
193
194
			// Yield from time to time
195 2
			$processed++;
196 2
			if ($processed % 10 === 0) {
197
				$this->logDebug(sprintf('Processed %d/%d stale images for user %s', $processed, count($allImages), $userId));
198 2
				yield;
199
			}
200
		}
201
202
		// Remove this value when we are done, so next cleanup can start from 0
203 2
		$this->config->deleteUserValue($userId, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_LAST_CHECKED_KEY);
204 2
		return $imagesRemoved;
205
	}
206
207 2
	private function deleteImage(Image $image, string $userId) {
208 2
		$this->logInfo(sprintf('Removing stale image %d for user %s', $image->id, $userId));
209
		// note that invalidatePersons depends on existence of faces for a given image,
210
		// and we must invalidate before we delete faces!
211
		// TODO: this is same method as in Watcher, find where to unify them.
212 2
		$this->personMapper->invalidatePersons($image->id);
213 2
		$this->faceMapper->removeFaces($image->id);
214 2
		$this->imageMapper->delete($image);
215 2
	}
216
}
217