Issues (9)

lib/FileStorage.php (2 issues)

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
Consider adding parentheses for clarity. Current Interpretation: (file_exists($ttl_mark) ... file_exists($pathname), Probably Intended Meaning: file_exists($ttl_mark) &...file_exists($pathname))
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
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
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