From 89dee41d583e57251ea9315402a757f03571d7ad Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Thu, 13 Jan 2022 16:02:14 +0000
Subject: [PATCH] feat: add housekeeping task to run after migrations

add run housekeeping button in general settings page
---
 app/Models/MediaModel.php                     |  15 +++
 app/Resources/icons/home-gear.svg             |   6 +
 app/Views/Components/Button.php               |  10 +-
 modules/Admin/Config/Routes.php               |   4 +
 .../Admin/Controllers/SettingsController.php  | 106 ++++++++++++++++++
 modules/Admin/Language/en/Settings.php        |   6 +
 modules/Admin/Language/fr/Settings.php        |   6 +
 themes/cp_admin/settings/general.php          |  14 +++
 8 files changed, 165 insertions(+), 2 deletions(-)
 create mode 100644 app/Resources/icons/home-gear.svg

diff --git a/app/Models/MediaModel.php b/app/Models/MediaModel.php
index 85695c410b..bc61689d23 100644
--- a/app/Models/MediaModel.php
+++ b/app/Models/MediaModel.php
@@ -145,6 +145,21 @@ class MediaModel extends Model
         return $this->update($media->id, $media);
     }
 
+    /**
+     * @return array<mixed>
+     */
+    public function getAllOfType(): array
+    {
+        $result = $this->where('type', $this->fileType)
+            ->findAll();
+        $mediaClass = $this->returnType;
+        foreach ($result as $key => $media) {
+            $result[$key] = new $mediaClass($media->toArray(false, true));
+        }
+
+        return $result;
+    }
+
     public function deleteMedia(object $media): bool
     {
         $media->deleteFile();
diff --git a/app/Resources/icons/home-gear.svg b/app/Resources/icons/home-gear.svg
new file mode 100644
index 0000000000..2504da6dd9
--- /dev/null
+++ b/app/Resources/icons/home-gear.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+    <g>
+        <path fill="none" d="M0 0h24v24H0z"/>
+        <path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zM8.592 13.808l-.991.572 1 1.733.993-.573a3.5 3.5 0 0 0 1.405.811v1.145h2.002V16.35a3.5 3.5 0 0 0 1.405-.81l.992.572L16.4 14.38l-.991-.572a3.504 3.504 0 0 0 0-1.62l.991-.573-1-1.733-.993.573A3.5 3.5 0 0 0 13 9.645V8.5h-2.002v1.144a3.5 3.5 0 0 0-1.405.811l-.992-.573L7.6 11.616l.991.572a3.504 3.504 0 0 0 0 1.62zm3.408.69a1.5 1.5 0 1 1-.002-3.001 1.5 1.5 0 0 1 .002 3z"/>
+    </g>
+</svg>
diff --git a/app/Views/Components/Button.php b/app/Views/Components/Button.php
index 651dcd4882..79c96ac5fd 100644
--- a/app/Views/Components/Button.php
+++ b/app/Views/Components/Button.php
@@ -46,6 +46,12 @@ class Button extends Component
             'large' => 'text-base leading-6',
         ];
 
+        $iconSize = [
+            'small' => 'text-sm',
+            'base' => 'text-lg',
+            'large' => 'text-2xl',
+        ];
+
         $basePaddings = [
             'small' => 'px-3 py-1',
             'base' => 'px-3 py-2',
@@ -77,14 +83,14 @@ class Button extends Component
         if ($this->iconLeft !== '') {
             $this->slot = (new Icon([
                 'glyph' => $this->iconLeft,
-                'class' => 'mr-2 opacity-75',
+                'class' => 'mr-2 opacity-75' . ' ' . $iconSize[$this->size],
             ]))->render() . $this->slot;
         }
 
         if ($this->iconRight !== '') {
             $this->slot .= (new Icon([
                 'glyph' => $this->iconRight,
-                'class' => 'ml-2 opacity-75',
+                'class' => 'ml-2 opacity-75' . ' ' . $iconSize[$this->size],
             ]))->render();
         }
 
diff --git a/modules/Admin/Config/Routes.php b/modules/Admin/Config/Routes.php
index c22fba6998..b263686ee7 100644
--- a/modules/Admin/Config/Routes.php
+++ b/modules/Admin/Config/Routes.php
@@ -40,6 +40,10 @@ $routes->group(
                 'as' => 'settings-images-regenerate',
                 'filter' => 'permission:settings-manage',
             ]);
+            $routes->post('instance-housekeeping-run', 'SettingsController::runHousekeeping', [
+                'as' => 'settings-housekeeping-run',
+                'filter' => 'permission:settings-manage',
+            ]);
             $routes->get('theme', 'SettingsController::theme', [
                 'as' => 'settings-theme',
                 'filter' => 'permission:settings-manage',
diff --git a/modules/Admin/Controllers/SettingsController.php b/modules/Admin/Controllers/SettingsController.php
index 7f91371bbf..879d5d4376 100644
--- a/modules/Admin/Controllers/SettingsController.php
+++ b/modules/Admin/Controllers/SettingsController.php
@@ -10,8 +10,12 @@ declare(strict_types=1);
 
 namespace Modules\Admin\Controllers;
 
+use App\Entities\Media\Image;
+use App\Models\ActorModel;
+use App\Models\MediaModel;
 use App\Models\PersonModel;
 use App\Models\PodcastModel;
+use CodeIgniter\Files\File;
 use CodeIgniter\HTTP\RedirectResponse;
 use PHP_ICO;
 
@@ -156,6 +160,108 @@ class SettingsController extends BaseController
         return redirect('settings-general')->with('message', lang('Settings.images.regenerationSuccess'));
     }
 
+    public function runHousekeeping(): RedirectResponse
+    {
+        helper('media');
+
+        // Delete all podcast image sizes to recreate them
+        $allPodcasts = (new PodcastModel())->findAll();
+        foreach ($allPodcasts as $podcast) {
+            $podcastImages = glob(
+                ROOTPATH . 'public/' . config('App')->mediaRoot . "/podcasts/{$podcast->handle}/*_*{jpg,png,webp}",
+                GLOB_BRACE
+            );
+
+            if ($podcastImages) {
+                foreach ($podcastImages as $podcastImage) {
+                    if (is_file($podcastImage)) {
+                        unlink($podcastImage);
+                    }
+                }
+            }
+        }
+
+        // Delete all person image sizes to recreate them
+        $personsImages = glob(
+            ROOTPATH . 'public/' . config('App')->mediaRoot . '/persons/*_*{jpg,png,webp}',
+            GLOB_BRACE
+        );
+        if ($personsImages) {
+            foreach ($personsImages as $personsImage) {
+                if (is_file($personsImage)) {
+                    unlink($personsImage);
+                }
+            }
+        }
+
+        $allImages = (new MediaModel('image'))->getAllOfType();
+        foreach ($allImages as $image) {
+            if (str_starts_with($image->file_path, 'podcasts')) {
+                if (str_ends_with($image->file_path, 'banner.jpg') || str_ends_with($image->file_path, 'banner.png')) {
+                    $image->sizes = config('Images')
+                        ->podcastBannerSizes;
+                } else {
+                    $image->sizes = config('Images')
+                        ->podcastCoverSizes;
+                }
+            } elseif (str_starts_with($image->file_path, 'persons')) {
+                $image->sizes = config('Images')
+                    ->personAvatarSizes;
+            }
+            $image->setFile(new File(media_path($image->file_path)));
+
+            (new MediaModel('image'))->updateMedia($image);
+        }
+
+        $allAudio = (new MediaModel('audio'))->getAllOfType();
+        foreach ($allAudio as $audio) {
+            $audio->setFile(new File(media_path($audio->file_path)));
+
+            (new MediaModel('audio'))->updateMedia($audio);
+        }
+
+        $allTranscripts = (new MediaModel('transcript'))->getAllOfType();
+        foreach ($allTranscripts as $transcript) {
+            $transcript->setFile(new File(media_path($transcript->file_path)));
+
+            (new MediaModel('transcript'))->updateMedia($transcript);
+        }
+
+        $allChapters = (new MediaModel('chapters'))->getAllOfType();
+        foreach ($allChapters as $chapters) {
+            $chapters->setFile(new File(media_path($chapters->file_path)));
+
+            (new MediaModel('chapters'))->updateMedia($chapters);
+        }
+
+        $allVideos = (new MediaModel('video'))->getAllOfType();
+        foreach ($allVideos as $video) {
+            $video->setFile(new File(media_path($video->file_path)));
+
+            (new MediaModel('video'))->updateMedia($video);
+        }
+
+        // reset avatar and banner image urls for each podcast actor
+        foreach ($allPodcasts as $podcast) {
+            $actorModel = new ActorModel();
+            $actor = $actorModel->getActorById($podcast->actor_id);
+
+            if ($actor !== null) {
+                // update values
+                $actor->avatar_image_url = $podcast->cover->federation_url;
+                $actor->avatar_image_mimetype = $podcast->cover->file_mimetype;
+                $actor->cover_image_url = $podcast->banner->federation_url;
+                $actor->cover_image_mimetype = $podcast->banner->file_mimetype;
+
+                if ($actor->hasChanged()) {
+                    $actorModel->update($actor->id, $actor);
+                }
+            }
+        }
+
+        return redirect('settings-general')->with('message', lang('Settings.housekeeping.runSuccess'));
+    }
+
     public function theme(): string
     {
         helper('form');
diff --git a/modules/Admin/Language/en/Settings.php b/modules/Admin/Language/en/Settings.php
index 48580a5e88..8d0ece529e 100644
--- a/modules/Admin/Language/en/Settings.php
+++ b/modules/Admin/Language/en/Settings.php
@@ -28,6 +28,12 @@ return [
         'regenerate' => 'Regenerate images',
         'regenerationSuccess' => 'All images have been regenerated successfully!',
     ],
+    'housekeeping' => [
+        'title' => 'Housekeeping',
+        'subtitle' => 'Runs various housekeeping tasks, such as rewriting media files metadata (images, audio files, transcripts, chapters, …).',
+        'run' => 'Run housekeeping',
+        'runSuccess' => 'Housekeeping has been run successfully!',
+    ],
     'theme' => [
         'title' => 'Theme',
         'accent_section_title' => 'Accent color',
diff --git a/modules/Admin/Language/fr/Settings.php b/modules/Admin/Language/fr/Settings.php
index 6c7dece2d5..0d0fe8df0f 100644
--- a/modules/Admin/Language/fr/Settings.php
+++ b/modules/Admin/Language/fr/Settings.php
@@ -28,6 +28,12 @@ return [
         'regenerate' => 'Regénérer les images',
         'regenerationSuccess' => 'Toutes les images ont été regénérés avec succès !',
     ],
+    'housekeeping' => [
+        'title' => 'Ménage',
+        'subtitle' => 'Exécute un nombre de tâches de nettoyage, comme la réécriture de métadonnées des fichiers media (images, fichiers audio, transcript, chapitres, …).',
+        'run' => 'Faire le ménage',
+        'runSuccess' => 'Le ménage a été effectué avec succès !',
+    ],
     'theme' => [
         'title' => 'Thème',
         'accent_section_title' => 'Couleur d’accentuation',
diff --git a/themes/cp_admin/settings/general.php b/themes/cp_admin/settings/general.php
index 26c8e86d9b..bd806aa240 100644
--- a/themes/cp_admin/settings/general.php
+++ b/themes/cp_admin/settings/general.php
@@ -69,6 +69,20 @@
 </Forms.Section>
 
 </form>
+
+<form action="<?= route_to('settings-housekeeping-run') ?>" method="POST" class="flex flex-col max-w-xl gap-y-4">
+<?= csrf_field() ?>
+
+<Forms.Section
+    title="<?= lang('Settings.housekeeping.title') ?>"
+    subtitle="<?= lang('Settings.housekeeping.subtitle') ?>" >
+
+    <Button variant="primary" type="submit" iconLeft="home-gear"><?= lang('Settings.housekeeping.run') ?></Button>
+
+</Forms.Section>
+
+</form>
+
 </div>
 
 <?= $this->endSection() ?>
-- 
GitLab