Js file uploader to save file to kirby

hi!

i’m building a js single-page app and using kirby as part of the backend.

i need to upload files through the js app, and having some problems making this work.

tl;dr my question is if im doing something wrong in receiving the file, or in sending it.

so far this is what i’m trying:

  • input
<form method="post" enctype="multipart/form-data">
  <input id="upload" type="file" onchange=${ upload }>
</form>
function upload (e) {
    e.preventDefault()
    const form = e.currentTarget
    const file = form.files[0]
    let formData = new FormData()

    formData.append('module', state.page.content.title)
    formData.append('file', file)

    xhr({
      method: 'post',
      headers: {'Content-Type': undefined},
      url: '/apiupload',
      body: formData,
    }, function (err, resp, body) {
      if (err) throw err
      console.log(err, resp, body)
    })
  }
  • kirby plugin
<?php

Kirby::plugin('mooc/upload', [
  'routes' => [
    [
      'method' => 'POST',
      'pattern' => 'apiupload',
      'action'  => function () {
        if(r::is('POST')) {

          $data = array(
            'module' => get('module'),
            'file' => get('file')
          );

          $p = page('error')->children()->create(str::slug(time()), 'assignment', $data);

          return response::json($data);
        }

      }
    ]
  ]
]);

using this answer as guidance.

the file never gets sent through. i can get the string value from 'module' => get('module'),, but never that of file.

i read you should not set the headers: content-type in the js post call, or set them to undefined. in either case, it does not change.

also, should i use GET or POST in the 'method' => type? i think GET, as im fetching something from an endpoint, but im still too noob and confused about these stuff to be sure :upside_down_face:

any hint? thanks!

What is the output of console.log(formData) before the xhr() call?

Regarding the route, you are using an example from Kirby 2 with Kirby 3. That’s not going to work exactly like this.

Using POST I think is the right way, but since you already set 'method' => 'POST', there is no need for that if statement if(r::is('POST')) { again.

Another thing that makes we wonder is why you create children of the error page. That shouldn’t be a thing.

Those get() calls won’t probably work either in this way in Kirby 3. Best would be to have a look how Kirby deals with file uploads in its core routes itself: https://github.com/getkirby/kirby/blob/master/config/api/routes/files.php

In Kirby 3 you can also return an array directly from a route and Kirby will make sure to deliver it as JSON. So it could simply be return $data.

This for a start. I’m not sure if your Javascript even send the file.

thanks for the answer!

both logging the file before sending it through xhr, as well as doing print_r(r::data()); show me i got the file sent correctly.

i checked the core routes for the files, and i think i could use this code if i pass the $path, $source and $filename?

how would i get access to this data? by grabbing it from r::data()?

'action'  => function (string $path) {
  return $this->upload(function ($source, $filename) use ($path) {
    return $this->parent($path)->createFile([
      'source'   => $source,
      'template' => $this->requestBody('template'),
      'filename' => $filename
    ]);
  });
}

Unfortunately I don’t have much time this weekend to go through it step by step. Only can you provide with pointers:

many thanks again.

i still am not sure how to set the $path variable in the function, though. i read the files you shared and it seems like $path is a variable i should set myself? what is its purpose? where the file will be saved?

coming from kirby 2 and liking the changes but still a bit confused.

chiming in on this again, as I wasn’t able to move on.

if any good soul can spend 10 mins to give more hints on how to make the file creation from a POST request work with the new kirby APIs, i’d be very grateful and i think it would be valuable for others as well.

:pray:

Could you share your current code, please? What is irritating me is that you want to create children of the error page. Also, the create method requires authentication.

ahah, i don’t wanna create children of the Error page, that was just a test to see if the folder name was correct!

this is my code so far, it’s a copy paste from the kirby own method to create files. i tried to set $path to $path='/path/to' but it gives errors.

<?php

Kirby::plugin('mooc/upload', [
  'routes' => [
    [
      'method' => 'POST',
      'pattern' => 'apiupload',
      'action'  => function (string $path) {
        return $this->upload(function ($source, $filename) use ($path) {
          return $this->parent($path)->createFile([
            'source' => $source,
            'template' => $this->requestBody('template'),
            'filename' => $filename
          ]);
        });
      }
    ]
  ]
]);

re: authentication, does that mean i need to be logged in to the panel?

thanks

Either logged in to the panel or you need to authenticate: Kirby's PHP API | Kirby CMS

OK, im back on this.

currently im logged in to the panel, i’ll figure the authentication part later once everything else is working.

if i upload the file from my frontend, i get back status code: 404 from the xhr function callback. my file is in the formData object though, so that should be good.

my current kirby file upload plugin looks like this, i added a value to the $path variable to try it out (i was getting an error that the closure function was expecting 1 argument and 0 were given).

<?php

Kirby::plugin('mooc/upload', [
  'routes' => [
    [
      'method' => 'POST',
      'pattern' => 'apiupload',
      'action'  => function (string $path = '/') {
        return $this->upload(function ($source, $filename) use ($path) {
          return $this->parent($path)->createFile([
            'source' => $source,
            'template' => $this->requestBody('template'),
            'filename' => $filename
          ]);
        });
      }
    ]
  ]
]);

any more hints on how to proceed with this? i feel like i’m almost there.

thankss

just thinking if this whole thing would be easier to do using the API methods?

quite slow on my side to realize this only now but when a month ago a approached the new kirby APIs it was quite confusing.

OK, so I went the kirby way, super happy about it.

  1. make a new blueprint for the API user role, eg site/blueprints/users/api.yml, with the following:
title: API
permissions:
  access:
    panel: true
    site: true
    settings: false
  files:
    *: true
  site:
    update: false
  pages:
    *: false
  user:
    *: false
  users:
    *: false

this allows the user to only work with files (upload, delete, update, etc). if you try to login to the panel, you will see only the first panel page with no visible pages and nothing else available. this is good.

  1. make a new panel user and set it to the API user role.

  2. add this to you site/config/config.php:

return [
  'api' => [
    'basicAuth' => true
  ]
];
  1. javascript frontend / custom upload function
function upload (e) {
  e.preventDefault()
  const form = e.currentTarget
  const file = form.files[0]

  let formData = new FormData()
  formData.append('file', file)

  const auth = Buffer.from([email,password].join(':')).toString('base64')

  xhr({
    method: 'post',
    headers: {
      Authorization: `Basic ${auth}`
    },
    uri: `/api/pages/${ state.page.id.replace('/', '+') }/files`,
    body: formData
  }, function (err, resp, body) {
    if (err) throw err
    console.log(resp)
  })
}

you’ll get a status: ok in the body of the xhr call response message! file uploaded.