Skip to content
Open
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
127 changes: 69 additions & 58 deletions apps/files_trashbin/lib/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)) {
Expand All @@ -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 !== '') {
Expand All @@ -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);
}
}
40 changes: 18 additions & 22 deletions apps/files_trashbin/lib/Sabre/TrashRoot.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
}

Expand Down Expand Up @@ -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'),
};
}
}
9 changes: 9 additions & 0 deletions apps/files_trashbin/lib/Trash/ITrashManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
66 changes: 34 additions & 32 deletions apps/files_trashbin/lib/Trash/LegacyTrashBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> */
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]
Expand Down Expand Up @@ -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');
Expand Down
20 changes: 20 additions & 0 deletions apps/files_trashbin/lib/Trash/TrashManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we force all implementations to add this method, this check is not really needed. If the implementation doesn't have the function yet, it will hard fail before this code is even reached.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getTrashRootItem is not yet in the interface so that this is easy to backport. I would add it to the interface and remove the method_exists check as a followup

$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();
}
Expand Down
Loading
Loading