feat(trash): Store folder hierarchy of deleted files in trash

When deleting a nested file, this will create a copy of the folder hierarchy in the trash folder and then copy the file inside the copied folder.

This will prevent too many files to appears in the trash bin.

Signed-off-by: Carl Schwan <carl.schwan@nextclound.com>
This commit is contained in:
Carl Schwan 2025-08-05 15:34:14 +02:00
parent 2211390ca5
commit 44a9c39712
6 changed files with 48 additions and 11 deletions

View file

@ -45,17 +45,30 @@ class Helper {
foreach ($dirContent as $entry) {
$entryName = $entry->getName();
$name = $entryName;
if ($dir === '' || $dir === '/') {
$pathparts = pathinfo($entryName);
$timestamp = substr($pathparts['extension'], 1);
$name = $pathparts['filename'];
$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);
$timestamp = '';
for ($i = 0, $count = count($parts); $i < $count && $timestamp === ''; $i++) {
$timestamp = substr(pathinfo($parts[0], PATHINFO_EXTENSION), 1);
}
if ($timestamp === '') {
$pathParts = pathinfo($entryName);
$timestamp = substr($pathParts['extension'], 1);
$name = $pathParts['filename'];
}
}
$originalPath = '';
$originalName = substr($entryName, 0, -strlen($timestamp) - 2);
$originalName = $timestamp === '' ? $entryName : substr($entryName, 0, -strlen($timestamp) - 2);
$hasFileTrashEntry = array_key_exists($originalName, $extraData);
if (isset($extraData[$originalName][$timestamp]['location'])) {
$originalPath = $extraData[$originalName][$timestamp]['location'];
if (substr($originalPath, -1) === '/') {
@ -73,6 +86,7 @@ class Helper {
'etag' => '',
'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE,
'fileid' => $entry->getId(),
'is_trash_root' => !$hasFileTrashEntry,
];
if ($originalPath) {
if ($originalPath !== '.') {

View file

@ -19,6 +19,13 @@ abstract class AbstractTrashFolder extends AbstractTrash implements ICollection,
$entries = $this->trashManager->listTrashFolder($this->data);
$children = array_map(function (ITrashItem $entry) {
if (str_starts_with($entry->getTrashPath(), '/' . $entry->getOriginalLocation())) {
// parent folder is a fake trash folder
if ($entry->getType() === FileInfo::TYPE_FOLDER) {
return new TrashFolder($this->trashManager, $entry);
}
return new TrashFile($this->trashManager, $entry);
}
if ($entry->getType() === FileInfo::TYPE_FOLDER) {
return new TrashFolderFolder($this->trashManager, $entry);
}

View file

@ -12,6 +12,10 @@ use OCA\Files_Trashbin\Trashbin;
class TrashFolder extends AbstractTrashFolder {
public function getName(): string {
return Trashbin::getTrashFilename($this->data->getName(), $this->getDeletionTime());
if ($this->getDeletionTime() === 0) {
return $this->data->getName();
} else {
return Trashbin::getTrashFilename($this->data->getName(), $this->getDeletionTime());
}
}
}

View file

@ -45,12 +45,12 @@ class LegacyTrashBackend implements ITrashBackend {
}
/** @psalm-suppress UndefinedInterfaceMethod */
$deletedBy = $this->userManager->get($file['deletedBy']) ?? $parent?->getDeletedBy();
$trashFilename = Trashbin::getTrashFilename($file->getName(), $file->getMtime());
$trashFilename = $file->getMtime() === 0 ? $file->getName() : Trashbin::getTrashFilename($file->getName(), $file->getMtime());
return new TrashItem(
$this,
$originalLocation,
$file->getMTime(),
$parentTrashPath . '/' . ($isRoot ? $trashFilename : $file->getName()),
$parentTrashPath . '/' . $trashFilename,
$file,
$user,
$deletedBy,

View file

@ -39,7 +39,7 @@ class TrashItem implements ITrashItem {
}
public function isRootItem(): bool {
return substr_count($this->getTrashPath(), '/') === 1;
return substr_count($this->getTrashPath(), '/') === 1 || str_ends_with($this->trashPath, strval($this->deletedTime));
}
public function getUser(): IUser {

View file

@ -264,9 +264,21 @@ class Trashbin implements IEventListener {
$lockingProvider = Server::get(ILockingProvider::class);
// disable proxy to prevent recursive calls
$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
$trashPath = '/files_trashbin/files/' . $location . '/' . static::getTrashFilename($filename, $timestamp);
$gotLock = false;
// Reproduce folder hierarchy of deleted file in trash
$parentDirs = explode('/', $location);
$pathPrefix = '/files_trashbin/files/';
foreach ($parentDirs as $parentDir) {
$pathPrefix .= $parentDir . '/';
if ($ownerView->is_dir($pathPrefix)) {
continue;
}
$ownerView->mkdir($pathPrefix);
}
do {
/** @var ILockingStorage & IStorage $trashStorage */
[$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
@ -279,7 +291,7 @@ class Trashbin implements IEventListener {
$timestamp = $timestamp + 1;
$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
$trashPath = '/files_trashbin/files/' . $location . static::getTrashFilename($filename, $timestamp);
}
} while (!$gotLock);