PluginProbe ʕ •ᴥ•ʔ
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization / 1.5.2
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization v1.5.2
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 5 years ago DiskFileHandle.php 5 years ago Redis.php 5 years ago RedisFileHandle.php 5 years ago
Redis.php
459 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) {
35 $path = $this->preparePathInput($path);
36 $parent = dirname($path);
37 $key = basename($path);
38 if ($this->isDir($parent)) {
39 $this->redis->hSet($parent, "::mtime::" . $key, time());
40 $this->redis->hSetNx($parent, "::content::" . $key, "");
41 return true;
42 } else {
43 return false;
44 }
45 }
46
47 public function setContent($path, $content) {
48 $path = $this->preparePathInput($path);
49 if ($this->isDir($path)) {
50 return false;
51 } else {
52 try {
53 //TODO: Create parent dir if it doesn't exist. This can impact performance though. Maybe make it optional
54 $dir = dirname($path);
55 $file = basename($path);
56 $this->redis->hMSet($dir, array(
57 "::content::" . $file => $content,
58 "::mtime::" . $file => time()
59 ));
60 } catch (\Exception $e) {
61 return false;
62 }
63 return true;
64 }
65 }
66
67 public function createDir($dir) {
68 $dir = $this->preparePathInput($dir);
69 $now = time();
70 $childDir = NULL;
71 $numDirsCreated = 0;
72 try {
73 while ($childDir !== "" && !$this->exists($dir)) {
74 $this->redis->hSet($dir, "::self::ctime::", $now);
75 $numDirsCreated++;
76 if ($childDir) {
77 $this->touch($this->getOsPath(array($dir, $childDir)));
78 }
79 $childDir = basename($dir);
80 $dir = dirname($dir);
81 }
82 if ($numDirsCreated > 0 && $childDir) {
83 $this->touch($this->getOsPath(array($dir, $childDir)));
84 }
85 } catch (\Exception $e) {
86 return false;
87 }
88
89 return true;
90 }
91
92 public function deletePath($path) {
93 $path = $this->preparePathInput($path);
94 $dirKey = dirname($path);
95 $fileName = basename($path);
96
97 try {
98 $deleted = $this->redis->hDel($dirKey, "::content::" . $fileName, "::mtime::" . $fileName);
99 if ($deleted) {
100 $this->touch($dirKey);
101 }
102 } catch (\Exception $e) {
103 return false;
104 }
105
106 return true;
107 }
108
109 public function deleteFile($path) {
110 return !$this->isDir($path) && $this->deletePath($path);
111 }
112
113 public function deleteDir($dir) {
114 $dir = $this->preparePathInput($dir);
115 try {
116 if (!$this->isDir($dir)) return true;
117 $this->trunkDir($dir) && $this->redis->unlink($dir) && $this->deletePath($dir);
118 } catch (\Exception $e) {
119 return false;
120 }
121 return true;
122 }
123
124 public function trunkDir($dir) {
125 $dir = $this->preparePathInput($dir);
126 if (!$this->isDir($dir)) return false;
127
128 if ($dir == DIRECTORY_SEPARATOR) {
129 $osPath = DIRECTORY_SEPARATOR . "*";
130 } else {
131 $osPath = $this->getOsPath(array($dir, "*"));
132 }
133
134 $success = false;
135 try {
136 $this->redis->eval('
137 local cursor = "0";
138 repeat
139 local t = redis.call("SCAN", cursor, "MATCH", ARGV[1]);
140 cursor = t[1];
141 local list = t[2];
142 for i = 1, #list do
143 redis.call("UNLINK", list[i]);
144 end;
145 until cursor == "0";
146 ', array($osPath), 0);
147 $success = true;
148 } catch (\Exception $e) {
149 // TODO: Log an error
150 }
151 return $success;
152 }
153
154 public function isDirEmpty($dir) {
155 $dir = $this->preparePathInput($dir);
156 return (int)$this->redis->hLen($dir) <= 1;
157 }
158
159 private function isDir($dir) {
160 $dir = $this->preparePathInput($dir);
161 return !!$this->redis->hLen($dir); // if this is a non-empty sorted set then it is a dir
162 }
163
164 public function dirForeach($dir, $callback) {
165 $dir = $this->preparePathInput($dir);
166 if (!$this->isDir($dir)) return false;
167 $result = true;
168 $it = NULL;
169 $prevScanMode = $this->redis->getOption(\Redis::OPT_SCAN);
170 $this->redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
171 try {
172 while($entries = $this->redis->hScan($dir, $it, "::mtime::*")) {
173 foreach($entries as $entry => $mtime) {
174 $entry = substr($entry, 9);//remove the ::mtime:: prefix
175 $path = $dir != DIRECTORY_SEPARATOR ? $this->getOsPath(array($dir, $entry)) : $dir . $entry;
176 call_user_func($callback, $path);
177 }
178 }
179 } catch (\Exception $e) {
180 // TODO: Log an error
181 $result = false;
182 } finally {
183 $this->redis->setOption(\Redis::OPT_SCAN, $prevScanMode);
184 }
185 return $result;
186 }
187
188 public function mtime($path) {
189 $path = $this->preparePathInput($path);
190 $dir = dirname($path);
191 $file = basename($path);
192 return $this->redis->hGet($dir, "::mtime::" . $file);
193 }
194
195 public function exists($path) {
196 $path = $this->preparePathInput($path);
197 $dir = dirname($path);
198 $file = basename($path);
199 return $this->redis->hExists($dir, "::mtime::" . $file);
200 }
201
202 public function getContent($path) {
203 $path = $this->preparePathInput($path);
204 if ($this->isDir($path)) {
205 return false;
206 } else {
207 $dir = dirname($path);
208 $file = basename($path);
209 return $this->redis->hGet($dir, "::content::" . $file);
210 }
211 }
212
213 public function rename($oldKey, $newKey, $innerCall = false) {
214 $oldKey = $this->preparePathInput($oldKey);
215 $newKey = $this->preparePathInput($newKey);
216 if ($this->exists($newKey)) return false;
217
218 $success = false;
219
220 try {
221 $isDir = $this->isDir($oldKey);
222 if (!$isDir) {
223 $content = $this->getContent($oldKey);
224 $this->deleteFile($oldKey);
225 $this->setContent($newKey, $content);
226 } else {
227 $this->deletePath($oldKey);
228 $this->createDir($newKey);
229 $this->redis->rename($oldKey, $newKey);
230 $this->redis->eval('
231 local cursor = "0";
232 repeat
233 local t = redis.call("SCAN", cursor, "MATCH", ARGV[1]);
234 cursor = t[1];
235 local list = t[2];
236 for i = 1, #list do
237 local s = list[i];
238 local changed = s:gsub(ARGV[2], ARGV[3], 1);
239 redis.call("RENAME", s, changed);
240 end;
241 until cursor == "0";
242 ', array($this->getOsPath(array($oldKey, "*")), $this->prepareForLuaPattern($oldKey . DIRECTORY_SEPARATOR), $newKey . DIRECTORY_SEPARATOR), 0);
243 }
244
245 $success = true;
246 } catch (\Exception $e) {
247 // TODO: Log an error
248 }
249 return $success;
250 }
251
252 private function prepareForLuaPattern($pattern) {
253 $specialPatternChars = array("%", "(", ")", ".", "+", "-", "*", "?", "[", "^", "$");
254 $regex = "/(" . implode("|", array_map("preg_quote", $specialPatternChars)) . ")/";
255 return preg_replace($regex, "%$1", $pattern);
256 //return str_replace(array("%", "(", ")", ".", "+", "-", "*", "?", "[", "^", "$"), array("%%", "%(", "%)", "%.", "%+", "%-", "%*", "%?", "%[", "%^", "%$"), $pattern);
257 }
258
259 public function fopen($file, $mode) {
260 $fh = new \stdClass();
261 $fh->pos = 0;
262 $fh->content = "";
263 $fh->canRead = in_array($mode, array("r", "r+", "w+", "a+", "x+", "c+"));
264 $fh->canWrite = in_array($mode, array("r+", "w", "w+", "a", "a+", "x", "x+", "c", "c+"));
265 $fh->mode = $mode;
266 $fh->writeOccurred = false;
267 $fh->isOpen = true;
268 $fh->path = $file;
269
270 switch ($mode) {
271 case "r":
272 case "r+":
273 if (!$this->exists($file)) return false;
274 $fh->content = $this->getContent($file);
275 break;
276 case "w":
277 case "w+":
278 // Do nothing
279 break;
280 case "a":
281 case "a+":
282 if (!$this->exists($file)) return false;
283 $fh->content = $this->getContent($file);
284 break;
285 case "x":
286 case "x+":
287 if ($this->exists($file)) return false;
288 break;
289 case "c":
290 case "c+":
291 if ($this->exists($file)) {
292 $fh->content = $this->getContent($file);
293 }
294 break;
295 }
296
297 return new RedisFileHandle($fh);
298 }
299
300 public function fclose($handle) {
301 if (!($handle instanceof FileHandle)) return false;
302 $fh = $handle->getHandle();
303 if (!$fh->isOpen) return true;
304
305 if ($fh->canWrite && $fh->writeOccurred) {
306 $status = $this->fflush($handle);
307 if ($status) {
308 $fh->isOpen = false;
309 $fh->canRead = false;
310 $fh->canWrite = false;
311 }
312 return $status;
313 }
314
315 return true;
316 }
317
318 public function fflush($fh) {
319 if (!($fh instanceof FileHandle)) return false;
320 $fh = $fh->getHandle();
321 if ($fh->canWrite && $fh->writeOccurred && $fh->isOpen) {
322 if ($this->setContent($fh->path, $fh->content)) {
323 $fh->writeOccurred = false; // Reset the counter
324 return true;
325 } else {
326 return false;
327 }
328 }
329
330 return false;
331 }
332
333 public function fseek($fh, $offset, $whence = SEEK_SET) {
334 if (!($fh instanceof FileHandle)) return false;
335 $fh = $fh->getHandle();
336 switch ($whence) {
337 case SEEK_CUR:
338 $fh->pos += $offset;
339 break;
340 case SEEK_END:
341 $fh->pos = strlen($fh->content) + $offset;
342 break;
343 default:
344 $fh->pos = $offset;
345 break;
346 }
347
348 return 0;
349 }
350
351 public function ftell($fh) {
352 if (!($fh instanceof FileHandle)) return false;
353 $fh = $fh->getHandle();
354 return $fh->pos;
355 }
356
357 public function fwrite($fh, $string, $length = NULL) {
358 if (!($fh instanceof FileHandle)) return false;
359 $fh = $fh->getHandle();
360 if (!$fh->canWrite) return false;
361
362 if ($length === NULL || $length > strlen($string)) {
363 $length = strlen($string);
364 }
365
366 if ($fh->mode[0] == "a" || $fh->pos > strlen($fh->content)) {
367 $fh->content .= substr($string, 0, $length);
368 $fh->pos = strlen($fh->content);
369 } else {
370 $head = substr($fh->content, 0, $fh->pos);
371 $tail = substr($fh->content, $fh->pos + $length);
372 $fh->content = $head . substr($string, 0, $length) . $tail;
373 $fh->pos = strlen($head) + $length;
374 }
375
376 $fh->writeOccurred = true;
377
378 return $length;
379 }
380
381 public function fread($fh, $length) {
382 if (!($fh instanceof FileHandle)) return false;
383 $fh = $fh->getHandle();
384 if (!$fh->canRead) return false;
385
386 $result = substr($fh->content, $fh->pos, $length);
387 $fh->pos += strlen($result);//$length;
388
389 return $result;
390 }
391
392 public function fgetc($fh) {
393 if (!($fh instanceof FileHandle)) return false;
394 $fh = $fh->getHandle();
395 if (!$fh->canRead) return false;
396
397 return $fh->content[$fh->pos++];
398 }
399
400 public function fgets($fh, $length = NULL) {
401 if (!($fh instanceof FileHandle)) return false;
402 $fh = $fh->getHandle();
403 if (!$fh->canRead) return false;
404
405 if ($length === NULL) {
406 $length = strlen($fh->content);
407 }
408
409 $pos = strpos($fh->content, "\n", $fh->pos);
410 if ($pos === false) {
411 if ($fh->pos >= strlen($fh->content)) return false;
412 $result = substr($fh->content, $fh->pos, $length);
413 } else {
414 $result = substr($fh->content, $fh->pos, $pos - $fh->pos+1);
415 }
416 $fh->pos += strlen($result);
417 return $result;
418 }
419
420 public function flock($fh, $operation, $wouldblock = NULL) {
421 if (!($fh instanceof FileHandle)) return false;
422 $fh = $fh->getHandle();
423 if (!$fh->isOpen) return false;
424 switch ($operation) {
425 case LOCK_SH:
426 // In this case we acquire a lock in order to wait for any other process that has currently locked the file
427 // And then release the lock immediately.
428 // The sole purpose of this is to block until the writer process is complete.
429 return $this->acquireLock($fh->path, self::LOCK_TTL) && $this->releaseLock($fh->path);
430 case LOCK_EX:
431 return $this->acquireLock($fh->path, self::LOCK_TTL);
432 case LOCK_UN:
433 return $this->releaseLock($fh->path);
434 default:
435 return true;
436 }
437 return true;
438 }
439
440 public function feof($fh) {
441 if (!($fh instanceof FileHandle)) return false;
442 $fh = $fh->getHandle();
443 if (!$fh->isOpen) return false;
444 return $fh->pos >= strlen($fh->content);
445 }
446
447 private function acquireLock($path, $ttl) {
448 $startTime = microtime(true);
449 while (false === ($result = $this->redis->set('lock:' . $path, time(), ['nx', 'px' => self::LOCK_TTL])) && microtime(true) - $startTime < self::LOCK_TIMEOUT) {
450 usleep(50000);
451 }
452 return $result;
453 }
454
455 private function releaseLock($path) {
456 return $this->redis->del('lock:' . $path);
457 }
458 }
459