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 /** @inheritdoc */ 81 public function retrieveRow(int $rid): string 82 { 83 return $this->data[$rid] ?? ''; 84 } 85 86 /** @inheritdoc */ 87 public function retrieveRows(array $rids): array 88 { 89 $result = []; 90 foreach ($rids as $rid) { 91 if (isset($this->data[$rid])) $result[$rid] = $this->data[$rid]; 92 } 93 94 return $result; 95 } 96 97 /** @inheritdoc */ 98 public function getRowIDs(array $values): array 99 { 100 $values = array_map(trim(...), $values); 101 $values = array_fill_keys($values, 1); // easier access as associative array 102 103 $result = []; 104 $count = count($this->data); 105 for ($ln = 0; $ln < $count; $ln++) { 106 $line = $this->data[$ln]; 107 if (isset($values[$line])) { 108 $result[$line] = $ln; 109 unset($values[$line]); 110 } 111 } 112 113 if (!$this->isWritable) return $result; 114 115 // if there are still values, they have not been found and will be appended 116 foreach (array_keys($values) as $value) { 117 $this->data[] = $value; 118 $result[$value] = $ln++; 119 $this->dirty = true; 120 } 121 122 return $result; 123 } 124 125 /** @inheritdoc */ 126 public function search(string $re): array 127 { 128 return preg_grep($re, $this->data); 129 } 130 131 /** 132 * Save the changed index back to its file 133 * 134 * The method will check the internal dirty state and will only write when the index has actually been changed 135 * 136 * @throws IndexWriteException 137 * @throws IndexLockException 138 */ 139 public function save(): void 140 { 141 global $conf; 142 143 if (!$this->isDirty()) { 144 return; 145 } 146 147 if (!$this->isWritable) throw new IndexLockException(); 148 149 $tempname = $this->filename . '.tmp'; 150 151 $fh = @fopen($tempname, 'w'); 152 if (!$fh) { 153 throw new IndexWriteException("Failed to write $tempname"); 154 } 155 fwrite($fh, implode("\n", $this->data)); 156 if ($this->data !== []) { 157 fwrite($fh, "\n"); 158 } 159 fclose($fh); 160 161 if ($conf['fperm']) { 162 chmod($tempname, $conf['fperm']); 163 } 164 165 if (!io_rename($tempname, $this->filename)) { 166 throw new IndexWriteException("Failed to write $this->filename"); 167 } 168 169 $this->dirty = false; 170 } 171 172 /** 173 * Check if the index has been modified and needs to be saved 174 * @return bool 175 */ 176 public function isDirty(): bool 177 { 178 return $this->dirty; 179 } 180 181 /** @inheritdoc */ 182 public function count(): int 183 { 184 return count($this->data); 185 } 186 187 /** @inheritdoc */ 188 public function getIterator(): \ArrayIterator 189 { 190 return new \ArrayIterator($this->data); 191 } 192} 193