Completed
Push — master ( effccc...1d6376 )
by Dorian
01:23
created

FilesystemManager::getAllDownloadsFolderFinder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 1
1
<?php declare(strict_types=1);
2
3
namespace App\Platform\YouTube;
4
5
use App\Domain\Collection;
6
use App\Domain\Path;
7
use App\Domain\PathPart;
8
use Symfony\Component\Filesystem\Filesystem;
9
use Symfony\Component\Finder\Finder;
10
11
trait FilesystemManager
12
{
13
    use DryRunner;
14
15
    /** @var array */
16
    private $options;
17
18
    /**
19
     * @param \App\Domain\Path $downloadPath
20
     *
21
     * @return \Symfony\Component\Finder\Finder
22
     */
23
    private function getAllDownloadsFolderFinder(Path $downloadPath): Finder
24
    {
25
        /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
26
        return (new Finder())
27
            ->directories()
28
            ->in((string) $downloadPath)
29
            ->sort(function (\SplFileInfo $fileInfoA, \SplFileInfo $fileInfoB) {
30
                // Sort the result by folder depth
31
                $a = substr_count($fileInfoA->getRealPath(), DIRECTORY_SEPARATOR);
32
                $b = substr_count($fileInfoB->getRealPath(), DIRECTORY_SEPARATOR);
33
34
                return $a <=> $b;
35
            });
36
    }
37
38
    /**
39
     * @param \App\Platform\YouTube\Download $download
40
     *
41
     * @return \Symfony\Component\Finder\Finder|\SplFileInfo[]
42
     * @throws \InvalidArgumentException
43
     */
44
    private function getDownloadFolderFinder(Download $download): Finder
45
    {
46
        $placeholders = [
47
            '%video_id%' => $download->getVideoId(),
48
            '%file_extension%' => $download->getFileExtension(),
49
        ];
50
51
        $downloadPathPart = new PathPart([
52
            'path' => (string) $download->getPath(),
53
            'priority' => 0,
54
        ]);
55
        $folderPathPart = new PathPart([
56
            'path' => $this->options['patterns']['folder'],
57
            'priority' => 1,
58
            'substitutions' => $placeholders,
59
        ]);
60
61
        return (new Finder())
62
            ->files()
63
            ->depth('== 0')
64
            ->in((string) new Path([$downloadPathPart, $folderPathPart]))
65
            ->name(
66
                str_replace(
67
                    array_keys($placeholders),
68
                    array_values($placeholders),
69
                    $this->options['patterns']['filename']
70
                )
71
            );
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     * @param \App\Platform\YouTube\Download[]|\App\Domain\Collection $downloads
77
     *
78
     * @throws \RuntimeException
79
     */
80
    private function cleanFilesystem(Collection $downloads, Path $downloadPath): void
81
    {
82
        $this->ui->write(
83
            sprintf(
84
                'Synchronize the <info>%s</info> folder with the downloaded contents... ',
85
                (string) $downloadPath
86
            )
87
        );
88
89
        $foldersToRemove = new Collection();
90
        try {
91
            $completedDownloadsFolders = $this->getCompletedDownloadsFolders($downloads);
92
93
            foreach ($this->getAllDownloadsFolderFinder($downloadPath)->getIterator() as $folder) {
94
                if (!$this->isFolderInCollection($folder, $foldersToRemove, true, $downloadPath) &&
95
                    !$this->isFolderInCollection($folder, $completedDownloadsFolders)
1 ignored issue
show
Bug introduced by
It seems like $completedDownloadsFolders defined by $this->getCompletedDownloadsFolders($downloads) on line 91 can also be of type array<integer,object<SplFileInfo>>; however, App\Platform\YouTube\Fil...:isFolderInCollection() does only seem to accept object<App\Domain\Collection>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
96
                ) {
97
                    $foldersToRemove->add($folder);
98
                }
99
            }
100
        } catch (\LogicException $e) {
101
            // Here we know that the download folder will exist.
102
        }
103
104
        $hasRemovedFolders = $this->removeFolders($foldersToRemove, $downloadPath);
105
106
        $newLine = $hasRemovedFolders ? PHP_EOL : '';
107
        $this->ui->writeln($newLine.'<info>Done.</info>'.$newLine);
108
    }
109
110
    /**
111
     * Checks if a folder (or one of its parent, up to the $limit parameter) is found in the collection of folders.
112
     *
113
     * @param \SplFileInfo $folderToSearchFor
114
     * @param \SplFileInfo[]|\App\Domain\Collection $folders
115
     * @param bool $loopOverParentsFolders
116
     * @param Path $untilPath
117
     *
118
     * @return bool
119
     * @throws \RuntimeException
120
     */
121
    private function isFolderInCollection(
122
        \SplFileInfo $folderToSearchFor,
123
        Collection $folders,
124
        bool $loopOverParentsFolders = false,
125
        ?Path $untilPath = null
126
    ): bool {
127
        foreach ($folders as $folder) {
128
            do {
129
                // This allows to match "/root/path" in "/root/path" or "/root/path/sub_path"
130
                if (0 === strpos($folder->getRealPath(), $folderToSearchFor->getRealPath())) {
131
                    return true;
132
                }
133
134
                if (!$loopOverParentsFolders) {
135
                    break;
136
                }
137
                if (null === $untilPath) {
138
                    throw new \RuntimeException(
139
                        'If $loopOverParentsFolders is set to true, then $untilPath must be provided.'.
140
                        'Otherwise you will experience infinite loops.'
141
                    );
142
                }
143
144
                $folderToSearchFor = $folderToSearchFor->getPathInfo();
145
146
            } while ($folderToSearchFor->getRealPath() !== (string) $untilPath);
147
        }
148
149
        return false;
150
    }
151
152
    /**
153
     * @param \SplFileInfo[]|\App\Domain\Collection $foldersToRemove
154
     * @param \App\Domain\Path $downloadPath
155
     *
156
     * @return bool Whether folders were removed or not.
157
     */
158
    private function removeFolders(Collection $foldersToRemove, Path $downloadPath): bool
159
    {
160
        $foldersWereRemoved = false;
161
162
        if (!$this->shouldRemoveFolders($foldersToRemove, $downloadPath)) {
163
            return $foldersWereRemoved;
164
        }
165
166
        $errors = [];
167
        foreach ($foldersToRemove as $folderToRemove) {
168
            $relativeFolderPath = $folderToRemove->getRelativePathname();
169
170
            try {
171
                (new Filesystem())->remove($folderToRemove->getRealPath());
172
173
                $foldersWereRemoved = true;
174
175
                $this->ui->writeln(
176
                    sprintf(
177
                        '%s* The folder <info>%s</info> has been removed.',
178
                        $this->ui->indent(2),
179
                        $relativeFolderPath
180
                    )
181
                );
182
            } catch (\Exception $e) {
183
                $this->ui->logError(
184
                    sprintf(
185
                        '%s* <error>The folder %s could not be removed.</error>',
186
                        $this->ui->indent(2),
187
                        $relativeFolderPath
188
                    ),
189
                    $errors
190
                );
191
            }
192
        }
193
        $this->ui->displayErrors($errors, 'the removal of folders', 'info', 1);
194
195
        return $foldersWereRemoved;
196
    }
197
198
    /**
199
     * @param \App\Platform\YouTube\Download[]|\App\Domain\Collection $downloads
200
     *
201
     * @return \SplFileInfo[]|\App\Domain\Collection
202
     */
203
    private function getCompletedDownloadsFolders(Collection $downloads): Collection
204
    {
205
        $completedDownloadsFolders = new Collection();
206
        foreach ($downloads as $download) {
207
            try {
208
                foreach ($this->getDownloadFolderFinder($download) as $downloadFolder) {
209
                    $parentFolder = $downloadFolder->getPathInfo();
210
211
                    // Using a key ensures there's no duplicates
212
                    $completedDownloadsFolders->set($parentFolder->getRealPath(), $parentFolder);
213
                }
214
            } catch (\InvalidArgumentException $e) {
215
            }
216
        }
217
218
        return $completedDownloadsFolders;
219
    }
220
221
    /**
222
     * @param \SplFileInfo[]|\App\Domain\Collection $foldersToRemove
223
     * @param \App\Domain\Path $downloadPath
224
     *
225
     * @return bool
226
     */
227
    private function shouldRemoveFolders(Collection $foldersToRemove, Path $downloadPath): bool
228
    {
229
        $nbFoldersToRemove = $foldersToRemove->count();
230
        if (empty($nbFoldersToRemove)) {
231
            return false;
232
        }
233
234
        $this->ui->writeln(PHP_EOL);
235
236
        $confirmationDefault = true;
237
238
        // If there's less than 10 folders, we can display them
239
        if ($nbFoldersToRemove <= 10) {
240
            $this->ui->writeln(
241
                sprintf(
242
                    '%sThe script is about to remove the following folders from <info>%s</info>:',
243
                    $this->ui->indent(),
244
                    (string) $downloadPath
245
                )
246
            );
247
            $this->ui->listing(
248
                $foldersToRemove
249
                    ->map(function (\SplFileInfo $folder) use ($downloadPath) {
250
                        return sprintf(
251
                            '<info>%s</info>',
252
                            str_replace((string) $downloadPath.DIRECTORY_SEPARATOR, '', $folder->getRealPath())
253
                        );
254
                    })
255
                    ->toArray(),
256
                3
257
            );
258
        } else {
259
            $confirmationDefault = false;
260
261
            $this->ui->write(
262
                sprintf(
263
                    '%sThe script is about to remove <question> %s </question> folders from <info>%s</info>. ',
264
                    $this->ui->indent(),
265
                    $nbFoldersToRemove,
266
                    (string) $downloadPath
267
                )
268
            );
269
        }
270
271
        $this->ui->write($this->ui->indent());
272
273
        return !($this->skip() || !$this->ui->confirm($confirmationDefault));
274
    }
275
}
276