307 lines
9.6 KiB
PHP
307 lines
9.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\GroupFolders\Mount;
|
|
|
|
use OC\Files\Cache\Cache;
|
|
use OC\Files\ObjectStore\ObjectStoreStorage;
|
|
use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
|
|
use OC\Files\Storage\Local;
|
|
use OC\Files\Storage\Wrapper\Jail;
|
|
use OCA\GroupFolders\ACL\ACLManagerFactory;
|
|
use OCA\GroupFolders\ACL\ACLStorageWrapper;
|
|
use OCA\GroupFolders\AppInfo\Application;
|
|
use OCA\GroupFolders\Folder\FolderDefinition;
|
|
use OCP\Files\Folder;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\Files\NotFoundException;
|
|
use OCP\Files\Storage\IStorage;
|
|
use OCP\IAppConfig;
|
|
use OCP\IConfig;
|
|
use OCP\IUser;
|
|
|
|
class FolderStorageManager {
|
|
private readonly bool $enableEncryption;
|
|
private array $cachedFolders = [];
|
|
|
|
public function __construct(
|
|
private readonly IRootFolder $rootFolder,
|
|
private readonly IAppConfig $appConfig,
|
|
private readonly ACLManagerFactory $aclManagerFactory,
|
|
private readonly IConfig $config,
|
|
private readonly PrimaryObjectStoreConfig $primaryObjectStoreConfig,
|
|
) {
|
|
$this->enableEncryption = $this->appConfig->getValueString(Application::APP_ID, 'enable_encryption', 'false') === 'true';
|
|
}
|
|
|
|
/**
|
|
* @return array{storage_id: int, root_id: int}
|
|
*/
|
|
public function initRootAndStorageForFolder(int $folderId, bool $separateStorage, array $options): array {
|
|
$storage = $this->getBaseStorageForFolder($folderId, $separateStorage, init: true, options: $options);
|
|
$cache = $storage->getCache();
|
|
$id = $cache->getId('');
|
|
if ($id === -1) {
|
|
$storage->getScanner()->scan('');
|
|
$id = $cache->getId('');
|
|
if ($id === -1) {
|
|
throw new \Exception('Group folder root is not in cache even after scanning for folder ' . $folderId);
|
|
}
|
|
}
|
|
return [
|
|
'storage_id' => $cache->getNumericStorageId(),
|
|
'root_id' => $id,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param 'files'|'trash'|'versions'|'' $type
|
|
*/
|
|
public function getBaseStorageForFolder(
|
|
int $folderId,
|
|
bool $separateStorage,
|
|
?FolderDefinition $folder = null,
|
|
?IUser $user = null,
|
|
bool $inShare = false,
|
|
string $type = 'files',
|
|
bool $init = false,
|
|
array $options = [],
|
|
): IStorage {
|
|
if ($separateStorage) {
|
|
return $this->getBaseStorageForFolderSeparate($folderId, $folder, $user, $inShare, $type, $init, $options);
|
|
} else {
|
|
return $this->getBaseStorageForFolderRootJail($folderId, $folder, $user, $inShare, $type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param 'files'|'trash'|'versions'|'' $type
|
|
*/
|
|
private function getBaseStorageForFolderSeparate(
|
|
int $folderId,
|
|
?FolderDefinition $folder = null,
|
|
?IUser $user = null,
|
|
bool $inShare = false,
|
|
string $type = 'files',
|
|
bool $init = false,
|
|
array $options = [],
|
|
): IStorage {
|
|
if ($this->primaryObjectStoreConfig->hasObjectStore()) {
|
|
$storage = $this->getBaseStorageForFolderSeparateStorageObject($folderId, $init, $options['bucket'] ?? null);
|
|
} else {
|
|
$storage = $this->getBaseStorageForFolderSeparateStorageLocal($folderId, $init);
|
|
}
|
|
|
|
if ($folder?->acl && $user) {
|
|
$aclManager = $this->aclManagerFactory->getACLManager($user);
|
|
$storage = new ACLStorageWrapper([
|
|
'storage' => $storage,
|
|
'acl_manager' => $aclManager,
|
|
'in_share' => $inShare,
|
|
'storage_id' => $storage->getCache()->getNumericStorageId(),
|
|
]);
|
|
}
|
|
|
|
if ($this->enableEncryption) {
|
|
return new GroupFolderEncryptionJail([
|
|
'storage' => $storage,
|
|
'root' => $type,
|
|
]);
|
|
} else {
|
|
return new Jail([
|
|
'storage' => $storage,
|
|
'root' => $type,
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function getBaseStorageForFolderSeparateStorageLocal(
|
|
int $folderId,
|
|
bool $init = false,
|
|
): IStorage {
|
|
$dataDirectory = $this->config->getSystemValue('datadirectory');
|
|
$rootPath = $dataDirectory . '/__groupfolders/' . $folderId;
|
|
if ($init) {
|
|
$result = mkdir($rootPath . '/files', recursive: true);
|
|
$result = $result && mkdir($rootPath . '/trash');
|
|
$result = $result && mkdir($rootPath . '/versions');
|
|
|
|
if (!$result) {
|
|
throw new \Exception('Failed to create base directories for group folder ' . $folderId);
|
|
}
|
|
}
|
|
|
|
$storage = new Local([
|
|
'datadir' => $rootPath,
|
|
]);
|
|
|
|
if ($init) {
|
|
$storage->getScanner()->scan('');
|
|
}
|
|
return $storage;
|
|
}
|
|
|
|
private function getBaseStorageForFolderSeparateStorageObject(
|
|
int $folderId,
|
|
bool $init = false,
|
|
?string $bucket = null,
|
|
): IStorage {
|
|
$objectStoreConfig = $this->primaryObjectStoreConfig->getObjectStoreConfiguration($this->getObjectStorageKey($folderId));
|
|
|
|
$bucketKey = 'object_store_bucket_' . $folderId;
|
|
$savedBucket = $this->appConfig->getValueString(Application::APP_ID, $bucketKey);
|
|
if ($savedBucket) {
|
|
$objectStoreConfig['arguments']['bucket'] = $savedBucket;
|
|
} elseif ($objectStoreConfig['arguments']['multibucket'] || $bucket) {
|
|
$objectStoreConfig['arguments']['bucket'] = $this->getObjectStorageBucket($folderId, $objectStoreConfig, $bucket);
|
|
}
|
|
|
|
$objectStore = $this->primaryObjectStoreConfig->buildObjectStore($objectStoreConfig);
|
|
$arguments = array_merge($objectStoreConfig['arguments'], [
|
|
'objectstore' => $objectStore,
|
|
]);
|
|
$arguments['storageid'] = 'object::groupfolder:' . $folderId . '.' . $objectStore->getStorageId();
|
|
|
|
$storage = new ObjectStoreStorage($arguments);
|
|
|
|
if ($init) {
|
|
$result = $storage->mkdir('files');
|
|
$result = $result && $storage->mkdir('trash');
|
|
$result = $result && $storage->mkdir('versions');
|
|
|
|
if (!$result) {
|
|
throw new \Exception('Failed to create base directories for group folder ' . $folderId);
|
|
}
|
|
}
|
|
return $storage;
|
|
}
|
|
|
|
/**
|
|
* @param 'files'|'trash'|'versions'|'' $type
|
|
*/
|
|
private function getBaseStorageForFolderRootJail(
|
|
int $folderId,
|
|
?FolderDefinition $folder = null,
|
|
?IUser $user = null,
|
|
bool $inShare = false,
|
|
string $type = 'files',
|
|
): IStorage {
|
|
if (isset($this->cachedFolders['root'])) {
|
|
$parentFolder = $this->cachedFolders['root'];
|
|
} else {
|
|
try {
|
|
/** @var Folder $parentFolder */
|
|
$parentFolder = $this->rootFolder->get('__groupfolders');
|
|
} catch (NotFoundException) {
|
|
$parentFolder = $this->rootFolder->newFolder('__groupfolders');
|
|
}
|
|
$this->cachedFolders['root'] = $parentFolder;
|
|
}
|
|
|
|
if ($type !== 'files') {
|
|
if (isset($this->cachedFolders[$type])) {
|
|
$parentFolder = $this->cachedFolders[$type];
|
|
} else {
|
|
try {
|
|
/** @var Folder $parentFolder */
|
|
$parentFolder = $parentFolder->get($type);
|
|
} catch (NotFoundException) {
|
|
$parentFolder = $parentFolder->newFolder($type);
|
|
}
|
|
$this->cachedFolders[$type] = $parentFolder;
|
|
}
|
|
}
|
|
|
|
try {
|
|
/** @var Folder $storageFolder */
|
|
$storageFolder = $parentFolder->get((string)$folderId);
|
|
} catch (NotFoundException) {
|
|
$storageFolder = $parentFolder->newFolder((string)$folderId);
|
|
}
|
|
$rootStorage = $storageFolder->getStorage();
|
|
$rootPath = $storageFolder->getInternalPath();
|
|
|
|
// apply acl before jail, trash doesn't get the ACL wrapper as it does its own ACL filtering
|
|
if ($folder && $folder->acl && $user && $type !== 'trash') {
|
|
$aclManager = $this->aclManagerFactory->getACLManager($user);
|
|
$rootStorage = new ACLStorageWrapper([
|
|
'storage' => $rootStorage,
|
|
'acl_manager' => $aclManager,
|
|
'in_share' => $inShare,
|
|
'storage_id' => $rootStorage->getCache()->getNumericStorageId(),
|
|
]);
|
|
}
|
|
|
|
if ($this->enableEncryption) {
|
|
return new GroupFolderEncryptionJail([
|
|
'storage' => $rootStorage,
|
|
'root' => $rootPath,
|
|
]);
|
|
} else {
|
|
return new Jail([
|
|
'storage' => $rootStorage,
|
|
'root' => $rootPath,
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function deleteStoragesForFolder(FolderDefinition $folder): void {
|
|
foreach (['files', 'trash', 'versions', ''] as $type) {
|
|
$storage = $this->getBaseStorageForFolder($folder->id, $folder->useSeparateStorage(), $folder, null, false, $type);
|
|
/** @var Cache $cache */
|
|
$cache = $storage->getCache();
|
|
$cache->clear();
|
|
$storage->rmdir('');
|
|
}
|
|
}
|
|
|
|
private function getObjectStorageKey(int $folderId): string {
|
|
$configs = $this->primaryObjectStoreConfig->getObjectStoreConfigs();
|
|
if ($this->primaryObjectStoreConfig->hasMultipleObjectStorages()) {
|
|
$configKey = 'object_store_key_' . $folderId;
|
|
$storageConfigKey = $this->appConfig->getValueString(Application::APP_ID, $configKey);
|
|
if (!$storageConfigKey) {
|
|
$storageConfigKey = isset($configs['groupfolders']) ? $this->primaryObjectStoreConfig->resolveAlias('groupfolders') : $this->primaryObjectStoreConfig->resolveAlias('default');
|
|
$this->appConfig->setValueString(Application::APP_ID, $configKey, $storageConfigKey);
|
|
}
|
|
return $storageConfigKey;
|
|
} else {
|
|
return 'default';
|
|
}
|
|
}
|
|
|
|
private function getObjectStorageBucket(int $folderId, array $objectStoreConfig, ?string $overwriteBucket = null): string {
|
|
$bucketKey = 'object_store_bucket_' . $folderId;
|
|
$bucket = $this->appConfig->getValueString(Application::APP_ID, $bucketKey);
|
|
if (!$bucket) {
|
|
if ($overwriteBucket) {
|
|
$bucket = $overwriteBucket;
|
|
} else {
|
|
$bucketBase = $objectStoreConfig['arguments']['bucket'] ?? '';
|
|
$bucket = $bucketBase . $this->calculateBucketNum((string)$folderId, $objectStoreConfig);
|
|
}
|
|
|
|
$this->appConfig->setValueString(Application::APP_ID, $bucketKey, $bucket);
|
|
}
|
|
return $bucket;
|
|
}
|
|
|
|
// logic taken from OC\Files\ObjectStore\Mapper which we can't use because it requires an IUser
|
|
private function calculateBucketNum(string $key, array $objectStoreConfig): string {
|
|
$numBuckets = $objectStoreConfig['arguments']['num_buckets'] ?? 64;
|
|
|
|
// Get the bucket config and shift if provided.
|
|
// Allow us to prevent writing in old filled buckets
|
|
$minBucket = (int)($objectStoreConfig['arguments']['min_bucket'] ?? 0);
|
|
|
|
$hash = md5($key);
|
|
$num = hexdec(substr($hash, 0, 4));
|
|
return (string)(($num % ($numBuckets - $minBucket)) + $minBucket);
|
|
}
|
|
}
|