1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of the ICanBoogie package. |
||
5 | * |
||
6 | * (c) Olivier Laviale <[email protected]> |
||
7 | * |
||
8 | * For the full copyright and license information, please view the LICENSE |
||
9 | * file that was distributed with this source code. |
||
10 | */ |
||
11 | |||
12 | namespace ICanBoogie\Storage; |
||
13 | |||
14 | use ICanBoogie\Storage\FileStorage\Adapter; |
||
15 | use ICanBoogie\Storage\FileStorage\Adapter\SerializeAdapter; |
||
16 | use ICanBoogie\Storage\FileStorage\Iterator; |
||
17 | |||
18 | /** |
||
19 | * A storage using the file system. |
||
20 | */ |
||
21 | class FileStorage implements Storage, \ArrayAccess |
||
22 | { |
||
23 | use Storage\ArrayAccess; |
||
24 | use Storage\ClearWithIterator; |
||
25 | |||
26 | static private $release_after; |
||
27 | |||
28 | /** |
||
29 | * Absolute path to the storage directory. |
||
30 | * |
||
31 | * @var string |
||
32 | */ |
||
33 | private $path; |
||
34 | |||
35 | /** |
||
36 | * @var Adapter |
||
37 | */ |
||
38 | private $adapter; |
||
39 | |||
40 | /** |
||
41 | * Constructor. |
||
42 | * |
||
43 | * @param string $path Absolute path to the storage directory. |
||
44 | * @param Adapter $adapter |
||
45 | */ |
||
46 | public function __construct(string $path, Adapter $adapter = null) |
||
47 | { |
||
48 | $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
||
49 | $this->adapter = $adapter ?: new SerializeAdapter; |
||
50 | |||
51 | if (self::$release_after === null) |
||
52 | { |
||
53 | self::$release_after = strpos(PHP_OS, 'WIN') === 0 ? false : true; |
||
54 | } |
||
55 | } |
||
56 | |||
57 | /** |
||
58 | * @inheritdoc |
||
59 | */ |
||
60 | public function exists(string $key): bool |
||
61 | { |
||
62 | $pathname = $this->format_pathname($key); |
||
63 | $ttl_mark = $this->format_pathname_with_ttl($pathname); |
||
64 | |||
65 | if (file_exists($ttl_mark) && fileatime($ttl_mark) < time() || !file_exists($pathname)) |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
66 | { |
||
67 | return false; |
||
68 | } |
||
69 | |||
70 | return file_exists($pathname); |
||
71 | } |
||
72 | |||
73 | /** |
||
74 | * @inheritdoc |
||
75 | */ |
||
76 | public function retrieve(string $key) |
||
77 | { |
||
78 | if (!$this->exists($key)) { |
||
79 | return null; |
||
80 | } |
||
81 | |||
82 | return $this->read($this->format_pathname($key)); |
||
83 | } |
||
84 | |||
85 | /** |
||
86 | * @inheritdoc |
||
87 | * |
||
88 | * @throws \Exception when a file operation fails. |
||
89 | */ |
||
90 | public function store(string $key, $value, int $ttl = null): void |
||
91 | { |
||
92 | $this->check_writable(); |
||
93 | |||
94 | $pathname = $this->format_pathname($key); |
||
95 | $ttl_mark = $this->format_pathname_with_ttl($pathname); |
||
96 | |||
97 | if ($ttl) |
||
98 | { |
||
99 | $future = time() + $ttl; |
||
100 | |||
101 | touch($ttl_mark, $future, $future); |
||
102 | } |
||
103 | elseif (file_exists($ttl_mark)) |
||
104 | { |
||
105 | unlink($ttl_mark); |
||
106 | } |
||
107 | |||
108 | if ($value === true) |
||
109 | { |
||
110 | touch($pathname); |
||
111 | |||
112 | return; |
||
113 | } |
||
114 | |||
115 | if ($value === null) |
||
116 | { |
||
117 | $this->eliminate($key); |
||
118 | |||
119 | return; |
||
120 | } |
||
121 | |||
122 | set_error_handler(function() {}); |
||
123 | |||
124 | try |
||
125 | { |
||
126 | $this->safe_store($pathname, $value); |
||
127 | } |
||
128 | catch (\Exception $e) |
||
129 | { |
||
130 | throw $e; |
||
131 | } |
||
132 | finally |
||
133 | { |
||
134 | restore_error_handler(); |
||
135 | } |
||
136 | } |
||
137 | |||
138 | /** |
||
139 | * @inheritdoc |
||
140 | */ |
||
141 | public function eliminate(string $key): void |
||
142 | { |
||
143 | $pathname = $this->format_pathname($key); |
||
144 | |||
145 | if (!file_exists($pathname)) |
||
146 | { |
||
147 | return; |
||
148 | } |
||
149 | |||
150 | unlink($pathname); |
||
151 | } |
||
152 | |||
153 | /** |
||
154 | * Normalizes a key into a valid filename. |
||
155 | */ |
||
156 | private function normalize_key(string $key): string |
||
157 | { |
||
158 | return str_replace('/', '--', $key); |
||
159 | } |
||
160 | |||
161 | /** |
||
162 | * Formats a key into an absolute pathname. |
||
163 | */ |
||
164 | private function format_pathname(string $key): string |
||
165 | { |
||
166 | return $this->path . $this->normalize_key($key); |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * Formats a pathname with a TTL extension. |
||
171 | */ |
||
172 | private function format_pathname_with_ttl(string $pathname): string |
||
173 | { |
||
174 | return $pathname . '.ttl'; |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * @return bool|string |
||
179 | */ |
||
180 | private function read(string $pathname) |
||
181 | { |
||
182 | return $this->adapter->read($pathname); |
||
183 | } |
||
184 | |||
185 | /** |
||
186 | * @param mixed $value |
||
187 | */ |
||
188 | private function write(string $pathname, $value): void |
||
189 | { |
||
190 | $this->adapter->write($pathname, $value); |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * Safely store the value. |
||
195 | * |
||
196 | * @param mixed $value |
||
197 | * |
||
198 | * @throws \Exception if an error occurs. |
||
199 | */ |
||
200 | private function safe_store(string $pathname, $value): void |
||
201 | { |
||
202 | $dir = dirname($pathname); |
||
203 | $uniqid = uniqid(mt_rand(), true); |
||
204 | $tmp_pathname = $dir . '/var-' . $uniqid; |
||
205 | $garbage_pathname = $dir . '/garbage-var-' . $uniqid; |
||
206 | |||
207 | # |
||
208 | # We lock the file create/update, but we write the data in a temporary file, which is then |
||
209 | # renamed once the data is written. |
||
210 | # |
||
211 | |||
212 | $fh = fopen($pathname, 'a+'); |
||
213 | |||
214 | if (!$fh) |
||
0 ignored issues
–
show
|
|||
215 | { |
||
216 | throw new \Exception("Unable to open $pathname."); |
||
217 | } |
||
218 | |||
219 | if (self::$release_after && !flock($fh, LOCK_EX)) |
||
220 | { |
||
221 | throw new \Exception("Unable to get to exclusive lock on $pathname."); |
||
222 | } |
||
223 | |||
224 | $this->write($tmp_pathname, $value); |
||
225 | |||
226 | # |
||
227 | # Windows, this is for you |
||
228 | # |
||
229 | if (!self::$release_after) |
||
230 | { |
||
231 | fclose($fh); |
||
232 | } |
||
233 | |||
234 | if (!rename($pathname, $garbage_pathname)) |
||
235 | { |
||
236 | throw new \Exception("Unable to rename $pathname as $garbage_pathname."); |
||
237 | } |
||
238 | |||
239 | if (!rename($tmp_pathname, $pathname)) |
||
240 | { |
||
241 | throw new \Exception("Unable to rename $tmp_pathname as $pathname."); |
||
242 | } |
||
243 | |||
244 | if (!unlink($garbage_pathname)) |
||
245 | { |
||
246 | throw new \Exception("Unable to delete $garbage_pathname."); |
||
247 | } |
||
248 | |||
249 | # |
||
250 | # Unix, this is for you |
||
251 | # |
||
252 | if (self::$release_after) |
||
253 | { |
||
254 | flock($fh, LOCK_UN); |
||
255 | fclose($fh); |
||
256 | } |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * @inheritdoc |
||
261 | */ |
||
262 | public function getIterator(): iterable |
||
263 | { |
||
264 | if (!is_dir($this->path)) |
||
265 | { |
||
266 | return; |
||
267 | } |
||
268 | |||
269 | $iterator = new \DirectoryIterator($this->path); |
||
270 | |||
271 | foreach ($iterator as $file) |
||
272 | { |
||
273 | if ($file->isDot() || $file->isDir()) |
||
274 | { |
||
275 | continue; |
||
276 | } |
||
277 | |||
278 | yield $file->getFilename(); |
||
279 | } |
||
280 | } |
||
281 | |||
282 | /** |
||
283 | * Returns an iterator for the keys matching a specified regex. |
||
284 | */ |
||
285 | public function matching(string $regex): iterable |
||
286 | { |
||
287 | return new Iterator(new \RegexIterator(new \DirectoryIterator($this->path), $regex)); |
||
288 | } |
||
289 | |||
290 | private $is_writable; |
||
291 | |||
292 | /** |
||
293 | * Checks whether the storage directory is writable. |
||
294 | * |
||
295 | * @throws \Exception when the storage directory is not writable. |
||
296 | */ |
||
297 | public function check_writable(): bool |
||
298 | { |
||
299 | if ($this->is_writable) |
||
300 | { |
||
301 | return true; |
||
302 | } |
||
303 | |||
304 | $path = $this->path; |
||
305 | |||
306 | if (!file_exists($path)) |
||
307 | { |
||
308 | set_error_handler(function() {}); |
||
309 | mkdir($path, 0705, true); |
||
310 | restore_error_handler(); |
||
311 | } |
||
312 | |||
313 | if (!is_writable($path)) |
||
314 | { |
||
315 | throw new \Exception("The directory $path is not writable."); |
||
316 | } |
||
317 | |||
318 | return $this->is_writable = true; |
||
319 | } |
||
320 | } |
||
321 |