feat(trashbin): Add deleted by propertiespull/45000/head
@@ -26,6 +26,7 @@ return array( | |||
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => $baseDir . '/../lib/Listeners/LoadAdditionalScripts.php', | |||
'OCA\\Files_Trashbin\\Listeners\\SyncLivePhotosListener' => $baseDir . '/../lib/Listeners/SyncLivePhotosListener.php', | |||
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => $baseDir . '/../lib/Migration/Version1010Date20200630192639.php', | |||
'OCA\\Files_Trashbin\\Migration\\Version1020Date20240403003535' => $baseDir . '/../lib/Migration/Version1020Date20240403003535.php', | |||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => $baseDir . '/../lib/Sabre/AbstractTrash.php', | |||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => $baseDir . '/../lib/Sabre/AbstractTrashFile.php', | |||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFolder' => $baseDir . '/../lib/Sabre/AbstractTrashFolder.php', |
@@ -41,6 +41,7 @@ class ComposerStaticInitFiles_Trashbin | |||
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => __DIR__ . '/..' . '/../lib/Listeners/LoadAdditionalScripts.php', | |||
'OCA\\Files_Trashbin\\Listeners\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listeners/SyncLivePhotosListener.php', | |||
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => __DIR__ . '/..' . '/../lib/Migration/Version1010Date20200630192639.php', | |||
'OCA\\Files_Trashbin\\Migration\\Version1020Date20240403003535' => __DIR__ . '/..' . '/../lib/Migration/Version1020Date20240403003535.php', | |||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrash.php', | |||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrashFile.php', | |||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFolder' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrashFolder.php', |
@@ -60,7 +60,7 @@ class Helper { | |||
$absoluteDir = $view->getAbsolutePath($dir); | |||
$internalPath = $mount->getInternalPath($absoluteDir); | |||
$originalLocations = \OCA\Files_Trashbin\Trashbin::getLocations($user); | |||
$extraData = \OCA\Files_Trashbin\Trashbin::getExtraData($user); | |||
$dirContent = $storage->getCache()->getFolderContents($mount->getInternalPath($view->getAbsolutePath($dir))); | |||
foreach ($dirContent as $entry) { | |||
$entryName = $entry->getName(); | |||
@@ -76,8 +76,8 @@ class Helper { | |||
} | |||
$originalPath = ''; | |||
$originalName = substr($entryName, 0, -strlen($timestamp) - 2); | |||
if (isset($originalLocations[$originalName][$timestamp])) { | |||
$originalPath = $originalLocations[$originalName][$timestamp]; | |||
if (isset($extraData[$originalName][$timestamp]['location'])) { | |||
$originalPath = $extraData[$originalName][$timestamp]['location']; | |||
if (substr($originalPath, -1) === '/') { | |||
$originalPath = substr($originalPath, 0, -1); | |||
} | |||
@@ -101,6 +101,7 @@ class Helper { | |||
$i['extraData'] = $originalName; | |||
} | |||
} | |||
$i['deletedBy'] = $extraData[$originalName][$timestamp]['deletedBy'] ?? null; | |||
$result[] = new FileInfo($absoluteDir . '/' . $i['name'], $storage, $internalPath . '/' . $i['name'], $i, $mount); | |||
} | |||
@@ -0,0 +1,56 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright 2024 Christopher Ng <chrng8@gmail.com> | |||
* | |||
* @author Christopher Ng <chrng8@gmail.com> | |||
* | |||
* @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\Files_Trashbin\Migration; | |||
use Closure; | |||
use OCP\DB\ISchemaWrapper; | |||
use OCP\DB\Types; | |||
use OCP\Migration\IOutput; | |||
use OCP\Migration\SimpleMigrationStep; | |||
class Version1020Date20240403003535 extends SimpleMigrationStep { | |||
/** | |||
* @param Closure(): ISchemaWrapper $schemaClosure | |||
*/ | |||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { | |||
/** @var ISchemaWrapper $schema */ | |||
$schema = $schemaClosure(); | |||
if (!$schema->hasTable('files_trash')) { | |||
return null; | |||
} | |||
$table = $schema->getTable('files_trash'); | |||
$table->addColumn('deleted_by', Types::STRING, [ | |||
'notnull' => false, | |||
'length' => 64, | |||
]); | |||
return $schema; | |||
} | |||
} |
@@ -28,6 +28,7 @@ namespace OCA\Files_Trashbin\Sabre; | |||
use OCA\Files_Trashbin\Trash\ITrashItem; | |||
use OCA\Files_Trashbin\Trash\ITrashManager; | |||
use OCP\Files\FileInfo; | |||
use OCP\IUser; | |||
abstract class AbstractTrash implements ITrash { | |||
/** @var ITrashItem */ | |||
@@ -89,6 +90,10 @@ abstract class AbstractTrash implements ITrash { | |||
return $this->data->getTitle(); | |||
} | |||
public function getDeletedBy(): ?IUser { | |||
return $this->data->getDeletedBy(); | |||
} | |||
public function delete() { | |||
$this->trashManager->removeItem($this->data); | |||
} |
@@ -27,6 +27,7 @@ declare(strict_types=1); | |||
namespace OCA\Files_Trashbin\Sabre; | |||
use OCP\Files\FileInfo; | |||
use OCP\IUser; | |||
interface ITrash { | |||
public function restore(): bool; | |||
@@ -39,6 +40,8 @@ interface ITrash { | |||
public function getDeletionTime(): int; | |||
public function getDeletedBy(): ?IUser; | |||
public function getSize(): int|float; | |||
public function getFileId(): int; |
@@ -41,6 +41,8 @@ class TrashbinPlugin extends ServerPlugin { | |||
public const TRASHBIN_ORIGINAL_LOCATION = '{http://nextcloud.org/ns}trashbin-original-location'; | |||
public const TRASHBIN_DELETION_TIME = '{http://nextcloud.org/ns}trashbin-deletion-time'; | |||
public const TRASHBIN_TITLE = '{http://nextcloud.org/ns}trashbin-title'; | |||
public const TRASHBIN_DELETED_BY_ID = '{http://nextcloud.org/ns}trashbin-deleted-by-id'; | |||
public const TRASHBIN_DELETED_BY_DISPLAY_NAME = '{http://nextcloud.org/ns}trashbin-deleted-by-display-name'; | |||
/** @var Server */ | |||
private $server; | |||
@@ -83,6 +85,14 @@ class TrashbinPlugin extends ServerPlugin { | |||
return $node->getDeletionTime(); | |||
}); | |||
$propFind->handle(self::TRASHBIN_DELETED_BY_ID, function () use ($node) { | |||
return $node->getDeletedBy()?->getUID(); | |||
}); | |||
$propFind->handle(self::TRASHBIN_DELETED_BY_DISPLAY_NAME, function () use ($node) { | |||
return $node->getDeletedBy()?->getDisplayName(); | |||
}); | |||
$propFind->handle(FilesPlugin::SIZE_PROPERTYNAME, function () use ($node) { | |||
return $node->getSize(); | |||
}); |
@@ -77,5 +77,10 @@ interface ITrashItem extends FileInfo { | |||
*/ | |||
public function getUser(): IUser; | |||
/** | |||
* @since 30.0.0 | |||
*/ | |||
public function getDeletedBy(): ?IUser; | |||
public function getTitle(): string; | |||
} |
@@ -33,16 +33,16 @@ use OCP\Files\IRootFolder; | |||
use OCP\Files\NotFoundException; | |||
use OCP\Files\Storage\IStorage; | |||
use OCP\IUser; | |||
use OCP\IUserManager; | |||
class LegacyTrashBackend implements ITrashBackend { | |||
/** @var array */ | |||
private $deletedFiles = []; | |||
/** @var IRootFolder */ | |||
private $rootFolder; | |||
public function __construct(IRootFolder $rootFolder) { | |||
$this->rootFolder = $rootFolder; | |||
public function __construct( | |||
private IRootFolder $rootFolder, | |||
private IUserManager $userManager, | |||
) { | |||
} | |||
/** | |||
@@ -59,6 +59,8 @@ class LegacyTrashBackend implements ITrashBackend { | |||
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, | |||
@@ -66,7 +68,8 @@ class LegacyTrashBackend implements ITrashBackend { | |||
$file->getMTime(), | |||
$parentTrashPath . '/' . ($isRoot ? $trashFilename : $file->getName()), | |||
$file, | |||
$user | |||
$user, | |||
$deletedBy, | |||
); | |||
}, $items); | |||
} |
@@ -27,33 +27,16 @@ use OCP\Files\FileInfo; | |||
use OCP\IUser; | |||
class TrashItem implements ITrashItem { | |||
/** @var ITrashBackend */ | |||
private $backend; | |||
/** @var string */ | |||
private $orignalLocation; | |||
/** @var int */ | |||
private $deletedTime; | |||
/** @var string */ | |||
private $trashPath; | |||
/** @var FileInfo */ | |||
private $fileInfo; | |||
/** @var IUser */ | |||
private $user; | |||
public function __construct( | |||
ITrashBackend $backend, | |||
string $originalLocation, | |||
int $deletedTime, | |||
string $trashPath, | |||
FileInfo $fileInfo, | |||
IUser $user | |||
private ITrashBackend $backend, | |||
private string $originalLocation, | |||
private int $deletedTime, | |||
private string $trashPath, | |||
private FileInfo $fileInfo, | |||
private IUser $user, | |||
private ?IUser $deletedBy, | |||
) { | |||
$this->backend = $backend; | |||
$this->orignalLocation = $originalLocation; | |||
$this->deletedTime = $deletedTime; | |||
$this->trashPath = $trashPath; | |||
$this->fileInfo = $fileInfo; | |||
$this->user = $user; | |||
} | |||
public function getTrashBackend(): ITrashBackend { | |||
@@ -61,7 +44,7 @@ class TrashItem implements ITrashItem { | |||
} | |||
public function getOriginalLocation(): string { | |||
return $this->orignalLocation; | |||
return $this->originalLocation; | |||
} | |||
public function getDeletedTime(): int { | |||
@@ -192,6 +175,10 @@ class TrashItem implements ITrashItem { | |||
return $this->fileInfo->getParentId(); | |||
} | |||
public function getDeletedBy(): ?IUser { | |||
return $this->deletedBy; | |||
} | |||
/** | |||
* @inheritDoc | |||
* @return array<string, int|string|bool|float|string[]|int[]> |
@@ -126,24 +126,23 @@ class Trashbin { | |||
} | |||
/** | |||
* get original location of files for user | |||
* get original location and deleted by of files for user | |||
* | |||
* @param string $user | |||
* @return array (filename => array (timestamp => original location)) | |||
* @return array<string, array<string, array{location: string, deletedBy: string}>> | |||
*/ | |||
public static function getLocations($user) { | |||
public static function getExtraData($user) { | |||
$query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); | |||
$query->select('id', 'timestamp', 'location') | |||
$query->select('id', 'timestamp', 'location', 'deleted_by') | |||
->from('files_trash') | |||
->where($query->expr()->eq('user', $query->createNamedParameter($user))); | |||
$result = $query->executeQuery(); | |||
$array = []; | |||
while ($row = $result->fetch()) { | |||
if (isset($array[$row['id']])) { | |||
$array[$row['id']][$row['timestamp']] = $row['location']; | |||
} else { | |||
$array[$row['id']] = [$row['timestamp'] => $row['location']]; | |||
} | |||
$array[$row['id']][$row['timestamp']] = [ | |||
'location' => (string)$row['location'], | |||
'deletedBy' => (string)$row['deleted_by'], | |||
]; | |||
} | |||
$result->closeCursor(); | |||
return $array; | |||
@@ -228,7 +227,8 @@ class Trashbin { | |||
->setValue('id', $query->createNamedParameter($targetFilename)) | |||
->setValue('timestamp', $query->createNamedParameter($timestamp)) | |||
->setValue('location', $query->createNamedParameter($targetLocation)) | |||
->setValue('user', $query->createNamedParameter($user)); | |||
->setValue('user', $query->createNamedParameter($user)) | |||
->setValue('deleted_by', $query->createNamedParameter($user)); | |||
$result = $query->executeStatement(); | |||
if (!$result) { | |||
\OC::$server->get(LoggerInterface::class)->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']); | |||
@@ -358,7 +358,8 @@ class Trashbin { | |||
->setValue('id', $query->createNamedParameter($filename)) | |||
->setValue('timestamp', $query->createNamedParameter($timestamp)) | |||
->setValue('location', $query->createNamedParameter($location)) | |||
->setValue('user', $query->createNamedParameter($owner)); | |||
->setValue('user', $query->createNamedParameter($owner)) | |||
->setValue('deleted_by', $query->createNamedParameter($user)); | |||
$result = $query->executeStatement(); | |||
if (!$result) { | |||
\OC::$server->get(LoggerInterface::class)->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']); |
@@ -96,7 +96,15 @@ class TrashbinMigrator implements IMigrator, ISizeEstimationMigrator { | |||
} | |||
$output->writeln("Exporting trashbin files…"); | |||
$exportDestination->copyFolder($trashbinFolder, static::PATH_FILES_FOLDER); | |||
$originalLocations = \OCA\Files_Trashbin\Trashbin::getLocations($uid); | |||
$originalLocations = []; | |||
// TODO Export all extra data and bump migrator to v2 | |||
foreach (\OCA\Files_Trashbin\Trashbin::getExtraData($uid) as $filename => $extraData) { | |||
$locationData = []; | |||
foreach ($extraData as $timestamp => ['location' => $location]) { | |||
$locationData[$timestamp] = $location; | |||
} | |||
$originalLocations[$filename] = $locationData; | |||
} | |||
$exportDestination->addFileContents(static::PATH_LOCATIONS_FILE, json_encode($originalLocations)); | |||
} catch (NotFoundException $e) { | |||
$output->writeln("No trashbin to export…"); |