PluginProbe ʕ •ᴥ•ʔ
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization / trunk
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization vtrunk
1.19.8 1.19.7 1.19.6 1.19.5 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.12.0 1.13.0 1.14.0 1.15.0 1.15.1 1.15.2 1.15.3 1.16.0 1.16.1 1.16.2 1.16.3 1.16.4 1.16.5 1.16.6 1.16.7 1.16.8 1.17.0 1.17.6 1.17.7 1.17.8 1.17.9 1.18.0 1.18.1 1.18.2 1.18.3 1.18.4 1.18.5 1.18.6 1.18.7 1.18.8 1.18.9 1.19.0 1.19.1 1.19.2 1.19.3 1.19.4 1.3.19 1.3.20 1.4.0 1.4.1 1.5.0 1.5.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.16 1.5.17 1.5.18 1.5.19 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.8.0 1.8.1 1.8.3 1.9.0 1.9.1 1.9.2
nitropack / nitropack-sdk / NitroPack / SDK / StorageDriver / Redis.php
nitropack / nitropack-sdk / NitroPack / SDK / StorageDriver Last commit date
Disk.php 1 year ago DiskFileHandle.php 1 year ago Redis.php 1 year ago RedisFileHandle.php 1 year ago
Redis.php
460 lines
1 <?php
2 // Disclaimer mtimes are only accurate for the final entries. But mtimes for parent directories (more than 1 level up the hierarchy) might not be updated correctly when children have been modified
3 // This is also a bad design for emulating a file system performance wise. Only use this driver when you need shared storage between multiple servers. On a single server with an SSD using the Disk diver is a better idea.
4 namespace NitroPack\SDK\StorageDriver;
5
6 use \NitroPack\SDK\FileHandle;
7
8 class Redis {
9 const LOCK_TTL = 30000;
10 const LOCK_TIMEOUT = 30;
11 private $redis;
12
13 private function preparePathInput($path) {
14 return $path == DIRECTORY_SEPARATOR ? $path : rtrim($path, DIRECTORY_SEPARATOR);
15 }
16
17 public function __construct($host = "127.0.0.1", $port = 6379, $password = NULL, $db = NULL) {
18 $this->redis = new \Redis();
19 $this->redis->connect($host, $port);
20
21 if ($password !== NULL) {
22 $this->redis->auth($password);
23 }
24
25 if ($db !== NULL) {
26 $this->redis->select($db);
27 }
28 }
29
30 public function getOsPath($parts) {
31 return implode(DIRECTORY_SEPARATOR, $parts);
32 }
33
34 public function touch($path, $time = NULL) {
35 if ($time === NULL || !is_numeric($time)) $time = time();
36 $path = $this->preparePathInput($path);
37 $parent = dirname($path);
38 $key = basename($path);
39 if ($this->isDir($parent)) {
40 $this->redis->hSet($parent, "::mtime::" . $key, (int)$time);
41 $this->redis->hSetNx($parent, "::content::" . $key, "");
42 return true;
43 } else {
44 return false;
45 }
46 }
47
48 public function setContent($path, $content) {
49 $path = $this->preparePathInput($path);
50 if ($this->isDir($path)) {
51 return false;
52 } else {
53 try {
54 //TODO: Create parent dir if it doesn't exist. This can impact performance though. Maybe make it optional
55 $dir = dirname($path);
56 $file = basename($path);
57 $this->redis->hMSet($dir, array(
58 "::content::" . $file => $content,
59 "::mtime::" . $file => time()
60 ));
61 } catch (\Exception $e) {
62 return false;
63 }
64 return true;
65 }
66 }
67
68 public function createDir($dir) {
69 $dir = $this->preparePathInput($dir);
70 $now = time();
71 $childDir = NULL;
72 $numDirsCreated = 0;
73 try {
74 while ($childDir !== "" && !$this->exists($dir)) {
75 $this->redis->hSet($dir, "::self::ctime::", $now);
76 $numDirsCreated++;
77 if ($childDir) {
78 $this->touch($this->getOsPath(array($dir, $childDir)));
79 }
80 $childDir = basename($dir);
81 $dir = dirname($dir);
82 }
83 if ($numDirsCreated > 0 && $childDir) {
84 $this->touch($this->getOsPath(array($dir, $childDir)));
85 }
86 } catch (\Exception $e) {
87 return false;
88 }
89
90 return true;
91 }
92
93 public function deletePath($path) {
94 $path = $this->preparePathInput($path);
95 $dirKey = dirname($path);
96 $fileName = basename($path);
97
98 try {
99 $deleted = $this->redis->hDel($dirKey, "::content::" . $fileName, "::mtime::" . $fileName);
100 if ($deleted) {
101 $this->touch($dirKey);
102 }
103 } catch (\Exception $e) {
104 return false;
105 }
106
107 return true;
108 }
109
110 public function deleteFile($path) {
111 return !$this->isDir($path) && $this->deletePath($path);
112 }
113
114 public function deleteDir($dir) {
115 $dir = $this->preparePathInput($dir);
116 try {
117 if (!$this->isDir($dir)) return true;
118 $this->trunkDir($dir) && $this->redis->unlink($dir) && $this->deletePath($dir);
119 } catch (\Exception $e) {
120 return false;
121 }
122 return true;
123 }
124
125 public function trunkDir($dir) {
126 $dir = $this->preparePathInput($dir);
127 if (!$this->isDir($dir)) return false;
128
129 if ($dir == DIRECTORY_SEPARATOR) {
130 $osPath = DIRECTORY_SEPARATOR . "*";
131 } else {
132 $osPath = $this->getOsPath(array($dir, "*"));
133 }
134
135 $success = false;
136 try {
137 $this->redis->eval('
138 local cursor = "0";
139 repeat
140 local t = redis.call("SCAN", cursor, "MATCH", ARGV[1]);
141 cursor = t[1];
142 local list = t[2];
143 for i = 1, #list do
144 redis.call("UNLINK", list[i]);
145 end;
146 until cursor == "0";
147 ', array($osPath), 0);
148 $success = true;
149 } catch (\Exception $e) {
150 // TODO: Log an error
151 }
152 return $success;
153 }
154
155 public function isDirEmpty($dir) {
156 $dir = $this->preparePathInput($dir);
157 return (int)$this->redis->hLen($dir) <= 1;
158 }
159
160 private function isDir($dir) {
161 $dir = $this->preparePathInput($dir);
162 return !!$this->redis->hLen($dir); // if this is a non-empty sorted set then it is a dir
163 }
164
165 public function dirForeach($dir, $callback) {
166 $dir = $this->preparePathInput($dir);
167 if (!$this->isDir($dir)) return false;
168 $result = true;
169 $it = NULL;
170 $prevScanMode = $this->redis->getOption(\Redis::OPT_SCAN);
171 $this->redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
172 try {
173 while($entries = $this->redis->hScan($dir, $it, "::mtime::*")) {
174 foreach($entries as $entry => $mtime) {
175 $entry = substr($entry, 9);//remove the ::mtime:: prefix
176 $path = $dir != DIRECTORY_SEPARATOR ? $this->getOsPath(array($dir, $entry)) : $dir . $entry;
177 call_user_func($callback, $path);
178 }
179 }
180 } catch (\Exception $e) {
181 // TODO: Log an error
182 $result = false;
183 } finally {
184 $this->redis->setOption(\Redis::OPT_SCAN, $prevScanMode);
185 }
186 return $result;
187 }
188
189 public function mtime($path) {
190 $path = $this->preparePathInput($path);
191 $dir = dirname($path);
192 $file = basename($path);
193 return $this->redis->hGet($dir, "::mtime::" . $file);
194 }
195
196 public function exists($path) {
197 $path = $this->preparePathInput($path);
198 $dir = dirname($path);
199 $file = basename($path);
200 return $this->redis->hExists($dir, "::mtime::" . $file);
201 }
202
203 public function getContent($path) {
204 $path = $this->preparePathInput($path);
205 if ($this->isDir($path)) {
206 return false;
207 } else {
208 $dir = dirname($path);
209 $file = basename($path);
210 return $this->redis->hGet($dir, "::content::" . $file);
211 }
212 }
213
214 public function rename($oldKey, $newKey, $innerCall = false) {
215 $oldKey = $this->preparePathInput($oldKey);
216 $newKey = $this->preparePathInput($newKey);
217 if ($this->exists($newKey)) return false;
218
219 $success = false;
220
221 try {
222 $isDir = $this->isDir($oldKey);
223 if (!$isDir) {
224 $content = $this->getContent($oldKey);
225 $this->deleteFile($oldKey);
226 $this->setContent($newKey, $content);
227 } else {
228 $this->deletePath($oldKey);
229 $this->createDir($newKey);
230 $this->redis->rename($oldKey, $newKey);
231 $this->redis->eval('
232 local cursor = "0";
233 repeat
234 local t = redis.call("SCAN", cursor, "MATCH", ARGV[1]);
235 cursor = t[1];
236 local list = t[2];
237 for i = 1, #list do
238 local s = list[i];
239 local changed = s:gsub(ARGV[2], ARGV[3], 1);
240 redis.call("RENAME", s, changed);
241 end;
242 until cursor == "0";
243 ', array($this->getOsPath(array($oldKey, "*")), $this->prepareForLuaPattern($oldKey . DIRECTORY_SEPARATOR), $newKey . DIRECTORY_SEPARATOR), 0);
244 }
245
246 $success = true;
247 } catch (\Exception $e) {
248 // TODO: Log an error
249 }
250 return $success;
251 }
252
253 private function prepareForLuaPattern($pattern) {
254 $specialPatternChars = array("%", "(", ")", ".", "+", "-", "*", "?", "[", "^", "$");
255 $regex = "/(" . implode("|", array_map("preg_quote", $specialPatternChars)) . ")/";
256 return preg_replace($regex, "%$1", $pattern);
257 //return str_replace(array("%", "(", ")", ".", "+", "-", "*", "?", "[", "^", "$"), array("%%", "%(", "%)", "%.", "%+", "%-", "%*", "%?", "%[", "%^", "%$"), $pattern);
258 }
259
260 public function fopen($file, $mode) {
261 $fh = new \stdClass();
262 $fh->pos = 0;
263 $fh->content = "";
264 $fh->canRead = in_array($mode, array("r", "r+", "w+", "a+", "x+", "c+"));
265 $fh->canWrite = in_array($mode, array("r+", "w", "w+", "a", "a+", "x", "x+", "c", "c+"));
266 $fh->mode = $mode;
267 $fh->writeOccurred = false;
268 $fh->isOpen = true;
269 $fh->path = $file;
270
271 switch ($mode) {
272 case "r":
273 case "r+":
274 if (!$this->exists($file)) return false;
275 $fh->content = $this->getContent($file);
276 break;
277 case "w":
278 case "w+":
279 // Do nothing
280 break;
281 case "a":
282 case "a+":
283 if (!$this->exists($file)) return false;
284 $fh->content = $this->getContent($file);
285 break;
286 case "x":
287 case "x+":
288 if ($this->exists($file)) return false;
289 break;
290 case "c":
291 case "c+":
292 if ($this->exists($file)) {
293 $fh->content = $this->getContent($file);
294 }
295 break;
296 }
297
298 return new RedisFileHandle($fh);
299 }
300
301 public function fclose($handle) {
302 if (!($handle instanceof FileHandle)) return false;
303 $fh = $handle->getHandle();
304 if (!$fh->isOpen) return true;
305
306 if ($fh->canWrite && $fh->writeOccurred) {
307 $status = $this->fflush($handle);
308 if ($status) {
309 $fh->isOpen = false;
310 $fh->canRead = false;
311 $fh->canWrite = false;
312 }
313 return $status;
314 }
315
316 return true;
317 }
318
319 public function fflush($fh) {
320 if (!($fh instanceof FileHandle)) return false;
321 $fh = $fh->getHandle();
322 if ($fh->canWrite && $fh->writeOccurred && $fh->isOpen) {
323 if ($this->setContent($fh->path, $fh->content)) {
324 $fh->writeOccurred = false; // Reset the counter
325 return true;
326 } else {
327 return false;
328 }
329 }
330
331 return false;
332 }
333
334 public function fseek($fh, $offset, $whence = SEEK_SET) {
335 if (!($fh instanceof FileHandle)) return false;
336 $fh = $fh->getHandle();
337 switch ($whence) {
338 case SEEK_CUR:
339 $fh->pos += $offset;
340 break;
341 case SEEK_END:
342 $fh->pos = strlen($fh->content) + $offset;
343 break;
344 default:
345 $fh->pos = $offset;
346 break;
347 }
348
349 return 0;
350 }
351
352 public function ftell($fh) {
353 if (!($fh instanceof FileHandle)) return false;
354 $fh = $fh->getHandle();
355 return $fh->pos;
356 }
357
358 public function fwrite($fh, $string, $length = NULL) {
359 if (!($fh instanceof FileHandle)) return false;
360 $fh = $fh->getHandle();
361 if (!$fh->canWrite) return false;
362
363 if ($length === NULL || $length > strlen($string)) {
364 $length = strlen($string);
365 }
366
367 if ($fh->mode[0] == "a" || $fh->pos > strlen($fh->content)) {
368 $fh->content .= substr($string, 0, $length);
369 $fh->pos = strlen($fh->content);
370 } else {
371 $head = substr($fh->content, 0, $fh->pos);
372 $tail = substr($fh->content, $fh->pos + $length);
373 $fh->content = $head . substr($string, 0, $length) . $tail;
374 $fh->pos = strlen($head) + $length;
375 }
376
377 $fh->writeOccurred = true;
378
379 return $length;
380 }
381
382 public function fread($fh, $length) {
383 if (!($fh instanceof FileHandle)) return false;
384 $fh = $fh->getHandle();
385 if (!$fh->canRead) return false;
386
387 $result = substr($fh->content, $fh->pos, $length);
388 $fh->pos += strlen($result);//$length;
389
390 return $result;
391 }
392
393 public function fgetc($fh) {
394 if (!($fh instanceof FileHandle)) return false;
395 $fh = $fh->getHandle();
396 if (!$fh->canRead) return false;
397
398 return $fh->content[$fh->pos++];
399 }
400
401 public function fgets($fh, $length = NULL) {
402 if (!($fh instanceof FileHandle)) return false;
403 $fh = $fh->getHandle();
404 if (!$fh->canRead) return false;
405
406 if ($length === NULL) {
407 $length = strlen($fh->content);
408 }
409
410 $pos = strpos($fh->content, "\n", $fh->pos);
411 if ($pos === false) {
412 if ($fh->pos >= strlen($fh->content)) return false;
413 $result = substr($fh->content, $fh->pos, $length);
414 } else {
415 $result = substr($fh->content, $fh->pos, $pos - $fh->pos+1);
416 }
417 $fh->pos += strlen($result);
418 return $result;
419 }
420
421 public function flock($fh, $operation, $wouldblock = NULL) {
422 if (!($fh instanceof FileHandle)) return false;
423 $fh = $fh->getHandle();
424 if (!$fh->isOpen) return false;
425 switch ($operation) {
426 case LOCK_SH:
427 // In this case we acquire a lock in order to wait for any other process that has currently locked the file
428 // And then release the lock immediately.
429 // The sole purpose of this is to block until the writer process is complete.
430 return $this->acquireLock($fh->path, self::LOCK_TTL) && $this->releaseLock($fh->path);
431 case LOCK_EX:
432 return $this->acquireLock($fh->path, self::LOCK_TTL);
433 case LOCK_UN:
434 return $this->releaseLock($fh->path);
435 default:
436 return true;
437 }
438 return true;
439 }
440
441 public function feof($fh) {
442 if (!($fh instanceof FileHandle)) return false;
443 $fh = $fh->getHandle();
444 if (!$fh->isOpen) return false;
445 return $fh->pos >= strlen($fh->content);
446 }
447
448 private function acquireLock($path, $ttl) {
449 $startTime = microtime(true);
450 while (false === ($result = $this->redis->set('lock:' . $path, time(), ['nx', 'px' => self::LOCK_TTL])) && microtime(true) - $startTime < self::LOCK_TIMEOUT) {
451 usleep(50000);
452 }
453 return $result;
454 }
455
456 private function releaseLock($path) {
457 return $this->redis->del('lock:' . $path);
458 }
459 }
460