Completed
Push — master ( f54c15...91bfdb )
by Matias
13s queued 11s
created

StaleImagesRemovalTask::deleteImage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 4
c 1
b 1
f 0
nc 1
nop 2
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 1
rs 10
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 3
	public function __construct(ImageMapper     $imageMapper,
73
	                            FaceMapper      $faceMapper,
74
	                            PersonMapper    $personMapper,
75
	                            FileService     $fileService,
76
	                            SettingsService $settingsService)
77
	{
78 3
		parent::__construct();
79
80 3
		$this->imageMapper     = $imageMapper;
81 3
		$this->faceMapper      = $faceMapper;
82 3
		$this->personMapper    = $personMapper;
83 3
		$this->fileService     = $fileService;
84 3
		$this->settingsService = $settingsService;
85 3
	}
86
87
	/**
88
	 * @inheritdoc
89
	 */
90 2
	public function description() {
91 2
		return "Crawl for stale images (either missing in filesystem or under .nomedia) and remove them from DB";
92
	}
93
94
	/**
95
	 * @inheritdoc
96
	 */
97 3
	public function execute(FaceRecognitionContext $context) {
98 3
		$this->setContext($context);
99
100
		// Check if we are called for one user only, or for all user in instance.
101 3
		$staleRemovedImages = 0;
102 3
		$eligable_users = array();
103 3
		if (is_null($this->context->user)) {
104
			$this->context->userManager->callForSeenUsers(function (IUser $user) use (&$eligable_users) {
105 3
				$eligable_users[] = $user->getUID();
106 3
			});
107
		} else {
108
			$eligable_users[] = $this->context->user->getUID();
109
		}
110
111 3
		foreach($eligable_users as $user) {
112 3
			if (!$this->settingsService->getNeedRemoveStaleImages($user)) {
113
				// Completely skip this task for this user, seems that we already did full scan for him
114 3
				$this->logDebug(sprintf('Skipping stale images removal for user %s as there is no need for it', $user));
115 3
				continue;
116
			}
117
118
			// Since method below can take long time, it is generator itself
119 2
			$generator = $this->staleImagesRemovalForUser($user, $this->settingsService->getCurrentFaceModel());
120 2
			foreach ($generator as $_) {
121 2
				yield;
122
			}
123 2
			$staleRemovedImages += $generator->getReturn();
124
125 2
			$this->settingsService->setNeedRemoveStaleImages(false, $user);
126
127 2
			yield;
128
		}
129
130 3
		$this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages'] = $staleRemovedImages;
131 3
		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 2
	private function staleImagesRemovalForUser(string $userId, int $model) {
145
146 2
		$this->fileService->setupFS($userId);
147
148 2
		$this->logDebug(sprintf('Getting all images for user %s', $userId));
149 2
		$allImages = $this->imageMapper->findImages($userId, $model);
150 2
		$this->logDebug(sprintf('Found %d images for user %s', count($allImages), $userId));
151 2
		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 2
		$lastChecked = $this->settingsService->getLastStaleImageChecked($userId);
160 2
		$this->logDebug(sprintf('Last checked image id for user %s is %d', $userId, $lastChecked));
161 2
		yield;
162
163
		// Now filter by those above last checked and sort remaining images
164
		$allImages = array_filter($allImages, function ($i) use($lastChecked) {
165 2
			return $i->id > $lastChecked;
166 2
		});
167
		usort($allImages, function ($i1, $i2) {
168 1
			return $i1->id <=> $i2->id;
169 2
		});
170 2
		$this->logDebug(sprintf(
171 2
			'After filtering and sorting, there is %d remaining stale images to check for user %s',
172 2
			count($allImages), $userId));
173 2
		yield;
174
175
		// Now iterate and check remaining images
176 2
		$processed = 0;
177 2
		$imagesRemoved = 0;
178 2
		foreach ($allImages as $image) {
179 2
			$file = $this->fileService->getFileById($image->getFile(), $userId);
180
181
			// Delete image doesn't exist anymore in filesystem or it is under .nomedia
182 2
			if (($file === null) || (!$this->fileService->isAllowedNode($file)) ||
183 2
			    ($this->fileService->isUnderNoDetection($file))) {
184 2
				$this->deleteImage($image, $userId);
185 2
				$imagesRemoved++;
186
			}
187
188
			// Remember last processed image
189 2
			$this->settingsService->setLastStaleImageChecked($image->id, $userId);
190
191
			// Yield from time to time
192 2
			$processed++;
193 2
			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 2
		$this->settingsService->setLastStaleImageChecked(0, $userId);
201
202 2
		return $imagesRemoved;
203
	}
204
205 2
	private function deleteImage(Image $image, string $userId) {
206 2
		$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 2
		$this->personMapper->invalidatePersons($image->id);
211 2
		$this->faceMapper->removeFromImage($image->id);
212 2
		$this->imageMapper->delete($image);
213 2
	}
214
}
215