mirror of
https://github.com/friendica/friendica
synced 2024-11-18 19:43:46 +00:00
Merge pull request #8062 from nupplaphil/Make-Storage
Make Storage testable & add tests
This commit is contained in:
commit
84cff91283
26 changed files with 1430 additions and 340 deletions
|
@ -1,6 +1,6 @@
|
|||
-- ------------------------------------------
|
||||
-- Friendica 2020.03-dev (Dalmatian Bellflower)
|
||||
-- DB_UPDATE_VERSION 1329
|
||||
-- DB_UPDATE_VERSION 1330
|
||||
-- ------------------------------------------
|
||||
|
||||
|
||||
|
|
|
@ -17,22 +17,24 @@ namespace Friendica\Model\Storage;
|
|||
```php
|
||||
interface IStorage
|
||||
{
|
||||
public static function get($ref);
|
||||
public static function put($data, $ref = "");
|
||||
public static function delete($ref);
|
||||
public static function getOptions();
|
||||
public static function saveOptions($data);
|
||||
public function get(string $reference);
|
||||
public function put(string $data, string $reference = '');
|
||||
public function delete(string $reference);
|
||||
public function getOptions();
|
||||
public function saveOptions(array $data);
|
||||
public function __toString();
|
||||
public static function getName();
|
||||
}
|
||||
```
|
||||
|
||||
- `get($ref)` returns data pointed by `$ref`
|
||||
- `put($data, $ref)` saves data in `$data` to position `$ref`, or a new position if `$ref` is empty.
|
||||
- `delete($ref)` delete data pointed by `$ref`
|
||||
- `get(string $reference)` returns data pointed by `$reference`
|
||||
- `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`
|
||||
|
||||
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.
|
||||
- `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:
|
||||
|
||||
|
@ -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.
|
||||
|
||||
`Friendica\Core\StorageManager::register($name, $class)` is used to register the backend class.
|
||||
The `$name` must be univocal and will be shown to admin.
|
||||
`DI::facStorage()->register(string $class)` is used to register the backend class.
|
||||
|
||||
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
|
||||
|
||||
|
@ -112,60 +141,91 @@ use Friendica\Core\L10n;
|
|||
|
||||
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
|
||||
$filename = Config::get("storage", "samplestorage", "sample.jpg");
|
||||
$filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
|
||||
return file_get_contents($filename);
|
||||
}
|
||||
|
||||
public static function put($data, $ref = "")
|
||||
public function put(string $data, string $reference = '')
|
||||
{
|
||||
if ($ref === "") {
|
||||
$ref = "sample";
|
||||
if ($reference === '') {
|
||||
$reference = 'sample';
|
||||
}
|
||||
// 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
|
||||
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 [
|
||||
"filename" => [
|
||||
"input", // will use a simple text input
|
||||
L10n::t("The file to return"), // the label
|
||||
'filename' => [
|
||||
'input', // will use a simple text input
|
||||
$this->l10n->t('The file to return'), // the label
|
||||
$filename, // the current value
|
||||
L10n::t("Enter the path to a file"), // the help text
|
||||
// no extra data for "input" type..
|
||||
$this->l10n->t('Enter the path to a file'), // the help text
|
||||
// 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()
|
||||
$newfilename = trim($data["filename"]);
|
||||
$newfilename = trim($data['filename']);
|
||||
|
||||
// this function should always validate the data.
|
||||
// in this example we check if file exists
|
||||
if (!file_exists($newfilename)) {
|
||||
// in case of error we return an array with
|
||||
// ["optionname" => "error message"]
|
||||
return ["filename" => "The file doesn't exists"];
|
||||
// ['optionname' => 'error message']
|
||||
return ['filename' => 'The file doesn\'t exists'];
|
||||
}
|
||||
|
||||
Config::set("storage", "samplestorage", $newfilename);
|
||||
$this->config->set('storage', 'samplestorage', $newfilename);
|
||||
|
||||
// no errors, return empty array
|
||||
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>
|
||||
*/
|
||||
|
||||
use Friendica\Core\StorageManager;
|
||||
use Friendica\Addon\samplestorage\SampleStorageBackend;
|
||||
use Friendica\DI;
|
||||
|
||||
function samplestorage_install()
|
||||
{
|
||||
// on addon install, we register our class with name "Sample Storage".
|
||||
// note: we use `::class` property, which returns full class name as string
|
||||
// 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()
|
||||
{
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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('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
|
||||
|
||||
Hook::callAll('globaldir_update', $arr);
|
||||
|
|
|
@ -424,6 +424,10 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
|
|||
### src/Core/Authentication.php
|
||||
|
||||
Hook::callAll('logged_in', $a->user);
|
||||
|
||||
### src/Core/StorageManager
|
||||
|
||||
Hook::callAll('storage_instance', $data);
|
||||
|
||||
### src/Worker/Directory.php
|
||||
|
||||
|
|
|
@ -13,6 +13,19 @@ class Storage extends \Asika\SimpleConsole\Console
|
|||
{
|
||||
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()
|
||||
{
|
||||
$help = <<<HELP
|
||||
|
@ -69,11 +82,11 @@ HELP;
|
|||
protected function doList()
|
||||
{
|
||||
$rowfmt = ' %-3s | %-20s';
|
||||
$current = StorageManager::getBackend();
|
||||
$current = $this->storageManager->getBackend();
|
||||
$this->out(sprintf($rowfmt, 'Sel', 'Name'));
|
||||
$this->out('-----------------------');
|
||||
$isregisterd = false;
|
||||
foreach (StorageManager::listBackends() as $name => $class) {
|
||||
foreach ($this->storageManager->listBackends() as $name => $class) {
|
||||
$issel = ' ';
|
||||
if ($current === $class) {
|
||||
$issel = '*';
|
||||
|
@ -100,14 +113,14 @@ HELP;
|
|||
}
|
||||
|
||||
$name = $this->args[1];
|
||||
$class = StorageManager::getByName($name);
|
||||
$class = $this->storageManager->getByName($name);
|
||||
|
||||
if ($class === '') {
|
||||
$this->out($name . ' is not a registered backend.');
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!StorageManager::setBackend($class)) {
|
||||
if (!$this->storageManager->setBackend($class)) {
|
||||
$this->out($class . ' is not a valid backend storage class.');
|
||||
return -1;
|
||||
}
|
||||
|
@ -130,11 +143,11 @@ HELP;
|
|||
$tables = [$table];
|
||||
}
|
||||
|
||||
$current = StorageManager::getBackend();
|
||||
$current = $this->storageManager->getBackend();
|
||||
$total = 0;
|
||||
|
||||
do {
|
||||
$moved = StorageManager::move($current, $tables, $this->getOption('n', 5000));
|
||||
$moved = $this->storageManager->move($current, $tables, $this->getOption('n', 5000));
|
||||
if ($moved) {
|
||||
$this->out(date('[Y-m-d H:i:s] ') . sprintf('Moved %d files', $moved));
|
||||
}
|
||||
|
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
namespace Friendica\Core;
|
||||
|
||||
use Friendica\Database\DBA;
|
||||
use Friendica\Model\Storage\IStorage;
|
||||
use Exception;
|
||||
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
|
||||
{
|
||||
private static $default_backends = [
|
||||
'Filesystem' => \Friendica\Model\Storage\Filesystem::class,
|
||||
'Database' => \Friendica\Model\Storage\Database::class,
|
||||
// Default tables to look for data
|
||||
const TABLES = ['photo', 'attach'];
|
||||
|
||||
// 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) {
|
||||
self::$backends = Config::get('storage', 'backends', self::$default_backends);
|
||||
}
|
||||
$this->dba = $dba;
|
||||
$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
|
||||
*
|
||||
* @return string
|
||||
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
|
||||
* @return Storage\IStorage|null
|
||||
*/
|
||||
public static function getBackend()
|
||||
public function getBackend()
|
||||
{
|
||||
return Config::get('storage', 'class', '');
|
||||
return $this->currentBackend;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return storage backend class by registered name
|
||||
*
|
||||
* @param string $name Backend name
|
||||
* @return string Empty if no backend registered at $name exists
|
||||
* @param string|null $name Backend name
|
||||
* @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();
|
||||
return self::$backends[$name] ?? '';
|
||||
// If there's no cached instance create a new instance
|
||||
if (!isset($this->backendInstances[$name])) {
|
||||
// If the current name isn't a valid backend (or the SystemResource instance) create it
|
||||
if ($this->isValidBackend($name, $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
|
||||
*
|
||||
* @param string $class Backend class name
|
||||
* @return bool
|
||||
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
|
||||
* @param string $name Backend class name
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
Config::set('storage', 'class', $class);
|
||||
|
||||
return true;
|
||||
if ($this->config->set('storage', 'name', $name)) {
|
||||
$this->currentBackend = $this->getByName($name);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,42 +165,63 @@ class StorageManager
|
|||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function listBackends()
|
||||
public function listBackends()
|
||||
{
|
||||
self::setup();
|
||||
return self::$backends;
|
||||
return $this->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
|
||||
* @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
|
||||
self::setup();
|
||||
self::$backends[$name] = $class;
|
||||
Config::set('storage', 'backends', self::$backends);
|
||||
}
|
||||
if (is_subclass_of($class, Storage\IStorage::class)) {
|
||||
/** @var Storage\IStorage $class */
|
||||
|
||||
$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
|
||||
*
|
||||
* @param string $name User readable backend name
|
||||
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
|
||||
* @param string $class Backend class name
|
||||
*
|
||||
* @return boolean True, if unregistering was successful
|
||||
*/
|
||||
public static function unregister($name)
|
||||
public function unregister(string $class)
|
||||
{
|
||||
self::setup();
|
||||
unset(self::$backends[$name]);
|
||||
Config::set('storage', 'backends', self::$backends);
|
||||
}
|
||||
if (is_subclass_of($class, Storage\IStorage::class)) {
|
||||
/** @var Storage\IStorage $class */
|
||||
|
||||
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
|
||||
|
@ -117,64 +229,60 @@ class StorageManager
|
|||
* Copy existing data to destination storage and delete from source.
|
||||
* This method cannot move to legacy in-table `data` field.
|
||||
*
|
||||
* @param string $destination Storage class name
|
||||
* @param array|null $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 Storage\IStorage $destination Destination storage class name
|
||||
* @param array $tables Tables to look in for resources. Optional, defaults to ['photo', 'attach']
|
||||
* @param int $limit Limit of the process batch size, defaults to 5000
|
||||
*
|
||||
* @return int Number of moved resources
|
||||
* @throws \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)) {
|
||||
throw new \Exception('Can\'t move to NULL storage backend');
|
||||
}
|
||||
|
||||
if (is_null($tables)) {
|
||||
$tables = ['photo', 'attach'];
|
||||
if ($destination === null) {
|
||||
throw new Storage\StorageException('Can\'t move to NULL storage backend');
|
||||
}
|
||||
|
||||
$moved = 0;
|
||||
foreach ($tables as $table) {
|
||||
// Get the rows where backend class is not the destination backend class
|
||||
$resources = DBA::select(
|
||||
$table,
|
||||
$resources = $this->dba->select(
|
||||
$table,
|
||||
['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]
|
||||
);
|
||||
|
||||
while ($resource = DBA::fetch($resources)) {
|
||||
$id = $resource['id'];
|
||||
$data = $resource['data'];
|
||||
/** @var IStorage $backendClass */
|
||||
$backendClass = $resource['backend-class'];
|
||||
$backendRef = $resource['backend-ref'];
|
||||
if (!empty($backendClass)) {
|
||||
Logger::log("get data from old backend " . $backendClass . " : " . $backendRef);
|
||||
$data = $backendClass::get($backendRef);
|
||||
while ($resource = $this->dba->fetch($resources)) {
|
||||
$id = $resource['id'];
|
||||
$data = $resource['data'];
|
||||
$source = $this->getByName($resource['backend-class']);
|
||||
$sourceRef = $resource['backend-ref'];
|
||||
|
||||
if (!empty($source)) {
|
||||
$this->logger->info('Get data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
|
||||
$data = $source->get($sourceRef);
|
||||
}
|
||||
|
||||
Logger::log("save data to new backend " . $destination);
|
||||
/** @var IStorage $destination */
|
||||
$ref = $destination::put($data);
|
||||
Logger::log("saved data as " . $ref);
|
||||
$this->logger->info('Save data to new backend.', ['newBackend' => $destination]);
|
||||
$destinationRef = $destination->put($data);
|
||||
$this->logger->info('Saved data.', ['newReference' => $destinationRef]);
|
||||
|
||||
if ($ref !== '') {
|
||||
Logger::log("update row");
|
||||
if (DBA::update($table, ['backend-class' => $destination, 'backend-ref' => $ref, 'data' => ''], ['id' => $id])) {
|
||||
if (!empty($backendClass)) {
|
||||
Logger::log("delete data from old backend " . $backendClass . " : " . $backendRef);
|
||||
$backendClass::delete($backendRef);
|
||||
if ($destinationRef !== '') {
|
||||
$this->logger->info('update row');
|
||||
if ($this->dba->update($table, ['backend-class' => $destination, 'backend-ref' => $destinationRef, 'data' => ''], ['id' => $id])) {
|
||||
if (!empty($source)) {
|
||||
$this->logger->info('Delete data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
|
||||
$source->delete($sourceRef);
|
||||
}
|
||||
$moved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DBA::close($resources);
|
||||
$this->dba->close($resources);
|
||||
}
|
||||
|
||||
return $moved;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
64
src/DI.php
64
src/DI.php
|
@ -27,6 +27,7 @@ use Psr\Log\LoggerInterface;
|
|||
* @method static Core\L10n\L10n l10n()
|
||||
* @method static Core\Process process()
|
||||
* @method static Core\Session\ISession session()
|
||||
* @method static Core\StorageManager storageManager()
|
||||
* @method static Database\Database dba()
|
||||
* @method static Factory\Mastodon\Account mstdnAccount()
|
||||
* @method static Factory\Mastodon\FollowRequest mstdnFollowRequest()
|
||||
|
@ -34,6 +35,7 @@ use Psr\Log\LoggerInterface;
|
|||
* @method static Model\User\Cookie cookie()
|
||||
* @method static Model\Notify notify()
|
||||
* @method static Repository\Introduction intro()
|
||||
* @method static Model\Storage\IStorage storage()
|
||||
* @method static Protocol\Activity activity()
|
||||
* @method static Util\ACLFormatter aclFormatter()
|
||||
* @method static Util\DateTimeFormat dtFormat()
|
||||
|
@ -47,38 +49,40 @@ use Psr\Log\LoggerInterface;
|
|||
abstract class DI
|
||||
{
|
||||
const CLASS_MAPPING = [
|
||||
'app' => App::class,
|
||||
'auth' => App\Authentication::class,
|
||||
'args' => App\Arguments::class,
|
||||
'baseUrl' => App\BaseURL::class,
|
||||
'mode' => App\Mode::class,
|
||||
'module' => App\Module::class,
|
||||
'page' => App\Page::class,
|
||||
'router' => App\Router::class,
|
||||
'contentItem' => Content\Item::class,
|
||||
'bbCodeVideo' => Content\Text\BBCode\Video::class,
|
||||
'cache' => Core\Cache\ICache::class,
|
||||
'config' => Core\Config\IConfiguration::class,
|
||||
'pConfig' => Core\Config\IPConfiguration::class,
|
||||
'l10n' => Core\L10n\L10n::class,
|
||||
'lock' => Core\Lock\ILock::class,
|
||||
'process' => Core\Process::class,
|
||||
'session' => Core\Session\ISession::class,
|
||||
'dba' => Database\Database::class,
|
||||
'mstdnAccount' => Factory\Mastodon\Account::class,
|
||||
'app' => App::class,
|
||||
'auth' => App\Authentication::class,
|
||||
'args' => App\Arguments::class,
|
||||
'baseUrl' => App\BaseURL::class,
|
||||
'mode' => App\Mode::class,
|
||||
'module' => App\Module::class,
|
||||
'page' => App\Page::class,
|
||||
'router' => App\Router::class,
|
||||
'contentItem' => Content\Item::class,
|
||||
'bbCodeVideo' => Content\Text\BBCode\Video::class,
|
||||
'cache' => Core\Cache\ICache::class,
|
||||
'config' => Core\Config\IConfiguration::class,
|
||||
'pConfig' => Core\Config\IPConfiguration::class,
|
||||
'l10n' => Core\L10n\L10n::class,
|
||||
'lock' => Core\Lock\ILock::class,
|
||||
'process' => Core\Process::class,
|
||||
'session' => Core\Session\ISession::class,
|
||||
'storageManager' => Core\StorageManager::class,
|
||||
'dba' => Database\Database::class,
|
||||
'mstdnAccount' => Factory\Mastodon\Account::class,
|
||||
'mstdnFollowRequest' => Factory\Mastodon\FollowRequest::class,
|
||||
'mstdnRelationship' => Factory\Mastodon\Relationship::class,
|
||||
'cookie' => Model\User\Cookie::class,
|
||||
'notify' => Model\Notify::class,
|
||||
'intro' => Repository\Introduction::class,
|
||||
'activity' => Protocol\Activity::class,
|
||||
'aclFormatter' => Util\ACLFormatter::class,
|
||||
'dtFormat' => Util\DateTimeFormat::class,
|
||||
'fs' => Util\FileSystem::class,
|
||||
'workerLogger' => Util\Logger\WorkerLogger::class,
|
||||
'profiler' => Util\Profiler::class,
|
||||
'logger' => LoggerInterface::class,
|
||||
'devLogger' => '$devLogger',
|
||||
'cookie' => Model\User\Cookie::class,
|
||||
'notify' => Model\Notify::class,
|
||||
'storage' => Model\Storage\IStorage::class,
|
||||
'intro' => Repository\Introduction::class,
|
||||
'activity' => Protocol\Activity::class,
|
||||
'aclFormatter' => Util\ACLFormatter::class,
|
||||
'dtFormat' => Util\DateTimeFormat::class,
|
||||
'fs' => Util\FileSystem::class,
|
||||
'workerLogger' => Util\Logger\WorkerLogger::class,
|
||||
'profiler' => Util\Profiler::class,
|
||||
'logger' => LoggerInterface::class,
|
||||
'devLogger' => '$devLogger',
|
||||
];
|
||||
|
||||
/** @var Dice */
|
||||
|
|
|
@ -6,12 +6,10 @@
|
|||
*/
|
||||
namespace Friendica\Model;
|
||||
|
||||
use Friendica\Core\StorageManager;
|
||||
use Friendica\Core\System;
|
||||
use Friendica\Database\DBA;
|
||||
use Friendica\Database\DBStructure;
|
||||
use Friendica\DI;
|
||||
use Friendica\Model\Storage\IStorage;
|
||||
use Friendica\Object\Image;
|
||||
use Friendica\Util\DateTimeFormat;
|
||||
use Friendica\Util\Mimetype;
|
||||
|
@ -186,13 +184,8 @@ class Attach
|
|||
$filesize = strlen($data);
|
||||
}
|
||||
|
||||
/** @var IStorage $backend_class */
|
||||
$backend_class = StorageManager::getBackend();
|
||||
$backend_ref = '';
|
||||
if ($backend_class !== '') {
|
||||
$backend_ref = $backend_class::put($data);
|
||||
$data = '';
|
||||
}
|
||||
$backend_ref = DI::storage()->put($data);
|
||||
$data = '';
|
||||
|
||||
$hash = System::createGUID(64);
|
||||
$created = DateTimeFormat::utcNow();
|
||||
|
@ -210,7 +203,7 @@ class Attach
|
|||
'allow_gid' => $allow_gid,
|
||||
'deny_cid' => $deny_cid,
|
||||
'deny_gid' => $deny_gid,
|
||||
'backend-class' => $backend_class,
|
||||
'backend-class' => (string)DI::storage(),
|
||||
'backend-ref' => $backend_ref
|
||||
];
|
||||
|
||||
|
@ -266,10 +259,9 @@ class Attach
|
|||
$items = self::selectToArray(['backend-class','backend-ref'], $conditions);
|
||||
|
||||
foreach($items as $item) {
|
||||
/** @var IStorage $backend_class */
|
||||
$backend_class = (string)$item['backend-class'];
|
||||
$backend_class = DI::storageManager()->getByName($item['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 {
|
||||
$fields['data'] = $img->asString();
|
||||
}
|
||||
|
@ -299,10 +291,9 @@ class Attach
|
|||
$items = self::selectToArray(['backend-class','backend-ref'], $conditions);
|
||||
|
||||
foreach($items as $item) {
|
||||
/** @var IStorage $backend_class */
|
||||
$backend_class = (string)$item['backend-class'];
|
||||
if ($backend_class !== '') {
|
||||
$backend_class::delete($item['backend-ref']);
|
||||
$backend_class = DI::storageManager()->getByName($item['backend-class'] ?? '');
|
||||
if ($backend_class !== null) {
|
||||
$backend_class->delete($item['backend-ref'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,12 +10,11 @@ use Friendica\Core\Cache;
|
|||
use Friendica\Core\Config;
|
||||
use Friendica\Core\L10n;
|
||||
use Friendica\Core\Logger;
|
||||
use Friendica\Core\StorageManager;
|
||||
use Friendica\Core\System;
|
||||
use Friendica\Database\DBA;
|
||||
use Friendica\Database\DBStructure;
|
||||
use Friendica\DI;
|
||||
use Friendica\Model\Storage\IStorage;
|
||||
use Friendica\Model\Storage\SystemResource;
|
||||
use Friendica\Object\Image;
|
||||
use Friendica\Util\DateTimeFormat;
|
||||
use Friendica\Util\Images;
|
||||
|
@ -172,26 +171,24 @@ class Photo
|
|||
*/
|
||||
public static function getImageForPhoto(array $photo)
|
||||
{
|
||||
$data = "";
|
||||
|
||||
if ($photo["backend-class"] == "") {
|
||||
if (empty($photo['backend-class'])) {
|
||||
// legacy data storage in "data" column
|
||||
$i = self::selectFirst(["data"], ["id" => $photo["id"]]);
|
||||
$i = self::selectFirst(['data'], ['id' => $photo['id']]);
|
||||
if ($i === false) {
|
||||
return null;
|
||||
}
|
||||
$data = $i["data"];
|
||||
$data = $i['data'];
|
||||
} else {
|
||||
$backendClass = $photo["backend-class"];
|
||||
$backendRef = $photo["backend-ref"];
|
||||
$data = $backendClass::get($backendRef);
|
||||
$backendClass = DI::storageManager()->getByName($photo['backend-class'] ?? '');
|
||||
$backendRef = $photo['backend-ref'] ?? '';
|
||||
$data = $backendClass->get($backendRef);
|
||||
}
|
||||
|
||||
if ($data === "") {
|
||||
if (empty($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Image($data, $photo["type"]);
|
||||
return new Image($data, $photo['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -222,11 +219,11 @@ class Photo
|
|||
$fields = self::getFields();
|
||||
$values = array_fill(0, count($fields), "");
|
||||
|
||||
$photo = array_combine($fields, $values);
|
||||
$photo["backend-class"] = Storage\SystemResource::class;
|
||||
$photo["backend-ref"] = $filename;
|
||||
$photo["type"] = $mimetype;
|
||||
$photo["cacheable"] = false;
|
||||
$photo = array_combine($fields, $values);
|
||||
$photo['backend-class'] = SystemResource::NAME;
|
||||
$photo['backend-ref'] = $filename;
|
||||
$photo['type'] = $mimetype;
|
||||
$photo['cacheable'] = false;
|
||||
|
||||
return $photo;
|
||||
}
|
||||
|
@ -273,18 +270,17 @@ class Photo
|
|||
$data = "";
|
||||
$backend_ref = "";
|
||||
|
||||
/** @var IStorage $backend_class */
|
||||
if (DBA::isResult($existing_photo)) {
|
||||
$backend_ref = (string)$existing_photo["backend-ref"];
|
||||
$backend_class = (string)$existing_photo["backend-class"];
|
||||
$storage = DI::storageManager()->getByName($existing_photo["backend-class"] ?? '');
|
||||
} else {
|
||||
$backend_class = StorageManager::getBackend();
|
||||
$storage = DI::storage();
|
||||
}
|
||||
|
||||
if ($backend_class === "") {
|
||||
if ($storage === null) {
|
||||
$data = $Image->asString();
|
||||
} 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_gid" => $deny_gid,
|
||||
"desc" => $desc,
|
||||
"backend-class" => $backend_class,
|
||||
"backend-class" => (string)$storage,
|
||||
"backend-ref" => $backend_ref
|
||||
];
|
||||
|
||||
|
@ -340,10 +336,9 @@ class Photo
|
|||
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
|
||||
|
||||
foreach($photos as $photo) {
|
||||
/** @var IStorage $backend_class */
|
||||
$backend_class = (string)$photo["backend-class"];
|
||||
if ($backend_class !== "") {
|
||||
$backend_class::delete($photo["backend-ref"]);
|
||||
$backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? '');
|
||||
if ($backend_class !== null) {
|
||||
$backend_class->delete($photo["backend-ref"] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -370,10 +365,9 @@ class Photo
|
|||
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
|
||||
|
||||
foreach($photos as $photo) {
|
||||
/** @var IStorage $backend_class */
|
||||
$backend_class = (string)$photo["backend-class"];
|
||||
if ($backend_class !== "") {
|
||||
$fields["backend-ref"] = $backend_class::put($img->asString(), $photo["backend-ref"]);
|
||||
$backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? '');
|
||||
if ($backend_class !== null) {
|
||||
$fields["backend-ref"] = $backend_class->put($img->asString(), $photo['backend-ref']);
|
||||
} else {
|
||||
$fields["data"] = $img->asString();
|
||||
}
|
||||
|
|
32
src/Model/Storage/AbstractStorage.php
Normal file
32
src/Model/Storage/AbstractStorage.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -6,58 +6,100 @@
|
|||
|
||||
namespace Friendica\Model\Storage;
|
||||
|
||||
use Friendica\Core\Logger;
|
||||
use Friendica\Core\L10n;
|
||||
use Friendica\Database\DBA;
|
||||
use Friendica\Core\L10n\L10n;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Friendica\Database\Database as DBA;
|
||||
|
||||
/**
|
||||
* @brief Database based storage system
|
||||
*
|
||||
* 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]);
|
||||
if (!DBA::isResult($r)) {
|
||||
parent::__construct($l10n, $logger);
|
||||
|
||||
$this->dba = $dba;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function get(string $reference)
|
||||
{
|
||||
$result = $this->dba->selectFirst('storage', ['data'], ['id' => $reference]);
|
||||
if (!$this->dba->isResult($result)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $r['data'];
|
||||
return $result['data'];
|
||||
}
|
||||
|
||||
public static function put($data, $ref = '')
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function put(string $data, string $reference = '')
|
||||
{
|
||||
if ($ref !== '') {
|
||||
$r = DBA::update('storage', ['data' => $data], ['id' => $ref]);
|
||||
if ($r === false) {
|
||||
Logger::log('Failed to update data with id ' . $ref . ': ' . DBA::errorNo() . ' : ' . DBA::errorMessage());
|
||||
throw new StorageException(L10n::t('Database storage failed to update %s', $ref));
|
||||
}
|
||||
return $ref;
|
||||
} else {
|
||||
$r = DBA::insert('storage', ['data' => $data]);
|
||||
if ($r === false) {
|
||||
Logger::log('Failed to insert data: ' . DBA::errorNo() . ' : ' . DBA::errorMessage());
|
||||
throw new StorageException(L10n::t('Database storage failed to insert data'));
|
||||
if ($reference !== '') {
|
||||
$result = $this->dba->update('storage', ['data' => $data], ['id' => $reference]);
|
||||
if ($result === false) {
|
||||
$this->logger->warning('Failed to update data.', ['id' => $reference, 'errorCode' => $this->dba->errorNo(), 'errorMessage' => $this->dba->errorMessage()]);
|
||||
throw new StorageException($this->l10n->t('Database storage failed to update %s', $reference));
|
||||
}
|
||||
return DBA::lastInsertId();
|
||||
|
||||
return $reference;
|
||||
} else {
|
||||
$result = $this->dba->insert('storage', ['data' => $data]);
|
||||
if ($result === false) {
|
||||
$this->logger->warning('Failed to insert data.', ['errorCode' => $this->dba->errorNo(), 'errorMessage' => $this->dba->errorMessage()]);
|
||||
throw new StorageException($this->l10n->t('Database storage failed to insert data'));
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
|
||||
public static function saveOptions($data)
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function saveOptions(array $data)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getName()
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
namespace Friendica\Model\Storage;
|
||||
|
||||
use Friendica\Core\Config;
|
||||
use Friendica\Core\L10n;
|
||||
use Friendica\Core\Logger;
|
||||
use Friendica\Core\Config\IConfiguration;
|
||||
use Friendica\Core\L10n\L10n;
|
||||
use Friendica\Util\Strings;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* @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
|
||||
* folder tree stucture created from that value.
|
||||
*/
|
||||
class Filesystem implements IStorage
|
||||
class Filesystem extends AbstractStorage
|
||||
{
|
||||
const NAME = 'Filesystem';
|
||||
|
||||
// Default base folder
|
||||
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);
|
||||
return rtrim($path, '/');
|
||||
parent::__construct($l10n, $logger);
|
||||
|
||||
$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
|
||||
* @param string $ref Data reference
|
||||
*
|
||||
* @param string $reference Data reference
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function pathForRef($ref)
|
||||
private function pathForRef(string $reference)
|
||||
{
|
||||
$base = self::getBasePath();
|
||||
$fold1 = substr($ref, 0, 2);
|
||||
$fold2 = substr($ref, 2, 2);
|
||||
$file = substr($ref, 4);
|
||||
$fold1 = substr($reference, 0, 2);
|
||||
$fold2 = substr($reference, 2, 2);
|
||||
$file = substr($reference, 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
|
||||
*
|
||||
* @param string $file Path and filename
|
||||
*
|
||||
* @throws StorageException
|
||||
*/
|
||||
private static function createFoldersForFile($file)
|
||||
private function createFoldersForFile(string $file)
|
||||
{
|
||||
$path = dirname($file);
|
||||
|
||||
if (!is_dir($path)) {
|
||||
if (!mkdir($path, 0770, true)) {
|
||||
Logger::log('Failed to create dirs ' . $path);
|
||||
throw new StorageException(L10n::t('Filesystem storage failed to create "%s". Check you write permissions.', $path));
|
||||
$this->logger->warning('Failed to create dir.', ['path' => $path]);
|
||||
throw new StorageException($this->l10n->t('Filesystem storage failed to create "%s". Check you write permissions.', $path));
|
||||
}
|
||||
}
|
||||
|
||||
$base = self::getBasePath();
|
||||
|
||||
while ($path !== $base) {
|
||||
while ($path !== $this->basePath) {
|
||||
if (!is_file($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)) {
|
||||
return '';
|
||||
}
|
||||
|
@ -90,27 +113,33 @@ class Filesystem implements IStorage
|
|||
return file_get_contents($file);
|
||||
}
|
||||
|
||||
public static function put($data, $ref = '')
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function put(string $data, string $reference = '')
|
||||
{
|
||||
if ($ref === '') {
|
||||
$ref = Strings::getRandomHex();
|
||||
if ($reference === '') {
|
||||
$reference = Strings::getRandomHex();
|
||||
}
|
||||
$file = self::pathForRef($ref);
|
||||
$file = $this->pathForRef($reference);
|
||||
|
||||
self::createFoldersForFile($file);
|
||||
$this->createFoldersForFile($file);
|
||||
|
||||
$r = file_put_contents($file, $data);
|
||||
if ($r === FALSE) {
|
||||
Logger::log('Failed to write data to ' . $file);
|
||||
throw new StorageException(L10n::t('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
|
||||
if (!file_put_contents($file, $data)) {
|
||||
$this->logger->warning('Failed to write data.', ['file' => $file]);
|
||||
throw new StorageException($this->l10n->t('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
|
||||
}
|
||||
|
||||
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!
|
||||
if (!is_file($file)) {
|
||||
return true;
|
||||
|
@ -118,28 +147,42 @@ class Filesystem implements IStorage
|
|||
return unlink($file);
|
||||
}
|
||||
|
||||
public static function getOptions()
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
return [
|
||||
'storagepath' => [
|
||||
'input',
|
||||
L10n::t('Storage base path'),
|
||||
self::getBasePath(),
|
||||
L10n::t('Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree')
|
||||
$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')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function saveOptions($data)
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function saveOptions(array $data)
|
||||
{
|
||||
$storagepath = $data['storagepath'] ?? '';
|
||||
if ($storagepath === '' || !is_dir($storagepath)) {
|
||||
$storagePath = $data['storagepath'] ?? '';
|
||||
if ($storagePath === '' || !is_dir($storagePath)) {
|
||||
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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getName()
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,26 +13,32 @@ interface IStorage
|
|||
{
|
||||
/**
|
||||
* @brief Get data from backend
|
||||
* @param string $ref Data reference
|
||||
*
|
||||
* @param string $reference Data reference
|
||||
*
|
||||
* @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.
|
||||
* @param string $data Data to save
|
||||
* @param string $ref Data referece. Optional.
|
||||
* @return string Saved data referece
|
||||
*
|
||||
* @param string $data Data to save
|
||||
* @param string $reference Data reference. Optional.
|
||||
*
|
||||
* @return string Saved data reference
|
||||
*/
|
||||
public static function put($data, $ref = "");
|
||||
public function put(string $data, string $reference = "");
|
||||
|
||||
/**
|
||||
* @brief Remove data from backend
|
||||
* @param string $ref Data referece
|
||||
*
|
||||
* @param string $reference Data reference
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public static function delete($ref);
|
||||
|
||||
public function delete(string $reference);
|
||||
|
||||
/**
|
||||
* @brief Get info about storage options
|
||||
*
|
||||
|
@ -71,19 +77,30 @@ interface IStorage
|
|||
*
|
||||
* See https://github.com/friendica/friendica/wiki/Quick-Template-Guide
|
||||
*/
|
||||
public static function getOptions();
|
||||
|
||||
public function getOptions();
|
||||
|
||||
/**
|
||||
* @brief Validate and save options
|
||||
*
|
||||
* @param array $data Array [optionname => value] to be saved
|
||||
* @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 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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -16,10 +16,15 @@ use \BadMethodCallException;
|
|||
*/
|
||||
class SystemResource implements IStorage
|
||||
{
|
||||
const NAME = 'SystemResource';
|
||||
|
||||
// Valid folders to look for resources
|
||||
const VALID_FOLDERS = ["images"];
|
||||
|
||||
public static function get($filename)
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function get(string $filename)
|
||||
{
|
||||
$folder = dirname($filename);
|
||||
if (!in_array($folder, self::VALID_FOLDERS)) {
|
||||
|
@ -31,25 +36,48 @@ class SystemResource implements IStorage
|
|||
return file_get_contents($filename);
|
||||
}
|
||||
|
||||
|
||||
public static function put($data, $filename = "")
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function put(string $data, string $filename = '')
|
||||
{
|
||||
throw new BadMethodCallException();
|
||||
}
|
||||
|
||||
public static function delete($filename)
|
||||
public function delete(string $filename)
|
||||
{
|
||||
throw new BadMethodCallException();
|
||||
}
|
||||
|
||||
public static function getOptions()
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function saveOptions($data)
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function saveOptions(array $data)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getName()
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -199,42 +199,37 @@ class Site extends BaseAdminModule
|
|||
$relay_user_tags = !empty($_POST['relay_user_tags']);
|
||||
$active_panel = (!empty($_POST['active_panel']) ? "#" . Strings::escapeTags(trim($_POST['active_panel'])) : '');
|
||||
|
||||
/**
|
||||
* @var $storagebackend \Friendica\Model\Storage\IStorage
|
||||
*/
|
||||
$storagebackend = Strings::escapeTags(trim($_POST['storagebackend'] ?? ''));
|
||||
|
||||
// save storage backend form
|
||||
if (!is_null($storagebackend) && $storagebackend != "") {
|
||||
if (StorageManager::setBackend($storagebackend)) {
|
||||
$storage_opts = $storagebackend::getOptions();
|
||||
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $storagebackend);
|
||||
$storage_opts_data = [];
|
||||
foreach ($storage_opts as $name => $info) {
|
||||
$fieldname = $storage_form_prefix . '_' . $name;
|
||||
switch ($info[0]) { // type
|
||||
case 'checkbox':
|
||||
case 'yesno':
|
||||
$value = !empty($_POST[$fieldname]);
|
||||
break;
|
||||
default:
|
||||
$value = $_POST[$fieldname] ?? '';
|
||||
}
|
||||
$storage_opts_data[$name] = $value;
|
||||
if (DI::storageManager()->setBackend($storagebackend)) {
|
||||
$storage_opts = DI::storage()->getOptions();
|
||||
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $storagebackend);
|
||||
$storage_opts_data = [];
|
||||
foreach ($storage_opts as $name => $info) {
|
||||
$fieldname = $storage_form_prefix . '_' . $name;
|
||||
switch ($info[0]) { // type
|
||||
case 'checkbox':
|
||||
case 'yesno':
|
||||
$value = !empty($_POST[$fieldname]);
|
||||
break;
|
||||
default:
|
||||
$value = $_POST[$fieldname] ?? '';
|
||||
}
|
||||
unset($name);
|
||||
unset($info);
|
||||
|
||||
$storage_form_errors = $storagebackend::saveOptions($storage_opts_data);
|
||||
if (count($storage_form_errors)) {
|
||||
foreach ($storage_form_errors as $name => $err) {
|
||||
notice('Storage backend, ' . $storage_opts[$name][1] . ': ' . $err);
|
||||
}
|
||||
DI::baseUrl()->redirect('admin/site' . $active_panel);
|
||||
}
|
||||
} else {
|
||||
info(L10n::t('Invalid storage backend setting value.'));
|
||||
$storage_opts_data[$name] = $value;
|
||||
}
|
||||
unset($name);
|
||||
unset($info);
|
||||
|
||||
$storage_form_errors = DI::storage()->saveOptions($storage_opts_data);
|
||||
if (count($storage_form_errors)) {
|
||||
foreach ($storage_form_errors as $name => $err) {
|
||||
notice('Storage backend, ' . $storage_opts[$name][1] . ': ' . $err);
|
||||
}
|
||||
DI::baseUrl()->redirect('admin/site' . $active_panel);
|
||||
}
|
||||
} else {
|
||||
info(L10n::t('Invalid storage backend setting value.'));
|
||||
}
|
||||
|
||||
// Has the directory url changed? If yes, then resubmit the existing profiles there
|
||||
|
@ -530,29 +525,25 @@ class Site extends BaseAdminModule
|
|||
$optimize_max_tablesize = -1;
|
||||
}
|
||||
|
||||
$storage_backends = StorageManager::listBackends();
|
||||
/** @var $current_storage_backend \Friendica\Model\Storage\IStorage */
|
||||
$current_storage_backend = StorageManager::getBackend();
|
||||
|
||||
$current_storage_backend = DI::storage();
|
||||
$available_storage_backends = [];
|
||||
|
||||
// show legacy option only if it is the current backend:
|
||||
// once changed can't be selected anymore
|
||||
if ($current_storage_backend == '') {
|
||||
if ($current_storage_backend == null) {
|
||||
$available_storage_backends[''] = L10n::t('Database (legacy)');
|
||||
}
|
||||
|
||||
foreach ($storage_backends as $name => $class) {
|
||||
$available_storage_backends[$class] = $name;
|
||||
foreach (DI::storageManager()->listBackends() as $name => $class) {
|
||||
$available_storage_backends[$name] = $name;
|
||||
}
|
||||
unset($storage_backends);
|
||||
|
||||
// build storage config form,
|
||||
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|' ,'', $current_storage_backend);
|
||||
|
||||
$storage_form = [];
|
||||
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];
|
||||
$info[0] = $storage_form_prefix . '_' . $name;
|
||||
$info['type'] = $type;
|
||||
|
|
|
@ -324,8 +324,8 @@ class CronJobs
|
|||
*/
|
||||
private static function moveStorage()
|
||||
{
|
||||
$current = StorageManager::getBackend();
|
||||
$moved = StorageManager::move($current);
|
||||
$current = DI::storage();
|
||||
$moved = DI::storageManager()->move($current);
|
||||
|
||||
if ($moved) {
|
||||
Worker::add(PRIORITY_LOW, "CronJobs", "move_storage");
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
use Friendica\Database\DBA;
|
||||
|
||||
if (!defined('DB_UPDATE_VERSION')) {
|
||||
define('DB_UPDATE_VERSION', 1329);
|
||||
define('DB_UPDATE_VERSION', 1330);
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
|
@ -8,8 +8,10 @@ use Friendica\Core\L10n\L10n;
|
|||
use Friendica\Core\Lock\ILock;
|
||||
use Friendica\Core\Process;
|
||||
use Friendica\Core\Session\ISession;
|
||||
use Friendica\Core\StorageManager;
|
||||
use Friendica\Database\Database;
|
||||
use Friendica\Factory;
|
||||
use Friendica\Model\Storage\IStorage;
|
||||
use Friendica\Model\User\Cookie;
|
||||
use Friendica\Util;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
@ -193,5 +195,11 @@ return [
|
|||
'constructParams' => [
|
||||
$_SERVER, $_COOKIE
|
||||
],
|
||||
]
|
||||
],
|
||||
IStorage::class => [
|
||||
'instanceOf' => StorageManager::class,
|
||||
'call' => [
|
||||
['getBackend', [], Dice::CHAIN_CALL],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
106
tests/Util/SampleStorageBackend.php
Normal file
106
tests/Util/SampleStorageBackend.php
Normal 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');
|
||||
}
|
||||
}
|
||||
|
18
tests/Util/SampleStorageBackendInstance.php
Normal file
18
tests/Util/SampleStorageBackendInstance.php
Normal 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);
|
||||
}
|
||||
}
|
40
tests/datasets/storage/database.fixture.php
Normal file
40
tests/datasets/storage/database.fixture.php
Normal 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',
|
||||
],
|
||||
],
|
||||
];
|
264
tests/src/Core/StorageManagerTest.php
Normal file
264
tests/src/Core/StorageManagerTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
52
tests/src/Model/Storage/DatabaseStorageTest.php
Normal file
52
tests/src/Model/Storage/DatabaseStorageTest.php
Normal 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());
|
||||
}
|
||||
}
|
111
tests/src/Model/Storage/FilesystemStorageTest.php
Normal file
111
tests/src/Model/Storage/FilesystemStorageTest.php
Normal 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));
|
||||
}
|
||||
}
|
96
tests/src/Model/Storage/StorageTest.php
Normal file
96
tests/src/Model/Storage/StorageTest.php
Normal 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));
|
||||
}
|
||||
}
|
24
update.php
24
update.php
|
@ -12,6 +12,7 @@ use Friendica\Model\Contact;
|
|||
use Friendica\Model\GContact;
|
||||
use Friendica\Model\Item;
|
||||
use Friendica\Model\User;
|
||||
use Friendica\Model\Storage;
|
||||
use Friendica\Util\DateTimeFormat;
|
||||
use Friendica\Worker\Delivery;
|
||||
|
||||
|
@ -408,3 +409,26 @@ function update_1327()
|
|||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue