Cleanup::deleteFilesFromDatabase()   A
last analyzed

Complexity

Conditions 5
Paths 12

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
c 1
b 0
f 0
nc 12
nop 2
dl 0
loc 15
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AbterPhp\Files\Console\Commands\File;
6
7
use AbterPhp\Files\Domain\Entities\File as Entity;
8
use AbterPhp\Files\Orm\FileRepo;
9
use AbterPhp\Framework\Filesystem\Uploader;
10
use FilesystemIterator;
11
use Opulence\Console\Commands\Command;
12
use Opulence\Console\Requests\Option;
13
use Opulence\Console\Requests\OptionTypes;
14
use Opulence\Console\Responses\IResponse;
15
use Opulence\Console\StatusCodes;
16
use Opulence\Orm\IUnitOfWork;
17
18
class Cleanup extends Command
19
{
20
    const COMMAND_NAME            = 'files:cleanup';
21
    const COMMAND_DESCRIPTION     = 'Cleanup missing files both from database and filesystem';
22
    const COMMAND_SUCCESS         = '<success>Files are cleaned up.</success>';
23
    const COMMAND_DRY_RUN_MESSAGE = '<info>Dry run prevented deleting files and database rows.</info>';
24
25
    const OPTION_DRY_RUN    = 'dry-run';
26
    const SHORTENED_DRY_RUN = 'd';
27
28
    const DELETING_FILES    = 'Deleting files missing from database:';
29
    const DELETING_ENTITIES = 'Deleting database entities missing from filesystem:';
30
31
    const FS_ONLY = 'Files to delete: %d';
32
    const DB_ONLY = 'Database entities to delete: %d';
33
34
    /** @var FileRepo */
35
    protected $fileRepo;
36
37
    /** @var IUnitOfWork */
38
    protected $unitOfWork;
39
40
    /** @var Uploader */
41
    protected $uploader;
42
43
    /** @var array */
44
    protected $filesToSkip = ['.gitignore', '.gitkeep'];
45
46
    /**
47
     * Cleanup constructor.
48
     *
49
     * @param FileRepo    $fileRepo
50
     * @param IUnitOfWork $unitOfWork
51
     * @param Uploader    $uploader
52
     */
53
    public function __construct(
54
        FileRepo $fileRepo,
55
        IUnitOfWork $unitOfWork,
56
        Uploader $uploader
57
    ) {
58
        $this->fileRepo   = $fileRepo;
59
        $this->unitOfWork = $unitOfWork;
60
        $this->uploader   = $uploader;
61
62
        parent::__construct();
63
    }
64
65
    /**
66
     * @inheritdoc
67
     */
68
    protected function define()
69
    {
70
        $this->setName(static::COMMAND_NAME)
71
            ->setDescription(static::COMMAND_DESCRIPTION)
72
            ->addOption(
73
                new Option(
74
                    static::OPTION_DRY_RUN,
75
                    static::SHORTENED_DRY_RUN,
76
                    OptionTypes::OPTIONAL_VALUE,
77
                    'Dry run (default: 0)',
78
                    '0'
79
                )
80
            );
81
    }
82
83
    /**
84
     * @inheritdoc
85
     */
86
    protected function doExecute(IResponse $response)
87
    {
88
        $dbPaths = $this->getDatabasePaths($response);
89
        $fsPaths = $this->getFilesystemPaths();
90
91
        $fsOnly = $this->filterFilesystemOnly($response, $dbPaths, $fsPaths);
92
        $dbOnly = $this->filterDatabaseOnly($response, $dbPaths, $fsPaths);
93
94
        $dryRun = $this->isDryRun($response);
95
        if ($dryRun) {
96
            return StatusCodes::OK;
97
        }
98
99
        $this->deleteFilesFromDatabase($response, $dbOnly);
100
        $this->deleteFilesFromFilesystem($response, $fsOnly);
101
102
        $this->commit($response);
103
104
        return StatusCodes::OK;
105
    }
106
107
    /**
108
     * @param IResponse $response
109
     *
110
     * @return string[]
111
     */
112
    protected function getDatabasePaths(IResponse $response): array
113
    {
114
        try {
115
            /** @var Entity[] $entities */
116
            $entities = $this->fileRepo->getAll();
117
        } catch (\Exception $e) {
118
            if ($e->getPrevious()) {
119
                $response->writeln(sprintf('<error>%s</error>', $e->getPrevious()->getMessage()));
120
            }
121
            $response->writeln(sprintf('<fatal>%s</fatal>', $e->getMessage()));
122
123
            return [];
124
        }
125
126
        $paths = [];
127
        foreach ($entities as $entity) {
128
            $paths[$entity->getId()] = $this->uploader->getPath(
129
                Uploader::DEFAULT_KEY,
130
                $entity->getFilesystemName()
131
            );
132
        }
133
134
        return $paths;
135
    }
136
137
    /**
138
     * @return string[]
139
     */
140
    protected function getFilesystemPaths(): array
141
    {
142
        $path = $this->uploader->getPath(Uploader::DEFAULT_KEY);
143
144
        $iterator = new FilesystemIterator($path);
145
146
        $paths = [];
147
        /** @var \SplFileInfo $fileInfo */
148
        foreach ($iterator as $fileInfo) {
149
            if (!$fileInfo->isFile()) {
150
                continue;
151
            }
152
            $filename = $fileInfo->getFilename();
153
            if (in_array($filename, $this->filesToSkip)) {
154
                continue;
155
            }
156
157
            $paths[] = $fileInfo->getRealPath();
158
        }
159
160
        return $paths;
161
    }
162
163
    /**
164
     * @param IResponse $response
165
     * @param string[]  $dbPaths
166
     * @param string[]  $fsPaths
167
     *
168
     * @return string[]
169
     */
170
    protected function filterFilesystemOnly(IResponse $response, array $dbPaths, array $fsPaths): array
171
    {
172
        $dbPaths = array_flip($dbPaths);
173
174
        $filteredPaths = [];
175
        foreach ($fsPaths as $fsPath) {
176
            if (array_key_exists($fsPath, $dbPaths)) {
177
                continue;
178
            }
179
180
            $filteredPaths[] = $fsPath;
181
        }
182
183
        $response->writeln(sprintf('<info>%s</info>', sprintf(static::FS_ONLY, count($filteredPaths))));
184
185
        return $filteredPaths;
186
    }
187
188
    /**
189
     * @param IResponse $response
190
     * @param string[]  $dbPaths
191
     * @param string[]  $fsPaths
192
     *
193
     * @return string[]
194
     */
195
    protected function filterDatabaseOnly(IResponse $response, array $dbPaths, array $fsPaths): array
196
    {
197
        $fsPaths = array_flip($fsPaths);
198
199
        $filteredPaths = [];
200
        foreach ($dbPaths as $id => $dbPath) {
201
            if (array_key_exists($dbPath, $fsPaths)) {
202
                continue;
203
            }
204
205
            $filteredPaths[$id] = $dbPath;
206
        }
207
208
        $response->writeln(sprintf('<info>%s</info>', sprintf(static::DB_ONLY, count($filteredPaths))));
209
210
        return $filteredPaths;
211
    }
212
213
    /**
214
     * @param IResponse $response
215
     *
216
     * @return bool
217
     */
218
    protected function isDryRun(IResponse $response): bool
219
    {
220
        $dryRun = (bool)$this->getOptionValue(static::OPTION_DRY_RUN);
221
        if (!$dryRun) {
222
            return $dryRun;
223
        }
224
225
        $this->unitOfWork->dispose();
226
        $response->writeln(static::COMMAND_DRY_RUN_MESSAGE);
227
228
        return $dryRun;
229
    }
230
231
    /**
232
     * @param IResponse $response
233
     * @param string[]  $fsOnly
234
     */
235
    protected function deleteFilesFromDatabase(IResponse $response, array $fsOnly)
236
    {
237
        if (count($fsOnly) > 0) {
238
            $response->writeln(sprintf('<info>%s</info>', static::DELETING_ENTITIES));
239
        }
240
241
        foreach ($fsOnly as $id => $path) {
242
            try {
243
                $this->fileRepo->delete(new Entity($id, '', '', '', ''));
244
                $response->writeln(sprintf('<comment>%d: %s</comment>', $id, $path));
245
            } catch (\Exception $e) {
246
                if ($e->getPrevious()) {
247
                    $response->writeln(sprintf('<error>%s</error>', $e->getPrevious()->getMessage()));
248
                }
249
                $response->writeln(sprintf('<fatal>%s</fatal>', $e->getMessage()));
250
            }
251
        }
252
    }
253
254
    /**
255
     * @param IResponse $response
256
     * @param string[]  $dbOnly
257
     */
258
    protected function deleteFilesFromFilesystem(IResponse $response, array $dbOnly)
259
    {
260
        if (count($dbOnly) > 0) {
261
            $response->writeln(sprintf('<info>%s</info>', static::DELETING_FILES));
262
        }
263
264
        foreach ($dbOnly as $path) {
265
            try {
266
                @unlink($path);
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

266
                /** @scrutinizer ignore-unhandled */ @unlink($path);

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...
267
                $response->writeln(sprintf('<comment>%s</comment>', $path));
268
            } catch (\Exception $e) {
269
                if ($e->getPrevious()) {
270
                    $response->writeln(sprintf('<error>%s</error>', $e->getPrevious()->getMessage()));
271
                }
272
                $response->writeln(sprintf('<fatal>%s</fatal>', $e->getMessage()));
273
            }
274
        }
275
    }
276
277
    /**
278
     * @param IResponse $response
279
     */
280
    protected function commit(IResponse $response)
281
    {
282
        try {
283
            $this->unitOfWork->commit();
284
            $response->writeln(static::COMMAND_SUCCESS);
285
        } catch (\Exception $e) {
286
            if ($e->getPrevious()) {
287
                $response->writeln(sprintf('<error>%s</error>', $e->getPrevious()->getMessage()));
288
            }
289
            $response->writeln(sprintf('<fatal>%s</fatal>', $e->getMessage()));
290
        }
291
    }
292
}
293