xref: /dokuwiki/inc/Search/Index/MemoryIndex.php (revision db8be586414d0dc05ca5131baddfa84f08c55520)
19bd7d62fSAndreas Gohr<?php
29bd7d62fSAndreas Gohr
39bd7d62fSAndreas Gohrnamespace dokuwiki\Search\Index;
49bd7d62fSAndreas Gohr
5*db8be586SAndreas Gohruse dokuwiki\Logger;
67fcedc39SAndreas Gohruse dokuwiki\Search\Exception\IndexLockException;
79bd7d62fSAndreas Gohruse dokuwiki\Search\Exception\IndexWriteException;
89bd7d62fSAndreas Gohr
99bd7d62fSAndreas Gohr/**
109bd7d62fSAndreas Gohr * Access to a single index file
119bd7d62fSAndreas Gohr *
129bd7d62fSAndreas Gohr * Access using this class always happens by loading the full index into memory.
13*db8be586SAndreas Gohr * Changes can be made permanent explicitly via save(), but will also be
14*db8be586SAndreas Gohr * auto-saved on destruction to prevent data loss when indexes are used in tandem
15*db8be586SAndreas Gohr * (a new RID in one index may already be referenced by another).
169bd7d62fSAndreas Gohr * Should be used for small indexes that receive many changes at once.
179bd7d62fSAndreas Gohr */
189bd7d62fSAndreas Gohrclass MemoryIndex extends AbstractIndex
199bd7d62fSAndreas Gohr{
209bd7d62fSAndreas Gohr    /** @var string the raw data lines of the index, no newlines */
219bd7d62fSAndreas Gohr    protected $data;
229bd7d62fSAndreas Gohr
23b3cb0bc3SAndreas Gohr    /** @var bool has the index been modified? */
24b3cb0bc3SAndreas Gohr    protected $dirty = false;
25b3cb0bc3SAndreas Gohr
269bd7d62fSAndreas Gohr    /**
279bd7d62fSAndreas Gohr     * Loads the full contents of the index into memory
289bd7d62fSAndreas Gohr     *
299bd7d62fSAndreas Gohr     * @inheritdoc
309bd7d62fSAndreas Gohr     */
317fcedc39SAndreas Gohr    public function __construct($idx, $suffix = '', $isWritable = false)
329bd7d62fSAndreas Gohr    {
337fcedc39SAndreas Gohr        parent::__construct($idx, $suffix, $isWritable);
349bd7d62fSAndreas Gohr
359bd7d62fSAndreas Gohr        $this->data = [];
36b3cb0bc3SAndreas Gohr        if (!file_exists($this->filename)) {
37b3cb0bc3SAndreas Gohr            return;
38b3cb0bc3SAndreas Gohr        }
399bd7d62fSAndreas Gohr        $this->data = file($this->filename, FILE_IGNORE_NEW_LINES);
409bd7d62fSAndreas Gohr    }
419bd7d62fSAndreas Gohr
427fcedc39SAndreas Gohr    /**
43*db8be586SAndreas Gohr     * Auto-save dirty data before releasing the lock
44*db8be586SAndreas Gohr     *
45*db8be586SAndreas Gohr     * When indexes are used in tandem, a new RID written to one index may already
46*db8be586SAndreas Gohr     * be referenced by other indexes that were saved. Losing unsaved data here
47*db8be586SAndreas Gohr     * would leave dangling references, causing silent index corruption.
48*db8be586SAndreas Gohr     *
49*db8be586SAndreas Gohr     * The try/catch is necessary because unlock() is called from __destruct()
50*db8be586SAndreas Gohr     * (in the parent class), and PHP destructors must not throw — a throw
51*db8be586SAndreas Gohr     * during exception unwinding causes a fatal error.
52c66b5ec6SAndreas Gohr     *
53c66b5ec6SAndreas Gohr     * @inheritdoc
547fcedc39SAndreas Gohr     */
55c66b5ec6SAndreas Gohr    public function unlock()
567fcedc39SAndreas Gohr    {
577fcedc39SAndreas Gohr        if ($this->isDirty()) {
58*db8be586SAndreas Gohr            try {
59*db8be586SAndreas Gohr                $this->save();
60*db8be586SAndreas Gohr            } catch (\Exception $e) {
61*db8be586SAndreas Gohr                Logger::error('MemoryIndex failed to save on unlock: ' . $e->getMessage());
62*db8be586SAndreas Gohr            }
637fcedc39SAndreas Gohr        }
64c66b5ec6SAndreas Gohr        parent::unlock();
657fcedc39SAndreas Gohr    }
667fcedc39SAndreas Gohr
677fcedc39SAndreas Gohr    /**
687fcedc39SAndreas Gohr     * @inheritdoc
697fcedc39SAndreas Gohr     * @throws IndexLockException
707fcedc39SAndreas Gohr     */
719bd7d62fSAndreas Gohr    public function changeRow($rid, $value)
729bd7d62fSAndreas Gohr    {
737fcedc39SAndreas Gohr        if (!$this->isWritable) throw new IndexLockException();
747fcedc39SAndreas Gohr
759bd7d62fSAndreas Gohr        if ($rid > count($this->data)) {
769bd7d62fSAndreas Gohr            $this->data = array_pad($this->data, $rid, '');
779bd7d62fSAndreas Gohr        }
789bd7d62fSAndreas Gohr        $this->data[$rid] = $value;
79b3cb0bc3SAndreas Gohr        $this->dirty = true;
809bd7d62fSAndreas Gohr    }
819bd7d62fSAndreas Gohr
827fcedc39SAndreas Gohr    /**
837fcedc39SAndreas Gohr     * @inheritdoc
847fcedc39SAndreas Gohr     * @throws IndexLockException
857fcedc39SAndreas Gohr     */
869bd7d62fSAndreas Gohr    public function retrieveRow($rid)
879bd7d62fSAndreas Gohr    {
88b3cb0bc3SAndreas Gohr        if (isset($this->data[$rid])) {
89b3cb0bc3SAndreas Gohr            return $this->data[$rid];
90b3cb0bc3SAndreas Gohr        }
917fcedc39SAndreas Gohr        if ($this->isWritable) {
92dec26820SAndreas Gohr            $this->changeRow($rid, ''); // add to index
937fcedc39SAndreas Gohr        }
949bd7d62fSAndreas Gohr        return '';
959bd7d62fSAndreas Gohr    }
969bd7d62fSAndreas Gohr
97d6396b6dSAndreas Gohr    /** @inheritdoc */
989f63f003SAndreas Gohr    public function retrieveRows($rids)
999f63f003SAndreas Gohr    {
1009f63f003SAndreas Gohr        $result = [];
1019f63f003SAndreas Gohr        foreach ($rids as $rid) {
1029f63f003SAndreas Gohr            if (isset($this->data[$rid])) $result[$rid] = $this->data[$rid];
1039f63f003SAndreas Gohr        }
1049f63f003SAndreas Gohr
1059f63f003SAndreas Gohr        return $result;
1069f63f003SAndreas Gohr    }
1079f63f003SAndreas Gohr
1089f63f003SAndreas Gohr    /** @inheritdoc */
1098ed35011SAndreas Gohr    public function getRowIDs($values)
110d6396b6dSAndreas Gohr    {
111d6396b6dSAndreas Gohr        $values = array_map('trim', $values);
112d6396b6dSAndreas Gohr        $values = array_fill_keys($values, 1); // easier access as associative array
113d6396b6dSAndreas Gohr
114d6396b6dSAndreas Gohr        $result = [];
115d6396b6dSAndreas Gohr        $count = count($this->data);
116d6396b6dSAndreas Gohr        for ($ln = 0; $ln < $count; $ln++) {
117d6396b6dSAndreas Gohr            $line = $this->data[$ln];
118d6396b6dSAndreas Gohr            if (isset($values[$line])) {
119d6396b6dSAndreas Gohr                $result[$line] = $ln;
120d6396b6dSAndreas Gohr                unset($values[$line]);
121d6396b6dSAndreas Gohr            }
122d6396b6dSAndreas Gohr        }
123d6396b6dSAndreas Gohr
1247fcedc39SAndreas Gohr        if (!$this->isWritable) return $result;
1257fcedc39SAndreas Gohr
126d6396b6dSAndreas Gohr        // if there are still values, they have not been found and will be appended
127d6396b6dSAndreas Gohr        foreach (array_keys($values) as $value) {
128d6396b6dSAndreas Gohr            $this->data[] = $value;
129d6396b6dSAndreas Gohr            $result[$value] = $ln++;
130b3cb0bc3SAndreas Gohr            $this->dirty = true;
131d6396b6dSAndreas Gohr        }
132d6396b6dSAndreas Gohr
133d6396b6dSAndreas Gohr        return $result;
134d6396b6dSAndreas Gohr    }
135d6396b6dSAndreas Gohr
13603a35633SAndreas Gohr    /** @inheritdoc */
13703a35633SAndreas Gohr    public function search($re)
13803a35633SAndreas Gohr    {
13903a35633SAndreas Gohr        return preg_grep($re, $this->data);
14003a35633SAndreas Gohr    }
14103a35633SAndreas Gohr
1429bd7d62fSAndreas Gohr    /**
1439bd7d62fSAndreas Gohr     * Save the changed index back to its file
1449bd7d62fSAndreas Gohr     *
145b3cb0bc3SAndreas Gohr     * The method will check the internal dirty state and will only write when the index has actually been changed
146b3cb0bc3SAndreas Gohr     *
1479bd7d62fSAndreas Gohr     * @throws IndexWriteException
1487fcedc39SAndreas Gohr     * @throws IndexLockException
1499bd7d62fSAndreas Gohr     */
1509bd7d62fSAndreas Gohr    public function save()
1519bd7d62fSAndreas Gohr    {
1529bd7d62fSAndreas Gohr        global $conf;
1539bd7d62fSAndreas Gohr
154b3cb0bc3SAndreas Gohr        if (!$this->isDirty()) {
155b3cb0bc3SAndreas Gohr            return;
156b3cb0bc3SAndreas Gohr        }
157b3cb0bc3SAndreas Gohr
1587fcedc39SAndreas Gohr        if (!$this->isWritable) throw new IndexLockException();
1597fcedc39SAndreas Gohr
1609bd7d62fSAndreas Gohr        $tempname = $this->filename . '.tmp';
1619bd7d62fSAndreas Gohr
1629bd7d62fSAndreas Gohr        $fh = @fopen($tempname, 'w');
1639bd7d62fSAndreas Gohr        if (!$fh) {
1649bd7d62fSAndreas Gohr            throw new IndexWriteException("Failed to write $tempname");
1659bd7d62fSAndreas Gohr        }
1669bd7d62fSAndreas Gohr        fwrite($fh, implode("\n", $this->data));
167dec26820SAndreas Gohr        if (count($this->data)) {
1689bd7d62fSAndreas Gohr            fwrite($fh, "\n");
1699bd7d62fSAndreas Gohr        }
1709bd7d62fSAndreas Gohr        fclose($fh);
1719bd7d62fSAndreas Gohr
1729bd7d62fSAndreas Gohr        if ($conf['fperm']) {
1739bd7d62fSAndreas Gohr            chmod($tempname, $conf['fperm']);
1749bd7d62fSAndreas Gohr        }
1759bd7d62fSAndreas Gohr
1769bd7d62fSAndreas Gohr        if (!io_rename($tempname, $this->filename)) {
1779bd7d62fSAndreas Gohr            throw new IndexWriteException("Failed to write {$this->filename}");
1789bd7d62fSAndreas Gohr        }
179b3cb0bc3SAndreas Gohr
180b3cb0bc3SAndreas Gohr        $this->dirty = false;
1819bd7d62fSAndreas Gohr    }
1829bd7d62fSAndreas Gohr
183b3cb0bc3SAndreas Gohr    /**
184b3cb0bc3SAndreas Gohr     * Check if the index has been modified and needs to be saved
185b3cb0bc3SAndreas Gohr     * @return bool
186b3cb0bc3SAndreas Gohr     */
187b3cb0bc3SAndreas Gohr    public function isDirty()
188b3cb0bc3SAndreas Gohr    {
189b3cb0bc3SAndreas Gohr        return $this->dirty;
190b3cb0bc3SAndreas Gohr    }
19183b3acccSAndreas Gohr
19283b3acccSAndreas Gohr    /** @inheritdoc */
19321fbd01bSAndreas Gohr    public function count(): int
19421fbd01bSAndreas Gohr    {
19521fbd01bSAndreas Gohr        return count($this->data);
19621fbd01bSAndreas Gohr    }
19721fbd01bSAndreas Gohr
19821fbd01bSAndreas Gohr    /** @inheritdoc */
19983b3acccSAndreas Gohr    public function getIterator(): \ArrayIterator
20083b3acccSAndreas Gohr    {
20183b3acccSAndreas Gohr        return new \ArrayIterator($this->data);
20283b3acccSAndreas Gohr    }
2039bd7d62fSAndreas Gohr}
204