Completed
Push — master ( bd4a48...a13ca6 )
by Branko
12s queued 10s
created

StaleImagesRemovalTask::description()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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
				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