Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 129
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
storage
0.00% covered (danger)
0.00%
0 / 129
0.00% covered (danger)
0.00%
0 / 19
1722
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 get_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_adapter
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 put_contents
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_contents
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 exists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
12
 delete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 rename
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 copy
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 read_stream
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 write_stream
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 track_file
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 untrack_file
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 is_tracked
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 track_rename
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 file_size
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 get_size
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 get_num_files
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 free_space
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\storage;
15
16use phpbb\cache\driver\driver_interface as cache;
17use phpbb\db\driver\driver_interface as db;
18use phpbb\storage\adapter\adapter_interface;
19use phpbb\storage\exception\storage_exception;
20
21/**
22 * Experimental
23 */
24class storage
25{
26    /**
27     * @var adapter_interface
28     */
29    protected $adapter;
30
31    /**
32     * @var db
33     */
34    protected $db;
35
36    /**
37     * Cache driver
38     * @var cache
39     */
40    protected $cache;
41
42    /**
43     * @var adapter_factory
44     */
45    protected $factory;
46
47    /**
48     * @var string
49     */
50    protected $storage_name;
51
52    /**
53     * @var string
54     */
55    protected $storage_table;
56
57    /**
58     * Constructor
59     *
60     * @param db                                $db
61     * @param cache                                $cache
62     * @param adapter_factory                     $factory
63     * @param string                            $storage_name
64     * @param string                            $storage_table
65     */
66    public function __construct(db $db, cache $cache, adapter_factory $factory, string $storage_name, string $storage_table)
67    {
68        $this->db = $db;
69        $this->cache = $cache;
70        $this->factory = $factory;
71        $this->storage_name = $storage_name;
72        $this->storage_table = $storage_table;
73    }
74
75    /**
76     * Returns storage name
77     *
78     * @return string
79     */
80    public function get_name(): string
81    {
82        return $this->storage_name;
83    }
84
85    /**
86     * Returns an adapter instance
87     *
88     * @return adapter_interface
89     */
90    protected function get_adapter(): mixed
91    {
92        if ($this->adapter === null)
93        {
94            $this->adapter = $this->factory->get($this->storage_name);
95        }
96
97        return $this->adapter;
98    }
99
100    /**
101     * Dumps content into a file
102     *
103     * @param string    $path        The file to be written to.
104     * @param string    $content        The data to write into the file.
105     *
106     * @throws storage_exception    When the file already exists
107     *                         When the file cannot be written
108     */
109    public function put_contents(string $path, string $content): void
110    {
111        if ($this->exists($path))
112        {
113            throw new storage_exception('STORAGE_FILE_EXISTS', $path);
114        }
115
116        $this->get_adapter()->put_contents($path, $content);
117        $this->track_file($path);
118    }
119
120    /**
121     * Read the contents of a file
122     *
123     * @param string    $path    The file to read
124     *
125     * @return string    Returns file contents
126     *
127     * @throws storage_exception    When the file doesn't exist
128     *                         When cannot read file contents
129     *
130     */
131    public function get_contents(string $path): string
132    {
133        if (!$this->exists($path))
134        {
135            throw new storage_exception('STORAGE_FILE_NO_EXIST', $path);
136        }
137
138        return $this->get_adapter()->get_contents($path);
139    }
140
141    /**
142     * Checks the existence of files or directories
143     *
144     * @param string    $path        file/directory to check
145     * @param bool        $full_check    check in the filesystem too
146     *
147     * @return bool    Returns true if the file/directory exist, false otherwise
148     */
149    public function exists(string $path, bool $full_check = false): bool
150    {
151        return ($this->is_tracked($path) && (!$full_check || $this->get_adapter()->exists($path)));
152    }
153
154    /**
155     * Removes files or directories
156     *
157     * @param string $path    file/directory to remove
158     *
159     * @throws storage_exception    When removal fails
160     *                        When the file doesn't exist
161     */
162    public function delete(string $path): void
163    {
164        if (!$this->exists($path))
165        {
166            throw new storage_exception('STORAGE_FILE_NO_EXIST', $path);
167        }
168
169        $this->get_adapter()->delete($path);
170        $this->untrack_file($path);
171    }
172
173    /**
174     * Rename a file or a directory
175     *
176     * @param string $path_orig    The original file/direcotry
177     * @param string $path_dest    The target file/directory
178     *
179     * @throws storage_exception    When the file doesn't exist
180     *                        When target exists
181     *                         When file/directory cannot be renamed
182     */
183    public function rename(string $path_orig, string $path_dest): void
184    {
185        if (!$this->exists($path_orig))
186        {
187            throw new storage_exception('STORAGE_FILE_NO_EXIST', $path_orig);
188        }
189
190        if ($this->exists($path_dest))
191        {
192            throw new storage_exception('STORAGE_FILE_EXISTS', $path_dest);
193        }
194
195        $this->get_adapter()->rename($path_orig, $path_dest);
196        $this->track_rename($path_orig, $path_dest);
197    }
198
199    /**
200     * Copies a file
201     *
202     * @param string $path_orig    The original filename
203     * @param string $path_dest    The target filename
204     *
205     * @throws storage_exception    When the file doesn't exist
206     *                        When target exists
207     *                         When the file cannot be copied
208     */
209    public function copy(string $path_orig, string $path_dest): void
210    {
211        if (!$this->exists($path_orig))
212        {
213            throw new storage_exception('STORAGE_FILE_NO_EXIST', $path_orig);
214        }
215
216        if ($this->exists($path_dest))
217        {
218            throw new storage_exception('STORAGE_FILE_EXISTS', $path_dest);
219        }
220
221        $this->get_adapter()->copy($path_orig, $path_dest);
222        $this->track_file($path_dest);
223    }
224
225    /**
226     * Reads a file as a stream
227     *
228     * @param string $path    File to read
229     *
230     * @return resource    Returns a file pointer
231     * @throws storage_exception    When the file doesn't exist
232     *                        When unable to open file
233     *
234     */
235    public function read_stream(string $path)
236    {
237        if (!$this->exists($path))
238        {
239            throw new storage_exception('STORAGE_FILE_NO_EXIST', $path);
240        }
241
242        $stream = null;
243        $adapter = $this->get_adapter();
244
245        if ($adapter instanceof stream_interface)
246        {
247            $stream = $adapter->read_stream($path);
248        }
249        else
250        {
251            // Simulate the stream
252            $stream = fopen('php://temp', 'w+b');
253            fwrite($stream, $adapter->get_contents($path));
254            rewind($stream);
255        }
256
257        return $stream;
258    }
259
260    /**
261     * Writes a new file using a stream
262     *
263     * @param string $path        The target file
264     * @param resource    $resource    The resource
265     *
266     * @throws storage_exception    When the file exist
267     *                        When target file cannot be created
268     */
269    public function write_stream(string $path, $resource): void
270    {
271        if ($this->exists($path))
272        {
273            throw new storage_exception('STORAGE_FILE_EXISTS', $path);
274        }
275
276        if (!is_resource($resource))
277        {
278            throw new storage_exception('STORAGE_INVALID_RESOURCE');
279        }
280
281        $adapter = $this->get_adapter();
282
283        if ($adapter instanceof stream_interface)
284        {
285            $adapter->write_stream($path, $resource);
286            $this->track_file($path);
287        }
288        else
289        {
290            // Simulate the stream
291            $adapter->put_contents($path, stream_get_contents($resource));
292        }
293    }
294
295    /**
296     * Track file in database
297     *
298     * @param string $path        The target file
299     * @param bool $update        Update file size when already tracked
300     */
301    public function track_file(string $path, bool $update = false): void
302    {
303        if (!$this->get_adapter()->exists($path))
304        {
305            throw new storage_exception('STORAGE_FILE_NO_EXIST', $path);
306        }
307
308        $sql_ary = array(
309            'file_path'        => $path,
310            'storage'        => $this->get_name(),
311        );
312
313        // Get file, if exist update filesize, if not add new record
314        $sql = 'SELECT * FROM ' .  $this->storage_table . '
315                WHERE ' . $this->db->sql_build_array('SELECT', $sql_ary);
316        $result = $this->db->sql_query($sql);
317        $row = $this->db->sql_fetchrow($result);
318        $this->db->sql_freeresult($result);
319
320        if (!$row)
321        {
322            $sql_ary['filesize'] = $this->get_adapter()->file_size($path);
323
324            $sql = 'INSERT INTO ' . $this->storage_table . $this->db->sql_build_array('INSERT', $sql_ary);
325            $this->db->sql_query($sql);
326        }
327        else if ($update)
328        {
329            $sql = 'UPDATE ' . $this->storage_table . '
330                SET filesize = ' . $this->get_adapter()->file_size($path) . '
331                WHERE ' . $this->db->sql_build_array('SELECT', $sql_ary);
332            $this->db->sql_query($sql);
333        }
334
335        $this->cache->destroy('_storage_' . $this->get_name() . '_totalsize');
336        $this->cache->destroy('_storage_' . $this->get_name() . '_numfiles');
337    }
338
339    /**
340     * Untrack file
341     *
342     * @param string    $path        The target file
343     */
344    public function untrack_file($path)
345    {
346        $sql_ary = array(
347            'file_path'        => $path,
348            'storage'        => $this->get_name(),
349        );
350
351        $sql = 'DELETE FROM ' . $this->storage_table . '
352            WHERE ' . $this->db->sql_build_array('DELETE', $sql_ary);
353        $this->db->sql_query($sql);
354
355        $this->cache->destroy('_storage_' . $this->get_name() . '_totalsize');
356        $this->cache->destroy('_storage_' . $this->get_name() . '_numfiles');
357    }
358
359    /**
360     * Check if a file is tracked
361     *
362     * @param string $path    The file
363     *
364     * @return bool    True if file is tracked
365     */
366    public function is_tracked(string $path): bool
367    {
368        $sql_ary = array(
369            'file_path'        => $path,
370            'storage'        => $this->get_name(),
371        );
372
373        $sql = 'SELECT file_id FROM ' .  $this->storage_table . '
374                WHERE ' . $this->db->sql_build_array('SELECT', $sql_ary);
375        $result = $this->db->sql_query($sql);
376        $row = $this->db->sql_fetchrow($result);
377        $this->db->sql_freeresult($result);
378
379        return $row !== false;
380    }
381
382    /**
383     * Rename tracked file
384     *
385     * @param string $path_orig    The original file/direcotry
386     * @param string $path_dest    The target file/directory
387     */
388    protected function track_rename(string $path_orig, string $path_dest): void
389    {
390        $sql = 'UPDATE ' . $this->storage_table . "
391            SET file_path = '" . $this->db->sql_escape($path_dest) . "'
392            WHERE file_path = '" . $this->db->sql_escape($path_orig) . "'
393                AND storage = '" . $this->db->sql_escape($this->get_name()) . "'";
394        $this->db->sql_query($sql);
395    }
396
397    /**
398     * Get file size in bytes
399     *
400     * @param string $path The file
401     *
402     * @return int Size in bytes.
403     *
404     * @throws storage_exception When unable to retrieve file size
405     */
406    public function file_size(string $path): int
407    {
408        $sql_ary = array(
409            'file_path'        => $path,
410            'storage'        => $this->get_name(),
411        );
412
413        $sql = 'SELECT filesize FROM ' .  $this->storage_table . '
414                WHERE ' . $this->db->sql_build_array('SELECT', $sql_ary);
415        $result = $this->db->sql_query($sql);
416        $row = $this->db->sql_fetchrow($result);
417        $this->db->sql_freeresult($result);
418
419        return $row !== false && !empty($row['filesize']) ? $row['filesize'] : $this->get_adapter()->file_size($path);
420    }
421
422    /**
423     * Get total storage size
424     *
425     * @return int    Size in bytes
426     */
427    public function get_size(): int
428    {
429        $total_size = $this->cache->get('_storage_' . $this->get_name() . '_totalsize');
430
431        if ($total_size === false)
432        {
433            $sql = 'SELECT SUM(filesize) AS totalsize
434                FROM ' .  $this->storage_table . "
435                WHERE storage = '" . $this->db->sql_escape($this->get_name()) . "'";
436            $result = $this->db->sql_query($sql);
437
438            $total_size = (int) $this->db->sql_fetchfield('totalsize');
439            $this->cache->put('_storage_' . $this->get_name() . '_totalsize', $total_size);
440
441            $this->db->sql_freeresult($result);
442        }
443
444        return (int) $total_size;
445    }
446
447    /**
448     * Get number of storage files
449     *
450     * @return int    Number of files
451     */
452    public function get_num_files(): int
453    {
454        $number_files = $this->cache->get('_storage_' . $this->get_name() . '_numfiles');
455
456        if ($number_files === false)
457        {
458            $sql = 'SELECT COUNT(file_id) AS numfiles
459                FROM ' .  $this->storage_table . "
460                WHERE storage = '" . $this->db->sql_escape($this->get_name()) . "'";
461            $result = $this->db->sql_query($sql);
462
463            $number_files = (int) $this->db->sql_fetchfield('numfiles');
464            $this->cache->put('_storage_' . $this->get_name() . '_numfiles', $number_files);
465
466            $this->db->sql_freeresult($result);
467        }
468
469        return (int) $number_files;
470    }
471
472    /**
473     * Get space available in bytes
474     *
475     * @return float    Returns available space
476     * @throws storage_exception        When unable to retrieve available storage space
477     *
478     */
479    public function free_space()
480    {
481        return $this->get_adapter()->free_space();
482    }
483
484}