2021-01-18 12:30:19 -05:00
< ? php
declare ( strict_types = 1 );
2021-06-04 15:52:51 -04:00
2021-01-18 12:30:19 -05:00
/**
2024-05-28 10:42:42 -04:00
* SPDX - FileCopyrightText : 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2021-01-18 12:30:19 -05:00
*/
namespace OCA\Files\Command ;
2025-12-04 05:43:12 -05:00
use OCP\DB\QueryBuilder\IQueryBuilder ;
2021-01-18 12:30:19 -05:00
use OCP\IDBConnection ;
use Symfony\Component\Console\Command\Command ;
use Symfony\Component\Console\Input\InputInterface ;
2025-12-04 05:43:12 -05:00
use Symfony\Component\Console\Input\InputOption ;
2021-01-18 12:30:19 -05:00
use Symfony\Component\Console\Output\OutputInterface ;
class RepairTree extends Command {
public const CHUNK_SIZE = 200 ;
2023-07-04 14:43:32 -04:00
public function __construct (
protected IDBConnection $connection ,
) {
2021-01-18 12:30:19 -05:00
parent :: __construct ();
}
2023-07-04 14:43:32 -04:00
protected function configure () : void {
2021-01-18 12:30:19 -05:00
$this
-> setName ( 'files:repair-tree' )
2025-10-06 10:25:45 -04:00
-> setDescription ( 'Try and repair malformed filesystem tree structures (may be necessary to run multiple times for nested malformations)' )
2025-12-04 05:43:12 -05:00
-> addOption ( 'dry-run' )
-> addOption ( 'storage-id' , 's' , InputOption :: VALUE_OPTIONAL , 'If set, only repair files within the given storage numeric ID' , null )
-> addOption ( 'path' , 'p' , InputOption :: VALUE_OPTIONAL , 'If set, only repair files within the given path' , null );
2021-01-18 12:30:19 -05:00
}
public function execute ( InputInterface $input , OutputInterface $output ) : int {
2025-12-04 05:43:12 -05:00
$rows = $this -> findBrokenTreeBits (
$input -> getOption ( 'storage-id' ),
$input -> getOption ( 'path' ),
);
2021-01-18 12:30:19 -05:00
$fix = ! $input -> getOption ( 'dry-run' );
$output -> writeln ( 'Found ' . count ( $rows ) . ' file entries with an invalid path' );
if ( $fix ) {
$this -> connection -> beginTransaction ();
}
$query = $this -> connection -> getQueryBuilder ();
$query -> update ( 'filecache' )
-> set ( 'path' , $query -> createParameter ( 'path' ))
-> set ( 'path_hash' , $query -> func () -> md5 ( $query -> createParameter ( 'path' )))
2021-01-22 10:05:14 -05:00
-> set ( 'storage' , $query -> createParameter ( 'storage' ))
2021-01-18 12:30:19 -05:00
-> where ( $query -> expr () -> eq ( 'fileid' , $query -> createParameter ( 'fileid' )));
foreach ( $rows as $row ) {
2022-07-28 07:11:38 -04:00
$output -> writeln ( " Path of file { $row [ 'fileid' ] } is { $row [ 'path' ] } but should be { $row [ 'parent_path' ] } / { $row [ 'name' ] } based on its parent " , OutputInterface :: VERBOSITY_VERBOSE );
2021-01-18 12:30:19 -05:00
if ( $fix ) {
2021-01-27 12:08:10 -05:00
$fileId = $this -> getFileId (( int ) $row [ 'parent_storage' ], $row [ 'parent_path' ] . '/' . $row [ 'name' ]);
2021-01-26 14:24:30 -05:00
if ( $fileId > 0 ) {
$output -> writeln ( " Cache entry has already be recreated with id $fileId , deleting instead " );
2021-01-27 12:08:10 -05:00
$this -> deleteById (( int ) $row [ 'fileid' ]);
2021-01-26 14:24:30 -05:00
} else {
$query -> setParameters ([
'fileid' => $row [ 'fileid' ],
'path' => $row [ 'parent_path' ] . '/' . $row [ 'name' ],
'storage' => $row [ 'parent_storage' ],
]);
2025-09-12 10:58:19 -04:00
$query -> executeStatement ();
2021-01-26 14:24:30 -05:00
}
2021-01-18 12:30:19 -05:00
}
}
if ( $fix ) {
$this -> connection -> commit ();
}
2023-07-04 14:43:32 -04:00
return self :: SUCCESS ;
2021-01-18 12:30:19 -05:00
}
2021-01-26 14:24:30 -05:00
private function getFileId ( int $storage , string $path ) {
$query = $this -> connection -> getQueryBuilder ();
$query -> select ( 'fileid' )
-> from ( 'filecache' )
-> where ( $query -> expr () -> eq ( 'storage' , $query -> createNamedParameter ( $storage )))
-> andWhere ( $query -> expr () -> eq ( 'path_hash' , $query -> createNamedParameter ( md5 ( $path ))));
2025-11-17 06:20:11 -05:00
return $query -> executeQuery () -> fetchOne ();
2021-01-26 14:24:30 -05:00
}
2023-07-04 14:43:32 -04:00
private function deleteById ( int $fileId ) : void {
2021-01-26 14:24:30 -05:00
$query = $this -> connection -> getQueryBuilder ();
$query -> delete ( 'filecache' )
-> where ( $query -> expr () -> eq ( 'fileid' , $query -> createNamedParameter ( $fileId )));
2025-09-12 10:58:19 -04:00
$query -> executeStatement ();
2021-01-26 14:24:30 -05:00
}
2025-12-04 05:43:12 -05:00
private function findBrokenTreeBits ( ? string $storageId , ? string $path ) : array {
2021-01-18 12:30:19 -05:00
$query = $this -> connection -> getQueryBuilder ();
$query -> select ( 'f.fileid' , 'f.path' , 'f.parent' , 'f.name' )
-> selectAlias ( 'p.path' , 'parent_path' )
2021-01-22 10:05:14 -05:00
-> selectAlias ( 'p.storage' , 'parent_storage' )
2021-01-18 12:30:19 -05:00
-> from ( 'filecache' , 'f' )
-> innerJoin ( 'f' , 'filecache' , 'p' , $query -> expr () -> eq ( 'f.parent' , 'p.fileid' ))
-> where ( $query -> expr () -> orX (
$query -> expr () -> andX (
$query -> expr () -> neq ( 'p.path_hash' , $query -> createNamedParameter ( md5 ( '' ))),
$query -> expr () -> neq ( 'f.path' , $query -> func () -> concat ( 'p.path' , $query -> func () -> concat ( $query -> createNamedParameter ( '/' ), 'f.name' )))
),
$query -> expr () -> andX (
$query -> expr () -> eq ( 'p.path_hash' , $query -> createNamedParameter ( md5 ( '' ))),
$query -> expr () -> neq ( 'f.path' , 'f.name' )
),
$query -> expr () -> neq ( 'f.storage' , 'p.storage' )
));
2025-12-04 05:43:12 -05:00
if ( $storageId !== null ) {
$query -> andWhere ( $query -> expr () -> eq ( 'f.storage' , $query -> createNamedParameter ( $storageId , IQueryBuilder :: PARAM_INT )));
}
if ( $path !== null ) {
$query -> andWhere ( $query -> expr () -> like ( 'f.path' , $query -> createNamedParameter ( $path . '%' )));
}
2025-11-17 06:20:11 -05:00
return $query -> executeQuery () -> fetchAllAssociative ();
2021-01-18 12:30:19 -05:00
}
}