Skip to content
Open
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Requirements:
The app does not send any sensitive data to cloud providers or similar services. All processing is done on your Nextcloud machine, using Tensorflow.js running in Node.js.

]]></description>
<version>13.1.0</version>
<version>13.2.0-dev</version>
<licence>agpl</licence>
<author mail="mklehr@gmx.net">Marcel Klehr</author>
<types>
Expand Down
4 changes: 4 additions & 0 deletions lib/Classifiers/Images/ClusteringFaceClassifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ public function classify(array $queueFiles): void {

$classifierProcess = $this->classifyFiles(self::MODEL_NAME, $filteredQueueFiles, $timeout);

// The mount tables are read for every file in this batch, so refresh them once here
// rather than on every lookup.
$this->userMountCache->clear();

/**
* @var list<array> $faces
*/
Expand Down
42 changes: 23 additions & 19 deletions lib/Db/FsActionMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,8 @@ public function findByStorageId(string $className, int $storageId, int $limit =
* @param class-string<FsCreation|FsDeletion|FsMove|FsAccessUpdate> $className
* @return list<FsCreation|FsDeletion|FsMove|FsAccessUpdate>
* @throws \OCP\DB\Exception
* @throws \Exception
*/
public function find(string $className, int $limit = 0): array {
if (!in_array('storage_id', $className::$columns, true)) {
throw new \Exception('entity does not have a storage_id column');
}
$qb = $this->db->getQueryBuilder();
$qb->selectDistinct($className::$columns)
->from($className::$tableName);
Expand Down Expand Up @@ -227,26 +223,34 @@ public function insertDeletion(int $storageId, int $nodeId): FsCreation|FsDeleti

/**
* @param int $nodeId
* @param string $owner
* @param list<string> $addedUsers
* @param list<string> $targetUsers
* @return FsCreation|FsDeletion|FsMove|FsAccessUpdate
* @throws Exception|MultipleObjectsReturnedException
* @return FsMove
* @throws Exception
*/
public function insertMove(int $nodeId, string $owner, array $addedUsers, array $targetUsers): Entity {
public function insertMove(int $nodeId): FsMove {
// A move for this node may still be pending. Replace it with a fresh row rather than
// keeping it: the job deletes the rows it has processed by ID, so a row the job was
// already working on would be deleted without this move having been processed. A
// fresh row has a new ID, which the running job doesn't know about, so it survives
// until the next run.
$this->db->beginTransaction();
try {
$move = $this->findByNodeId(FsMove::class, $nodeId);
} catch (DoesNotExistException $e) {
$qb = $this->db->getQueryBuilder();
$qb->delete(FsMove::$tableName)
->where($qb->expr()->eq('node_id', $qb->createPositionalParameter($nodeId, IQueryBuilder::PARAM_INT)));
$qb->executeStatement();

$move = new FsMove();
$move->setNodeId($nodeId);
$move->setOwner($owner);
$move->setAddedUsers($addedUsers);
$move->setTargetUsers($targetUsers);
$this->insert($move);
$arguments = [ 'type' => FsDeletion::class ];
if (!$this->jobList->has(ProcessFsActionsJob::class, $arguments)) {
$this->jobList->add(ProcessFsActionsJob::class, $arguments);
}
$this->db->commit();
} catch (\Throwable $e) {
$this->db->rollBack();
throw $e;
}

$arguments = [ 'type' => FsMove::class ];
if (!$this->jobList->has(ProcessFsActionsJob::class, $arguments)) {
$this->jobList->add(ProcessFsActionsJob::class, $arguments);
}
return $move;
}
Expand Down
40 changes: 2 additions & 38 deletions lib/Db/FsMove.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,60 +15,24 @@
* @package OCA\Recognize\Db
* @method int getNodeId()
* @method setNodeId(int $nodeId)
* @method string getOwner()
* @method setOwner(string $owner)
*/
final class FsMove extends Entity {
protected ?int $nodeId = null;
protected ?string $owner = null;
protected ?string $addedUsers = null;
protected ?string $targetUsers = null;

/**
* @var string[]
*/
public static array $columns = ['id', 'node_id', 'owner', 'added_users', 'target_users'];
public static array $columns = ['id', 'node_id'];

/**
* @var string[]
*/
public static array $fields = ['id', 'nodeId', 'owner', 'addedUsers', 'targetUsers'];
public static array $fields = ['id', 'nodeId'];

public static string $tableName = 'recognize_fs_moves';

public function __construct() {
// add types in constructor
$this->addType('nodeId', 'integer');
$this->addType('owner', 'string');
$this->addType('addedUsers', 'string');
$this->addType('targetUsers', 'string');
}

/**
* @return list<string>
*/
public function getAddedUsers(): array {
return explode(',', $this->addedUsers ?? '');
}

/**
* @return list<string>
*/
public function getTargetUsers(): array {
return explode(',', $this->targetUsers ?? '');
}

/**
* @param list<string> $users
*/
public function setAddedUsers(array $users): void {
$this->setter('addedUsers', [implode(',', $users)]);
}

/**
* @param list<string> $users
*/
public function setTargetUsers(array $users): void {
$this->setter('targetUsers', [implode(',', $users)]);
}
}
41 changes: 5 additions & 36 deletions lib/Hooks/FileListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Cache\CacheEntryInsertedEvent;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\NodeCreatedEvent;
Expand All @@ -37,9 +35,6 @@
final class FileListener implements IEventListener {
private ?bool $movingFromIgnoredTerritory;
private ?array $movingDirFromIgnoredTerritory;
/** @var list<string> */
private array $sourceUserIds;
private ?Node $source = null;

/** @var array<string, bool> */
private array $addedMounts = [];
Expand All @@ -48,28 +43,10 @@ public function __construct(
private LoggerInterface $logger,
private IgnoreService $ignoreService,
private IRootFolder $rootFolder,
private IUserMountCache $userMountCache,
private FsActionMapper $fsActionMapper,
) {
$this->movingFromIgnoredTerritory = null;
$this->movingDirFromIgnoredTerritory = null;
$this->sourceUserIds = [];
}

/**
* @param int $nodeId
* @return list<string>
* @throws InvalidPathException
* @throws NotFoundException
*/
private function getUsersWithFileAccess(int $nodeId): array {
$this->userMountCache->clear();
$mountInfos = $this->userMountCache->getMountsForFileId($nodeId);
$userIds = array_map(static function (ICachedMountInfo $mountInfo) {
return $mountInfo->getUser()->getUID();
}, $mountInfos);

return array_values(array_unique($userIds));
}

public function handle(Event $event): void {
Expand Down Expand Up @@ -112,8 +89,6 @@ public function handle(Event $event): void {
} else {
$this->movingDirFromIgnoredTerritory = $this->getDirIgnores($event->getSource());
}
$this->sourceUserIds = $this->getUsersWithFileAccess($event->getSource()->getId());
$this->source = $event->getSource();
return;
}
if ($event instanceof NodeRenamedEvent) {
Expand Down Expand Up @@ -171,7 +146,7 @@ public function handle(Event $event): void {
return;
}
}
$this->postRename($this->source ?? $event->getSource(), $event->getTarget());
$this->postRename($event->getTarget());
return;
}
if ($event instanceof BeforeNodeDeletedEvent) {
Expand Down Expand Up @@ -282,20 +257,14 @@ public function postInsert(Node $node, bool $recurse = true, ?array $mimeTypes =
* @throws NotFoundException
* @throws Exception
*/
public function postRename(Node $source, Node $target): void {
$targetUserIds = $this->getUsersWithFileAccess($target->getId());

$usersToAdd = array_values(array_diff($targetUserIds, $this->sourceUserIds));
$existingUsers = array_diff($targetUserIds, $usersToAdd);
$sourceOwner = $source->getOwner();
$targetOwner = $target->getOwner();
$ownerId = $sourceOwner?->getUID() ?? $targetOwner?->getUID() ?? $existingUsers[0];

public function postRename(Node $target): void {
if (preg_match('#^/[^/]*?/files/#', $target->getPath()) !== 1 && preg_match('#^/groupfolders/#', $target->getPath()) !== 1) {
return;
}

$this->fsActionMapper->insertMove($target->getId(), $ownerId, $usersToAdd, $targetUserIds);
// Who has access to the moved node, and to each file below it, is resolved when
// the move is processed, so the node ID is all that needs to be recorded.
$this->fsActionMapper->insertMove($target->getId());
}

/**
Expand Down
47 changes: 47 additions & 0 deletions lib/Migration/Version013002000Date20260924120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* Copyright (c) 2026 The Recognize contributors.
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
*/
declare(strict_types=1);
namespace OCA\Recognize\Migration;

use Closure;
use Doctrine\DBAL\Schema\SchemaException;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* Drops the move payload columns: users with access are resolved when a move is processed.
*/
final class Version013002000Date20260924120000 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return ?ISchemaWrapper
* @throws SchemaException
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

if (!$schema->hasTable('recognize_fs_moves')) {
return null;
}

$changed = false;
$table = $schema->getTable('recognize_fs_moves');
foreach (['owner', 'added_users', 'target_users'] as $column) {
if ($table->hasColumn($column)) {
$table->dropColumn($column);
$changed = true;
}
}
return $changed ? $schema : null;
}
}
Loading
Loading