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 static $ridCache = []; 20 21 /** 22 * @inheritdoc 23 * @throws IndexWriteException 24 * @throws IndexLockException 25 * @author Tom N Harris <tnharris@whoopdedo.org> 26 */ 27 public function changeRow($rid, $value) 28 { 29 global $conf; 30 31 if (!$this->isWritable) throw new IndexLockException(); 32 33 if (substr($value, -1) !== "\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 /** 68 * @inheritdoc 69 * @throws IndexWriteException 70 * @author Tom N Harris <tnharris@whoopdedo.org> 71 */ 72 public function retrieveRow($rid) 73 { 74 if (!file_exists($this->filename)) { 75 return ''; 76 } 77 $fh = @fopen($this->filename, 'r'); 78 if (!$fh) { 79 return ''; 80 } 81 $ln = -1; 82 while (($line = fgets($fh)) !== false) { 83 if (++$ln == $rid) { 84 fclose($fh); 85 return rtrim((string)$line); 86 } 87 } 88 fclose($fh); 89 90 if(!$this->isWritable) return ''; 91 92 // still here? pad the index for the given ID 93 // we do not simply call changeRow() here because appending is faster than line-by-line copying 94 if (!file_put_contents($this->filename, join("\n", array_fill(0, $rid - $ln + 1, '')), FILE_APPEND)) { 95 throw new IndexWriteException("Failed to write {$this->filename}"); 96 } 97 98 return ''; 99 } 100 101 /** @inheritdoc */ 102 public function retrieveRows($rids) 103 { 104 $result = []; 105 sort($rids); 106 $next = array_shift($rids); 107 108 if (!file_exists($this->filename)) { 109 return $result; 110 } 111 $fh = @fopen($this->filename, 'r'); 112 if (!$fh) { 113 return $result; 114 } 115 $ln = -1; 116 while (($line = fgets($fh)) !== false) { 117 if (++$ln === $next) { 118 $result[$ln] = rtrim((string)$line); 119 $next = array_shift($rids); 120 if ($next === false) break; 121 } 122 } 123 fclose($fh); 124 return $result; 125 } 126 127 128 /** 129 * @inheritdoc 130 * @throws IndexAccessException 131 * @throws IndexWriteException 132 */ 133 public function getRowIDs($values) 134 { 135 $values = array_map('trim', $values); 136 $values = array_fill_keys($values, 1); // easier access as associative array 137 138 // search for the values 139 $result = []; 140 $ln = 0; 141 if (file_exists($this->filename)) { 142 $fh = @fopen($this->filename, 'r'); 143 if (!$fh) { 144 throw new IndexAccessException("Failed to read {$this->filename}"); 145 } 146 while (($line = fgets($fh)) !== false && $values) { 147 $line = trim($line); 148 if (isset($values[$line])) { 149 $result[$line] = $ln; 150 unset($values[$line]); 151 } 152 $ln++; 153 } 154 fclose($fh); 155 } 156 157 if(!$this->isWritable) return $result; 158 159 // if there are still values, they have not been found and will be appended 160 foreach (array_keys($values) as $value) { 161 if (!file_put_contents($this->filename, "$value\n", FILE_APPEND)) { 162 throw new IndexWriteException("Failed to write {$this->filename}"); 163 } 164 $result[$value] = $ln++; 165 } 166 167 return $result; 168 } 169 170 /** @inheritdoc */ 171 public function search($re) 172 { 173 $result = []; 174 $ln = 0; 175 if (file_exists($this->filename)) { 176 $fh = @fopen($this->filename, 'r'); 177 if (!$fh) { 178 throw new IndexAccessException("Failed to read {$this->filename}"); 179 } 180 while (($line = fgets($fh)) !== false) { 181 $line = trim($line); 182 if (preg_match($re, $line)) { 183 $result[$ln] = $line; 184 } 185 $ln++; 186 } 187 fclose($fh); 188 } 189 return $result; 190 } 191 192 /** 193 * Cached version of accessCachedValue() 194 * 195 * @param string $value 196 * @return int the RID of the entry 197 * @throws IndexAccessException 198 * @throws IndexWriteException 199 */ 200 public function accessCachedValue($value) 201 { 202 if (isset(static::$ridCache['value'])) { 203 return static::$ridCache['value']; 204 } 205 206 // limit cache to 10 entries by discarding the oldest element 207 // as in DokuWiki usually only the most recently 208 // added item will be requested again 209 if (count(static::$ridCache) > 10) { 210 array_shift(static::$ridCache); 211 } 212 static::$ridCache[$value] = $this->getRowID($value); 213 return static::$ridCache[$value]; 214 } 215} 216