Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.41% covered (success)
92.41%
73 / 79
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeletesCommand
92.41% covered (success)
92.41%
73 / 79
50.00% covered (danger)
50.00%
2 / 4
17.13
0.00% covered (danger)
0.00%
0 / 1
 configure
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
1
 getIdsFromFlatFile
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getIdsFromMarcFile
71.43% covered (warning)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
8.14
 execute
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
6.01
1<?php
2
3/**
4 * Console command: delete from Solr
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2020.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Console
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development Wiki
28 */
29
30namespace VuFindConsole\Command\Util;
31
32use Symfony\Component\Console\Attribute\AsCommand;
33use Symfony\Component\Console\Formatter\OutputFormatter;
34use Symfony\Component\Console\Input\InputArgument;
35use Symfony\Component\Console\Input\InputInterface;
36use Symfony\Component\Console\Input\InputOption;
37use Symfony\Component\Console\Output\OutputInterface;
38use VuFind\Marc\MarcCollectionFile;
39
40use function count;
41use function strlen;
42
43/**
44 * Console command: delete from Solr
45 *
46 * @category VuFind
47 * @package  Console
48 * @author   Demian Katz <demian.katz@villanova.edu>
49 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
50 * @link     https://vufind.org/wiki/development Wiki
51 */
52#[AsCommand(
53    name: 'util/deletes',
54    description: 'Tool for deleting Solr records'
55)]
56class DeletesCommand extends AbstractSolrCommand
57{
58    /**
59     * Configure the command.
60     *
61     * @return void
62     */
63    protected function configure()
64    {
65        $this
66            ->setHelp('Deletes a set of records from the Solr index.')
67            ->addArgument(
68                'filename',
69                InputArgument::REQUIRED,
70                'the file containing records to delete.'
71            )->addArgument(
72                'format',
73                InputArgument::OPTIONAL,
74                "the format of the file -- it may be one of the following:\n"
75                . 'flat - flat text format '
76                . "(deletes all IDs in newline-delimited file)\n"
77                . 'marc - MARC record in binary or MARCXML format (deletes all '
78                . "record IDs from 001 fields)\n"
79                . "marcxml - DEPRECATED; use marc instead\n",
80                'marc'
81            )->addArgument(
82                'index',
83                InputArgument::OPTIONAL,
84                'Name of Solr core/backend to update',
85                'Solr'
86            )->addOption(
87                'id-prefix',
88                null,
89                InputOption::VALUE_REQUIRED,
90                'Prefix to prepend to all IDs',
91                ''
92            );
93    }
94
95    /**
96     * Load IDs from a flat file.
97     *
98     * @param string $filename Filename to load from
99     *
100     * @return array
101     */
102    protected function getIdsFromFlatFile(string $filename): array
103    {
104        $ids = [];
105        foreach (array_map('trim', file($filename)) as $id) {
106            if (strlen($id)) {
107                $ids[] = $id;
108            }
109        }
110        return $ids;
111    }
112
113    /**
114     * Load IDs from a MARC file
115     *
116     * @param string          $filename MARC file
117     * @param OutputInterface $output   Output object
118     *
119     * @return array
120     */
121    protected function getIdsFromMarcFile(
122        string $filename,
123        OutputInterface $output
124    ): array {
125        $ids = [];
126        // MARC file mode:
127        $messageCallback = function (string $msg, int $level) use ($output) {
128            if ($output->isVerbose() || $level !== E_NOTICE) {
129                $output->writeln(
130                    '<comment>' . OutputFormatter::escape($msg) . '</comment>'
131                );
132            }
133        };
134        $collection = new MarcCollectionFile($filename, $messageCallback);
135
136        // Once the records are loaded, the rest of the logic is always the same:
137        $missingIdCount = 0;
138        foreach ($collection as $record) {
139            if ($id = $record->getField('001')) {
140                $ids[] = $id;
141            } else {
142                $missingIdCount++;
143            }
144        }
145        if ($output->isVerbose() && $missingIdCount) {
146            $output->writeln(
147                "Encountered $missingIdCount record(s) without IDs."
148            );
149        }
150        return $ids;
151    }
152
153    /**
154     * Run the command.
155     *
156     * @param InputInterface  $input  Input object
157     * @param OutputInterface $output Output object
158     *
159     * @return int 0 for success
160     */
161    protected function execute(InputInterface $input, OutputInterface $output)
162    {
163        $filename = $input->getArgument('filename');
164        $mode = $input->getArgument('format');
165        $index = $input->getArgument('index');
166        $prefix = $input->getOption('id-prefix');
167
168        // File doesn't exist?
169        if (!file_exists($filename)) {
170            $output->writeln("Cannot find file: {$filename}");
171            return 1;
172        }
173
174        $output->writeln(
175            "Loading IDs in {$mode} mode.",
176            OutputInterface::VERBOSITY_VERBOSE
177        );
178
179        // Build list of records to delete:
180        $ids = ($mode == 'flat')
181            ? $this->getIdsFromFlatFile($filename)
182            : $this->getIdsFromMarcFile($filename, $output);
183
184        // Delete, Commit and Optimize if necessary:
185        if (!empty($ids)) {
186            $output->writeln(
187                'Attempting to delete ' . count($ids) . ' record(s): '
188                . implode(', ', $ids),
189                OutputInterface::VERBOSITY_VERBOSE
190            );
191            if (!empty($prefix)) {
192                $callback = function ($id) use ($prefix) {
193                    return $prefix . $id;
194                };
195                $ids = array_map($callback, $ids);
196            }
197            $this->solr->deleteRecords($index, $ids);
198            $output->writeln(
199                'Delete operation completed.',
200                OutputInterface::VERBOSITY_VERBOSE
201            );
202        } elseif ($output->isVerbose()) {
203            $output->writeln('Nothing to delete.');
204        }
205
206        return 0;
207    }
208}