Merge pull request #10825 from nupplaphil/feat/Storage_refactor

Storage refactoring
pull/10835/head
Hypolite Petovan 2021-10-05 14:41:42 -04:00 committed by GitHub
commit 2ff89ba9d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 520 additions and 248 deletions

View File

@ -10,18 +10,20 @@ A storage backend is implemented as a class, and the plugin register the class t
The class must live in `Friendica\Addon\youraddonname` namespace, where `youraddonname` the folder name of your addon. The class must live in `Friendica\Addon\youraddonname` namespace, where `youraddonname` the folder name of your addon.
There are two different interfaces you need to implement.
### `IWritableStorage`
The class must implement `Friendica\Model\Storage\IWritableStorage` interface. All method in the interface must be implemented: The class must implement `Friendica\Model\Storage\IWritableStorage` interface. All method in the interface must be implemented:
namespace Friendica\Model\IWritableStorage;
```php ```php
namespace Friendica\Model\Storage\IWritableStorage;
interface IWritableStorage interface IWritableStorage
{ {
public function get(string $reference); public function get(string $reference);
public function put(string $data, string $reference = ''); public function put(string $data, string $reference = '');
public function delete(string $reference); public function delete(string $reference);
public function getOptions();
public function saveOptions(array $data);
public function __toString(); public function __toString();
public static function getName(); public static function getName();
} }
@ -31,7 +33,22 @@ interface IWritableStorage
- `put(string $data, string $reference)` saves data in `$data` to position `$reference`, or a new position if `$reference` is empty. - `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` - `delete(string $reference)` delete data pointed by `$reference`
### `IStorageConfiguration`
Each storage backend can have options the admin can set in admin page. 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.
All methods in the interface must be implemented:
```php
namespace Friendica\Model\Storage\IStorageConfiguration;
interface IStorageConfiguration
{
public function getOptions();
public function saveOptions(array $data);
}
```
- `getOptions()` returns an array with details about each option to build the interface. - `getOptions()` returns an array with details about each option to build the interface.
- `saveOptions(array $data)` get `$data` from admin page, validate it and save it. - `saveOptions(array $data)` get `$data` from admin page, validate it and save it.
@ -156,7 +173,7 @@ class ExampleStorage implements IWritableStorage
## Example ## Example
Here an hypotetical addon which register a useless storage backend. Here is a hypothetical addon which register a useless storage backend.
Let's call it `samplestorage`. Let's call it `samplestorage`.
This backend will discard all data we try to save and will return always the same image when we ask for some data. This backend will discard all data we try to save and will return always the same image when we ask for some data.
@ -178,30 +195,25 @@ class SampleStorageBackend implements IWritableStorage
{ {
const NAME = 'Sample Storage'; const NAME = 'Sample Storage';
/** @var IConfig */ /** @var string */
private $config; private $filename;
/** @var L10n */
private $l10n;
/** /**
* SampleStorageBackend constructor. * SampleStorageBackend constructor.
* @param IConfig $config The configuration of Friendica *
*
* You can add here every dynamic class as dependency you like and add them to a private field * You can add here every dynamic class as dependency you like and add them to a private field
* Friendica automatically creates these classes and passes them as argument to the constructor * Friendica automatically creates these classes and passes them as argument to the constructor
*/ */
public function __construct(IConfig $config, L10n $l10n) public function __construct(string $filename)
{ {
$this->config = $config; $this->filename = $filename;
$this->l10n = $l10n;
} }
public function get(string $reference) public function get(string $reference)
{ {
// we return always the same image data. Which file we load is defined by // we return always the same image data. Which file we load is defined by
// a config key // a config key
$filename = $this->config->get('storage', 'samplestorage', 'sample.jpg'); return file_get_contents($this->filename);
return file_get_contents($filename);
} }
public function put(string $data, string $reference = '') public function put(string $data, string $reference = '')
@ -219,6 +231,51 @@ class SampleStorageBackend implements IWritableStorage
return true; return true;
} }
public function __toString()
{
return self::NAME;
}
public static function getName()
{
return self::NAME;
}
}
```
```php
<?php
namespace Friendica\Addon\samplestorage;
use Friendica\Model\Storage\IStorageConfiguration;
use Friendica\Core\Config\IConfig;
use Friendica\Core\L10n;
class SampleStorageBackendConfig implements IStorageConfiguration
{
/** @var IConfig */
private $config;
/** @var L10n */
private $l10n;
/**
* SampleStorageBackendConfig constructor.
*
* You can add here every dynamic class as dependency you like and add them to a private field
* Friendica automatically creates these classes and passes them as argument to the constructor
*/
public function __construct(IConfig $config, L10n $l10n)
{
$this->config = $config;
$this->l10n = $l10n;
}
public function getFileName(): string
{
return $this->config->get('storage', 'samplestorage', 'sample.jpg');
}
public function getOptions() public function getOptions()
{ {
$filename = $this->config->get('storage', 'samplestorage', 'sample.jpg'); $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
@ -252,15 +309,6 @@ class SampleStorageBackend implements IWritableStorage
return []; return [];
} }
public function __toString()
{
return self::NAME;
}
public static function getName()
{
return self::NAME;
}
} }
``` ```
@ -278,29 +326,32 @@ The file is `addon/samplestorage/samplestorage.php`
*/ */
use Friendica\Addon\samplestorage\SampleStorageBackend; use Friendica\Addon\samplestorage\SampleStorageBackend;
use Friendica\Addon\samplestorage\SampleStorageBackendConfig;
use Friendica\DI; use Friendica\DI;
function samplestorage_install() function samplestorage_install()
{ {
// on addon install, we register our class with name "Sample Storage". Hook::register('storage_instance' , __FILE__, 'samplestorage_storage_instance');
// note: we use `::class` property, which returns full class name as string Hook::register('storage_config' , __FILE__, 'samplestorage_storage_config');
// this save us the problem of correctly escape backslashes in class name
DI::storageManager()->register(SampleStorageBackend::class); DI::storageManager()->register(SampleStorageBackend::class);
} }
function samplestorage_unistall() function samplestorage_storage_uninstall()
{ {
// when the plugin is uninstalled, we unregister the backend.
DI::storageManager()->unregister(SampleStorageBackend::class); DI::storageManager()->unregister(SampleStorageBackend::class);
} }
function samplestorage_storage_instance(\Friendica\App $a, array $data) function samplestorage_storage_instance(App $a, array &$data)
{ {
if ($data['name'] === SampleStorageBackend::getName()) { $config = new SampleStorageBackendConfig(DI::l10n(), DI::config());
// instance a new sample storage instance and pass it back to the core for usage $data['storage'] = new SampleStorageBackendConfig($config->getFileName());
$data['storage'] = new SampleStorageBackend(DI::config(), DI::l10n(), DI::cache());
}
} }
function samplestorage_storage_config(App $a, array &$data)
{
$data['storage_config'] = new SampleStorageBackendConfig(DI::l10n(), DI::config());
}
``` ```
**Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`: **Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:

View File

@ -538,6 +538,22 @@ Hook data:
- **uid** (input): the user id to revoke the block for. - **uid** (input): the user id to revoke the block for.
- **result** (output): a boolean value indicating wether the operation was successful or not. - **result** (output): a boolean value indicating wether the operation was successful or not.
### storage_instance
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`)
### storage_config
Called when the admin of the node wants to configure a custom storage (e.g. webdav_storage)
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`)
## Complete list of hook callbacks ## Complete list of hook callbacks
Here is a complete list of all hook callbacks with file locations (as of 24-Sep-2018). Please see the source for details of any hooks not documented above. Here is a complete list of all hook callbacks with file locations (as of 24-Sep-2018). Please see the source for details of any hooks not documented above.
@ -802,6 +818,7 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep-
### src/Core/StorageManager ### src/Core/StorageManager
Hook::callAll('storage_instance', $data); Hook::callAll('storage_instance', $data);
Hook::callAll('storage_config', $data);
### src/Worker/Directory.php ### src/Worker/Directory.php

View File

@ -425,6 +425,7 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
### src/Core/StorageManager ### src/Core/StorageManager
Hook::callAll('storage_instance', $data); Hook::callAll('storage_instance', $data);
Hook::callAll('storage_config', $data);
### src/Module/PermissionTooltip.php ### src/Module/PermissionTooltip.php

View File

@ -119,6 +119,43 @@ class StorageManager
return $storage; 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 * Return storage backend class by registered name
* *
@ -142,7 +179,8 @@ class StorageManager
switch ($name) { switch ($name) {
// Try the filesystem backend // Try the filesystem backend
case Storage\Filesystem::getName(): case Storage\Filesystem::getName():
$this->backendInstances[$name] = new Storage\Filesystem($this->config, $this->l10n); $storageConfig = new Storage\FilesystemConfig($this->config, $this->l10n);
$this->backendInstances[$name] = new Storage\Filesystem($storageConfig->getStoragePath());
break; break;
// try the database backend // try the database backend
case Storage\Database::getName(): case Storage\Database::getName():

View File

@ -113,22 +113,6 @@ class Database implements IWritableStorage
} }
} }
/**
* @inheritDoc
*/
public function getOptions(): array
{
return [];
}
/**
* @inheritDoc
*/
public function saveOptions(array $data): array
{
return [];
}
/** /**
* @inheritDoc * @inheritDoc
*/ */

View File

@ -22,8 +22,6 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use Exception; use Exception;
use Friendica\Core\Config\IConfig;
use Friendica\Core\L10n;
use Friendica\Util\Strings; use Friendica\Util\Strings;
/** /**
@ -40,31 +38,24 @@ class Filesystem implements IWritableStorage
{ {
const NAME = 'Filesystem'; const NAME = 'Filesystem';
// Default base folder
const DEFAULT_BASE_FOLDER = 'storage';
/** @var IConfig */
private $config;
/** @var string */ /** @var string */
private $basePath; private $basePath;
/** @var L10n */
private $l10n;
/** /**
* Filesystem constructor. * Filesystem constructor.
* *
* @param IConfig $config * @param string $filesystemPath
* @param L10n $l10n *
* @throws StorageException in case the path doesn't exist or isn't writeable
*/ */
public function __construct(IConfig $config, L10n $l10n) public function __construct(string $filesystemPath = FilesystemConfig::DEFAULT_BASE_FOLDER)
{ {
$this->config = $config; $path = $filesystemPath;
$this->l10n = $l10n;
$path = $this->config->get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER);
$this->basePath = rtrim($path, '/'); $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));
}
} }
/** /**
@ -176,37 +167,6 @@ class Filesystem implements IWritableStorage
} }
} }
/**
* @inheritDoc
*/
public function getOptions(): array
{
return [
'storagepath' => [
'input',
$this->l10n->t('Storage base path'),
$this->basePath,
$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->basePath = $storagePath;
return [];
}
/** /**
* @inheritDoc * @inheritDoc
*/ */

View File

@ -0,0 +1,99 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Model\Storage;
use Friendica\Core\Config\IConfig;
use Friendica\Core\L10n;
/**
* Filesystem based storage backend configuration
*/
class FilesystemConfig implements IStorageConfiguration
{
// Default base folder
const DEFAULT_BASE_FOLDER = 'storage';
/** @var IConfig */
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 IConfig $config
* @param L10n $l10n
*/
public function __construct(IConfig $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 [];
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
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;
}

View File

@ -50,54 +50,4 @@ interface IWritableStorage extends IStorage
* @throws ReferenceStorageException in case the reference doesn't exist * @throws ReferenceStorageException in case the reference doesn't exist
*/ */
public function delete(string $reference); public function delete(string $reference);
/**
* Get info about storage options
*
* @return array
*
* This method return an array with informations 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;
} }

View File

@ -24,6 +24,7 @@ namespace Friendica\Module\Admin;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Storage\InvalidClassStorageException; use Friendica\Model\Storage\InvalidClassStorageException;
use Friendica\Model\Storage\IStorageConfiguration;
use Friendica\Model\Storage\IWritableStorage; use Friendica\Model\Storage\IWritableStorage;
use Friendica\Module\BaseAdmin; use Friendica\Module\BaseAdmin;
use Friendica\Util\Strings; use Friendica\Util\Strings;
@ -39,38 +40,40 @@ class Storage extends BaseAdmin
$storagebackend = Strings::escapeTags(trim($parameters['name'] ?? '')); $storagebackend = Strings::escapeTags(trim($parameters['name'] ?? ''));
try { try {
/** @var IWritableStorage $newstorage */ /** @var IStorageConfiguration|false $newStorageConfig */
$newstorage = DI::storageManager()->getWritableStorageByName($storagebackend); $newStorageConfig = DI::storageManager()->getConfigurationByName($storagebackend);
} catch (InvalidClassStorageException $storageException) { } catch (InvalidClassStorageException $storageException) {
notice(DI::l10n()->t('Storage backend, %s is invalid.', $storagebackend)); notice(DI::l10n()->t('Storage backend, %s is invalid.', $storagebackend));
DI::baseUrl()->redirect('admin/storage'); DI::baseUrl()->redirect('admin/storage');
} }
// save storage backend form if ($newStorageConfig !== false) {
$storage_opts = $newstorage->getOptions(); // save storage backend form
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $storagebackend); $storage_opts = $newStorageConfig->getOptions();
$storage_opts_data = []; $storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $storagebackend);
foreach ($storage_opts as $name => $info) { $storage_opts_data = [];
$fieldname = $storage_form_prefix . '_' . $name; foreach ($storage_opts as $name => $info) {
switch ($info[0]) { // type $fieldname = $storage_form_prefix . '_' . $name;
case 'checkbox': switch ($info[0]) { // type
case 'yesno': case 'checkbox':
$value = !empty($_POST[$fieldname]); case 'yesno':
break; $value = !empty($_POST[$fieldname]);
default: break;
$value = $_POST[$fieldname] ?? ''; default:
$value = $_POST[$fieldname] ?? '';
}
$storage_opts_data[$name] = $value;
} }
$storage_opts_data[$name] = $value; unset($name);
} unset($info);
unset($name);
unset($info);
$storage_form_errors = $newstorage->saveOptions($storage_opts_data); $storage_form_errors = $newStorageConfig->saveOptions($storage_opts_data);
if (count($storage_form_errors)) { if (count($storage_form_errors)) {
foreach ($storage_form_errors as $name => $err) { foreach ($storage_form_errors as $name => $err) {
notice(DI::l10n()->t('Storage backend %s error: %s', $storage_opts[$name][1], $err)); notice(DI::l10n()->t('Storage backend %s error: %s', $storage_opts[$name][1], $err));
}
DI::baseUrl()->redirect('admin/storage');
} }
DI::baseUrl()->redirect('admin/storage');
} }
if (!empty($_POST['submit_save_set'])) { if (!empty($_POST['submit_save_set'])) {
@ -101,20 +104,25 @@ class Storage extends BaseAdmin
// build storage config form, // build storage config form,
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $name); $storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $name);
$storage_form = []; $storage_form = [];
foreach (DI::storageManager()->getWritableStorageByName($name)->getOptions() as $option => $info) { $storageConfig = DI::storageManager()->getConfigurationByName($name);
$type = $info[0];
// Backward compatibilty with yesno field description
if ($type == 'yesno') {
$type = 'checkbox';
// Remove translated labels Yes No from field info
unset($info[4]);
}
$info[0] = $storage_form_prefix . '_' . $option; if ($storageConfig !== false) {
$info['type'] = $type; foreach ($storageConfig->getOptions() as $option => $info) {
$info['field'] = 'field_' . $type . '.tpl';
$storage_form[$option] = $info; $type = $info[0];
// Backward compatibilty with yesno field description
if ($type == 'yesno') {
$type = 'checkbox';
// Remove translated labels Yes No from field info
unset($info[4]);
}
$info[0] = $storage_form_prefix . '_' . $option;
$info['type'] = $type;
$info['field'] = 'field_' . $type . '.tpl';
$storage_form[$option] = $info;
}
} }
$available_storage_forms[] = [ $available_storage_forms[] = [

View File

@ -40,6 +40,7 @@ use Friendica\Test\Util\Database\StaticDatabase;
use Friendica\Test\Util\VFSTrait; use Friendica\Test\Util\VFSTrait;
use Friendica\Util\ConfigFileLoader; use Friendica\Util\ConfigFileLoader;
use Friendica\Util\Profiler; use Friendica\Util\Profiler;
use org\bovigo\vfs\vfsStream;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Friendica\Test\Util\SampleStorageBackend; use Friendica\Test\Util\SampleStorageBackend;
@ -64,6 +65,8 @@ class StorageManagerTest extends DatabaseTest
$this->setUpVfsDir(); $this->setUpVfsDir();
vfsStream::newDirectory(Storage\FilesystemConfig::DEFAULT_BASE_FOLDER, 0777)->at($this->root);
$this->logger = new NullLogger(); $this->logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class); $profiler = \Mockery::mock(Profiler::class);
@ -81,12 +84,20 @@ class StorageManagerTest extends DatabaseTest
$configModel = new Config($this->dba); $configModel = new Config($this->dba);
$this->config = new PreloadConfig($configCache, $configModel); $this->config = new PreloadConfig($configCache, $configModel);
$this->config->set('storage', 'name', 'Database'); $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->l10n = \Mockery::mock(L10n::class);
$this->httpRequest = \Mockery::mock(HTTPClient::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 * Test plain instancing first
*/ */

View File

@ -23,11 +23,9 @@ namespace Friendica\Test\src\Model\Storage;
use Friendica\Factory\ConfigFactory; use Friendica\Factory\ConfigFactory;
use Friendica\Model\Storage\Database; use Friendica\Model\Storage\Database;
use Friendica\Model\Storage\IWritableStorage;
use Friendica\Test\DatabaseTestTrait; use Friendica\Test\DatabaseTestTrait;
use Friendica\Test\Util\Database\StaticDatabase; use Friendica\Test\Util\Database\StaticDatabase;
use Friendica\Test\Util\VFSTrait; use Friendica\Test\Util\VFSTrait;
use Friendica\Util\ConfigFileLoader;
use Friendica\Util\Profiler; use Friendica\Util\Profiler;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
@ -47,7 +45,7 @@ class DatabaseStorageTest extends StorageTest
protected function getInstance() protected function getInstance()
{ {
$logger = new NullLogger(); $logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class); $profiler = \Mockery::mock(Profiler::class);
$profiler->shouldReceive('startRecording'); $profiler->shouldReceive('startRecording');
$profiler->shouldReceive('stopRecording'); $profiler->shouldReceive('stopRecording');
@ -55,19 +53,14 @@ class DatabaseStorageTest extends StorageTest
// load real config to avoid mocking every config-entry which is related to the Database class // load real config to avoid mocking every config-entry which is related to the Database class
$configFactory = new ConfigFactory(); $configFactory = new ConfigFactory();
$loader = (new ConfigFactory())->createConfigFileLoader($this->root->url(), []); $loader = (new ConfigFactory())->createConfigFileLoader($this->root->url(), []);
$configCache = $configFactory->createCache($loader); $configCache = $configFactory->createCache($loader);
$dba = new StaticDatabase($configCache, $profiler, $logger); $dba = new StaticDatabase($configCache, $profiler, $logger);
return new Database($dba); return new Database($dba);
} }
protected function assertOption(IWritableStorage $storage)
{
self::assertEmpty($storage->getOptions());
}
protected function tearDown(): void protected function tearDown(): void
{ {
$this->tearDownDb(); $this->tearDownDb();

View File

@ -0,0 +1,67 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\src\Model\Storage;
use Friendica\Core\Config\IConfig;
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(IConfig::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());
}
}

View File

@ -21,23 +21,16 @@
namespace Friendica\Test\src\Model\Storage; namespace Friendica\Test\src\Model\Storage;
use Friendica\Core\Config\IConfig;
use Friendica\Core\L10n;
use Friendica\Model\Storage\Filesystem; use Friendica\Model\Storage\Filesystem;
use Friendica\Model\Storage\IWritableStorage; use Friendica\Model\Storage\FilesystemConfig;
use Friendica\Model\Storage\StorageException; use Friendica\Model\Storage\StorageException;
use Friendica\Test\Util\VFSTrait; use Friendica\Test\Util\VFSTrait;
use Friendica\Util\Profiler;
use Mockery\MockInterface;
use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
class FilesystemStorageTest extends StorageTest class FilesystemStorageTest extends StorageTest
{ {
use VFSTrait; use VFSTrait;
/** @var MockInterface|IConfig */
protected $config;
protected function setUp(): void protected function setUp(): void
{ {
$this->setUpVfsDir(); $this->setUpVfsDir();
@ -49,43 +42,34 @@ class FilesystemStorageTest extends StorageTest
protected function getInstance() protected function getInstance()
{ {
$profiler = \Mockery::mock(Profiler::class); return new Filesystem($this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->url());
$profiler->shouldReceive('startRecording');
$profiler->shouldReceive('stopRecording');
$profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true);
/** @var MockInterface|L10n $l10n */
$l10n = \Mockery::mock(L10n::class)->makePartial();
$this->config = \Mockery::mock(IConfig::class);
$this->config->shouldReceive('get')
->with('storage', 'filesystem_path', Filesystem::DEFAULT_BASE_FOLDER)
->andReturn($this->root->getChild('storage')->url());
return new Filesystem($this->config, $l10n);
}
protected function assertOption(IWritableStorage $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());
} }
/** /**
* Test the exception in case of missing directorsy permissions * 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() public function testMissingDirPermissions()
{ {
$this->expectException(StorageException::class); $this->expectException(StorageException::class);
$this->expectExceptionMessageMatches("/Filesystem storage failed to create \".*\". Check you write permissions./"); $this->expectExceptionMessageMatches("/Path \".*\" does not exist or is not writeable./");
$this->root->getChild('storage')->chmod(000); $this->root->getChild(FilesystemConfig::DEFAULT_BASE_FOLDER)->chmod(0000);
$instance = $this->getInstance(); $this->getInstance();
$instance->put('test');
} }
/** /**
@ -116,7 +100,7 @@ class FilesystemStorageTest extends StorageTest
$instance->put('test', 'f0c0d0i0'); $instance->put('test', 'f0c0d0i0');
$dir = $this->root->getChild('storage/f0/c0')->url(); $dir = $this->root->getChild('storage/f0/c0')->url();
$file = $this->root->getChild('storage/f0/c0/d0i0')->url(); $file = $this->root->getChild('storage/f0/c0/d0i0')->url();
self::assertDirectoryExists($dir); self::assertDirectoryExists($dir);

View File

@ -0,0 +1,43 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
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);
}
}

View File

@ -31,8 +31,6 @@ abstract class StorageTest extends MockedTest
/** @return IWritableStorage */ /** @return IWritableStorage */
abstract protected function getInstance(); abstract protected function getInstance();
abstract protected function assertOption(IWritableStorage $storage);
/** /**
* Test if the instance is "really" implementing the interface * Test if the instance is "really" implementing the interface
*/ */
@ -42,16 +40,6 @@ abstract class StorageTest extends MockedTest
self::assertInstanceOf(IStorage::class, $instance); self::assertInstanceOf(IStorage::class, $instance);
} }
/**
* Test if the "getOption" is asserted
*/
public function testGetOptions()
{
$instance = $this->getInstance();
$this->assertOption($instance);
}
/** /**
* Test basic put, get and delete operations * Test basic put, get and delete operations
*/ */