Passed
Push — translations ( 9a02a6...bc5e48 )
by Matias
04:30
created

staleImagesRemovalForUser()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 62
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 5.0003

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 5
eloc 37
c 1
b 1
f 0
nc 7
nop 2
dl 0
loc 62
ccs 39
cts 40
cp 0.975
crap 5.0003
rs 9.0168

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\IHomeStorage;
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
41
/**
42
 * Task that, for each user, crawls for all images in database,
43
 * checks if they actually exist and removes them if they don't.
44
 * It should be executed rarely.
45
 */
46
class StaleImagesRemovalTask extends FaceRecognitionBackgroundTask {
47
	const STALE_IMAGES_REMOVAL_NEEDED_KEY = "stale_images_removal_needed";
48
	const STALE_IMAGES_LAST_CHECKED_KEY = "stale_images_last_checked";
49
50
	/** @var IConfig Config */
51
	private $config;
52
53
	/** @var ImageMapper Image mapper */
54
	private $imageMapper;
55
56
	/** @var FaceMapper Face mapper */
57
	private $faceMapper;
58
59
	/** @var PersonMapper Person mapper */
60
	private $personMapper;
61
62
	/**
63
	 * @param IConfig $config Config
64
	 * @param ImageMapper $imageMapper Image mapper
65
	 * @param FaceMapper $faceMapper Face mapper
66
	 * @param PersonMapper $personMapper Person mapper
67
	 */
68 3
	public function __construct(IConfig $config, ImageMapper $imageMapper, FaceMapper $faceMapper, PersonMapper $personMapper) {
69 3
		parent::__construct();
70 3
		$this->config = $config;
71 3
		$this->imageMapper = $imageMapper;
72 3
		$this->faceMapper = $faceMapper;
73 3
		$this->personMapper = $personMapper;
74 3
	}
75
76
	/**
77
	 * @inheritdoc
78
	 */
79 2
	public function description() {
80 2
		return "Crawl for stale images (either missing in filesystem or under .nomedia) and remove them from DB";
81
	}
82
83
	/**
84
	 * @inheritdoc
85
	 */
86 3
	public function execute(FaceRecognitionContext $context) {
87 3
		$this->setContext($context);
88
89 3
		$model = intval($this->config->getAppValue('facerecognition', 'model', AddDefaultFaceModel::DEFAULT_FACE_MODEL_ID));
90
91
		// Check if we are called for one user only, or for all user in instance.
92 3
		$staleRemovedImages = 0;
93 3
		$eligable_users = array();
94 3
		if (is_null($this->context->user)) {
95 3
			$this->context->userManager->callForSeenUsers(function (IUser $user) use (&$eligable_users) {
96 3
				$eligable_users[] = $user->getUID();
97 3
			});
98
		} else {
99
			$eligable_users[] = $this->context->user->getUID();
100
		}
101
102 3
		foreach($eligable_users as $user) {
103 3
			$staleImagesRemovalNeeded = $this->config->getUserValue(
104 3
				$user, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_REMOVAL_NEEDED_KEY, 'false');
105 3
			if ($staleImagesRemovalNeeded === 'false') {
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, $model);
113 2
			foreach ($generator as $_) {
114 2
				yield;
115
			}
116 2
			$staleRemovedImages += $generator->getReturn();
117
118 2
			$this->config->setUserValue($user, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_REMOVAL_NEEDED_KEY, 'false');
119 2
			yield;
120
		}
121
122 3
		$this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages'] = $staleRemovedImages;
123 3
		return true;
124
	}
125
126
	/**
127
	 * Gets all images in database for a given user. For each image, check if it
128
	 * actually present in filesystem (and there is no .nomedia for it) and removes
129
	 * it from database if it is not present.
130
	 *
131
	 * @param string $userId ID of the user for which to remove stale images for
132
	 * @param int $model Used model
133
	 * @return \Generator|int Returns generator during yielding and finally returns int,
134
	 * which represent number of stale images removed
135
	 */
136 2
	private function staleImagesRemovalForUser(string $userId, int $model) {
137 2
		\OC_Util::tearDownFS();
0 ignored issues
show
Bug introduced by
The type OC_Util was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
138 2
		\OC_Util::setupFS($userId);
139
140 2
		$this->logDebug(sprintf('Getting all images for user %s', $userId));
141 2
		$allImages = $this->imageMapper->findImages($userId, $model);
142 2
		$this->logDebug(sprintf('Found %d images for user %s', count($allImages), $userId));
143 2
		yield;
144
145
		// Find if we stopped somewhere abruptly before. If we are, we need to start from that point.
146
		// If there is value, we start from beggining. Important is that:
147
		// * There needs to be some (any!) ordering here, we used "id" for ordering key
148
		// * New images will be processed, or some might be checked more than once, and that is OK
149
		//   Important part is that we make continuous progess.
150 2
		$lastChecked = intval($this->config->getUserValue(
151 2
			$userId, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_LAST_CHECKED_KEY, '0'));
152 2
		$this->logDebug(sprintf('Last checked image id for user %s is %d', $userId, $lastChecked));
153 2
		yield;
154
155
		// Now filter by those above last checked and sort remaining images
156 2
		$allImages = array_filter($allImages, function ($i) use($lastChecked) {
157 2
			return $i->id > $lastChecked;
158 2
		});
159 2
		usort($allImages, function ($i1, $i2) {
160 1
			return $i1->id <=> $i2->id;
161 2
		});
162 2
		$this->logDebug(sprintf(
163 2
			'After filtering and sorting, there is %d remaining stale images to check for user %s',
164 2
			count($allImages), $userId));
165 2
		yield;
166
167
		// Now iterate and check remaining images
168 2
		$userFolder = $this->context->rootFolder->getUserFolder($userId);
169 2
		$processed = 0;
170 2
		$imagesRemoved = 0;
171 2
		foreach ($allImages as $image) {
172
			// Delete image doesn't exist anymore in filesystem or it is under .nomedia
173 2
			$mount = $this->getHomeMount($userFolder, $image);
174
175 2
			if ($mount === null) {
176 1
				$this->deleteImage($image, $userId);
177 1
				$imagesRemoved++;
178 1
			} else if ($this->isUnderNoMedia($mount)) {
179 1
				$this->deleteImage($image, $userId);
180 1
				$imagesRemoved++;
181
			}
182
183
			// Remember last processed image
184 2
			$this->config->setUserValue(
185 2
				$userId, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_LAST_CHECKED_KEY, $image->id);
186
187
			// Yield from time to time
188 2
			$processed++;
189 2
			if ($processed % 10 == 0) {
190
				$this->logDebug(sprintf('Processed %d/%d stale images for user %s', $processed, count($allImages), $userId));
191 2
				yield;
192
			}
193
		}
194
195
		// Remove this value when we are done, so next cleanup can start from 0
196 2
		$this->config->deleteUserValue($userId, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_LAST_CHECKED_KEY);
197 2
		return $imagesRemoved;
198
	}
199
200
	/**
201
	 * For a given image, tries to find home mount. Returns null if it is not found (equivalent of image does not exist).
202
	 *
203
	 * @param Folder $userFolder User folder to search in
204
	 * @param Image $image Image to find home mount for
205
	 *
206
	 * @return File|null File if image file node is found, null otherwise.
207
	 */
208 2
	private function getHomeMount(Folder $userFolder, Image $image) {
209 2
		$allMounts = $userFolder->getById($image->file);
210 2
		$homeMounts = array_filter($allMounts, function ($m) {
211 1
			return $m->getStorage()->instanceOfStorage(IHomeStorage::class);
212 2
		});
213
214 2
		if (count($homeMounts) === 0) {
215 1
			return null;
216
		} else {
217 1
			return $homeMounts[0];
218
		}
219
	}
220
221
	/**
222
	 * Checks if this file is located somewhere under .nomedia file and should be therefore ignored.
223
	 * TODO: same method is in Watcher.php, find a place for both methods
224
	 *
225
	 * @param File $file File to search for
226
	 * @return bool True if file is located under .nomedia, false otherwise
227
	 */
228 1
	private function isUnderNoMedia(File $file): bool {
229
		// If we detect .nomedia file anywhere on the path to root folder (id===null), bail out
230 1
		$parentNode = $file->getParent();
231 1
		while (($parentNode instanceof Folder) && ($parentNode->getId() !== null)) {
232 1
			if ($parentNode->nodeExists('.nomedia')) {
233 1
				return true;
234
			}
235 1
			$parentNode = $parentNode->getParent();
236
		}
237
238 1
		return false;
239
	}
240
241 2
	private function deleteImage(Image $image, string $userId) {
242 2
		$this->logInfo(sprintf('Removing stale image %d for user %s', $image->id, $userId));
243
		// note that invalidatePersons depends on existence of faces for a given image,
244
		// and we must invalidate before we delete faces!
245
		// TODO: this is same method as in Watcher, find where to unify them.
246 2
		$this->personMapper->invalidatePersons($image->id);
247 2
		$this->faceMapper->removeFaces($image->id);
248 2
		$this->imageMapper->delete($image);
249 2
	}
250
}
251