1<?php 2 3namespace dokuwiki\Search\Index; 4 5use dokuwiki\Logger; 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 by loading the full index into memory. 13 * Changes can be made permanent explicitly via save(), but will also be 14 * auto-saved on destruction to prevent data loss when indexes are used in tandem 15 * (a new RID in one index may already be referenced by another). 16 * Should be used for small indexes that receive many changes at once. 17 */ 18class MemoryIndex extends AbstractIndex 19{ 20 /** @var string[] the raw data lines of the index, no newlines */ 21 protected array $data = []; 22 23 /** @var bool has the index been modified? */ 24 protected bool $dirty = false; 25 26 /** 27 * Loads the full contents of the index into memory 28 * 29 * @inheritdoc 30 */ 31 public function __construct($idx, $suffix = '', $isWritable = false) 32 { 33 parent::__construct($idx, $suffix, $isWritable); 34 if (!file_exists($this->filename)) { 35 return; 36 } 37 $this->data = file($this->filename, FILE_IGNORE_NEW_LINES); 38 } 39 40 /** 41 * Auto-save dirty data before releasing the lock 42 * 43 * When indexes are used in tandem, a new RID written to one index may already 44 * be referenced by other indexes that were saved. Losing unsaved data here 45 * would leave dangling references, causing silent index corruption. 46 * 47 * The try/catch is necessary because unlock() is called from __destruct() 48 * (in the parent class), and PHP destructors must not throw — a throw 49 * during exception unwinding causes a fatal error. 50 * 51 * @inheritdoc 52 */ 53 public function unlock(): void 54 { 55 if ($this->isDirty()) { 56 try { 57 $this->save(); 58 } catch (\Exception $e) { 59 Logger::error('MemoryIndex failed to save on unlock: ' . $e->getMessage()); 60 } 61 } 62 parent::unlock(); 63 } 64 65 /** 66 * @inheritdoc 67 * @throws IndexLockException 68 */ 69 public function changeRow(int $rid, string $value): void 70 { 71 if (!$this->isWritable) throw new IndexLockException(); 72 73 if ($rid > count($this->data)) { 74 $this->data = array_pad($this->data, $rid, ''); 75 } 76 $this->data[$rid] = $value; 77 $this->dirty = true; 78 } 79 80 /** 81 * @inheritdoc 82 * @throws IndexLockException 83 */ 84 public function retrieveRow(int $rid): string 85 { 86 if (isset($this->data[$rid])) { 87 return $this->data[$rid]; 88 } 89 if ($this->isWritable) { 90 $this->changeRow($rid, ''); // add to index 91 } 92 return ''; 93 } 94 95 /** @inheritdoc */ 96 public function retrieveRows(array $rids): array 97 { 98 $result = []; 99 foreach ($rids as $rid) { 100 if (isset($this->data[$rid])) $result[$rid] = $this->data[$rid]; 101 } 102 103 return $result; 104 } 105 106 /** @inheritdoc */ 107 public function getRowIDs(array $values): array 108 { 109 $values = array_map(trim(...), $values); 110 $values = array_fill_keys($values, 1); // easier access as associative array 111 112 $result = []; 113 $count = count($this->data); 114 for ($ln = 0; $ln < $count; $ln++) { 115 $line = $this->data[$ln]; 116 if (isset($values[$line])) { 117 $result[$line] = $ln; 118 unset($values[$line]); 119 } 120 } 121 122 if (!$this->isWritable) return $result; 123 124 // if there are still values, they have not been found and will be appended 125 foreach (array_keys($values) as $value) { 126 $this->data[] = $value; 127 $result[$value] = $ln++; 128 $this->dirty = true; 129 } 130 131 return $result; 132 } 133 134 /** @inheritdoc */ 135 public function search(string $re): array 136 { 137 return preg_grep($re, $this->data); 138 } 139 140 /** 141 * Save the changed index back to its file 142 * 143 * The method will check the internal dirty state and will only write when the index has actually been changed 144 * 145 * @throws IndexWriteException 146 * @throws IndexLockException 147 */ 148 public function save(): void 149 { 150 global $conf; 151 152 if (!$this->isDirty()) { 153 return; 154 } 155 156 if (!$this->isWritable) throw new IndexLockException(); 157 158 $tempname = $this->filename . '.tmp'; 159 160 $fh = @fopen($tempname, 'w'); 161 if (!$fh) { 162 throw new IndexWriteException("Failed to write $tempname"); 163 } 164 fwrite($fh, implode("\n", $this->data)); 165 if ($this->data !== []) { 166 fwrite($fh, "\n"); 167 } 168 fclose($fh); 169 170 if ($conf['fperm']) { 171 chmod($tempname, $conf['fperm']); 172 } 173 174 if (!io_rename($tempname, $this->filename)) { 175 throw new IndexWriteException("Failed to write $this->filename"); 176 } 177 178 $this->dirty = false; 179 } 180 181 /** 182 * Check if the index has been modified and needs to be saved 183 * @return bool 184 */ 185 public function isDirty(): bool 186 { 187 return $this->dirty; 188 } 189 190 /** @inheritdoc */ 191 public function count(): int 192 { 193 return count($this->data); 194 } 195 196 /** @inheritdoc */ 197 public function getIterator(): \ArrayIterator 198 { 199 return new \ArrayIterator($this->data); 200 } 201} 202