diff --git a/appinfo/info.xml b/appinfo/info.xml
index f85c804ff..2e531b5d1 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -10,7 +10,7 @@ Folders can be configured from *Group folders* in the admin settings.
After a folder is created, the admin can give access to the folder to one or more groups, control their write/sharing permissions and assign a quota for the folder.
Note: Encrypting the contents of group folders is currently not supported.]]>
- 15.0.2
+ 15.1.0
agpl
Robin Appelman
GroupFolders
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 0bed887ce..31eb86a54 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -49,6 +49,7 @@
use OCA\GroupFolders\Trash\TrashBackend;
use OCA\GroupFolders\Trash\TrashManager;
use OCA\GroupFolders\Versions\GroupVersionsExpireManager;
+use OCA\GroupFolders\Versions\GroupVersionsMapper;
use OCA\GroupFolders\Versions\VersionsBackend;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -57,6 +58,8 @@
use OCP\AppFramework\IAppContainer;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\Config\IMountProviderCollection;
+use OCP\Files\IMimeTypeLoader;
+use OCP\Files\IRootFolder;
use OCP\ICacheFactory;
use OCP\IDBConnection;
use OCP\IGroup;
@@ -133,10 +136,13 @@ public function register(IRegistrationContext $context): void {
$context->registerService(VersionsBackend::class, function (IAppContainer $c): VersionsBackend {
return new VersionsBackend(
+ $c->get(IRootFolder::class),
$c->get('GroupAppFolder'),
$c->get(MountProvider::class),
$c->get(ITimeFactory::class),
- $c->get(LoggerInterface::class)
+ $c->get(LoggerInterface::class),
+ $c->get(GroupVersionsMapper::class),
+ $c->get(IMimeTypeLoader::class),
);
});
diff --git a/lib/Helper/LazyFolder.php b/lib/Helper/LazyFolder.php
index 13ae2364f..b0d50dff7 100644
--- a/lib/Helper/LazyFolder.php
+++ b/lib/Helper/LazyFolder.php
@@ -262,4 +262,8 @@ public function changeLock($targetType) {
public function unlock($type) {
return $this->__call(__FUNCTION__, func_get_args());
}
+
+ public function getParentId(): int {
+ return $this->__call(__FUNCTION__, func_get_args());
+ }
}
diff --git a/lib/Migration/Version16000Date20230821085801.php b/lib/Migration/Version16000Date20230821085801.php
new file mode 100644
index 000000000..e34346bff
--- /dev/null
+++ b/lib/Migration/Version16000Date20230821085801.php
@@ -0,0 +1,86 @@
+
+ *
+ * @author Louis Chmn
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\GroupFolders\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Auto-generated migration step: Please modify to your needs!
+ */
+class Version16000Date20230821085801 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if ($schema->hasTable("group_folders_versions")) {
+ return null;
+ }
+
+ $table = $schema->createTable("group_folders_versions");
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('file_id', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('timestamp', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('size', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('mimetype', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('metadata', Types::TEXT, [
+ 'notnull' => true,
+ 'default' => '{}',
+ ]);
+
+ $table->setPrimaryKey(['id']);
+ $table->addUniqueIndex(['file_id', 'timestamp'], 'gf_versions_uniq_index');
+
+ return $schema;
+ }
+}
diff --git a/lib/Versions/GroupVersion.php b/lib/Versions/GroupVersion.php
index 6e40fd780..20fda8d72 100644
--- a/lib/Versions/GroupVersion.php
+++ b/lib/Versions/GroupVersion.php
@@ -30,11 +30,6 @@
use OCP\IUser;
class GroupVersion extends Version {
- /** @var File */
- private $versionFile;
-
- /** @var int */
- private $folderId;
public function __construct(
int $timestamp,
@@ -46,12 +41,11 @@ public function __construct(
FileInfo $sourceFileInfo,
IVersionBackend $backend,
IUser $user,
- File $versionFile,
- int $folderId
+ string $label,
+ private File $versionFile,
+ private int $folderId,
) {
- parent::__construct($timestamp, $revisionId, $name, $size, $mimetype, $path, $sourceFileInfo, $backend, $user);
- $this->versionFile = $versionFile;
- $this->folderId = $folderId;
+ parent::__construct($timestamp, $revisionId, $name, $size, $mimetype, $path, $sourceFileInfo, $backend, $user, $label);
}
public function getVersionFile(): File {
diff --git a/lib/Versions/GroupVersionEntity.php b/lib/Versions/GroupVersionEntity.php
new file mode 100644
index 000000000..6284e18ba
--- /dev/null
+++ b/lib/Versions/GroupVersionEntity.php
@@ -0,0 +1,92 @@
+
+ *
+ * @author Louis Chmn
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\GroupFolders\Versions;
+
+use JsonSerializable;
+
+use OCP\AppFramework\Db\Entity;
+use OCP\DB\Types;
+
+/**
+ * @method int getFileId()
+ * @method void setFileId(int $fileId)
+ * @method int getTimestamp()
+ * @method void setTimestamp(int $timestamp)
+ * @method int|float getSize()
+ * @method void setSize(int|float $size)
+ * @method int getMimetype()
+ * @method void setMimetype(int $mimetype)
+ * @method string getMetadata()
+ * @method void setMetadata(string $metadata)
+ */
+class GroupVersionEntity extends Entity implements JsonSerializable {
+ protected ?int $fileId = null;
+ protected ?int $timestamp = null;
+ protected ?int $size = null;
+ protected ?int $mimetype = null;
+ protected ?string $metadata = null;
+
+ public function __construct() {
+ $this->addType('id', Types::INTEGER);
+ $this->addType('file_id', Types::INTEGER);
+ $this->addType('timestamp', Types::INTEGER);
+ $this->addType('size', Types::INTEGER);
+ $this->addType('mimetype', Types::INTEGER);
+ $this->addType('metadata', Types::STRING);
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->id,
+ 'file_id' => $this->fileId,
+ 'timestamp' => $this->timestamp,
+ 'size' => $this->size,
+ 'mimetype' => $this->mimetype,
+ 'metadata' => $this->metadata,
+ ];
+ }
+
+ public function getLabel(): string {
+ return $this->getDecodedMetadata()['label'] ?? '';
+ }
+
+ public function setLabel(string $label): void {
+ $metadata = $this->getDecodedMetadata();
+ $metadata['label'] = $label;
+ $this->setDecodedMetadata($metadata);
+ $this->markFieldUpdated('metadata');
+ }
+
+ public function getDecodedMetadata(): array {
+ return json_decode($this->metadata ?? '', true, 512, JSON_THROW_ON_ERROR) ?? [];
+ }
+
+ public function setDecodedMetadata(array $value): void {
+ $this->metadata = json_encode($value, JSON_THROW_ON_ERROR);
+ $this->markFieldUpdated('metadata');
+ }
+}
diff --git a/lib/Versions/GroupVersionsMapper.php b/lib/Versions/GroupVersionsMapper.php
new file mode 100644
index 000000000..e7d932051
--- /dev/null
+++ b/lib/Versions/GroupVersionsMapper.php
@@ -0,0 +1,86 @@
+
+ *
+ * @author Louis Chmn
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\GroupFolders\Versions;
+
+use OCP\AppFramework\Db\QBMapper;
+use OCP\IDBConnection;
+
+/**
+ * @extends QBMapper
+ */
+class GroupVersionsMapper extends QBMapper {
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'group_folders_versions', GroupVersionEntity::class);
+ }
+
+ /**
+ * @return GroupVersionEntity[]
+ */
+ public function findAllVersionsForFileId(int $fileId): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)));
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @return GroupVersionEntity
+ */
+ public function findCurrentVersionForFileId(int $fileId): GroupVersionEntity {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
+ ->orderBy('timestamp', 'DESC')
+ ->setMaxResults(1);
+
+ return $this->findEntity($qb);
+ }
+
+ public function findVersionForFileId(int $fileId, int $timestamp): GroupVersionEntity {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
+ ->andWhere($qb->expr()->eq('timestamp', $qb->createNamedParameter($timestamp)));
+
+ return $this->findEntity($qb);
+ }
+
+ public function deleteAllVersionsForFileId(int $fileId): int {
+ $qb = $this->db->getQueryBuilder();
+
+ return $qb->delete($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
+ ->executeStatement();
+ }
+}
diff --git a/lib/Versions/VersionsBackend.php b/lib/Versions/VersionsBackend.php
index 6a62ea13f..7fb53aa79 100644
--- a/lib/Versions/VersionsBackend.php
+++ b/lib/Versions/VersionsBackend.php
@@ -23,6 +23,9 @@
namespace OCA\GroupFolders\Versions;
+use OCA\Files_Versions\Versions\IDeletableVersionBackend;
+use OCA\Files_Versions\Versions\INameableVersionBackend;
+use OCA\Files_Versions\Versions\INeedSyncVersionBackend;
use OCA\Files_Versions\Versions\IVersion;
use OCA\Files_Versions\Versions\IVersionBackend;
use OCA\GroupFolders\Mount\GroupMountPoint;
@@ -31,6 +34,8 @@
use OCP\Files\File;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
+use OCP\Files\IMimeTypeLoader;
+use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\IStorage;
@@ -38,60 +43,113 @@
use OCP\Constants;
use Psr\Log\LoggerInterface;
-class VersionsBackend implements IVersionBackend {
- private Folder $appFolder;
- private MountProvider $mountProvider;
- private ITimeFactory $timeFactory;
- private LoggerInterface $logger;
-
- public function __construct(Folder $appFolder, MountProvider $mountProvider, ITimeFactory $timeFactory, LoggerInterface $logger) {
- $this->appFolder = $appFolder;
- $this->mountProvider = $mountProvider;
- $this->timeFactory = $timeFactory;
- $this->logger = $logger;
+class VersionsBackend implements IVersionBackend, INameableVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend {
+ public function __construct(
+ private IRootFolder $rootFolder,
+ private Folder $appFolder,
+ private MountProvider $mountProvider,
+ private ITimeFactory $timeFactory,
+ private LoggerInterface $logger,
+ private GroupVersionsMapper $groupVersionsMapper,
+ private IMimeTypeLoader $mimeTypeLoader,
+ ) {
}
public function useBackendForStorage(IStorage $storage): bool {
return true;
}
- public function getVersionsForFile(IUser $user, FileInfo $file): array {
- $mount = $file->getMountPoint();
- if ($mount instanceof GroupMountPoint) {
- try {
- $folderId = $mount->getFolderId();
- /** @var Folder $versionsFolder */
- $versionsFolder = $this->getVersionsFolder($mount->getFolderId())->get((string)$file->getId());
- $versions = array_map(function (Node $versionFile) use ($file, $user, $folderId): GroupVersion {
- if ($versionFile instanceof Folder) {
- $this->logger->error('Found an unexpected subfolder inside the groupfolder version folder.');
- }
- return new GroupVersion(
- (int)$versionFile->getName(),
- (int)$versionFile->getName(),
- $file->getName(),
- $versionFile->getSize(),
- $versionFile->getMimetype(),
- $versionFile->getPath(),
- $file,
- $this,
- $user,
- $versionFile,
- $folderId
- );
- }, $versionsFolder->getDirectoryListing());
- usort($versions, function (GroupVersion $v1, GroupVersion $v2): int {
- return $v2->getTimestamp() <=> $v1->getTimestamp();
- });
+ public function getVersionsForFile(IUser $user, FileInfo $fileInfo): array {
+ $mount = $fileInfo->getMountPoint();
+ if (!($mount instanceof GroupMountPoint)) {
+ return [];
+ }
+
+ try {
+ $folderId = $mount->getFolderId();
+ /** @var Folder $versionsFolder */
+ $versionsFolder = $this->getVersionsFolder($mount->getFolderId())->get((string)$fileInfo->getId());
+
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ $nodes = $userFolder->getById($fileInfo->getId());
+ $file = array_pop($nodes);
+
+ $versions = $this->getVersionsForFileFromDB($fileInfo, $user, $folderId);
+
+ // Early exit if we find any version in the database.
+ // Else we continue to populate the DB from what's on disk.
+ if (count($versions) > 0) {
return $versions;
- } catch (NotFoundException $e) {
- return [];
}
- } else {
+
+ // Insert the entry in the DB for the current version.
+ $versionEntity = new GroupVersionEntity();
+ $versionEntity->setFileId($file->getId());
+ $versionEntity->setTimestamp($file->getMTime());
+ $versionEntity->setSize($file->getSize());
+ $versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype()));
+ $versionEntity->setDecodedMetadata([]);
+ $this->groupVersionsMapper->insert($versionEntity);
+
+ // Insert entries in the DB for existing versions.
+ $versionsOnFS = $versionsFolder->getDirectoryListing();
+ foreach ($versionsOnFS as $version) {
+ if ($version instanceof Folder) {
+ $this->logger->error('Found an unexpected subfolder inside the groupfolder version folder.');
+ }
+
+ $versionEntity = new GroupVersionEntity();
+ $versionEntity->setFileId($file->getId());
+ // HACK: before this commit, versions were created with the current timestamp instead of the version's mtime.
+ // This means that the name of some versions is the exact mtime of the next version. This behavior is now fixed.
+ // To prevent occasional conflicts between the last version and the current one, we decrement the last version mtime.
+ $mtime = (int)$version->getName();
+ if ($mtime === $file->getMTime()) {
+ $versionEntity->setTimestamp($mtime - 1);
+ $version->move($version->getParent()->getPath() . '/' . ($mtime - 1));
+ } else {
+ $versionEntity->setTimestamp($mtime);
+ }
+ $versionEntity->setSize($version->getSize());
+ // Use the main file mimetype for this initialization as the original mimetype is unknown.
+ $versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype()));
+ $versionEntity->setDecodedMetadata([]);
+ $this->groupVersionsMapper->insert($versionEntity);
+ }
+
+ return $this->getVersionsForFileFromDB($file, $user, $folderId);
+ } catch (NotFoundException $e) {
return [];
}
}
+ /**
+ * @return IVersion[]
+ */
+ private function getVersionsForFileFromDB(FileInfo $file, IUser $user, int $folderId): array {
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ /** @var Folder $versionsFolder */
+ $versionsFolder = $this->getVersionsFolder($folderId)->get((string)$file->getId());
+
+ return array_map(
+ fn (GroupVersionEntity $versionEntity) => new GroupVersion(
+ $versionEntity->getTimestamp(),
+ $versionEntity->getTimestamp(),
+ $file->getName(),
+ $versionEntity->getSize(),
+ $this->mimeTypeLoader->getMimetypeById($versionEntity->getMimetype()),
+ $userFolder->getRelativePath($file->getPath()),
+ $file,
+ $this,
+ $user,
+ $versionEntity->getLabel(),
+ $file->getMtime() === $versionEntity->getTimestamp() ? $file : $versionsFolder->get((string)$versionEntity->getTimestamp()),
+ $folderId,
+ ),
+ $this->groupVersionsMapper->findAllVersionsForFileId($file->getId())
+ );
+ }
+
/**
* @return void
*/
@@ -111,7 +169,7 @@ public function createVersion(IUser $user, FileInfo $file) {
$versionMount = $versionFolder->getMountPoint();
$sourceMount = $file->getMountPoint();
$sourceCache = $sourceMount->getStorage()->getCache();
- $revision = $this->timeFactory->getTime();
+ $revision = $file->getMtime();
$versionInternalPath = $versionFolder->getInternalPath() . '/' . $revision;
$sourceInternalPath = $file->getInternalPath();
@@ -198,6 +256,7 @@ public function deleteAllVersionsForFile(int $folderId, int $fileId): void {
$versionsFolder = $this->getVersionsFolder($folderId);
try {
$versionsFolder->get((string)$fileId)->delete();
+ $this->groupVersionsMapper->deleteAllVersionsForFileId($fileId);
} catch (NotFoundException $e) {
}
}
@@ -211,4 +270,67 @@ private function getVersionsFolder(int $folderId): Folder {
return $trashRoot->newFolder((string)$folderId);
}
}
+
+ public function setVersionLabel(IVersion $version, string $label): void {
+ $versionEntity = $this->groupVersionsMapper->findVersionForFileId(
+ $version->getSourceFile()->getId(),
+ $version->getTimestamp(),
+ );
+ if (trim($label) === '') {
+ $label = null;
+ }
+ $versionEntity->setLabel($label ?? '');
+ $this->groupVersionsMapper->update($versionEntity);
+ }
+
+ public function deleteVersion(IVersion $version): void {
+ $sourceFile = $version->getSourceFile();
+ $mount = $sourceFile->getMountPoint();
+
+ if (!($mount instanceof GroupMountPoint)) {
+ return;
+ }
+
+ $versionsFolder = $this->getVersionsFolder($mount->getFolderId())->get((string)$sourceFile->getId());
+ /** @var Folder $versionsFolder */
+ $versionsFolder->get((string)$version->getRevisionId())->delete();
+
+ $versionEntity = $this->groupVersionsMapper->findVersionForFileId(
+ $version->getSourceFile()->getId(),
+ $version->getTimestamp(),
+ );
+ $this->groupVersionsMapper->delete($versionEntity);
+ }
+
+ public function createVersionEntity(File $file): void {
+ $versionEntity = new GroupVersionEntity();
+ $versionEntity->setFileId($file->getId());
+ $versionEntity->setTimestamp($file->getMTime());
+ $versionEntity->setSize($file->getSize());
+ $versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype()));
+ $versionEntity->setDecodedMetadata([]);
+ $this->groupVersionsMapper->insert($versionEntity);
+ }
+
+ public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void {
+ $versionEntity = $this->groupVersionsMapper->findVersionForFileId($sourceFile->getId(), $revision);
+
+ if (isset($properties['timestamp'])) {
+ $versionEntity->setTimestamp($properties['timestamp']);
+ }
+
+ if (isset($properties['size'])) {
+ $versionEntity->setSize($properties['size']);
+ }
+
+ if (isset($properties['mimetype'])) {
+ $versionEntity->setMimetype($properties['mimetype']);
+ }
+
+ $this->groupVersionsMapper->update($versionEntity);
+ }
+
+ public function deleteVersionsEntity(File $file): void {
+ $this->groupVersionsMapper->deleteAllVersionsForFileId($file->getId());
+ }
}
diff --git a/tests/stub.phpstub b/tests/stub.phpstub
index 5b8d85955..866ac0d3e 100644
--- a/tests/stub.phpstub
+++ b/tests/stub.phpstub
@@ -260,6 +260,20 @@ namespace OCA\Files_Versions\Versions {
public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): ?File;
}
+ interface INameableVersionBackend {
+ public function setVersionLabel(IVersion $version, string $label): void;
+ }
+
+ interface IDeletableVersionBackend {
+ public function deleteVersion(IVersion $version): void;
+ }
+
+ interface INeedSyncVersionBackend {
+ public function createVersionEntity(File $file): void;
+ public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void;
+ public function deleteVersionsEntity(File $file): void;
+ }
+
interface IVersion {
public function getBackend(): IVersionBackend;
@@ -293,7 +307,8 @@ namespace OCA\Files_Versions\Versions {
string $path,
FileInfo $sourceFileInfo,
IVersionBackend $backend,
- IUser $user
+ IUser $user,
+ string $label = ''
) {
}