Completed
Pull Request — master (#1)
by Dorian
02:14
created

FilesystemManager::skip()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
1
<?php declare(strict_types=1);
2
3
namespace App\Filesystem;
4
5
use App\Domain\Downloads;
6
use App\Domain\Path;
7
use App\Domain\Download;
8
use App\UI\Skippable;
9
use App\UI\UserInterface;
10
use Symfony\Component\Filesystem\Filesystem;
11
use Symfony\Component\Finder\Finder;
12
13
abstract class FilesystemManager
14
{
15
    use Skippable;
16
17
    /** @var \App\UI\UserInterface */
18
    protected $ui;
19
20
    /**
21
     * @param \App\UI\UserInterface $ui
22
     */
23
    public function __construct(UserInterface $ui)
24
    {
25
        $this->ui = $ui;
26
    }
27
28
    /**
29
     * @param \App\Domain\Path $downloadPath
30
     *
31
     * @return \Symfony\Component\Finder\Finder
32
     * @throws \InvalidArgumentException
33
     */
34
    protected function getAllDownloadsFolderFinder(Path $downloadPath): Finder
35
    {
36
        return (new Finder())
37
            ->directories()
38
            ->in((string) $downloadPath)
39
            ->sort(function (\SplFileInfo $fileInfoA, \SplFileInfo $fileInfoB) {
40
                // Sort the result by folder depth
41
                $a = substr_count($fileInfoA->getRealPath(), DIRECTORY_SEPARATOR);
42
                $b = substr_count($fileInfoB->getRealPath(), DIRECTORY_SEPARATOR);
43
44
                return $a <=> $b;
45
            });
46
    }
47
48
    /**
49
     * @param \App\Domain\Download $download
50
     *
51
     * @return string
52
     */
53
    abstract protected function getDownloadFolder(Download $download): string;
54
55
    /**
56
     * @param \App\Domain\Download $download
57
     *
58
     * @return \Symfony\Component\Finder\Finder|\SplFileInfo[]
59
     * @throws \InvalidArgumentException
60
     */
61
    protected function getDownloadFolderFinder(Download $download): Finder
62
    {
63
        return (new Finder())
64
            ->files()
65
            ->depth('== 0')
66
            ->in($this->getDownloadFolder($download));
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     * @param \App\Domain\Downloads $downloads
72
     *
73
     * @throws \RuntimeException
74
     */
75
    protected function cleanFilesystem(Downloads $downloads, Path $downloadPath): void
76
    {
77
        $foldersToRemove = $this->getFoldersToRemove($downloads, $downloadPath);
78
79
        if ($this->shouldRemoveFolders($foldersToRemove, $downloadPath)) {
80
            $this->removeFolders($foldersToRemove);
81
        }
82
    }
83
84
    /**
85
     * @param \App\Domain\Downloads $downloads
86
     * @param \App\Domain\Path $downloadPath
87
     *
88
     * @return \App\Filesystem\FilesystemObjects
89
     * @throws \RuntimeException
90
     */
91
    protected function getFoldersToRemove(Downloads $downloads, Path $downloadPath): FilesystemObjects
92
    {
93
        $foldersToRemove = new FilesystemObjects();
0 ignored issues
show
Bug introduced by
The call to FilesystemObjects::__construct() misses a required argument $elements.

This check looks for function calls that miss required arguments.

Loading history...
94
        try {
95
            $completedDownloadsFolders = $this->getCompletedDownloadsFolders($downloads);
96
97
            foreach ($this->getAllDownloadsFolderFinder($downloadPath)->getIterator() as $folder) {
98
99
                // If the folder has already been tagged for removal, we skip it
100
                if ($this->isFolderInCollection($folder, $foldersToRemove, true, $downloadPath)) {
101
                    continue;
102
                }
103
104
                // If the folder contains downloaded files, we skip it
105
                if ($this->isFolderInCollection($folder, $completedDownloadsFolders)) {
106
                    continue;
107
                }
108
109
                $foldersToRemove->add($folder);
110
            }
111
        } catch (\LogicException $e) {
112
            // Here we know that the download folder will exist.
113
        }
114
115
        return $foldersToRemove;
116
    }
117
118
    /**
119
     * Checks if a folder (or one of its parent, up to the $limit parameter) is found in the collection of folders.
120
     *
121
     * @param \SplFileInfo $folderToSearchFor
122
     * @param \App\Filesystem\FilesystemObjects $folders
123
     * @param bool $loopOverParentsFolders
124
     * @param \App\Domain\Path $untilPath
125
     *
126
     * @return bool
127
     * @throws \RuntimeException
128
     */
129
    protected function isFolderInCollection(
130
        \SplFileInfo $folderToSearchFor,
131
        FilesystemObjects $folders,
132
        bool $loopOverParentsFolders = false,
133
        ?Path $untilPath = null
134
    ): bool {
135
        foreach ($folders as $folder) {
136
            do {
137
                // This allows to match "/root/path" in "/root/path" or "/root/path/sub_path"
138
                if (0 === strpos($folder->getRealPath(), $folderToSearchFor->getRealPath())) {
139
                    return true;
140
                }
141
142
                if (!$loopOverParentsFolders) {
143
                    break;
144
                }
145
                if (null === $untilPath) {
146
                    throw new \RuntimeException(
147
                        'If $loopOverParentsFolders is set to true, then $untilPath must be provided.'.
148
                        'Otherwise you will experience infinite loops.'
149
                    );
150
                }
151
152
                $folderToSearchFor = $folderToSearchFor->getPathInfo();
153
154
            } while ($folderToSearchFor->getRealPath() !== (string) $untilPath);
155
        }
156
157
        return false;
158
    }
159
160
    /**
161
     * @param \App\Filesystem\FilesystemObjects $foldersToRemove
162
     */
163
    protected function removeFolders(FilesystemObjects $foldersToRemove): void
164
    {
165
        $errors = [];
166
        foreach ($foldersToRemove as $folderToRemove) {
167
            $relativeFolderPath = $folderToRemove->getRelativePathname();
168
169
            try {
170
                (new Filesystem())->remove($folderToRemove->getRealPath());
171
172
                $this->ui->writeln(
173
                    sprintf(
174
                        '%s* The folder <info>%s</info> has been removed.',
175
                        $this->ui->indent(2),
176
                        $relativeFolderPath
177
                    )
178
                );
179
            } catch (\Exception $e) {
180
                $this->ui->logError(
181
                    sprintf(
182
                        '%s* <error>The folder %s could not be removed.</error>',
183
                        $this->ui->indent(2),
184
                        $relativeFolderPath
185
                    ),
186
                    $errors
187
                );
188
            }
189
        }
190
        $this->ui->displayErrors($errors, 'the removal of folders', 'info', 1);
191
192
        $this->ui->writeln(PHP_EOL.'<info>Done.</info>'.PHP_EOL);
193
    }
194
195
    /**
196
     * @param \App\Domain\Downloads $downloads
197
     *
198
     * @return \App\Filesystem\FilesystemObjects
199
     */
200
    protected function getCompletedDownloadsFolders(Downloads $downloads): FilesystemObjects
201
    {
202
        $completedDownloadsFolders = new FilesystemObjects();
0 ignored issues
show
Bug introduced by
The call to FilesystemObjects::__construct() misses a required argument $elements.

This check looks for function calls that miss required arguments.

Loading history...
203
        foreach ($downloads as $download) {
204
            try {
205
                foreach ($this->getDownloadFolderFinder($download) as $downloadFolder) {
206
                    $completedDownloadsFolders->add($downloadFolder->getPathInfo());
207
                }
208
            } catch (\InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
209
            }
210
        }
211
212
        return $completedDownloadsFolders;
213
    }
214
215
    /**
216
     * @param \App\Filesystem\FilesystemObjects $foldersToRemove
217
     * @param \App\Domain\Path $downloadPath
218
     *
219
     * @return bool
220
     */
221
    protected function shouldRemoveFolders(FilesystemObjects $foldersToRemove, Path $downloadPath): bool
222
    {
223
        $this->ui->write(
224
            sprintf(
225
                'Synchronize the <info>%s</info> folder with the downloaded contents... ',
226
                (string) $downloadPath
227
            )
228
        );
229
230
        if ($foldersToRemove->isEmpty()) {
231
            $this->ui->writeln('<info>Done.</info>');
232
233
            return false;
234
        }
235
236
        $this->ui->writeln(PHP_EOL);
237
238
        if (!$this->ui->isDryRun() && !$this->ui->isInteractive()) {
239
            return true;
240
        }
241
242
        $confirmationDefault = true;
243
244
        // If there's less than 10 folders, we can display them
245
        $nbFoldersToRemove = $foldersToRemove->count();
246
        if ($nbFoldersToRemove <= 10) {
247
            $this->ui->writeln(
248
                sprintf(
249
                    '%sThe script is about to remove the following folders from <info>%s</info>:',
250
                    $this->ui->indent(),
251
                    (string) $downloadPath
252
                )
253
            );
254
            $this->ui->listing(
255
                $foldersToRemove
256
                    ->map(function (\SplFileInfo $folder) use ($downloadPath) {
257
                        return sprintf(
258
                            '<info>%s</info>',
259
                            str_replace((string) $downloadPath.DIRECTORY_SEPARATOR, '', $folder->getRealPath())
260
                        );
261
                    })
262
                    ->toArray(),
263
                3
264
            );
265
        } else {
266
            $confirmationDefault = false;
267
268
            $this->ui->write(
269
                sprintf(
270
                    '%sThe script is about to remove <question> %s </question> folders from <info>%s</info>. ',
271
                    $this->ui->indent(),
272
                    $nbFoldersToRemove,
273
                    (string) $downloadPath
274
                )
275
            );
276
        }
277
278
        $this->ui->write($this->ui->indent());
279
280
        if ($this->skip($this->ui) || !$this->ui->confirm($confirmationDefault)) {
281
            $this->ui->writeln(($this->ui->isDryRun() ? '' : PHP_EOL).'<info>Done.</info>'.PHP_EOL);
282
283
            return false;
284
        }
285
286
        return true;
287
    }
288
}
289