• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / caching / 15478823916

05 Jun 2025 10:47PM UTC coverage: 87.445% (-0.1%) from 87.592%
15478823916

push

github

dg
cs

592 of 677 relevant lines covered (87.44%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.84
/src/Caching/Storages/FileStorage.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Caching\Storages;
11

12
use Nette;
13
use Nette\Caching\Cache;
14

15

16
/**
17
 * Cache file storage.
18
 */
19
class FileStorage implements Nette\Caching\Storage
20
{
21
        /**
22
         * Atomic thread safe logic:
23
         *
24
         * 1) reading: open(r+b), lock(SH), read
25
         *     - delete?: delete*, close
26
         * 2) deleting: delete*
27
         * 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
28
         *
29
         * delete* = try unlink, if fails (on NTFS) { lock(EX), truncate, close, unlink } else close (on ext3)
30
         */
31

32
        /** @internal cache file structure: meta-struct size + serialized meta-struct + data */
33
        private const
34
                MetaHeaderLen = 6,
35
                // meta structure: array of
36
                MetaTime = 'time', // timestamp
37
                MetaSerialized = 'serialized', // is content serialized?
38
                MetaExpire = 'expire', // expiration timestamp
39
                MetaDelta = 'delta', // relative (sliding) expiration
40
                MetaItems = 'di', // array of dependent items (file => timestamp)
41
                MetaCallbacks = 'callbacks'; // array of callbacks (function, args)
42

43
        /** additional cache structure */
44
        private const
45
                File = 'file',
46
                Handle = 'handle';
47

48
        /** probability that the clean() routine is started */
49
        public static float $gcProbability = 0.001;
50

51
        private string $dir;
52
        private ?Journal $journal;
53
        private array $locks;
54

55

56
        public function __construct(string $dir, ?Journal $journal = null)
1✔
57
        {
58
                if (!is_dir($dir) || !Nette\Utils\FileSystem::isAbsolute($dir)) {
1✔
59
                        throw new Nette\DirectoryNotFoundException("Directory '$dir' not found or is not absolute.");
1✔
60
                }
61

62
                $this->dir = $dir;
1✔
63
                $this->journal = $journal;
1✔
64

65
                if (mt_rand() / mt_getrandmax() < static::$gcProbability) {
1✔
66
                        $this->clean([]);
×
67
                }
68
        }
1✔
69

70

71
        public function read(string $key): mixed
1✔
72
        {
73
                $meta = $this->readMetaAndLock($this->getCacheFile($key), LOCK_SH);
1✔
74
                return $meta && $this->verify($meta)
1✔
75
                        ? $this->readData($meta) // calls fclose()
1✔
76
                        : null;
1✔
77
        }
78

79

80
        /**
81
         * Verifies dependencies.
82
         */
83
        private function verify(array $meta): bool
1✔
84
        {
85
                do {
86
                        if (!empty($meta[self::MetaDelta])) {
1✔
87
                                // meta[file] was added by readMetaAndLock()
88
                                if (filemtime($meta[self::File]) + $meta[self::MetaDelta] < time()) {
1✔
89
                                        break;
1✔
90
                                }
91

92
                                touch($meta[self::File]);
1✔
93

94
                        } elseif (!empty($meta[self::MetaExpire]) && $meta[self::MetaExpire] < time()) {
1✔
95
                                break;
1✔
96
                        }
97

98
                        if (!empty($meta[self::MetaCallbacks]) && !Cache::checkCallbacks($meta[self::MetaCallbacks])) {
1✔
99
                                break;
1✔
100
                        }
101

102
                        if (!empty($meta[self::MetaItems])) {
1✔
103
                                foreach ($meta[self::MetaItems] as $depFile => $time) {
1✔
104
                                        $m = $this->readMetaAndLock($depFile, LOCK_SH);
1✔
105
                                        if (($m[self::MetaTime] ?? null) !== $time || ($m && !$this->verify($m))) {
1✔
106
                                                break 2;
1✔
107
                                        }
108
                                }
109
                        }
110

111
                        return true;
1✔
112
                } while (false);
×
113

114
                $this->delete($meta[self::File], $meta[self::Handle]); // meta[handle] & meta[file] was added by readMetaAndLock()
1✔
115
                return false;
1✔
116
        }
117

118

119
        public function lock(string $key): void
1✔
120
        {
121
                $cacheFile = $this->getCacheFile($key);
1✔
122
                if (!is_dir($dir = dirname($cacheFile))) {
1✔
123
                        @mkdir($dir); // @ - directory may already exist
1✔
124
                }
125

126
                $handle = fopen($cacheFile, 'c+b');
1✔
127
                if (!$handle) {
1✔
128
                        return;
×
129
                }
130

131
                $this->locks[$key] = $handle;
1✔
132
                flock($handle, LOCK_EX);
1✔
133
        }
1✔
134

135

136
        public function write(string $key, $data, array $dp): void
1✔
137
        {
138
                $meta = [
1✔
139
                        self::MetaTime => microtime(),
1✔
140
                ];
141

142
                if (isset($dp[Cache::Expire])) {
1✔
143
                        if (empty($dp[Cache::Sliding])) {
1✔
144
                                $meta[self::MetaExpire] = $dp[Cache::Expire] + time(); // absolute time
1✔
145
                        } else {
146
                                $meta[self::MetaDelta] = (int) $dp[Cache::Expire]; // sliding time
1✔
147
                        }
148
                }
149

150
                if (isset($dp[Cache::Items])) {
1✔
151
                        foreach ($dp[Cache::Items] as $item) {
1✔
152
                                $depFile = $this->getCacheFile($item);
1✔
153
                                $m = $this->readMetaAndLock($depFile, LOCK_SH);
1✔
154
                                $meta[self::MetaItems][$depFile] = $m[self::MetaTime] ?? null;
1✔
155
                                unset($m);
1✔
156
                        }
157
                }
158

159
                if (isset($dp[Cache::Callbacks])) {
1✔
160
                        $meta[self::MetaCallbacks] = $dp[Cache::Callbacks];
1✔
161
                }
162

163
                if (!isset($this->locks[$key])) {
1✔
164
                        $this->lock($key);
1✔
165
                        if (!isset($this->locks[$key])) {
1✔
166
                                return;
×
167
                        }
168
                }
169

170
                $handle = $this->locks[$key];
1✔
171
                unset($this->locks[$key]);
1✔
172

173
                $cacheFile = $this->getCacheFile($key);
1✔
174

175
                if (isset($dp[Cache::Tags]) || isset($dp[Cache::Priority])) {
1✔
176
                        if (!$this->journal) {
1✔
177
                                throw new Nette\InvalidStateException('CacheJournal has not been provided.');
1✔
178
                        }
179

180
                        $this->journal->write($cacheFile, $dp);
1✔
181
                }
182

183
                ftruncate($handle, 0);
1✔
184

185
                if (!is_string($data)) {
1✔
186
                        $data = serialize($data);
1✔
187
                        $meta[self::MetaSerialized] = true;
1✔
188
                }
189

190
                $head = serialize($meta);
1✔
191
                $head = str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
1✔
192
                $headLen = strlen($head);
1✔
193

194
                do {
195
                        if (fwrite($handle, str_repeat("\x00", $headLen)) !== $headLen) {
1✔
196
                                break;
×
197
                        }
198

199
                        if (fwrite($handle, $data) !== strlen($data)) {
1✔
200
                                break;
×
201
                        }
202

203
                        fseek($handle, 0);
1✔
204
                        if (fwrite($handle, $head) !== $headLen) {
1✔
205
                                break;
×
206
                        }
207

208
                        flock($handle, LOCK_UN);
1✔
209
                        fclose($handle);
1✔
210
                        return;
1✔
211
                } while (false);
×
212

213
                $this->delete($cacheFile, $handle);
×
214
        }
215

216

217
        public function remove(string $key): void
1✔
218
        {
219
                unset($this->locks[$key]);
1✔
220
                $this->delete($this->getCacheFile($key));
1✔
221
        }
1✔
222

223

224
        public function clean(array $conditions): void
1✔
225
        {
226
                $all = !empty($conditions[Cache::All]);
1✔
227
                $collector = empty($conditions);
1✔
228
                $namespaces = $conditions[Cache::Namespaces] ?? null;
1✔
229

230
                // cleaning using file iterator
231
                if ($all || $collector) {
1✔
232
                        $now = time();
1✔
233
                        foreach (Nette\Utils\Finder::find('_*')->from($this->dir)->childFirst() as $entry) {
1✔
234
                                $path = (string) $entry;
1✔
235
                                if ($entry->isDir()) { // collector: remove empty dirs
1✔
236
                                        @rmdir($path); // @ - removing dirs is not necessary
1✔
237
                                        continue;
1✔
238
                                }
239

240
                                if ($all) {
1✔
241
                                        $this->delete($path);
1✔
242

243
                                } else { // collector
244
                                        $meta = $this->readMetaAndLock($path, LOCK_SH);
×
245
                                        if (!$meta) {
×
246
                                                continue;
×
247
                                        }
248

249
                                        if ((!empty($meta[self::MetaDelta]) && filemtime($meta[self::File]) + $meta[self::MetaDelta] < $now)
×
250
                                                || (!empty($meta[self::MetaExpire]) && $meta[self::MetaExpire] < $now)
×
251
                                        ) {
252
                                                $this->delete($path, $meta[self::Handle]);
×
253
                                                continue;
×
254
                                        }
255

256
                                        flock($meta[self::Handle], LOCK_UN);
×
257
                                        fclose($meta[self::Handle]);
×
258
                                }
259
                        }
260

261
                        if ($this->journal) {
1✔
262
                                $this->journal->clean($conditions);
×
263
                        }
264

265
                        return;
1✔
266

267
                } elseif ($namespaces) {
1✔
268
                        foreach ($namespaces as $namespace) {
1✔
269
                                $dir = $this->dir . '/_' . urlencode($namespace);
1✔
270
                                if (!is_dir($dir)) {
1✔
271
                                        continue;
×
272
                                }
273

274
                                foreach (Nette\Utils\Finder::findFiles('_*')->in($dir) as $entry) {
1✔
275
                                        $this->delete((string) $entry);
1✔
276
                                }
277

278
                                @rmdir($dir); // may already contain new files
1✔
279
                        }
280
                }
281

282
                // cleaning using journal
283
                if ($this->journal) {
1✔
284
                        foreach ($this->journal->clean($conditions) as $file) {
1✔
285
                                $this->delete($file);
1✔
286
                        }
287
                }
288
        }
1✔
289

290

291
        /**
292
         * Reads cache data from disk.
293
         */
294
        protected function readMetaAndLock(string $file, int $lock): ?array
1✔
295
        {
296
                $handle = @fopen($file, 'r+b'); // @ - file may not exist
1✔
297
                if (!$handle) {
1✔
298
                        return null;
1✔
299
                }
300

301
                flock($handle, $lock);
1✔
302

303
                $size = (int) stream_get_contents($handle, self::MetaHeaderLen);
1✔
304
                if ($size) {
1✔
305
                        $meta = stream_get_contents($handle, $size, self::MetaHeaderLen);
1✔
306
                        $meta = unserialize($meta);
1✔
307
                        $meta[self::File] = $file;
1✔
308
                        $meta[self::Handle] = $handle;
1✔
309
                        return $meta;
1✔
310
                }
311

312
                flock($handle, LOCK_UN);
×
313
                fclose($handle);
×
314
                return null;
×
315
        }
316

317

318
        /**
319
         * Reads cache data from disk and closes cache file handle.
320
         */
321
        protected function readData(array $meta): mixed
1✔
322
        {
323
                $data = stream_get_contents($meta[self::Handle]);
1✔
324
                flock($meta[self::Handle], LOCK_UN);
1✔
325
                fclose($meta[self::Handle]);
1✔
326

327
                return empty($meta[self::MetaSerialized]) ? $data : unserialize($data);
1✔
328
        }
329

330

331
        /**
332
         * Returns file name.
333
         */
334
        protected function getCacheFile(string $key): string
1✔
335
        {
336
                $file = urlencode($key);
1✔
337
                if ($a = strrpos($file, '%00')) { // %00 = urlencode(Nette\Caching\Cache::NamespaceSeparator)
1✔
338
                        $file = substr_replace($file, '/_', $a, 3);
1✔
339
                }
340

341
                return $this->dir . '/_' . $file;
1✔
342
        }
343

344

345
        /**
346
         * Deletes and closes file.
347
         * @param  resource  $handle
348
         */
349
        private static function delete(string $file, $handle = null): void
1✔
350
        {
351
                if (@unlink($file)) { // @ - file may not already exist
1✔
352
                        if ($handle) {
1✔
353
                                flock($handle, LOCK_UN);
1✔
354
                                fclose($handle);
1✔
355
                        }
356

357
                        return;
1✔
358
                }
359

360
                if (!$handle) {
×
361
                        $handle = @fopen($file, 'r+'); // @ - file may not exist
×
362
                }
363

364
                if (!$handle) {
×
365
                        return;
×
366
                }
367

368
                flock($handle, LOCK_EX);
×
369
                ftruncate($handle, 0);
×
370
                flock($handle, LOCK_UN);
×
371
                fclose($handle);
×
372
                @unlink($file); // @ - file may not already exist
×
373
        }
374
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc