Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Name | Description
-----|------------
Processor cores | Defines the number of processor cores to use for OCR processing. When the input is a PDF file, this corresponds to the [`ocrmypdf` CPU limit](https://ocrmypdf.readthedocs.io/en/latest/pdfsecurity.html?highlight=%22-j%22#limiting-cpu-usage). This setting can be especially useful if you have a small backend system which has only limited power.
Request timeout | Defines a global timeout (in seconds) when using the `workflow_ocr_backend` app. Use this to limit how long the app will wait for OCR responses. Default value is 60 seconds. |
OCR user | Defines a dedicated user account which is used to run the OCR processing. If this setting is left empty (default), the app impersonates the owner of the processed file, which means that the resulting file version and the file activity are attributed to that user. By configuring a dedicated user you can make automated OCR operations distinguishable from manual user edits in the audit trail. Please note that the configured user needs write access to all files which should be OCR processed (for example via group shares) - otherwise the processing will fail. If the configured user does not exist, the app logs a warning and falls back to the file owner. Notifications are always sent to the owner of the processed file. |

### Testing your configuration

Expand Down
2 changes: 2 additions & 0 deletions lib/Controller/GlobalSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public function setGlobalSettings(array $globalSettings) : JSONResponse {
$globalSettingsObject = new GlobalSettings();
$globalSettingsObject->processorCount = isset($globalSettings['processorCount']) ? (int)$globalSettings['processorCount'] : null;
$globalSettingsObject->timeout = isset($globalSettings['timeout']) ? (int)$globalSettings['timeout'] : null;
$processingUserId = isset($globalSettings['processingUserId']) ? trim((string)$globalSettings['processingUserId']) : '';
$globalSettingsObject->processingUserId = $processingUserId !== '' ? $processingUserId : null;

$this->globalSettingsService->setGlobalSettings($globalSettingsObject);
return $this->globalSettingsService->getGlobalSettings();
Expand Down
6 changes: 6 additions & 0 deletions lib/Model/GlobalSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@
class GlobalSettings {
public ?int $processorCount = null;
public ?int $timeout = null;
/**
* Optional uid of a dedicated user which is used to run the OCR
* processing instead of impersonating the file owner. If this is
* null, the file owner is used (default behaviour).
*/
public ?string $processingUserId = null;
}
5 changes: 4 additions & 1 deletion lib/Service/GlobalSettingsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ public function getGlobalSettings() : GlobalSettings {
foreach ($this->getProperties($settings) as $prop) {
$key = $prop->getName();
$type = $prop->getType();
$isNullable = $type !== null && $type->allowsNull();
if ($type instanceof \ReflectionNamedType && $type->getName() === 'int') {
$intValue = $this->config->getValueInt(Application::APP_NAME, $key, 0);
// 0 is used as the "not configured" sentinel: none of the integer settings
// have a meaningful zero value, so 0 → null (not set).
$settings->$key = $intValue > 0 ? $intValue : null;
} else {
$settings->$key = $this->config->getValueString(Application::APP_NAME, $key);
$stringValue = $this->config->getValueString(Application::APP_NAME, $key);
// The empty string is used as the "not configured" sentinel for string settings.
$settings->$key = $stringValue === '' && $isNullable ? null : $stringValue;
}
}

Expand Down
50 changes: 42 additions & 8 deletions lib/Service/OcrService.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use OCA\Files_Versions\Versions\IVersionManager;
use OCA\WorkflowOcr\Exception\OcrNotPossibleException;
use OCA\WorkflowOcr\Helper\IProcessingFileAccessor;
use OCA\WorkflowOcr\Model\GlobalSettings;
use OCA\WorkflowOcr\Model\WorkflowSettings;
use OCA\WorkflowOcr\OcrProcessors\IOcrProcessorFactory;
use OCA\WorkflowOcr\OcrProcessors\OcrProcessorResult;
Expand Down Expand Up @@ -136,7 +137,10 @@ public function runOcrProcessWithJobArgument($argument): void {
/** @inheritdoc */
public function runOcrProcess(int $fileId, string $uid, WorkflowSettings $settings) : void { // TODO :: make private
try {
$this->initUserEnvironment($uid);
$globalSettings = $this->globalSettingsService->getGlobalSettings();
$processingUid = $this->determineProcessingUid($uid, $globalSettings);

$this->initUserEnvironment($processingUid);

$file = $this->getNode($fileId);

Expand All @@ -155,7 +159,6 @@ public function runOcrProcess(int $fileId, string $uid, WorkflowSettings $settin
}

$ocrProcessor = $this->ocrProcessorFactory->create($file->getMimeType());
$globalSettings = $this->globalSettingsService->getGlobalSettings();

$result = $ocrProcessor->ocrFile($file, $settings, $globalSettings);

Expand Down Expand Up @@ -197,7 +200,7 @@ public function runOcrProcess(int $fileId, string $uid, WorkflowSettings $settin
return;
}

$this->doPostProcessing($file, $uid, $settings, $result, $fileMtime);
$this->doPostProcessing($file, $processingUid, $uid, $settings, $result, $fileMtime);
} finally {
$this->shutdownUserEnvironment();
}
Expand Down Expand Up @@ -228,7 +231,33 @@ private function parseArguments($argument) : array {
}

/**
* * @param string $uid The owners userId of the file to be processed
* Determines which user the OCR process should be run with. By default this is
* the owner of the file to be processed. If an admin configured a dedicated
* processing user globally, this user is used instead (#385).
*
* @param string $uid The owners userId of the file to be processed
* @param GlobalSettings $globalSettings The globally configured settings
* @return string The userId to run the OCR process with
*/
private function determineProcessingUid(string $uid, GlobalSettings $globalSettings) : string {
$configuredUid = trim($globalSettings->processingUserId ?? '');

if ($configuredUid === '' || $configuredUid === $uid) {
return $uid;
}

if (!$this->userManager->userExists($configuredUid)) {
$this->logger->warning('Configured OCR processing user \'{configuredUid}\' does not exist. Falling back to file owner \'{uid}\'.', ['configuredUid' => $configuredUid, 'uid' => $uid]);
return $uid;
}

$this->logger->debug('Running OCR process as configured processing user {configuredUid} instead of file owner {uid}', ['configuredUid' => $configuredUid, 'uid' => $uid]);

return $configuredUid;
}

/**
* * @param string $uid The userId to run the OCR process with
*/
private function initUserEnvironment(string $uid) : void {
/** @var IUser */
Expand Down Expand Up @@ -315,7 +344,7 @@ private function createNewFileVersion(string $filePath, string $ocrContent, ?int

/**
* @param File $file The file to set the label for
* @param string $uid The userId of the file owner
* @param string $uid The userId of the user running the OCR process
* @param string $label The label to set
*/
private function setFileVersionsLabel(File $file, string $uid, string $label): void {
Expand Down Expand Up @@ -352,7 +381,12 @@ private function setFileVersionsLabel(File $file, string $uid, string $label): v
}
}

private function doPostProcessing(Node $file, string $uid, WorkflowSettings $settings, OcrProcessorResult $result, ?int $fileMtime = null): void {
/**
* @param Node $file The OCR processed file
* @param string $processingUid The userId the OCR process is running with
* @param string $ownerUid The userId of the file owner (receiver of notifications)
*/
private function doPostProcessing(Node $file, string $processingUid, string $ownerUid, WorkflowSettings $settings, OcrProcessorResult $result, ?int $fileMtime = null): void {
$this->processTagsAfterSuccessfulOcr($file, $settings);

$fileId = $file->getId();
Expand All @@ -363,7 +397,7 @@ private function doPostProcessing(Node $file, string $uid, WorkflowSettings $set
if ($result->getRecognizedText() !== '') {
if ($settings->getKeepOriginalFileVersion() && $file->isUpdateable()) {
// Add label to original file to prevent its expiry
$this->setFileVersionsLabel($file, $uid, self::FILE_VERSION_LABEL_VALUE);
$this->setFileVersionsLabel($file, $processingUid, self::FILE_VERSION_LABEL_VALUE);
}

$newFilePath = $this->determineNewFilePath($file, $originalFileExtension);
Expand All @@ -378,7 +412,7 @@ private function doPostProcessing(Node $file, string $uid, WorkflowSettings $set
$this->eventService->textRecognized($result, $file);

if ($settings->getSendSuccessNotification()) {
$this->notificationService->createSuccessNotification($uid, $fileId);
$this->notificationService->createSuccessNotification($ownerUid, $fileId);
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/components/GlobalSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@
@input="settings.timeout = Number($event.target.value) || null; save()">
</div>
</div>
<div class="div-table-row">
<div class="div-table-col div-table-col-left">
<span class="leftcol">{{ translate('OCR user:') }}</span>
<br>
<em>{{ translate('User account which is used to run the OCR processing. If not set, the owner of the processed file is used.') }}</em>
<br>
<em><strong>{{ translate('Note:') }}</strong> {{ translate('This user needs write access to all files which should be OCR processed.') }}</em>
</div>
<div class="div-table-col">
<input
:value="settings.processingUserId"
name="processingUserId"
type="text"
@change="settings.processingUserId = $event.target.value.trim() || null; save()">
</div>
</div>
</NcSettingsSection>
</div>
</template>
Expand Down
46 changes: 46 additions & 0 deletions src/test/components/GlobalSettings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,52 @@ describe('Interaction tests', () => {
}))
})

test('Should update settings when processingUserId is changed', async () => {
const initialMockSettings = { processingUserId: null }
getGlobalSettings.mockResolvedValueOnce(initialMockSettings)

const afterSaveMockSettings = { processingUserId: 'ocruser' }
setGlobalSettings.mockResolvedValueOnce(afterSaveMockSettings)

const wrapper = mount(GlobalSettings, mountOptions)
await new Promise(process.nextTick)

const processingUserId = wrapper.find('input[name="processingUserId"]')
expect(processingUserId.element.value).toBe('')

processingUserId.element.value = ' ocruser '
await processingUserId.trigger('change')

expect(wrapper.vm.settings.processingUserId).toBe('ocruser')
expect(setGlobalSettings).toHaveBeenCalledTimes(1)
expect(setGlobalSettings).toHaveBeenCalledWith(expect.objectContaining({
processingUserId: 'ocruser',
}))
})

test('Should convert empty processingUserId to null', async () => {
const initialMockSettings = { processingUserId: 'ocruser' }
getGlobalSettings.mockResolvedValueOnce(initialMockSettings)

const afterSaveMockSettings = { processingUserId: null }
setGlobalSettings.mockResolvedValueOnce(afterSaveMockSettings)

const wrapper = mount(GlobalSettings, mountOptions)
await new Promise(process.nextTick)

const processingUserId = wrapper.find('input[name="processingUserId"]')
expect(processingUserId.element.value).toBe('ocruser')

processingUserId.element.value = ' '
await processingUserId.trigger('change')

expect(wrapper.vm.settings.processingUserId).toBeNull()
expect(setGlobalSettings).toHaveBeenCalledTimes(1)
expect(setGlobalSettings).toHaveBeenCalledWith(expect.objectContaining({
processingUserId: null,
}))
})

test('Should show error when save fails', async () => {
const initialMockSettings = { processorCount: 2 }
getGlobalSettings.mockResolvedValueOnce(initialMockSettings)
Expand Down
44 changes: 43 additions & 1 deletion tests/Unit/Controller/GlobalSettingsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use OCA\WorkflowOcr\Service\IGlobalSettingsService;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

Expand Down Expand Up @@ -79,12 +80,53 @@ public function testSetSettingsCallsService() {
$settings = [
'processorCount' => 42,
'timeout' => 120,
'processingUserId' => 'ocruser',
];

$this->globalSettingsService->expects($this->once())
->method('setGlobalSettings')
->with($this->callback(function (GlobalSettings $settings) {
return $settings->processorCount === 42 && $settings->timeout === 120;
return $settings->processorCount === 42
&& $settings->timeout === 120
&& $settings->processingUserId === 'ocruser';
}));

$this->controller->setGlobalSettings($settings);
}

#[DataProvider('dataProvider_EmptyProcessingUserIds')]
public function testSetSettingsMapsEmptyProcessingUserIdToNull($processingUserId) {
$settings = [
'processorCount' => 42,
'processingUserId' => $processingUserId,
];

$this->globalSettingsService->expects($this->once())
->method('setGlobalSettings')
->with($this->callback(function (GlobalSettings $settings) {
return $settings->processingUserId === null;
}));

$this->controller->setGlobalSettings($settings);
}

public static function dataProvider_EmptyProcessingUserIds() {
return [
[null],
[''],
[' '],
];
}

public function testSetSettingsTrimsProcessingUserId() {
$settings = [
'processingUserId' => ' ocruser ',
];

$this->globalSettingsService->expects($this->once())
->method('setGlobalSettings')
->with($this->callback(function (GlobalSettings $settings) {
return $settings->processingUserId === 'ocruser';
}));

$this->controller->setGlobalSettings($settings);
Expand Down
36 changes: 33 additions & 3 deletions tests/Unit/Service/GlobalSettingsServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,36 @@ public function testGetSettings_ReturnsCorrectSettings() {
[Application::APP_NAME, 'processorCount', 0, 2],
[Application::APP_NAME, 'timeout', 0, 30],
]);
$this->config->expects($this->any())
->method('getValueString')
->willReturn('ocruser');

$settings = $this->globalSettingsService->getGlobalSettings();

$this->assertInstanceOf(GlobalSettings::class, $settings);
$this->assertEquals(2, $settings->processorCount);
$this->assertEquals(30, $settings->timeout);
$this->assertEquals('ocruser', $settings->processingUserId);
}

public function testGetSettings_ReturnsNullForUnsetValues() {
// getValueInt returns 0 for keys that have never been stored.
// 0 is not a valid value for processorCount or timeout, so it is
// treated as "not configured" and mapped to null.
// treated as "not configured" and mapped to null. The same applies
// to the empty string for string based settings.
$this->config->expects($this->any())
->method('getValueInt')
->willReturn(0);
$this->config->expects($this->any())
->method('getValueString')
->willReturn('');

$settings = $this->globalSettingsService->getGlobalSettings();

$this->assertInstanceOf(GlobalSettings::class, $settings);
$this->assertNull($settings->processorCount);
$this->assertNull($settings->timeout);
$this->assertNull($settings->processingUserId);
}

public function testSetSettings_CallsConfigSetValueInt() {
Expand All @@ -96,20 +105,41 @@ function (string $appName, string $key, int $value) use ($settings) {
$this->globalSettingsService->setGlobalSettings($settings);
}

public function testSetSettings_CallsConfigSetValueString() {
$settings = new GlobalSettings();
$settings->processingUserId = 'ocruser';

$this->config->expects($this->once())
->method('setValueString')
->willReturnCallback(
function (string $appName, string $key, string $value) {
$this->assertEquals(Application::APP_NAME, $appName);
$this->assertEquals('processingUserId', $key);
$this->assertEquals('ocruser', $value);
return true;
}
);

$this->globalSettingsService->setGlobalSettings($settings);
}

public function testSetSettings_DeletesKeyForNullValues() {
$settings = new GlobalSettings();
$settings->processorCount = null;
$settings->timeout = null;
$settings->processingUserId = null;

$this->config->expects($this->never())
->method('setValueInt');
$this->config->expects($this->never())
->method('setValueString');

$this->config->expects($this->exactly(2))
$this->config->expects($this->exactly(3))
->method('deleteKey')
->willReturnCallback(
function (string $appName, string $key) {
$this->assertEquals(Application::APP_NAME, $appName);
$this->assertContains($key, ['processorCount', 'timeout']);
$this->assertContains($key, ['processorCount', 'timeout', 'processingUserId']);
}
);

Expand Down
Loading
Loading