xref: /dokuwiki/inc/Search/Index/MemoryIndex.php (revision db8be586414d0dc05ca5131baddfa84f08c55520)
1<?php
2
3namespace dokuwiki\Search\Index;
4
5use dokuwiki\Logger;
6use dokuwiki\Search\Exception\IndexLockException;
7use dokuwiki\Search\Exception\IndexWriteException;
8
9/**
10 * Access to a single index file
11 *
12 * Access using this class always happens by loading the full index into memory.
13 * Changes can be made permanent explicitly via save(), but will also be
14 * auto-saved on destruction to prevent data loss when indexes are used in tandem
15 * (a new RID in one index may already be referenced by another).
16 * Should be used for small indexes that receive many changes at once.
17 */
18class MemoryIndex extends AbstractIndex
19{
20    /** @var string the raw data lines of the index, no newlines */
21    protected $data;
22
23    /** @var bool has the index been modified? */
24    protected $dirty = false;
25
26    /**
27     * Loads the full contents of the index into memory
28     *
29     * @inheritdoc
30     */
31    public function __construct($idx, $suffix = '', $isWritable = false)
32    {
33        parent::__construct($idx, $suffix, $isWritable);
34
35        $this->data = [];
36        if (!file_exists($this->filename)) {
37            return;
38        }
39        $this->data = file($this->filename, FILE_IGNORE_NEW_LINES);
40    }
41
42    /**
43     * Auto-save dirty data before releasing the lock
44     *
45     * When indexes are used in tandem, a new RID written to one index may already
46     * be referenced by other indexes that were saved. Losing unsaved data here
47     * would leave dangling references, causing silent index corruption.
48     *
49     * The try/catch is necessary because unlock() is called from __destruct()
50     * (in the parent class), and PHP destructors must not throw — a throw
51     * during exception unwinding causes a fatal error.
52     *
53     * @inheritdoc
54     */
55    public function unlock()
56    {
57        if ($this->isDirty()) {
58            try {
59                $this->save();
60            } catch (\Exception $e) {
61                Logger::error('MemoryIndex failed to save on unlock: ' . $e->getMessage());
62            }
63        }
64        parent::unlock();
65    }
66
67    /**
68     * @inheritdoc
69     * @throws IndexLockException
70     */
71    public function changeRow($rid, $value)
72    {
73        if (!$this->isWritable) throw new IndexLockException();
74
75        if ($rid > count($this->data)) {
76            $this->data = array_pad($this->data, $rid, '');
77        }
78        $this->data[$rid] = $value;
79        $this->dirty = true;
80    }
81
82    /**
83     * @inheritdoc
84     * @throws IndexLockException
85     */
86    public function retrieveRow($rid)
87    {
88        if (isset($this->data[$rid])) {
89            return $this->data[$rid];
90        }
91        if ($this->isWritable) {
92            $this->changeRow($rid, ''); // add to index
93        }
94        return '';
95    }
96
97    /** @inheritdoc */
98    public function retrieveRows($rids)
99    {
100        $result = [];
101        foreach ($rids as $rid) {
102            if (isset($this->data[$rid])) $result[$rid] = $this->data[$rid];
103        }
104
105        return $result;
106    }
107
108    /** @inheritdoc */
109    public function getRowIDs($values)
110    {
111        $values = array_map('trim', $values);
112        $values = array_fill_keys($values, 1); // easier access as associative array
113
114        $result = [];
115        $count = count($this->data);
116        for ($ln = 0; $ln < $count; $ln++) {
117            $line = $this->data[$ln];
118            if (isset($values[$line])) {
119                $result[$line] = $ln;
120                unset($values[$line]);
121            }
122        }
123
124        if (!$this->isWritable) return $result;
125
126        // if there are still values, they have not been found and will be appended
127        foreach (array_keys($values) as $value) {
128            $this->data[] = $value;
129            $result[$value] = $ln++;
130            $this->dirty = true;
131        }
132
133        return $result;
134    }
135
136    /** @inheritdoc */
137    public function search($re)
138    {
139        return preg_grep($re, $this->data);
140    }
141
142    /**
143     * Save the changed index back to its file
144     *
145     * The method will check the internal dirty state and will only write when the index has actually been changed
146     *
147     * @throws IndexWriteException
148     * @throws IndexLockException
149     */
150    public function save()
151    {
152        global $conf;
153
154        if (!$this->isDirty()) {
155            return;
156        }
157
158        if (!$this->isWritable) throw new IndexLockException();
159
160        $tempname = $this->filename . '.tmp';
161
162        $fh = @fopen($tempname, 'w');
163        if (!$fh) {
164            throw new IndexWriteException("Failed to write $tempname");
165        }
166        fwrite($fh, implode("\n", $this->data));
167        if (count($this->data)) {
168            fwrite($fh, "\n");
169        }
170        fclose($fh);
171
172        if ($conf['fperm']) {
173            chmod($tempname, $conf['fperm']);
174        }
175
176        if (!io_rename($tempname, $this->filename)) {
177            throw new IndexWriteException("Failed to write {$this->filename}");
178        }
179
180        $this->dirty = false;
181    }
182
183    /**
184     * Check if the index has been modified and needs to be saved
185     * @return bool
186     */
187    public function isDirty()
188    {
189        return $this->dirty;
190    }
191
192    /** @inheritdoc */
193    public function count(): int
194    {
195        return count($this->data);
196    }
197
198    /** @inheritdoc */
199    public function getIterator(): \ArrayIterator
200    {
201        return new \ArrayIterator($this->data);
202    }
203}
204