Passed
Push — shared-storage-experiments ( 711936...cbb9c9 )
by Matias
16:42
created

staleImagesRemovalForUser()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 64
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 7.0008

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 7
eloc 36
c 1
b 1
f 0
nc 9
nop 2
dl 0
loc 64
ccs 37
cts 38
cp 0.9737
crap 7.0008
rs 8.4106

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
			// Try to get the file to ensure that exist.
182
			try {
183 2
				$file = $this->fileService->getFileById($image->getFile(), $userId);
184 1
			} catch (\OCP\Files\NotFoundException $e) {
185 1
				$file = null;
186
			}
187
188
			// Delete image doesn't exist anymore in filesystem or it is under .nomedia
189 2
			if (($file === null) || (!$this->fileService->isAllowedNode($file)) ||
190 2
			    ($this->fileService->isUnderNoDetection($file))) {
191 2
				$this->deleteImage($image, $userId);
192 2
				$imagesRemoved++;
193
			}
194
195
			// Remember last processed image
196 2
			$this->config->setUserValue(
197 2
				$userId, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_LAST_CHECKED_KEY, $image->id);
198
199
			// Yield from time to time
200 2
			$processed++;
201 2
			if ($processed % 10 === 0) {
202
				$this->logDebug(sprintf('Processed %d/%d stale images for user %s', $processed, count($allImages), $userId));
203 2
				yield;
204
			}
205
		}
206
207
		// Remove this value when we are done, so next cleanup can start from 0
208 2
		$this->config->deleteUserValue($userId, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_LAST_CHECKED_KEY);
209 2
		return $imagesRemoved;
210
	}
211
212 2
	private function deleteImage(Image $image, string $userId) {
213 2
		$this->logInfo(sprintf('Removing stale image %d for user %s', $image->id, $userId));
214
		// note that invalidatePersons depends on existence of faces for a given image,
215
		// and we must invalidate before we delete faces!
216
		// TODO: this is same method as in Watcher, find where to unify them.
217 2
		$this->personMapper->invalidatePersons($image->id);
218 2
		$this->faceMapper->removeFaces($image->id);
219 2
		$this->imageMapper->delete($image);
220 2
	}
221
}
222