Kirby files section to field

Files sections used to be the only way to handle direct file uploads in the Kirby Panel. Since version 3.2.0 (6 years ago?!), files fields support this too and they’re often a better choice. Unfortunately, migrating the content from one approach to the other is quite complex, so I’ve held off touching this on some older websites.

Recently I’ve finally spent some time finding an efficient solution and I want to share it with you.

About sections and fields

In Kirby, sections are used to display content in the Panel, while fields are used to store content. Most of the times you want to have both a files section and (multiple) files fields. The section lists the files for your page and the fields allow you to select specific files for specific purposes like a slideshow.

Because it was not possible to upload files to the files field directly, I often used only a files section for these specific purposes. This way my users didn’t have to navigate to the files section to upload some files and then select the same files in another place. They just uploaded the files in the section and ā€œselectedā€ them right there. Now that we can upload files directly in the files field, this is no longer necessary and only comes with downsides.

The necessary changes

First, let’s see what we’ll actually have to change in the blueprint and the content files.

Adjusting the blueprint

We want to change the blueprint from a files section with a template…

sections:
  images:
    type: files
    template: slideshow

…to a simple files field allowing the user to select any image on the page:

fields:
  images:
    type: files
    query: page.images

That’s easy enough. We don’t really need to automate anything here. But obviously we’re not even close to done with the migration.

Let’s look at the difference in the content files next:

Adjusting the content files

From individual image content files with a common template like these:

Alt: An image of a cute cat

----

Uuid: NaacIvCVo48DeHrT

----

Template: slideshow
Alt: Another image of a cute cat

----

Uuid: mdNBF6k1jhqJr6DQ

----

Template: slideshow
Alt: An image of an extra fluffy cat

----

Uuid: qBGsw42NnLGGRBo6

----

Template: slideshow

To a simple list of UUIDs in the page’s content file:

Slideshow:
  - file://NaacIvCVo48DeHrT
  - file://mdNBF6k1jhqJr6DQ
  - file://qBGsw42NnLGGRBo6

That means we need to find all images with a specific template (e.g. ā€œslideshowā€), get their UUIDs and store them in a field on their parent page.

Automating the migration

This change could take a long time, when done manually. Thankfully Kirby has a CLI and it’s quite easy to create custom commands for it.

My Kirby CLI command

This command does the following things:

  1. Looping through all pages — We use ⁠site()->index(true) to get all pages including drafts
  2. Finding relevant images — For each page, we filter images by the specified template and maintain their sort order with ⁠->sorted().
  3. Collecting UUIDs — We extract the UUID from each matching image and build an array.
  4. Updating the page — Using Kirby’s ⁠impersonate() method to run with admin privileges, we update the page with the new field containing the image UUIDs.
<?php

declare(strict_types=1);

use Kirby\CLI\CLI;

return [
    'description' => 'Migrate from files sections to files fields',
    'args' => [
        'template' => [
            'prefix' => 't',
            'longPrefix' => 'template',
            'description' => 'File template to migrate',
            'defaultValue' => 'slideshow'
        ],
        'dry' => [
            'longPrefix' => 'dry',
            'description' => 'Dry run',
            'defaultValue' => false,
            'noValue' => true,
        ],
    ],
    'command' => static function (CLI $cli): void {
        $isDryRun = $cli->arg('dry');
        $template = $cli->arg('template');
        $pages = site()->index(true);

        foreach ($pages as $page) {
            try {
                $cli->out("šŸ“ /" . $page->id());

                $images = $page->images()->template($template)->sorted();

                if ($images->count() === 0) {
                    $cli->out("ā­ļø skipped\n");
                    continue;
                }

                $imagesArray = [];

                foreach ($images as $image) {
                    $imagesArray[] = $image->uuid()->toString();
                    $cli->out('šŸ“„ ' . $image->name());
                }

                if (!$isDryRun) {
                    kirby()->impersonate('kirby', function () use ($page, $imagesArray, $template) {
                        $page->update([
                            $template => $imagesArray
                        ]);
                    });
                }

                $cli->out("ā˜‘ļø " . count($imagesArray) . " image(s)\n");
            } catch (\Exception $e) {
                $cli->error("Error on page /" . $page->id());
                $cli->error($e->getMessage());
                $cli->error($e->getTraceAsString());
                break;
            }
        }

        $cli->success('All pages have been updated');
    }
];

Installing the command

Have a look at the documentation for global CLI commands.

TL;DR: Put the code in ~/.kirby/commands/files-section-to-field.php

Running the command

Test it first with a dry run (no changes will be made):

kirby files-section-to-field --dry

Then run the actual migration:

kirby files-section-to-field

If your image template happens to be something other than ā€œslideshowā€, you can specify it with the template argument:

kirby files-section-to-field --template cat

Always back up your content before running commands you find on random people’s websites! Thankfully that’s super easy with file-based systems like Kirby: just drag the content folder to your desktop.

Final thoughts

I’ve been putting off this migration for years because it seemed like such a tedious task. Creating a CLI command turned out to be quite straightforward and saved me hours of manual work across multiple projects.

Am I the only one that used separate files sections just for file uploads? I can’t be the only one, right? If this post helped you, please let me know! You can find me on Mastodon.