Skip to content
Merged
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
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)]);
}
}
58 changes: 7 additions & 51 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,61 +35,27 @@
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 = [];

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 {
try {
if ($event instanceof \OCP\Files\Config\Event\UserMountAddedEvent) {
$rootId = $event->mountPoint->getRootId();
// Asynchronous, because we potentially recurse and this event needs to be handled fast
$this->onAccessUpdate($event->mountPoint->getStorageId(), $rootId);
// Remember that this mount was added in the current process (see UserMountRemovedEvent below)
$this->addedMounts[$event->mountPoint->getUser()->getUID() . '-' . $rootId] = true;
$this->onAccessUpdate($event->mountPoint->getStorageId(), $event->mountPoint->getRootId());
}

if ($event instanceof \OCP\Files\Config\Event\UserMountRemovedEvent) {
// If we just added this mount, ignore the removal, as the 'removal' event is always fired after
// the 'added' event in server
$rootId = $event->mountPoint->getRootId();
$mountKey = $event->mountPoint->getUser()->getUID() . '-' . $rootId;
if (array_key_exists($mountKey, $this->addedMounts) && $this->addedMounts[$mountKey] === true) {
return;
}
// Asynchronous, because we potentially recurse and this event needs to be handled fast
$this->onAccessUpdate($event->mountPoint->getStorageId(), $rootId);
$this->onAccessUpdate($event->mountPoint->getStorageId(), $event->mountPoint->getRootId());
}

if ($event instanceof BeforeNodeRenamedEvent) {
Expand All @@ -112,8 +76,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 +133,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 +244,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