diff --git a/apps/files_trashbin/lib/Helper.php b/apps/files_trashbin/lib/Helper.php index fc7e97ec8f80d..cdfa847633b14 100644 --- a/apps/files_trashbin/lib/Helper.php +++ b/apps/files_trashbin/lib/Helper.php @@ -13,6 +13,7 @@ use OCP\Constants; use OCP\Files\Cache\ICacheEntry; use OCP\Files\IMimeTypeDetector; +use OCP\Files\Mount\IMountPoint; use OCP\Server; class Helper { @@ -27,9 +28,6 @@ class Helper { * @return \OCP\Files\FileInfo[] */ public static function getTrashFiles($dir, $user, $sortAttribute = '', $sortDescending = false) { - $result = []; - $timestamp = null; - $view = new View('/' . $user . '/files_trashbin/files'); if (ltrim($dir, '/') !== '' && !$view->is_dir($dir)) { @@ -40,50 +38,13 @@ public static function getTrashFiles($dir, $user, $sortAttribute = '', $sortDesc $storage = $mount->getStorage(); $absoluteDir = $view->getAbsolutePath($dir); $internalPath = $mount->getInternalPath($absoluteDir); - $extraData = Trashbin::getExtraData($user); + + $result = []; + $timestamp = null; $dirContent = $storage->getCache()->getFolderContents($mount->getInternalPath($view->getAbsolutePath($dir))); foreach ($dirContent as $entry) { - $entryName = $entry->getName(); - $name = $entryName; - if ($dir === '' || $dir === '/') { - $pathparts = pathinfo($entryName); - $timestamp = substr($pathparts['extension'], 1); - $name = $pathparts['filename']; - } elseif ($timestamp === null) { - // for subfolders we need to calculate the timestamp only once - $parts = explode('/', ltrim($dir, '/')); - $timestamp = substr(pathinfo($parts[0], PATHINFO_EXTENSION), 1); - } - $originalPath = ''; - $originalName = substr($entryName, 0, -strlen($timestamp) - 2); - if (isset($extraData[$originalName][$timestamp]['location'])) { - $originalPath = $extraData[$originalName][$timestamp]['location']; - if (substr($originalPath, -1) === '/') { - $originalPath = substr($originalPath, 0, -1); - } - } - $type = $entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE ? 'dir' : 'file'; - $i = [ - 'name' => $name, - 'mtime' => $timestamp, - 'mimetype' => $type === 'dir' ? 'httpd/unix-directory' : Server::get(IMimeTypeDetector::class)->detectPath($name), - 'type' => $type, - 'directory' => ($dir === '/') ? '' : $dir, - 'size' => $entry->getSize(), - 'etag' => '', - 'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, - 'fileid' => $entry->getId(), - ]; - if ($originalPath) { - if ($originalPath !== '.') { - $i['extraData'] = $originalPath . '/' . $originalName; - } else { - $i['extraData'] = $originalName; - } - } - $i['deletedBy'] = $extraData[$originalName][$timestamp]['deletedBy'] ?? null; - $result[] = new FileInfo($absoluteDir . '/' . $i['name'], $storage, $internalPath . '/' . $i['name'], $i, $mount); + $result[] = self::buildFileInfo($entry, $dir, $timestamp, $extraData, $storage, $absoluteDir, $internalPath, $mount); } if ($sortAttribute !== '') { @@ -92,20 +53,70 @@ public static function getTrashFiles($dir, $user, $sortAttribute = '', $sortDesc return $result; } - /** - * Format file infos for JSON - * - * @param \OCP\Files\FileInfo[] $fileInfos file infos - */ - public static function formatFileInfos($fileInfos) { - $files = []; - foreach ($fileInfos as $i) { - $entry = \OCA\Files\Helper::formatFileInfo($i); - $entry['id'] = $i->getId(); - $entry['etag'] = $entry['mtime']; // add fake etag, it is only needed to identify the preview image - $entry['permissions'] = Constants::PERMISSION_READ; - $files[] = $entry; + public static function getTrashFile(string $dir, string $user, string $name): ?FileInfo { + $timestamp = null; + + $view = new View('/' . $user . '/files_trashbin/files'); + + if (ltrim($dir, '/') !== '' && !$view->is_dir($dir)) { + throw new \Exception('Directory does not exists'); + } + + $mount = $view->getMount($dir); + $storage = $mount->getStorage(); + $absoluteDir = $view->getAbsolutePath($dir); + $internalPath = $mount->getInternalPath($absoluteDir); + + $extraData = Trashbin::getExtraData($user); + $entry = $storage->getCache()->get($mount->getInternalPath($view->getAbsolutePath($dir . '/' . $name))); + if ($entry === false) { + return null; + } + + $timestamp = null; + return self::buildFileInfo($entry, $dir, $timestamp, $extraData, $storage, $absoluteDir, $internalPath, $mount); + } + + private static function buildFileInfo(ICacheEntry $entry, string $dir, ?string &$timestamp, array $extraData, $storage, string $absoluteDir, string $internalPath, IMountPoint $mount): FileInfo { + $entryName = $entry->getName(); + $name = $entryName; + if ($dir === '' || $dir === '/') { + $pathparts = pathinfo($entryName); + $timestamp = substr($pathparts['extension'], 1); + $name = $pathparts['filename']; + } elseif ($timestamp === null) { + // for subfolders we need to calculate the timestamp only once + $parts = explode('/', ltrim($dir, '/')); + $timestamp = substr(pathinfo($parts[0], PATHINFO_EXTENSION), 1); + } + $originalPath = ''; + $originalName = substr($entryName, 0, -strlen($timestamp) - 2); + if (isset($extraData[$originalName][$timestamp]['location'])) { + $originalPath = $extraData[$originalName][$timestamp]['location']; + if (substr($originalPath, -1) === '/') { + $originalPath = substr($originalPath, 0, -1); + } + } + $type = $entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE ? 'dir' : 'file'; + $i = [ + 'name' => $name, + 'mtime' => $timestamp, + 'mimetype' => $type === 'dir' ? 'httpd/unix-directory' : Server::get(IMimeTypeDetector::class)->detectPath($name), + 'type' => $type, + 'directory' => ($dir === '/') ? '' : $dir, + 'size' => $entry->getSize(), + 'etag' => '', + 'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, + 'fileid' => $entry->getId(), + ]; + if ($originalPath) { + if ($originalPath !== '.') { + $i['extraData'] = $originalPath . '/' . $originalName; + } else { + $i['extraData'] = $originalName; + } } - return $files; + $i['deletedBy'] = $extraData[$originalName][$timestamp]['deletedBy'] ?? null; + return new FileInfo($absoluteDir . '/' . $i['name'], $storage, $internalPath . '/' . $i['name'], $i, $mount); } } diff --git a/apps/files_trashbin/lib/Sabre/TrashRoot.php b/apps/files_trashbin/lib/Sabre/TrashRoot.php index 9c730c69d8770..0d124c3d6eb1a 100644 --- a/apps/files_trashbin/lib/Sabre/TrashRoot.php +++ b/apps/files_trashbin/lib/Sabre/TrashRoot.php @@ -15,15 +15,17 @@ use OCA\Files_Trashbin\Trashbin; use OCP\Files\FileInfo; use OCP\IUser; +use RuntimeException; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\ICollection; +use Sabre\DAV\INode; class TrashRoot implements ICollection { public function __construct( - private IUser $user, - private ITrashManager $trashManager, + private readonly IUser $user, + private readonly ITrashManager $trashManager, ) { } @@ -63,41 +65,35 @@ public function createDirectory($name) { public function getChildren(): array { $entries = $this->trashManager->listTrashRoot($this->user); - $children = array_map(function (ITrashItem $entry) { - if ($entry->getType() === FileInfo::TYPE_FOLDER) { - return new TrashFolder($this->trashManager, $entry); - } - return new TrashFile($this->trashManager, $entry); - }, $entries); - - return $children; + return array_map($this->trashItemToTrashNode(...), $entries); } #[\Override] public function getChild($name): ITrash { - $entries = $this->getChildren(); + $entry = $this->trashManager->getTrashRootItem($this->user, $name); - foreach ($entries as $entry) { - if ($entry->getName() === $name) { - return $entry; - } + if ($entry === null) { + throw new NotFound(); } - throw new NotFound(); + return $this->trashItemToTrashNode($entry); } #[\Override] public function childExists($name): bool { - try { - $this->getChild($name); - return true; - } catch (NotFound $e) { - return false; - } + return $this->trashManager->getTrashRootItem($this->user, $name) !== null; } #[\Override] public function getLastModified(): int { return 0; } + + private function trashItemToTrashNode(ITrashItem $entry): ITrash&INode { + return match ($entry->getType()) { + FileInfo::TYPE_FOLDER => new TrashFolder($this->trashManager, $entry), + FileInfo::TYPE_FILE => new TrashFile($this->trashManager, $entry), + default => throw new RuntimeException('Invalid FileInfo object'), + }; + } } diff --git a/apps/files_trashbin/lib/Trash/ITrashManager.php b/apps/files_trashbin/lib/Trash/ITrashManager.php index 25a07d66c8b8c..f5188dc19de71 100644 --- a/apps/files_trashbin/lib/Trash/ITrashManager.php +++ b/apps/files_trashbin/lib/Trash/ITrashManager.php @@ -31,6 +31,15 @@ public function registerBackend(string $storageType, ITrashBackend $backend); #[\Override] public function listTrashRoot(IUser $user): array; + /** + * Get a specific item in the root of the trashbin + * + * @param IUser $user + * @return ?ITrashItem + * @since 35.0.0 + */ + public function getTrashRootItem(IUser $user, string $name): ?ITrashItem; + /** * Temporally prevent files from being moved to the trash * diff --git a/apps/files_trashbin/lib/Trash/LegacyTrashBackend.php b/apps/files_trashbin/lib/Trash/LegacyTrashBackend.php index 54ffd6f4cabb9..11c4bc18321c3 100644 --- a/apps/files_trashbin/lib/Trash/LegacyTrashBackend.php +++ b/apps/files_trashbin/lib/Trash/LegacyTrashBackend.php @@ -15,61 +15,63 @@ use OCP\Files\FileInfo; use OCP\Files\Folder; use OCP\Files\IRootFolder; +use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; use OCP\IUser; use OCP\IUserManager; class LegacyTrashBackend implements ITrashBackend { - /** @var array */ - private $deletedFiles = []; + /** @var array */ + private array $deletedFiles = []; public function __construct( - private IRootFolder $rootFolder, - private IUserManager $userManager, + private readonly IRootFolder $rootFolder, + private readonly IUserManager $userManager, ) { } - /** - * @param array $items - * @param IUser $user - * @param ITrashItem $parent - * @return ITrashItem[] - */ - private function mapTrashItems(array $items, IUser $user, ?ITrashItem $parent = null): array { + private function mapTrashItem(FileInfo $file, IUser $user, ?ITrashItem $parent = null): ITrashItem { $parentTrashPath = ($parent instanceof ITrashItem) ? $parent->getTrashPath() : ''; $isRoot = $parent === null; - return array_map(function (FileInfo $file) use ($parent, $parentTrashPath, $isRoot, $user) { - $originalLocation = $isRoot ? $file['extraData'] : $parent->getOriginalLocation() . '/' . $file->getName(); - if (!$originalLocation) { - $originalLocation = $file->getName(); - } - /** @psalm-suppress UndefinedInterfaceMethod */ - $deletedBy = $this->userManager->get($file['deletedBy']) ?? $parent?->getDeletedBy(); - $trashFilename = Trashbin::getTrashFilename($file->getName(), $file->getMtime()); - return new TrashItem( - $this, - $originalLocation, - $file->getMTime(), - $parentTrashPath . '/' . ($isRoot ? $trashFilename : $file->getName()), - $file, - $user, - $deletedBy, - ); - }, $items); + + $originalLocation = $isRoot ? $file['extraData'] : $parent->getOriginalLocation() . '/' . $file->getName(); + if (!$originalLocation) { + $originalLocation = $file->getName(); + } + /** @psalm-suppress UndefinedInterfaceMethod */ + $deletedBy = $this->userManager->get($file['deletedBy']) ?? $parent?->getDeletedBy(); + $trashFilename = Trashbin::getTrashFilename($file->getName(), $file->getMtime()); + return new TrashItem( + $this, + $originalLocation, + $file->getMTime(), + $parentTrashPath . '/' . ($isRoot ? $trashFilename : $file->getName()), + $file, + $user, + $deletedBy, + ); } #[\Override] public function listTrashRoot(IUser $user): array { $entries = Helper::getTrashFiles('/', $user->getUID()); - return $this->mapTrashItems($entries, $user); + return array_map(fn (FileInfo $fileInfo): ITrashItem => $this->mapTrashItem($fileInfo, $user), $entries); + } + + public function getTrashRootItem(IUser $user, string $name): ?ITrashItem { + $entry = Helper::getTrashFile('/', $user->getUID(), $name); + if ($entry === null) { + return null; + } + return $this->mapTrashItem($entry, $user); } #[\Override] public function listTrashFolder(ITrashItem $folder): array { $user = $folder->getUser(); $entries = Helper::getTrashFiles($folder->getTrashPath(), $user->getUID()); - return $this->mapTrashItems($entries, $user, $folder); + return array_map(fn (FileInfo $fileInfo): ITrashItem => $this->mapTrashItem($fileInfo, $user, $folder), $entries); } #[\Override] @@ -112,7 +114,7 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { } #[\Override] - public function getTrashNodeById(IUser $user, int $fileId) { + public function getTrashNodeById(IUser $user, int $fileId): ?Node { try { $userFolder = $this->rootFolder->getUserFolder($user->getUID()); $trash = $userFolder->getParent()->get('files_trashbin/files'); diff --git a/apps/files_trashbin/lib/Trash/TrashManager.php b/apps/files_trashbin/lib/Trash/TrashManager.php index 3706a6a2e8033..cf0f34cff6d69 100644 --- a/apps/files_trashbin/lib/Trash/TrashManager.php +++ b/apps/files_trashbin/lib/Trash/TrashManager.php @@ -39,6 +39,26 @@ public function listTrashRoot(IUser $user): array { return $items; } + #[\Override] + public function getTrashRootItem(IUser $user, string $name): ?ITrashItem { + foreach ($this->getBackends() as $backend) { + if (method_exists($backend, 'getTrashRootItem')) { + $item = $backend->getTrashRootItem($user, $name); + if ($item !== null) { + return $item; + } + } else { + $items = $backend->listTrashRoot($user); + foreach ($items as $item) { + if ($item->getName() === $name) { + return $item; + } + } + } + } + return null; + } + private function getBackendForItem(ITrashItem $item) { return $item->getTrashBackend(); } diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 33204e7699da1..8f7a3f3f7c213 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -1877,12 +1877,18 @@ + + + + + + @@ -1915,14 +1921,6 @@ - - - - - - - -