]> git.mxchange.org Git - friendica.git/blob - src/Core/Storage/Repository/StorageManager.php
Improved request value handling
[friendica.git] / src / Core / Storage / Repository / StorageManager.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Core\Storage\Repository;
23
24 use Exception;
25 use Friendica\Core\Config\Capability\IManageConfigValues;
26 use Friendica\Core\Hook;
27 use Friendica\Core\L10n;
28 use Friendica\Core\Storage\Exception\InvalidClassStorageException;
29 use Friendica\Core\Storage\Exception\ReferenceStorageException;
30 use Friendica\Core\Storage\Exception\StorageException;
31 use Friendica\Core\Storage\Capability\ICanReadFromStorage;
32 use Friendica\Core\Storage\Capability\ICanConfigureStorage;
33 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
34 use Friendica\Database\Database;
35 use Friendica\Core\Storage\Type;
36 use Friendica\Network\HTTPException\InternalServerErrorException;
37 use Psr\Log\LoggerInterface;
38
39 /**
40  * Manage storage backends
41  *
42  * Core code uses this class to get and set current storage backend class.
43  * Addons use this class to register and unregister additional backends.
44  */
45 class StorageManager
46 {
47         // Default tables to look for data
48         const TABLES = ['photo', 'attach'];
49
50         // Default storage backends
51         /** @var string[]  */
52         const DEFAULT_BACKENDS = [
53                 Type\Filesystem::NAME,
54                 Type\Database::NAME,
55         ];
56
57         /** @var string[] List of valid backend classes */
58         private $validBackends;
59
60         /**
61          * @var ICanReadFromStorage[] A local cache for storage instances
62          */
63         private $backendInstances = [];
64
65         /** @var Database */
66         private $dba;
67         /** @var IManageConfigValues */
68         private $config;
69         /** @var LoggerInterface */
70         private $logger;
71         /** @var L10n */
72         private $l10n;
73
74         /** @var ICanWriteToStorage */
75         private $currentBackend;
76
77         /**
78          * @param Database            $dba
79          * @param IManageConfigValues $config
80          * @param LoggerInterface     $logger
81          * @param L10n                $l10n
82          *
83          * @throws InvalidClassStorageException in case the active backend class is invalid
84          * @throws StorageException in case of unexpected errors during the active backend class loading
85          */
86         public function __construct(Database $dba, IManageConfigValues $config, LoggerInterface $logger, L10n $l10n)
87         {
88                 $this->dba           = $dba;
89                 $this->config        = $config;
90                 $this->logger        = $logger;
91                 $this->l10n          = $l10n;
92                 $this->validBackends = $config->get('storage', 'backends', self::DEFAULT_BACKENDS);
93
94                 $currentName = $this->config->get('storage', 'name');
95
96                 // you can only use user backends as a "default" backend, so the second parameter is true
97                 $this->currentBackend = $this->getWritableStorageByName($currentName);
98         }
99
100         /**
101          * Return current storage backend class
102          *
103          * @return ICanWriteToStorage
104          */
105         public function getBackend(): ICanWriteToStorage
106         {
107                 return $this->currentBackend;
108         }
109
110         /**
111          * Returns a writable storage backend class by registered name
112          *
113          * @param string $name Backend name
114          *
115          * @return ICanWriteToStorage
116          *
117          * @throws InvalidClassStorageException in case there's no backend class for the name
118          * @throws StorageException in case of an unexpected failure during the hook call
119          */
120         public function getWritableStorageByName(string $name): ICanWriteToStorage
121         {
122                 $storage = $this->getByName($name, $this->validBackends);
123                 if (!$storage instanceof ICanWriteToStorage) {
124                         throw new InvalidClassStorageException(sprintf('Backend %s is not writable', $name));
125                 }
126
127                 return $storage;
128         }
129
130         /**
131          * Return storage backend configuration by registered name
132          *
133          * @param string     $name Backend name
134          *
135          * @return ICanConfigureStorage|false
136          *
137          * @throws InvalidClassStorageException in case there's no backend class for the name
138          * @throws StorageException in case of an unexpected failure during the hook call
139          */
140         public function getConfigurationByName(string $name)
141         {
142                 switch ($name) {
143                         // Try the filesystem backend
144                         case Type\Filesystem::getName():
145                                 return new Type\FilesystemConfig($this->config, $this->l10n);
146                         // try the database backend
147                         case Type\Database::getName():
148                                 return false;
149                         default:
150                                 $data = [
151                                         'name'           => $name,
152                                         'storage_config' => null,
153                                 ];
154                                 try {
155                                         Hook::callAll('storage_config', $data);
156                                         if (!($data['storage_config'] ?? null) instanceof ICanConfigureStorage) {
157                                                 throw new InvalidClassStorageException(sprintf('Configuration for backend %s was not found', $name));
158                                         }
159
160                                         return $data['storage_config'];
161                                 } catch (InternalServerErrorException $exception) {
162                                         throw new StorageException(sprintf('Failed calling hook::storage_config for backend %s', $name), $exception);
163                                 }
164                 }
165         }
166
167         /**
168          * Return storage backend class by registered name
169          *
170          * @param string     $name Backend name
171          * @param string[]|null $validBackends possible, manual override of the valid backends
172          *
173          * @return ICanReadFromStorage
174          *
175          * @throws InvalidClassStorageException in case there's no backend class for the name
176          * @throws StorageException in case of an unexpected failure during the hook call
177          */
178         public function getByName(string $name, array $validBackends = null): ICanReadFromStorage
179         {
180                 // If there's no cached instance create a new instance
181                 if (!isset($this->backendInstances[$name])) {
182                         // If the current name isn't a valid backend (or the SystemResource instance) create it
183                         if (!$this->isValidBackend($name, $validBackends)) {
184                                 throw new InvalidClassStorageException(sprintf('Backend %s is not valid', $name));
185                         }
186
187                         switch ($name) {
188                                 // Try the filesystem backend
189                                 case Type\Filesystem::getName():
190                                         $storageConfig                 = new Type\FilesystemConfig($this->config, $this->l10n);
191                                         $this->backendInstances[$name] = new Type\Filesystem($storageConfig->getStoragePath());
192                                         break;
193                                 // try the database backend
194                                 case Type\Database::getName():
195                                         $this->backendInstances[$name] = new Type\Database($this->dba);
196                                         break;
197                                 // at least, try if there's an addon for the backend
198                                 case Type\SystemResource::getName():
199                                         $this->backendInstances[$name] = new Type\SystemResource();
200                                         break;
201                                 case Type\ExternalResource::getName():
202                                         $this->backendInstances[$name] = new Type\ExternalResource();
203                                         break;
204                                 default:
205                                         $data = [
206                                                 'name'    => $name,
207                                                 'storage' => null,
208                                         ];
209                                         try {
210                                                 Hook::callAll('storage_instance', $data);
211                                                 if (!($data['storage'] ?? null) instanceof ICanReadFromStorage) {
212                                                         throw new InvalidClassStorageException(sprintf('Backend %s was not found', $name));
213                                                 }
214
215                                                 $this->backendInstances[$data['name'] ?? $name] = $data['storage'];
216                                         } catch (InternalServerErrorException $exception) {
217                                                 throw new StorageException(sprintf('Failed calling hook::storage_instance for backend %s', $name), $exception);
218                                         }
219                                         break;
220                         }
221                 }
222
223                 return $this->backendInstances[$name];
224         }
225
226         /**
227          * Checks, if the storage is a valid backend
228          *
229          * @param string|null   $name          The name or class of the backend
230          * @param string[]|null $validBackends Possible, valid backends to check
231          *
232          * @return boolean True, if the backend is a valid backend
233          */
234         public function isValidBackend(string $name = null, array $validBackends = null): bool
235         {
236                 $validBackends = $validBackends ?? array_merge($this->validBackends,
237                                 [
238                                         Type\SystemResource::getName(),
239                                         Type\ExternalResource::getName(),
240                                 ]);
241                 return in_array($name, $validBackends);
242         }
243
244         /**
245          * Set current storage backend class
246          *
247          * @param ICanWriteToStorage $storage The storage class
248          *
249          * @return boolean True, if the set was successful
250          */
251         public function setBackend(ICanWriteToStorage $storage): bool
252         {
253                 if ($this->config->set('storage', 'name', $storage::getName())) {
254                         $this->currentBackend = $storage;
255                         return true;
256                 } else {
257                         return false;
258                 }
259         }
260
261         /**
262          * Get registered backends
263          *
264          * @return string[]
265          */
266         public function listBackends(): array
267         {
268                 return $this->validBackends;
269         }
270
271         /**
272          * Register a storage backend class
273          *
274          * You have to register the hook "storage_instance" as well to make this class work!
275          *
276          * @param string $class Backend class name
277          *
278          * @return boolean True, if the registration was successful
279          */
280         public function register(string $class): bool
281         {
282                 if (is_subclass_of($class, ICanReadFromStorage::class)) {
283                         /** @var ICanReadFromStorage $class */
284                         if ($this->isValidBackend($class::getName(), $this->validBackends)) {
285                                 return true;
286                         }
287
288                         $backends   = $this->validBackends;
289                         $backends[] = $class::getName();
290
291                         if ($this->config->set('storage', 'backends', $backends)) {
292                                 $this->validBackends = $backends;
293                                 return true;
294                         } else {
295                                 return false;
296                         }
297                 } else {
298                         return false;
299                 }
300         }
301
302         /**
303          * Unregister a storage backend class
304          *
305          * @param string $class Backend class name
306          *
307          * @return boolean True, if unregistering was successful
308          *
309          * @throws StorageException
310          */
311         public function unregister(string $class): bool
312         {
313                 if (is_subclass_of($class, ICanReadFromStorage::class)) {
314                         /** @var ICanReadFromStorage $class */
315                         if ($this->currentBackend::getName() == $class::getName()) {
316                                 throw new StorageException(sprintf('Cannot unregister %s, because it\'s currently active.', $class::getName()));
317                         }
318
319                         $key = array_search($class::getName(), $this->validBackends);
320
321                         if ($key !== false) {
322                                 $backends = $this->validBackends;
323                                 unset($backends[$key]);
324                                 $backends = array_values($backends);
325                                 if ($this->config->set('storage', 'backends', $backends)) {
326                                         $this->validBackends = $backends;
327                                         return true;
328                                 } else {
329                                         return false;
330                                 }
331                         } else {
332                                 return true;
333                         }
334                 } else {
335                         return false;
336                 }
337         }
338
339         /**
340          * Move up to 5000 resources to storage $dest
341          *
342          * Copy existing data to destination storage and delete from source.
343          * This method cannot move to legacy in-table `data` field.
344          *
345          * @param ICanWriteToStorage $destination Destination storage class name
346          * @param array              $tables      Tables to look in for resources. Optional, defaults to ['photo', 'attach']
347          * @param int                $limit       Limit of the process batch size, defaults to 5000
348          *
349          * @return int Number of moved resources
350          * @throws StorageException
351          * @throws Exception
352          */
353         public function move(ICanWriteToStorage $destination, array $tables = self::TABLES, int $limit = 5000): int
354         {
355                 if (!$this->isValidBackend($destination, $this->validBackends)) {
356                         throw new StorageException(sprintf("Can't move to storage backend '%s'", $destination::getName()));
357                 }
358
359                 $moved = 0;
360                 foreach ($tables as $table) {
361                         // Get the rows where backend class is not the destination backend class
362                         $resources = $this->dba->select(
363                                 $table,
364                                 ['id', 'data', 'backend-class', 'backend-ref'],
365                                 ['`backend-class` IS NULL or `backend-class` != ?', $destination::getName()],
366                                 ['limit' => $limit]
367                         );
368
369                         while ($resource = $this->dba->fetch($resources)) {
370                                 $id        = $resource['id'];
371                                 $sourceRef = $resource['backend-ref'];
372                                 $source    = null;
373
374                                 try {
375                                         $source = $this->getWritableStorageByName($resource['backend-class'] ?? '');
376                                         $this->logger->info('Get data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
377                                         $data = $source->get($sourceRef);
378                                 } catch (InvalidClassStorageException $exception) {
379                                         $this->logger->info('Get data from DB resource field.', ['oldReference' => $sourceRef]);
380                                         $data = $resource['data'];
381                                 } catch (ReferenceStorageException $exception) {
382                                         $this->logger->info('Invalid source reference.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
383                                         continue;
384                                 }
385
386                                 $this->logger->info('Save data to new backend.', ['newBackend' => $destination::getName()]);
387                                 $destinationRef = $destination->put($data);
388                                 $this->logger->info('Saved data.', ['newReference' => $destinationRef]);
389
390                                 if ($destinationRef !== '') {
391                                         $this->logger->info('update row');
392                                         if ($this->dba->update($table, ['backend-class' => $destination::getName(), 'backend-ref' => $destinationRef, 'data' => ''], ['id' => $id])) {
393                                                 if (!empty($source)) {
394                                                         $this->logger->info('Deleted data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
395                                                         $source->delete($sourceRef);
396                                                 }
397                                                 $moved++;
398                                         }
399                                 }
400                         }
401
402                         $this->dba->close($resources);
403                 }
404
405                 return $moved;
406         }
407 }