1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
/** |
4
|
|
|
* @copyright Copyright (c) 2016, Roeland Jago Douma <[email protected]> |
5
|
|
|
* @copyright Copyright (c) 2017-2021 Matias De lellis <[email protected]> |
6
|
|
|
* |
7
|
|
|
* @author Roeland Jago Douma <[email protected]> |
8
|
|
|
* @author Matias De lellis <[email protected]> |
9
|
|
|
* |
10
|
|
|
* @license GNU AGPL version 3 or any later version |
11
|
|
|
* |
12
|
|
|
* This program is free software: you can redistribute it and/or modify |
13
|
|
|
* it under the terms of the GNU Affero General Public License as |
14
|
|
|
* published by the Free Software Foundation, either version 3 of the |
15
|
|
|
* License, or (at your option) any later version. |
16
|
|
|
* |
17
|
|
|
* This program is distributed in the hope that it will be useful, |
18
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
19
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
20
|
|
|
* GNU Affero General Public License for more details. |
21
|
|
|
* |
22
|
|
|
* You should have received a copy of the GNU Affero General Public License |
23
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
24
|
|
|
* |
25
|
|
|
*/ |
26
|
|
|
|
27
|
|
|
namespace OCA\FaceRecognition\Hooks; |
28
|
|
|
|
29
|
|
|
use OCP\Files\IRootFolder; |
30
|
|
|
use OCP\Files\Folder; |
31
|
|
|
use OCP\Files\Node; |
32
|
|
|
use OCP\ILogger; |
33
|
|
|
use OCP\IUserManager; |
34
|
|
|
|
35
|
|
|
use OCA\FaceRecognition\Service\FileService; |
36
|
|
|
use OCA\FaceRecognition\Service\SettingsService; |
37
|
|
|
|
38
|
|
|
use OCA\FaceRecognition\Db\Face; |
39
|
|
|
use OCA\FaceRecognition\Db\Image; |
40
|
|
|
|
41
|
|
|
use OCA\FaceRecognition\Db\FaceMapper; |
42
|
|
|
use OCA\FaceRecognition\Db\ImageMapper; |
43
|
|
|
use OCA\FaceRecognition\Db\PersonMapper; |
44
|
|
|
|
45
|
|
|
class FileHooks { |
46
|
|
|
|
47
|
|
|
/** @var IRootFolder */ |
48
|
|
|
private $root; |
49
|
|
|
|
50
|
|
|
/** @var ILogger Logger */ |
51
|
|
|
private $logger; |
52
|
|
|
|
53
|
|
|
/** @var IUserManager */ |
54
|
|
|
private $userManager; |
55
|
|
|
|
56
|
|
|
/** @var FaceMapper */ |
57
|
|
|
private $faceMapper; |
58
|
|
|
|
59
|
|
|
/** @var ImageMapper */ |
60
|
|
|
private $imageMapper; |
61
|
|
|
|
62
|
|
|
/** @var PersonMapper */ |
63
|
|
|
private $personMapper; |
64
|
|
|
|
65
|
|
|
/** @var SettingsService */ |
66
|
|
|
private $settingsService; |
67
|
|
|
|
68
|
|
|
/** @var FileService */ |
69
|
|
|
private $fileService; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Watcher constructor. |
73
|
|
|
* |
74
|
|
|
* @param IRootFolder $root |
75
|
|
|
* @param ILogger $logger |
76
|
|
|
* @param IUserManager $userManager |
77
|
|
|
* @param FaceMapper $faceMapper |
78
|
|
|
* @param ImageMapper $imageMapper |
79
|
|
|
* @param PersonMapper $personMapper |
80
|
|
|
* @param SettingsService $settingsService |
81
|
|
|
* @param FileService $fileService |
82
|
|
|
*/ |
83
|
|
|
public function __construct(IRootFolder $root, |
84
|
|
|
ILogger $logger, |
85
|
|
|
IUserManager $userManager, |
86
|
|
|
FaceMapper $faceMapper, |
87
|
|
|
ImageMapper $imageMapper, |
88
|
|
|
PersonMapper $personMapper, |
89
|
|
|
SettingsService $settingsService, |
90
|
|
|
FileService $fileService) |
91
|
|
|
{ |
92
|
|
|
$this->root = $root; |
93
|
|
|
$this->logger = $logger; |
94
|
|
|
$this->userManager = $userManager; |
95
|
|
|
$this->faceMapper = $faceMapper; |
96
|
|
|
$this->imageMapper = $imageMapper; |
97
|
|
|
$this->personMapper = $personMapper; |
98
|
|
|
$this->settingsService = $settingsService; |
99
|
|
|
$this->fileService = $fileService; |
100
|
|
|
} |
101
|
|
|
|
102
|
28 |
|
public function register() { |
103
|
|
|
// Watch on postWrite to handle new and changes files |
104
|
|
|
$this->root->listen('\OC\Files', 'postWrite', function (Node $node) { |
105
|
28 |
|
$this->postWrite($node); |
106
|
28 |
|
}); |
107
|
|
|
|
108
|
|
|
// We want to react on postDelete and not preDelete as in preDelete we don't know if |
109
|
|
|
// file actually got deleted (locked, other errors...) |
110
|
|
|
$this->root->listen('\OC\Files', 'postDelete', function (Node $node) { |
111
|
28 |
|
$this->postDelete($node); |
112
|
28 |
|
}); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* A node has been updated. We just store the file id |
117
|
|
|
* with the current user in the DB |
118
|
|
|
* |
119
|
|
|
* @param Node $node |
120
|
|
|
*/ |
121
|
28 |
|
public function postWrite(Node $node) { |
122
|
28 |
|
if (!$this->fileService->isAllowedNode($node)) { |
123
|
|
|
// Nextcloud sends the Hooks when create thumbnails for example. |
124
|
28 |
|
return; |
125
|
|
|
} |
126
|
|
|
|
127
|
28 |
|
if ($node instanceof Folder) { |
128
|
28 |
|
return; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
$modelId = $this->settingsService->getCurrentFaceModel(); |
132
|
|
|
if ($modelId === SettingsService::FALLBACK_CURRENT_MODEL) { |
133
|
|
|
$this->logger->debug("Skipping inserting file since there are no configured model"); |
|
|
|
|
134
|
|
|
return; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$owner = null; |
138
|
|
|
if ($this->fileService->isUserFile($node)) { |
139
|
|
|
$owner = $node->getOwner()->getUid(); |
140
|
|
|
} else { |
141
|
|
|
if (!\OC::$server->getUserSession()->isLoggedIn()) { |
142
|
|
|
$this->logger->debug('Skipping interting file ' . $node->getName() . ' since we cannot determine the owner.'); |
|
|
|
|
143
|
|
|
return; |
144
|
|
|
} |
145
|
|
|
$owner = \OC::$server->getUserSession()->getUser()->getUID(); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
if (!$this->userManager->userExists($owner)) { |
149
|
|
|
$this->logger->debug( |
|
|
|
|
150
|
|
|
"Skipping inserting file " . $node->getName() . " because it seems that user " . $owner . " doesn't exist"); |
151
|
|
|
return; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
$enabled = $this->settingsService->getUserEnabled($owner); |
155
|
|
|
if (!$enabled) { |
156
|
|
|
$this->logger->debug('The user ' . $owner . ' not have the analysis enabled. Skipping'); |
|
|
|
|
157
|
|
|
return; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
if ($node->getName() === FileService::NOMEDIA_FILE || |
161
|
|
|
$node->getName() === FileService::NOIMAGE_FILE) { |
162
|
|
|
// If user added this file, it means all images in this and all child directories should be removed. |
163
|
|
|
// Instead of doing that here, it's better to just add flag that image removal should be done. |
164
|
|
|
$this->settingsService->setNeedRemoveStaleImages(true, $owner); |
165
|
|
|
return; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
if ($node->getName() === FileService::FACERECOGNITION_SETTINGS_FILE) { |
169
|
|
|
// This file can enable or disable the analysis, so I have to look for new files and forget others. |
170
|
|
|
$this->settingsService->setNeedRemoveStaleImages(true, $owner); |
171
|
|
|
$this->settingsService->setUserFullScanDone(false, $owner); |
172
|
|
|
return; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
if (!$this->settingsService->isAllowedMimetype($node->getMimeType())) { |
176
|
|
|
// The file is not an image or the model does not support it |
177
|
|
|
return; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
if ($this->fileService->isUnderNoDetection($node)) { |
181
|
|
|
$this->logger->debug( |
|
|
|
|
182
|
|
|
"Skipping inserting image " . $node->getName() . " because is inside an folder that contains a .nomedia file"); |
183
|
|
|
return; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
$this->logger->debug("Inserting/updating image " . $node->getName() . " for face recognition"); |
|
|
|
|
187
|
|
|
|
188
|
|
|
$image = new Image(); |
189
|
|
|
$image->setUser($owner); |
190
|
|
|
$image->setFile($node->getId()); |
191
|
|
|
$image->setModel($modelId); |
192
|
|
|
|
193
|
|
|
$imageId = $this->imageMapper->imageExists($image); |
194
|
|
|
if ($imageId === null) { |
195
|
|
|
// todo: can we have larger transaction with bulk insert? |
196
|
|
|
$this->imageMapper->insert($image); |
197
|
|
|
} else { |
198
|
|
|
$this->imageMapper->resetImage($image); |
199
|
|
|
// note that invalidatePersons depends on existence of faces for a given image, |
200
|
|
|
// and we must invalidate before we delete faces! |
201
|
|
|
$this->personMapper->invalidatePersons($imageId); |
202
|
|
|
|
203
|
|
|
// Fetch all faces to be deleted before deleting them, and then delete them |
204
|
|
|
$facesToRemove = $this->faceMapper->findByImage($imageId); |
205
|
|
|
$this->faceMapper->removeFromImage($imageId); |
206
|
|
|
|
207
|
|
|
// If any person is now without faces, remove those (empty) persons |
208
|
|
|
foreach ($facesToRemove as $faceToRemove) { |
209
|
|
|
if ($faceToRemove->getPerson() !== null) { |
210
|
|
|
$this->personMapper->removeIfEmpty($faceToRemove->getPerson()); |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* A node has been deleted. Remove faces with file id |
218
|
|
|
* with the current user in the DB |
219
|
|
|
* |
220
|
|
|
* @param Node $node |
221
|
|
|
*/ |
222
|
28 |
|
public function postDelete(Node $node) { |
223
|
28 |
|
if (!$this->fileService->isAllowedNode($node)) { |
224
|
|
|
// Nextcloud sends the Hooks when create thumbnails for example. |
225
|
28 |
|
return; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
if ($node instanceof Folder) { |
229
|
|
|
return; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
$modelId = $this->settingsService->getCurrentFaceModel(); |
233
|
|
|
if ($modelId === SettingsService::FALLBACK_CURRENT_MODEL) { |
234
|
|
|
$this->logger->debug("Skipping deleting file since there are no configured model"); |
|
|
|
|
235
|
|
|
return; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
$owner = null; |
239
|
|
|
if ($this->fileService->isUserFile($node)) { |
240
|
|
|
$owner = $node->getOwner()->getUid(); |
241
|
|
|
} else { |
242
|
|
|
if (!\OC::$server->getUserSession()->isLoggedIn()) { |
243
|
|
|
$this->logger->debug('Skipping deleting the file ' . $node->getName() . ' since we cannot determine the owner'); |
|
|
|
|
244
|
|
|
return; |
245
|
|
|
} |
246
|
|
|
$owner = \OC::$server->getUserSession()->getUser()->getUID(); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
$enabled = $this->settingsService->getUserEnabled($owner); |
250
|
|
|
if (!$enabled) { |
251
|
|
|
$this->logger->debug('The user ' . $owner . ' not have the analysis enabled. Skipping'); |
|
|
|
|
252
|
|
|
return; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
if ($node->getName() === FileService::NOMEDIA_FILE || |
256
|
|
|
$node->getName() === FileService::NOIMAGE_FILE) { |
257
|
|
|
// If user deleted file named .nomedia, that means all images in this and all child directories should be added. |
258
|
|
|
// But, instead of doing that here, better option seem to be to just reset flag that image scan is not done. |
259
|
|
|
// This will trigger another round of image crawling in AddMissingImagesTask for this user and those images will be added. |
260
|
|
|
$this->settingsService->setUserFullScanDone(false, $owner); |
261
|
|
|
return; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
if ($node->getName() === FileService::FACERECOGNITION_SETTINGS_FILE) { |
265
|
|
|
// This file can enable or disable the analysis, so I have to look for new files and forget others. |
266
|
|
|
$this->settingsService->setNeedRemoveStaleImages(true, $owner); |
267
|
|
|
$this->settingsService->setUserFullScanDone(false, $owner); |
268
|
|
|
return; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
if (!$this->settingsService->isAllowedMimetype($node->getMimeType())) { |
272
|
|
|
// The file is not an image or the model does not support it |
273
|
|
|
return; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
$this->logger->debug("Deleting image " . $node->getName() . " from face recognition"); |
|
|
|
|
277
|
|
|
|
278
|
|
|
$image = new Image(); |
279
|
|
|
$image->setUser($owner); |
280
|
|
|
$image->setFile($node->getId()); |
281
|
|
|
$image->setModel($modelId); |
282
|
|
|
|
283
|
|
|
$imageId = $this->imageMapper->imageExists($image); |
284
|
|
|
if ($imageId !== null) { |
285
|
|
|
// note that invalidatePersons depends on existence of faces for a given image, |
286
|
|
|
// and we must invalidate before we delete faces! |
287
|
|
|
$this->personMapper->invalidatePersons($imageId); |
288
|
|
|
|
289
|
|
|
// Fetch all faces to be deleted before deleting them, and then delete them |
290
|
|
|
$facesToRemove = $this->faceMapper->findByImage($imageId); |
291
|
|
|
$this->faceMapper->removeFromImage($imageId); |
292
|
|
|
|
293
|
|
|
$image->setId($imageId); |
294
|
|
|
$this->imageMapper->delete($image); |
295
|
|
|
|
296
|
|
|
// If any person is now without faces, remove those (empty) persons |
297
|
|
|
foreach ($facesToRemove as $faceToRemove) { |
298
|
|
|
if ($faceToRemove->getPerson() !== null) { |
299
|
|
|
$this->personMapper->removeIfEmpty($faceToRemove->getPerson()); |
300
|
|
|
} |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
} |
305
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.