Merge pull request #8062 from nupplaphil/Make-Storage

Make Storage testable & add tests
This commit is contained in:
Hypolite Petovan 2020-01-10 08:45:07 -05:00 committed by GitHub
commit 84cff91283
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1430 additions and 340 deletions

View file

@ -1,6 +1,6 @@
-- ------------------------------------------ -- ------------------------------------------
-- Friendica 2020.03-dev (Dalmatian Bellflower) -- Friendica 2020.03-dev (Dalmatian Bellflower)
-- DB_UPDATE_VERSION 1329 -- DB_UPDATE_VERSION 1330
-- ------------------------------------------ -- ------------------------------------------

View file

@ -17,22 +17,24 @@ namespace Friendica\Model\Storage;
```php ```php
interface IStorage interface IStorage
{ {
public static function get($ref); public function get(string $reference);
public static function put($data, $ref = ""); public function put(string $data, string $reference = '');
public static function delete($ref); public function delete(string $reference);
public static function getOptions(); public function getOptions();
public static function saveOptions($data); public function saveOptions(array $data);
public function __toString();
public static function getName();
} }
``` ```
- `get($ref)` returns data pointed by `$ref` - `get(string $reference)` returns data pointed by `$reference`
- `put($data, $ref)` saves data in `$data` to position `$ref`, or a new position if `$ref` is empty. - `put(string $data, string $reference)` saves data in `$data` to position `$reference`, or a new position if `$reference` is empty.
- `delete($ref)` delete data pointed by `$ref` - `delete(string $reference)` delete data pointed by `$reference`
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.
- `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($data)` get `$data` from admin page, validate it and save it. - `saveOptions(array $data)` get `$data` from admin page, validate it and save it.
The array returned by `getOptions()` is defined as: The array returned by `getOptions()` is defined as:
@ -84,11 +86,38 @@ See doxygen documentation of `IStorage` interface for details about each method.
Each backend must be registered in the system when the plugin is installed, to be aviable. Each backend must be registered in the system when the plugin is installed, to be aviable.
`Friendica\Core\StorageManager::register($name, $class)` is used to register the backend class. `DI::facStorage()->register(string $class)` is used to register the backend class.
The `$name` must be univocal and will be shown to admin.
When the plugin is uninstalled, registered backends must be unregistered using When the plugin is uninstalled, registered backends must be unregistered using
`Friendica\Core\StorageManager::unregister($class)`. `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.
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.
## Adding tests
**Currently testing is limited to core Friendica only, this shows theoretically how tests should work in the future**
Each new Storage class should be added to the test-environment at [Storage Tests](https://github.com/friendica/friendica/tree/develop/tests/src/Model/Storage/).
Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory.
Override the two necessary instances:
```php
use Friendica\Model\Storage\IStorage;
abstract class StorageTest
{
// returns an instance of your newly created storage class
abstract protected function getInstance();
// Assertion for the option array you return for your new StorageClass
abstract protected function assertOption(IStorage $storage);
}
```
## Example ## Example
@ -112,60 +141,91 @@ use Friendica\Core\L10n;
class SampleStorageBackend implements IStorage class SampleStorageBackend implements IStorage
{ {
public static function get($ref) const NAME = 'Sample Storage';
/** @var Config\IConfiguration */
private $config;
/** @var L10n\L10n */
private $l10n;
/**
* SampleStorageBackend constructor.
* @param Config\IConfiguration $config The configuration of Friendica
*
* 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(Config\IConfiguration $config, L10n\L10n $l10n)
{ {
// we return alwais the same image data. Which file we load is defined by $this->config = $config;
$this->l10n = $l10n;
}
public function get(string $reference)
{
// we return always the same image data. Which file we load is defined by
// a config key // a config key
$filename = Config::get("storage", "samplestorage", "sample.jpg"); $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
return file_get_contents($filename); return file_get_contents($filename);
} }
public static function put($data, $ref = "") public function put(string $data, string $reference = '')
{ {
if ($ref === "") { if ($reference === '') {
$ref = "sample"; $reference = 'sample';
} }
// we don't save $data ! // we don't save $data !
return $ref; return $reference;
} }
public static function delete($ref) public function delete(string $reference)
{ {
// we pretend to delete the data // we pretend to delete the data
return true; return true;
} }
public static function getOptions() public function getOptions()
{ {
$filename = Config::get("storage", "samplestorage", "sample.jpg"); $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
return [ return [
"filename" => [ 'filename' => [
"input", // will use a simple text input 'input', // will use a simple text input
L10n::t("The file to return"), // the label $this->l10n->t('The file to return'), // the label
$filename, // the current value $filename, // the current value
L10n::t("Enter the path to a file"), // the help text $this->l10n->t('Enter the path to a file'), // the help text
// no extra data for "input" type.. // no extra data for 'input' type..
],
]; ];
} }
public static function saveOptions($data) public function saveOptions(array $data)
{ {
// the keys in $data are the same keys we defined in getOptions() // the keys in $data are the same keys we defined in getOptions()
$newfilename = trim($data["filename"]); $newfilename = trim($data['filename']);
// this function should always validate the data. // this function should always validate the data.
// in this example we check if file exists // in this example we check if file exists
if (!file_exists($newfilename)) { if (!file_exists($newfilename)) {
// in case of error we return an array with // in case of error we return an array with
// ["optionname" => "error message"] // ['optionname' => 'error message']
return ["filename" => "The file doesn't exists"]; return ['filename' => 'The file doesn\'t exists'];
} }
Config::set("storage", "samplestorage", $newfilename); $this->config->set('storage', 'samplestorage', $newfilename);
// no errors, return empty array // no errors, return empty array
return []; return [];
} }
public function __toString()
{
return self::NAME;
}
public static function getName()
{
return self::NAME;
}
} }
``` ```
@ -182,23 +242,59 @@ The file is `addon/samplestorage/samplestorage.php`
* Author: Alice <https://alice.social/~alice> * Author: Alice <https://alice.social/~alice>
*/ */
use Friendica\Core\StorageManager;
use Friendica\Addon\samplestorage\SampleStorageBackend; use Friendica\Addon\samplestorage\SampleStorageBackend;
use Friendica\DI;
function samplestorage_install() function samplestorage_install()
{ {
// on addon install, we register our class with name "Sample Storage". // on addon install, we register our class with name "Sample Storage".
// note: we use `::class` property, which returns full class name as string // note: we use `::class` property, which returns full class name as string
// this save us the problem of correctly escape backslashes in class name // this save us the problem of correctly escape backslashes in class name
StorageManager::register("Sample Storage", SampleStorageBackend::class); DI::storageManager()->register(SampleStorageBackend::class);
} }
function samplestorage_unistall() function samplestorage_unistall()
{ {
// when the plugin is uninstalled, we unregister the backend. // when the plugin is uninstalled, we unregister the backend.
StorageManager::unregister("Sample Storage"); DI::storageManager()->unregister(SampleStorageBackend::class);
}
function samplestorage_storage_instance(\Friendica\App $a, array $data)
{
if ($data['name'] === SampleStorageBackend::getName()) {
// instance a new sample storage instance and pass it back to the core for usage
$data['storage'] = new SampleStorageBackend(DI::config(), DI::l10n(), DI::cache());
}
} }
``` ```
**Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
```php
use Friendica\Model\Storage\IStorage;
use Friendica\Test\src\Model\Storage\StorageTest;
class SampleStorageTest extends StorageTest
{
// returns an instance of your newly created storage class
protected function getInstance()
{
// create a new SampleStorageBackend instance with all it's dependencies
// Have a look at DatabaseStorageTest or FilesystemStorageTest for further insights
return new SampleStorageBackend();
}
// Assertion for the option array you return for your new StorageClass
protected function assertOption(IStorage $storage)
{
$this->assertEquals([
'filename' => [
'input',
'The file to return',
'sample.jpg',
'Enter the path to a file'
],
], $storage->getOptions());
}
}
```

View file

@ -706,6 +706,14 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep-
Hook::callAll('page_header', DI::page()['nav']); Hook::callAll('page_header', DI::page()['nav']);
Hook::callAll('nav_info', $nav); Hook::callAll('nav_info', $nav);
### src/Core/Authentication.php
Hook::callAll('logged_in', $a->user);
### src/Core/StorageManager
Hook::callAll('storage_instance', $data);
### src/Worker/Directory.php ### src/Worker/Directory.php
Hook::callAll('globaldir_update', $arr); Hook::callAll('globaldir_update', $arr);

View file

@ -425,6 +425,10 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
Hook::callAll('logged_in', $a->user); Hook::callAll('logged_in', $a->user);
### src/Core/StorageManager
Hook::callAll('storage_instance', $data);
### src/Worker/Directory.php ### src/Worker/Directory.php
Hook::callAll('globaldir_update', $arr); Hook::callAll('globaldir_update', $arr);

View file

@ -13,6 +13,19 @@ class Storage extends \Asika\SimpleConsole\Console
{ {
protected $helpOptions = ['h', 'help', '?']; protected $helpOptions = ['h', 'help', '?'];
/** @var StorageManager */
private $storageManager;
/**
* @param StorageManager $storageManager
*/
public function __construct(StorageManager $storageManager, array $argv = [])
{
parent::__construct($argv);
$this->storageManager = $storageManager;
}
protected function getHelp() protected function getHelp()
{ {
$help = <<<HELP $help = <<<HELP
@ -69,11 +82,11 @@ HELP;
protected function doList() protected function doList()
{ {
$rowfmt = ' %-3s | %-20s'; $rowfmt = ' %-3s | %-20s';
$current = StorageManager::getBackend(); $current = $this->storageManager->getBackend();
$this->out(sprintf($rowfmt, 'Sel', 'Name')); $this->out(sprintf($rowfmt, 'Sel', 'Name'));
$this->out('-----------------------'); $this->out('-----------------------');
$isregisterd = false; $isregisterd = false;
foreach (StorageManager::listBackends() as $name => $class) { foreach ($this->storageManager->listBackends() as $name => $class) {
$issel = ' '; $issel = ' ';
if ($current === $class) { if ($current === $class) {
$issel = '*'; $issel = '*';
@ -100,14 +113,14 @@ HELP;
} }
$name = $this->args[1]; $name = $this->args[1];
$class = StorageManager::getByName($name); $class = $this->storageManager->getByName($name);
if ($class === '') { if ($class === '') {
$this->out($name . ' is not a registered backend.'); $this->out($name . ' is not a registered backend.');
return -1; return -1;
} }
if (!StorageManager::setBackend($class)) { if (!$this->storageManager->setBackend($class)) {
$this->out($class . ' is not a valid backend storage class.'); $this->out($class . ' is not a valid backend storage class.');
return -1; return -1;
} }
@ -130,11 +143,11 @@ HELP;
$tables = [$table]; $tables = [$table];
} }
$current = StorageManager::getBackend(); $current = $this->storageManager->getBackend();
$total = 0; $total = 0;
do { do {
$moved = StorageManager::move($current, $tables, $this->getOption('n', 5000)); $moved = $this->storageManager->move($current, $tables, $this->getOption('n', 5000));
if ($moved) { if ($moved) {
$this->out(date('[Y-m-d H:i:s] ') . sprintf('Moved %d files', $moved)); $this->out(date('[Y-m-d H:i:s] ') . sprintf('Moved %d files', $moved));
} }

View file

@ -2,8 +2,12 @@
namespace Friendica\Core; namespace Friendica\Core;
use Friendica\Database\DBA; use Exception;
use Friendica\Model\Storage\IStorage; use Friendica\Core\Config\IConfiguration;
use Friendica\Core\L10n\L10n;
use Friendica\Database\Database;
use Friendica\Model\Storage;
use Psr\Log\LoggerInterface;
/** /**
@ -14,59 +18,146 @@ use Friendica\Model\Storage\IStorage;
*/ */
class StorageManager class StorageManager
{ {
private static $default_backends = [ // Default tables to look for data
'Filesystem' => \Friendica\Model\Storage\Filesystem::class, const TABLES = ['photo', 'attach'];
'Database' => \Friendica\Model\Storage\Database::class,
// Default storage backends
const DEFAULT_BACKENDS = [
Storage\Filesystem::NAME => Storage\Filesystem::class,
Storage\Database::NAME => Storage\Database::class,
]; ];
private static $backends = []; private $backends = [];
private static function setup() /**
* @var Storage\IStorage[] A local cache for storage instances
*/
private $backendInstances = [];
/** @var Database */
private $dba;
/** @var IConfiguration */
private $config;
/** @var LoggerInterface */
private $logger;
/** @var L10n */
private $l10n;
/** @var Storage\IStorage */
private $currentBackend;
/**
* @param Database $dba
* @param IConfiguration $config
* @param LoggerInterface $logger
* @param L10n $l10n
*/
public function __construct(Database $dba, IConfiguration $config, LoggerInterface $logger, L10n $l10n)
{ {
if (count(self::$backends) == 0) { $this->dba = $dba;
self::$backends = Config::get('storage', 'backends', self::$default_backends); $this->config = $config;
} $this->logger = $logger;
$this->l10n = $l10n;
$this->backends = $config->get('storage', 'backends', self::DEFAULT_BACKENDS);
$currentName = $this->config->get('storage', 'name', '');
$this->currentBackend = $this->getByName($currentName);
} }
/** /**
* @brief Return current storage backend class * @brief Return current storage backend class
* *
* @return string * @return Storage\IStorage|null
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function getBackend() public function getBackend()
{ {
return Config::get('storage', 'class', ''); return $this->currentBackend;
} }
/** /**
* @brief Return storage backend class by registered name * @brief Return storage backend class by registered name
* *
* @param string $name Backend name * @param string|null $name Backend name
* @return string Empty if no backend registered at $name exists * @param boolean $userBackend Just return instances in case it's a user backend (e.g. not SystemResource)
*
* @return Storage\IStorage|null null if no backend registered at $name
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function getByName($name) public function getByName(string $name = null, $userBackend = true)
{ {
self::setup(); // If there's no cached instance create a new instance
return self::$backends[$name] ?? ''; if (!isset($this->backendInstances[$name])) {
// If the current name isn't a valid backend (or the SystemResource instance) create it
if ($this->isValidBackend($name, $userBackend)) {
switch ($name) {
// Try the filesystem backend
case Storage\Filesystem::getName():
$this->backendInstances[$name] = new Storage\Filesystem($this->config, $this->logger, $this->l10n);
break;
// try the database backend
case Storage\Database::getName():
$this->backendInstances[$name] = new Storage\Database($this->dba, $this->logger, $this->l10n);
break;
// at least, try if there's an addon for the backend
case Storage\SystemResource::getName():
$this->backendInstances[$name] = new Storage\SystemResource();
break;
default:
$data = [
'name' => $name,
'storage' => null,
];
Hook::callAll('storage_instance', $data);
if (($data['storage'] ?? null) instanceof Storage\IStorage) {
$this->backendInstances[$data['name'] ?? $name] = $data['storage'];
} else {
return null;
}
break;
}
} else {
return null;
}
}
return $this->backendInstances[$name];
}
/**
* Checks, if the storage is a valid backend
*
* @param string|null $name The name or class of the backend
* @param boolean $userBackend True, if just user backend should get returned (e.g. not SystemResource)
*
* @return boolean True, if the backend is a valid backend
*/
public function isValidBackend(string $name = null, bool $userBackend = true)
{
return array_key_exists($name, $this->backends) ||
(!$userBackend && $name === Storage\SystemResource::getName());
} }
/** /**
* @brief Set current storage backend class * @brief Set current storage backend class
* *
* @param string $class Backend class name * @param string $name Backend class name
* @return bool *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @return boolean True, if the set was successful
*/ */
public static function setBackend($class) public function setBackend(string $name = null)
{ {
if (!in_array('Friendica\Model\Storage\IStorage', class_implements($class))) { if (!$this->isValidBackend($name)) {
return false; return false;
} }
Config::set('storage', 'class', $class); if ($this->config->set('storage', 'name', $name)) {
$this->currentBackend = $this->getByName($name);
return true; return true;
} else {
return false;
}
} }
/** /**
@ -74,42 +165,63 @@ class StorageManager
* *
* @return array * @return array
*/ */
public static function listBackends() public function listBackends()
{ {
self::setup(); return $this->backends;
return self::$backends;
} }
/** /**
* @brief Register a storage backend class * Register a storage backend class
*
* You have to register the hook "storage_instance" as well to make this class work!
* *
* @param string $name User readable backend name
* @param string $class Backend class name * @param string $class Backend class name
* @throws \Friendica\Network\HTTPException\InternalServerErrorException *
* @return boolean True, if the registration was successful
*/ */
public static function register($name, $class) public function register(string $class)
{ {
/// @todo Check that $class implements IStorage if (is_subclass_of($class, Storage\IStorage::class)) {
self::setup(); /** @var Storage\IStorage $class */
self::$backends[$name] = $class;
Config::set('storage', 'backends', self::$backends);
}
$backends = $this->backends;
$backends[$class::getName()] = $class;
if ($this->config->set('storage', 'backends', $backends)) {
$this->backends = $backends;
return true;
} else {
return false;
}
} else {
return false;
}
}
/** /**
* @brief Unregister a storage backend class * @brief Unregister a storage backend class
* *
* @param string $name User readable backend name * @param string $class Backend class name
* @throws \Friendica\Network\HTTPException\InternalServerErrorException *
* @return boolean True, if unregistering was successful
*/ */
public static function unregister($name) public function unregister(string $class)
{ {
self::setup(); if (is_subclass_of($class, Storage\IStorage::class)) {
unset(self::$backends[$name]); /** @var Storage\IStorage $class */
Config::set('storage', 'backends', self::$backends);
unset($this->backends[$class::getName()]);
if ($this->currentBackend instanceof $class) {
$this->config->set('storage', 'name', null);
$this->currentBackend = null;
} }
return $this->config->set('storage', 'backends', $this->backends);
} else {
return false;
}
}
/** /**
* @brief Move up to 5000 resources to storage $dest * @brief Move up to 5000 resources to storage $dest
@ -117,64 +229,60 @@ class StorageManager
* Copy existing data to destination storage and delete from source. * Copy existing data to destination storage and delete from source.
* This method cannot move to legacy in-table `data` field. * This method cannot move to legacy in-table `data` field.
* *
* @param string $destination Storage class name * @param Storage\IStorage $destination Destination storage class name
* @param array|null $tables Tables to look in for resources. Optional, defaults to ['photo', 'attach'] * @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 * @param int $limit Limit of the process batch size, defaults to 5000
*
* @return int Number of moved resources * @return int Number of moved resources
* @throws \Exception * @throws Storage\StorageException
* @throws Exception
*/ */
public static function move($destination, $tables = null, $limit = 5000) public function move(Storage\IStorage $destination, array $tables = self::TABLES, int $limit = 5000)
{ {
if (empty($destination)) { if ($destination === null) {
throw new \Exception('Can\'t move to NULL storage backend'); throw new Storage\StorageException('Can\'t move to NULL storage backend');
}
if (is_null($tables)) {
$tables = ['photo', 'attach'];
} }
$moved = 0; $moved = 0;
foreach ($tables as $table) { foreach ($tables as $table) {
// Get the rows where backend class is not the destination backend class // Get the rows where backend class is not the destination backend class
$resources = DBA::select( $resources = $this->dba->select(
$table, $table,
['id', 'data', 'backend-class', 'backend-ref'], ['id', 'data', 'backend-class', 'backend-ref'],
['`backend-class` IS NULL or `backend-class` != ?', $destination], ['`backend-class` IS NULL or `backend-class` != ?', $destination::getName()],
['limit' => $limit] ['limit' => $limit]
); );
while ($resource = DBA::fetch($resources)) { while ($resource = $this->dba->fetch($resources)) {
$id = $resource['id']; $id = $resource['id'];
$data = $resource['data']; $data = $resource['data'];
/** @var IStorage $backendClass */ $source = $this->getByName($resource['backend-class']);
$backendClass = $resource['backend-class']; $sourceRef = $resource['backend-ref'];
$backendRef = $resource['backend-ref'];
if (!empty($backendClass)) { if (!empty($source)) {
Logger::log("get data from old backend " . $backendClass . " : " . $backendRef); $this->logger->info('Get data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
$data = $backendClass::get($backendRef); $data = $source->get($sourceRef);
} }
Logger::log("save data to new backend " . $destination); $this->logger->info('Save data to new backend.', ['newBackend' => $destination]);
/** @var IStorage $destination */ $destinationRef = $destination->put($data);
$ref = $destination::put($data); $this->logger->info('Saved data.', ['newReference' => $destinationRef]);
Logger::log("saved data as " . $ref);
if ($ref !== '') { if ($destinationRef !== '') {
Logger::log("update row"); $this->logger->info('update row');
if (DBA::update($table, ['backend-class' => $destination, 'backend-ref' => $ref, 'data' => ''], ['id' => $id])) { if ($this->dba->update($table, ['backend-class' => $destination, 'backend-ref' => $destinationRef, 'data' => ''], ['id' => $id])) {
if (!empty($backendClass)) { if (!empty($source)) {
Logger::log("delete data from old backend " . $backendClass . " : " . $backendRef); $this->logger->info('Delete data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
$backendClass::delete($backendRef); $source->delete($sourceRef);
} }
$moved++; $moved++;
} }
} }
} }
DBA::close($resources); $this->dba->close($resources);
} }
return $moved; return $moved;
} }
} }

View file

@ -27,6 +27,7 @@ use Psr\Log\LoggerInterface;
* @method static Core\L10n\L10n l10n() * @method static Core\L10n\L10n l10n()
* @method static Core\Process process() * @method static Core\Process process()
* @method static Core\Session\ISession session() * @method static Core\Session\ISession session()
* @method static Core\StorageManager storageManager()
* @method static Database\Database dba() * @method static Database\Database dba()
* @method static Factory\Mastodon\Account mstdnAccount() * @method static Factory\Mastodon\Account mstdnAccount()
* @method static Factory\Mastodon\FollowRequest mstdnFollowRequest() * @method static Factory\Mastodon\FollowRequest mstdnFollowRequest()
@ -34,6 +35,7 @@ use Psr\Log\LoggerInterface;
* @method static Model\User\Cookie cookie() * @method static Model\User\Cookie cookie()
* @method static Model\Notify notify() * @method static Model\Notify notify()
* @method static Repository\Introduction intro() * @method static Repository\Introduction intro()
* @method static Model\Storage\IStorage storage()
* @method static Protocol\Activity activity() * @method static Protocol\Activity activity()
* @method static Util\ACLFormatter aclFormatter() * @method static Util\ACLFormatter aclFormatter()
* @method static Util\DateTimeFormat dtFormat() * @method static Util\DateTimeFormat dtFormat()
@ -64,12 +66,14 @@ abstract class DI
'lock' => Core\Lock\ILock::class, 'lock' => Core\Lock\ILock::class,
'process' => Core\Process::class, 'process' => Core\Process::class,
'session' => Core\Session\ISession::class, 'session' => Core\Session\ISession::class,
'storageManager' => Core\StorageManager::class,
'dba' => Database\Database::class, 'dba' => Database\Database::class,
'mstdnAccount' => Factory\Mastodon\Account::class, 'mstdnAccount' => Factory\Mastodon\Account::class,
'mstdnFollowRequest' => Factory\Mastodon\FollowRequest::class, 'mstdnFollowRequest' => Factory\Mastodon\FollowRequest::class,
'mstdnRelationship' => Factory\Mastodon\Relationship::class, 'mstdnRelationship' => Factory\Mastodon\Relationship::class,
'cookie' => Model\User\Cookie::class, 'cookie' => Model\User\Cookie::class,
'notify' => Model\Notify::class, 'notify' => Model\Notify::class,
'storage' => Model\Storage\IStorage::class,
'intro' => Repository\Introduction::class, 'intro' => Repository\Introduction::class,
'activity' => Protocol\Activity::class, 'activity' => Protocol\Activity::class,
'aclFormatter' => Util\ACLFormatter::class, 'aclFormatter' => Util\ACLFormatter::class,

View file

@ -6,12 +6,10 @@
*/ */
namespace Friendica\Model; namespace Friendica\Model;
use Friendica\Core\StorageManager;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\Database\DBStructure; use Friendica\Database\DBStructure;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Storage\IStorage;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Mimetype; use Friendica\Util\Mimetype;
@ -186,13 +184,8 @@ class Attach
$filesize = strlen($data); $filesize = strlen($data);
} }
/** @var IStorage $backend_class */ $backend_ref = DI::storage()->put($data);
$backend_class = StorageManager::getBackend();
$backend_ref = '';
if ($backend_class !== '') {
$backend_ref = $backend_class::put($data);
$data = ''; $data = '';
}
$hash = System::createGUID(64); $hash = System::createGUID(64);
$created = DateTimeFormat::utcNow(); $created = DateTimeFormat::utcNow();
@ -210,7 +203,7 @@ class Attach
'allow_gid' => $allow_gid, 'allow_gid' => $allow_gid,
'deny_cid' => $deny_cid, 'deny_cid' => $deny_cid,
'deny_gid' => $deny_gid, 'deny_gid' => $deny_gid,
'backend-class' => $backend_class, 'backend-class' => (string)DI::storage(),
'backend-ref' => $backend_ref 'backend-ref' => $backend_ref
]; ];
@ -266,10 +259,9 @@ class Attach
$items = self::selectToArray(['backend-class','backend-ref'], $conditions); $items = self::selectToArray(['backend-class','backend-ref'], $conditions);
foreach($items as $item) { foreach($items as $item) {
/** @var IStorage $backend_class */ $backend_class = DI::storageManager()->getByName($item['backend-class'] ?? '');
$backend_class = (string)$item['backend-class'];
if ($backend_class !== '') { if ($backend_class !== '') {
$fields['backend-ref'] = $backend_class::put($img->asString(), $item['backend-ref']); $fields['backend-ref'] = $backend_class->put($img->asString(), $item['backend-ref'] ?? '');
} else { } else {
$fields['data'] = $img->asString(); $fields['data'] = $img->asString();
} }
@ -299,10 +291,9 @@ class Attach
$items = self::selectToArray(['backend-class','backend-ref'], $conditions); $items = self::selectToArray(['backend-class','backend-ref'], $conditions);
foreach($items as $item) { foreach($items as $item) {
/** @var IStorage $backend_class */ $backend_class = DI::storageManager()->getByName($item['backend-class'] ?? '');
$backend_class = (string)$item['backend-class']; if ($backend_class !== null) {
if ($backend_class !== '') { $backend_class->delete($item['backend-ref'] ?? '');
$backend_class::delete($item['backend-ref']);
} }
} }

View file

@ -10,12 +10,11 @@ use Friendica\Core\Cache;
use Friendica\Core\Config; use Friendica\Core\Config;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\StorageManager;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\Database\DBStructure; use Friendica\Database\DBStructure;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\SystemResource;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Images; use Friendica\Util\Images;
@ -172,26 +171,24 @@ class Photo
*/ */
public static function getImageForPhoto(array $photo) public static function getImageForPhoto(array $photo)
{ {
$data = ""; if (empty($photo['backend-class'])) {
if ($photo["backend-class"] == "") {
// legacy data storage in "data" column // legacy data storage in "data" column
$i = self::selectFirst(["data"], ["id" => $photo["id"]]); $i = self::selectFirst(['data'], ['id' => $photo['id']]);
if ($i === false) { if ($i === false) {
return null; return null;
} }
$data = $i["data"]; $data = $i['data'];
} else { } else {
$backendClass = $photo["backend-class"]; $backendClass = DI::storageManager()->getByName($photo['backend-class'] ?? '');
$backendRef = $photo["backend-ref"]; $backendRef = $photo['backend-ref'] ?? '';
$data = $backendClass::get($backendRef); $data = $backendClass->get($backendRef);
} }
if ($data === "") { if (empty($data)) {
return null; return null;
} }
return new Image($data, $photo["type"]); return new Image($data, $photo['type']);
} }
/** /**
@ -223,10 +220,10 @@ class Photo
$values = array_fill(0, count($fields), ""); $values = array_fill(0, count($fields), "");
$photo = array_combine($fields, $values); $photo = array_combine($fields, $values);
$photo["backend-class"] = Storage\SystemResource::class; $photo['backend-class'] = SystemResource::NAME;
$photo["backend-ref"] = $filename; $photo['backend-ref'] = $filename;
$photo["type"] = $mimetype; $photo['type'] = $mimetype;
$photo["cacheable"] = false; $photo['cacheable'] = false;
return $photo; return $photo;
} }
@ -273,18 +270,17 @@ class Photo
$data = ""; $data = "";
$backend_ref = ""; $backend_ref = "";
/** @var IStorage $backend_class */
if (DBA::isResult($existing_photo)) { if (DBA::isResult($existing_photo)) {
$backend_ref = (string)$existing_photo["backend-ref"]; $backend_ref = (string)$existing_photo["backend-ref"];
$backend_class = (string)$existing_photo["backend-class"]; $storage = DI::storageManager()->getByName($existing_photo["backend-class"] ?? '');
} else { } else {
$backend_class = StorageManager::getBackend(); $storage = DI::storage();
} }
if ($backend_class === "") { if ($storage === null) {
$data = $Image->asString(); $data = $Image->asString();
} else { } else {
$backend_ref = $backend_class::put($Image->asString(), $backend_ref); $backend_ref = $storage->put($Image->asString(), $backend_ref);
} }
@ -309,7 +305,7 @@ class Photo
"deny_cid" => $deny_cid, "deny_cid" => $deny_cid,
"deny_gid" => $deny_gid, "deny_gid" => $deny_gid,
"desc" => $desc, "desc" => $desc,
"backend-class" => $backend_class, "backend-class" => (string)$storage,
"backend-ref" => $backend_ref "backend-ref" => $backend_ref
]; ];
@ -340,10 +336,9 @@ class Photo
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions); $photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
foreach($photos as $photo) { foreach($photos as $photo) {
/** @var IStorage $backend_class */ $backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? '');
$backend_class = (string)$photo["backend-class"]; if ($backend_class !== null) {
if ($backend_class !== "") { $backend_class->delete($photo["backend-ref"] ?? '');
$backend_class::delete($photo["backend-ref"]);
} }
} }
@ -370,10 +365,9 @@ class Photo
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions); $photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
foreach($photos as $photo) { foreach($photos as $photo) {
/** @var IStorage $backend_class */ $backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? '');
$backend_class = (string)$photo["backend-class"]; if ($backend_class !== null) {
if ($backend_class !== "") { $fields["backend-ref"] = $backend_class->put($img->asString(), $photo['backend-ref']);
$fields["backend-ref"] = $backend_class::put($img->asString(), $photo["backend-ref"]);
} else { } else {
$fields["data"] = $img->asString(); $fields["data"] = $img->asString();
} }

View file

@ -0,0 +1,32 @@
<?php
namespace Friendica\Model\Storage;
use Friendica\Core\L10n\L10n;
use Psr\Log\LoggerInterface;
/**
* A general storage class which loads common dependencies and implements common methods
*/
abstract class AbstractStorage implements IStorage
{
/** @var L10n */
protected $l10n;
/** @var LoggerInterface */
protected $logger;
/**
* @param L10n $l10n
* @param LoggerInterface $logger
*/
public function __construct(L10n $l10n, LoggerInterface $logger)
{
$this->l10n = $l10n;
$this->logger = $logger;
}
public function __toString()
{
return static::getName();
}
}

View file

@ -6,58 +6,100 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use Friendica\Core\Logger; use Friendica\Core\L10n\L10n;
use Friendica\Core\L10n; use Psr\Log\LoggerInterface;
use Friendica\Database\DBA; use Friendica\Database\Database as DBA;
/** /**
* @brief Database based storage system * @brief Database based storage system
* *
* This class manage data stored in database table. * This class manage data stored in database table.
*/ */
class Database implements IStorage class Database extends AbstractStorage
{ {
public static function get($ref) const NAME = 'Database';
/** @var DBA */
private $dba;
/**
* @param DBA $dba
* @param LoggerInterface $logger
* @param L10n $l10n
*/
public function __construct(DBA $dba, LoggerInterface $logger, L10n $l10n)
{ {
$r = DBA::selectFirst('storage', ['data'], ['id' => $ref]); parent::__construct($l10n, $logger);
if (!DBA::isResult($r)) {
$this->dba = $dba;
}
/**
* @inheritDoc
*/
public function get(string $reference)
{
$result = $this->dba->selectFirst('storage', ['data'], ['id' => $reference]);
if (!$this->dba->isResult($result)) {
return ''; return '';
} }
return $r['data']; return $result['data'];
} }
public static function put($data, $ref = '') /**
* @inheritDoc
*/
public function put(string $data, string $reference = '')
{ {
if ($ref !== '') { if ($reference !== '') {
$r = DBA::update('storage', ['data' => $data], ['id' => $ref]); $result = $this->dba->update('storage', ['data' => $data], ['id' => $reference]);
if ($r === false) { if ($result === false) {
Logger::log('Failed to update data with id ' . $ref . ': ' . DBA::errorNo() . ' : ' . DBA::errorMessage()); $this->logger->warning('Failed to update data.', ['id' => $reference, 'errorCode' => $this->dba->errorNo(), 'errorMessage' => $this->dba->errorMessage()]);
throw new StorageException(L10n::t('Database storage failed to update %s', $ref)); throw new StorageException($this->l10n->t('Database storage failed to update %s', $reference));
} }
return $ref;
return $reference;
} else { } else {
$r = DBA::insert('storage', ['data' => $data]); $result = $this->dba->insert('storage', ['data' => $data]);
if ($r === false) { if ($result === false) {
Logger::log('Failed to insert data: ' . DBA::errorNo() . ' : ' . DBA::errorMessage()); $this->logger->warning('Failed to insert data.', ['errorCode' => $this->dba->errorNo(), 'errorMessage' => $this->dba->errorMessage()]);
throw new StorageException(L10n::t('Database storage failed to insert data')); throw new StorageException($this->l10n->t('Database storage failed to insert data'));
} }
return DBA::lastInsertId();
return $this->dba->lastInsertId();
} }
} }
public static function delete($ref) /**
* @inheritDoc
*/
public function delete(string $reference)
{ {
return DBA::delete('storage', ['id' => $ref]); return $this->dba->delete('storage', ['id' => $reference]);
} }
public static function getOptions() /**
* @inheritDoc
*/
public function getOptions()
{ {
return []; return [];
} }
public static function saveOptions($data) /**
* @inheritDoc
*/
public function saveOptions(array $data)
{ {
return []; return [];
} }
/**
* @inheritDoc
*/
public static function getName()
{
return self::NAME;
}
} }

View file

@ -6,10 +6,10 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use Friendica\Core\Config; use Friendica\Core\Config\IConfiguration;
use Friendica\Core\L10n; use Friendica\Core\L10n\L10n;
use Friendica\Core\Logger;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
/** /**
* @brief Filesystem based storage backend * @brief Filesystem based storage backend
@ -21,52 +21,72 @@ use Friendica\Util\Strings;
* Each new resource gets a value as reference and is saved in a * Each new resource gets a value as reference and is saved in a
* folder tree stucture created from that value. * folder tree stucture created from that value.
*/ */
class Filesystem implements IStorage class Filesystem extends AbstractStorage
{ {
const NAME = 'Filesystem';
// Default base folder // Default base folder
const DEFAULT_BASE_FOLDER = 'storage'; const DEFAULT_BASE_FOLDER = 'storage';
private static function getBasePath() /** @var IConfiguration */
private $config;
/** @var string */
private $basePath;
/**
* Filesystem constructor.
*
* @param IConfiguration $config
* @param LoggerInterface $logger
* @param L10n $l10n
*/
public function __construct(IConfiguration $config, LoggerInterface $logger, L10n $l10n)
{ {
$path = Config::get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER); parent::__construct($l10n, $logger);
return rtrim($path, '/');
$this->config = $config;
$path = $this->config->get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER);
$this->basePath = rtrim($path, '/');
} }
/** /**
* @brief Split data ref and return file path * @brief Split data ref and return file path
* @param string $ref Data reference *
* @param string $reference Data reference
*
* @return string * @return string
*/ */
private static function pathForRef($ref) private function pathForRef(string $reference)
{ {
$base = self::getBasePath(); $fold1 = substr($reference, 0, 2);
$fold1 = substr($ref, 0, 2); $fold2 = substr($reference, 2, 2);
$fold2 = substr($ref, 2, 2); $file = substr($reference, 4);
$file = substr($ref, 4);
return implode('/', [$base, $fold1, $fold2, $file]); return implode('/', [$this->basePath, $fold1, $fold2, $file]);
} }
/** /**
* @brief Create dirctory tree to store file, with .htaccess and index.html files * @brief Create dirctory tree to store file, with .htaccess and index.html files
*
* @param string $file Path and filename * @param string $file Path and filename
*
* @throws StorageException * @throws StorageException
*/ */
private static function createFoldersForFile($file) private function createFoldersForFile(string $file)
{ {
$path = dirname($file); $path = dirname($file);
if (!is_dir($path)) { if (!is_dir($path)) {
if (!mkdir($path, 0770, true)) { if (!mkdir($path, 0770, true)) {
Logger::log('Failed to create dirs ' . $path); $this->logger->warning('Failed to create dir.', ['path' => $path]);
throw new StorageException(L10n::t('Filesystem storage failed to create "%s". Check you write permissions.', $path)); throw new StorageException($this->l10n->t('Filesystem storage failed to create "%s". Check you write permissions.', $path));
} }
} }
$base = self::getBasePath(); while ($path !== $this->basePath) {
while ($path !== $base) {
if (!is_file($path . '/index.html')) { if (!is_file($path . '/index.html')) {
file_put_contents($path . '/index.html', ''); file_put_contents($path . '/index.html', '');
} }
@ -80,9 +100,12 @@ class Filesystem implements IStorage
} }
} }
public static function get($ref) /**
* @inheritDoc
*/
public function get(string $reference)
{ {
$file = self::pathForRef($ref); $file = $this->pathForRef($reference);
if (!is_file($file)) { if (!is_file($file)) {
return ''; return '';
} }
@ -90,27 +113,33 @@ class Filesystem implements IStorage
return file_get_contents($file); return file_get_contents($file);
} }
public static function put($data, $ref = '') /**
* @inheritDoc
*/
public function put(string $data, string $reference = '')
{ {
if ($ref === '') { if ($reference === '') {
$ref = Strings::getRandomHex(); $reference = Strings::getRandomHex();
} }
$file = self::pathForRef($ref); $file = $this->pathForRef($reference);
self::createFoldersForFile($file); $this->createFoldersForFile($file);
$r = file_put_contents($file, $data); if (!file_put_contents($file, $data)) {
if ($r === FALSE) { $this->logger->warning('Failed to write data.', ['file' => $file]);
Logger::log('Failed to write data to ' . $file); throw new StorageException($this->l10n->t('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
throw new StorageException(L10n::t('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
} }
chmod($file, 0660); chmod($file, 0660);
return $ref; return $reference;
} }
public static function delete($ref) /**
* @inheritDoc
*/
public function delete(string $reference)
{ {
$file = self::pathForRef($ref); $file = $this->pathForRef($reference);
// return true if file doesn't exists. we want to delete it: success with zero work! // return true if file doesn't exists. we want to delete it: success with zero work!
if (!is_file($file)) { if (!is_file($file)) {
return true; return true;
@ -118,28 +147,42 @@ class Filesystem implements IStorage
return unlink($file); return unlink($file);
} }
public static function getOptions() /**
* @inheritDoc
*/
public function getOptions()
{ {
return [ return [
'storagepath' => [ 'storagepath' => [
'input', 'input',
L10n::t('Storage base path'), $this->l10n->t('Storage base path'),
self::getBasePath(), $this->basePath,
L10n::t('Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree') $this->l10n->t('Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree')
] ]
]; ];
} }
public static function saveOptions($data) /**
* @inheritDoc
*/
public function saveOptions(array $data)
{ {
$storagepath = $data['storagepath'] ?? ''; $storagePath = $data['storagepath'] ?? '';
if ($storagepath === '' || !is_dir($storagepath)) { if ($storagePath === '' || !is_dir($storagePath)) {
return [ return [
'storagepath' => L10n::t('Enter a valid existing folder') 'storagepath' => $this->l10n->t('Enter a valid existing folder')
]; ];
}; };
Config::set('storage', 'filesystem_path', $storagepath); $this->config->set('storage', 'filesystem_path', $storagePath);
$this->basePath = $storagePath;
return []; return [];
} }
/**
* @inheritDoc
*/
public static function getName()
{
return self::NAME;
}
} }

View file

@ -13,25 +13,31 @@ interface IStorage
{ {
/** /**
* @brief Get data from backend * @brief Get data from backend
* @param string $ref Data reference *
* @param string $reference Data reference
*
* @return string * @return string
*/ */
public static function get($ref); public function get(string $reference);
/** /**
* @brief Put data in backend as $ref. If $ref is not defined a new reference is created. * @brief Put data in backend as $ref. If $ref is not defined a new reference is created.
*
* @param string $data Data to save * @param string $data Data to save
* @param string $ref Data referece. Optional. * @param string $reference Data reference. Optional.
* @return string Saved data referece *
* @return string Saved data reference
*/ */
public static function put($data, $ref = ""); public function put(string $data, string $reference = "");
/** /**
* @brief Remove data from backend * @brief Remove data from backend
* @param string $ref Data referece *
* @param string $reference Data reference
*
* @return boolean True on success * @return boolean True on success
*/ */
public static function delete($ref); public function delete(string $reference);
/** /**
* @brief Get info about storage options * @brief Get info about storage options
@ -71,7 +77,7 @@ interface IStorage
* *
* See https://github.com/friendica/friendica/wiki/Quick-Template-Guide * See https://github.com/friendica/friendica/wiki/Quick-Template-Guide
*/ */
public static function getOptions(); public function getOptions();
/** /**
* @brief Validate and save options * @brief Validate and save options
@ -82,8 +88,19 @@ interface IStorage
* *
* Return array must be empty if no error. * Return array must be empty if no error.
*/ */
public static function saveOptions($data); public function saveOptions(array $data);
/**
* The name of the backend
*
* @return string
*/
public function __toString();
/**
* The name of the backend
*
* @return string
*/
public static function getName();
} }

View file

@ -16,10 +16,15 @@ use \BadMethodCallException;
*/ */
class SystemResource implements IStorage class SystemResource implements IStorage
{ {
const NAME = 'SystemResource';
// Valid folders to look for resources // Valid folders to look for resources
const VALID_FOLDERS = ["images"]; const VALID_FOLDERS = ["images"];
public static function get($filename) /**
* @inheritDoc
*/
public function get(string $filename)
{ {
$folder = dirname($filename); $folder = dirname($filename);
if (!in_array($folder, self::VALID_FOLDERS)) { if (!in_array($folder, self::VALID_FOLDERS)) {
@ -31,25 +36,48 @@ class SystemResource implements IStorage
return file_get_contents($filename); return file_get_contents($filename);
} }
/**
public static function put($data, $filename = "") * @inheritDoc
*/
public function put(string $data, string $filename = '')
{ {
throw new BadMethodCallException(); throw new BadMethodCallException();
} }
public static function delete($filename) public function delete(string $filename)
{ {
throw new BadMethodCallException(); throw new BadMethodCallException();
} }
public static function getOptions() /**
* @inheritDoc
*/
public function getOptions()
{ {
return []; return [];
} }
public static function saveOptions($data) /**
* @inheritDoc
*/
public function saveOptions(array $data)
{ {
return []; return [];
} }
/**
* @inheritDoc
*/
public function __toString()
{
return self::NAME;
}
/**
* @inheritDoc
*/
public static function getName()
{
return self::NAME;
}
} }

View file

@ -199,15 +199,11 @@ class Site extends BaseAdminModule
$relay_user_tags = !empty($_POST['relay_user_tags']); $relay_user_tags = !empty($_POST['relay_user_tags']);
$active_panel = (!empty($_POST['active_panel']) ? "#" . Strings::escapeTags(trim($_POST['active_panel'])) : ''); $active_panel = (!empty($_POST['active_panel']) ? "#" . Strings::escapeTags(trim($_POST['active_panel'])) : '');
/**
* @var $storagebackend \Friendica\Model\Storage\IStorage
*/
$storagebackend = Strings::escapeTags(trim($_POST['storagebackend'] ?? '')); $storagebackend = Strings::escapeTags(trim($_POST['storagebackend'] ?? ''));
// save storage backend form // save storage backend form
if (!is_null($storagebackend) && $storagebackend != "") { if (DI::storageManager()->setBackend($storagebackend)) {
if (StorageManager::setBackend($storagebackend)) { $storage_opts = DI::storage()->getOptions();
$storage_opts = $storagebackend::getOptions();
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $storagebackend); $storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $storagebackend);
$storage_opts_data = []; $storage_opts_data = [];
foreach ($storage_opts as $name => $info) { foreach ($storage_opts as $name => $info) {
@ -225,7 +221,7 @@ class Site extends BaseAdminModule
unset($name); unset($name);
unset($info); unset($info);
$storage_form_errors = $storagebackend::saveOptions($storage_opts_data); $storage_form_errors = DI::storage()->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('Storage backend, ' . $storage_opts[$name][1] . ': ' . $err); notice('Storage backend, ' . $storage_opts[$name][1] . ': ' . $err);
@ -235,7 +231,6 @@ class Site extends BaseAdminModule
} else { } else {
info(L10n::t('Invalid storage backend setting value.')); info(L10n::t('Invalid storage backend setting value.'));
} }
}
// Has the directory url changed? If yes, then resubmit the existing profiles there // Has the directory url changed? If yes, then resubmit the existing profiles there
if ($global_directory != Config::get('system', 'directory') && ($global_directory != '')) { if ($global_directory != Config::get('system', 'directory') && ($global_directory != '')) {
@ -530,29 +525,25 @@ class Site extends BaseAdminModule
$optimize_max_tablesize = -1; $optimize_max_tablesize = -1;
} }
$storage_backends = StorageManager::listBackends(); $current_storage_backend = DI::storage();
/** @var $current_storage_backend \Friendica\Model\Storage\IStorage */
$current_storage_backend = StorageManager::getBackend();
$available_storage_backends = []; $available_storage_backends = [];
// show legacy option only if it is the current backend: // show legacy option only if it is the current backend:
// once changed can't be selected anymore // once changed can't be selected anymore
if ($current_storage_backend == '') { if ($current_storage_backend == null) {
$available_storage_backends[''] = L10n::t('Database (legacy)'); $available_storage_backends[''] = L10n::t('Database (legacy)');
} }
foreach ($storage_backends as $name => $class) { foreach (DI::storageManager()->listBackends() as $name => $class) {
$available_storage_backends[$class] = $name; $available_storage_backends[$name] = $name;
} }
unset($storage_backends);
// build storage config form, // build storage config form,
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|' ,'', $current_storage_backend); $storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|' ,'', $current_storage_backend);
$storage_form = []; $storage_form = [];
if (!is_null($current_storage_backend) && $current_storage_backend != '') { if (!is_null($current_storage_backend) && $current_storage_backend != '') {
foreach ($current_storage_backend::getOptions() as $name => $info) { foreach ($current_storage_backend->getOptions() as $name => $info) {
$type = $info[0]; $type = $info[0];
$info[0] = $storage_form_prefix . '_' . $name; $info[0] = $storage_form_prefix . '_' . $name;
$info['type'] = $type; $info['type'] = $type;

View file

@ -324,8 +324,8 @@ class CronJobs
*/ */
private static function moveStorage() private static function moveStorage()
{ {
$current = StorageManager::getBackend(); $current = DI::storage();
$moved = StorageManager::move($current); $moved = DI::storageManager()->move($current);
if ($moved) { if ($moved) {
Worker::add(PRIORITY_LOW, "CronJobs", "move_storage"); Worker::add(PRIORITY_LOW, "CronJobs", "move_storage");

View file

@ -34,7 +34,7 @@
use Friendica\Database\DBA; use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) { if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1329); define('DB_UPDATE_VERSION', 1330);
} }
return [ return [

View file

@ -8,8 +8,10 @@ use Friendica\Core\L10n\L10n;
use Friendica\Core\Lock\ILock; use Friendica\Core\Lock\ILock;
use Friendica\Core\Process; use Friendica\Core\Process;
use Friendica\Core\Session\ISession; use Friendica\Core\Session\ISession;
use Friendica\Core\StorageManager;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Factory; use Friendica\Factory;
use Friendica\Model\Storage\IStorage;
use Friendica\Model\User\Cookie; use Friendica\Model\User\Cookie;
use Friendica\Util; use Friendica\Util;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -193,5 +195,11 @@ return [
'constructParams' => [ 'constructParams' => [
$_SERVER, $_COOKIE $_SERVER, $_COOKIE
], ],
] ],
IStorage::class => [
'instanceOf' => StorageManager::class,
'call' => [
['getBackend', [], Dice::CHAIN_CALL],
],
],
]; ];

View file

@ -0,0 +1,106 @@
<?php
namespace Friendica\Test\Util;
use Friendica\App;
use Friendica\Core\Hook;
use Friendica\Model\Storage\IStorage;
use Friendica\Core\L10n\L10n;
use Mockery\MockInterface;
/**
* A backend storage example class
*/
class SampleStorageBackend implements IStorage
{
const NAME = 'Sample Storage';
/** @var L10n */
private $l10n;
/** @var array */
private $options = [
'filename' => [
'input', // will use a simple text input
'The file to return', // the label
'sample', // the current value
'Enter the path to a file', // the help text
// no extra data for 'input' type..
],
];
/** @var array Just save the data in memory */
private $data = [];
/**
* SampleStorageBackend constructor.
*
* @param L10n $l10n The configuration of Friendica
*
* 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(L10n $l10n)
{
$this->l10n = $l10n;
}
public function get(string $reference)
{
// we return always the same image data. Which file we load is defined by
// a config key
return $this->data[$reference] ?? null;
}
public function put(string $data, string $reference = '')
{
if ($reference === '') {
$reference = 'sample';
}
$this->data[$reference] = $data;
return $reference;
}
public function delete(string $reference)
{
if (isset($this->data[$reference])) {
unset($this->data[$reference]);
}
return true;
}
public function getOptions()
{
return $this->options;
}
public function saveOptions(array $data)
{
$this->options = $data;
// no errors, return empty array
return $this->options;
}
public function __toString()
{
return self::NAME;
}
public static function getName()
{
return self::NAME;
}
/**
* This one is a hack to register this class to the hook
*/
public static function registerHook()
{
Hook::register('storage_instance', __DIR__ . '/SampleStorageBackendInstance.php', 'create_instance');
}
}

View file

@ -0,0 +1,18 @@
<?php
// contains a test-hook call for creating a storage instance
use Friendica\App;
use Friendica\Core\L10n\L10n;
use Friendica\Test\Util\SampleStorageBackend;
use Mockery\MockInterface;
function create_instance(App $a, &$data)
{
/** @var L10n|MockInterface $l10n */
$l10n = \Mockery::mock(L10n::class);
if ($data['name'] == SampleStorageBackend::getName()) {
$data['storage'] = new SampleStorageBackend($l10n);
}
}

View file

@ -0,0 +1,40 @@
<?php
return [
'photo' => [
// move from data-attribute to storage backend
[
'id' => 1,
'backend-class' => null,
'backend-ref' => 'f0c0d0i2',
'data' => 'without class',
],
// move from storage-backend to maybe filesystem backend, skip at database backend
[
'id' => 2,
'backend-class' => 'Database',
'backend-ref' => 1,
'data' => '',
],
// move data if invalid storage
[
'id' => 3,
'backend-class' => 'invalid!',
'backend-ref' => 'unimported',
'data' => 'invalid data moved',
],
// skip everytime because of invalid storage and no data
[
'id' => 3,
'backend-class' => 'invalid!',
'backend-ref' => 'unimported',
'data' => '',
],
],
'storage' => [
[
'id' => 1,
'data' => 'inside database',
],
],
];

View file

@ -0,0 +1,264 @@
<?php
namespace Friendica\Test\src\Core;
use Dice\Dice;
use Friendica\Core\Config\IConfiguration;
use Friendica\Core\Config\PreloadConfiguration;
use Friendica\Core\Hook;
use Friendica\Core\L10n\L10n;
use Friendica\Core\Session\ISession;
use Friendica\Core\StorageManager;
use Friendica\Database\Database;
use Friendica\DI;
use Friendica\Factory\ConfigFactory;
use Friendica\Model\Config\Config;
use Friendica\Model\Storage;
use Friendica\Core\Session;
use Friendica\Test\DatabaseTest;
use Friendica\Test\Util\Database\StaticDatabase;
use Friendica\Test\Util\VFSTrait;
use Friendica\Util\ConfigFileLoader;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Friendica\Test\Util\SampleStorageBackend;
class StorageManagerTest extends DatabaseTest
{
/** @var Database */
private $dba;
/** @var IConfiguration */
private $config;
/** @var LoggerInterface */
private $logger;
/** @var L10n */
private $l10n;
use VFSTrait;
public function setUp()
{
parent::setUp();
$this->setUpVfsDir();
$this->logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class);
$profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true);
// load real config to avoid mocking every config-entry which is related to the Database class
$configFactory = new ConfigFactory();
$loader = new ConfigFileLoader($this->root->url());
$configCache = $configFactory->createCache($loader);
$this->dba = new StaticDatabase($configCache, $profiler, $this->logger);
$configModel = new Config($this->dba);
$this->config = new PreloadConfiguration($configCache, $configModel);
$this->l10n = \Mockery::mock(L10n::class);
}
/**
* Test plain instancing first
*/
public function testInstance()
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertInstanceOf(StorageManager::class, $storageManager);
}
public function dataStorages()
{
return [
'empty' => [
'name' => '',
'assert' => null,
'assertName' => '',
'userBackend' => false,
],
'database' => [
'name' => Storage\Database::NAME,
'assert' => Storage\Database::class,
'assertName' => Storage\Database::NAME,
'userBackend' => true,
],
'filesystem' => [
'name' => Storage\Filesystem::NAME,
'assert' => Storage\Filesystem::class,
'assertName' => Storage\Filesystem::NAME,
'userBackend' => true,
],
'systemresource' => [
'name' => Storage\SystemResource::NAME,
'assert' => Storage\SystemResource::class,
'assertName' => Storage\SystemResource::NAME,
// false here, because SystemResource isn't meant to be a user backend,
// it's for system resources only
'userBackend' => false,
],
'invalid' => [
'name' => 'invalid',
'assert' => null,
'assertName' => '',
'userBackend' => false,
],
];
}
/**
* Test the getByName() method
*
* @dataProvider dataStorages
*/
public function testGetByName($name, $assert, $assertName, $userBackend)
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$storage = $storageManager->getByName($name, $userBackend);
if (!empty($assert)) {
$this->assertInstanceOf(Storage\IStorage::class, $storage);
$this->assertInstanceOf($assert, $storage);
$this->assertEquals($name, $storage::getName());
} else {
$this->assertNull($storage);
}
$this->assertEquals($assertName, $storage);
}
/**
* Test the isValidBackend() method
*
* @dataProvider dataStorages
*/
public function testIsValidBackend($name, $assert, $assertName, $userBackend)
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertEquals($userBackend, $storageManager->isValidBackend($name));
}
/**
* Test the method listBackends() with default setting
*/
public function testListBackends()
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends());
}
/**
* Test the method getBackend()
*
* @dataProvider dataStorages
*/
public function testGetBackend($name, $assert, $assertName, $userBackend)
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertNull($storageManager->getBackend());
if ($userBackend) {
$storageManager->setBackend($name);
$this->assertInstanceOf($assert, $storageManager->getBackend());
}
}
/**
* Test the method getBackend() with a pre-configured backend
*
* @dataProvider dataStorages
*/
public function testPresetBackend($name, $assert, $assertName, $userBackend)
{
$this->config->set('storage', 'name', $name);
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
if ($userBackend) {
$this->assertInstanceOf($assert, $storageManager->getBackend());
} else {
$this->assertNull($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(ISession::class, ['instanceOf' => Session\Memory::class, 'shared' => true, 'call' => null]);
DI::init($dice);
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertTrue($storageManager->register(SampleStorageBackend::class));
$this->assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [
SampleStorageBackend::getName() => SampleStorageBackend::class,
]), $storageManager->listBackends());
$this->assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [
SampleStorageBackend::getName() => SampleStorageBackend::class,
]), $this->config->get('storage', 'backends'));
// inline call to register own class as hook (testing purpose only)
SampleStorageBackend::registerHook();
Hook::loadHooks();
$this->assertTrue($storageManager->setBackend(SampleStorageBackend::NAME));
$this->assertEquals(SampleStorageBackend::NAME, $this->config->get('storage', 'name'));
$this->assertInstanceOf(SampleStorageBackend::class, $storageManager->getBackend());
$this->assertTrue($storageManager->unregister(SampleStorageBackend::class));
$this->assertEquals(StorageManager::DEFAULT_BACKENDS, $this->config->get('storage', 'backends'));
$this->assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends());
$this->assertNull($storageManager->getBackend());
$this->assertNull($this->config->get('storage', 'name'));
}
/**
* Test moving data to a new storage (currently testing db & filesystem)
*
* @dataProvider dataStorages
*/
public function testMoveStorage($name, $assert, $assertName, $userBackend)
{
if (!$userBackend) {
return;
}
$this->loadFixture(__DIR__ . '/../../datasets/storage/database.fixture.php', $this->dba);
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$storage = $storageManager->getByName($name);
$storageManager->move($storage);
$photos = $this->dba->select('photo', ['backend-ref', 'backend-class', 'id', 'data']);
while ($photo = $this->dba->fetch($photos)) {
$this->assertEmpty($photo['data']);
$storage = $storageManager->getByName($photo['backend-class']);
$data = $storage->get($photo['backend-ref']);
$this->assertNotEmpty($data);
}
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Friendica\Test\src\Model\Storage;
use Friendica\Core\L10n\L10n;
use Friendica\Factory\ConfigFactory;
use Friendica\Model\Storage\Database;
use Friendica\Model\Storage\IStorage;
use Friendica\Test\DatabaseTestTrait;
use Friendica\Test\Util\Database\StaticDatabase;
use Friendica\Test\Util\VFSTrait;
use Friendica\Util\ConfigFileLoader;
use Friendica\Util\Profiler;
use Mockery\MockInterface;
use Psr\Log\NullLogger;
class DatabaseStorageTest extends StorageTest
{
use DatabaseTestTrait;
use VFSTrait;
protected function setUp()
{
$this->setUpVfsDir();
parent::setUp();
}
protected function getInstance()
{
$logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class);
$profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true);
// load real config to avoid mocking every config-entry which is related to the Database class
$configFactory = new ConfigFactory();
$loader = new ConfigFileLoader($this->root->url());
$configCache = $configFactory->createCache($loader);
$dba = new StaticDatabase($configCache, $profiler, $logger);
/** @var MockInterface|L10n $l10n */
$l10n = \Mockery::mock(L10n::class)->makePartial();
return new Database($dba, $logger, $l10n);
}
protected function assertOption(IStorage $storage)
{
$this->assertEmpty($storage->getOptions());
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Friendica\Test\src\Model\Storage;
use Friendica\Core\Config\IConfiguration;
use Friendica\Core\L10n\L10n;
use Friendica\Model\Storage\Filesystem;
use Friendica\Model\Storage\IStorage;
use Friendica\Test\Util\VFSTrait;
use Friendica\Util\Profiler;
use Mockery\MockInterface;
use org\bovigo\vfs\vfsStream;
use Psr\Log\NullLogger;
use function GuzzleHttp\Psr7\uri_for;
class FilesystemStorageTest extends StorageTest
{
use VFSTrait;
/** @var MockInterface|IConfiguration */
protected $config;
protected function setUp()
{
$this->setUpVfsDir();
vfsStream::create(['storage' => []], $this->root);
parent::setUp();
}
protected function getInstance()
{
$logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class);
$profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true);
/** @var MockInterface|L10n $l10n */
$l10n = \Mockery::mock(L10n::class)->makePartial();
$this->config = \Mockery::mock(IConfiguration::class);
$this->config->shouldReceive('get')
->with('storage', 'filesystem_path', Filesystem::DEFAULT_BASE_FOLDER)
->andReturn($this->root->getChild('storage')->url());
return new Filesystem($this->config, $logger, $l10n);
}
protected function assertOption(IStorage $storage)
{
$this->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
*
* @expectedException \Friendica\Model\Storage\StorageException
* @expectedExceptionMessageRegExp /Filesystem storage failed to create \".*\". Check you write permissions./
*/
public function testMissingDirPermissions()
{
$this->root->getChild('storage')->chmod(000);
$instance = $this->getInstance();
$instance->put('test');
}
/**
* Test the exception in case of missing file permissions
*
* @expectedException \Friendica\Model\Storage\StorageException
* @expectedExceptionMessageRegExp /Filesystem storage failed to save data to \".*\". Check your write permissions/
*/
public function testMissingFilePermissions()
{
$this->markTestIncomplete("Cannot catch file_put_content() error due vfsStream failure");
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();
$this->assertDirectoryExists($dir);
$this->assertFileExists($file);
$this->assertDirectoryIsWritable($dir);
$this->assertFileIsWritable($file);
$this->assertEquals('test', file_get_contents($file));
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Friendica\Test\src\Model\Storage;
use Friendica\Model\Storage\IStorage;
use Friendica\Test\MockedTest;
abstract class StorageTest extends MockedTest
{
/** @return IStorage */
abstract protected function getInstance();
abstract protected function assertOption(IStorage $storage);
/**
* Test if the instance is "really" implementing the interface
*/
public function testInstance()
{
$instance = $this->getInstance();
$this->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
*/
public function testPutGetDelete()
{
$instance = $this->getInstance();
$ref = $instance->put('data12345');
$this->assertNotEmpty($ref);
$this->assertEquals('data12345', $instance->get($ref));
$this->assertTrue($instance->delete($ref));
}
/**
* Test a delete with an invalid reference
*/
public function testInvalidDelete()
{
$instance = $this->getInstance();
// Even deleting not existing references should return "true"
$this->assertTrue($instance->delete(-1234456));
}
/**
* Test a get with an invalid reference
*/
public function testInvalidGet()
{
$instance = $this->getInstance();
// Invalid references return an empty string
$this->assertEmpty($instance->get(-123456));
}
/**
* Test an update with a given reference
*/
public function testUpdateReference()
{
$instance = $this->getInstance();
$ref = $instance->put('data12345');
$this->assertNotEmpty($ref);
$this->assertEquals('data12345', $instance->get($ref));
$this->assertEquals($ref, $instance->put('data5432', $ref));
$this->assertEquals('data5432', $instance->get($ref));
}
/**
* Test that an invalid update results in an insert
*/
public function testInvalidUpdate()
{
$instance = $this->getInstance();
$this->assertEquals(-123, $instance->put('data12345', -123));
}
}

View file

@ -12,6 +12,7 @@ use Friendica\Model\Contact;
use Friendica\Model\GContact; use Friendica\Model\GContact;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Model\Storage;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Worker\Delivery; use Friendica\Worker\Delivery;
@ -408,3 +409,26 @@ function update_1327()
return Update::SUCCESS; return Update::SUCCESS;
} }
function update_1330()
{
$currStorage = Config::get('storage', 'class', '');
// set the name of the storage instead of the classpath as config
if (!empty($currStorage)) {
/** @var Storage\IStorage $currStorage */
if (!Config::set('storage', 'name', $currStorage::getName())) {
return Update::FAILED;
}
// try to delete the class since it isn't needed. This won't work with config files
Config::delete('storage', 'class');
}
// Update attachments and photos
if (!DBA::p("UPDATE `photo` SET `photo`.`backend-class` = SUBSTR(`photo`.`backend-class`, 22) WHERE `photo`.`backend-class` LIKE 'Friendica\\Model\\Storage\\%'") ||
!DBA::p("UPDATE `attach` SET `attach`.`backend-class` = SUBSTR(`attach`.`backend-class`, 22) WHERE `attach`.`backend-class` LIKE 'Friendica\\Model\\Storage\\%'")) {
return Update::FAILED;
};
return Update::SUCCESS;
}