Passed
Push — master ( 0d7705...62fa37 )
by Yannick
09:28
created

TempUploadHelper::purge()   C

Complexity

Conditions 15
Paths 21

Size

Total Lines 52
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 15
eloc 30
c 1
b 0
f 1
nc 21
nop 3
dl 0
loc 52
rs 5.9166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Helpers;
8
9
use Symfony\Component\DependencyInjection\Attribute\Autowire;
10
11
/**
12
 * Cleans temporary upload artifacts inside %kernel.project_dir%/var/cache
13
 * while SAFELY skipping Symfony's own cache directories/files.
14
 */
15
final class TempUploadHelper
16
{
17
    /** @var string[] Top-level directories to skip under var/cache */
18
    private array $excludeTop = ['dev', 'prod', 'test', 'pools'];
19
20
    /** @var string[] Regex patterns to skip anywhere under var/cache */
21
    private array $excludePatterns = [
22
        '#/(vich_uploader|twig|doctrine|http_cache|profiler)/#i',
23
        '#/jms_[^/]+/#i',
24
    ];
25
26
    public function __construct(
27
        #[Autowire('%kernel.project_dir%/var/cache')]
28
        private readonly string $tempUploadDir
29
    ) {}
30
31
    public function getTempDir(): string
32
    {
33
        return rtrim($this->tempUploadDir, DIRECTORY_SEPARATOR);
34
    }
35
36
    /**
37
     * Stats for files that WOULD be targeted (i.e., excluding Symfony cache).
38
     * @return array{files:int,bytes:int}
39
     */
40
    public function stats(): array
41
    {
42
        $dir = $this->getTempDir();
43
        $this->assertBaseDir($dir);
44
45
        $files = 0; $bytes = 0;
46
47
        if (!is_dir($dir) || !is_readable($dir)) {
48
            return ['files' => 0, 'bytes' => 0];
49
        }
50
51
        $it = new \RecursiveIteratorIterator(
52
            new \RecursiveDirectoryIterator(
53
                $dir,
54
                \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS
55
            ),
56
            \RecursiveIteratorIterator::SELF_FIRST
57
        );
58
59
        foreach ($it as $f) {
60
            if ($this->isExcluded($dir, $f)) {
61
                // Skip whole excluded subtree quickly
62
                if ($f->isDir()) {
63
                    $it->next(); // let iterator move on
64
                }
65
                continue;
66
            }
67
            if ($f->isFile()) {
68
                $bn = $f->getBasename();
69
                if ($this->isProtected($bn)) {
70
                    continue;
71
                }
72
                $files++;
73
                $bytes += (int) $f->getSize();
74
            }
75
        }
76
77
        return ['files' => $files, 'bytes' => $bytes];
78
    }
79
80
    /**
81
     * Purge target files (excluding Symfony cache) older than $olderThanMinutes.
82
     * If $olderThanMinutes = 0, delete all target files.
83
     * If $dryRun = true, do not delete; only count.
84
     *
85
     * If $strict = true, DO NOT exclude Symfony cache: dangerous; use only
86
     * for manual maintenance and ensure proper permissions afterwards.
87
     *
88
     * @return array{files:int,bytes:int}
89
     */
90
    public function purge(int $olderThanMinutes = 0, bool $dryRun = false, bool $strict = false): array
91
    {
92
        $dir = $this->getTempDir();
93
        $this->assertBaseDir($dir);
94
95
        $deleted = 0; $bytes = 0;
96
97
        if (!is_dir($dir) || !is_readable($dir)) {
98
            return ['files' => 0, 'bytes' => 0];
99
        }
100
101
        $cutoff = $olderThanMinutes > 0 ? (time() - $olderThanMinutes * 60) : null;
102
103
        $rii = new \RecursiveIteratorIterator(
104
            new \RecursiveDirectoryIterator(
105
                $dir,
106
                \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS
107
            ),
108
            \RecursiveIteratorIterator::CHILD_FIRST
109
        );
110
111
        foreach ($rii as $f) {
112
            if (!$strict && $this->isExcluded($dir, $f)) {
113
                // Skip excluded subtree
114
                if ($f->isDir()) {
115
                    $rii->next();
116
                }
117
                continue;
118
            }
119
120
            $bn = $f->getBasename();
121
            if ($this->isProtected($bn)) {
122
                continue;
123
            }
124
125
            if ($f->isFile()) {
126
                if (null !== $cutoff && $f->getMTime() > $cutoff) {
127
                    continue;
128
                }
129
                $bytes += (int) $f->getSize();
130
                if (!$dryRun) {
131
                    @unlink($f->getPathname());
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

131
                    /** @scrutinizer ignore-unhandled */ @unlink($f->getPathname());

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...
132
                }
133
                $deleted++;
134
            } elseif ($f->isDir()) {
135
                if (!$dryRun) {
136
                    @rmdir($f->getPathname()); // best-effort (only if empty)
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rmdir(). 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

136
                    /** @scrutinizer ignore-unhandled */ @rmdir($f->getPathname()); // best-effort (only if empty)

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...
137
                }
138
            }
139
        }
140
141
        return ['files' => $deleted, 'bytes' => $bytes];
142
    }
143
144
    private function isProtected(string $basename): bool
145
    {
146
        return $basename === '.htaccess' || $basename === '.gitignore';
147
    }
148
149
    /**
150
     * Prevent catastrophes and ensure directory exists & is writable.
151
     */
152
    private function assertBaseDir(string $dir): void
153
    {
154
        // Ensure base exists
155
        if (!is_dir($dir)) {
156
            @mkdir($dir, 0775, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). 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

156
            /** @scrutinizer ignore-unhandled */ @mkdir($dir, 0775, true);

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...
157
        }
158
        if (!is_writable($dir)) {
159
            throw new \InvalidArgumentException(sprintf('Temp dir not writable: %s', $dir));
160
        }
161
    }
162
163
    /**
164
     * Decide if a file/dir should be excluded from cleanup.
165
     */
166
    private function isExcluded(string $base, \SplFileInfo $f): bool
167
    {
168
        $path = $f->getPathname();
169
        $rel  = ltrim(str_replace('\\', '/', substr($path, strlen($base))), '/');
170
171
        // Top-level directory name?
172
        $first = explode('/', $rel, 2)[0] ?? '';
173
        if ($first !== '' && in_array($first, $this->excludeTop, true)) {
174
            return true;
175
        }
176
177
        // Pattern matches anywhere in the path
178
        foreach ($this->excludePatterns as $re) {
179
            if (preg_match($re, '/'.$rel.'/')) {
180
                return true;
181
            }
182
        }
183
184
        return false;
185
    }
186
}
187