Passed
Push — master ( 8666b2...9e6899 )
by Hong
04:52 queued 02:01
created

FileAdaptor::garbageCollect()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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