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