1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Phoole (PHP7.2+) |
5
|
|
|
* |
6
|
|
|
* @category Library |
7
|
|
|
* @package Phoole\Base |
8
|
|
|
* @copyright Copyright (c) 2019 Hong Zhang |
9
|
|
|
*/ |
10
|
|
|
declare(strict_types=1); |
11
|
|
|
|
12
|
|
|
namespace Phoole\Base\Storage; |
13
|
|
|
|
14
|
|
|
use Phoole\Base\Exception\NotFoundException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Storage using filesystem |
18
|
|
|
* |
19
|
|
|
* Good for cache / session etc. |
20
|
|
|
* |
21
|
|
|
* @package Phoole\Base |
22
|
|
|
*/ |
23
|
|
|
class Filesystem implements StorageInterface |
24
|
|
|
{ |
25
|
|
|
/** |
26
|
|
|
* @var string |
27
|
|
|
*/ |
28
|
|
|
protected $rootPath; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* normally 0 - 3 |
32
|
|
|
* |
33
|
|
|
* @var int |
34
|
|
|
*/ |
35
|
|
|
protected $hashLevel; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @param string $rootPath the base/root storage directory |
39
|
|
|
* @param int $hashLevel directory hash depth |
40
|
|
|
* @throws \RuntimeException if mkdir failed |
41
|
|
|
*/ |
42
|
|
|
public function __construct( |
43
|
|
|
string $rootPath, |
44
|
|
|
int $hashLevel = 2 |
45
|
|
|
) { |
46
|
|
|
if (!file_exists($rootPath) && !@mkdir($rootPath, 0777, true)) { |
47
|
|
|
throw new \RuntimeException("Failed to create $rootPath"); |
48
|
|
|
} |
49
|
|
|
$this->rootPath = realpath($rootPath) . \DIRECTORY_SEPARATOR; |
50
|
|
|
$this->hashLevel = $hashLevel; |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* {@inheritDoc} |
55
|
|
|
*/ |
56
|
|
|
public function get(string $key): array |
57
|
|
|
{ |
58
|
|
|
$path = $this->getPath($key); |
59
|
|
|
if (file_exists($path)) { |
60
|
|
|
return [file_get_contents($path), filemtime($path)]; |
61
|
|
|
} |
62
|
|
|
throw new NotFoundException("Not found for $key"); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* {@inheritDoc} |
67
|
|
|
*/ |
68
|
|
|
public function set(string $key, string $value, int $ttl): bool |
69
|
|
|
{ |
70
|
|
|
$path = $this->getPath($key); |
71
|
|
|
|
72
|
|
|
// write to a temp file first |
73
|
|
|
$temp = tempnam(dirname($path), 'TMP'); |
74
|
|
|
file_put_contents($temp, $value); |
75
|
|
|
|
76
|
|
|
// rename the temp file with locking |
77
|
|
View Code Duplication |
if ($fp = $this->getLock($path)) { |
|
|
|
|
78
|
|
|
$res = rename($temp, $path) && touch($path, time() + $ttl); |
79
|
|
|
$this->releaseLock($path, $fp); |
80
|
|
|
return $res; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
// locking failed |
84
|
|
|
return false; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* {@inheritDoc} |
89
|
|
|
*/ |
90
|
|
|
public function delete(string $key): bool |
91
|
|
|
{ |
92
|
|
|
$path = $this->getPath($key); |
93
|
|
View Code Duplication |
if (file_exists($path) && $fp = $this->getLock($path)) { |
|
|
|
|
94
|
|
|
$res = unlink($path); |
95
|
|
|
$this->releaseLock($path, $fp); |
96
|
|
|
return $res; |
97
|
|
|
} |
98
|
|
|
return false; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* {@inheritDoc} |
103
|
|
|
*/ |
104
|
|
|
public function clear(): bool |
105
|
|
|
{ |
106
|
|
|
$path = rtrim($this->getRoot(), '/\\'); |
107
|
|
|
if (file_exists($path)) { |
108
|
|
|
$temp = $path . '_' . substr(md5($path . microtime(true)), -5); |
109
|
|
|
return rename($path, $temp) && mkdir($path, 0777, true); |
110
|
|
|
} |
111
|
|
|
return false; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* {@inheritDoc} |
116
|
|
|
*/ |
117
|
|
|
public function garbageCollect() |
118
|
|
|
{ |
119
|
|
|
$root = $this->getRoot(); |
120
|
|
|
|
121
|
|
|
// remove staled file |
122
|
|
|
$this->rmDir($root, true); |
123
|
|
|
|
124
|
|
|
// remove staled directory |
125
|
|
|
$pattern = rtrim($root, '/\\') . '_*'; |
126
|
|
|
foreach (glob($pattern, GLOB_MARK) as $dir) { |
127
|
|
|
$this->rmDir($dir); |
128
|
|
|
} |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @return string |
133
|
|
|
*/ |
134
|
|
|
protected function getRoot(): string |
135
|
|
|
{ |
136
|
|
|
return $this->rootPath; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* @param string $key |
141
|
|
|
* @return string |
142
|
|
|
*/ |
143
|
|
|
protected function getPath(string $key): string |
144
|
|
|
{ |
145
|
|
|
// get hashed directory |
146
|
|
|
$name = $key . '00'; |
147
|
|
|
$path = $this->getRoot(); |
148
|
|
|
for ($i = 0; $i < $this->hashLevel; ++$i) { |
149
|
|
|
$path .= $name[$i] . \DIRECTORY_SEPARATOR; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
// make sure hashed directory exists |
153
|
|
|
if (!file_exists($path)) { |
154
|
|
|
mkdir($path, 0777, true); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
// return full path |
158
|
|
|
return $path . $key; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* get lock of the path |
163
|
|
|
* |
164
|
|
|
* @param string $path |
165
|
|
|
* @return resource|false |
166
|
|
|
*/ |
167
|
|
|
protected function getLock(string $path) |
168
|
|
|
{ |
169
|
|
|
$lock = $path . '.lock'; |
170
|
|
|
$count = 0; |
171
|
|
|
if ($fp = fopen($lock, 'c')) { |
172
|
|
|
while (true) { |
173
|
|
|
// try 3 times only |
174
|
|
|
if ($count++ > 3) { |
175
|
|
|
break; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
// get lock non-blockingly |
179
|
|
|
if (flock($fp, \LOCK_EX | \LOCK_NB)) { |
180
|
|
|
return $fp; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
// sleep randon time between attempts |
184
|
|
|
usleep(rand(1, 10)); |
185
|
|
|
} |
186
|
|
|
// failed |
187
|
|
|
fclose($fp); |
188
|
|
|
} |
189
|
|
|
return false; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* @param string $path |
194
|
|
|
* @param resource $fp |
195
|
|
|
* @return bool |
196
|
|
|
*/ |
197
|
|
|
protected function releaseLock(string $path, $fp): bool |
198
|
|
|
{ |
199
|
|
|
$lock = $path . '.lock'; |
200
|
|
|
return flock($fp, LOCK_UN) && fclose($fp) && unlink($lock); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Remove a whole directory or stale files only |
205
|
|
|
* |
206
|
|
|
* @param string $dir |
207
|
|
|
* @return void |
208
|
|
|
*/ |
209
|
|
|
protected function rmDir(string $dir, bool $staleOnly = false) |
210
|
|
|
{ |
211
|
|
|
foreach (glob($dir . '{,.}[!.,!..]*', GLOB_MARK | GLOB_BRACE) as $file) { |
212
|
|
|
if (is_dir($file)) { |
213
|
|
|
$this->rmDir($file, $staleOnly); |
214
|
|
|
} else { |
215
|
|
|
if ($staleOnly && filemtime($file) > time()) { |
216
|
|
|
continue; |
217
|
|
|
} |
218
|
|
|
unlink($file); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
$staleOnly || rmdir($dir); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.