Commit c06f9dc4 authored by Vincent Petry's avatar Vincent Petry Committed by Thomas Müller
Browse files

Store avatars outside of the home (#26124)

* Store avatars outside of the home

Inside a hidden "metadata-avatars" folder.
Doesn't require home FS setup

* Fix it

* Store avatars in e.g. a/d/admin, using md5 on non alphanumeric characters and refactor tests

* Always do an md5 of the user name

* Update Util.php

* Adjust avatar migration version check

* Fix LazyRoot folder to properly return get() value

* Fix avatar migration with the new folder

* Don't fail move avatar step if user has no home at all

* Fix use statement for IRootFolder
parent 5cc4496f
......@@ -84,15 +84,37 @@ class AvatarManager implements IAvatarManager {
throw new \Exception('user does not exist');
}
/*
* Fix for #22119
* Basically we do not want to copy the skeleton folder
*/
\OC\Files\Filesystem::initMountPoints($userId);
$dir = '/' . $userId;
/** @var Folder $folder */
$folder = $this->rootFolder->get($dir);
$avatarsFolder = $this->getAvatarFolder($userId);
return new Avatar($avatarsFolder, $this->l, $user, $this->logger);
}
private function getFolder(Folder $folder, $path) {
try {
return $folder->get($path);
} catch (NotFoundException $e) {
return $folder->newFolder($path);
}
}
private function buildAvatarPath($userId) {
$avatar = substr_replace(substr_replace(md5($userId), '/', 4, 0), '/', 2, 0);
return explode('/', $avatar);
}
return new Avatar($folder, $this->l, $user, $this->logger);
/**
* Returns the avatar folder for the given user
*
* @param $userId user id
* @return Folder|\OCP\Files\Node
*
* @internal
*/
public function getAvatarFolder($userId) {
$avatarsFolder = $this->getFolder($this->rootFolder, 'avatars');
$parts = $this->buildAvatarPath($userId);
foreach ($parts as $part) {
$avatarsFolder = $this->getFolder($avatarsFolder, $part);
}
return $avatarsFolder;
}
}
......@@ -96,6 +96,9 @@ class Util {
$this->config = $config;
$this->excludedPaths[] = 'files_encryption';
$this->excludedPaths[] = 'avatars';
$this->excludedPaths[] = 'avatar.png';
$this->excludedPaths[] = 'avatar.jpg';
}
/**
......
......@@ -138,7 +138,7 @@ class LazyRoot implements IRootFolder {
* @inheritDoc
*/
public function get($path) {
$this->__call(__FUNCTION__, func_get_args());
return $this->__call(__FUNCTION__, func_get_args());
}
/**
......
......@@ -54,6 +54,7 @@ use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\GenericEvent;
use OC\Repair\MoveAvatarOutsideHome;
class Repair implements IOutput{
/* @var IRepairStep[] */
......@@ -139,6 +140,15 @@ class Repair implements IOutput{
new SharePropagation(\OC::$server->getConfig()),
new RemoveOldShares(\OC::$server->getDatabaseConnection()),
new AvatarPermissions(\OC::$server->getDatabaseConnection()),
new MoveAvatarOutsideHome(
\OC::$server->getConfig(),
\OC::$server->getDatabaseConnection(),
\OC::$server->getUserManager(),
\OC::$server->getAvatarManager(),
\OC::$server->getLazyRootFolder(),
\OC::$server->getL10N('core'),
\OC::$server->getLogger()
),
new RemoveRootShares(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager(), \OC::$server->getLazyRootFolder()),
new RepairUnmergedShares(
\OC::$server->getConfig(),
......
<?php
/**
* @author Vincent Petry <pvince81@owncloud.com>
*
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @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\Repair;
use OCP\Files\IRootFolder;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IUserManager;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use OCP\IUser;
use OC\Avatar;
use OCP\IConfig;
use OCP\IAvatarManager;
use OCP\Files\NotFoundException;
/**
* Move avatars outside of their homes to the new location
*
* @package OC\Repair
*/
class MoveAvatarOutsideHome implements IRepairStep {
/** @var \OCP\IConfig */
protected $config;
/** @var IDBConnection */
private $connection;
/** @var IUserManager */
private $userManager;
/** @var IAvatarManager */
private $avatarManager;
/** @var IRootFolder */
private $rootFolder;
/** @var \OCP\ILogger */
private $logger;
/** @var \OCP\IL10N */
private $l;
/**
* @param IConfig $config config
* @param IDBConnection $connection database connection
* @param IUserManager $userManager user manager
* @param IAvatarManager $avatarManager
* @param IRootFolder $rootFolder
* @param IL10N $l10n
* @param ILogger $logger
*/
public function __construct(
IConfig $config,
IDBConnection $connection,
IUserManager $userManager,
IAvatarManager $avatarManager,
IRootFolder $rootFolder,
IL10N $l10n,
ILogger $logger
) {
$this->config = $config;
$this->connection = $connection;
$this->userManager = $userManager;
$this->avatarManager = $avatarManager;
$this->rootFolder = $rootFolder;
$this->l = $l10n;
$this->logger = $logger;
}
/**
* @return string
*/
public function getName() {
return 'Move user avatars outside the homes to the new location';
}
/**
* Move avatars outside of their homes
*
* @param IOutput $out
* @param IUser $user
*/
private function moveAvatars(IOutput $out, IUser $user) {
$userId = $user->getUID();
\OC\Files\Filesystem::initMountPoints($userId);
// call get instead of getUserFolder to avoid needless skeleton copy
try {
$oldAvatarUserFolder = $this->rootFolder->get('/' . $userId);
$oldAvatar = new Avatar($oldAvatarUserFolder, $this->l, $user, $this->logger);
if ($oldAvatar->exists()) {
$newAvatarsUserFolder = $this->avatarManager->getAvatarFolder($userId);
// get original file
$oldAvatarFile = $oldAvatar->getFile(-1);
$oldAvatarFile->move($newAvatarsUserFolder->getPath() . '/' . $oldAvatarFile->getName());
$oldAvatar->remove();
}
} catch (NotFoundException $e) {
// not all users have a home, ignore
}
\OC_Util::tearDownFS();
}
/**
* Count all the users
*
* @return int
*/
private function countUsers() {
$allCount = $this->userManager->countUsers();
$totalCount = 0;
foreach ($allCount as $backend => $count) {
$totalCount += $count;
}
return $totalCount;
}
/**
* @param IOutput $output
*/
public function run(IOutput $output) {
$ocVersionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0');
if (version_compare($ocVersionFromBeforeUpdate, '9.2.0.2', '<')) {
$function = function(IUser $user) use ($output) {
$this->moveAvatars($output, $user);
$output->advance();
};
$userCount = $this->countUsers();
$output->startProgress($userCount);
$this->userManager->callForAllUsers($function);
$output->finishProgress();
}
}
}
......@@ -22,32 +22,43 @@
namespace Test;
use OC\AvatarManager;
use Test\Traits\UserTrait;
use Test\Traits\MountProviderTrait;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IUser;
use OCP\IUserManager;
/**
* Class AvatarManagerTest
* @group DB
*/
class AvatarManagerTest extends \Test\TestCase {
use UserTrait;
use MountProviderTrait;
class AvatarManagerTest extends TestCase {
/** @var AvatarManager */
/** @var AvatarManager | \PHPUnit_Framework_MockObject_MockObject */
private $avatarManager;
/** @var \OC\Files\Storage\Temporary */
private $storage;
/** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */
private $userManager;
/** @var IRootFolder | \PHPUnit_Framework_MockObject_MockObject */
private $rootFolder;
public function setUp() {
parent::setUp();
$this->createUser('valid-user', 'valid-user');
$this->userManager = $this->createMock(IUserManager::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
$l = $this->createMock(IL10N::class);
$logger = $this->createMock(ILogger::class);
$this->storage = new \OC\Files\Storage\Temporary();
$this->registerMount('valid-user', $this->storage, '/valid-user/');
$this->avatarManager = \OC::$server->getAvatarManager();
$this->avatarManager = $this->getMockBuilder(AvatarManager::class)
->setMethods(['getAvatarFolder'])
->setConstructorArgs([$this->userManager,
$this->rootFolder,
$l,
$logger])
->getMock();
}
/**
......@@ -59,10 +70,31 @@ class AvatarManagerTest extends \Test\TestCase {
}
public function testGetAvatarValidUser() {
$user = $this->createMock(IUser::class);
$this->userManager->expects($this->once())->method('get')->willReturn($user);
$folder = $this->createMock(Folder::class);
$this->avatarManager->expects($this->once())->method('getAvatarFolder')->willReturn($folder);
$avatar = $this->avatarManager->getAvatar('valid-user');
$this->assertInstanceOf('\OCP\IAvatar', $avatar);
$this->assertFalse($this->storage->file_exists('files'));
}
/**
* @dataProvider providesUserIds
*/
public function testPathBuilding($expectedPath, $userId) {
$path = $this->invokePrivate($this->avatarManager, 'buildAvatarPath', [$userId]);
$this->assertEquals($expectedPath, implode('/', $path));
}
public function providesUserIds() {
return [
['21/23/2f297a57a5a743894a0e4a801fc3', 'admin'],
['c4/ca/4238a0b923820dcc509a6f75849b', '1'],
['f9/5b/70fdc3088560732a5ac135644506', '{'],
['d4/1d/8cd98f00b204e9800998ecf8427e', ''],
];
}
}
......@@ -23,7 +23,7 @@
// We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
// when updating major/minor version number.
$OC_Version = array(9, 2, 0, 1);
$OC_Version = array(9, 2, 0, 2);
// The human readable string
$OC_VersionString = '9.2.0 pre alpha';
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment