aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/bookmark/BookmarkIO.php
blob: 8439d470da21fdff3d0b7da95230de0594e5a341 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
<?php

declare(strict_types=1);

namespace Shaarli\Bookmark;

use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\mutex\Mutex;
use malkusch\lock\mutex\NoMutex;
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
use Shaarli\Config\ConfigManager;

/**
 * Class BookmarkIO
 *
 * This class performs read/write operation to the file data store.
 * Used by BookmarkFileService.
 *
 * @package Shaarli\Bookmark
 */
class BookmarkIO
{
    /**
     * @var string Datastore file path
     */
    protected $datastore;

    /**
     * @var ConfigManager instance
     */
    protected $conf;


    /** @var Mutex */
    protected $mutex;

    /**
     * string Datastore PHP prefix
     */
    protected static $phpPrefix = '<?php /* ';
    /**
     * string Datastore PHP suffix
     */
    protected static $phpSuffix = ' */ ?>';

    /**
     * LinksIO constructor.
     *
     * @param ConfigManager $conf instance
     */
    public function __construct(ConfigManager $conf, Mutex $mutex = null)
    {
        if ($mutex === null) {
            // This should only happen with legacy classes
            $mutex = new NoMutex();
        }
        $this->conf = $conf;
        $this->datastore = $conf->get('resource.datastore');
        $this->mutex = $mutex;
    }

    /**
     * Reads database from disk to memory
     *
     * @return Bookmark[]
     *
     * @throws NotWritableDataStoreException    Data couldn't be loaded
     * @throws EmptyDataStoreException          Datastore file exists but does not contain any bookmark
     * @throws DatastoreNotInitializedException File does not exists
     */
    public function read()
    {
        if (! file_exists($this->datastore)) {
            throw new DatastoreNotInitializedException();
        }

        if (!is_writable($this->datastore)) {
            throw new NotWritableDataStoreException($this->datastore);
        }

        $content = null;
        $this->synchronized(function () use (&$content) {
            $content = file_get_contents($this->datastore);
        });

        // Note that gzinflate is faster than gzuncompress.
        // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
        $links = unserialize(gzinflate(base64_decode(
            substr($content, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
        )));

        if (empty($links)) {
            if (filesize($this->datastore) > 100) {
                throw new NotWritableDataStoreException($this->datastore);
            }
            throw new EmptyDataStoreException();
        }

        return $links;
    }

    /**
     * Saves the database from memory to disk
     *
     * @param Bookmark[] $links
     *
     * @throws NotWritableDataStoreException the datastore is not writable
     */
    public function write($links)
    {
        if (is_file($this->datastore) && !is_writeable($this->datastore)) {
            // The datastore exists but is not writeable
            throw new NotWritableDataStoreException($this->datastore);
        } elseif (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
            // The datastore does not exist and its parent directory is not writeable
            throw new NotWritableDataStoreException(dirname($this->datastore));
        }

        $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix;

        $this->synchronized(function () use ($data) {
            file_put_contents(
                $this->datastore,
                $data
            );
        });
    }

    /**
     * Wrapper applying mutex to provided function.
     * If the lock can't be acquired (e.g. some shared hosting provider), we execute the function without mutex.
     *
     * @see https://github.com/shaarli/Shaarli/issues/1650
     *
     * @param callable $function
     */
    protected function synchronized(callable $function): void
    {
        try {
            $this->mutex->synchronized($function);
        } catch (LockAcquireException $exception) {
            $function();
        }
    }
}