Skip to content
Merged
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
5 changes: 5 additions & 0 deletions changelog/unreleased/39136
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Add console command to move a user's home folder

occ user:move-home <user_id> <new_location>

https://github.com/owncloud/core/pull/39136
137 changes: 137 additions & 0 deletions core/Command/User/MoveHome.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php
/**
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2021, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace OC\Core\Command\User;

use InvalidArgumentException;
use OC\User\AccountMapper;
use OCP\IUser;
use OCP\IUserManager;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class MoveHome extends Command {

/**
* @var IUserManager
*/
private $userManager;

public function __construct(
IUserManager $userManager
) {
parent::__construct();
$this->userManager = $userManager;
}

protected function configure() {
$this->setName('user:move-home')
->setDescription('Move a user\'s home folder to a new location.')
->addArgument('user_id', InputArgument::REQUIRED, 'Id of the user whose home folder is to be moved. ')
->addArgument('new_location', InputArgument::REQUIRED, 'Absolute path to the parent folder of the new location of the home folder.')
;
}

protected function execute(InputInterface $input, OutputInterface $output) {
$user = $this->getUser($input);
$userId = $user->getUID();
$oldHome = $user->getHome();

$newLocation = $this->getNewLocationForUser($input, $user);
$output->writeln("Move $userId from $oldHome to $newLocation");

# disable user
$output->writeln("Disabling user ....");
$user->setEnabled(false);

# copy files
$output->writeln("Syncing files from $oldHome/ to $newLocation ...");
$this->rsync($oldHome, $newLocation);

# set new home folder
$output->writeln("Updating user home path ....");
$user->setHome($newLocation);

# enable user
$output->writeln("Enabling user ....");
$user->setEnabled(true);

# notify admin about necessary LDAP change
$output->writeln('');
$output->writeln("Files for user $userId have been moved to $newLocation");
$output->writeln("In case you are using LDAP Home Folder please update the property in your LDAP directory for this user");
$output->writeln('');

$output->writeln('');
$output->writeln("The old home folder can now be deleted (rm -rf $oldHome)");
$output->writeln('');

return 0;
}

protected function getUser(InputInterface $input): IUser {
$userId = $input->getArgument('user_id');
$user = $this->userManager->get($userId);
if ($user === null) {
throw new InvalidArgumentException('User is not known.');
}

return $user;
}

protected function getNewLocationForUser(InputInterface $input, IUser $user): string {
$newLocation = $input->getArgument('new_location');
if (!is_dir($newLocation)) {
throw new InvalidArgumentException('New root location has to exist');
}

$oldHome = $user->getHome();
if (!is_dir($oldHome)) {
throw new InvalidArgumentException("Current user home $oldHome does not exist. Not mounted? Non local storage?");
}
$userFolder = basename($oldHome);
$newLocation .= "/$userFolder";

mkdir($newLocation);
if ($this->is_dir_empty($newLocation) !== true) {
throw new InvalidArgumentException("New user folder $newLocation is either not readable or not empty");
}

return realpath($newLocation);
}

private function is_dir_empty($dir): ?bool {
if (!is_readable($dir)) {
return null;
}
return (\count(scandir($dir)) === 2);
}

private function rsync(string $oldHome, string $newLocation): void {
exec("rsync -aAX $oldHome/ $newLocation", $output, $return_var);
if ($return_var !== 0) {
throw new RuntimeException("Copying files failed: \n $output");
}
}
}
1 change: 1 addition & 0 deletions core/register_command.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
));
$application->add(new OC\Core\Command\User\Setting(\OC::$server->getUserManager(), \OC::$server->getConfig(), \OC::$server->getDatabaseConnection()));
$application->add(new OC\Core\Command\User\Modify(\OC::$server->getUserManager(), \OC::$server->getMailer()));
$application->add(new OC\Core\Command\User\MoveHome(\OC::$server->getUserManager()));
$application->add(new OC\Core\Command\User\SyncBackend(\OC::$server->getAccountMapper(), \OC::$server->getConfig(), \OC::$server->getUserManager(), \OC::$server->getLogger()));

$application->add(new OC\Core\Command\Group\Add(\OC::$server->getGroupManager()));
Expand Down
4 changes: 4 additions & 0 deletions lib/private/User/DeletedUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ public function getHome() {
throw new \Exception("Not Implemented", 1);
}

public function setHome(string $newLocation) {
throw new \Exception("Not Implemented", 1);
}

public function getBackendClassName() {
throw new \Exception("Not Implemented", 1);
}
Expand Down
3 changes: 3 additions & 0 deletions lib/private/User/RemoteUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ public function getHome() {
return '';
}

public function setHome(string $newLocation) {
}

/**
* @inheritdoc
*/
Expand Down
4 changes: 4 additions & 0 deletions lib/private/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ public function getHome() {
return $this->account->getHome();
}

public function setHome(string $newLocation) {
$this->account->setHome($newLocation);
}

/**
* Get the name of the backend class the user is connected with
*
Expand Down
8 changes: 8 additions & 0 deletions lib/public/IUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ public function setPassword($password, $recoveryPassword = null);
*/
public function getHome();

/**
* set the users home folder to mount
*
* @return void
* @since 10.9.0
*/
public function setHome(string $newLocation);

/**
* Get the name of the backend class the user is connected with
*
Expand Down
95 changes: 95 additions & 0 deletions tests/Core/Command/User/MoveHomeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
/**
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2021, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace Tests\Core\Command\User;

use OC\Core\Command\User\MoveHome;
use OC\Files\ObjectStore\ObjectStoreStorage;
use OCP\IUser;
use Symfony\Component\Console\Tester\CommandTester;
use Test\TestCase;

/**
* Class MoveHomeTest
*
* @group DB
* @package Tests\Core\Command\User
*/
class MoveHomeTest extends TestCase {
/**
* @var CommandTester
*/
private $commandTester;
/**
* @var string
*/
private $newLocation;
/**
* @var bool|IUser
*/
private $user;

protected function setUp(): void {
parent::setUp();

$command = new MoveHome(\OC::$server->getUserManager());
$this->commandTester = new CommandTester($command);

# create a new location in the data folder
$newLocation = \OC::$server->getSystemConfig()->getValue('datadirectory');
$this->newLocation = uniqid($newLocation . '/');
mkdir($this->newLocation);

# test user
$this->user = \OC::$server->getUserManager()->createUser('test-user', 'test-user');
}

protected function tearDown(): void {
$this->user->delete();
parent::tearDown();

rmdir($this->newLocation);
}

public function test(): void {
\OC::$server->getUserSession()->login('test-user', 'test-user');
$file = \OC::$server->getUserFolder($this->user->getUID())->newFile('hello.txt');
$file->putContent('1234567890');

if ($file->getStorage()->instanceOfStorage(ObjectStoreStorage::class)) {
$this->markTestSkipped('user:move-home is only implemented for posix file systems');
}

$exitCode = $this->commandTester->execute([
'user_id' => 'test-user',
'new_location' => $this->newLocation
]);

# assert command output
self::assertEquals(0, $exitCode, $this->commandTester->getDisplay());

# assert that the new location holds the file
self::assertFileExists("{$this->newLocation}/test-user/files/hello.txt");

# assert the user has a new home set
self::assertEquals("{$this->newLocation}/test-user", $this->user->getHome());
}
}