Make Storage testable & add tests

- Making StorageManager dynamic (DI::facStorage())
- Making concrete Storage dynamic (DI::storage())
- Add tests for Storage backend and failure handling
- Bumping Level-2/Dice to "dev-master" until new release
- Using Storage-Names instead of Storage-Classes in config (includes migration)
This commit is contained in:
Philipp Holzer 2020-01-05 01:58:49 +01:00
parent a5895f8623
commit 08edeae2f9
No known key found for this signature in database
GPG key ID: D8365C3D36B77D90
18 changed files with 744 additions and 242 deletions

View file

@ -32,7 +32,7 @@
"ezyang/htmlpurifier": "^4.7", "ezyang/htmlpurifier": "^4.7",
"friendica/json-ld": "^1.0", "friendica/json-ld": "^1.0",
"league/html-to-markdown": "^4.8", "league/html-to-markdown": "^4.8",
"level-2/dice": "^4", "level-2/dice": "dev-master",
"lightopenid/lightopenid": "dev-master", "lightopenid/lightopenid": "dev-master",
"michelf/php-markdown": "^1.7", "michelf/php-markdown": "^1.7",
"mobiledetect/mobiledetectlib": "^2.8", "mobiledetect/mobiledetectlib": "^2.8",

90
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "34ad225ce21474eb84ce78047d9f2c01", "content-hash": "bf05cd52bc7307f45aff80f1d1fd8214",
"packages": [ "packages": [
{ {
"name": "asika/simple-console", "name": "asika/simple-console",
@ -485,6 +485,7 @@
"jsonld.php" "jsonld.php"
] ]
}, },
"notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"BSD-3-Clause" "BSD-3-Clause"
], ],
@ -502,11 +503,11 @@
"description": "A JSON-LD Processor and API implementation in PHP.", "description": "A JSON-LD Processor and API implementation in PHP.",
"homepage": "https://git.friendi.ca/friendica/php-json-ld", "homepage": "https://git.friendi.ca/friendica/php-json-ld",
"keywords": [ "keywords": [
"JSON",
"JSON-LD", "JSON-LD",
"Linked Data", "Linked Data",
"RDF", "RDF",
"Semantic Web", "Semantic Web",
"json",
"jsonld" "jsonld"
], ],
"time": "2018-10-08T20:41:00+00:00" "time": "2018-10-08T20:41:00+00:00"
@ -823,16 +824,16 @@
}, },
{ {
"name": "level-2/dice", "name": "level-2/dice",
"version": "4.0.1", "version": "dev-master",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Level-2/Dice.git", "url": "https://github.com/Level-2/Dice.git",
"reference": "e631f110f0520294fec902814c61cac26566023c" "reference": "2fea2749a625c3adcc29c402218b0dcaed11586f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Level-2/Dice/zipball/e631f110f0520294fec902814c61cac26566023c", "url": "https://api.github.com/repos/Level-2/Dice/zipball/2fea2749a625c3adcc29c402218b0dcaed11586f",
"reference": "e631f110f0520294fec902814c61cac26566023c", "reference": "2fea2749a625c3adcc29c402218b0dcaed11586f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -865,7 +866,7 @@
"di", "di",
"ioc" "ioc"
], ],
"time": "2019-05-01T12:55:36+00:00" "time": "2019-10-03T16:08:46+00:00"
}, },
{ {
"name": "lightopenid/lightopenid", "name": "lightopenid/lightopenid",
@ -1314,6 +1315,22 @@
"require": { "require": {
"npm-asset/ev-emitter": ">=1.0.0,<2.0.0" "npm-asset/ev-emitter": ">=1.0.0,<2.0.0"
}, },
"require-dev": {
"npm-asset/chalk": ">=1.1.1,<2.0.0",
"npm-asset/cheerio": ">=0.19.0,<0.20.0",
"npm-asset/gulp": ">=3.9.0,<4.0.0",
"npm-asset/gulp-jshint": ">=1.11.2,<2.0.0",
"npm-asset/gulp-json-lint": ">=0.1.0,<0.2.0",
"npm-asset/gulp-rename": ">=1.2.2,<2.0.0",
"npm-asset/gulp-replace": ">=0.5.4,<0.6.0",
"npm-asset/gulp-requirejs-optimize": "dev-github:metafizzy/gulp-requirejs-optimize",
"npm-asset/gulp-uglify": ">=1.4.2,<2.0.0",
"npm-asset/gulp-util": ">=3.0.7,<4.0.0",
"npm-asset/highlight.js": ">=8.9.1,<9.0.0",
"npm-asset/marked": ">=0.3.5,<0.4.0",
"npm-asset/minimist": ">=1.2.0,<2.0.0",
"npm-asset/transfob": ">=1.0.0,<2.0.0"
},
"type": "npm-asset-library", "type": "npm-asset-library",
"extra": { "extra": {
"npm-asset-bugs": { "npm-asset-bugs": {
@ -1358,6 +1375,14 @@
"url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.6.tgz", "url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.6.tgz",
"shasum": "2736e332aaee73ccf0a14a5f0066391a0a13f4a3" "shasum": "2736e332aaee73ccf0a14a5f0066391a0a13f4a3"
}, },
"require-dev": {
"npm-asset/grunt": "~0.4.2",
"npm-asset/grunt-contrib-cssmin": "~0.9.0",
"npm-asset/grunt-contrib-jshint": "~0.6.3",
"npm-asset/grunt-contrib-less": "~0.11.0",
"npm-asset/grunt-contrib-uglify": "~0.4.0",
"npm-asset/grunt-contrib-watch": "~0.6.1"
},
"type": "npm-asset-library", "type": "npm-asset-library",
"extra": { "extra": {
"npm-asset-bugs": { "npm-asset-bugs": {
@ -1390,6 +1415,32 @@
"url": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz", "url": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
"shasum": "2c89d6889b5eac522a7eea32c14521559c6cbf02" "shasum": "2c89d6889b5eac522a7eea32c14521559c6cbf02"
}, },
"require-dev": {
"npm-asset/commitplease": "2.0.0",
"npm-asset/core-js": "0.9.17",
"npm-asset/grunt": "0.4.5",
"npm-asset/grunt-babel": "5.0.1",
"npm-asset/grunt-cli": "0.1.13",
"npm-asset/grunt-compare-size": "0.4.0",
"npm-asset/grunt-contrib-jshint": "0.11.2",
"npm-asset/grunt-contrib-uglify": "0.9.2",
"npm-asset/grunt-contrib-watch": "0.6.1",
"npm-asset/grunt-git-authors": "2.0.1",
"npm-asset/grunt-jscs": "2.1.0",
"npm-asset/grunt-jsonlint": "1.0.4",
"npm-asset/grunt-npmcopy": "0.1.0",
"npm-asset/gzip-js": "0.3.2",
"npm-asset/jsdom": "5.6.1",
"npm-asset/load-grunt-tasks": "1.0.0",
"npm-asset/qunit-assert-step": "1.0.3",
"npm-asset/qunitjs": "1.17.1",
"npm-asset/requirejs": "2.1.17",
"npm-asset/sinon": "1.10.3",
"npm-asset/sizzle": "2.2.1",
"npm-asset/strip-json-comments": "1.0.3",
"npm-asset/testswarm": "1.1.0",
"npm-asset/win-spawn": "2.0.0"
},
"type": "npm-asset-library", "type": "npm-asset-library",
"extra": { "extra": {
"npm-asset-bugs": { "npm-asset-bugs": {
@ -1537,6 +1588,12 @@
"url": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz", "url": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz",
"shasum": "06f0335f16e353a695e7206bf50503cb523a6ee5" "shasum": "06f0335f16e353a695e7206bf50503cb523a6ee5"
}, },
"require-dev": {
"npm-asset/grunt": "~0.4.1",
"npm-asset/grunt-contrib-connect": "~0.5.0",
"npm-asset/grunt-contrib-jshint": "~0.7.1",
"npm-asset/grunt-contrib-uglify": "~0.2.7"
},
"type": "npm-asset-library", "type": "npm-asset-library",
"extra": { "extra": {
"npm-asset-bugs": { "npm-asset-bugs": {
@ -3335,8 +3392,8 @@
"authors": [ "authors": [
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"role": "lead", "email": "sb@sebastian-bergmann.de",
"email": "sb@sebastian-bergmann.de" "role": "lead"
} }
], ],
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
@ -3603,8 +3660,8 @@
"authors": [ "authors": [
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"role": "lead", "email": "sebastian@phpunit.de",
"email": "sebastian@phpunit.de" "role": "lead"
} }
], ],
"description": "The PHP Unit Testing framework.", "description": "The PHP Unit Testing framework.",
@ -3777,7 +3834,7 @@
} }
], ],
"description": "Provides the functionality to compare PHP values for equality", "description": "Provides the functionality to compare PHP values for equality",
"homepage": "http://www.github.com/sebastianbergmann/comparator", "homepage": "https://github.com/sebastianbergmann/comparator",
"keywords": [ "keywords": [
"comparator", "comparator",
"compare", "compare",
@ -3879,7 +3936,7 @@
} }
], ],
"description": "Provides functionality to handle HHVM/PHP environments", "description": "Provides functionality to handle HHVM/PHP environments",
"homepage": "http://www.github.com/sebastianbergmann/environment", "homepage": "https://github.com/sebastianbergmann/environment",
"keywords": [ "keywords": [
"Xdebug", "Xdebug",
"environment", "environment",
@ -3947,7 +4004,7 @@
} }
], ],
"description": "Provides the functionality to export PHP variables for visualization", "description": "Provides the functionality to export PHP variables for visualization",
"homepage": "http://www.github.com/sebastianbergmann/exporter", "homepage": "https://github.com/sebastianbergmann/exporter",
"keywords": [ "keywords": [
"export", "export",
"exporter" "exporter"
@ -3999,7 +4056,7 @@
} }
], ],
"description": "Snapshotting of global state", "description": "Snapshotting of global state",
"homepage": "http://www.github.com/sebastianbergmann/global-state", "homepage": "https://github.com/sebastianbergmann/global-state",
"keywords": [ "keywords": [
"global state" "global state"
], ],
@ -4101,7 +4158,7 @@
} }
], ],
"description": "Provides functionality to recursively process PHP variables", "description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context", "homepage": "https://github.com/sebastianbergmann/recursion-context",
"time": "2016-11-19T07:33:16+00:00" "time": "2016-11-19T07:33:16+00:00"
}, },
{ {
@ -4360,6 +4417,7 @@
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": { "stability-flags": {
"level-2/dice": 20,
"lightopenid/lightopenid": 20 "lightopenid/lightopenid": 20
}, },
"prefer-stable": false, "prefer-stable": false,

View file

@ -182,21 +182,21 @@ 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::facStorage()->register("Sample Storage", 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::facStorage()->unregister("Sample Storage");
} }
``` ```

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 Dice\Dice;
use Friendica\Model\Storage\IStorage; use Exception;
use Friendica\Core\Config\IConfiguration;
use Friendica\Database\Database;
use Friendica\Model\Storage;
use Psr\Log\LoggerInterface;
/** /**
@ -14,59 +18,108 @@ 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 Database */
private $dba;
/** @var IConfiguration */
private $config;
/** @var LoggerInterface */
private $logger;
/** @var Dice */
private $dice;
/** @var Storage\IStorage */
private $currentBackend;
/**
* @param Database $dba
* @param IConfiguration $config
* @param LoggerInterface $logger
*/
public function __construct(Database $dba, IConfiguration $config, LoggerInterface $logger, Dice $dice)
{ {
if (count(self::$backends) == 0) { $this->dba = $dba;
self::$backends = Config::get('storage', 'backends', self::$default_backends); $this->config = $config;
$this->logger = $logger;
$this->dice = $dice;
$this->backends = $config->get('storage', 'backends', self::DEFAULT_BACKENDS);
$currentName = $this->config->get('storage', 'name', '');
if ($this->isValidBackend($currentName)) {
$this->currentBackend = $this->dice->create($this->backends[$currentName]);
} else {
$this->currentBackend = null;
} }
} }
/** /**
* @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 $name Backend name
* @return string Empty if no backend registered at $name exists *
* @return Storage\IStorage|null null if no backend registered at $name
*/ */
public static function getByName($name) public function getByName(string $name)
{ {
self::setup(); if (!$this->isValidBackend($name)) {
return self::$backends[$name] ?? ''; return null;
}
return $this->dice->create($this->backends[$name]);
}
/**
* Checks, if the storage is a valid backend
*
* @param string $name The name or class of the backend
*
* @return boolean True, if the backend is a valid backend
*/
public function isValidBackend(string $name)
{
return array_key_exists($name, $this->backends);
} }
/** /**
* @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)
{ {
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->dice->create($this->backends[$name]);
return true; return true;
} else {
return false;
}
} }
/** /**
@ -74,107 +127,109 @@ 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 * @brief Register a storage backend class
* *
* @param string $name User readable backend name * @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 $name, string $class)
{ {
/// @todo Check that $class implements IStorage if (!is_subclass_of($class, Storage\IStorage::class)) {
self::setup(); return false;
self::$backends[$name] = $class; }
Config::set('storage', 'backends', self::$backends);
}
$backends = $this->backends;
$backends[$name] = $class;
if ($this->config->set('storage', 'backends', $this->backends)) {
$this->backends = $backends;
return true;
} else {
return false;
}
}
/** /**
* @brief Unregister a storage backend class * @brief Unregister a storage backend class
* *
* @param string $name User readable backend name * @param string $name User readable backend name
* @throws \Friendica\Network\HTTPException\InternalServerErrorException *
* @return boolean True, if unregistering was successful
*/ */
public static function unregister($name) public function unregister(string $name)
{ {
self::setup(); unset($this->backends[$name]);
unset(self::$backends[$name]); return $this->config->set('storage', 'backends', $this->backends);
Config::set('storage', 'backends', self::$backends);
} }
/** /**
* @brief Move up to 5000 resources to storage $dest * @brief Move up to 5000 resources to storage $dest
* *
* 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],
['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 facStorage()
* @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,
'facStorage' => 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

@ -186,13 +186,8 @@ class Attach
$filesize = strlen($data); $filesize = strlen($data);
} }
/** @var IStorage $backend_class */ $backend_ref = DI::storage()->put($data);
$backend_class = StorageManager::getBackend(); $data = '';
$backend_ref = '';
if ($backend_class !== '') {
$backend_ref = $backend_class::put($data);
$data = '';
}
$hash = System::createGUID(64); $hash = System::createGUID(64);
$created = DateTimeFormat::utcNow(); $created = DateTimeFormat::utcNow();
@ -210,7 +205,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
]; ];

View file

@ -273,18 +273,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::facStorage()->getByName((string)$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 +308,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
]; ];

View file

@ -6,9 +6,8 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use Friendica\Core\Logger;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Database\DBA; use Psr\Log\LoggerInterface;
/** /**
* @brief Database based storage system * @brief Database based storage system
@ -17,47 +16,93 @@ use Friendica\Database\DBA;
*/ */
class Database implements IStorage class Database implements IStorage
{ {
public static function get($ref) const NAME = 'Database';
/** @var \Friendica\Database\Database */
private $dba;
/** @var LoggerInterface */
private $logger;
/** @var L10n\L10n */
private $l10n;
/**
* @param \Friendica\Database\Database $dba
* @param LoggerInterface $logger
* @param L10n\L10n $l10n
*/
public function __construct(\Friendica\Database\Database $dba, LoggerInterface $logger, L10n\L10n $l10n)
{ {
$r = DBA::selectFirst('storage', ['data'], ['id' => $ref]); $this->dba = $dba;
if (!DBA::isResult($r)) { $this->logger = $logger;
$this->l10n = $l10n;
}
/**
* @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 function __toString()
{
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
@ -23,50 +23,74 @@ use Friendica\Util\Strings;
*/ */
class Filesystem implements IStorage class Filesystem implements IStorage
{ {
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 LoggerInterface */
private $logger;
/** @var L10n */
private $l10n;
/** @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); $this->config = $config;
return rtrim($path, '/'); $this->logger = $logger;
$this->l10n = $l10n;
$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 +104,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 +117,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_exists($file) && !is_writable($file)) || !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 +151,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 function __toString()
{
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 $ref Data referece. Optional. * @param string $data Data to save
* @return string Saved data referece * @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 * @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,19 +77,23 @@ 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
* *
* @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 Validation errors: [optionname => error message]
* *
* 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();
} }

View file

@ -199,42 +199,37 @@ 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::facStorage()->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) { $fieldname = $storage_form_prefix . '_' . $name;
$fieldname = $storage_form_prefix . '_' . $name; switch ($info[0]) { // type
switch ($info[0]) { // type case 'checkbox':
case 'checkbox': case 'yesno':
case 'yesno': $value = !empty($_POST[$fieldname]);
$value = !empty($_POST[$fieldname]); break;
break; default:
default: $value = $_POST[$fieldname] ?? '';
$value = $_POST[$fieldname] ?? '';
}
$storage_opts_data[$name] = $value;
} }
unset($name); $storage_opts_data[$name] = $value;
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.'));
} }
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 // 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; $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::facStorage()->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::facStorage()->move($current);
if ($moved) { if ($moved) {
Worker::add(PRIORITY_LOW, "CronJobs", "move_storage"); Worker::add(PRIORITY_LOW, "CronJobs", "move_storage");

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,19 @@ return [
'constructParams' => [ 'constructParams' => [
$_SERVER, $_COOKIE $_SERVER, $_COOKIE
], ],
] ],
StorageManager::class => [
'constructParams' => [
[Dice::INSTANCE => Dice::SELF],
]
],
IStorage::class => [
// Don't share this class with other creations, because it's possible to switch the backend
// and so we wouldn't be possible to update it
'shared' => false,
'instanceOf' => StorageManager::class,
'call' => [
['getBackend', [], Dice::CHAIN_CALL],
],
],
]; ];

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,109 @@
<?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()
{
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

@ -408,3 +408,15 @@ function update_1327()
return Update::SUCCESS; return Update::SUCCESS;
} }
function update_1329()
{
$currStorage = Config::get('storage', 'class', '');
if (!empty($currStorage)) {
$storageName = array_key_first(\Friendica\Core\StorageManager::DEFAULT_BACKENDS, $currStorage);
Config::set('storage', 'name', $storageName);
Config::delete('storage', 'class');
}
return Update::SUCCESS;
}