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