2021-09-22 18:00:31 -04:00
< ? php
2025-06-30 09:04:05 -04:00
2021-09-22 18:00:31 -04:00
/**
2024-06-02 09:26:54 -04:00
* SPDX - FileCopyrightText : 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - only
2021-09-22 18:00:31 -04:00
*/
namespace OCA\Files_Trashbin\Command ;
2021-11-29 11:37:10 -05:00
use OC\Core\Command\Base ;
2025-09-04 08:07:14 -04:00
use OC\Files\SetupManager ;
2023-08-06 08:12:24 -04:00
use OCA\Files_Trashbin\Trash\ITrashManager ;
2024-10-10 06:40:31 -04:00
use OCA\Files_Trashbin\Trash\TrashItem ;
2021-09-22 18:00:31 -04:00
use OCP\Files\IRootFolder ;
use OCP\IDBConnection ;
2021-11-29 11:37:10 -05:00
use OCP\IL10N ;
2021-09-22 18:00:31 -04:00
use OCP\IUserBackend ;
use OCP\IUserManager ;
2025-09-04 08:07:14 -04:00
use OCP\IUserSession ;
2021-11-29 11:37:10 -05:00
use OCP\L10N\IFactory ;
2021-09-22 18:00:31 -04:00
use Symfony\Component\Console\Exception\InvalidOptionException ;
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\OutputInterface ;
2021-11-29 11:37:10 -05:00
class RestoreAllFiles extends Base {
2021-09-22 18:00:31 -04:00
2023-08-06 08:12:24 -04:00
private const SCOPE_ALL = 0 ;
private const SCOPE_USER = 1 ;
private const SCOPE_GROUPFOLDERS = 2 ;
2023-08-14 06:38:28 -04:00
private static array $SCOPE_MAP = [
2023-08-13 12:34:06 -04:00
'user' => self :: SCOPE_USER ,
'groupfolders' => self :: SCOPE_GROUPFOLDERS ,
'all' => self :: SCOPE_ALL
];
2021-11-29 11:37:10 -05:00
/** @var IL10N */
protected $l10n ;
2021-09-22 18:00:31 -04:00
/**
* @ param IRootFolder $rootFolder
* @ param IUserManager $userManager
* @ param IDBConnection $dbConnection
2023-08-06 08:12:24 -04:00
* @ param ITrashManager $trashManager
* @ param IFactory $l10nFactory
2021-09-22 18:00:31 -04:00
*/
2024-10-18 06:04:22 -04:00
public function __construct (
protected IRootFolder $rootFolder ,
protected IUserManager $userManager ,
protected IDBConnection $dbConnection ,
protected ITrashManager $trashManager ,
2025-09-04 08:07:14 -04:00
protected SetupManager $setupManager ,
protected IUserSession $userSession ,
2024-10-18 06:04:22 -04:00
IFactory $l10nFactory ,
) {
2021-09-22 18:00:31 -04:00
parent :: __construct ();
2021-11-29 11:37:10 -05:00
$this -> l10n = $l10nFactory -> get ( 'files_trashbin' );
2021-09-22 18:00:31 -04:00
}
2021-11-29 11:37:10 -05:00
protected function configure () : void {
parent :: configure ();
2021-09-22 18:00:31 -04:00
$this
-> setName ( 'trashbin:restore' )
2023-08-06 08:12:24 -04:00
-> setDescription ( 'Restore all deleted files according to the given filters' )
2021-09-22 18:00:31 -04:00
-> addArgument (
'user_id' ,
InputArgument :: OPTIONAL | InputArgument :: IS_ARRAY ,
'restore all deleted files of the given user(s)'
)
-> addOption (
'all-users' ,
null ,
InputOption :: VALUE_NONE ,
'run action on all users'
2023-08-06 08:12:24 -04:00
)
-> addOption (
'scope' ,
's' ,
InputOption :: VALUE_OPTIONAL ,
'Restore files from the given scope. Possible values are "user", "groupfolders" or "all"' ,
'user'
)
-> addOption (
2023-08-14 06:38:28 -04:00
'since' ,
null ,
2023-08-06 08:12:24 -04:00
InputOption :: VALUE_OPTIONAL ,
2024-07-30 14:33:56 -04:00
'Only restore files deleted after the given date and time, see https://www.php.net/manual/en/function.strtotime.php for more information on supported formats'
2023-08-06 08:12:24 -04:00
)
-> addOption (
2023-08-14 06:38:28 -04:00
'until' ,
null ,
2023-08-06 08:12:24 -04:00
InputOption :: VALUE_OPTIONAL ,
2024-07-30 14:33:56 -04:00
'Only restore files deleted before the given date and time, see https://www.php.net/manual/en/function.strtotime.php for more information on supported formats'
2023-08-06 08:12:24 -04:00
)
-> addOption (
'dry-run' ,
'd' ,
InputOption :: VALUE_NONE ,
'Only show which files would be restored but do not perform any action'
2021-09-22 18:00:31 -04:00
);
}
protected function execute ( InputInterface $input , OutputInterface $output ) : int {
2021-11-29 11:37:10 -05:00
/** @var string[] $users */
2021-09-22 18:00:31 -04:00
$users = $input -> getArgument ( 'user_id' );
2023-08-14 06:38:28 -04:00
if (( ! empty ( $users )) && ( $input -> getOption ( 'all-users' ))) {
2021-09-22 18:00:31 -04:00
throw new InvalidOptionException ( 'Either specify a user_id or --all-users' );
2023-08-06 08:12:24 -04:00
}
2023-08-14 06:38:28 -04:00
[ $scope , $since , $until , $dryRun ] = $this -> parseArgs ( $input );
2023-08-06 08:12:24 -04:00
if ( ! empty ( $users )) {
2021-09-22 18:00:31 -04:00
foreach ( $users as $user ) {
2023-08-12 09:06:34 -04:00
$output -> writeln ( " Restoring deleted files for user <info> $user </info> " );
2023-08-14 06:38:28 -04:00
$this -> restoreDeletedFiles ( $user , $scope , $since , $until , $dryRun , $output );
2021-09-22 18:00:31 -04:00
}
} elseif ( $input -> getOption ( 'all-users' )) {
$output -> writeln ( 'Restoring deleted files for all users' );
foreach ( $this -> userManager -> getBackends () as $backend ) {
$name = get_class ( $backend );
if ( $backend instanceof IUserBackend ) {
$name = $backend -> getBackendName ();
}
$output -> writeln ( " Restoring deleted files for users on backend <info> $name </info> " );
$limit = 500 ;
$offset = 0 ;
do {
$users = $backend -> getUsers ( '' , $limit , $offset );
foreach ( $users as $user ) {
$output -> writeln ( " <info> $user </info> " );
2023-08-14 06:38:28 -04:00
$this -> restoreDeletedFiles ( $user , $scope , $since , $until , $dryRun , $output );
2021-09-22 18:00:31 -04:00
}
$offset += $limit ;
} while ( count ( $users ) >= $limit );
}
} else {
throw new InvalidOptionException ( 'Either specify a user_id or --all-users' );
}
return 0 ;
}
/**
2023-08-14 06:38:28 -04:00
* Restore deleted files for the given user according to the given filters
2021-09-22 18:00:31 -04:00
*/
2023-08-14 06:38:28 -04:00
protected function restoreDeletedFiles ( string $uid , int $scope , ? int $since , ? int $until , bool $dryRun , OutputInterface $output ) : void {
2023-08-06 08:12:24 -04:00
$user = $this -> userManager -> get ( $uid );
2025-09-04 08:07:14 -04:00
if ( ! $user ) {
2023-08-12 09:06:34 -04:00
$output -> writeln ( " <error>Unknown user $uid </error> " );
return ;
}
2025-09-04 08:07:14 -04:00
$this -> setupManager -> tearDown ();
$this -> setupManager -> setupForUser ( $user );
$this -> userSession -> setUser ( $user );
2023-08-06 08:12:24 -04:00
$userTrashItems = $this -> filterTrashItems (
$this -> trashManager -> listTrashRoot ( $user ),
$scope ,
2023-08-14 06:38:28 -04:00
$since ,
$until ,
2023-08-06 08:12:24 -04:00
$output );
2022-05-19 20:43:12 -04:00
2023-08-06 08:12:24 -04:00
$trashCount = count ( $userTrashItems );
2021-11-29 11:37:10 -05:00
if ( $trashCount == 0 ) {
2023-08-14 06:38:28 -04:00
$output -> writeln ( 'User has no deleted files in the trashbin matching the given filters' );
2021-11-29 11:37:10 -05:00
return ;
}
2023-08-06 08:12:24 -04:00
$prepMsg = $dryRun ? 'Would restore' : 'Preparing to restore' ;
$output -> writeln ( " $prepMsg <info> $trashCount </info> files... " );
2021-11-29 11:37:10 -05:00
$count = 0 ;
2023-08-06 08:12:24 -04:00
foreach ( $userTrashItems as $trashItem ) {
$filename = $trashItem -> getName ();
$humanTime = $this -> l10n -> l ( 'datetime' , $trashItem -> getDeletedTime ());
// We use getTitle() here instead of getOriginalLocation() because
// for groupfolders this contains the groupfolder name itself as prefix
// which makes it more human readable
$location = $trashItem -> getTitle ();
if ( $dryRun ) {
$output -> writeln ( " Would restore <info> $filename </info> originally deleted at <info> $humanTime </info> to <info>/ $location </info> " );
continue ;
2021-11-29 11:37:10 -05:00
}
2023-08-06 08:12:24 -04:00
$output -> write ( " File <info> $filename </info> originally deleted at <info> $humanTime </info> restoring to <info>/ $location </info>: " );
try {
$trashItem -> getTrashBackend () -> restoreItem ( $trashItem );
} catch ( \Throwable $e ) {
2023-08-14 06:38:28 -04:00
$output -> writeln ( ' <error>Failed: ' . $e -> getMessage () . '</error>' );
$output -> writeln ( ' <error>' . $e -> getTraceAsString () . '</error>' , OutputInterface :: VERBOSITY_VERY_VERBOSE );
2023-08-06 08:12:24 -04:00
continue ;
2021-11-29 11:37:10 -05:00
}
2023-08-06 08:12:24 -04:00
2023-08-14 06:38:28 -04:00
$count ++ ;
2023-08-06 08:12:24 -04:00
$output -> writeln ( ' <info>success</info>' );
}
2023-08-12 09:06:34 -04:00
2023-08-06 08:12:24 -04:00
if ( ! $dryRun ) {
$output -> writeln ( " Successfully restored <info> $count </info> out of <info> $trashCount </info> files. " );
}
}
protected function parseArgs ( InputInterface $input ) : array {
2023-08-14 06:38:28 -04:00
$since = $this -> parseTimestamp ( $input -> getOption ( 'since' ));
$until = $this -> parseTimestamp ( $input -> getOption ( 'until' ));
2023-08-06 08:12:24 -04:00
2023-08-14 06:38:28 -04:00
if ( $since !== null && $until !== null && $since > $until ) {
throw new InvalidOptionException ( 'since must be before until' );
2021-11-29 11:37:10 -05:00
}
2021-09-22 18:00:31 -04:00
2023-08-06 08:12:24 -04:00
return [
$this -> parseScope ( $input -> getOption ( 'scope' )),
2023-08-14 06:38:28 -04:00
$since ,
$until ,
2023-08-06 08:12:24 -04:00
$input -> getOption ( 'dry-run' )
];
}
protected function parseScope ( string $scope ) : int {
2023-08-13 12:34:06 -04:00
if ( isset ( self :: $SCOPE_MAP [ $scope ])) {
return self :: $SCOPE_MAP [ $scope ];
2023-08-06 08:12:24 -04:00
}
2023-08-13 12:34:06 -04:00
throw new InvalidOptionException ( " Invalid scope ' $scope ' " );
2023-08-06 08:12:24 -04:00
}
protected function parseTimestamp ( ? string $timestamp ) : ? int {
if ( $timestamp === null ) {
return null ;
}
$timestamp = strtotime ( $timestamp );
if ( $timestamp === false ) {
throw new InvalidOptionException ( " Invalid timestamp ' $timestamp ' " );
}
return $timestamp ;
}
2023-08-14 06:38:28 -04:00
protected function filterTrashItems ( array $trashItems , int $scope , ? int $since , ? int $until , OutputInterface $output ) : array {
2023-08-06 08:12:24 -04:00
$filteredTrashItems = [];
foreach ( $trashItems as $trashItem ) {
2023-08-12 09:06:34 -04:00
$trashItemClass = get_class ( $trashItem );
// Check scope with exact class name for locally deleted files
2024-10-10 06:40:31 -04:00
if ( $scope === self :: SCOPE_USER && $trashItemClass !== TrashItem :: class ) {
2023-08-06 08:12:24 -04:00
$output -> writeln ( 'Skipping <info>' . $trashItem -> getName () . '</info> because it is not a user trash item' , OutputInterface :: VERBOSITY_VERBOSE );
continue ;
}
2023-08-12 09:06:34 -04:00
2023-08-14 06:38:28 -04:00
/**
* Check scope for groupfolders by string because the groupfolders app might not be installed .
* That 's why PSALM doesn' t know the class GroupTrashItem .
* @ psalm - suppress RedundantCondition
*/
2023-08-12 09:06:34 -04:00
if ( $scope === self :: SCOPE_GROUPFOLDERS && $trashItemClass !== 'OCA\GroupFolders\Trash\GroupTrashItem' ) {
2023-08-06 08:12:24 -04:00
$output -> writeln ( 'Skipping <info>' . $trashItem -> getName () . '</info> because it is not a groupfolders trash item' , OutputInterface :: VERBOSITY_VERBOSE );
continue ;
}
// Check left timestamp boundary
2023-08-14 06:38:28 -04:00
if ( $since !== null && $trashItem -> getDeletedTime () <= $since ) {
2023-08-16 04:40:21 -04:00
$output -> writeln ( 'Skipping <info>' . $trashItem -> getName () . " </info> because it was deleted before the 'since' timestamp " , OutputInterface :: VERBOSITY_VERBOSE );
2023-08-06 08:12:24 -04:00
continue ;
}
// Check right timestamp boundary
2023-08-14 06:38:28 -04:00
if ( $until !== null && $trashItem -> getDeletedTime () >= $until ) {
2023-08-16 04:40:21 -04:00
$output -> writeln ( 'Skipping <info>' . $trashItem -> getName () . " </info> because it was deleted after the 'until' timestamp " , OutputInterface :: VERBOSITY_VERBOSE );
2023-08-06 08:12:24 -04:00
continue ;
}
2023-08-12 09:06:34 -04:00
2023-08-06 08:12:24 -04:00
$filteredTrashItems [] = $trashItem ;
}
return $filteredTrashItems ;
2021-09-22 18:00:31 -04:00
}
}