diff options
author | Robin Appelman <robin@icewind.nl> | 2018-09-19 20:02:15 +0300 |
---|---|---|
committer | Robin Appelman <robin@icewind.nl> | 2018-11-25 03:39:54 +0300 |
commit | 15408c164ee8e11fb1fb40fb9e9bfa10e61b8a64 (patch) | |
tree | 603aec5edb156b0b3de50ba358313abfbfef7f99 | |
parent | d0b3055c4c415ee9c6845b6a73097457fe41b3d9 (diff) |
Trashbin backend for group folders
Signed-off-by: Robin Appelman <robin@icewind.nl>
-rw-r--r-- | appinfo/info.xml | 4 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 27 | ||||
-rw-r--r-- | lib/Folder/FolderManager.php | 34 | ||||
-rw-r--r-- | lib/Migration/Version104000Date20180918132853.php | 59 | ||||
-rw-r--r-- | lib/Mount/GroupFolderStorage.php | 39 | ||||
-rw-r--r-- | lib/Mount/MountProvider.php | 35 | ||||
-rw-r--r-- | lib/Trash/GroupTrashItem.php | 33 | ||||
-rw-r--r-- | lib/Trash/TrashBackend.php | 237 | ||||
-rw-r--r-- | lib/Trash/TrashManager.php | 63 |
9 files changed, 498 insertions, 33 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index d8566944..0800bbaf 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -36,4 +36,8 @@ Note: encrypting the contents of group folders is currently not supported.]]></d <admin>OCA\GroupFolders\Settings\Admin</admin> <admin-section>OCA\GroupFolders\Settings\Section</admin-section> </settings> + + <trash> + <backend for="OCA\GroupFolders\Mount\GroupFolderStorage">OCA\GroupFolders\Trash\TrashBackend</backend> + </trash> </info> diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 98c3e3aa..7b41a950 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -24,6 +24,8 @@ namespace OCA\GroupFolders\AppInfo; use OC\Group\Manager; use OCA\GroupFolders\Folder\FolderManager; use OCA\GroupFolders\Mount\MountProvider; +use OCA\GroupFolders\Trash\TrashBackend; +use OCA\GroupFolders\Trash\TrashManager; use OCP\AppFramework\App; use OCP\AppFramework\IAppContainer; use OCP\Files\NotFoundException; @@ -35,25 +37,36 @@ class Application extends App { parent::__construct('groupfolders', $urlParams); $container = $this->getContainer(); - $container->registerService(FolderManager::class, function (IAppContainer $c) { - return new FolderManager($c->getServer()->getDatabaseConnection()); + + $container->registerService('GroupAppFolder', function(IAppContainer $c) { + try { + return $c->getServer()->getRootFolder()->get('__groupfolders'); + } catch (NotFoundException $e) { + return $c->getServer()->getRootFolder()->newFolder('__groupfolders'); + } }); $container->registerService(MountProvider::class, function (IAppContainer $c) { $rootProvider = function () use ($c) { - try { - return $c->getServer()->getRootFolder()->get('__groupfolders'); - } catch (NotFoundException $e) { - return $c->getServer()->getRootFolder()->newFolder('__groupfolders'); - } + return $c->query('GroupAppFolder'); }; return new MountProvider( $c->getServer()->getGroupManager(), $c->query(FolderManager::class), + $c->getServer()->getStorageFactory(), $rootProvider ); }); + + $container->registerService(TrashBackend::class, function(IAppContainer $c) { + return new TrashBackend( + $c->query(FolderManager::class), + $c->query(TrashManager::class), + $c->query('GroupAppFolder'), + $c->query(MountProvider::class) + ); + }); } public function register() { diff --git a/lib/Folder/FolderManager.php b/lib/Folder/FolderManager.php index a6a779ac..47de8b48 100644 --- a/lib/Folder/FolderManager.php +++ b/lib/Folder/FolderManager.php @@ -24,16 +24,19 @@ namespace OCA\GroupFolders\Folder; use OCP\Constants; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUser; class FolderManager { /** @var IDBConnection */ private $connection; - /** - * @param IDBConnection $connection - */ - public function __construct(IDBConnection $connection) { + /** @var IGroupManager */ + private $groupManager; + + public function __construct(IDBConnection $connection, IGroupManager $groupManager) { $this->connection = $connection; + $this->groupManager = $groupManager; } public function getAllFolders() { @@ -252,4 +255,27 @@ class FolderManager { ->where($query->expr()->eq('group_id', $query->createNamedParameter($groupId))); $query->execute(); } + + /** + * @param IUser $user + * @return array[] + */ + public function getFoldersForUser(IUser $user) { + $groups = $this->groupManager->getUserGroupIds($user); + $folders = array_reduce($groups, function ($folders, $groupId) { + return array_merge($folders, $this->getFoldersForGroup($groupId)); + }, []); + + $mergedFolders = []; + foreach ($folders as $folder) { + $id = $folder['folder_id']; + if (isset($mergedFolders[$id])) { + $mergedFolders[$id]['permissions'] |= $folder['permissions']; + } else { + $mergedFolders[$id] = $folder; + } + } + + return array_values($mergedFolders); + } } diff --git a/lib/Migration/Version104000Date20180918132853.php b/lib/Migration/Version104000Date20180918132853.php new file mode 100644 index 00000000..4a0f2549 --- /dev/null +++ b/lib/Migration/Version104000Date20180918132853.php @@ -0,0 +1,59 @@ +<?php + +declare(strict_types=1); + +namespace OCA\GroupFolders\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +/** + * Auto-generated migration step: Please modify to your needs! + */ +class Version104000Date20180918132853 extends SimpleMigrationStep { + public function name(): string { + return 'Add group_folders_trash table'; + } + + public function description(): string { + return 'Adds table to store trashbin information for group folders'; + } + + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('group_folders_trash')) { + $table = $schema->createTable('group_folders_trash'); + $table->addColumn('trash_id', 'bigint', [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('name', 'string', [ + 'notnull' => true, + 'length' => 250, + ]); + $table->addColumn('original_location', 'string', [ + 'notnull' => true, + 'length' => 4000, + ]); + $table->addColumn('deleted_time', 'bigint', [ + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('folder_id', 'bigint', [ + 'notnull' => true, + 'length' => 6, + ]); + $table->setPrimaryKey(['trash_id']); + $table->addIndex(['folder_id'], 'groups_folder_trash_folder'); + $table->addIndex(['folder_id', 'name'], 'groups_folder_name'); + $table->addUniqueIndex(['folder_id', 'name', 'deleted_time'], 'groups_folder_trash_unique'); + } + + return $schema; + } +} diff --git a/lib/Mount/GroupFolderStorage.php b/lib/Mount/GroupFolderStorage.php new file mode 100644 index 00000000..ce278dde --- /dev/null +++ b/lib/Mount/GroupFolderStorage.php @@ -0,0 +1,39 @@ +<?php +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\GroupFolders\Mount; + + +use OC\Files\Storage\Wrapper\Quota; + +class GroupFolderStorage extends Quota { + /** @var int */ + private $folderId; + + public function __construct($parameters) { + parent::__construct($parameters); + $this->folderId = $parameters['folder_id']; + } + + public function getFolderId() { + return $this->folderId; + } +} diff --git a/lib/Mount/MountProvider.php b/lib/Mount/MountProvider.php index df913591..e3719888 100644 --- a/lib/Mount/MountProvider.php +++ b/lib/Mount/MountProvider.php @@ -23,7 +23,6 @@ namespace OCA\GroupFolders\Mount; use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\PermissionsMask; -use OC\Files\Storage\Wrapper\Quota; use OCA\GroupFolders\Folder\FolderManager; use OCP\Files\Config\IMountProvider; use OCP\Files\Folder; @@ -46,34 +45,23 @@ class MountProvider implements IMountProvider { /** @var FolderManager */ private $folderManager; + /** @var IStorageFactory */ + private $storageFactory; + /** * @param IGroupManager $groupProvider * @param FolderManager $folderManager * @param callable $rootProvider */ - public function __construct(IGroupManager $groupProvider, FolderManager $folderManager, $rootProvider) { + public function __construct(IGroupManager $groupProvider, FolderManager $folderManager, IStorageFactory $storageFactory, callable $rootProvider) { $this->groupProvider = $groupProvider; $this->folderManager = $folderManager; + $this->storageFactory = $storageFactory; $this->rootProvider = $rootProvider; } public function getFoldersForUser(IUser $user) { - $groups = $this->groupProvider->getUserGroupIds($user); - $folders = array_reduce($groups, function ($folders, $groupId) { - return array_merge($folders, $this->folderManager->getFoldersForGroup($groupId)); - }, []); - - $mergedFolders = []; - foreach ($folders as $folder) { - $id = $folder['folder_id']; - if (isset($mergedFolders[$id])) { - $mergedFolders[$id]['permissions'] |= $folder['permissions']; - } else { - $mergedFolders[$id] = $folder; - } - } - - return array_values($mergedFolders); + return $this->folderManager->getFoldersForUser($user); } public function getMountsForUser(IUser $user, IStorageFactory $loader) { @@ -91,7 +79,7 @@ class MountProvider implements IMountProvider { * @param int $quota * @return IMountPoint */ - private function getMount($id, $mountPoint, $permissions, $quota) { + public function getMount($id, $mountPoint, $permissions, $quota) { $folder = $this->getFolder($id); $baseStorage = new Jail([ 'storage' => $folder->getStorage(), @@ -101,14 +89,17 @@ class MountProvider implements IMountProvider { 'storage' => $baseStorage, 'mask' => $permissions ]); - $quotaStorage = new Quota([ + $quotaStorage = new GroupFolderStorage([ 'storage' => $maskedStore, - 'quota' => $quota + 'quota' => $quota, + 'folder_id' => $id ]); return new GroupMountPoint( $quotaStorage, - $mountPoint + $mountPoint, + null, + $this->storageFactory ); } diff --git a/lib/Trash/GroupTrashItem.php b/lib/Trash/GroupTrashItem.php new file mode 100644 index 00000000..8bb7568f --- /dev/null +++ b/lib/Trash/GroupTrashItem.php @@ -0,0 +1,33 @@ +<?php +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\GroupFolders\Trash; + +use OCA\Files_Trashbin\Trash\TrashItem; + +class GroupTrashItem extends TrashItem { + /** @var Fold */ + private $groupFolder; + + public function isRootItem(): bool { + return substr_count($this->getTrashPath(), '/') === 2; + } +} diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php new file mode 100644 index 00000000..efd3e1b3 --- /dev/null +++ b/lib/Trash/TrashBackend.php @@ -0,0 +1,237 @@ +<?php +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\GroupFolders\Trash; + +use OCA\Files_Trashbin\Trash\ITrashBackend; +use OCA\Files_Trashbin\Trash\ITrashItem; +use OCA\Files_Trashbin\Trash\TrashItem; +use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Mount\GroupFolderStorage; +use OCA\GroupFolders\Mount\MountProvider; +use OCP\Files\Folder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\Files\Storage\IStorage; +use OCP\IUser; + +class TrashBackend implements ITrashBackend { + /** @var FolderManager */ + private $folderManager; + + /** @var TrashManager */ + private $trashManager; + + /** @var Folder */ + private $appFolder; + + /** @var MountProvider */ + private $mountProvider; + + public function __construct( + FolderManager $folderManager, + TrashManager $trashManager, + Folder $appFolder, + MountProvider $mountProvider + ) { + $this->folderManager = $folderManager; + $this->trashManager = $trashManager; + $this->appFolder = $appFolder; + $this->mountProvider = $mountProvider; + } + + public function listTrashRoot(IUser $user): array { + $folders = $this->folderManager->getFoldersForUser($user); + return $this->getTrashForFolders($user, array_map(function (array $folder) { + return $folder['folder_id']; + }, $folders)); + } + + public function listTrashFolder(ITrashItem $trashItem): array { + $user = $trashItem->getUser(); + $folder = $this->getNodeForTrashItem($user, $trashItem); + if (!$folder instanceof Folder) { + return []; + } + $content = $folder->getDirectoryListing(); + return array_map(function (Node $node) use ($trashItem, $user) { + return new GroupTrashItem( + $this, + $trashItem->getOriginalLocation() . '/' . $node->getName(), + $trashItem->getDeletedTime(), + $trashItem->getTrashPath() . '/' . $node->getName(), + $node, + $user + ); + }, $content); + } + + public function restoreItem(ITrashItem $item) { + $user = $item->getUser(); + list(, $folderId) = explode('/', $item->getTrashPath()); + $node = $this->getNodeForTrashItem($user, $item); + if ($node === null) { + throw new NotFoundException(); + } + + $trashStorage = $node->getStorage(); + $targetFolder = $this->mountProvider->getFolder($folderId); + $originalLocation = $item->getOriginalLocation(); + $parent = dirname($originalLocation); + if ($parent === '.') { + $parent = ''; + } + if ($parent !== '' && !$targetFolder->nodeExists($parent)) { + $originalLocation = basename($originalLocation); + } + $targetLocation = $targetFolder->getInternalPath() . '/' . $originalLocation; + $targetFolder->getStorage()->moveFromStorage($trashStorage, $node->getInternalPath(), $targetLocation); + $targetFolder->getStorage()->getCache()->moveFromCache($trashStorage->getCache(), $node->getInternalPath(), $targetLocation); + $this->trashManager->removeItem($folderId, $item->getName(), $item->getDeletedTime()); + } + + public function removeItem(ITrashItem $item) { + $user = $item->getUser(); + list(, $folderId) = explode('/', $item->getTrashPath()); + $node = $this->getNodeForTrashItem($user, $item); + if ($node === null) { + throw new NotFoundException(); + } + $node->getStorage()->unlink($node->getInternalPath()); + $node->getStorage()->getCache()->remove($node->getInternalPath()); + if ($item->isRootItem()) { + $this->trashManager->removeItem($folderId, $item->getName(), $item->getDeletedTime()); + } + } + + public function moveToTrash(IStorage $storage, string $internalPath): bool { + if ($storage->instanceOfStorage(GroupFolderStorage::class)) { + /** @var GroupFolderStorage $storage */ + $name = basename($internalPath); + $folderId = $storage->getFolderId(); + $trashFolder = $this->getTrashFolder($folderId); + $trashStorage = $trashFolder->getStorage(); + $time = time(); + $trashName = $name . '.d' . $time; + $targetInternalPath = $trashFolder->getInternalPath() . '/' . $trashName; + $trashStorage->moveFromStorage($storage, $internalPath, $targetInternalPath); + $this->trashManager->addTrashItem($folderId, $name, $time, $internalPath); + $trashStorage->getCache()->moveFromCache($storage->getCache(), $internalPath, $targetInternalPath); + return true; + } else { + return false; + } + } + + private function userHasAccessToFolder(IUser $user, int $folderId) { + $folders = $this->folderManager->getFoldersForUser($user); + $folderIds = array_map(function (array $folder) { + return $folder['folder_id']; + }, $folders); + return in_array($folderId, $folderIds); + } + + /** + * @param IUser $user + * @param ITrashItem $trashItem + * @return null|Node + */ + private function getNodeForTrashItem(IUser $user, ITrashItem $trashItem) { + list(, $folderId, $path) = explode('/', $trashItem->getTrashPath(), 3); + $folders = $this->folderManager->getFoldersForUser($user); + foreach ($folders as $groupFolder) { + if ($groupFolder['folder_id'] === (int)$folderId) { + $trashRoot = $this->getTrashFolder($folderId); + try { + $node = $trashRoot->get($path); + return $node; + } catch (NotFoundException $e) { + return null; + } + } + } + return null; + } + + private function getTrashFolder($folderId) { + try { + return $this->appFolder->get('trash/' . $folderId); + } catch (NotFoundException $e) { + /** @var Folder $trashRoot */ + $trashRoot = $this->appFolder->nodeExists('trash') ? $this->appFolder->get('trash') : $this->appFolder->newFolder('trash'); + return $trashRoot->newFolder($folderId); + } + } + + private function getTrashForFolders(IUser $user, array $folderIds) { + $rows = $this->trashManager->listTrashForFolders($folderIds); + $indexedRows = []; + foreach ($rows as $row) { + $key = $row['folder_id'] . '/' . $row['name'] . '/' . $row['deleted_time']; + $indexedRows[$key] = $row; + } + $items = []; + foreach ($folderIds as $folderId) { + $trashFolder = $this->getTrashFolder($folderId); + $content = $trashFolder->getDirectoryListing(); + foreach ($content as $item) { + $pathParts = pathinfo($item->getName()); + $timestamp = substr($pathParts['extension'], 1); + $name = $pathParts['filename']; + $key = $folderId . '/' . $name . '/' . $timestamp; + $originalLocation = isset($indexedRows[$key]) ? $indexedRows[$key]['original_location'] : ''; + $info = $item->getFileInfo(); + $info['name'] = $name; + $items[] = new GroupTrashItem( + $this, + $originalLocation, + $timestamp, + '/' . $folderId . '/' . $item->getName(), + $info, + $user + ); + } + } + return $items; + } + + public function getTrashNodeById(IUser $user, int $fileId) { + try { + /** @var Folder $trashFolder */ + $trashFolder = $this->appFolder->get('trash'); + $storage = $this->appFolder->getStorage(); + $path = $storage->getCache()->getPathById($fileId); + if (!$path) { + return null; + } + $absolutePath = $this->appFolder->getMountPoint()->getMountPoint() . $path; + $relativePath = $trashFolder->getRelativePath($absolutePath); + list(, $folderId) = explode('/', $relativePath); + if ($this->userHasAccessToFolder($user, (int)$folderId)) { + return $trashFolder->get($relativePath); + } else { + return null; + } + } catch (NotFoundException $e) { + return null; + } + } +} diff --git a/lib/Trash/TrashManager.php b/lib/Trash/TrashManager.php new file mode 100644 index 00000000..ef5e8504 --- /dev/null +++ b/lib/Trash/TrashManager.php @@ -0,0 +1,63 @@ +<?php +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\GroupFolders\Trash; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class TrashManager { + /** @var IDBConnection */ + private $connection; + + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + } + + public function listTrashForFolders(array $folderIds): array { + $query = $this->connection->getQueryBuilder(); + $query->select(['trash_id', 'name', 'deleted_time', 'original_location', 'folder_id']) + ->from('group_folders_trash') + ->where($query->expr()->in('folder_id', $query->createNamedParameter($folderIds, IQueryBuilder::PARAM_INT_ARRAY))); + return $query->execute()->fetchAll(); + } + + public function addTrashItem(int $folderId, string $name, int $deletedTime, string $originalLocation) { + $query = $this->connection->getQueryBuilder(); + $query->insert('group_folders_trash') + ->values([ + 'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), + 'name' => $query->createNamedParameter($name), + 'deleted_time' => $query->createNamedParameter($deletedTime, IQueryBuilder::PARAM_INT), + 'original_location' => $query->createNamedParameter($originalLocation), + ]); + $query->execute(); + } + + public function removeItem(int $folderId, string $name, int $deletedTime) { + $query = $this->connection->getQueryBuilder(); + $query->delete('group_folders_trash') + ->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('name', $query->createNamedParameter($name))) + ->andWhere($query->expr()->eq('deleted_time', $query->createNamedParameter($deletedTime, IQueryBuilder::PARAM_INT))); + $query->execute(); + } +} |