mirror of https://github.com/sualko/cloud_bbb
feat: add optional avatar cache
parent
b7ad4367e2
commit
dd87506f22
34
README.md
34
README.md
|
@ -76,6 +76,40 @@ Key | Description
|
||||||
`api.url` | URL to your BBB server. Should start with `https://`
|
`api.url` | URL to your BBB server. Should start with `https://`
|
||||||
`api.secret` | Secret of your BBB server
|
`api.secret` | Secret of your BBB server
|
||||||
`app.shortener` | Value of your shortener service. Should start with `https://` and contain `{token}`.
|
`app.shortener` | Value of your shortener service. Should start with `https://` and contain `{token}`.
|
||||||
|
`avatar.path` | Absolute path to an optional avatar cache directory.
|
||||||
|
`avatar.url` | URL which serves `avatar.path` to be used as avatar cache.
|
||||||
|
|
||||||
|
### Avatar cache (v2.2+)
|
||||||
|
The generation of avatars puts a high load on your Nextcloud instance, since the
|
||||||
|
number of requests increases squarely to the number of participants in a room.
|
||||||
|
To mitigate this situation, this app provides an optional avatar file cache. To
|
||||||
|
activate the cache `avatar.path` and `avatar.url` have to be configured.
|
||||||
|
`avatar.path` must provide an absolute path (e.g. `/srv/bbb-avatar-cache/`) to a
|
||||||
|
directory which is writable by the PHP user. `avatar.url` must contain the url
|
||||||
|
which serves all files from `avatar.path`. To bypass browser connection limits
|
||||||
|
we recommend to setup a dedicated host.
|
||||||
|
|
||||||
|
Example Apache configuration for a dedicated host with `avatar.path = /srv/bbb-avatar-cache/`
|
||||||
|
and `avatar.url = https://avatar-cache.your-nextcloud.com/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName avatar-cache.your-nextcloud.com
|
||||||
|
|
||||||
|
Header always set Strict-Transport-Security "max-age=15768000;"
|
||||||
|
|
||||||
|
DocumentRoot /srv/bbb-avatar-cache
|
||||||
|
<Directory /srv/bbb-avatar-cache>
|
||||||
|
Options -FollowSymLinks -Indexes
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
SSLEngine On
|
||||||
|
# SSL config...
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
For additional security, we recommend to disable directory listing, symlinks and
|
||||||
|
any language interpreter such as php for the cache directory.
|
||||||
|
|
||||||
|
|
||||||
## :bowtie: User guide
|
## :bowtie: User guide
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\AppInfo\Application;
|
||||||
|
use OCA\BigBlueButton\Db\Room;
|
||||||
|
use OCP\IAvatarManager;
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\Security\ISecureRandom;
|
||||||
|
|
||||||
|
class AvatarRepository {
|
||||||
|
public const CONF_KEY_PATH = 'avatar.path';
|
||||||
|
public const CONF_KEY_URL = 'avatar.url';
|
||||||
|
|
||||||
|
/** @var IAvatarManager */
|
||||||
|
private $avatarManager;
|
||||||
|
|
||||||
|
/** @var ISecureRandom */
|
||||||
|
private $random;
|
||||||
|
|
||||||
|
/** @var IURLGenerator */
|
||||||
|
private $urlGenerator;
|
||||||
|
|
||||||
|
/** @var IConfig */
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
IAvatarManager $avatarManager,
|
||||||
|
IURLGenerator $urlGenerator,
|
||||||
|
ISecureRandom $random,
|
||||||
|
IConfig $config) {
|
||||||
|
$this->avatarManager = $avatarManager;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
|
$this->random = $random;
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvatarUrl(Room $room, string $userId): string {
|
||||||
|
if (!$this->isAvatarCacheConfigured()) {
|
||||||
|
return $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $userId, 'size' => 32]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomDirName = $room->uid . '-' . $this->generateUrlSafeRandom(16);
|
||||||
|
$roomDirPath = $this->getRootPath() . $roomDirName . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
if (!file_exists($roomDirPath)) {
|
||||||
|
if (!mkdir($roomDirPath, 0755, true)) {
|
||||||
|
throw new \RuntimeException('Can not create room directory: ' . $roomDirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($roomDirPath . 'index.html', '');
|
||||||
|
|
||||||
|
if (!file_exists($this->getRootPath() . 'index.html')) {
|
||||||
|
file_put_contents($this->getRootPath() . 'index.html', 'Avatar cache is working.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$avatar = $this->avatarManager->getAvatar($userId);
|
||||||
|
$file = $avatar->getFile(32);
|
||||||
|
|
||||||
|
$avatarFileName = $this->generateUrlSafeRandom(16) . '-' . $file->getName();
|
||||||
|
$avatarPath = $roomDirPath . $avatarFileName;
|
||||||
|
|
||||||
|
if (!file_put_contents($avatarPath, $file->getContent())) {
|
||||||
|
throw new \RuntimeException('Could not write avatar file: ' . $avatarPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
chmod($avatarPath, 0644);
|
||||||
|
|
||||||
|
return $this->getBaseUrl() . $roomDirName . '/' . $avatarFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearRoom(string $roomUid): int {
|
||||||
|
if (!$this->isAvatarCacheConfigured() || empty($roomUid)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileCounter = 0;
|
||||||
|
|
||||||
|
foreach (glob($this->getRootPath() . $roomUid . '-*' . DIRECTORY_SEPARATOR) as $dir) {
|
||||||
|
foreach (scandir($dir) as $file) {
|
||||||
|
if (in_array($file, ['.', '..'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink($dir . $file);
|
||||||
|
|
||||||
|
if ($file !== 'index.html') {
|
||||||
|
$fileCounter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir($dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fileCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearAllRooms(): array {
|
||||||
|
if (!$this->isAvatarCacheConfigured()) {
|
||||||
|
return [
|
||||||
|
'rooms' => 0,
|
||||||
|
'files' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $this->getRootPath();
|
||||||
|
$roomCounter = 0;
|
||||||
|
$fileCounter = 0;
|
||||||
|
|
||||||
|
foreach (scandir($path) as $dir) {
|
||||||
|
if (in_array($dir, ['.', '..']) || $dir === 'index.html') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomUid = \explode("-", $dir)[0];
|
||||||
|
|
||||||
|
$fileCounter += $this->clearRoom($roomUid);
|
||||||
|
|
||||||
|
$roomCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'rooms' => $roomCounter,
|
||||||
|
'files' => $fileCounter,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateUrlSafeRandom(int $length): string {
|
||||||
|
// from Nextcloud 23 ISecureRandom::CHAR_ALPHANUMERIC can be used as shortcut
|
||||||
|
return $this->random->generate($length, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAvatarCacheConfigured(): bool {
|
||||||
|
return !empty($this->getRootPath()) && !empty($this->getBaseUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRootPath(): string {
|
||||||
|
$path = $this->config->getAppValue(Application::ID, self::CONF_KEY_PATH);
|
||||||
|
|
||||||
|
if (empty($path)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($path, -\strlen($path)) === DIRECTORY_SEPARATOR ? $path : ($path . DIRECTORY_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getBaseUrl(): string {
|
||||||
|
$url = $this->config->getAppValue(Application::ID, self::CONF_KEY_URL);
|
||||||
|
|
||||||
|
if (empty($url)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^https?:\/\//', $url) === 0) {
|
||||||
|
$url = $this->urlGenerator->getAbsoluteURL($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/\/$/', $url) === 0) {
|
||||||
|
$url .= '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ use BigBlueButton\Parameters\GetRecordingsParameters;
|
||||||
use BigBlueButton\Parameters\IsMeetingRunningParameters;
|
use BigBlueButton\Parameters\IsMeetingRunningParameters;
|
||||||
use BigBlueButton\Parameters\JoinMeetingParameters;
|
use BigBlueButton\Parameters\JoinMeetingParameters;
|
||||||
use OCA\BigBlueButton\AppInfo\Application;
|
use OCA\BigBlueButton\AppInfo\Application;
|
||||||
|
use OCA\BigBlueButton\AvatarRepository;
|
||||||
use OCA\BigBlueButton\Crypto;
|
use OCA\BigBlueButton\Crypto;
|
||||||
use OCA\BigBlueButton\Db\Room;
|
use OCA\BigBlueButton\Db\Room;
|
||||||
use OCA\BigBlueButton\Event\MeetingStartedEvent;
|
use OCA\BigBlueButton\Event\MeetingStartedEvent;
|
||||||
|
@ -50,6 +51,9 @@ class API {
|
||||||
/** @var IAppManager */
|
/** @var IAppManager */
|
||||||
private $appManager;
|
private $appManager;
|
||||||
|
|
||||||
|
/** @var AvatarRepository */
|
||||||
|
private $avatarRepository;
|
||||||
|
|
||||||
/** @var IRequest */
|
/** @var IRequest */
|
||||||
private $request;
|
private $request;
|
||||||
|
|
||||||
|
@ -62,6 +66,7 @@ class API {
|
||||||
UrlHelper $urlHelper,
|
UrlHelper $urlHelper,
|
||||||
Defaults $defaults,
|
Defaults $defaults,
|
||||||
IAppManager $appManager,
|
IAppManager $appManager,
|
||||||
|
AvatarRepository $avatarRepository,
|
||||||
IRequest $request
|
IRequest $request
|
||||||
) {
|
) {
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
@ -72,6 +77,7 @@ class API {
|
||||||
$this->urlHelper = $urlHelper;
|
$this->urlHelper = $urlHelper;
|
||||||
$this->defaults = $defaults;
|
$this->defaults = $defaults;
|
||||||
$this->appManager = $appManager;
|
$this->appManager = $appManager;
|
||||||
|
$this->avatarRepository = $avatarRepository;
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,8 +127,10 @@ class API {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($uid) {
|
if ($uid) {
|
||||||
$joinMeetingParams->setUserId($uid);
|
$avatarUrl = $this->avatarRepository->getAvatarUrl($room, $uid);
|
||||||
$joinMeetingParams->setAvatarURL($this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $uid, 'size' => 32]));
|
|
||||||
|
$joinMeetingParams->setUserID($uid);
|
||||||
|
$joinMeetingParams->setAvatarURL($avatarUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->getServer()->getJoinMeetingURL($joinMeetingParams);
|
return $this->getServer()->getJoinMeetingURL($joinMeetingParams);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace OCA\BigBlueButton\Controller;
|
namespace OCA\BigBlueButton\Controller;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\AvatarRepository;
|
||||||
use OCA\BigBlueButton\Db\Room;
|
use OCA\BigBlueButton\Db\Room;
|
||||||
use OCA\BigBlueButton\Event\MeetingEndedEvent;
|
use OCA\BigBlueButton\Event\MeetingEndedEvent;
|
||||||
use OCA\BigBlueButton\Event\RecordingReadyEvent;
|
use OCA\BigBlueButton\Event\RecordingReadyEvent;
|
||||||
|
@ -20,6 +21,9 @@ class HookController extends Controller {
|
||||||
/** @var RoomService */
|
/** @var RoomService */
|
||||||
private $service;
|
private $service;
|
||||||
|
|
||||||
|
/** @var AvatarRepository */
|
||||||
|
private $avatarRepository;
|
||||||
|
|
||||||
/** @var IEventDispatcher */
|
/** @var IEventDispatcher */
|
||||||
private $eventDispatcher;
|
private $eventDispatcher;
|
||||||
|
|
||||||
|
@ -27,11 +31,13 @@ class HookController extends Controller {
|
||||||
string $appName,
|
string $appName,
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
RoomService $service,
|
RoomService $service,
|
||||||
|
AvatarRepository $avatarRepository,
|
||||||
IEventDispatcher $eventDispatcher
|
IEventDispatcher $eventDispatcher
|
||||||
) {
|
) {
|
||||||
parent::__construct($appName, $request);
|
parent::__construct($appName, $request);
|
||||||
|
|
||||||
$this->service = $service;
|
$this->service = $service;
|
||||||
|
$this->avatarRepository = $avatarRepository;
|
||||||
$this->eventDispatcher = $eventDispatcher;
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,8 +61,11 @@ class HookController extends Controller {
|
||||||
*/
|
*/
|
||||||
public function meetingEnded($recordingmarks = false): void {
|
public function meetingEnded($recordingmarks = false): void {
|
||||||
$recordingmarks = \boolval($recordingmarks);
|
$recordingmarks = \boolval($recordingmarks);
|
||||||
|
$room = $this->getRoom();
|
||||||
|
|
||||||
$this->eventDispatcher->dispatch(MeetingEndedEvent::class, new MeetingEndedEvent($this->getRoom(), $recordingmarks));
|
$this->avatarRepository->clearRoom($room->uid);
|
||||||
|
|
||||||
|
$this->eventDispatcher->dispatch(MeetingEndedEvent::class, new MeetingEndedEvent($room, $recordingmarks));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue