xref: /dokuwiki/inc/Search/Index/FileIndex.php (revision 45fa8bb2a446dbebd88664c2568b73bf8c814ba3)
1<?php
2
3namespace dokuwiki\Search\Index;
4
5use dokuwiki\Search\Exception\IndexAccessException;
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 on a line-by-line basis. It is usually not read in full.
13 * All modifications are implicitly saved
14 * Should be used for large indexes that receive only few changes at once.
15 */
16class FileIndex extends AbstractIndex
17{
18    /** @var array RID cache for faster access */
19    protected array $ridCache = [];
20
21    /**
22     * @inheritdoc
23     * @throws IndexWriteException
24     * @throws IndexLockException
25     * @author Tom N Harris <tnharris@whoopdedo.org>
26     */
27    public function changeRow(int $rid, string $value): void
28    {
29        global $conf;
30
31        if (!$this->isWritable) throw new IndexLockException();
32
33        if (!str_ends_with($value, "\n")) {
34            $value .= "\n";
35        }
36
37        $tempname = $this->filename . '.tmp';
38        $fh = @fopen($tempname, 'w');
39        if (!$fh) {
40            throw new IndexWriteException("Failed to write $tempname");
41        }
42        $ih = @fopen($this->filename, 'r');
43
44        $ln = -1; // line counter
45        // copy previous index lines line-by-line, replacing the wanted line
46        if ($ih) {
47            while (($curline = fgets($ih)) !== false) {
48                fwrite($fh, (++$ln == $rid) ? $value : $curline);
49            }
50            fclose($ih);
51        }
52        // if wanted line is beyond the current line count, insert empty lines inbetween
53        if ($rid > $ln) {
54            while ($rid > ++$ln) {
55                fwrite($fh, "\n");
56            }
57            fwrite($fh, $value);
58        }
59        fclose($fh);
60
61        if ($conf['fperm']) {
62            chmod($tempname, $conf['fperm']);
63        }
64        io_rename($tempname, $this->filename);
65    }
66
67    /** @inheritdoc */
68    public function retrieveRow(int $rid): string
69    {
70        if (!file_exists($this->filename)) {
71            return '';
72        }
73        $fh = @fopen($this->filename, 'r');
74        if (!$fh) {
75            return '';
76        }
77        $ln = -1;
78        while (($line = fgets($fh)) !== false) {
79            if (++$ln == $rid) {
80                fclose($fh);
81                return rtrim($line);
82            }
83        }
84        fclose($fh);
85
86        return '';
87    }
88
89    /** @inheritdoc */
90    public function retrieveRows(array $rids): array
91    {
92        $result = [];
93        sort($rids);
94        $next = array_shift($rids);
95        if ($next === null) return $result;
96
97        if (!file_exists($this->filename)) {
98            return $result;
99        }
100        $fh = @fopen($this->filename, 'r');
101        if (!$fh) {
102            return $result;
103        }
104        $ln = -1;
105        while (($line = fgets($fh)) !== false) {
106            if (++$ln === $next) {
107                $result[$ln] = rtrim($line);
108                $next = array_shift($rids);
109                if ($next === null) break;
110            }
111        }
112        fclose($fh);
113        return $result;
114    }
115
116
117    /**
118     * @inheritdoc
119     * @throws IndexAccessException
120     * @throws IndexWriteException
121     */
122    public function getRowIDs(array $values): array
123    {
124        $values = array_map(trim(...), $values);
125        $values = array_fill_keys($values, 1); // easier access as associative array
126
127        // search for the values
128        $result = [];
129        $ln = 0;
130        if (file_exists($this->filename)) {
131            $fh = @fopen($this->filename, 'r');
132            if (!$fh) {
133                throw new IndexAccessException("Failed to read $this->filename");
134            }
135            while (($line = fgets($fh)) !== false && $values) {
136                $line = trim($line);
137                if (isset($values[$line])) {
138                    $result[$line] = $ln;
139                    unset($values[$line]);
140                }
141                $ln++;
142            }
143            fclose($fh);
144        }
145
146        if (!$this->isWritable) return $result;
147
148        // if there are still values, they have not been found and will be appended
149        foreach (array_keys($values) as $value) {
150            if (!file_put_contents($this->filename, "$value\n", FILE_APPEND)) {
151                throw new IndexWriteException("Failed to write $this->filename");
152            }
153            $result[$value] = $ln++;
154        }
155
156        return $result;
157    }
158
159    /** @inheritdoc */
160    public function search(string $re): array
161    {
162        $result = [];
163        $ln = 0;
164        if (file_exists($this->filename)) {
165            $fh = @fopen($this->filename, 'r');
166            if (!$fh) {
167                throw new IndexAccessException("Failed to read $this->filename");
168            }
169            while (($line = fgets($fh)) !== false) {
170                $line = trim($line);
171                if (preg_match($re, $line)) {
172                    $result[$ln] = $line;
173                }
174                $ln++;
175            }
176            fclose($fh);
177        }
178        return $result;
179    }
180
181    /**
182     * Cached mechanism to retrieve a single value
183     *
184     * @param string $value
185     * @return int the RID of the entry
186     * @see getRowID()
187     */
188    public function accessCachedValue(string $value): int
189    {
190        if (isset($this->ridCache[$value])) {
191            return $this->ridCache[$value];
192        }
193
194        // limit cache to 10 entries by discarding the oldest element
195        // as in DokuWiki usually only the most recently
196        // added item will be requested again
197        if (count($this->ridCache) > 10) {
198            array_shift($this->ridCache);
199        }
200        $this->ridCache[$value] = $this->getRowID($value);
201        return $this->ridCache[$value];
202    }
203
204    /** @inheritdoc */
205    public function count(): int
206    {
207        if (!file_exists($this->filename)) return 0;
208        $fh = @fopen($this->filename, 'r');
209        if (!$fh) return 0;
210        $count = 0;
211        while (fgets($fh) !== false) $count++;
212        fclose($fh);
213        return $count;
214    }
215
216    /** @inheritdoc */
217    public function getIterator(): \Generator
218    {
219        if (!file_exists($this->filename)) return;
220        $fh = @fopen($this->filename, 'r');
221        if (!$fh) return;
222        $ln = 0;
223        while (($line = fgets($fh)) !== false) {
224            yield $ln++ => rtrim($line);
225        }
226        fclose($fh);
227    }
228}
229