Passed
Push — master ( dc3f28...9cb9a1 )
by Andreas
16:51
created

blobdir::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midcom.console
4
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
namespace midcom\console\command\cleanup;
10
11
use Symfony\Component\Console\Command\Command;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Input\InputOption;
14
use Symfony\Component\Console\Output\OutputInterface;
15
use midcom_db_attachment;
16
use Symfony\Component\Console\Attribute\AsCommand;
17
use midgard\portable\storage\connection;
18
use Doctrine\ORM\AbstractQuery;
19
20
/**
21
 * Cleanup the blobdir
22
 * Search for corrupted (0 bytes) files
23
 *
24
 * @package midcom.console
25
 */
26
#[AsCommand(
27
    name: 'midcom:cleanup:blobdir',
28
    description: 'Cleanup the blobdir',
29
    aliases: ['blobdircleanup']
30
)]
31
class blobdir extends Command
32
{
33
    private int $_file_counter = 0;
34
35
    private string $_dir = "";
36
37
    private bool $dry = false;
38
39
    private array $findings = [
40
        'corrupted' => [],
41
        'orphaned' => [],
42
        'orphaned_attachments' => []
43
    ];
44
45
    protected function configure()
46
    {
47
        $this->addOption('dry', 'd', InputOption::VALUE_NONE, 'If set, files and attachments will not be deleted');
48
    }
49
50
    public function check_dir(string $outerDir)
51
    {
52
        $outerDir = rtrim($outerDir, "/");
53
        $dirs = array_diff(scandir($outerDir), [".", ".."]);
54
        foreach ($dirs as $d) {
55
            if (is_dir($outerDir . "/" . $d)) {
56
                $this->check_dir($outerDir . "/" . $d);
57
            } else {
58
                // got something
59
                $file = $outerDir . "/" . $d;
60
                if (filesize($file) == 0) {
61
                    $this->findings['corrupted'][] = $file;
62
                } else {
63
                    $attachment = $this->get_attachment($file);
64
                    if (!$attachment) {
65
                        $this->findings['orphaned'][] = $file;
66
                    } elseif (!$this->get_attachment_parent($attachment)) {
67
                        $this->findings['orphaned_attachments'][] = $attachment;
68
                    }
69
                }
70
                $this->_file_counter++;
71
            }
72
        }
73
    }
74
75
    private function _determine_location(string $path) : string
76
    {
77
        return ltrim(str_replace($this->_dir, "", $path), "/");
78
    }
79
80
    private function get_attachment_parent(midcom_db_attachment $attachment) : bool
81
    {
82
        $type = connection::get_em()
83
            ->createQuery('SELECT r.typename from midgard_repligard r WHERE r.guid = ?1')
84
            ->setParameter(1, $attachment->parentguid)
85
            ->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR);
86
87
        if (!$type) {
88
            return false;
89
        }
90
91
        $dba_type = \midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($type);
92
93
        $qb = \midcom::get()->dbfactory->new_query_builder($dba_type);
0 ignored issues
show
Bug introduced by
It seems like $dba_type can also be of type null; however, parameter $classname of midcom_helper__dbfactory::new_query_builder() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
        $qb = \midcom::get()->dbfactory->new_query_builder(/** @scrutinizer ignore-type */ $dba_type);
Loading history...
94
        $qb->include_deleted();
95
        $qb->add_constraint('guid', '=', $attachment->parentguid);
96
        return $qb->count() > 0;
97
    }
98
99
    private function get_attachment(string $file) : ?midcom_db_attachment
100
    {
101
        $location = $this->_determine_location($file);
102
        // get attachments
103
        $qb = midcom_db_attachment::new_query_builder();
104
        $qb->add_constraint("location", "=", $location);
105
        try {
106
            $attachments = $qb->execute();
107
        } catch (\Exception) {
108
            return null;
109
        }
110
        if (empty($attachments)) {
111
            return null;
112
        }
113
        if (count($attachments) > 1) {
114
            throw new \midcom_error('Multiple attachments share location ' . $location);
115
        }
116
117
        return $attachments[0];
118
    }
119
120
    private function cleanup_corrupted(OutputInterface $output, array $files)
121
    {
122
        $i = 0;
123
        foreach ($files as $file) {
124
            $i++;
125
            // cleanup file
126
            $output->writeln($i . ") " . $file);
127
            $this->cleanup_file($output, $file);
128
129
            if ($attachment = $this->get_attachment($file)) {
130
                $this->purge_attachment($output, $attachment);
131
            }
132
        }
133
    }
134
135
    private function purge_attachment(OutputInterface $output, midcom_db_attachment $attachment)
136
    {
137
        if (!$this->dry) {
138
            $stat = $attachment->purge();
139
            if (!$stat || $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
140
                $output->writeln(($stat) ? "<info>Purge OK</info>" : "<comment>Purge FAILED, reason: " . \midcom_connection::get_error_string() . "</comment>");
141
            }
142
        }
143
    }
144
145
    private function cleanup_file(OutputInterface $output, string $file)
146
    {
147
        if (!$this->dry) {
148
            $stat = unlink($file);
149
            if (!$stat || $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
150
                $output->writeln(($stat) ? "<info>Cleanup OK</info>" : "<comment>Cleanup FAILED</comment>");
151
            }
152
        }
153
    }
154
155
    protected function execute(InputInterface $input, OutputInterface $output) : int
156
    {
157
        $dir = \midgard_connection::get_instance()->config->blobdir;
158
        if (!is_dir($dir)) {
159
            $output->writeln("<comment>Unable to detect blobdir</comment>");
160
            return Command::FAILURE;
161
        }
162
        $this->_dir = $dir;
163
        $this->dry = $input->getOption("dry");
164
        if ($this->dry) {
165
            $output->writeln("<comment>Running in dry mode!</comment>");
166
        }
167
168
        $output->writeln("Start scanning dir: <comment>" . $dir . "</comment>");
169
170
        $this->check_dir($dir);
171
172
        $output->writeln("Scanned <info>" . $this->_file_counter . "</info> files");
173
        $output->writeln("Found <info>" . count($this->findings['corrupted']) . "</info> corrupted files");
174
        $output->writeln("Found <info>" . count($this->findings['orphaned']) . "</info> orphaned files");
175
        $output->writeln("Found <info>" . count($this->findings['orphaned_attachments']) . "</info> orphaned attachments");
176
177
        $this->cleanup_corrupted($output, $this->findings['corrupted']);
178
179
        foreach ($this->findings['orphaned'] as $file) {
180
            $this->cleanup_file($output, $file);
181
        }
182
183
        foreach ($this->findings['orphaned_attachments'] as $attachment) {
184
            $this->purge_attachment($output, $attachment);
185
        }
186
187
        $output->writeln("<comment>Done</comment>");
188
        return Command::SUCCESS;
189
    }
190
}
191