xref: /dokuwiki/inc/Search/Index/FileIndex.php (revision 03a35633c932eca1b7b0d373d08b9140884a0ebe)
1<?php
2
3namespace dokuwiki\Search\Index;
4
5use dokuwiki\Search\Exception\IndexAccessException;
6use dokuwiki\Search\Exception\IndexWriteException;
7
8/**
9 * Access to a single index file
10 *
11 * Access using this class always happens on a line-by-line basis. It is usually not read in full.
12 * All modifications are implicitly saved
13 * Should be used for large indexes that receive only few changes at once.
14 */
15class FileIndex extends AbstractIndex
16{
17    /** @var array RID cache for faster access */
18    protected static $ridCache = [];
19
20    /**
21     * @inheritdoc
22     * @throws IndexWriteException
23     * @author Tom N Harris <tnharris@whoopdedo.org>
24     */
25    public function changeRow($rid, $value)
26    {
27        global $conf;
28
29        if (substr($value, -1) !== "\n") {
30            $value .= "\n";
31        }
32
33        $tempname = $this->filename . '.tmp';
34        $fh = @fopen($tempname, 'w');
35        if (!$fh) {
36            throw new IndexWriteException("Failed to write {$tempname}");
37        }
38        $ih = @fopen($this->filename, 'r');
39
40        $ln = -1; // line counter
41        // copy previous index lines line-by-line, replacing the wanted line
42        if ($ih) {
43            while (($curline = fgets($ih)) !== false) {
44                fwrite($fh, (++$ln == $rid) ? $value : $curline);
45            }
46            fclose($ih);
47        }
48        // if wanted line is beyond the current line count, insert empty lines inbetween
49        if ($rid > $ln) {
50            while ($rid > ++$ln) {
51                fwrite($fh, "\n");
52            }
53            fwrite($fh, $value);
54        }
55        fclose($fh);
56
57        if ($conf['fperm']) {
58            chmod($tempname, $conf['fperm']);
59        }
60        io_rename($tempname, $this->filename);
61    }
62
63    /**
64     * @inheritdoc
65     * @author Tom N Harris <tnharris@whoopdedo.org>
66     */
67    public function retrieveRow($rid)
68    {
69        if (!file_exists($this->filename)) {
70            return '';
71        }
72        $fh = @fopen($this->filename, 'r');
73        if (!$fh) {
74            return '';
75        }
76        $ln = -1;
77        while (($line = fgets($fh)) !== false) {
78            if (++$ln == $rid) {
79                fclose($fh);
80                return rtrim((string) $line);
81            }
82        }
83        fclose($fh);
84
85        // still here? pad the index for the given ID
86        // we do not simply call changeRow() here because appending is faster than line-by-line copying
87        if (!file_put_contents($this->filename, join("\n", array_fill(0, $rid - $ln + 1, '')), FILE_APPEND)) {
88            throw new IndexWriteException("Failed to write {$this->filename}");
89        }
90
91        return '';
92    }
93
94    /**
95     * @inheritdoc
96     * @throws IndexAccessException
97     */
98    public function getRowIDs($values)
99    {
100        $values = array_map('trim', $values);
101        $values = array_fill_keys($values, 1); // easier access as associative array
102
103        // search for the values
104        $result = [];
105        $ln = 0;
106        if (file_exists($this->filename)) {
107            $fh = @fopen($this->filename, 'r');
108            if (!$fh) {
109                throw new IndexAccessException("Failed to read {$this->filename}");
110            }
111            while (($line = fgets($fh)) !== false && $values) {
112                $line = trim($line);
113                if (isset($values[$line])) {
114                    $result[$line] = $ln;
115                    unset($values[$line]);
116                }
117                $ln++;
118            }
119            fclose($fh);
120        }
121
122        // if there are still values, they have not been found and will be appended
123        foreach (array_keys($values) as $value) {
124            file_put_contents($this->filename, "$value\n", FILE_APPEND);
125            $result[$value] = $ln++;
126        }
127
128        return $result;
129    }
130
131    /** @inheritdoc */
132    public function search($re)
133    {
134        $result = [];
135        $ln = 0;
136        if (file_exists($this->filename)) {
137            $fh = @fopen($this->filename, 'r');
138            if (!$fh) {
139                throw new IndexAccessException("Failed to read {$this->filename}");
140            }
141            while (($line = fgets($fh)) !== false) {
142                $line = trim($line);
143                if (preg_match($re, $line)) {
144                    $result[$ln] = $line;
145                }
146                $ln++;
147            }
148            fclose($fh);
149        }
150        return $result;
151    }
152
153    /**
154     * Cached version of accessCachedValue()
155     *
156     * @param string $value
157     * @return int the RID of the entry
158     * @throws IndexAccessException
159     * @throws IndexWriteException
160     */
161    public function accessCachedValue($value)
162    {
163        if (isset(static::$ridCache['value'])) {
164            return static::$ridCache['value'];
165        }
166
167        // limit cache to 10 entries by discarding the oldest element
168        // as in DokuWiki usually only the most recently
169        // added item will be requested again
170        if (count(static::$ridCache) > 10) {
171            array_shift(static::$ridCache);
172        }
173        static::$ridCache[$value] = $this->getRowID($value);
174        return static::$ridCache[$value];
175    }
176}
177