source: spip-zone/_outils_/spip-cli/trunk/src/Command/ImagesVerifierExtensions.php @ 111691

Last change on this file since 111691 was 111691, checked in by marcimat@…, 3 months ago

Amélioration de la commande image:verifier:extension.
On utilise pour tester plus rapidement le mime type (on divise le temps par 10) la fonction getImageSize() ;
On utilise file $f uniquement si le mime ne correspond pas (ce qui est assez rare finalement), pour avoir une description complete du contenu du fichier.

On propose une option --supprimer qui permet de supprimer les fichiers en erreur *qui ne sont pas* dans la médiathèque (dans spip_documents).
Cette option passe après les réparations (lorsque --reparer --supprimer sont présents), de sorte que ça ne supprimera que les fichiers que l’on n’a pas réussi à réparer.
Typiquement des fichiers de 0 octets, ou des images TIFF dans des .jpg ou .png…

File size: 10.1 KB
Line 
1<?php
2
3namespace Spip\Cli\Command;
4
5use Spip\Cli\Console\Command;
6use Symfony\Component\Console\Input\InputInterface;
7use Symfony\Component\Console\Input\InputOption;
8use Symfony\Component\Console\Output\OutputInterface;
9
10
11class ImagesVerifierExtensions extends Command {
12
13        protected $mimes = [
14                'jpg' => 'image/jpeg',
15                'png' => 'image/png',
16                'gif' => 'image/gif',
17        ];
18
19        protected $requirements = [
20                'jpg' => 'JPEG image data',
21                'png' => 'PNG image data',
22                'gif' => 'GIF image data',
23        ];
24
25        protected $corrections_possibles_vers = [
26                'renommer' => ['png', 'jpg', 'gif'], // les logos n’ont que 3 extensions
27                'reecrire' => ['png', 'jpg', 'gif'], // les documents, on pourrait traiter plus de cas
28        ];
29
30        protected $dataDirectory;
31
32
33        protected function configure() {
34                $this->setName("images:verifier:extensions")
35                        ->setDescription("Vérifier les extensions d’images du répertoire IMG")
36                        ->addOption('logos', null, InputOption::VALUE_NONE, 'Uniquement les logos')
37                        ->addOption('documents', null, InputOption::VALUE_NONE, 'Uniquement les documents')
38                        ->addOption('extension', null, InputOption::VALUE_OPTIONAL, 'Uniquement cette extension. Choix : jpg, png, gif')
39                        ->addOption('reparer', null, InputOption::VALUE_NONE, 'Tente de réparer le format')
40                        ->addOption('supprimer', null, InputOption::VALUE_NONE, 'Supprime les documents en erreur s’ils ne sont pas connus dans la médiathèque')
41                        ->addUsage("")
42                        ->addUsage("--logos --extension=jpg")
43                        ->addUsage("--documents")
44                        ->addUsage("--reparer")
45                        ->addUsage("--logos --reparer")
46                        ->addUsage("--documents --reparer")
47                        ->addUsage("--documents --reparer --supprimer")
48                ;
49        }
50
51        protected function execute(InputInterface $input, OutputInterface $output) {
52
53                $this->demarrerSpip();
54                $this->io->title("Analyse des extensions d’images du répertoire IMG");
55                $this->setDataDirectory(_DIR_IMG);
56
57                $extensions = array_keys($this->requirements);
58                if ($extension = $input->getOption('extension')) {
59                        if (!in_array($extension, $extensions)) {
60                                $this->io->error("Extension <info>$extension</info> inconnue. Possibles : " . implode(', ', $extensions));
61                                return;
62                        }
63                        $extensions = [$extension];
64                }
65
66                $logos = $input->getOption('logos');
67                $documents = $input->getOption('documents');
68                // tout par défaut.
69                if (!$logos and !$documents) {
70                        $logos = $documents = true;
71                }
72
73                $reparer = $input->getOption('reparer');
74                $supprimer = $input->getOption('supprimer');
75
76                foreach ($extensions as $extension) {
77                        if ($logos) {
78                                $errors = $this->verifier_images_repertoire($extension, '', 'Logos');
79                                if ($errors and $reparer) {
80                                        $this->reparer_logos($errors);
81                                }
82                        }
83                        if ($documents) {
84                                $errors = $this->verifier_images_repertoire($extension, $extension . DIRECTORY_SEPARATOR, 'Documents');
85                                if ($errors and $reparer) {
86                                        $errors = $this->reparer_documents($errors);
87                                }
88                                if ($errors) {
89                                        $errors = $this->verifier_fichiers_en_base(array_keys($errors));
90                                        if ($errors and $supprimer) {
91                                                $this->effacer_fichiers($errors);
92                                        }
93                                }
94                        }
95                }
96        }
97
98        function setDataDirectory(string $dir) {
99                if (!is_dir($dir)) {
100                        throw new \Exception("Répertoire $dir inexistant.");
101                }
102                $this->dataDirectory = $dir;
103        }
104
105        function getDataDirectory() {
106                return $this->dataDirectory;
107        }
108
109        function log($log) {
110                spip_log($log, 'images.' . _LOG_INFO_IMPORTANTE);
111        }
112
113        function verifier_images_repertoire($extension, $repertoire, $titre) {
114                $this->io->section("$titre : $extension");
115                $base = $this->getDataDirectory();
116
117                $files = glob($base . $repertoire . '*.' . $extension);
118
119                $this->io->text(count($files) . " Fichiers");
120                if (!count($files)) {
121                        return;
122                }
123
124                $this->io->progressStart(count($files));
125                $errors = [];
126                foreach ($files as $file) {
127                        list($ok, $desc) = $this->verifier_fichier($file, $extension);
128                        if (!$ok) {
129                                $name = str_replace($base, '', $file);
130                                $errors[ $file ] = "<comment>$name</comment> : $desc";
131                        }
132                        $this->io->progressAdvance();
133                }
134                $this->io->progressFinish();
135                if ($errors) {
136                        $this->io->fail(count($errors) . ' fichiers erronnés');
137                        $this->io->listing($errors);
138                } else {
139                        $this->io->check('Tous les fichiers sont corrects');
140                }
141                return $errors;
142        }
143
144        function verifier_fichier($file, $extension) {
145                // Version rapide, si ça matche
146                // Sinon, on attrape plus d’infos avec `file`.
147                if ($info = @getimagesize($file)) {
148                        $mime = image_type_to_mime_type($info[2]);
149                        if ($mime === $this->mimes[$extension]) {
150                                return [true, $mime];
151                        }
152                }
153                $infos = `file {$file}`;
154                $ok = (false !== strpos($infos, $this->requirements[$extension]));
155                list (, $desc) = explode(':', $infos, 2);
156                return [$ok, trim($desc)];
157        }
158
159        function reparer_logos(array $errors) {
160                return $this->reparer($errors, 'renommer');
161        }
162
163        function reparer_documents(array $errors) {
164                return $this->reparer($errors, 'reecrire');
165        }
166
167
168        protected function reparer(array $errors, $mode = 'renommer') {
169                $this->io->section("Réparer " . count($errors) . " fichiers");
170                if (!in_array($mode, ['renommer', 'reecrire'])) {
171                        $this->io->error("Mode $mode inconnu");
172                        return false;
173                }
174
175                $echecs = [];
176                $this->io->progressStart(count($errors));
177                foreach ($errors as $file => $info) {
178                        $extension = $this->getExtensionFromInfo($info);
179                        if (!$extension) {
180                                $echecs[$file] = $info;
181                        } else {
182                                if ($mode == 'renommer') {
183                                        $err = $this->reparer_renommer($file, $extension);
184                                } else {
185                                        $err = $this->reparer_reecrire($file, $extension);
186                                }
187                                if ($err) {
188                                        $echecs[$file] = $info . "\n<error>$err</error>";
189                                }
190                        }
191                        $this->io->progressAdvance();
192                }
193                $this->io->progressFinish();
194                if ($echecs) {
195                        $this->io->fail(count($echecs) . " fichiers non réparés");
196                        $this->io->listing($echecs);
197                } else {
198                        $this->io->check("Tous les fichiers sont réparés");
199                }
200                return $echecs;
201        }
202
203        protected function reparer_renommer($file, $extension) {
204                if (!in_array($extension, $this->corrections_possibles_vers['renommer'])) {
205                        return "Renommage vers $extension non traitable";
206                }
207                $p = pathinfo($file);
208                $fromName = $p['basename'];
209                $toName = $p['filename'] . "." . $extension;
210                if (!rename($file,  $p['dirname'] . DIRECTORY_SEPARATOR. $toName)) {
211                        return "Echec du renommage ($fromName en  $toName)";
212                }
213                $this->log("Image $fromName corrigée (renommage) en $toName");
214                return '';
215        }
216
217        protected function reparer_reecrire($file, $extensionFrom) {
218                $extension = pathinfo($file, PATHINFO_EXTENSION);
219                if (!in_array($extension, $this->corrections_possibles_vers['reecrire'])) {
220                        return "Réécriture vers $extension non traitable";
221                }
222                $name = basename($file);
223                $function = $this->getGdReadFunctionFromExtension($extensionFrom);
224                if (!function_exists($function) or !$image = $function($file)) {
225                        if (!$image = imagecreatefromstring(file_get_contents($file))) {
226                                return "Echec lecture de l’image ($name)";
227                        }
228                }
229                $err = $this->creer_image($image, $extension, $file);
230                imagedestroy($image);
231                if ($err) {
232                        return $err;
233                }
234                $this->log("Image $name corrigée (reecrite) en $extension");
235                return '';
236        }
237
238
239        protected function getExtensionFromInfo($info) {
240                foreach ($this->requirements as $extension => $req) {
241                        if (false !== strpos($info, $req)) {
242                                return $extension;
243                        }
244                }
245                return false;
246        }
247
248        protected function getGdReadFunctionFromExtension($extension) {
249                $term = ($extension == 'jpg') ? 'jpeg' : $extension;
250                return "imagecreatefrom$term";
251        }
252
253        protected function getGdWriteFunctionFromExtension($extension) {
254                $term = ($extension == 'jpg') ? 'jpeg' : $extension;
255                return "image$term";
256        }
257
258        protected function creer_image($image, $extension, $fichier) {
259                $function = $this->getGdWriteFunctionFromExtension($extension);
260                if (!function_exists($function)) {
261                        return "Function $function indisponible";
262                }
263
264                $tmp = $fichier . ".tmp";
265                $ret = $function($image, $tmp);
266                if (!$ret) {
267                        if (file_exists($tmp)) {
268                                unlink($tmp);
269                        }
270                        return "Échec création image temporaire : " . basename($tmp);
271                }
272
273                if (!file_exists($tmp)) {
274                        return "Image temporaire absente après création : " . basename($tmp);
275                }
276
277                $taille_test = getimagesize($tmp);
278                if ($taille_test[0] < 1) {
279                        return "Image temporaire taille nulle : " . basename($tmp);
280                }
281
282                list($ok, $desc) = $this->verifier_fichier($tmp, $extension);
283                if (!$ok) {
284                        return "Image temporaire pas du type attendu : " . basename($tmp) . " ($desc)";
285                }
286
287                if (file_exists($fichier)) {
288                        unlink($fichier);
289                }
290
291                if (!rename($tmp, $fichier)) {
292                        return "Échec renommage de " . basename($tmp) . " en " . basename($fichier);
293                }
294
295                return "";
296        }
297
298        protected function verifier_fichiers_en_base($files) {
299                $base = $this->getDataDirectory();
300                $absents = array_map(function($file) use ($base) {
301                        return substr($file, strlen($base));
302                }, $files);
303
304                $presents = sql_allfetsel('id_document, fichier', 'spip_documents', sql_in('fichier', $_files));
305                if ($presents) {
306                        $absents = array_diff($absents, array_column($presents, 'fichier'));
307                }
308
309                if ($absents) {
310                        $this->io->text("<info>Fichiers absents dans spip_documents :</info>");
311                        $this->io->text("Ils peuvent peut être être supprimés du coup…");
312                        $this->io->listing(array_map(function($file) {
313                                return "<comment>$file</comment>";
314                        }, $absents));
315                } else {
316                        $this->io->text("Tous les fichiers en erreur sont présents dans spip_documents.");
317                        $this->io->text("");
318                }
319                if ($presents) {
320                        $this->io->text("<info>Fichiers présents dans spip_documents :</info>");
321                        $this->io->listing(array_map(function($row) {
322                                return "<comment>{$row['fichier']}</comment> : n° {$row['id_document']}";
323                        }, $presents));
324                }
325
326                // remettre le chemin complet
327                $absents = array_map(function($file) use ($base) {
328                        return $base . $file;
329                }, $absents);
330
331                return $absents;
332        }
333
334        function effacer_fichiers($fichiers) {
335                $fichiers = array_filter($fichiers);
336                $this->io->text("Effacer " . count($fichiers) . " fichiers");
337                foreach ($fichiers as $fichier) {
338                        $name = substr($fichier, strlen($this->getDataDirectory()));
339                        if (strpos($fichier, $this->getDataDirectory()) === 0) {
340                                unlink($fichier);
341                                if (file_exists($fichier)) {
342                                        $this->io->fail($name);
343                                } else {
344                                        $this->io->check($name);
345                                }
346                        } else {
347                                $this->io->care("<comment>$name</comment> : Fichier ignoré (hors IMG)");
348                        }
349                }
350        }
351}
Note: See TracBrowser for help on using the repository browser.