xref: /dokuwiki/inc/Search/Index/FileIndex.php (revision c66b5ec65fd5aa2f1037d2be542b49297f3aac0e)
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 $ridCache = [];
20
21    /**
22     * @inheritdoc
23     * @throws IndexWriteException
24     * @throws IndexLockException
25     * @author Tom N Harris <tnharris@whoopdedo.org>
26     */
27    public function changeRow($rid, $value)
28    {
29        global $conf;
30
31        if (!$this->isWritable) throw new IndexLockException();
32
33        if (substr($value, -1) !== "\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    /**
68     * @inheritdoc
69     * @throws IndexWriteException
70     * @author Tom N Harris <tnharris@whoopdedo.org>
71     */
72    public function retrieveRow($rid)
73    {
74        if (!file_exists($this->filename)) {
75            return '';
76        }
77        $fh = @fopen($this->filename, 'r');
78        if (!$fh) {
79            return '';
80        }
81        $ln = -1;
82        while (($line = fgets($fh)) !== false) {
83            if (++$ln == $rid) {
84                fclose($fh);
85                return rtrim((string)$line);
86            }
87        }
88        fclose($fh);
89
90        if (!$this->isWritable) return '';
91
92        // still here? pad the index for the given ID
93        // we do not simply call changeRow() here because appending is faster than line-by-line copying
94        if (!file_put_contents($this->filename, join("\n", array_fill(0, $rid - $ln + 1, '')), FILE_APPEND)) {
95            throw new IndexWriteException("Failed to write {$this->filename}");
96        }
97
98        return '';
99    }
100
101    /** @inheritdoc */
102    public function retrieveRows($rids)
103    {
104        $result = [];
105        sort($rids);
106        $next = array_shift($rids);
107
108        if (!file_exists($this->filename)) {
109            return $result;
110        }
111        $fh = @fopen($this->filename, 'r');
112        if (!$fh) {
113            return $result;
114        }
115        $ln = -1;
116        while (($line = fgets($fh)) !== false) {
117            if (++$ln === $next) {
118                $result[$ln] = rtrim((string)$line);
119                $next = array_shift($rids);
120                if ($next === false) break;
121            }
122        }
123        fclose($fh);
124        return $result;
125    }
126
127
128    /**
129     * @inheritdoc
130     * @throws IndexAccessException
131     * @throws IndexWriteException
132     */
133    public function getRowIDs($values)
134    {
135        $values = array_map('trim', $values);
136        $values = array_fill_keys($values, 1); // easier access as associative array
137
138        // search for the values
139        $result = [];
140        $ln = 0;
141        if (file_exists($this->filename)) {
142            $fh = @fopen($this->filename, 'r');
143            if (!$fh) {
144                throw new IndexAccessException("Failed to read {$this->filename}");
145            }
146            while (($line = fgets($fh)) !== false && $values) {
147                $line = trim($line);
148                if (isset($values[$line])) {
149                    $result[$line] = $ln;
150                    unset($values[$line]);
151                }
152                $ln++;
153            }
154            fclose($fh);
155        }
156
157        if (!$this->isWritable) return $result;
158
159        // if there are still values, they have not been found and will be appended
160        foreach (array_keys($values) as $value) {
161            if (!file_put_contents($this->filename, "$value\n", FILE_APPEND)) {
162                throw new IndexWriteException("Failed to write {$this->filename}");
163            }
164            $result[$value] = $ln++;
165        }
166
167        return $result;
168    }
169
170    /** @inheritdoc */
171    public function search($re)
172    {
173        $result = [];
174        $ln = 0;
175        if (file_exists($this->filename)) {
176            $fh = @fopen($this->filename, 'r');
177            if (!$fh) {
178                throw new IndexAccessException("Failed to read {$this->filename}");
179            }
180            while (($line = fgets($fh)) !== false) {
181                $line = trim($line);
182                if (preg_match($re, $line)) {
183                    $result[$ln] = $line;
184                }
185                $ln++;
186            }
187            fclose($fh);
188        }
189        return $result;
190    }
191
192    /**
193     * Cached mechanism to retrieve a single value
194     *
195     * @param string $value
196     * @return int the RID of the entry
197     * @throws IndexAccessException
198     * @throws IndexWriteException
199     * @see getRowID()
200     */
201    public function accessCachedValue($value)
202    {
203        if (isset($this->ridCache[$value])) {
204            return $this->ridCache[$value];
205        }
206
207        // limit cache to 10 entries by discarding the oldest element
208        // as in DokuWiki usually only the most recently
209        // added item will be requested again
210        if (count($this->ridCache) > 10) {
211            array_shift($this->ridCache);
212        }
213        $this->ridCache[$value] = $this->getRowID($value);
214        return $this->ridCache[$value];
215    }
216}
217