Create, update file programmatically

Hi,

I try to understand how to create and update file fetch from a front-end form, programmatically in a controller.

I have read the documentation and this is what I understood:

1- To get the file name, there is no Kirby function like get() that we use to get the other form type (text, textarea …) so we must use the PHP var $_FILES['img_field_name']['name']

My question: I’m right or did I miss a Kirby function to get the file information?

2- To create a file, I’ve found two functions in the documentation:

F::write() // Creates a new file

and …

File::create() // Creates a new file on disk and returns the File object.

My question: What is the difference between these two functions? Do I need to use both of them?

In this thread, Updating files field programmatically - #4 by texnixe @evjand seems to use both of them but I don’t understand why, the two functions seems to do the same things (create a file).

3- The first time, the file doesn’t exist so I need to create it. But after, if I want to replace this file by another one, how it happened?

My question: do I need to first delete the old file and then recreate a file with the functions above?

File::create creates a new file and returns the file object; that is what you need when you want to update the file afterwards.

F::write is a toolkit method, it returns a boolean only, not a file object.

As regards handling the request, the Kirby/Http/Request class has a Files subclass that might be useful

 * The Files object sanitizes
 * the input coming from the $_FILES
 * global. Especially for multiple uploads
 * for the same key, it will produce a more
 * usable array.

So the first time, to create the file, I have to use F::write and afterwards to replace it by another file I have to use File::create?

In both cases “$source” is the path where to store the file?
So in my case something like root_app/content/page/sub-page-xxx?

What do you mean by update a file? Replace the original file or update file meta data?

I mean replace the original file by another one

Playing around with it myself, so this is a very basic upload:

<?php

return function ($kirby, $page) {

    if($kirby->request()->is('post') && get('submit')) {
        $files = $kirby->request()->files()->get('file');
        if (count($files)) {
            $kirby->impersonate('kirby');
            foreach($files as $file) {
                File::create([
                    'source' => $file['tmp_name'],
                    'parent' => $page,
                    'filename' => $file['name'],
                ]);
            }
        }
      
    }
    
    return [];


};

From a form like this:

<form action="" method="post" enctype="multipart/form-data">
    <ul>
	    <li>
		    <label for="file">Pictures</label>
		    <input name="file[]" type="file" multiple>
	    </li>
	    <input type="submit" name="submit" value="Send it in!" class="button">
    </ul>
</form>

As regards the replacement, I guess you want to check if the file already exist and then overwrite it. Let me see.

New version. We check, if a file with that file name already exists and if so, we replace it with $file->replace():

<?php

return function ($kirby, $page) {

    if($kirby->request()->is('post') && get('submit')) {
        $files = $kirby->request()->files()->get('file');
        
        if (count($files)) {
            $kirby->impersonate('kirby');
            foreach ($files as $file) {
                if ($existing = $page->file($file['name'])) {
                    $existing->replace($file['tmp_name']);
                } else {
                    File::create([
                        'source' => $file['tmp_name'],
                        'parent' => $page,
                        'filename' => $file['name'],
                    ]);
                }

            }
        }
      
    }
    
    return [];


};

I’d then wrap all this stuff into their try-catch blocks to react on errors.

Ok I’m trying to adapt my code regarding your code example and see if I’m able to create a file and replace it afterward.

I’m progressing … :wink:

Ok, I get it! Thank you @texnixe, next step > update a date field programmatically …

Please note the above code was just a very basic example not to be used in production. In a real life project, you should be very careful what type of files you allow your users to upload (especially if without authentication) and set your restrictions accordingly.

Ok thanks for this advice, you are absolutely right. I hadn’t thought of it.

If I check if the file uploaded is an image with $file->type() do you think it’s enough or should I also check the file extension too (jpeg, jpg)?

One thing you can do is to set the accept option in a file template and only allow files with that template, e.g. in image.yml restrict access to images and also restrict file size:

accept:
  mime: image/*
  size: 20000

Then in your controller:

$alerts = [];
$prefix ='some random prefix';
  try {
      $file = $page->createFile([
          'source'     => $file['tmp_name'],
          'template'   => 'image',
           'filename'  => $prefix . '-' . $file['name'],
       ]);
  } catch (Exception $e) {
     $alerts[$file['name']] = $e->getMessage();
  }

It seems that Kirby by default doesn’t allow upload of exe, php etc. files. I’m not sure how far that protection goes with masked files, i.e. files that pretend to be an image while in reality they contain executable code. That’s something the devs can answer better than me.

Ideally, you would only allow file upload for authenticated users, but of course, there are often situations, where you have to allow random users to upload files.

Adding a random prefix to filenames is one additional security measure, so the uploader cannot guess the URL under which the uploaded file will be accessible.

However, adding the random prefix will also make it impossible to replace an existing file.

Excellent, I’ll set it all up and do some research on files that pretend to be an image.

Thanks for your precious advises as always!

You can send meta data when you create the file, no need for an additional step.

   try {
                        $file = $page->createFile([
                            'source'   => $file['tmp_name'],
                            'template' => 'image',
                            'filename' => $prefix. '-' . $file['name'],
                            'content' => [
                                'date' => date('Y-m-d'),
                                'caption' => 'A useful caption'
                            ]
                        ]);
                    } catch (Exception $e) {
                        $alerts[$file['name']] = $e->getMessage();
                    }

:+1: Thx :slightly_smiling_face: