Make Storage testable & add tests
authornupplaPhil <admin@philipp.info>
Sun, 5 Jan 2020 00:58:49 +0000 (01:58 +0100)
committernupplaPhil <admin@philipp.info>
Fri, 10 Jan 2020 12:21:57 +0000 (13:21 +0100)
- 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)

18 files changed:
composer.json
composer.lock
doc/AddonStorageBackend.md
src/Console/Storage.php
src/Core/StorageManager.php
src/DI.php
src/Model/Attach.php
src/Model/Photo.php
src/Model/Storage/Database.php
src/Model/Storage/Filesystem.php
src/Model/Storage/IStorage.php
src/Module/Admin/Site.php
src/Worker/CronJobs.php
static/dependencies.config.php
tests/src/Model/Storage/DatabaseStorageTest.php [new file with mode: 0644]
tests/src/Model/Storage/FilesystemStorageTest.php [new file with mode: 0644]
tests/src/Model/Storage/StorageTest.php [new file with mode: 0644]
update.php

index e372547..d587b72 100644 (file)
@@ -32,7 +32,7 @@
                "ezyang/htmlpurifier": "^4.7",
                "friendica/json-ld": "^1.0",
                "league/html-to-markdown": "^4.8",
-               "level-2/dice": "^4",
+               "level-2/dice": "dev-master",
                "lightopenid/lightopenid": "dev-master",
                "michelf/php-markdown": "^1.7",
                "mobiledetect/mobiledetectlib": "^2.8",
index 3a4670b..ad63fb1 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "34ad225ce21474eb84ce78047d9f2c01",
+    "content-hash": "bf05cd52bc7307f45aff80f1d1fd8214",
     "packages": [
         {
             "name": "asika/simple-console",
                     "jsonld.php"
                 ]
             },
+            "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "BSD-3-Clause"
             ],
             "description": "A JSON-LD Processor and API implementation in PHP.",
             "homepage": "https://git.friendi.ca/friendica/php-json-ld",
             "keywords": [
-                "JSON",
                 "JSON-LD",
                 "Linked Data",
                 "RDF",
                 "Semantic Web",
+                "json",
                 "jsonld"
             ],
             "time": "2018-10-08T20:41:00+00:00"
         },
         {
             "name": "level-2/dice",
-            "version": "4.0.1",
+            "version": "dev-master",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Level-2/Dice.git",
-                "reference": "e631f110f0520294fec902814c61cac26566023c"
+                "reference": "2fea2749a625c3adcc29c402218b0dcaed11586f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Level-2/Dice/zipball/e631f110f0520294fec902814c61cac26566023c",
-                "reference": "e631f110f0520294fec902814c61cac26566023c",
+                "url": "https://api.github.com/repos/Level-2/Dice/zipball/2fea2749a625c3adcc29c402218b0dcaed11586f",
+                "reference": "2fea2749a625c3adcc29c402218b0dcaed11586f",
                 "shasum": ""
             },
             "require": {
                 "di",
                 "ioc"
             ],
-            "time": "2019-05-01T12:55:36+00:00"
+            "time": "2019-10-03T16:08:46+00:00"
         },
         {
             "name": "lightopenid/lightopenid",
             "require": {
                 "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",
             "extra": {
                 "npm-asset-bugs": {
                 "url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.6.tgz",
                 "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",
             "extra": {
                 "npm-asset-bugs": {
                 "url": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
                 "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",
             "extra": {
                 "npm-asset-bugs": {
                 "url": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz",
                 "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",
             "extra": {
                 "npm-asset-bugs": {
             "authors": [
                 {
                     "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.",
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "role": "lead",
-                    "email": "sebastian@phpunit.de"
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
                 }
             ],
             "description": "The PHP Unit Testing framework.",
                 }
             ],
             "description": "Provides the functionality to compare PHP values for equality",
-            "homepage": "http://www.github.com/sebastianbergmann/comparator",
+            "homepage": "https://github.com/sebastianbergmann/comparator",
             "keywords": [
                 "comparator",
                 "compare",
                 }
             ],
             "description": "Provides functionality to handle HHVM/PHP environments",
-            "homepage": "http://www.github.com/sebastianbergmann/environment",
+            "homepage": "https://github.com/sebastianbergmann/environment",
             "keywords": [
                 "Xdebug",
                 "environment",
                 }
             ],
             "description": "Provides the functionality to export PHP variables for visualization",
-            "homepage": "http://www.github.com/sebastianbergmann/exporter",
+            "homepage": "https://github.com/sebastianbergmann/exporter",
             "keywords": [
                 "export",
                 "exporter"
                 }
             ],
             "description": "Snapshotting of global state",
-            "homepage": "http://www.github.com/sebastianbergmann/global-state",
+            "homepage": "https://github.com/sebastianbergmann/global-state",
             "keywords": [
                 "global state"
             ],
                 }
             ],
             "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"
         },
         {
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": {
+        "level-2/dice": 20,
         "lightopenid/lightopenid": 20
     },
     "prefer-stable": false,
index d42c8bb..d105b03 100644 (file)
@@ -182,21 +182,21 @@ 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::facStorage()->register("Sample Storage", SampleStorageBackend::class);
 }
 
 function samplestorage_unistall()
 {
        // when the plugin is uninstalled, we unregister the backend.
-       StorageManager::unregister("Sample Storage");
+       DI::facStorage()->unregister("Sample Storage");
 }
 ```
 
index 30b5567..f4b4de5 100644 (file)
@@ -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));
                        }
index 832d981..ede8966 100644 (file)
@@ -2,8 +2,12 @@
 
 namespace Friendica\Core;
 
-use Friendica\Database\DBA;
-use Friendica\Model\Storage\IStorage;
+use Dice\Dice;
+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
 {
-       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 = [];
+
+       /** @var Database */
+       private $dba;
+       /** @var IConfiguration */
+       private $config;
+       /** @var LoggerInterface */
+       private $logger;
+       /** @var Dice */
+       private $dice;
+
+       /** @var Storage\IStorage */
+       private $currentBackend;
 
-       private static function setup()
+       /**
+        * @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) {
-                       self::$backends = Config::get('storage', 'backends', self::$default_backends);
+               $this->dba      = $dba;
+               $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
         *
-        * @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 $name Backend name
+        *
+        * @return Storage\IStorage|null null if no backend registered at $name
+        */
+       public function getByName(string $name)
+       {
+               if (!$this->isValidBackend($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 static function getByName($name)
+       public function isValidBackend(string $name)
        {
-               self::setup();
-               return self::$backends[$name] ?? '';
+               return array_key_exists($name, $this->backends);
        }
 
        /**
         * @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)
        {
-               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->dice->create($this->backends[$name]);
+                       return true;
+               } else {
+                       return false;
+               }
        }
 
        /**
@@ -74,107 +127,109 @@ 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
         *
         * @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 $name, 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)) {
+                       return false;
+               }
 
+               $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
         *
         * @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(self::$backends[$name]);
-               Config::set('storage', 'backends', self::$backends);
+               unset($this->backends[$name]);
+               return $this->config->set('storage', 'backends', $this->backends);
        }
 
-
        /**
         * @brief Move up to 5000 resources to storage $dest
         *
         * Copy existing data to destination storage and delete from source.
         * This method cannot move to legacy in-table `data` field.
         *
-        * @param 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],
                                ['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);
-
-                               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);
+                               $this->logger->info('Save data to new backend.', ['newBackend' => $destination]);
+                               $destinationRef = $destination->put($data);
+                               $this->logger->info('Saved data.', ['newReference' => $destinationRef]);
+
+                               if ($destinationRef !== '') {
+                                       $this->logger->info('update row');
+                                       if ($this->dba->update($table, ['backend-class' => $destination, '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;
        }
 }
-
index 7ed5812..152e705 100644 (file)
@@ -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 facStorage()
  * @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()
@@ -64,12 +66,14 @@ abstract class DI
                'lock'         => Core\Lock\ILock::class,
                'process'      => Core\Process::class,
                'session'      => Core\Session\ISession::class,
+               'facStorage'   => 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,
+               'storage'      => Model\Storage\IStorage::class,
                'intro'        => Repository\Introduction::class,
                'activity'     => Protocol\Activity::class,
                'aclFormatter' => Util\ACLFormatter::class,
index c1d5c03..102e7b1 100644 (file)
@@ -186,13 +186,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 +205,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
                ];
 
index c4dbf2b..7aa6ffd 100644 (file)
@@ -273,18 +273,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::facStorage()->getByName((string)$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 +308,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
                ];
 
index 60bd154..84dd116 100644 (file)
@@ -6,9 +6,8 @@
 
 namespace Friendica\Model\Storage;
 
-use Friendica\Core\Logger;
 use Friendica\Core\L10n;
-use Friendica\Database\DBA;
+use Psr\Log\LoggerInterface;
 
 /**
  * @brief Database based storage system
@@ -17,47 +16,93 @@ use Friendica\Database\DBA;
  */
 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)
+       {
+               $this->dba    = $dba;
+               $this->logger = $logger;
+               $this->l10n   = $l10n;
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function get(string $reference)
        {
-               $r = DBA::selectFirst('storage', ['data'], ['id' => $ref]);
-               if (!DBA::isResult($r)) {
+               $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));
+               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 $ref;
+
+                       return $reference;
                } 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'));
+                       $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 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 [];
        }
 
-       public static function saveOptions($data)
+       /**
+        * @inheritDoc
+        */
+       public function saveOptions(array $data)
        {
                return [];
        }
+
+       /**
+        * @inheritDoc
+        */
+       public function __toString()
+       {
+               return self::NAME;
+       }
 }
index ff7c594..e954352 100644 (file)
@@ -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
@@ -23,50 +23,74 @@ use Friendica\Util\Strings;
  */
 class Filesystem implements IStorage
 {
+       const NAME = 'Filesystem';
+
        // Default base folder
        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);
-               return rtrim($path, '/');
+               $this->config = $config;
+               $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
-        * @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 +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)) {
                        return '';
                }
@@ -90,27 +117,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_exists($file) && !is_writable($file)) || !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 +151,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 function __toString()
+       {
+               return self::NAME;
+       }
 }
index 1b0129e..a7b7f6f 100644 (file)
@@ -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,23 @@ 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();
+}
index 3302e0d..e68f379 100644 (file)
@@ -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::facStorage()->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);
+                               $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);
                                }
-                       } else {
-                               info(L10n::t('Invalid storage backend setting value.'));
+                               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::facStorage()->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;
index 6267bf6..6fa0919 100644 (file)
@@ -324,8 +324,8 @@ class CronJobs
         */
        private static function moveStorage()
        {
-               $current = StorageManager::getBackend();
-               $moved = StorageManager::move($current);
+               $current = DI::storage();
+               $moved = DI::facStorage()->move($current);
 
                if ($moved) {
                        Worker::add(PRIORITY_LOW, "CronJobs", "move_storage");
index 6cd077b..0bf5a69 100644 (file)
@@ -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,19 @@ return [
                'constructParams' => [
                        $_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],
+               ],
+       ],
 ];
diff --git a/tests/src/Model/Storage/DatabaseStorageTest.php b/tests/src/Model/Storage/DatabaseStorageTest.php
new file mode 100644 (file)
index 0000000..d6dff99
--- /dev/null
@@ -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());
+       }
+}
diff --git a/tests/src/Model/Storage/FilesystemStorageTest.php b/tests/src/Model/Storage/FilesystemStorageTest.php
new file mode 100644 (file)
index 0000000..2dd4672
--- /dev/null
@@ -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));
+       }
+}
diff --git a/tests/src/Model/Storage/StorageTest.php b/tests/src/Model/Storage/StorageTest.php
new file mode 100644 (file)
index 0000000..ae3f8f0
--- /dev/null
@@ -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));
+       }
+}
index 40f39eb..90018b6 100644 (file)
@@ -408,3 +408,15 @@ function update_1327()
        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;
+}