From: Philipp Date: Sat, 23 Oct 2021 10:11:38 +0000 (+0200) Subject: Restructure Storage to new paradigm X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=2ab0d06996410f68cf501e6e2014bf4829b121ae;p=friendica.git Restructure Storage to new paradigm --- diff --git a/doc/AddonStorageBackend.md b/doc/AddonStorageBackend.md index 0b06723765..eaa58cd4d8 100644 --- a/doc/AddonStorageBackend.md +++ b/doc/AddonStorageBackend.md @@ -4,7 +4,7 @@ Friendica Storage Backend Addon development * [Home](help) Storage backends can be added via addons. -A storage backend is implemented as a class, and the plugin register the class to make it avaiable to the system. +A storage backend is implemented as a class, and the plugin register the class to make it available to the system. ## The Storage Backend Class @@ -12,14 +12,14 @@ The class must live in `Friendica\Addon\youraddonname` namespace, where `youradd There are two different interfaces you need to implement. -### `IWritableStorage` +### `ICanWriteToStorage` -The class must implement `Friendica\Model\Storage\IWritableStorage` interface. All method in the interface must be implemented: +The class must implement `Friendica\Core\Storage\Capability\ICanWriteToStorage` interface. All method in the interface must be implemented: ```php -namespace Friendica\Model\Storage\IWritableStorage; +namespace Friendica\Core\Storage\Capability\ICanWriteToStorage; -interface IWritableStorage +interface ICanWriteToStorage { public function get(string $reference); public function put(string $data, string $reference = ''); @@ -33,17 +33,17 @@ interface IWritableStorage - `put(string $data, string $reference)` saves data in `$data` to position `$reference`, or a new position if `$reference` is empty. - `delete(string $reference)` delete data pointed by `$reference` -### `IStorageConfiguration` +### `ICanConfigureStorage` Each storage backend can have options the admin can set in admin page. -To make the options possible, you need to implement the `Friendica\Model\Storage\IStorageConfiguration` interface. +To make the options possible, you need to implement the `Friendica\Core\Storage\Capability\ICanConfigureStorage` interface. All methods in the interface must be implemented: ```php -namespace Friendica\Model\Storage\IStorageConfiguration; +namespace Friendica\Core\Storage\Capability\ICanConfigureStorage; -interface IStorageConfiguration +interface ICanConfigureStorage { public function getOptions(); public function saveOptions(array $data); @@ -108,7 +108,7 @@ When the plugin is uninstalled, registered backends must be unregistered using `DI::facStorage()->unregister(string $class)`. You have to register a new hook in your addon, listening on `storage_instance(App $a, array $data)`. -In case `$data['name']` is your storage class name, you have to instance a new instance of your `Friendica\Model\Storage\IStorage` class. +In case `$data['name']` is your storage class name, you have to instance a new instance of your `Friendica\Core\Storage\Capability\ICanReadFromStorage` class. Set the instance of your class as `$data['storage']` to pass it back to the backend. This is necessary because it isn't always clear, if you need further construction arguments. @@ -124,7 +124,7 @@ Add a new test class which's naming convention is `StorageClassTest`, which exte Override the two necessary instances: ```php -use Friendica\Model\Storage\IWritableStorage; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; abstract class StorageTest { @@ -132,7 +132,7 @@ abstract class StorageTest abstract protected function getInstance(); // Assertion for the option array you return for your new StorageClass - abstract protected function assertOption(IWritableStorage $storage); + abstract protected function assertOption(ICanWriteToStorage $storage); } ``` @@ -156,16 +156,16 @@ If there's a predecessor to this exception (e.g. you caught an exception and are Example: ```php -use Friendica\Model\Storage\IWritableStorage; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; -class ExampleStorage implements IWritableStorage +class ExampleStorage implements ICanWriteToStorage { public function get(string $reference) : string { try { throw new Exception('a real bad exception'); } catch (Exception $exception) { - throw new \Friendica\Model\Storage\StorageException(sprintf('The Example Storage throws an exception for reference %s', $reference), 500, $exception); + throw new \Friendica\Core\Storage\Exception\StorageException(sprintf('The Example Storage throws an exception for reference %s', $reference), 500, $exception); } } } @@ -186,12 +186,12 @@ The file will be `addon/samplestorage/SampleStorageBackend.php`: assertEquals([ 'filename' => [ diff --git a/doc/Addons.md b/doc/Addons.md index debdc89dd4..6b3cd169bf 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -544,7 +544,7 @@ Called when a custom storage is used (e.g. webdav_storage) Hook data: - **name** (input): the name of the used storage backend -- **data['storage']** (output): the storage instance to use (**must** implement `\Friendica\Model\Storage\IWritableStorage`) +- **data['storage']** (output): the storage instance to use (**must** implement `\Friendica\Core\Storage\IWritableStorage`) ### storage_config @@ -552,7 +552,7 @@ Called when the admin of the node wants to configure a custom storage (e.g. webd Hook data: - **name** (input): the name of the used storage backend -- **data['storage_config']** (output): the storage configuration instance to use (**must** implement `\Friendica\Model\Storage\IStorageConfiguration`) +- **data['storage_config']** (output): the storage configuration instance to use (**must** implement `\Friendica\Core\Storage\Capability\IConfigureStorage`) ## Complete list of hook callbacks diff --git a/src/Console/Storage.php b/src/Console/Storage.php index 3377f33ddf..fbe55f34c3 100644 --- a/src/Console/Storage.php +++ b/src/Console/Storage.php @@ -22,9 +22,9 @@ namespace Friendica\Console; use Asika\SimpleConsole\CommandArgsException; -use Friendica\Core\StorageManager; -use Friendica\Model\Storage\ReferenceStorageException; -use Friendica\Model\Storage\StorageException; +use Friendica\Core\Storage\Repository\StorageManager; +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Exception\StorageException; /** * tool to manage storage backend and stored data from CLI @@ -33,7 +33,7 @@ class Storage extends \Asika\SimpleConsole\Console { protected $helpOptions = ['h', 'help', '?']; - /** @var StorageManager */ + /** @var \Friendica\Core\Storage\Repository\StorageManager */ private $storageManager; /** diff --git a/src/Core/Storage/Capability/ICanConfigureStorage.php b/src/Core/Storage/Capability/ICanConfigureStorage.php new file mode 100644 index 0000000000..09d0718cfe --- /dev/null +++ b/src/Core/Storage/Capability/ICanConfigureStorage.php @@ -0,0 +1,78 @@ +. + * + */ + +namespace Friendica\Core\Storage\Capability; + +/** + * The interface to use for configurable storage backends + */ +interface ICanConfigureStorage +{ + /** + * Get info about storage options + * + * @return array + * + * This method return an array with information about storage options + * from which the form presented to the user is build. + * + * The returned array is: + * + * [ + * 'option1name' => [ ..info.. ], + * 'option2name' => [ ..info.. ], + * ... + * ] + * + * An empty array can be returned if backend doesn't have any options + * + * The info array for each option MUST be as follows: + * + * [ + * 'type', // define the field used in form, and the type of data. + * // one of 'checkbox', 'combobox', 'custom', 'datetime', + * // 'input', 'intcheckbox', 'password', 'radio', 'richtext' + * // 'select', 'select_raw', 'textarea' + * + * 'label', // Translatable label of the field + * 'value', // Current value + * 'help text', // Translatable description for the field + * extra data // Optional. Depends on 'type': + * // select: array [ value => label ] of choices + * // intcheckbox: value of input element + * // select_raw: prebuild html string of < option > tags + * ] + * + * See https://github.com/friendica/friendica/wiki/Quick-Template-Guide + */ + public function getOptions(): array; + + /** + * Validate and save options + * + * @param array $data Array [optionname => value] to be saved + * + * @return array Validation errors: [optionname => error message] + * + * Return array must be empty if no error. + */ + public function saveOptions(array $data): array; +} diff --git a/src/Core/Storage/Capability/ICanReadFromStorage.php b/src/Core/Storage/Capability/ICanReadFromStorage.php new file mode 100644 index 0000000000..b6c63751dc --- /dev/null +++ b/src/Core/Storage/Capability/ICanReadFromStorage.php @@ -0,0 +1,57 @@ +. + * + */ + +namespace Friendica\Core\Storage\Capability; + +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Exception\StorageException; + +/** + * Interface for basic storage backends + */ +interface ICanReadFromStorage +{ + /** + * Get data from backend + * + * @param string $reference Data reference + * + * @return string + * + * @throws StorageException in case there's an unexpected error + * @throws ReferenceStorageException in case the reference doesn't exist + */ + public function get(string $reference): string; + + /** + * The name of the backend + * + * @return string + */ + public function __toString(): string; + + /** + * The name of the backend + * + * @return string + */ + public static function getName(): string; +} diff --git a/src/Core/Storage/Capability/ICanWriteToStorage.php b/src/Core/Storage/Capability/ICanWriteToStorage.php new file mode 100644 index 0000000000..fd9d0e14f3 --- /dev/null +++ b/src/Core/Storage/Capability/ICanWriteToStorage.php @@ -0,0 +1,56 @@ +. + * + */ + +namespace Friendica\Core\Storage\Capability; + +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Exception\StorageException; + +/** + * Interface for writable storage backends + * + * Used for storages with CRUD functionality, mainly used for user data (e.g. photos, attachements). + * There's only one active writable storage possible. This type of storage is selectable by the current administrator. + */ +interface ICanWriteToStorage extends ICanReadFromStorage +{ + /** + * Put data in backend as $ref. If $ref is not defined a new reference is created. + * + * @param string $data Data to save + * @param string $reference Data reference. Optional. + * + * @return string Saved data reference + * + * @throws StorageException in case there's an unexpected error + */ + public function put(string $data, string $reference = ""): string; + + /** + * Remove data from backend + * + * @param string $reference Data reference + * + * @throws StorageException in case there's an unexpected error + * @throws ReferenceStorageException in case the reference doesn't exist + */ + public function delete(string $reference); +} diff --git a/src/Core/Storage/Exception/InvalidClassStorageException.php b/src/Core/Storage/Exception/InvalidClassStorageException.php new file mode 100644 index 0000000000..9c1c7bb2b6 --- /dev/null +++ b/src/Core/Storage/Exception/InvalidClassStorageException.php @@ -0,0 +1,29 @@ +. + * + */ + +namespace Friendica\Core\Storage\Exception; + +/** + * Storage Exception in case of invalid storage class + */ +class InvalidClassStorageException extends StorageException +{ +} diff --git a/src/Core/Storage/Exception/ReferenceStorageException.php b/src/Core/Storage/Exception/ReferenceStorageException.php new file mode 100644 index 0000000000..7ef5960746 --- /dev/null +++ b/src/Core/Storage/Exception/ReferenceStorageException.php @@ -0,0 +1,29 @@ +. + * + */ + +namespace Friendica\Core\Storage\Exception; + +/** + * Storage Exception in case of invalid references + */ +class ReferenceStorageException extends StorageException +{ +} diff --git a/src/Core/Storage/Exception/StorageException.php b/src/Core/Storage/Exception/StorageException.php new file mode 100644 index 0000000000..7c7b3d12ab --- /dev/null +++ b/src/Core/Storage/Exception/StorageException.php @@ -0,0 +1,31 @@ +. + * + */ + +namespace Friendica\Core\Storage\Exception; + +use Exception; + +/** + * Storage Exception for unexpected failures + */ +class StorageException extends Exception +{ +} diff --git a/src/Core/Storage/Repository/StorageManager.php b/src/Core/Storage/Repository/StorageManager.php new file mode 100644 index 0000000000..5df6e5ac7d --- /dev/null +++ b/src/Core/Storage/Repository/StorageManager.php @@ -0,0 +1,407 @@ +. + * + */ + +namespace Friendica\Core\Storage\Repository; + +use Exception; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\Hook; +use Friendica\Core\L10n; +use Friendica\Core\Storage\Exception\InvalidClassStorageException; +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Exception\StorageException; +use Friendica\Core\Storage\Capability\ICanReadFromStorage; +use Friendica\Core\Storage\Capability\ICanConfigureStorage; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; +use Friendica\Database\Database; +use Friendica\Core\Storage\Type; +use Friendica\Network\HTTPException\InternalServerErrorException; +use Psr\Log\LoggerInterface; + +/** + * Manage storage backends + * + * Core code uses this class to get and set current storage backend class. + * Addons use this class to register and unregister additional backends. + */ +class StorageManager +{ + // Default tables to look for data + const TABLES = ['photo', 'attach']; + + // Default storage backends + /** @var string[] */ + const DEFAULT_BACKENDS = [ + Type\Filesystem::NAME, + Type\Database::NAME, + ]; + + /** @var string[] List of valid backend classes */ + private $validBackends; + + /** + * @var ICanReadFromStorage[] A local cache for storage instances + */ + private $backendInstances = []; + + /** @var Database */ + private $dba; + /** @var IManageConfigValues */ + private $config; + /** @var LoggerInterface */ + private $logger; + /** @var L10n */ + private $l10n; + + /** @var ICanWriteToStorage */ + private $currentBackend; + + /** + * @param Database $dba + * @param IManageConfigValues $config + * @param LoggerInterface $logger + * @param L10n $l10n + * + * @throws InvalidClassStorageException in case the active backend class is invalid + * @throws StorageException in case of unexpected errors during the active backend class loading + */ + public function __construct(Database $dba, IManageConfigValues $config, LoggerInterface $logger, L10n $l10n) + { + $this->dba = $dba; + $this->config = $config; + $this->logger = $logger; + $this->l10n = $l10n; + $this->validBackends = $config->get('storage', 'backends', self::DEFAULT_BACKENDS); + + $currentName = $this->config->get('storage', 'name'); + + // you can only use user backends as a "default" backend, so the second parameter is true + $this->currentBackend = $this->getWritableStorageByName($currentName); + } + + /** + * Return current storage backend class + * + * @return ICanWriteToStorage + */ + public function getBackend(): ICanWriteToStorage + { + return $this->currentBackend; + } + + /** + * Returns a writable storage backend class by registered name + * + * @param string $name Backend name + * + * @return ICanWriteToStorage + * + * @throws InvalidClassStorageException in case there's no backend class for the name + * @throws StorageException in case of an unexpected failure during the hook call + */ + public function getWritableStorageByName(string $name): ICanWriteToStorage + { + $storage = $this->getByName($name, $this->validBackends); + if (!$storage instanceof ICanWriteToStorage) { + throw new InvalidClassStorageException(sprintf('Backend %s is not writable', $name)); + } + + return $storage; + } + + /** + * Return storage backend configuration by registered name + * + * @param string $name Backend name + * + * @return ICanConfigureStorage|false + * + * @throws InvalidClassStorageException in case there's no backend class for the name + * @throws StorageException in case of an unexpected failure during the hook call + */ + public function getConfigurationByName(string $name) + { + switch ($name) { + // Try the filesystem backend + case Type\Filesystem::getName(): + return new Type\FilesystemConfig($this->config, $this->l10n); + // try the database backend + case Type\Database::getName(): + return false; + default: + $data = [ + 'name' => $name, + 'storage_config' => null, + ]; + try { + Hook::callAll('storage_config', $data); + if (!($data['storage_config'] ?? null) instanceof ICanConfigureStorage) { + throw new InvalidClassStorageException(sprintf('Configuration for backend %s was not found', $name)); + } + + return $data['storage_config']; + } catch (InternalServerErrorException $exception) { + throw new StorageException(sprintf('Failed calling hook::storage_config for backend %s', $name), $exception); + } + } + } + + /** + * Return storage backend class by registered name + * + * @param string $name Backend name + * @param string[]|null $validBackends possible, manual override of the valid backends + * + * @return ICanReadFromStorage + * + * @throws InvalidClassStorageException in case there's no backend class for the name + * @throws StorageException in case of an unexpected failure during the hook call + */ + public function getByName(string $name, array $validBackends = null): ICanReadFromStorage + { + // If there's no cached instance create a new instance + if (!isset($this->backendInstances[$name])) { + // If the current name isn't a valid backend (or the SystemResource instance) create it + if (!$this->isValidBackend($name, $validBackends)) { + throw new InvalidClassStorageException(sprintf('Backend %s is not valid', $name)); + } + + switch ($name) { + // Try the filesystem backend + case Type\Filesystem::getName(): + $storageConfig = new Type\FilesystemConfig($this->config, $this->l10n); + $this->backendInstances[$name] = new Type\Filesystem($storageConfig->getStoragePath()); + break; + // try the database backend + case Type\Database::getName(): + $this->backendInstances[$name] = new Type\Database($this->dba); + break; + // at least, try if there's an addon for the backend + case \Friendica\Core\Storage\Type\SystemResource::getName(): + $this->backendInstances[$name] = new \Friendica\Core\Storage\Type\SystemResource(); + break; + case \Friendica\Core\Storage\Type\ExternalResource::getName(): + $this->backendInstances[$name] = new \Friendica\Core\Storage\Type\ExternalResource(); + break; + default: + $data = [ + 'name' => $name, + 'storage' => null, + ]; + try { + Hook::callAll('storage_instance', $data); + if (!($data['storage'] ?? null) instanceof ICanReadFromStorage) { + throw new InvalidClassStorageException(sprintf('Backend %s was not found', $name)); + } + + $this->backendInstances[$data['name'] ?? $name] = $data['storage']; + } catch (InternalServerErrorException $exception) { + throw new StorageException(sprintf('Failed calling hook::storage_instance for backend %s', $name), $exception); + } + break; + } + } + + return $this->backendInstances[$name]; + } + + /** + * Checks, if the storage is a valid backend + * + * @param string|null $name The name or class of the backend + * @param string[]|null $validBackends Possible, valid backends to check + * + * @return boolean True, if the backend is a valid backend + */ + public function isValidBackend(string $name = null, array $validBackends = null): bool + { + $validBackends = $validBackends ?? array_merge($this->validBackends, + [ + Type\SystemResource::getName(), + Type\ExternalResource::getName(), + ]); + return in_array($name, $validBackends); + } + + /** + * Set current storage backend class + * + * @param ICanWriteToStorage $storage The storage class + * + * @return boolean True, if the set was successful + */ + public function setBackend(ICanWriteToStorage $storage): bool + { + if ($this->config->set('storage', 'name', $storage::getName())) { + $this->currentBackend = $storage; + return true; + } else { + return false; + } + } + + /** + * Get registered backends + * + * @return string[] + */ + public function listBackends(): array + { + return $this->validBackends; + } + + /** + * Register a storage backend class + * + * You have to register the hook "storage_instance" as well to make this class work! + * + * @param string $class Backend class name + * + * @return boolean True, if the registration was successful + */ + public function register(string $class): bool + { + if (is_subclass_of($class, ICanReadFromStorage::class)) { + /** @var ICanReadFromStorage $class */ + if ($this->isValidBackend($class::getName(), $this->validBackends)) { + return true; + } + + $backends = $this->validBackends; + $backends[] = $class::getName(); + + if ($this->config->set('storage', 'backends', $backends)) { + $this->validBackends = $backends; + return true; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Unregister a storage backend class + * + * @param string $class Backend class name + * + * @return boolean True, if unregistering was successful + * + * @throws StorageException + */ + public function unregister(string $class): bool + { + if (is_subclass_of($class, ICanReadFromStorage::class)) { + /** @var ICanReadFromStorage $class */ + if ($this->currentBackend::getName() == $class::getName()) { + throw new StorageException(sprintf('Cannot unregister %s, because it\'s currently active.', $class::getName())); + } + + $key = array_search($class::getName(), $this->validBackends); + + if ($key !== false) { + $backends = $this->validBackends; + unset($backends[$key]); + $backends = array_values($backends); + if ($this->config->set('storage', 'backends', $backends)) { + $this->validBackends = $backends; + return true; + } else { + return false; + } + } else { + return true; + } + } else { + return false; + } + } + + /** + * Move up to 5000 resources to storage $dest + * + * Copy existing data to destination storage and delete from source. + * This method cannot move to legacy in-table `data` field. + * + * @param ICanWriteToStorage $destination Destination storage class name + * @param array $tables Tables to look in for resources. Optional, defaults to ['photo', 'attach'] + * @param int $limit Limit of the process batch size, defaults to 5000 + * + * @return int Number of moved resources + * @throws StorageException + * @throws Exception + */ + public function move(ICanWriteToStorage $destination, array $tables = self::TABLES, int $limit = 5000): int + { + if (!$this->isValidBackend($destination, $this->validBackends)) { + throw new StorageException(sprintf("Can't move to storage backend '%s'", $destination::getName())); + } + + $moved = 0; + foreach ($tables as $table) { + // Get the rows where backend class is not the destination backend class + $resources = $this->dba->select( + $table, + ['id', 'data', 'backend-class', 'backend-ref'], + ['`backend-class` IS NULL or `backend-class` != ?', $destination::getName()], + ['limit' => $limit] + ); + + while ($resource = $this->dba->fetch($resources)) { + $id = $resource['id']; + $sourceRef = $resource['backend-ref']; + $source = null; + + try { + $source = $this->getWritableStorageByName($resource['backend-class'] ?? ''); + $this->logger->info('Get data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]); + $data = $source->get($sourceRef); + } catch (InvalidClassStorageException $exception) { + $this->logger->info('Get data from DB resource field.', ['oldReference' => $sourceRef]); + $data = $resource['data']; + } catch (ReferenceStorageException $exception) { + $this->logger->info('Invalid source reference.', ['oldBackend' => $source, 'oldReference' => $sourceRef]); + continue; + } + + $this->logger->info('Save data to new backend.', ['newBackend' => $destination::getName()]); + $destinationRef = $destination->put($data); + $this->logger->info('Saved data.', ['newReference' => $destinationRef]); + + if ($destinationRef !== '') { + $this->logger->info('update row'); + if ($this->dba->update($table, ['backend-class' => $destination::getName(), 'backend-ref' => $destinationRef, 'data' => ''], ['id' => $id])) { + if (!empty($source)) { + $this->logger->info('Deleted data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]); + $source->delete($sourceRef); + } + $moved++; + } + } + } + + $this->dba->close($resources); + } + + return $moved; + } +} diff --git a/src/Core/Storage/Type/Database.php b/src/Core/Storage/Type/Database.php new file mode 100644 index 0000000000..a22f8ae457 --- /dev/null +++ b/src/Core/Storage/Type/Database.php @@ -0,0 +1,131 @@ +. + * + */ + +namespace Friendica\Core\Storage\Type; + +use Exception; +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Exception\StorageException; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; +use Friendica\Database\Database as DBA; + +/** + * Database based storage system + * + * This class manage data stored in database table. + */ +class Database implements ICanWriteToStorage +{ + const NAME = 'Database'; + + /** @var DBA */ + private $dba; + + /** + * @param DBA $dba + */ + public function __construct(DBA $dba) + { + $this->dba = $dba; + } + + /** + * @inheritDoc + */ + public function get(string $reference): string + { + try { + $result = $this->dba->selectFirst('storage', ['data'], ['id' => $reference]); + if (!$this->dba->isResult($result)) { + throw new ReferenceStorageException(sprintf('Database storage cannot find data for reference %s', $reference)); + } + + return $result['data']; + } catch (Exception $exception) { + if ($exception instanceof ReferenceStorageException) { + throw $exception; + } else { + throw new StorageException(sprintf('Database storage failed to get %s', $reference), $exception->getCode(), $exception); + } + } + } + + /** + * @inheritDoc + */ + public function put(string $data, string $reference = ''): string + { + if ($reference !== '') { + try { + $result = $this->dba->update('storage', ['data' => $data], ['id' => $reference]); + } catch (Exception $exception) { + throw new StorageException(sprintf('Database storage failed to update %s', $reference), $exception->getCode(), $exception); + } + if ($result === false) { + throw new StorageException(sprintf('Database storage failed to update %s', $reference), 500, new Exception($this->dba->errorMessage(), $this->dba->errorNo())); + } + + return $reference; + } else { + try { + $result = $this->dba->insert('storage', ['data' => $data]); + } catch (Exception $exception) { + throw new StorageException(sprintf('Database storage failed to insert %s', $reference), $exception->getCode(), $exception); + } + if ($result === false) { + throw new StorageException(sprintf('Database storage failed to update %s', $reference), 500, new Exception($this->dba->errorMessage(), $this->dba->errorNo())); + } + + return $this->dba->lastInsertId(); + } + } + + /** + * @inheritDoc + */ + public function delete(string $reference) + { + try { + if (!$this->dba->delete('storage', ['id' => $reference]) || $this->dba->affectedRows() === 0) { + throw new ReferenceStorageException(sprintf('Database storage failed to delete %s', $reference)); + } + } catch (Exception $exception) { + if ($exception instanceof ReferenceStorageException) { + throw $exception; + } else { + throw new StorageException(sprintf('Database storage failed to delete %s', $reference), $exception->getCode(), $exception); + } + } + } + + /** + * @inheritDoc + */ + public static function getName(): string + { + return self::NAME; + } + + public function __toString(): string + { + return self::getName(); + } +} diff --git a/src/Core/Storage/Type/ExternalResource.php b/src/Core/Storage/Type/ExternalResource.php new file mode 100644 index 0000000000..33e65921b6 --- /dev/null +++ b/src/Core/Storage/Type/ExternalResource.php @@ -0,0 +1,81 @@ +. + * + */ + +namespace Friendica\Core\Storage\Type; + +use Exception; +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Capability\ICanReadFromStorage; +use Friendica\Util\HTTPSignature; + +/** + * External resource storage class + * + * This class is used to load external resources, like images. + * Is not intended to be selectable by admins as default storage class. + */ +class ExternalResource implements ICanReadFromStorage +{ + const NAME = 'ExternalResource'; + + /** + * @inheritDoc + */ + public function get(string $reference): string + { + $data = json_decode($reference); + if (empty($data->url)) { + throw new ReferenceStorageException(sprintf('Invalid reference %s, cannot retrieve URL', $reference)); + } + + $parts = parse_url($data->url); + if (empty($parts['scheme']) || empty($parts['host'])) { + throw new ReferenceStorageException(sprintf('Invalid reference %s, cannot extract scheme and host', $reference)); + } + + try { + $fetchResult = HTTPSignature::fetchRaw($data->url, $data->uid, ['accept_content' => []]); + } catch (Exception $exception) { + throw new ReferenceStorageException(sprintf('External resource failed to get %s', $reference), $exception->getCode(), $exception); + } + if ($fetchResult->isSuccess()) { + return $fetchResult->getBody(); + } else { + throw new ReferenceStorageException(sprintf('External resource failed to get %s', $reference), $fetchResult->getReturnCode(), new Exception($fetchResult->getBody())); + } + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + return self::NAME; + } + + /** + * @inheritDoc + */ + public static function getName(): string + { + return self::NAME; + } +} diff --git a/src/Core/Storage/Type/Filesystem.php b/src/Core/Storage/Type/Filesystem.php new file mode 100644 index 0000000000..21a531946d --- /dev/null +++ b/src/Core/Storage/Type/Filesystem.php @@ -0,0 +1,185 @@ +. + * + */ + +namespace Friendica\Core\Storage\Type; + +use Exception; +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Exception\StorageException; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; +use Friendica\Util\Strings; + +/** + * Filesystem based storage backend + * + * This class manage data on filesystem. + * Base folder for storage is set in storage.filesystem_path. + * Best would be for storage folder to be outside webserver folder, we are using a + * folder relative to code tree root as default to ease things for users in shared hostings. + * Each new resource gets a value as reference and is saved in a + * folder tree stucture created from that value. + */ +class Filesystem implements ICanWriteToStorage +{ + const NAME = 'Filesystem'; + + /** @var string */ + private $basePath; + + /** + * Filesystem constructor. + * + * @param string $filesystemPath + * + * @throws StorageException in case the path doesn't exist or isn't writeable + */ + public function __construct(string $filesystemPath = FilesystemConfig::DEFAULT_BASE_FOLDER) + { + $path = $filesystemPath; + $this->basePath = rtrim($path, '/'); + + if (!is_dir($this->basePath) || !is_writable($this->basePath)) { + throw new StorageException(sprintf('Path "%s" does not exist or is not writeable.', $this->basePath)); + } + } + + /** + * Split data ref and return file path + * + * @param string $reference Data reference + * + * @return string + */ + private function pathForRef(string $reference): string + { + $fold1 = substr($reference, 0, 2); + $fold2 = substr($reference, 2, 2); + $file = substr($reference, 4); + + return implode('/', [$this->basePath, $fold1, $fold2, $file]); + } + + + /** + * Create directory tree to store file, with .htaccess and index.html files + * + * @param string $file Path and filename + * + * @throws StorageException + */ + private function createFoldersForFile(string $file) + { + $path = dirname($file); + + if (!is_dir($path)) { + if (!mkdir($path, 0770, true)) { + throw new StorageException(sprintf('Filesystem storage failed to create "%s". Check you write permissions.', $path)); + } + } + + while ($path !== $this->basePath) { + if (!is_file($path . '/index.html')) { + file_put_contents($path . '/index.html', ''); + } + chmod($path . '/index.html', 0660); + chmod($path, 0770); + $path = dirname($path); + } + if (!is_file($path . '/index.html')) { + file_put_contents($path . '/index.html', ''); + chmod($path . '/index.html', 0660); + } + } + + /** + * @inheritDoc + */ + public function get(string $reference): string + { + $file = $this->pathForRef($reference); + if (!is_file($file)) { + throw new ReferenceStorageException(sprintf('Filesystem storage failed to get the file %s, The file is invalid', $reference)); + } + + $result = file_get_contents($file); + + if ($result === false) { + throw new StorageException(sprintf('Filesystem storage failed to get data to "%s". Check your write permissions', $file)); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function put(string $data, string $reference = ''): string + { + if ($reference === '') { + try { + $reference = Strings::getRandomHex(); + } catch (Exception $exception) { + throw new StorageException('Filesystem storage failed to generate a random hex', $exception->getCode(), $exception); + } + } + $file = $this->pathForRef($reference); + + $this->createFoldersForFile($file); + + $result = file_put_contents($file, $data); + + // just in case the result is REALLY false, not zero or empty or anything else, throw the exception + if ($result === false) { + throw new StorageException(sprintf('Filesystem storage failed to save data to "%s". Check your write permissions', $file)); + } + + chmod($file, 0660); + return $reference; + } + + /** + * @inheritDoc + */ + public function delete(string $reference) + { + $file = $this->pathForRef($reference); + if (!is_file($file)) { + throw new ReferenceStorageException(sprintf('File with reference "%s" doesn\'t exist', $reference)); + } + + if (!unlink($file)) { + throw new StorageException(sprintf('Cannot delete with file with reference "%s"', $reference)); + } + } + + /** + * @inheritDoc + */ + public static function getName(): string + { + return self::NAME; + } + + public function __toString(): string + { + return self::getName(); + } +} diff --git a/src/Core/Storage/Type/FilesystemConfig.php b/src/Core/Storage/Type/FilesystemConfig.php new file mode 100644 index 0000000000..b30b2af089 --- /dev/null +++ b/src/Core/Storage/Type/FilesystemConfig.php @@ -0,0 +1,100 @@ +. + * + */ + +namespace Friendica\Core\Storage\Type; + +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\L10n; +use Friendica\Core\Storage\Capability\ICanConfigureStorage; + +/** + * Filesystem based storage backend configuration + */ +class FilesystemConfig implements ICanConfigureStorage +{ + // Default base folder + const DEFAULT_BASE_FOLDER = 'storage'; + + /** @var IManageConfigValues */ + private $config; + + /** @var string */ + private $storagePath; + + /** @var L10n */ + private $l10n; + + /** + * Returns the current storage path + * + * @return string + */ + public function getStoragePath(): string + { + return $this->storagePath; + } + + /** + * Filesystem constructor. + * + * @param IManageConfigValues $config + * @param L10n $l10n + */ + public function __construct(IManageConfigValues $config, L10n $l10n) + { + $this->config = $config; + $this->l10n = $l10n; + + $path = $this->config->get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER); + $this->storagePath = rtrim($path, '/'); + } + + /** + * @inheritDoc + */ + public function getOptions(): array + { + return [ + 'storagepath' => [ + 'input', + $this->l10n->t('Storage base path'), + $this->storagePath, + $this->l10n->t('Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree') + ] + ]; + } + + /** + * @inheritDoc + */ + public function saveOptions(array $data): array + { + $storagePath = $data['storagepath'] ?? ''; + if ($storagePath === '' || !is_dir($storagePath)) { + return [ + 'storagepath' => $this->l10n->t('Enter a valid existing folder') + ]; + }; + $this->config->set('storage', 'filesystem_path', $storagePath); + $this->storagePath = $storagePath; + return []; + } +} diff --git a/src/Core/Storage/Type/SystemResource.php b/src/Core/Storage/Type/SystemResource.php new file mode 100644 index 0000000000..7b73b7c2ee --- /dev/null +++ b/src/Core/Storage/Type/SystemResource.php @@ -0,0 +1,77 @@ +. + * + */ + +namespace Friendica\Core\Storage\Type; + +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Exception\StorageException; +use Friendica\Core\Storage\Capability\ICanReadFromStorage; + +/** + * System resource storage class + * + * This class is used to load system resources, like images. + * Is not intended to be selectable by admins as default storage class. + */ +class SystemResource implements ICanReadFromStorage +{ + const NAME = 'SystemResource'; + + // Valid folders to look for resources + const VALID_FOLDERS = ["images"]; + + /** + * @inheritDoc + */ + public function get(string $reference): string + { + $folder = dirname($reference); + if (!in_array($folder, self::VALID_FOLDERS)) { + throw new ReferenceStorageException(sprintf('System Resource is invalid for reference %s, no valid folder found', $reference)); + } + if (!file_exists($reference)) { + throw new StorageException(sprintf('System Resource is invalid for reference %s, the file doesn\'t exist', $reference)); + } + $content = file_get_contents($reference); + + if ($content === false) { + throw new StorageException(sprintf('Cannot get content for reference %s', $reference)); + } + + return $content; + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + return self::NAME; + } + + /** + * @inheritDoc + */ + public static function getName(): string + { + return self::NAME; + } +} diff --git a/src/Core/StorageManager.php b/src/Core/StorageManager.php deleted file mode 100644 index e27b59edb0..0000000000 --- a/src/Core/StorageManager.php +++ /dev/null @@ -1,401 +0,0 @@ -. - * - */ - -namespace Friendica\Core; - -use Exception; -use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Database\Database; -use Friendica\Model\Storage; -use Friendica\Network\HTTPException\InternalServerErrorException; -use Psr\Log\LoggerInterface; - -/** - * Manage storage backends - * - * Core code uses this class to get and set current storage backend class. - * Addons use this class to register and unregister additional backends. - */ -class StorageManager -{ - // Default tables to look for data - const TABLES = ['photo', 'attach']; - - // Default storage backends - /** @var string[] */ - const DEFAULT_BACKENDS = [ - Storage\Filesystem::NAME, - Storage\Database::NAME, - ]; - - /** @var string[] List of valid backend classes */ - private $validBackends; - - /** - * @var Storage\IStorage[] A local cache for storage instances - */ - private $backendInstances = []; - - /** @var Database */ - private $dba; - /** @var IManageConfigValues */ - private $config; - /** @var LoggerInterface */ - private $logger; - /** @var L10n */ - private $l10n; - - /** @var Storage\IWritableStorage */ - private $currentBackend; - - /** - * @param Database $dba - * @param IManageConfigValues $config - * @param LoggerInterface $logger - * @param L10n $l10n - * - * @throws Storage\InvalidClassStorageException in case the active backend class is invalid - * @throws Storage\StorageException in case of unexpected errors during the active backend class loading - */ - public function __construct(Database $dba, IManageConfigValues $config, LoggerInterface $logger, L10n $l10n) - { - $this->dba = $dba; - $this->config = $config; - $this->logger = $logger; - $this->l10n = $l10n; - $this->validBackends = $config->get('storage', 'backends', self::DEFAULT_BACKENDS); - - $currentName = $this->config->get('storage', 'name'); - - // you can only use user backends as a "default" backend, so the second parameter is true - $this->currentBackend = $this->getWritableStorageByName($currentName); - } - - /** - * Return current storage backend class - * - * @return Storage\IWritableStorage - */ - public function getBackend() - { - return $this->currentBackend; - } - - /** - * Returns a writable storage backend class by registered name - * - * @param string $name Backend name - * - * @return Storage\IWritableStorage - * - * @throws Storage\InvalidClassStorageException in case there's no backend class for the name - * @throws Storage\StorageException in case of an unexpected failure during the hook call - */ - public function getWritableStorageByName(string $name): Storage\IWritableStorage - { - $storage = $this->getByName($name, $this->validBackends); - if (!$storage instanceof Storage\IWritableStorage) { - throw new Storage\InvalidClassStorageException(sprintf('Backend %s is not writable', $name)); - } - - return $storage; - } - - /** - * Return storage backend configuration by registered name - * - * @param string $name Backend name - * - * @return Storage\IStorageConfiguration|false - * - * @throws Storage\InvalidClassStorageException in case there's no backend class for the name - * @throws Storage\StorageException in case of an unexpected failure during the hook call - */ - public function getConfigurationByName(string $name) - { - switch ($name) { - // Try the filesystem backend - case Storage\Filesystem::getName(): - return new Storage\FilesystemConfig($this->config, $this->l10n); - // try the database backend - case Storage\Database::getName(): - return false; - default: - $data = [ - 'name' => $name, - 'storage_config' => null, - ]; - try { - Hook::callAll('storage_config', $data); - if (!($data['storage_config'] ?? null) instanceof Storage\IStorageConfiguration) { - throw new Storage\InvalidClassStorageException(sprintf('Configuration for backend %s was not found', $name)); - } - - return $data['storage_config']; - } catch (InternalServerErrorException $exception) { - throw new Storage\StorageException(sprintf('Failed calling hook::storage_config for backend %s', $name), $exception); - } - } - } - - /** - * Return storage backend class by registered name - * - * @param string $name Backend name - * @param string[]|null $validBackends possible, manual override of the valid backends - * - * @return Storage\IStorage - * - * @throws Storage\InvalidClassStorageException in case there's no backend class for the name - * @throws Storage\StorageException in case of an unexpected failure during the hook call - */ - public function getByName(string $name, array $validBackends = null): Storage\IStorage - { - // If there's no cached instance create a new instance - if (!isset($this->backendInstances[$name])) { - // If the current name isn't a valid backend (or the SystemResource instance) create it - if (!$this->isValidBackend($name, $validBackends)) { - throw new Storage\InvalidClassStorageException(sprintf('Backend %s is not valid', $name)); - } - - switch ($name) { - // Try the filesystem backend - case Storage\Filesystem::getName(): - $storageConfig = new Storage\FilesystemConfig($this->config, $this->l10n); - $this->backendInstances[$name] = new Storage\Filesystem($storageConfig->getStoragePath()); - break; - // try the database backend - case Storage\Database::getName(): - $this->backendInstances[$name] = new Storage\Database($this->dba); - break; - // at least, try if there's an addon for the backend - case Storage\SystemResource::getName(): - $this->backendInstances[$name] = new Storage\SystemResource(); - break; - case Storage\ExternalResource::getName(): - $this->backendInstances[$name] = new Storage\ExternalResource(); - break; - default: - $data = [ - 'name' => $name, - 'storage' => null, - ]; - try { - Hook::callAll('storage_instance', $data); - if (!($data['storage'] ?? null) instanceof Storage\IStorage) { - throw new Storage\InvalidClassStorageException(sprintf('Backend %s was not found', $name)); - } - - $this->backendInstances[$data['name'] ?? $name] = $data['storage']; - } catch (InternalServerErrorException $exception) { - throw new Storage\StorageException(sprintf('Failed calling hook::storage_instance for backend %s', $name), $exception); - } - break; - } - } - - return $this->backendInstances[$name]; - } - - /** - * Checks, if the storage is a valid backend - * - * @param string|null $name The name or class of the backend - * @param string[]|null $validBackends Possible, valid backends to check - * - * @return boolean True, if the backend is a valid backend - */ - public function isValidBackend(string $name = null, array $validBackends = null): bool - { - $validBackends = $validBackends ?? array_merge($this->validBackends, - [ - Storage\SystemResource::getName(), - Storage\ExternalResource::getName(), - ]); - return in_array($name, $validBackends); - } - - /** - * Set current storage backend class - * - * @param Storage\IWritableStorage $storage The storage class - * - * @return boolean True, if the set was successful - */ - public function setBackend(Storage\IWritableStorage $storage): bool - { - if ($this->config->set('storage', 'name', $storage::getName())) { - $this->currentBackend = $storage; - return true; - } else { - return false; - } - } - - /** - * Get registered backends - * - * @return string[] - */ - public function listBackends(): array - { - return $this->validBackends; - } - - /** - * Register a storage backend class - * - * You have to register the hook "storage_instance" as well to make this class work! - * - * @param string $class Backend class name - * - * @return boolean True, if the registration was successful - */ - public function register(string $class): bool - { - if (is_subclass_of($class, Storage\IStorage::class)) { - /** @var Storage\IStorage $class */ - - if ($this->isValidBackend($class::getName(), $this->validBackends)) { - return true; - } - - $backends = $this->validBackends; - $backends[] = $class::getName(); - - if ($this->config->set('storage', 'backends', $backends)) { - $this->validBackends = $backends; - return true; - } else { - return false; - } - } else { - return false; - } - } - - /** - * Unregister a storage backend class - * - * @param string $class Backend class name - * - * @return boolean True, if unregistering was successful - * - * @throws Storage\StorageException - */ - public function unregister(string $class): bool - { - if (is_subclass_of($class, Storage\IStorage::class)) { - /** @var Storage\IStorage $class */ - - if ($this->currentBackend::getName() == $class::getName()) { - throw new Storage\StorageException(sprintf('Cannot unregister %s, because it\'s currently active.', $class::getName())); - } - - $key = array_search($class::getName(), $this->validBackends); - - if ($key !== false) { - $backends = $this->validBackends; - unset($backends[$key]); - $backends = array_values($backends); - if ($this->config->set('storage', 'backends', $backends)) { - $this->validBackends = $backends; - return true; - } else { - return false; - } - } else { - return true; - } - } else { - return false; - } - } - - /** - * Move up to 5000 resources to storage $dest - * - * Copy existing data to destination storage and delete from source. - * This method cannot move to legacy in-table `data` field. - * - * @param Storage\IWritableStorage $destination Destination storage class name - * @param array $tables Tables to look in for resources. Optional, defaults to ['photo', 'attach'] - * @param int $limit Limit of the process batch size, defaults to 5000 - * - * @return int Number of moved resources - * @throws Storage\StorageException - * @throws Exception - */ - public function move(Storage\IWritableStorage $destination, array $tables = self::TABLES, int $limit = 5000): int - { - if (!$this->isValidBackend($destination, $this->validBackends)) { - throw new Storage\StorageException(sprintf("Can't move to storage backend '%s'", $destination::getName())); - } - - $moved = 0; - foreach ($tables as $table) { - // Get the rows where backend class is not the destination backend class - $resources = $this->dba->select( - $table, - ['id', 'data', 'backend-class', 'backend-ref'], - ['`backend-class` IS NULL or `backend-class` != ?', $destination::getName()], - ['limit' => $limit] - ); - - while ($resource = $this->dba->fetch($resources)) { - $id = $resource['id']; - $sourceRef = $resource['backend-ref']; - $source = null; - - try { - $source = $this->getWritableStorageByName($resource['backend-class'] ?? ''); - $this->logger->info('Get data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]); - $data = $source->get($sourceRef); - } catch (Storage\InvalidClassStorageException $exception) { - $this->logger->info('Get data from DB resource field.', ['oldReference' => $sourceRef]); - $data = $resource['data']; - } catch (Storage\ReferenceStorageException $exception) { - $this->logger->info('Invalid source reference.', ['oldBackend' => $source, 'oldReference' => $sourceRef]); - continue; - } - - $this->logger->info('Save data to new backend.', ['newBackend' => $destination::getName()]); - $destinationRef = $destination->put($data); - $this->logger->info('Saved data.', ['newReference' => $destinationRef]); - - if ($destinationRef !== '') { - $this->logger->info('update row'); - if ($this->dba->update($table, ['backend-class' => $destination::getName(), 'backend-ref' => $destinationRef, 'data' => ''], ['id' => $id])) { - if (!empty($source)) { - $this->logger->info('Delete data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]); - $source->delete($sourceRef); - } - $moved++; - } - } - } - - $this->dba->close($resources); - } - - return $moved; - } -} diff --git a/src/DI.php b/src/DI.php index d45801a9f6..5ba7e88db1 100644 --- a/src/DI.php +++ b/src/DI.php @@ -211,11 +211,11 @@ abstract class DI } /** - * @return Core\StorageManager + * @return \Friendica\Core\Storage\Repository\StorageManager */ public static function storageManager() { - return self::$dice->create(Core\StorageManager::class); + return self::$dice->create(Core\Storage\Repository\StorageManager::class); } // @@ -395,11 +395,11 @@ abstract class DI } /** - * @return Model\Storage\IWritableStorage + * @return Core\Storage\Capability\ICanWriteToStorage */ public static function storage() { - return self::$dice->create(Model\Storage\IWritableStorage::class); + return self::$dice->create(Core\Storage\Capability\ICanWriteToStorage::class); } /** diff --git a/src/Model/Attach.php b/src/Model/Attach.php index e11fd01bc3..dcc7906763 100644 --- a/src/Model/Attach.php +++ b/src/Model/Attach.php @@ -25,8 +25,8 @@ use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; -use Friendica\Model\Storage\InvalidClassStorageException; -use Friendica\Model\Storage\ReferenceStorageException; +use Friendica\Core\Storage\Exception\InvalidClassStorageException; +use Friendica\Core\Storage\Exception\ReferenceStorageException; use Friendica\Object\Image; use Friendica\Util\DateTimeFormat; use Friendica\Util\Mimetype; diff --git a/src/Model/Photo.php b/src/Model/Photo.php index ebd278753b..13beb05b3c 100644 --- a/src/Model/Photo.php +++ b/src/Model/Photo.php @@ -27,11 +27,11 @@ use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; -use Friendica\Model\Storage\ExternalResource; -use Friendica\Model\Storage\InvalidClassStorageException; -use Friendica\Model\Storage\ReferenceStorageException; -use Friendica\Model\Storage\StorageException; -use Friendica\Model\Storage\SystemResource; +use Friendica\Core\Storage\Type\ExternalResource; +use Friendica\Core\Storage\Exception\InvalidClassStorageException; +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Core\Storage\Exception\StorageException; +use Friendica\Core\Storage\Type\SystemResource; use Friendica\Object\Image; use Friendica\Util\DateTimeFormat; use Friendica\Util\Images; diff --git a/src/Model/Storage/Database.php b/src/Model/Storage/Database.php deleted file mode 100644 index 7b90d87890..0000000000 --- a/src/Model/Storage/Database.php +++ /dev/null @@ -1,128 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -use Exception; -use Friendica\Database\Database as DBA; - -/** - * Database based storage system - * - * This class manage data stored in database table. - */ -class Database implements IWritableStorage -{ - const NAME = 'Database'; - - /** @var DBA */ - private $dba; - - /** - * @param DBA $dba - */ - public function __construct(DBA $dba) - { - $this->dba = $dba; - } - - /** - * @inheritDoc - */ - public function get(string $reference): string - { - try { - $result = $this->dba->selectFirst('storage', ['data'], ['id' => $reference]); - if (!$this->dba->isResult($result)) { - throw new ReferenceStorageException(sprintf('Database storage cannot find data for reference %s', $reference)); - } - - return $result['data']; - } catch (Exception $exception) { - if ($exception instanceof ReferenceStorageException) { - throw $exception; - } else { - throw new StorageException(sprintf('Database storage failed to get %s', $reference), $exception->getCode(), $exception); - } - } - } - - /** - * @inheritDoc - */ - public function put(string $data, string $reference = ''): string - { - if ($reference !== '') { - try { - $result = $this->dba->update('storage', ['data' => $data], ['id' => $reference]); - } catch (Exception $exception) { - throw new StorageException(sprintf('Database storage failed to update %s', $reference), $exception->getCode(), $exception); - } - if ($result === false) { - throw new StorageException(sprintf('Database storage failed to update %s', $reference), 500, new Exception($this->dba->errorMessage(), $this->dba->errorNo())); - } - - return $reference; - } else { - try { - $result = $this->dba->insert('storage', ['data' => $data]); - } catch (Exception $exception) { - throw new StorageException(sprintf('Database storage failed to insert %s', $reference), $exception->getCode(), $exception); - } - if ($result === false) { - throw new StorageException(sprintf('Database storage failed to update %s', $reference), 500, new Exception($this->dba->errorMessage(), $this->dba->errorNo())); - } - - return $this->dba->lastInsertId(); - } - } - - /** - * @inheritDoc - */ - public function delete(string $reference) - { - try { - if (!$this->dba->delete('storage', ['id' => $reference]) || $this->dba->affectedRows() === 0) { - throw new ReferenceStorageException(sprintf('Database storage failed to delete %s', $reference)); - } - } catch (Exception $exception) { - if ($exception instanceof ReferenceStorageException) { - throw $exception; - } else { - throw new StorageException(sprintf('Database storage failed to delete %s', $reference), $exception->getCode(), $exception); - } - } - } - - /** - * @inheritDoc - */ - public static function getName(): string - { - return self::NAME; - } - - public function __toString() - { - return self::getName(); - } -} diff --git a/src/Model/Storage/ExternalResource.php b/src/Model/Storage/ExternalResource.php deleted file mode 100644 index 413050c306..0000000000 --- a/src/Model/Storage/ExternalResource.php +++ /dev/null @@ -1,79 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -use Exception; -use Friendica\Util\HTTPSignature; - -/** - * External resource storage class - * - * This class is used to load external resources, like images. - * Is not intended to be selectable by admins as default storage class. - */ -class ExternalResource implements IStorage -{ - const NAME = 'ExternalResource'; - - /** - * @inheritDoc - */ - public function get(string $reference): string - { - $data = json_decode($reference); - if (empty($data->url)) { - throw new ReferenceStorageException(sprintf('Invalid reference %s, cannot retrieve URL', $reference)); - } - - $parts = parse_url($data->url); - if (empty($parts['scheme']) || empty($parts['host'])) { - throw new ReferenceStorageException(sprintf('Invalid reference %s, cannot extract scheme and host', $reference)); - } - - try { - $fetchResult = HTTPSignature::fetchRaw($data->url, $data->uid, ['accept_content' => []]); - } catch (Exception $exception) { - throw new ReferenceStorageException(sprintf('External resource failed to get %s', $reference), $exception->getCode(), $exception); - } - if ($fetchResult->isSuccess()) { - return $fetchResult->getBody(); - } else { - throw new ReferenceStorageException(sprintf('External resource failed to get %s', $reference), $fetchResult->getReturnCode(), new Exception($fetchResult->getBody())); - } - } - - /** - * @inheritDoc - */ - public function __toString() - { - return self::NAME; - } - - /** - * @inheritDoc - */ - public static function getName(): string - { - return self::NAME; - } -} diff --git a/src/Model/Storage/Filesystem.php b/src/Model/Storage/Filesystem.php deleted file mode 100644 index 8b5078f548..0000000000 --- a/src/Model/Storage/Filesystem.php +++ /dev/null @@ -1,182 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -use Exception; -use Friendica\Util\Strings; - -/** - * Filesystem based storage backend - * - * This class manage data on filesystem. - * Base folder for storage is set in storage.filesystem_path. - * Best would be for storage folder to be outside webserver folder, we are using a - * folder relative to code tree root as default to ease things for users in shared hostings. - * Each new resource gets a value as reference and is saved in a - * folder tree stucture created from that value. - */ -class Filesystem implements IWritableStorage -{ - const NAME = 'Filesystem'; - - /** @var string */ - private $basePath; - - /** - * Filesystem constructor. - * - * @param string $filesystemPath - * - * @throws StorageException in case the path doesn't exist or isn't writeable - */ - public function __construct(string $filesystemPath = FilesystemConfig::DEFAULT_BASE_FOLDER) - { - $path = $filesystemPath; - $this->basePath = rtrim($path, '/'); - - if (!is_dir($this->basePath) || !is_writable($this->basePath)) { - throw new StorageException(sprintf('Path "%s" does not exist or is not writeable.', $this->basePath)); - } - } - - /** - * Split data ref and return file path - * - * @param string $reference Data reference - * - * @return string - */ - private function pathForRef(string $reference): string - { - $fold1 = substr($reference, 0, 2); - $fold2 = substr($reference, 2, 2); - $file = substr($reference, 4); - - return implode('/', [$this->basePath, $fold1, $fold2, $file]); - } - - - /** - * Create directory tree to store file, with .htaccess and index.html files - * - * @param string $file Path and filename - * - * @throws StorageException - */ - private function createFoldersForFile(string $file) - { - $path = dirname($file); - - if (!is_dir($path)) { - if (!mkdir($path, 0770, true)) { - throw new StorageException(sprintf('Filesystem storage failed to create "%s". Check you write permissions.', $path)); - } - } - - while ($path !== $this->basePath) { - if (!is_file($path . '/index.html')) { - file_put_contents($path . '/index.html', ''); - } - chmod($path . '/index.html', 0660); - chmod($path, 0770); - $path = dirname($path); - } - if (!is_file($path . '/index.html')) { - file_put_contents($path . '/index.html', ''); - chmod($path . '/index.html', 0660); - } - } - - /** - * @inheritDoc - */ - public function get(string $reference): string - { - $file = $this->pathForRef($reference); - if (!is_file($file)) { - throw new ReferenceStorageException(sprintf('Filesystem storage failed to get the file %s, The file is invalid', $reference)); - } - - $result = file_get_contents($file); - - if ($result === false) { - throw new StorageException(sprintf('Filesystem storage failed to get data to "%s". Check your write permissions', $file)); - } - - return $result; - } - - /** - * @inheritDoc - */ - public function put(string $data, string $reference = ''): string - { - if ($reference === '') { - try { - $reference = Strings::getRandomHex(); - } catch (Exception $exception) { - throw new StorageException('Filesystem storage failed to generate a random hex', $exception->getCode(), $exception); - } - } - $file = $this->pathForRef($reference); - - $this->createFoldersForFile($file); - - $result = file_put_contents($file, $data); - - // just in case the result is REALLY false, not zero or empty or anything else, throw the exception - if ($result === false) { - throw new StorageException(sprintf('Filesystem storage failed to save data to "%s". Check your write permissions', $file)); - } - - chmod($file, 0660); - return $reference; - } - - /** - * @inheritDoc - */ - public function delete(string $reference) - { - $file = $this->pathForRef($reference); - if (!is_file($file)) { - throw new ReferenceStorageException(sprintf('File with reference "%s" doesn\'t exist', $reference)); - } - - if (!unlink($file)) { - throw new StorageException(sprintf('Cannot delete with file with reference "%s"', $reference)); - } - } - - /** - * @inheritDoc - */ - public static function getName(): string - { - return self::NAME; - } - - public function __toString() - { - return self::getName(); - } -} diff --git a/src/Model/Storage/FilesystemConfig.php b/src/Model/Storage/FilesystemConfig.php deleted file mode 100644 index 1aa1c38173..0000000000 --- a/src/Model/Storage/FilesystemConfig.php +++ /dev/null @@ -1,99 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\L10n; - -/** - * Filesystem based storage backend configuration - */ -class FilesystemConfig implements IStorageConfiguration -{ - // Default base folder - const DEFAULT_BASE_FOLDER = 'storage'; - - /** @var IManageConfigValues */ - private $config; - - /** @var string */ - private $storagePath; - - /** @var L10n */ - private $l10n; - - /** - * Returns the current storage path - * - * @return string - */ - public function getStoragePath(): string - { - return $this->storagePath; - } - - /** - * Filesystem constructor. - * - * @param IManageConfigValues $config - * @param L10n $l10n - */ - public function __construct(IManageConfigValues $config, L10n $l10n) - { - $this->config = $config; - $this->l10n = $l10n; - - $path = $this->config->get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER); - $this->storagePath = rtrim($path, '/'); - } - - /** - * @inheritDoc - */ - public function getOptions(): array - { - return [ - 'storagepath' => [ - 'input', - $this->l10n->t('Storage base path'), - $this->storagePath, - $this->l10n->t('Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree') - ] - ]; - } - - /** - * @inheritDoc - */ - public function saveOptions(array $data): array - { - $storagePath = $data['storagepath'] ?? ''; - if ($storagePath === '' || !is_dir($storagePath)) { - return [ - 'storagepath' => $this->l10n->t('Enter a valid existing folder') - ]; - }; - $this->config->set('storage', 'filesystem_path', $storagePath); - $this->storagePath = $storagePath; - return []; - } -} diff --git a/src/Model/Storage/IStorage.php b/src/Model/Storage/IStorage.php deleted file mode 100644 index 8841487412..0000000000 --- a/src/Model/Storage/IStorage.php +++ /dev/null @@ -1,54 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -/** - * Interface for basic storage backends - */ -interface IStorage -{ - /** - * Get data from backend - * - * @param string $reference Data reference - * - * @return string - * - * @throws StorageException in case there's an unexpected error - * @throws ReferenceStorageException in case the reference doesn't exist - */ - public function get(string $reference): string; - - /** - * The name of the backend - * - * @return string - */ - public function __toString(); - - /** - * The name of the backend - * - * @return string - */ - public static function getName(): string; -} diff --git a/src/Model/Storage/IStorageConfiguration.php b/src/Model/Storage/IStorageConfiguration.php deleted file mode 100644 index c589f5ed5c..0000000000 --- a/src/Model/Storage/IStorageConfiguration.php +++ /dev/null @@ -1,78 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -/** - * The interface to use for configurable storage backends - */ -interface IStorageConfiguration -{ - /** - * Get info about storage options - * - * @return array - * - * This method return an array with information about storage options - * from which the form presented to the user is build. - * - * The returned array is: - * - * [ - * 'option1name' => [ ..info.. ], - * 'option2name' => [ ..info.. ], - * ... - * ] - * - * An empty array can be returned if backend doesn't have any options - * - * The info array for each option MUST be as follows: - * - * [ - * 'type', // define the field used in form, and the type of data. - * // one of 'checkbox', 'combobox', 'custom', 'datetime', - * // 'input', 'intcheckbox', 'password', 'radio', 'richtext' - * // 'select', 'select_raw', 'textarea' - * - * 'label', // Translatable label of the field - * 'value', // Current value - * 'help text', // Translatable description for the field - * extra data // Optional. Depends on 'type': - * // select: array [ value => label ] of choices - * // intcheckbox: value of input element - * // select_raw: prebuild html string of < option > tags - * ] - * - * See https://github.com/friendica/friendica/wiki/Quick-Template-Guide - */ - public function getOptions(): array; - - /** - * Validate and save options - * - * @param array $data Array [optionname => value] to be saved - * - * @return array Validation errors: [optionname => error message] - * - * Return array must be empty if no error. - */ - public function saveOptions(array $data): array; -} diff --git a/src/Model/Storage/IWritableStorage.php b/src/Model/Storage/IWritableStorage.php deleted file mode 100644 index 118f4b2561..0000000000 --- a/src/Model/Storage/IWritableStorage.php +++ /dev/null @@ -1,53 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -/** - * Interface for writable storage backends - * - * Used for storages with CRUD functionality, mainly used for user data (e.g. photos, attachements). - * There's only one active writable storage possible. This type of storage is selectable by the current administrator. - */ -interface IWritableStorage extends IStorage -{ - /** - * Put data in backend as $ref. If $ref is not defined a new reference is created. - * - * @param string $data Data to save - * @param string $reference Data reference. Optional. - * - * @return string Saved data reference - * - * @throws StorageException in case there's an unexpected error - */ - public function put(string $data, string $reference = ""): string; - - /** - * Remove data from backend - * - * @param string $reference Data reference - * - * @throws StorageException in case there's an unexpected error - * @throws ReferenceStorageException in case the reference doesn't exist - */ - public function delete(string $reference); -} diff --git a/src/Model/Storage/InvalidClassStorageException.php b/src/Model/Storage/InvalidClassStorageException.php deleted file mode 100644 index 9c39b3a60c..0000000000 --- a/src/Model/Storage/InvalidClassStorageException.php +++ /dev/null @@ -1,29 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -/** - * Storage Exception in case of invalid storage class - */ -class InvalidClassStorageException extends StorageException -{ -} diff --git a/src/Model/Storage/ReferenceStorageException.php b/src/Model/Storage/ReferenceStorageException.php deleted file mode 100644 index fcfd3ab59d..0000000000 --- a/src/Model/Storage/ReferenceStorageException.php +++ /dev/null @@ -1,29 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -/** - * Storage Exception in case of invalid references - */ -class ReferenceStorageException extends StorageException -{ -} diff --git a/src/Model/Storage/StorageException.php b/src/Model/Storage/StorageException.php deleted file mode 100644 index 34a09d57bc..0000000000 --- a/src/Model/Storage/StorageException.php +++ /dev/null @@ -1,31 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -use Exception; - -/** - * Storage Exception for unexpected failures - */ -class StorageException extends Exception -{ -} diff --git a/src/Model/Storage/SystemResource.php b/src/Model/Storage/SystemResource.php deleted file mode 100644 index 39bc0a0245..0000000000 --- a/src/Model/Storage/SystemResource.php +++ /dev/null @@ -1,73 +0,0 @@ -. - * - */ - -namespace Friendica\Model\Storage; - -/** - * System resource storage class - * - * This class is used to load system resources, like images. - * Is not intended to be selectable by admins as default storage class. - */ -class SystemResource implements IStorage -{ - const NAME = 'SystemResource'; - - // Valid folders to look for resources - const VALID_FOLDERS = ["images"]; - - /** - * @inheritDoc - */ - public function get(string $reference): string - { - $folder = dirname($reference); - if (!in_array($folder, self::VALID_FOLDERS)) { - throw new ReferenceStorageException(sprintf('System Resource is invalid for reference %s, no valid folder found', $reference)); - } - if (!file_exists($reference)) { - throw new StorageException(sprintf('System Resource is invalid for reference %s, the file doesn\'t exist', $reference)); - } - $content = file_get_contents($reference); - - if ($content === false) { - throw new StorageException(sprintf('Cannot get content for reference %s', $reference)); - } - - return $content; - } - - /** - * @inheritDoc - */ - public function __toString() - { - return self::NAME; - } - - /** - * @inheritDoc - */ - public static function getName(): string - { - return self::NAME; - } -} diff --git a/src/Module/Admin/Storage.php b/src/Module/Admin/Storage.php index 6b22d905cf..8eb706fa03 100644 --- a/src/Module/Admin/Storage.php +++ b/src/Module/Admin/Storage.php @@ -23,9 +23,9 @@ namespace Friendica\Module\Admin; use Friendica\Core\Renderer; use Friendica\DI; -use Friendica\Model\Storage\InvalidClassStorageException; -use Friendica\Model\Storage\IStorageConfiguration; -use Friendica\Model\Storage\IWritableStorage; +use Friendica\Core\Storage\Exception\InvalidClassStorageException; +use Friendica\Core\Storage\Capability\ICanConfigureStorage; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; use Friendica\Module\BaseAdmin; use Friendica\Util\Strings; @@ -40,7 +40,7 @@ class Storage extends BaseAdmin $storagebackend = Strings::escapeTags(trim($parameters['name'] ?? '')); try { - /** @var IStorageConfiguration|false $newStorageConfig */ + /** @var \Friendica\Core\Storage\Capability\ICanConfigureStorage|false $newStorageConfig */ $newStorageConfig = DI::storageManager()->getConfigurationByName($storagebackend); } catch (InvalidClassStorageException $storageException) { notice(DI::l10n()->t('Storage backend, %s is invalid.', $storagebackend)); @@ -78,7 +78,7 @@ class Storage extends BaseAdmin if (!empty($_POST['submit_save_set'])) { try { - /** @var IWritableStorage $newstorage */ + /** @var \Friendica\Core\Storage\Capability\ICanWriteToStorage $newstorage */ $newstorage = DI::storageManager()->getWritableStorageByName($storagebackend); if (!DI::storageManager()->setBackend($newstorage)) { @@ -129,7 +129,7 @@ class Storage extends BaseAdmin 'name' => $name, 'prefix' => $storage_form_prefix, 'form' => $storage_form, - 'active' => $current_storage_backend instanceof IWritableStorage && $name === $current_storage_backend::getName(), + 'active' => $current_storage_backend instanceof ICanWriteToStorage && $name === $current_storage_backend::getName(), ]; } @@ -147,7 +147,7 @@ class Storage extends BaseAdmin '$noconfig' => DI::l10n()->t('This backend doesn\'t have custom settings'), '$baseurl' => DI::baseUrl()->get(true), '$form_security_token' => self::getFormSecurityToken("admin_storage"), - '$storagebackend' => $current_storage_backend instanceof IWritableStorage ? $current_storage_backend::getName() : DI::l10n()->t('Database (legacy)'), + '$storagebackend' => $current_storage_backend instanceof ICanWriteToStorage ? $current_storage_backend::getName() : DI::l10n()->t('Database (legacy)'), '$availablestorageforms' => $available_storage_forms, ]); } diff --git a/src/Module/Photo.php b/src/Module/Photo.php index 0b03f4a799..db21803e6d 100644 --- a/src/Module/Photo.php +++ b/src/Module/Photo.php @@ -29,8 +29,8 @@ use Friendica\Model\Contact; use Friendica\Model\Photo as MPhoto; use Friendica\Model\Post; use Friendica\Model\Profile; -use Friendica\Model\Storage\ExternalResource; -use Friendica\Model\Storage\SystemResource; +use Friendica\Core\Storage\Type\ExternalResource; +use Friendica\Core\Storage\Type\SystemResource; use Friendica\Model\User; use Friendica\Network\HTTPException; use Friendica\Object\Image; diff --git a/static/dependencies.config.php b/static/dependencies.config.php index f4aacb340c..bbf8c5599c 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -42,10 +42,10 @@ use Friendica\Core\L10n; use Friendica\Core\Lock; use Friendica\Core\Process; use Friendica\Core\Session\Capability\IHandleSessions; -use Friendica\Core\StorageManager; +use Friendica\Core\Storage\Repository\StorageManager; use Friendica\Database\Database; use Friendica\Factory; -use Friendica\Model\Storage\IWritableStorage; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; use Friendica\Model\User\Cookie; use Friendica\Model\Log\ParsedLogIterator; use Friendica\Network; @@ -218,7 +218,7 @@ return [ $_SERVER, $_COOKIE ], ], - IWritableStorage::class => [ + ICanWriteToStorage::class => [ 'instanceOf' => StorageManager::class, 'call' => [ ['getBackend', [], Dice::CHAIN_CALL], diff --git a/tests/Util/SampleStorageBackend.php b/tests/Util/SampleStorageBackend.php index 1185a25646..03fca0171f 100644 --- a/tests/Util/SampleStorageBackend.php +++ b/tests/Util/SampleStorageBackend.php @@ -22,14 +22,14 @@ namespace Friendica\Test\Util; use Friendica\Core\Hook; -use Friendica\Model\Storage\IWritableStorage; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; use Friendica\Core\L10n; /** * A backend storage example class */ -class SampleStorageBackend implements IWritableStorage +class SampleStorageBackend implements ICanWriteToStorage { const NAME = 'Sample Storage'; @@ -102,7 +102,7 @@ class SampleStorageBackend implements IWritableStorage return $this->options; } - public function __toString() + public function __toString(): string { return self::NAME; } diff --git a/tests/src/Core/Storage/DatabaseStorageTest.php b/tests/src/Core/Storage/DatabaseStorageTest.php new file mode 100644 index 0000000000..b51f9fc2be --- /dev/null +++ b/tests/src/Core/Storage/DatabaseStorageTest.php @@ -0,0 +1,70 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Storage; + +use Friendica\Core\Config\Factory\Config; +use Friendica\Core\Storage\Type\Database; +use Friendica\Test\DatabaseTestTrait; +use Friendica\Test\Util\Database\StaticDatabase; +use Friendica\Test\Util\VFSTrait; +use Friendica\Util\Profiler; +use Psr\Log\NullLogger; + +class DatabaseStorageTest extends StorageTest +{ + use DatabaseTestTrait; + use VFSTrait; + + protected function setUp(): void + { + $this->setUpVfsDir(); + + $this->setUpDb(); + + parent::setUp(); + } + + protected function getInstance() + { + $logger = new NullLogger(); + $profiler = \Mockery::mock(Profiler::class); + $profiler->shouldReceive('startRecording'); + $profiler->shouldReceive('stopRecording'); + $profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true); + + // load real config to avoid mocking every config-entry which is related to the Database class + $configFactory = new Config(); + $loader = (new Config())->createConfigFileLoader($this->root->url(), []); + $configCache = $configFactory->createCache($loader); + + $dba = new StaticDatabase($configCache, $profiler, $logger); + + return new Database($dba); + } + + protected function tearDown(): void + { + $this->tearDownDb(); + + parent::tearDown(); + } +} diff --git a/tests/src/Core/Storage/FilesystemStorageConfigTest.php b/tests/src/Core/Storage/FilesystemStorageConfigTest.php new file mode 100644 index 0000000000..a48faed764 --- /dev/null +++ b/tests/src/Core/Storage/FilesystemStorageConfigTest.php @@ -0,0 +1,67 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Storage; + +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\L10n; +use Friendica\Core\Storage\Capability\ICanConfigureStorage; +use Friendica\Core\Storage\Type\FilesystemConfig; +use Friendica\Test\Util\VFSTrait; +use Mockery\MockInterface; +use org\bovigo\vfs\vfsStream; + +class FilesystemStorageConfigTest extends StorageConfigTest +{ + use VFSTrait; + + protected function setUp(): void + { + $this->setUpVfsDir(); + + vfsStream::create(['storage' => []], $this->root); + + parent::setUp(); + } + + protected function getInstance() + { + /** @var MockInterface|L10n $l10n */ + $l10n = \Mockery::mock(L10n::class)->makePartial(); + $config = \Mockery::mock(IManageConfigValues::class); + $config->shouldReceive('get') + ->with('storage', 'filesystem_path', FilesystemConfig::DEFAULT_BASE_FOLDER) + ->andReturn($this->root->getChild('storage')->url()); + + return new FilesystemConfig($config, $l10n); + } + + protected function assertOption(ICanConfigureStorage $storage) + { + self::assertEquals([ + 'storagepath' => [ + 'input', 'Storage base path', + $this->root->getChild('storage')->url(), + 'Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree' + ] + ], $storage->getOptions()); + } +} diff --git a/tests/src/Core/Storage/FilesystemStorageTest.php b/tests/src/Core/Storage/FilesystemStorageTest.php new file mode 100644 index 0000000000..c97b937553 --- /dev/null +++ b/tests/src/Core/Storage/FilesystemStorageTest.php @@ -0,0 +1,114 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Storage; + +use Friendica\Core\Storage\Exception\StorageException; +use Friendica\Core\Storage\Type\Filesystem; +use Friendica\Core\Storage\Type\FilesystemConfig; +use Friendica\Test\Util\VFSTrait; +use org\bovigo\vfs\vfsStream; + +class FilesystemStorageTest extends StorageTest +{ + use VFSTrait; + + protected function setUp(): void + { + $this->setUpVfsDir(); + + vfsStream::create(['storage' => []], $this->root); + + parent::setUp(); + } + + protected function getInstance() + { + return new Filesystem($this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->url()); + } + + /** + * Test the exception in case of missing directory permissions during put new files + */ + public function testMissingDirPermissionsDuringPut() + { + $this->expectException(StorageException::class); + $this->expectExceptionMessageMatches("/Filesystem storage failed to create \".*\". Check you write permissions./"); + $this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->chmod(0777); + + $instance = $this->getInstance(); + + $this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->chmod(0000); + $instance->put('test'); + } + + /** + * Test the exception in case the directory isn't writeable + */ + public function testMissingDirPermissions() + { + $this->expectException(StorageException::class); + $this->expectExceptionMessageMatches("/Path \".*\" does not exist or is not writeable./"); + $this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->chmod(0000); + + $this->getInstance(); + } + + /** + * Test the exception in case of missing file permissions + * + */ + public function testMissingFilePermissions() + { + static::markTestIncomplete("Cannot catch file_put_content() error due vfsStream failure"); + + $this->expectException(StorageException::class); + $this->expectExceptionMessageMatches("/Filesystem storage failed to save data to \".*\". Check your write permissions/"); + + vfsStream::create(['storage' => ['f0' => ['c0' => ['k0i0' => '']]]], $this->root); + + $this->root->getChild('storage/f0/c0/k0i0')->chmod(000); + + $instance = $this->getInstance(); + $instance->put('test', 'f0c0k0i0'); + } + + /** + * Test the backend storage of the Filesystem Storage class + */ + public function testDirectoryTree() + { + $instance = $this->getInstance(); + + $instance->put('test', 'f0c0d0i0'); + + $dir = $this->root->getChild('storage/f0/c0')->url(); + $file = $this->root->getChild('storage/f0/c0/d0i0')->url(); + + self::assertDirectoryExists($dir); + self::assertFileExists($file); + + self::assertDirectoryIsWritable($dir); + self::assertFileIsWritable($file); + + self::assertEquals('test', file_get_contents($file)); + } +} diff --git a/tests/src/Core/Storage/Repository/StorageManagerTest.php b/tests/src/Core/Storage/Repository/StorageManagerTest.php new file mode 100644 index 0000000000..c7b6569433 --- /dev/null +++ b/tests/src/Core/Storage/Repository/StorageManagerTest.php @@ -0,0 +1,358 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Storage\Repository; + +use Dice\Dice; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\Config\Type\PreloadConfig; +use Friendica\Core\Hook; +use Friendica\Core\L10n; +use Friendica\Core\Session\Capability\IHandleSessions; +use Friendica\Core\Session\Type\Memory; +use Friendica\Core\Storage\Exception\InvalidClassStorageException; +use Friendica\Core\Storage\Capability\ICanReadFromStorage; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; +use Friendica\Core\Storage\Repository\StorageManager; +use Friendica\Core\Storage\Type\Filesystem; +use Friendica\Core\Storage\Type\SystemResource; +use Friendica\Database\Database; +use Friendica\DI; +use Friendica\Core\Config\Factory\Config; +use Friendica\Core\Config\Repository; +use Friendica\Core\Storage\Type; +use Friendica\Network\HTTPClient; +use Friendica\Test\DatabaseTest; +use Friendica\Test\Util\Database\StaticDatabase; +use Friendica\Test\Util\VFSTrait; +use Friendica\Util\Profiler; +use org\bovigo\vfs\vfsStream; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Friendica\Test\Util\SampleStorageBackend; + +class StorageManagerTest extends DatabaseTest +{ + use VFSTrait; + /** @var Database */ + private $dba; + /** @var IManageConfigValues */ + private $config; + /** @var LoggerInterface */ + private $logger; + /** @var L10n */ + private $l10n; + /** @var HTTPClient */ + private $httpRequest; + + protected function setUp(): void + { + parent::setUp(); + + $this->setUpVfsDir(); + + vfsStream::newDirectory(Type\FilesystemConfig::DEFAULT_BASE_FOLDER, 0777)->at($this->root); + + $this->logger = new NullLogger(); + + $profiler = \Mockery::mock(Profiler::class); + $profiler->shouldReceive('startRecording'); + $profiler->shouldReceive('stopRecording'); + $profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true); + + // load real config to avoid mocking every config-entry which is related to the Database class + $configFactory = new Config(); + $loader = $configFactory->createConfigFileLoader($this->root->url(), []); + $configCache = $configFactory->createCache($loader); + + $this->dba = new StaticDatabase($configCache, $profiler, $this->logger); + + $configModel = new Repository\Config($this->dba); + $this->config = new PreloadConfig($configCache, $configModel); + $this->config->set('storage', 'name', 'Database'); + $this->config->set('storage', 'filesystem_path', $this->root->getChild(Type\FilesystemConfig::DEFAULT_BASE_FOLDER)->url()); + + $this->l10n = \Mockery::mock(L10n::class); + + $this->httpRequest = \Mockery::mock(HTTPClient::class); + } + + protected function tearDown(): void + { + $this->root->removeChild(Type\FilesystemConfig::DEFAULT_BASE_FOLDER); + + parent::tearDown(); + } + + /** + * Test plain instancing first + */ + public function testInstance() + { + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); + + self::assertInstanceOf(StorageManager::class, $storageManager); + } + + public function dataStorages() + { + return [ + 'empty' => [ + 'name' => '', + 'valid' => false, + 'interface' => ICanReadFromStorage::class, + 'assert' => null, + 'assertName' => '', + ], + 'database' => [ + 'name' => Type\Database::NAME, + 'valid' => true, + 'interface' => ICanWriteToStorage::class, + 'assert' => Type\Database::class, + 'assertName' => Type\Database::NAME, + ], + 'filesystem' => [ + 'name' => Filesystem::NAME, + 'valid' => true, + 'interface' => ICanWriteToStorage::class, + 'assert' => Filesystem::class, + 'assertName' => Filesystem::NAME, + ], + 'systemresource' => [ + 'name' => SystemResource::NAME, + 'valid' => true, + 'interface' => ICanReadFromStorage::class, + 'assert' => SystemResource::class, + 'assertName' => SystemResource::NAME, + ], + 'invalid' => [ + 'name' => 'invalid', + 'valid' => false, + 'interface' => null, + 'assert' => null, + 'assertName' => '', + 'userBackend' => false, + ], + ]; + } + + /** + * Test the getByName() method + * + * @dataProvider dataStorages + */ + public function testGetByName($name, $valid, $interface, $assert, $assertName) + { + if (!$valid) { + $this->expectException(InvalidClassStorageException::class); + } + + if ($interface === ICanWriteToStorage::class) { + $this->config->set('storage', 'name', $name); + } + + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + + if ($interface === ICanWriteToStorage::class) { + $storage = $storageManager->getWritableStorageByName($name); + } else { + $storage = $storageManager->getByName($name); + } + + self::assertInstanceOf($interface, $storage); + self::assertInstanceOf($assert, $storage); + self::assertEquals($assertName, $storage); + } + + /** + * Test the isValidBackend() method + * + * @dataProvider dataStorages + */ + public function testIsValidBackend($name, $valid, $interface, $assert, $assertName) + { + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + + // true in every of the backends + self::assertEquals(!empty($assertName), $storageManager->isValidBackend($name)); + + // if it's a ICanWriteToStorage, the valid backend should return true, otherwise false + self::assertEquals($interface === ICanWriteToStorage::class, $storageManager->isValidBackend($name, StorageManager::DEFAULT_BACKENDS)); + } + + /** + * Test the method listBackends() with default setting + */ + public function testListBackends() + { + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + + self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends()); + } + + /** + * Test the method getBackend() + * + * @dataProvider dataStorages + */ + public function testGetBackend($name, $valid, $interface, $assert, $assertName) + { + if ($interface !== ICanWriteToStorage::class) { + static::markTestSkipped('only works for ICanWriteToStorage'); + } + + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + + $selBackend = $storageManager->getWritableStorageByName($name); + $storageManager->setBackend($selBackend); + + self::assertInstanceOf($assert, $storageManager->getBackend()); + } + + /** + * Test the method getBackend() with a pre-configured backend + * + * @dataProvider dataStorages + */ + public function testPresetBackend($name, $valid, $interface, $assert, $assertName) + { + $this->config->set('storage', 'name', $name); + if ($interface !== ICanWriteToStorage::class) { + $this->expectException(InvalidClassStorageException::class); + } + + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + + self::assertInstanceOf($assert, $storageManager->getBackend()); + } + + /** + * Tests the register and unregister methods for a new backend storage class + * + * Uses a sample storage for testing + * + * @see SampleStorageBackend + */ + public function testRegisterUnregisterBackends() + { + /// @todo Remove dice once "Hook" is dynamic and mockable + $dice = (new Dice()) + ->addRules(include __DIR__ . '/../../../../../static/dependencies.config.php') + ->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]) + ->addRule(IHandleSessions::class, ['instanceOf' => Session\Type\Memory::class, 'shared' => true, 'call' => null]); + DI::init($dice); + + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + + self::assertTrue($storageManager->register(SampleStorageBackend::class)); + + self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ + SampleStorageBackend::getName(), + ]), $storageManager->listBackends()); + self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ + SampleStorageBackend::getName() + ]), $this->config->get('storage', 'backends')); + + self::assertTrue($storageManager->unregister(SampleStorageBackend::class)); + self::assertEquals(StorageManager::DEFAULT_BACKENDS, $this->config->get('storage', 'backends')); + self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends()); + } + + /** + * tests that an active backend cannot get unregistered + */ + public function testUnregisterActiveBackend() + { + /// @todo Remove dice once "Hook" is dynamic and mockable + $dice = (new Dice()) + ->addRules(include __DIR__ . '/../../../../../static/dependencies.config.php') + ->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]) + ->addRule(IHandleSessions::class, ['instanceOf' => Memory::class, 'shared' => true, 'call' => null]); + DI::init($dice); + + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + + self::assertTrue($storageManager->register(SampleStorageBackend::class)); + + self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ + SampleStorageBackend::getName(), + ]), $storageManager->listBackends()); + self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ + SampleStorageBackend::getName() + ]), $this->config->get('storage', 'backends')); + + // inline call to register own class as hook (testing purpose only) + SampleStorageBackend::registerHook(); + Hook::loadHooks(); + + self::assertTrue($storageManager->setBackend($storageManager->getWritableStorageByName(SampleStorageBackend::NAME))); + self::assertEquals(SampleStorageBackend::NAME, $this->config->get('storage', 'name')); + + self::assertInstanceOf(SampleStorageBackend::class, $storageManager->getBackend()); + + self::expectException(\Friendica\Core\Storage\Exception\StorageException::class); + self::expectExceptionMessage('Cannot unregister Sample Storage, because it\'s currently active.'); + + $storageManager->unregister(SampleStorageBackend::class); + } + + /** + * Test moving data to a new storage (currently testing db & filesystem) + * + * @dataProvider dataStorages + */ + public function testMoveStorage($name, $valid, $interface, $assert, $assertName) + { + if ($interface !== ICanWriteToStorage::class) { + self::markTestSkipped("No user backend"); + } + + $this->loadFixture(__DIR__ . '/../../../../datasets/storage/database.fixture.php', $this->dba); + + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + $storage = $storageManager->getWritableStorageByName($name); + $storageManager->move($storage); + + $photos = $this->dba->select('photo', ['backend-ref', 'backend-class', 'id', 'data']); + + while ($photo = $this->dba->fetch($photos)) { + self::assertEmpty($photo['data']); + + $storage = $storageManager->getByName($photo['backend-class']); + $data = $storage->get($photo['backend-ref']); + + self::assertNotEmpty($data); + } + } + + /** + * Test moving data to a WRONG storage + */ + public function testWrongWritableStorage() + { + $this->expectException(InvalidClassStorageException::class); + $this->expectExceptionMessage('Backend SystemResource is not valid'); + + $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); + $storage = $storageManager->getWritableStorageByName(SystemResource::getName()); + $storageManager->move($storage); + } +} diff --git a/tests/src/Core/Storage/StorageConfigTest.php b/tests/src/Core/Storage/StorageConfigTest.php new file mode 100644 index 0000000000..a01be343d2 --- /dev/null +++ b/tests/src/Core/Storage/StorageConfigTest.php @@ -0,0 +1,43 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Storage; + +use Friendica\Core\Storage\Capability\ICanConfigureStorage; +use Friendica\Test\MockedTest; + +abstract class StorageConfigTest extends MockedTest +{ + /** @return \Friendica\Core\Storage\Capability\ICanConfigureStorage */ + abstract protected function getInstance(); + + abstract protected function assertOption(\Friendica\Core\Storage\Capability\ICanConfigureStorage $storage); + + /** + * Test if the "getOption" is asserted + */ + public function testGetOptions() + { + $instance = $this->getInstance(); + + $this->assertOption($instance); + } +} diff --git a/tests/src/Core/Storage/StorageTest.php b/tests/src/Core/Storage/StorageTest.php new file mode 100644 index 0000000000..71377a9556 --- /dev/null +++ b/tests/src/Core/Storage/StorageTest.php @@ -0,0 +1,107 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Storage; + +use Friendica\Core\Storage\Capability\ICanReadFromStorage; +use Friendica\Core\Storage\Capability\ICanWriteToStorage; +use Friendica\Core\Storage\Exception\ReferenceStorageException; +use Friendica\Test\MockedTest; + +abstract class StorageTest extends MockedTest +{ + /** @return ICanWriteToStorage */ + abstract protected function getInstance(); + + /** + * Test if the instance is "really" implementing the interface + */ + public function testInstance() + { + $instance = $this->getInstance(); + self::assertInstanceOf(ICanReadFromStorage::class, $instance); + } + + /** + * Test basic put, get and delete operations + */ + public function testPutGetDelete() + { + $instance = $this->getInstance(); + + $ref = $instance->put('data12345'); + self::assertNotEmpty($ref); + + self::assertEquals('data12345', $instance->get($ref)); + + $instance->delete($ref); + } + + /** + * Test a delete with an invalid reference + */ + public function testInvalidDelete() + { + self::expectException(ReferenceStorageException::class); + + $instance = $this->getInstance(); + + $instance->delete(-1234456); + } + + /** + * Test a get with an invalid reference + */ + public function testInvalidGet() + { + self::expectException(ReferenceStorageException::class); + + $instance = $this->getInstance(); + + $instance->get(-123456); + } + + /** + * Test an update with a given reference + */ + public function testUpdateReference() + { + $instance = $this->getInstance(); + + $ref = $instance->put('data12345'); + self::assertNotEmpty($ref); + + self::assertEquals('data12345', $instance->get($ref)); + + self::assertEquals($ref, $instance->put('data5432', $ref)); + self::assertEquals('data5432', $instance->get($ref)); + } + + /** + * Test that an invalid update results in an insert + */ + public function testInvalidUpdate() + { + $instance = $this->getInstance(); + + self::assertEquals(-123, $instance->put('data12345', -123)); + } +} diff --git a/tests/src/Core/StorageManagerTest.php b/tests/src/Core/StorageManagerTest.php deleted file mode 100644 index f9d05e6374..0000000000 --- a/tests/src/Core/StorageManagerTest.php +++ /dev/null @@ -1,353 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Core; - -use Dice\Dice; -use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\Config\Type\PreloadConfig; -use Friendica\Core\Hook; -use Friendica\Core\L10n; -use Friendica\Core\Session\Capability\IHandleSessions; -use Friendica\Core\Session\Type\Memory; -use Friendica\Core\StorageManager; -use Friendica\Database\Database; -use Friendica\DI; -use Friendica\Core\Config\Factory\Config; -use Friendica\Core\Config\Repository; -use Friendica\Model\Storage; -use Friendica\Network\HTTPClient; -use Friendica\Test\DatabaseTest; -use Friendica\Test\Util\Database\StaticDatabase; -use Friendica\Test\Util\VFSTrait; -use Friendica\Util\Profiler; -use org\bovigo\vfs\vfsStream; -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; -use Friendica\Test\Util\SampleStorageBackend; - -class StorageManagerTest extends DatabaseTest -{ - use VFSTrait; - /** @var Database */ - private $dba; - /** @var IManageConfigValues */ - private $config; - /** @var LoggerInterface */ - private $logger; - /** @var L10n */ - private $l10n; - /** @var HTTPClient */ - private $httpRequest; - - protected function setUp(): void - { - parent::setUp(); - - $this->setUpVfsDir(); - - vfsStream::newDirectory(Storage\FilesystemConfig::DEFAULT_BASE_FOLDER, 0777)->at($this->root); - - $this->logger = new NullLogger(); - - $profiler = \Mockery::mock(Profiler::class); - $profiler->shouldReceive('startRecording'); - $profiler->shouldReceive('stopRecording'); - $profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true); - - // load real config to avoid mocking every config-entry which is related to the Database class - $configFactory = new Config(); - $loader = $configFactory->createConfigFileLoader($this->root->url(), []); - $configCache = $configFactory->createCache($loader); - - $this->dba = new StaticDatabase($configCache, $profiler, $this->logger); - - $configModel = new Repository\Config($this->dba); - $this->config = new PreloadConfig($configCache, $configModel); - $this->config->set('storage', 'name', 'Database'); - $this->config->set('storage', 'filesystem_path', $this->root->getChild(Storage\FilesystemConfig::DEFAULT_BASE_FOLDER)->url()); - - $this->l10n = \Mockery::mock(L10n::class); - - $this->httpRequest = \Mockery::mock(HTTPClient::class); - } - - protected function tearDown(): void - { - $this->root->removeChild(Storage\FilesystemConfig::DEFAULT_BASE_FOLDER); - - parent::tearDown(); - } - - /** - * Test plain instancing first - */ - public function testInstance() - { - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); - - self::assertInstanceOf(StorageManager::class, $storageManager); - } - - public function dataStorages() - { - return [ - 'empty' => [ - 'name' => '', - 'valid' => false, - 'interface' => Storage\IStorage::class, - 'assert' => null, - 'assertName' => '', - ], - 'database' => [ - 'name' => Storage\Database::NAME, - 'valid' => true, - 'interface' => Storage\IWritableStorage::class, - 'assert' => Storage\Database::class, - 'assertName' => Storage\Database::NAME, - ], - 'filesystem' => [ - 'name' => Storage\Filesystem::NAME, - 'valid' => true, - 'interface' => Storage\IWritableStorage::class, - 'assert' => Storage\Filesystem::class, - 'assertName' => Storage\Filesystem::NAME, - ], - 'systemresource' => [ - 'name' => Storage\SystemResource::NAME, - 'valid' => true, - 'interface' => Storage\IStorage::class, - 'assert' => Storage\SystemResource::class, - 'assertName' => Storage\SystemResource::NAME, - ], - 'invalid' => [ - 'name' => 'invalid', - 'valid' => false, - 'interface' => null, - 'assert' => null, - 'assertName' => '', - 'userBackend' => false, - ], - ]; - } - - /** - * Test the getByName() method - * - * @dataProvider dataStorages - */ - public function testGetByName($name, $valid, $interface, $assert, $assertName) - { - if (!$valid) { - $this->expectException(Storage\InvalidClassStorageException::class); - } - - if ($interface === Storage\IWritableStorage::class) { - $this->config->set('storage', 'name', $name); - } - - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - - if ($interface === Storage\IWritableStorage::class) { - $storage = $storageManager->getWritableStorageByName($name); - } else { - $storage = $storageManager->getByName($name); - } - - self::assertInstanceOf($interface, $storage); - self::assertInstanceOf($assert, $storage); - self::assertEquals($assertName, $storage); - } - - /** - * Test the isValidBackend() method - * - * @dataProvider dataStorages - */ - public function testIsValidBackend($name, $valid, $interface, $assert, $assertName) - { - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - - // true in every of the backends - self::assertEquals(!empty($assertName), $storageManager->isValidBackend($name)); - - // if it's a IWritableStorage, the valid backend should return true, otherwise false - self::assertEquals($interface === Storage\IWritableStorage::class, $storageManager->isValidBackend($name, StorageManager::DEFAULT_BACKENDS)); - } - - /** - * Test the method listBackends() with default setting - */ - public function testListBackends() - { - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - - self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends()); - } - - /** - * Test the method getBackend() - * - * @dataProvider dataStorages - */ - public function testGetBackend($name, $valid, $interface, $assert, $assertName) - { - if ($interface !== Storage\IWritableStorage::class) { - static::markTestSkipped('only works for IWritableStorage'); - } - - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - - $selBackend = $storageManager->getWritableStorageByName($name); - $storageManager->setBackend($selBackend); - - self::assertInstanceOf($assert, $storageManager->getBackend()); - } - - /** - * Test the method getBackend() with a pre-configured backend - * - * @dataProvider dataStorages - */ - public function testPresetBackend($name, $valid, $interface, $assert, $assertName) - { - $this->config->set('storage', 'name', $name); - if ($interface !== Storage\IWritableStorage::class) { - $this->expectException(Storage\InvalidClassStorageException::class); - } - - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - - self::assertInstanceOf($assert, $storageManager->getBackend()); - } - - /** - * Tests the register and unregister methods for a new backend storage class - * - * Uses a sample storage for testing - * - * @see SampleStorageBackend - */ - public function testRegisterUnregisterBackends() - { - /// @todo Remove dice once "Hook" is dynamic and mockable - $dice = (new Dice()) - ->addRules(include __DIR__ . '/../../../static/dependencies.config.php') - ->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]) - ->addRule(IHandleSessions::class, ['instanceOf' => Session\Type\Memory::class, 'shared' => true, 'call' => null]); - DI::init($dice); - - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - - self::assertTrue($storageManager->register(SampleStorageBackend::class)); - - self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ - SampleStorageBackend::getName(), - ]), $storageManager->listBackends()); - self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ - SampleStorageBackend::getName() - ]), $this->config->get('storage', 'backends')); - - self::assertTrue($storageManager->unregister(SampleStorageBackend::class)); - self::assertEquals(StorageManager::DEFAULT_BACKENDS, $this->config->get('storage', 'backends')); - self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends()); - } - - /** - * tests that an active backend cannot get unregistered - */ - public function testUnregisterActiveBackend() - { - /// @todo Remove dice once "Hook" is dynamic and mockable - $dice = (new Dice()) - ->addRules(include __DIR__ . '/../../../static/dependencies.config.php') - ->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]) - ->addRule(IHandleSessions::class, ['instanceOf' => Memory::class, 'shared' => true, 'call' => null]); - DI::init($dice); - - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - - self::assertTrue($storageManager->register(SampleStorageBackend::class)); - - self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ - SampleStorageBackend::getName(), - ]), $storageManager->listBackends()); - self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ - SampleStorageBackend::getName() - ]), $this->config->get('storage', 'backends')); - - // inline call to register own class as hook (testing purpose only) - SampleStorageBackend::registerHook(); - Hook::loadHooks(); - - self::assertTrue($storageManager->setBackend($storageManager->getWritableStorageByName(SampleStorageBackend::NAME))); - self::assertEquals(SampleStorageBackend::NAME, $this->config->get('storage', 'name')); - - self::assertInstanceOf(SampleStorageBackend::class, $storageManager->getBackend()); - - self::expectException(Storage\StorageException::class); - self::expectExceptionMessage('Cannot unregister Sample Storage, because it\'s currently active.'); - - $storageManager->unregister(SampleStorageBackend::class); - } - - /** - * Test moving data to a new storage (currently testing db & filesystem) - * - * @dataProvider dataStorages - */ - public function testMoveStorage($name, $valid, $interface, $assert, $assertName) - { - if ($interface !== Storage\IWritableStorage::class) { - self::markTestSkipped("No user backend"); - } - - $this->loadFixture(__DIR__ . '/../../datasets/storage/database.fixture.php', $this->dba); - - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - $storage = $storageManager->getWritableStorageByName($name); - $storageManager->move($storage); - - $photos = $this->dba->select('photo', ['backend-ref', 'backend-class', 'id', 'data']); - - while ($photo = $this->dba->fetch($photos)) { - self::assertEmpty($photo['data']); - - $storage = $storageManager->getByName($photo['backend-class']); - $data = $storage->get($photo['backend-ref']); - - self::assertNotEmpty($data); - } - } - - /** - * Test moving data to a WRONG storage - */ - public function testWrongWritableStorage() - { - $this->expectException(Storage\InvalidClassStorageException::class); - $this->expectExceptionMessage('Backend SystemResource is not valid'); - - $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n); - $storage = $storageManager->getWritableStorageByName(Storage\SystemResource::getName()); - $storageManager->move($storage); - } -} diff --git a/tests/src/Model/Storage/DatabaseStorageTest.php b/tests/src/Model/Storage/DatabaseStorageTest.php deleted file mode 100644 index 796b8937c0..0000000000 --- a/tests/src/Model/Storage/DatabaseStorageTest.php +++ /dev/null @@ -1,70 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Model\Storage; - -use Friendica\Core\Config\Factory\Config; -use Friendica\Model\Storage\Database; -use Friendica\Test\DatabaseTestTrait; -use Friendica\Test\Util\Database\StaticDatabase; -use Friendica\Test\Util\VFSTrait; -use Friendica\Util\Profiler; -use Psr\Log\NullLogger; - -class DatabaseStorageTest extends StorageTest -{ - use DatabaseTestTrait; - use VFSTrait; - - protected function setUp(): void - { - $this->setUpVfsDir(); - - $this->setUpDb(); - - parent::setUp(); - } - - protected function getInstance() - { - $logger = new NullLogger(); - $profiler = \Mockery::mock(Profiler::class); - $profiler->shouldReceive('startRecording'); - $profiler->shouldReceive('stopRecording'); - $profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true); - - // load real config to avoid mocking every config-entry which is related to the Database class - $configFactory = new Config(); - $loader = (new Config())->createConfigFileLoader($this->root->url(), []); - $configCache = $configFactory->createCache($loader); - - $dba = new StaticDatabase($configCache, $profiler, $logger); - - return new Database($dba); - } - - protected function tearDown(): void - { - $this->tearDownDb(); - - parent::tearDown(); - } -} diff --git a/tests/src/Model/Storage/FilesystemStorageConfigTest.php b/tests/src/Model/Storage/FilesystemStorageConfigTest.php deleted file mode 100644 index a5989b1cd6..0000000000 --- a/tests/src/Model/Storage/FilesystemStorageConfigTest.php +++ /dev/null @@ -1,67 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Model\Storage; - -use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\L10n; -use Friendica\Model\Storage\FilesystemConfig; -use Friendica\Model\Storage\IStorageConfiguration; -use Friendica\Test\Util\VFSTrait; -use Mockery\MockInterface; -use org\bovigo\vfs\vfsStream; - -class FilesystemStorageConfigTest extends StorageConfigTest -{ - use VFSTrait; - - protected function setUp(): void - { - $this->setUpVfsDir(); - - vfsStream::create(['storage' => []], $this->root); - - parent::setUp(); - } - - protected function getInstance() - { - /** @var MockInterface|L10n $l10n */ - $l10n = \Mockery::mock(L10n::class)->makePartial(); - $config = \Mockery::mock(IManageConfigValues::class); - $config->shouldReceive('get') - ->with('storage', 'filesystem_path', FilesystemConfig::DEFAULT_BASE_FOLDER) - ->andReturn($this->root->getChild('storage')->url()); - - return new FilesystemConfig($config, $l10n); - } - - protected function assertOption(IStorageConfiguration $storage) - { - self::assertEquals([ - 'storagepath' => [ - 'input', 'Storage base path', - $this->root->getChild('storage')->url(), - 'Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree' - ] - ], $storage->getOptions()); - } -} diff --git a/tests/src/Model/Storage/FilesystemStorageTest.php b/tests/src/Model/Storage/FilesystemStorageTest.php deleted file mode 100644 index 837c166819..0000000000 --- a/tests/src/Model/Storage/FilesystemStorageTest.php +++ /dev/null @@ -1,114 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Model\Storage; - -use Friendica\Model\Storage\Filesystem; -use Friendica\Model\Storage\FilesystemConfig; -use Friendica\Model\Storage\StorageException; -use Friendica\Test\Util\VFSTrait; -use org\bovigo\vfs\vfsStream; - -class FilesystemStorageTest extends StorageTest -{ - use VFSTrait; - - protected function setUp(): void - { - $this->setUpVfsDir(); - - vfsStream::create(['storage' => []], $this->root); - - parent::setUp(); - } - - protected function getInstance() - { - return new Filesystem($this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->url()); - } - - /** - * Test the exception in case of missing directory permissions during put new files - */ - public function testMissingDirPermissionsDuringPut() - { - $this->expectException(StorageException::class); - $this->expectExceptionMessageMatches("/Filesystem storage failed to create \".*\". Check you write permissions./"); - $this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->chmod(0777); - - $instance = $this->getInstance(); - - $this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->chmod(0000); - $instance->put('test'); - } - - /** - * Test the exception in case the directory isn't writeable - */ - public function testMissingDirPermissions() - { - $this->expectException(StorageException::class); - $this->expectExceptionMessageMatches("/Path \".*\" does not exist or is not writeable./"); - $this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->chmod(0000); - - $this->getInstance(); - } - - /** - * Test the exception in case of missing file permissions - * - */ - public function testMissingFilePermissions() - { - static::markTestIncomplete("Cannot catch file_put_content() error due vfsStream failure"); - - $this->expectException(StorageException::class); - $this->expectExceptionMessageMatches("/Filesystem storage failed to save data to \".*\". Check your write permissions/"); - - vfsStream::create(['storage' => ['f0' => ['c0' => ['k0i0' => '']]]], $this->root); - - $this->root->getChild('storage/f0/c0/k0i0')->chmod(000); - - $instance = $this->getInstance(); - $instance->put('test', 'f0c0k0i0'); - } - - /** - * Test the backend storage of the Filesystem Storage class - */ - public function testDirectoryTree() - { - $instance = $this->getInstance(); - - $instance->put('test', 'f0c0d0i0'); - - $dir = $this->root->getChild('storage/f0/c0')->url(); - $file = $this->root->getChild('storage/f0/c0/d0i0')->url(); - - self::assertDirectoryExists($dir); - self::assertFileExists($file); - - self::assertDirectoryIsWritable($dir); - self::assertFileIsWritable($file); - - self::assertEquals('test', file_get_contents($file)); - } -} diff --git a/tests/src/Model/Storage/StorageConfigTest.php b/tests/src/Model/Storage/StorageConfigTest.php deleted file mode 100644 index 80cbbaeb0a..0000000000 --- a/tests/src/Model/Storage/StorageConfigTest.php +++ /dev/null @@ -1,43 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Model\Storage; - -use Friendica\Model\Storage\IStorageConfiguration; -use Friendica\Test\MockedTest; - -abstract class StorageConfigTest extends MockedTest -{ - /** @return IStorageConfiguration */ - abstract protected function getInstance(); - - abstract protected function assertOption(IStorageConfiguration $storage); - - /** - * Test if the "getOption" is asserted - */ - public function testGetOptions() - { - $instance = $this->getInstance(); - - $this->assertOption($instance); - } -} diff --git a/tests/src/Model/Storage/StorageTest.php b/tests/src/Model/Storage/StorageTest.php deleted file mode 100644 index 7433747575..0000000000 --- a/tests/src/Model/Storage/StorageTest.php +++ /dev/null @@ -1,107 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Model\Storage; - -use Friendica\Model\Storage\IWritableStorage; -use Friendica\Model\Storage\IStorage; -use Friendica\Model\Storage\ReferenceStorageException; -use Friendica\Test\MockedTest; - -abstract class StorageTest extends MockedTest -{ - /** @return IWritableStorage */ - abstract protected function getInstance(); - - /** - * Test if the instance is "really" implementing the interface - */ - public function testInstance() - { - $instance = $this->getInstance(); - self::assertInstanceOf(IStorage::class, $instance); - } - - /** - * Test basic put, get and delete operations - */ - public function testPutGetDelete() - { - $instance = $this->getInstance(); - - $ref = $instance->put('data12345'); - self::assertNotEmpty($ref); - - self::assertEquals('data12345', $instance->get($ref)); - - $instance->delete($ref); - } - - /** - * Test a delete with an invalid reference - */ - public function testInvalidDelete() - { - self::expectException(ReferenceStorageException::class); - - $instance = $this->getInstance(); - - $instance->delete(-1234456); - } - - /** - * Test a get with an invalid reference - */ - public function testInvalidGet() - { - self::expectException(ReferenceStorageException::class); - - $instance = $this->getInstance(); - - $instance->get(-123456); - } - - /** - * Test an update with a given reference - */ - public function testUpdateReference() - { - $instance = $this->getInstance(); - - $ref = $instance->put('data12345'); - self::assertNotEmpty($ref); - - self::assertEquals('data12345', $instance->get($ref)); - - self::assertEquals($ref, $instance->put('data5432', $ref)); - self::assertEquals('data5432', $instance->get($ref)); - } - - /** - * Test that an invalid update results in an insert - */ - public function testInvalidUpdate() - { - $instance = $this->getInstance(); - - self::assertEquals(-123, $instance->put('data12345', -123)); - } -} diff --git a/update.php b/update.php index a2999ed5a5..368b70b228 100644 --- a/update.php +++ b/update.php @@ -41,6 +41,7 @@ */ use Friendica\Core\Logger; +use Friendica\Core\Storage\Capability\ICanReadFromStorage; use Friendica\Core\Update; use Friendica\Core\Worker; use Friendica\Database\Database; @@ -54,7 +55,6 @@ use Friendica\Model\Notification; use Friendica\Model\Photo; use Friendica\Model\Post; use Friendica\Model\Profile; -use Friendica\Model\Storage; use Friendica\Security\PermissionSet\Repository\PermissionSet; use Friendica\Worker\Delivery; @@ -183,7 +183,7 @@ function update_1330() // set the name of the storage instead of the classpath as config if (!empty($currStorage)) { - /** @var Storage\IStorage $currStorage */ + /** @var ICanReadFromStorage $currStorage */ if (!DI::config()->set('storage', 'name', $currStorage::getName())) { return Update::FAILED; } @@ -989,7 +989,7 @@ function update_1434() // in case of an empty config, set "Database" as default storage backend if (empty($name)) { - DI::config()->set('storage', 'name', Storage\Database::getName()); + DI::config()->set('storage', 'name', \Friendica\Core\Storage\Type\Database::getName()); } // In case of a Using deprecated storage class value, set the right name for it