1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Nextcloud - OCR |
5
|
|
|
* This file is licensed under the Affero General Public License version 3 or |
6
|
|
|
* later. |
7
|
|
|
* See the COPYING file. |
8
|
|
|
* |
9
|
|
|
* @author Janis Koehr <[email protected]> |
10
|
|
|
* @copyright Janis Koehr 2017 |
11
|
|
|
*/ |
12
|
|
|
namespace OCA\Ocr\Service; |
13
|
|
|
|
14
|
|
|
use OCA\Ocr\Db\OcrJobMapper; |
15
|
|
|
use OC\Files\View; |
16
|
|
|
use OCP\ILogger; |
17
|
|
|
use OCP\IL10N; |
18
|
|
|
use OCP\ITempManager; |
19
|
|
|
use OCA\Ocr\Db\OcrJob; |
20
|
|
|
use OCA\Ocr\Constants\OcrConstants; |
21
|
|
|
use OCA\Ocr\Util\PHPUtil; |
22
|
|
|
use Exception; |
23
|
|
|
use OCP\AppFramework\Db\DoesNotExistException; |
24
|
|
|
use OCA\Ocr\Util\FileUtil; |
25
|
|
|
|
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Class JobService |
29
|
|
|
* |
30
|
|
|
* @package OCA\Ocr\Service |
31
|
|
|
*/ |
32
|
|
|
class JobService { |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* |
36
|
|
|
* @var ILogger |
37
|
|
|
*/ |
38
|
|
|
private $logger; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* |
42
|
|
|
* @var RedisService |
43
|
|
|
*/ |
44
|
|
|
private $redisService; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* |
48
|
|
|
* @var OcrJobMapper |
49
|
|
|
*/ |
50
|
|
|
private $jobMapper; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* |
54
|
|
|
* @var View |
55
|
|
|
*/ |
56
|
|
|
private $view; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* |
60
|
|
|
* @var String |
61
|
|
|
*/ |
62
|
|
|
private $userId; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* |
66
|
|
|
* @var IL10N |
67
|
|
|
*/ |
68
|
|
|
private $l10n; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* |
72
|
|
|
* @var FileService |
73
|
|
|
*/ |
74
|
|
|
private $fileService; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* |
78
|
|
|
* @var ITempManager |
79
|
|
|
*/ |
80
|
|
|
private $tempM; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* |
84
|
|
|
* @var AppConfigService |
85
|
|
|
*/ |
86
|
|
|
private $appConfigService; |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* |
90
|
|
|
* @var PHPUtil |
91
|
|
|
*/ |
92
|
|
|
private $phpUtil; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* |
96
|
|
|
* @var FileUtil |
97
|
|
|
*/ |
98
|
|
|
private $fileUtil; |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* JobService constructor. |
102
|
|
|
* |
103
|
|
|
* @param IL10N $l10n |
104
|
|
|
* @param ILogger $logger |
105
|
|
|
* @param string $userId |
106
|
|
|
* @param View $view |
107
|
|
|
* @param RedisService $queueService |
108
|
|
|
* @param OcrJobMapper $mapper |
109
|
|
|
* @param FileService $fileService |
110
|
|
|
* @param AppConfigService $appConfigService |
111
|
|
|
* @param PHPUtil $phpUtil |
112
|
|
|
* @param FileUtil $fileUtil |
113
|
|
|
*/ |
114
|
22 |
|
public function __construct(IL10N $l10n, ILogger $logger, $userId, View $view, ITempManager $tempManager, |
115
|
|
|
RedisService $queueService, OcrJobMapper $mapper, FileService $fileService, |
116
|
|
|
AppConfigService $appConfigService, PHPUtil $phpUtil, FileUtil $fileUtil) { |
117
|
22 |
|
$this->logger = $logger; |
118
|
22 |
|
$this->redisService = $queueService; |
119
|
22 |
|
$this->jobMapper = $mapper; |
120
|
22 |
|
$this->view = $view; |
121
|
22 |
|
$this->userId = $userId; |
122
|
22 |
|
$this->l10n = $l10n; |
123
|
22 |
|
$this->fileService = $fileService; |
124
|
22 |
|
$this->tempM = $tempManager; |
125
|
22 |
|
$this->appConfigService = $appConfigService; |
126
|
22 |
|
$this->phpUtil = $phpUtil; |
127
|
22 |
|
$this->fileUtil = $fileUtil; |
128
|
22 |
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Processes and prepares the files for OCR. |
132
|
|
|
* Sends the stuff to the client in order to OCR async. |
133
|
|
|
* |
134
|
|
|
* @param string[] $languages |
135
|
|
|
* @param array $files |
136
|
|
|
* @param boolean $replace |
137
|
|
|
* @return string |
138
|
9 |
|
*/ |
139
|
|
|
public function process($languages, $files, $replace) { |
140
|
9 |
|
try { |
141
|
|
|
$this->logger->debug('Will now process files: {files} with languages: {languages} and replace: {replace}', |
142
|
9 |
|
[ |
143
|
9 |
|
'files' => json_encode($files), |
144
|
|
|
'languages' => json_encode($languages), |
145
|
|
|
'replace' => json_encode($replace) |
146
|
9 |
|
]); |
147
|
9 |
|
// Check if files and language not empty |
148
|
|
|
$noLang = $this->noLanguage($languages); |
149
|
6 |
|
if (!empty($files) && ($this->checkForAcceptedLanguages($languages) || $noLang) && is_bool($replace)) { |
150
|
5 |
|
// language part: |
151
|
|
|
if ($noLang) { |
152
|
|
|
$languages = []; |
153
|
6 |
|
} |
154
|
6 |
|
// file part: |
155
|
|
|
$fileInfo = $this->fileService->buildFileInfo($files); |
156
|
6 |
|
foreach ($fileInfo as $fInfo) { |
157
|
6 |
|
// Check Shared |
158
|
6 |
|
$shared = $this->fileService->checkSharedWithInitiator($fInfo); |
159
|
|
|
if($shared && $replace) { |
160
|
6 |
|
throw new NotFoundException($this->l10n->t('Cannot replace shared files.')); |
161
|
|
|
} |
162
|
6 |
|
$target = $this->fileService->buildTarget($fInfo, $shared, $replace); |
163
|
|
|
$source = $this->fileService->buildSource($fInfo, $shared); |
164
|
2 |
|
// set the running type |
165
|
2 |
|
$fType = $this->fileService->getCorrectType($fInfo); |
166
|
|
|
// create a temp file for ocr processing purposes |
167
|
|
|
$tempFile = $this->getTempFile($fType); |
168
|
2 |
|
// Create job object |
169
|
|
|
$job = new OcrJob(OcrConstants::STATUS_PENDING, $source, $target, $tempFile, $fType, $this->userId, |
170
|
2 |
|
false, $fInfo->getName(), null, $replace); |
171
|
|
|
// Init client and send task / job |
172
|
3 |
|
// Feed the worker |
173
|
|
|
$this->redisService->sendJob($job, $languages); |
174
|
7 |
|
} |
175
|
7 |
|
return 'PROCESSING'; |
176
|
|
|
} else { |
177
|
|
|
throw new NotFoundException($this->l10n->t('Empty parameters passed.')); |
178
|
|
|
} |
179
|
|
|
} catch (Exception $e) { |
180
|
|
|
$this->handleException($e); |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Delete an ocr job for a given id and userId. |
186
|
|
|
* |
187
|
3 |
|
* @param |
188
|
|
|
* $jobId |
189
|
3 |
|
* @param string $userId |
190
|
2 |
|
* @return OcrJob |
191
|
1 |
|
*/ |
192
|
|
|
public function deleteJob($jobId, $userId) { |
193
|
1 |
|
try { |
194
|
|
|
$job = $this->jobMapper->find($jobId); |
195
|
1 |
|
if ($job->getUserId() !== $userId) { |
196
|
1 |
|
throw new NotFoundException($this->l10n->t('Cannot delete because of wrong owner.')); |
197
|
1 |
|
} else { |
198
|
1 |
|
$job = $this->jobMapper->delete($job); |
199
|
1 |
|
} |
200
|
1 |
|
$job->setTarget(null); |
201
|
1 |
|
$job->setSource(null); |
202
|
1 |
|
$job->setStatus(null); |
203
|
1 |
|
$job->setTempFile(null); |
204
|
2 |
|
$job->setType(null); |
205
|
2 |
|
$job->setUserId(null); |
206
|
1 |
|
$job->setErrorDisplayed(null); |
207
|
1 |
|
$job->setErrorLog(null); |
208
|
|
|
$job->setReplace(null); |
209
|
1 |
|
return $job; |
210
|
|
|
} catch (Exception $e) { |
211
|
|
|
if ($e instanceof DoesNotExistException) { |
|
|
|
|
212
|
|
|
$ex = new NotFoundException($this->l10n->t('Cannot delete because of wrong ID.')); |
213
|
|
|
$this->handleException($ex); |
214
|
|
|
} else { |
215
|
|
|
$this->handleException($e); |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
1 |
|
/** |
221
|
|
|
* Gets all job objects for a specific user. |
222
|
1 |
|
* |
223
|
1 |
|
* @param string $userId |
224
|
1 |
|
* @return OcrJob[] |
225
|
1 |
|
*/ |
226
|
1 |
|
public function getAllJobsForUser($userId) { |
227
|
1 |
|
try { |
228
|
1 |
|
$jobs = $this->jobMapper->findAll($userId); |
229
|
1 |
|
$jobsNew = array(); |
230
|
1 |
|
foreach ($jobs as $job) { |
231
|
1 |
|
$job->setTarget(null); |
232
|
|
|
$job->setSource(null); |
233
|
1 |
|
$job->setTempFile(null); |
234
|
|
|
$job->setType(null); |
235
|
|
|
$job->setUserId(null); |
236
|
|
|
$job->setErrorDisplayed(null); |
237
|
|
|
array_push($jobsNew, $job); |
238
|
|
|
} |
239
|
|
|
return $jobsNew; |
240
|
|
|
} catch (Exception $e) { |
241
|
|
|
$this->handleException($e); |
242
|
|
|
} |
243
|
|
|
} |
244
|
2 |
|
|
245
|
|
|
/** |
246
|
2 |
|
* The function checks if there are finished jobs to process finally. |
247
|
2 |
|
* |
248
|
1 |
|
* @throws NotFoundException |
249
|
1 |
|
*/ |
250
|
|
|
public function checkForFinishedJobs() { |
251
|
1 |
|
try { |
252
|
|
|
$finishedJobs = $this->redisService->readingFinishedJobs(); |
253
|
1 |
|
foreach ($finishedJobs as $finishedJob) { |
254
|
|
|
$fJob = $this->transformJob($finishedJob); |
255
|
1 |
|
$this->logger->debug('The following job finished: {job}', |
256
|
1 |
|
[ |
257
|
|
|
'job' => $fJob |
258
|
1 |
|
]); |
259
|
|
|
$this->jobFinished($fJob->id, $fJob->error, $fJob->log); |
260
|
|
|
} |
261
|
|
|
} catch (Exception $e) { |
262
|
|
|
throw new NotFoundException($this->l10n->t('Reading the finished jobs from Redis went wrong.')); |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
|
266
|
2 |
|
/** |
267
|
|
|
* Finishes all Processed files by copying them to the right path and deleteing the temp files. |
268
|
2 |
|
* Returns the number of processed files. |
269
|
2 |
|
* |
270
|
2 |
|
* @return array |
271
|
2 |
|
*/ |
272
|
|
|
public function handleProcessed() { |
273
|
1 |
|
try { |
274
|
1 |
|
$this->logger->debug('Check if files were processed by ocr and if so, put them to the right dirs.'); |
275
|
1 |
|
$processed = $this->jobMapper->findAllProcessed($this->userId); |
276
|
1 |
|
foreach ($processed as $job) { |
277
|
|
|
if ($this->fileUtil->fileExists($job->getTempFile())) { |
278
|
1 |
|
// Save the tmp file with newname |
279
|
1 |
|
$this->pullResult($job); |
280
|
1 |
|
$this->jobMapper->delete($job); |
281
|
2 |
|
$this->fileUtil->execRemove($job->getTempFile()); |
282
|
|
|
} else { |
283
|
|
|
$job->setStatus(OcrConstants::STATUS_FAILED); |
284
|
1 |
|
$job->setErrorLog('Temp file does not exist.'); |
285
|
1 |
|
$this->jobMapper->update($job); |
286
|
1 |
|
throw new NotFoundException($this->l10n->t('Temp file does not exist.')); |
287
|
|
|
} |
288
|
|
|
} |
289
|
|
|
return $processed; |
290
|
|
|
} catch (Exception $e) { |
291
|
|
|
$this->handleException($e); |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
|
295
|
1 |
|
/** |
296
|
|
|
* Handles all failed orders of ocr processing queue and returns the job objects. |
297
|
1 |
|
* |
298
|
1 |
|
* @return array |
299
|
|
|
*/ |
300
|
1 |
|
public function handleFailed() { |
301
|
|
|
try { |
302
|
1 |
|
$failed = $this->jobMapper->findAllFailed($this->userId); |
303
|
1 |
|
foreach ($failed as $job) { |
304
|
|
|
// clean the tempfile |
305
|
1 |
|
$this->fileUtil->execRemove($job->getTempFile()); |
306
|
|
|
// set error displayed |
307
|
1 |
|
$job->setErrorDisplayed(true); |
308
|
|
|
$this->jobMapper->update($job); |
309
|
1 |
|
} |
310
|
|
|
$this->logger->debug('Following jobs failed: {failed}', |
311
|
|
|
[ |
312
|
|
|
'failed' => json_encode($failed) |
313
|
|
|
]); |
314
|
|
|
return $failed; |
315
|
|
|
} catch (Exception $e) { |
316
|
|
|
$this->handleException($e); |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Gets the OCR result and puts it to Nextcloud. |
322
|
|
|
* |
323
|
1 |
|
* @param OcrJob $job |
324
|
|
|
*/ |
325
|
1 |
|
private function pullResult($job) { |
326
|
1 |
|
if ($job->getReplace()) { |
327
|
1 |
|
$this->view->unlink(str_replace($this->userId . '/files', '', $job->getSource())); |
328
|
1 |
|
} |
329
|
|
|
$result = $this->view->file_put_contents($job->getTarget(), $this->fileUtil->getFileContents($job->getTempFile())); |
330
|
1 |
|
if (!$result) { |
331
|
1 |
|
throw new NotFoundException($this->l10n->t('OCR could not put processed file to the right target folder. If you selected the replace option, you can restore the file by using the trash bin.')); |
332
|
1 |
|
} |
333
|
1 |
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* The function the worker will call in order to set the jobs status. |
337
|
|
|
* The worker should call it automatically after each processing step. |
338
|
1 |
|
* |
339
|
|
|
* @param integer $jobId |
340
|
|
|
* @param boolean $error |
341
|
|
|
* @param string $log |
342
|
|
|
*/ |
343
|
|
|
private function jobFinished($jobId, $error, $log) { |
344
|
|
|
try { |
345
|
|
|
$job = $this->jobMapper->find($jobId); |
346
|
|
|
if (!$error) { |
347
|
6 |
|
$job->setStatus(OcrConstants::STATUS_PROCESSED); |
348
|
6 |
|
$this->jobMapper->update($job); |
349
|
5 |
|
} else { |
350
|
5 |
|
$job->setStatus(OcrConstants::STATUS_FAILED); |
351
|
4 |
|
$job->setErrorLog($log); |
352
|
4 |
|
$this->jobMapper->update($job); |
353
|
3 |
|
$this->logger->error($log); |
354
|
2 |
|
} |
355
|
|
|
} catch (Exception $e) { |
356
|
3 |
|
$this->handleException($e); |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Gives a temp file name back depending on the type of the OCR. |
362
|
|
|
* Later in the worker this file is used as an output. |
363
|
|
|
* |
364
|
|
|
* @param integer $type |
365
|
|
|
* @return string |
366
|
|
|
*/ |
367
|
1 |
|
private function getTempFile($type) { |
368
|
1 |
|
if ($type === OcrConstants::TESSERACT) { |
369
|
1 |
|
$fileName = $this->phpUtil->tempnamWrapper($this->tempM->getTempBaseDir(), OcrConstants::TEMPFILE_PREFIX); |
370
|
1 |
|
$this->phpUtil->unlinkWrapper($fileName); |
371
|
|
|
$fileNameWithPostfix = $fileName . '.txt'; |
372
|
|
|
$this->phpUtil->touchWrapper($fileNameWithPostfix); |
373
|
|
|
$this->phpUtil->chmodWrapper($fileNameWithPostfix, 0600); |
374
|
|
|
return $fileNameWithPostfix; |
375
|
|
|
} else { |
376
|
|
|
return $this->phpUtil->tempnamWrapper($this->tempM->getTempBaseDir(), OcrConstants::TEMPFILE_PREFIX); |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* Takes care of transforming an incoming finished job into a php readable object. |
382
|
|
|
* |
383
|
8 |
|
* @param string $job |
384
|
8 |
|
* @throws NotFoundException |
385
|
8 |
|
* @return mixed |
386
|
1 |
|
*/ |
387
|
|
|
private function transformJob($job) { |
388
|
7 |
|
$decoded = json_decode($job); |
389
|
|
|
if ($decoded !== null && isset($decoded->id)) { |
390
|
|
|
return $decoded; |
391
|
|
|
} else { |
392
|
|
|
$this->logger->debug('The finished job retrieved by Redis was corrupt.'); |
393
|
|
|
throw new NotFoundException('The finished job retrieved by Redis was corrupt.'); |
394
|
|
|
} |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
9 |
|
* Checks if the given languages are supported or not. |
399
|
9 |
|
* |
400
|
6 |
|
* @param string[] $languages |
401
|
|
|
* @return boolean |
402
|
3 |
|
*/ |
403
|
|
|
private function checkForAcceptedLanguages($languages) { |
404
|
|
|
$installedLanguages = explode(';', $this->appConfigService->getAppValue('languages')); |
405
|
|
|
if (count(array_diff($languages, $installedLanguages)) === 0) { |
406
|
|
|
return true; |
407
|
|
|
} else { |
408
|
|
|
return false; |
409
|
|
|
} |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
10 |
|
* Checks if the process should be initiated without any language specified. |
414
|
10 |
|
* |
415
|
|
|
* @param string[] $languages |
416
|
|
|
* @return boolean |
417
|
10 |
|
*/ |
418
|
10 |
|
private function noLanguage($languages) { |
419
|
|
|
if (in_array('any', $languages)) { |
420
|
|
|
return true; |
421
|
|
|
} else { |
422
|
|
|
return false; |
423
|
|
|
} |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** |
427
|
|
|
* Handle the possible thrown Exceptions from all methods of this class. |
428
|
|
|
* |
429
|
|
|
* @param Exception $e |
430
|
|
|
* @throws Exception |
431
|
|
|
* @throws NotFoundException |
432
|
|
|
*/ |
433
|
|
|
private function handleException($e) { |
434
|
|
|
$this->logger->logException($e, |
435
|
|
|
[ |
436
|
|
|
'message' => 'Exception during job service function processing' |
437
|
|
|
]); |
438
|
|
|
throw $e; |
439
|
|
|
} |
440
|
|
|
} |
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.