1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace Yiisoft\Log\Target\File; |
||||
6 | |||||
7 | use InvalidArgumentException; |
||||
8 | use RuntimeException; |
||||
9 | use Yiisoft\Files\FileHelper; |
||||
10 | |||||
11 | use function chmod; |
||||
12 | use function extension_loaded; |
||||
13 | use function fclose; |
||||
14 | use function feof; |
||||
15 | use function file_exists; |
||||
16 | use function filesize; |
||||
17 | use function flock; |
||||
18 | use function fread; |
||||
19 | use function ftruncate; |
||||
20 | use function is_file; |
||||
21 | use function rename; |
||||
22 | use function sprintf; |
||||
23 | use function substr; |
||||
24 | use function unlink; |
||||
25 | |||||
26 | use const LOCK_EX; |
||||
27 | use const LOCK_SH; |
||||
28 | use const LOCK_UN; |
||||
29 | |||||
30 | /** |
||||
31 | * FileRotator takes care of rotating files. |
||||
32 | * |
||||
33 | * If the size of the file exceeds {@see FileRotator::$maxFileSize} (in kilo-bytes), |
||||
34 | * a rotation will be performed, which renames the current file by suffixing the file name with '.1'. |
||||
35 | * |
||||
36 | * All existing files are moved backwards by one place, i.e., '.2' to '.3', '.1' to '.2', and so on. If compression |
||||
37 | * is enabled {@see FileRotator::$compressRotatedFiles}, the rotated files will be compressed into the '.gz' format. |
||||
38 | * The property {@see FileRotator::$maxFiles} specifies how many history files to keep. |
||||
39 | */ |
||||
40 | final class FileRotator implements FileRotatorInterface |
||||
41 | { |
||||
42 | /** |
||||
43 | * The extension of the compressed rotated files. |
||||
44 | */ |
||||
45 | private const COMPRESS_EXTENSION = '.gz'; |
||||
46 | |||||
47 | /** |
||||
48 | * @var int The maximum file size, in kilo-bytes. Defaults to 10240, meaning 10MB. |
||||
49 | */ |
||||
50 | private int $maxFileSize; |
||||
51 | |||||
52 | /** |
||||
53 | * @var int The number of files used for rotation. Defaults to 5. |
||||
54 | */ |
||||
55 | private int $maxFiles; |
||||
56 | |||||
57 | /** |
||||
58 | * @var bool Whether to compress rotated files with gzip. Defaults to `false`. |
||||
59 | * |
||||
60 | * If compression is enabled, the rotated files will be compressed into the '.gz' format. |
||||
61 | */ |
||||
62 | private bool $compressRotatedFiles; |
||||
63 | |||||
64 | /** |
||||
65 | * @param int $maxFileSize The maximum file size, in kilobytes. Defaults to 10240, meaning 10MB. |
||||
66 | * @param int $maxFiles The number of files used for rotation. Defaults to 5. |
||||
67 | * @param int|null $fileMode The permission to be set for newly created files. This value will be used by PHP |
||||
68 | * `chmod()` function. No umask will be applied. If not set, the permission will be determined by the current |
||||
69 | * environment. |
||||
70 | * @param bool $compressRotatedFiles Whether to compress rotated files with gzip. |
||||
71 | */ |
||||
72 | 10 | public function __construct( |
|||
73 | int $maxFileSize = 10240, |
||||
74 | int $maxFiles = 5, |
||||
75 | private ?int $fileMode = null, |
||||
76 | bool $compressRotatedFiles = false |
||||
77 | ) { |
||||
78 | 10 | $this->checkCannotBeLowerThanOne($maxFileSize, '$maxFileSize'); |
|||
79 | 9 | $this->checkCannotBeLowerThanOne($maxFiles, '$maxFiles'); |
|||
80 | |||||
81 | 8 | $this->maxFileSize = $maxFileSize; |
|||
82 | 8 | $this->maxFiles = $maxFiles; |
|||
83 | |||||
84 | 8 | if ($compressRotatedFiles && !extension_loaded('zlib')) { |
|||
85 | throw new RuntimeException(sprintf( |
||||
86 | 'The %s requires the PHP extension "ext-zlib" to compress rotated files.', |
||||
87 | self::class, |
||||
88 | )); |
||||
89 | } |
||||
90 | |||||
91 | 8 | $this->compressRotatedFiles = $compressRotatedFiles; |
|||
92 | } |
||||
93 | |||||
94 | 4 | public function rotateFile(string $file): void |
|||
95 | { |
||||
96 | 4 | for ($i = $this->maxFiles; $i >= 0; --$i) { |
|||
97 | // `$i === 0` is the original file |
||||
98 | 4 | $rotateFile = $file . ($i === 0 ? '' : '.' . $i); |
|||
99 | 4 | $newFile = $file . '.' . ($i + 1); |
|||
100 | |||||
101 | 4 | if ($i === $this->maxFiles) { |
|||
102 | 4 | $this->safeRemove($this->compressRotatedFiles ? $rotateFile . self::COMPRESS_EXTENSION : $rotateFile); |
|||
103 | 4 | continue; |
|||
104 | } |
||||
105 | |||||
106 | 4 | if ($this->compressRotatedFiles && is_file($rotateFile . self::COMPRESS_EXTENSION)) { |
|||
107 | 1 | $this->rotate($rotateFile . self::COMPRESS_EXTENSION, $newFile . self::COMPRESS_EXTENSION); |
|||
108 | 1 | continue; |
|||
109 | } |
||||
110 | |||||
111 | 4 | if (!is_file($rotateFile)) { |
|||
112 | 4 | continue; |
|||
113 | } |
||||
114 | |||||
115 | 4 | $this->rotate($rotateFile, $newFile); |
|||
116 | |||||
117 | 4 | if ($i === 0) { |
|||
118 | 4 | $this->clear($rotateFile); |
|||
119 | } |
||||
120 | } |
||||
121 | } |
||||
122 | |||||
123 | 7 | public function shouldRotateFile(string $file): bool |
|||
124 | { |
||||
125 | 7 | return file_exists($file) && @filesize($file) > ($this->maxFileSize * 1024); |
|||
126 | } |
||||
127 | |||||
128 | /*** |
||||
129 | * Renames rotated file into new file. |
||||
130 | * |
||||
131 | * @param string $rotateFile |
||||
132 | * @param string $newFile |
||||
133 | */ |
||||
134 | 4 | private function rotate(string $rotateFile, string $newFile): void |
|||
135 | { |
||||
136 | 4 | $this->safeRemove($newFile); |
|||
137 | 4 | rename($rotateFile, $newFile); |
|||
138 | |||||
139 | 4 | if ($this->compressRotatedFiles && !$this->isCompressed($newFile)) { |
|||
140 | 2 | $this->compress($newFile); |
|||
141 | 2 | $newFile .= self::COMPRESS_EXTENSION; |
|||
142 | } |
||||
143 | |||||
144 | 4 | if ($this->fileMode !== null) { |
|||
145 | 2 | chmod($newFile, $this->fileMode); |
|||
146 | } |
||||
147 | } |
||||
148 | |||||
149 | /** |
||||
150 | * Compresses a file with gzip and renames it by appending `.gz` to the file. |
||||
151 | */ |
||||
152 | 2 | private function compress(string $file): void |
|||
153 | { |
||||
154 | 2 | $filePointer = FileHelper::openFile($file, 'rb'); |
|||
155 | 2 | flock($filePointer, LOCK_SH); |
|||
156 | 2 | $gzFile = $file . self::COMPRESS_EXTENSION; |
|||
157 | 2 | $gzFilePointer = gzopen($gzFile, 'wb9'); |
|||
158 | |||||
159 | 2 | while (!feof($filePointer)) { |
|||
160 | 2 | gzwrite($gzFilePointer, fread($filePointer, 8192)); |
|||
161 | } |
||||
162 | |||||
163 | 2 | flock($filePointer, LOCK_UN); |
|||
164 | 2 | fclose($filePointer); |
|||
165 | 2 | gzclose($gzFilePointer); |
|||
166 | 2 | @unlink($file); |
|||
0 ignored issues
–
show
|
|||||
167 | } |
||||
168 | |||||
169 | /*** |
||||
170 | * Clears the file without closing any other process open handles. |
||||
171 | * |
||||
172 | * @param string $file |
||||
173 | */ |
||||
174 | 4 | private function clear(string $file): void |
|||
175 | { |
||||
176 | 4 | $filePointer = FileHelper::openFile($file, 'ab'); |
|||
177 | |||||
178 | 4 | flock($filePointer, LOCK_EX); |
|||
179 | 4 | ftruncate($filePointer, 0); |
|||
180 | 4 | flock($filePointer, LOCK_UN); |
|||
181 | 4 | fclose($filePointer); |
|||
182 | } |
||||
183 | |||||
184 | /** |
||||
185 | * Checks the existence of file and removes it. |
||||
186 | */ |
||||
187 | 4 | private function safeRemove(string $file): void |
|||
188 | { |
||||
189 | 4 | if (is_file($file)) { |
|||
190 | 2 | @unlink($file); |
|||
0 ignored issues
–
show
It seems like you do not handle an error condition for
unlink() . This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
![]() |
|||||
191 | } |
||||
192 | } |
||||
193 | |||||
194 | /** |
||||
195 | * Whether the file is compressed. |
||||
196 | */ |
||||
197 | 2 | private function isCompressed(string $file): bool |
|||
198 | { |
||||
199 | 2 | return substr($file, -3, 3) === self::COMPRESS_EXTENSION; |
|||
200 | } |
||||
201 | |||||
202 | /** |
||||
203 | * Checks that the value cannot be lower than one. |
||||
204 | * |
||||
205 | * @param int $value The value to be checked. |
||||
206 | * @param string $argumentName The name of the argument to check. |
||||
207 | */ |
||||
208 | 10 | private function checkCannotBeLowerThanOne(int $value, string $argumentName): void |
|||
209 | { |
||||
210 | 10 | if ($value < 1) { |
|||
211 | 2 | throw new InvalidArgumentException(sprintf( |
|||
212 | 2 | 'The argument "%s" cannot be lower than 1.', |
|||
213 | 2 | $argumentName, |
|||
214 | 2 | )); |
|||
215 | } |
||||
216 | } |
||||
217 | } |
||||
218 |
If you suppress an error, we recommend checking for the error condition explicitly: