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