Passed
Push — master ( e9b6c8...b0c53b )
by Evgeniy
01:55
created

FileRotator::checkCannotBeLowerThanOne()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Log\Target\File;
6
7
use InvalidArgumentException;
8
use RuntimeException;
9
10
use function chmod;
11
use function copy;
12
use function fclose;
13
use function file_exists;
14
use function filesize;
15
use function flock;
16
use function fopen;
17
use function ftruncate;
18
use function is_file;
19
use function rename;
20
use function sprintf;
21
use function unlink;
22
23
use const DIRECTORY_SEPARATOR;
24
use const LOCK_EX;
25
use const LOCK_UN;
26
27
/**
28
 * FileRotator takes care of rotating files.
29
 *
30
 * If the size of the file exceeds {@see FileRotator::$maxFileSize} (in kilo-bytes),
31
 * a rotation will be performed, which renames the current file by suffixing the file name with '.1'.
32
 *
33
 * All existing files are moved backwards by one place, i.e., '.2' to '.3', '.1' to '.2', and so on.
34
 * The property {@see FileRotator::$maxFiles} specifies how many history files to keep.
35
 */
36
final class FileRotator implements FileRotatorInterface
37
{
38
    /**
39
     * @var int The maximum file size, in kilo-bytes. Defaults to 10240, meaning 10MB.
40
     */
41
    private int $maxFileSize;
42
43
    /**
44
     * @var int The number of files used for rotation. Defaults to 5.
45
     */
46
    private int $maxFiles;
47
48
    /**
49
     * @var int|null The permission to be set for newly created files.
50
     * This value will be used by PHP chmod() function. No umask will be applied.
51
     * If not set, the permission will be determined by the current environment.
52
     */
53
    private ?int $fileMode;
54
55
    /**
56
     * @var bool|null Whether to rotate files by copy and truncate in contrast to rotation by
57
     * renaming files. Defaults to `true` to be more compatible with log tailers and is windows
58
     * systems which do not play well with rename on open files. Rotation by renaming however is
59
     * a bit faster.
60
     *
61
     * The problem with windows systems where the [rename()](http://www.php.net/manual/en/function.rename.php)
62
     * function does not work with files that are opened by some process is described in a
63
     * [comment by Martin Pelletier](http://www.php.net/manual/en/function.rename.php#102274) in
64
     * the PHP documentation. By setting rotateByCopy to `true` you can work
65
     * around this problem.
66
     */
67
    private ?bool $rotateByCopy;
68
69
    /**
70
     * @param int $maxFileSize The maximum file size, in kilo-bytes. Defaults to 10240, meaning 10MB.
71
     * @param int $maxFiles The number of files used for rotation. Defaults to 5.
72
     * @param int|null $fileMode The permission to be set for newly created files.
73
     * @param bool|null $rotateByCopy Whether to rotate files by copying and truncating or renaming them.
74
     */
75 7
    public function __construct(
76
        int $maxFileSize = 10240,
77
        int $maxFiles = 5,
78
        int $fileMode = null,
79
        bool $rotateByCopy = null
80
    ) {
81 7
        $this->checkCannotBeLowerThanOne($maxFileSize, '$maxFileSize');
82 6
        $this->checkCannotBeLowerThanOne($maxFiles, '$maxFiles');
83
84 5
        $this->maxFileSize = $maxFileSize;
85 5
        $this->maxFiles = $maxFiles;
86 5
        $this->fileMode = $fileMode;
87 5
        $this->rotateByCopy = $rotateByCopy ?? $this->isRunningOnWindows();
88 5
    }
89
90 3
    public function rotateFile(string $file): void
91
    {
92 3
        for ($i = $this->maxFiles; $i >= 0; --$i) {
93
            // `$i === 0` is the original file
94 3
            $rotateFile = $file . ($i === 0 ? '' : '.' . $i);
95 3
            if (is_file($rotateFile)) {
96
                // suppress errors because it's possible multiple processes enter into this section
97 3
                if ($i === $this->maxFiles) {
98 1
                    @unlink($rotateFile);
0 ignored issues
show
Security Best Practice introduced by
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 ignore-unhandled  annotation

98
                    /** @scrutinizer ignore-unhandled */ @unlink($rotateFile);

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.');
}
Loading history...
99 1
                    continue;
100
                }
101
102 3
                $newFile = $file . '.' . ($i + 1);
103 3
                $this->rotate($rotateFile, $newFile);
104
105 3
                if ($i === 0) {
106 3
                    $this->clear($rotateFile);
107
                }
108
            }
109
        }
110 3
    }
111
112 5
    public function shouldRotateFile(string $file): bool
113
    {
114 5
        return file_exists($file) && @filesize($file) > ($this->maxFileSize * 1024);
115
    }
116
117
    /***
118
     * Copies or renames rotated file into new file.
119
     *
120
     * @param string $rotateFile
121
     * @param string $newFile
122
     */
123 3
    private function rotate(string $rotateFile, string $newFile): void
124
    {
125 3
        if ($this->rotateByCopy !== true) {
126 1
            rename($rotateFile, $newFile);
127 1
            return;
128
        }
129
130 2
        copy($rotateFile, $newFile);
131
132 2
        if ($this->fileMode !== null) {
133 1
            chmod($newFile, $this->fileMode);
134
        }
135 2
    }
136
137
    /***
138
     * Clears the file without closing any other process open handles.
139
     *
140
     * @param string $rotateFile Rotated file.
141
     *
142
     * @throws RuntimeException For the log file could not be opened.
143
     */
144 3
    private function clear(string $rotateFile): void
145
    {
146 3
        $filePointer = @fopen($rotateFile, 'ab');
147
148 3
        if ($filePointer === false) {
149
            throw new RuntimeException(sprintf(
150
                'The log file "%s" could not be opened.',
151
                $rotateFile,
152
            ));
153
        }
154
155 3
        flock($filePointer, LOCK_EX);
156 3
        ftruncate($filePointer, 0);
157 3
        flock($filePointer, LOCK_UN);
158 3
        fclose($filePointer);
159 3
    }
160
161
    /**
162
     * Whether it works on Windows OS.
163
     *
164
     * @return bool
165
     */
166 1
    private function isRunningOnWindows(): bool
167
    {
168 1
        return DIRECTORY_SEPARATOR === '\\';
169
    }
170
171
    /**
172
     * Checks that the value cannot be lower than one.
173
     *
174
     * @param int $value The value to be checked.
175
     * @param string $argumentName The name of the argument to check.
176
     */
177 7
    private function checkCannotBeLowerThanOne(int $value, string $argumentName): void
178
    {
179 7
        if ($value < 1) {
180 2
            throw new InvalidArgumentException(sprintf(
181 2
                'The argument "%s" cannot be lower than 1.',
182 2
                $argumentName,
183
            ));
184
        }
185 6
    }
186
}
187