1<?php 2 3namespace dokuwiki\Search\Index; 4 5use dokuwiki\Search\Exception\IndexAccessException; 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 on a line-by-line basis. It is usually not read in full. 13 * All modifications are implicitly saved 14 * Should be used for large indexes that receive only few changes at once. 15 */ 16class FileIndex extends AbstractIndex 17{ 18 /** @var array RID cache for faster access */ 19 protected array $ridCache = []; 20 21 /** 22 * @inheritdoc 23 * @throws IndexWriteException 24 * @throws IndexLockException 25 * @author Tom N Harris <tnharris@whoopdedo.org> 26 */ 27 public function changeRow(int $rid, string $value): void 28 { 29 global $conf; 30 31 if (!$this->isWritable) throw new IndexLockException(); 32 33 if (!str_ends_with($value, "\n")) { 34 $value .= "\n"; 35 } 36 37 $tempname = $this->filename . '.tmp'; 38 $fh = @fopen($tempname, 'w'); 39 if (!$fh) { 40 throw new IndexWriteException("Failed to write $tempname"); 41 } 42 $ih = @fopen($this->filename, 'r'); 43 44 $ln = -1; // line counter 45 // copy previous index lines line-by-line, replacing the wanted line 46 if ($ih) { 47 while (($curline = fgets($ih)) !== false) { 48 fwrite($fh, (++$ln == $rid) ? $value : $curline); 49 } 50 fclose($ih); 51 } 52 // if wanted line is beyond the current line count, insert empty lines inbetween 53 if ($rid > $ln) { 54 while ($rid > ++$ln) { 55 fwrite($fh, "\n"); 56 } 57 fwrite($fh, $value); 58 } 59 fclose($fh); 60 61 if ($conf['fperm']) { 62 chmod($tempname, $conf['fperm']); 63 } 64 io_rename($tempname, $this->filename); 65 } 66 67 /** @inheritdoc */ 68 public function retrieveRow(int $rid): string 69 { 70 if (!file_exists($this->filename)) { 71 return ''; 72 } 73 $fh = @fopen($this->filename, 'r'); 74 if (!$fh) { 75 return ''; 76 } 77 $ln = -1; 78 while (($line = fgets($fh)) !== false) { 79 if (++$ln == $rid) { 80 fclose($fh); 81 return rtrim($line); 82 } 83 } 84 fclose($fh); 85 86 return ''; 87 } 88 89 /** @inheritdoc */ 90 public function retrieveRows(array $rids): array 91 { 92 $result = []; 93 sort($rids); 94 $next = array_shift($rids); 95 if ($next === null) return $result; 96 97 if (!file_exists($this->filename)) { 98 return $result; 99 } 100 $fh = @fopen($this->filename, 'r'); 101 if (!$fh) { 102 return $result; 103 } 104 $ln = -1; 105 while (($line = fgets($fh)) !== false) { 106 if (++$ln === $next) { 107 $result[$ln] = rtrim($line); 108 $next = array_shift($rids); 109 if ($next === null) break; 110 } 111 } 112 fclose($fh); 113 return $result; 114 } 115 116 117 /** 118 * @inheritdoc 119 * @throws IndexAccessException 120 * @throws IndexWriteException 121 */ 122 public function getRowIDs(array $values): array 123 { 124 $values = array_map(trim(...), $values); 125 $values = array_fill_keys($values, 1); // easier access as associative array 126 127 // search for the values 128 $result = []; 129 $ln = 0; 130 if (file_exists($this->filename)) { 131 $fh = @fopen($this->filename, 'r'); 132 if (!$fh) { 133 throw new IndexAccessException("Failed to read $this->filename"); 134 } 135 while (($line = fgets($fh)) !== false && $values) { 136 $line = trim($line); 137 if (isset($values[$line])) { 138 $result[$line] = $ln; 139 unset($values[$line]); 140 } 141 $ln++; 142 } 143 fclose($fh); 144 } 145 146 if (!$this->isWritable) return $result; 147 148 // if there are still values, they have not been found and will be appended 149 foreach (array_keys($values) as $value) { 150 if (!file_put_contents($this->filename, "$value\n", FILE_APPEND)) { 151 throw new IndexWriteException("Failed to write $this->filename"); 152 } 153 $result[$value] = $ln++; 154 } 155 156 return $result; 157 } 158 159 /** @inheritdoc */ 160 public function search(string $re): array 161 { 162 $result = []; 163 $ln = 0; 164 if (file_exists($this->filename)) { 165 $fh = @fopen($this->filename, 'r'); 166 if (!$fh) { 167 throw new IndexAccessException("Failed to read $this->filename"); 168 } 169 while (($line = fgets($fh)) !== false) { 170 $line = trim($line); 171 if (preg_match($re, $line)) { 172 $result[$ln] = $line; 173 } 174 $ln++; 175 } 176 fclose($fh); 177 } 178 return $result; 179 } 180 181 /** 182 * Cached mechanism to retrieve a single value 183 * 184 * @param string $value 185 * @return int the RID of the entry 186 * @see getRowID() 187 */ 188 public function accessCachedValue(string $value): int 189 { 190 if (isset($this->ridCache[$value])) { 191 return $this->ridCache[$value]; 192 } 193 194 // limit cache to 10 entries by discarding the oldest element 195 // as in DokuWiki usually only the most recently 196 // added item will be requested again 197 if (count($this->ridCache) > 10) { 198 array_shift($this->ridCache); 199 } 200 $this->ridCache[$value] = $this->getRowID($value); 201 return $this->ridCache[$value]; 202 } 203 204 /** @inheritdoc */ 205 public function count(): int 206 { 207 if (!file_exists($this->filename)) return 0; 208 $fh = @fopen($this->filename, 'r'); 209 if (!$fh) return 0; 210 $count = 0; 211 while (fgets($fh) !== false) $count++; 212 fclose($fh); 213 return $count; 214 } 215 216 /** @inheritdoc */ 217 public function getIterator(): \Generator 218 { 219 if (!file_exists($this->filename)) return; 220 $fh = @fopen($this->filename, 'r'); 221 if (!$fh) return; 222 $ln = 0; 223 while (($line = fgets($fh)) !== false) { 224 yield $ln++ => rtrim($line); 225 } 226 fclose($fh); 227 } 228} 229