Completed
Push — master ( 59e972...112aa3 )
by Hong
03:17 queued 01:31
created

Filesystem::delete()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10

Duplication

Lines 5
Ratio 50 %

Importance

Changes 0
Metric Value
dl 5
loc 10
c 0
b 0
f 0
rs 9.9332
cc 3
nc 2
nop 1
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)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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