Upload multiples files to an entry after its creation (and how to add a caption ?)

System Information
  • Strapi Version: 3.2.3
  • Operating System: alpine3.12
  • Database: postgres 13
  • Node Version: node 12.19
  • NPM Version: ?
  • Yarn Version:

Hello, community,

I am trying to upload multiples files to an entry already created.

async addImagesToPost(images, postId) {
  const bodyFormData = new FormData()

  bodyFormData.append('ref', 'posts')
  bodyFormData.append('refId', postId)
  bodyFormData.append('field', 'images')
  images.forEach(({ file }) => bodyFormData.append(`files.images`, file, file.name))

  try {
    // the client is an axios instance.
    return await client.post('/upload', bodyFormData)
  } catch (error) {
    console.error("error while adding images to post", error)
    return error.response
  }
}

This is the code I am using and it doesn’t work. Even if instead of using files.images I am using files[] (which a standard look at here in the example) or 'files.' + index it does not works.

If I am uploading a single file it works bodyFormData.append(files, images[0].file, images[0].name)

They said how to upload multiples files at the same time as creating the entry but not when we want to add some files to an entry.

At the same time in the documentation there no mention of how to add a caption for instance on an image.

If somebody has the answer it will be great :slight_smile:

Everything looks fine, expect:

It should be just files, without .images

images.forEach(({ file }) => bodyFormData.append(`files`, file, file.name))

I don’t know if you are uploading from backend or frontend, or if you set the headers to the axios instance, but if you use axios on the backend, then I want to mention that you need to use getHeaders() in Node.js because Axios doesn’t automatically set the multipart form boundary in Node.

So for the backend, the correct axios request will be:

return await client.post('/upload', bodyFormData,{
    headers: {
      ...data.getHeaders(),
    },
  })
1 Like

I am from the client-side. Okay so just uploading to files works with multiples files too.
And for the caption, there is any way to add it? Or do I need to create a route specifically for updating an image id with a caption?

Yes, you can add them, in fileInfo:

bodyFormData.append('fileInfo', '{"caption":"caption text","alternativeText":"alternative text"}');

Currently, there is no way to add caption and alternativeText for multiple files, you can send it only for a single file. If you send it with multiple files then only the first one will get them.

I would recommend sending each file individually to the /upload path, something like:

   images.forEach(({ file }) => {
        const bodyFormData = new FormData()
        bodyFormData.append('ref', 'posts')
        bodyFormData.append('refId', postId)
        bodyFormData.append('field', 'images')
        bodyFormData.append(`files`, file)
        bodyFormData.append('fileInfo', `{"caption":"${file.caption}","alternativeText":"${file.alternativeText}","name":"${file.name}"}`);
        client.post('/upload', bodyFormData)
    })
2 Likes

Amazing, you should add it to the documentation! It’s a must-have information :slight_smile:

1 Like

Yeah, a lot of things are not documented yet. Also a lot of cool things are made by contributors and not all of them update the documentation.

I am doing the same
Collection: article
field: image

new Image is being uploaded but the id is not being updated in article entry nor the image caption and alternative text.

Actually you can send multiple fileInfo objects (1 for each file you are uploading). Works great.

Can you show an example of multiple fileInfo objects with captions? I’m currently trying to do it and not having any luck

I am not using Javascript for my client code but here is an example that should be close.

Also, don’t you need the full api name for the ref? Instead of ‘posts’, it would be ‘api::post.post’.
Cool if that works for you.

 let fileInfos = [];
    const bodyFormData = new FormData()
    bodyFormData.append('ref', 'posts')
    bodyFormData.append('refId', postId)
    bodyFormData.append('field', 'images')   
   images.forEach(({ file }) => {
        bodyFormData.append(`files`, file)
        fileInfos.push({"caption": file.caption, "alternativeText": file.alternativeText, "name": file.name})
    })
    bodyFormData.append('fileInfo', JSON.stringify(fileInfos));
    client.post('/upload', bodyFormData)

Hmm, I couldn’t get it working. I didn’t really need it to work but was testing something out.

Can I ask you something slightly different? When I’m uploading images, I do it with a custom controller while the entry is created. The code you have above is after the entry is created and you reference it with the refId. Should I be doing this also? The reason I ask is when I delete an entry the images accciated are not being deleted from the media library.

const entity = await strapi.service("api::listing.listing").create({
      data: {
        contact_firstname: listing.contact_firstname,
        contact_lastname: listing.contact_lastname,
        contact_phone: listing.contact_phone,
        contact_email: listing.contact_email,
        contact_city: listing.contact_city,
        contact_state: listing.contact_state,
        title: listing.title,
        price: listing.price,
        description: listing.description,
        category: listing.category,
        featured: listing.featured,
        publishedAt: listing.publishedAt,
        session_id: session.id,
      },
      files,
    });

Hey @thanneman
To answer your questions:

You don’t necessarily need a custom controller to add media files during entity creation. As long as you set the form data fields from the Strapi docs (being sure to start all of the file objects with ‘files.’ And specify the field that the belong to using the input field.

You are right that the code I showed was when adding media files to and existing entry. No need to specify these on creating a new entry.

Strapi does not automatically remove the media files associated with a content type record when the record is deleted. I created a util service (see below) that I call from a custom controller that overrides the default delete method and calls a util method to delete any related files

I too have created custom code (a util service, and custom plugin extension code for user registration) that handles several file upload challenges like:

Uploading images (like an Avatar) during user registration

Passing fileInfo information for any files

Deleting files when a content type that has associated media fields is deleted (No, Strapi doesn’t automatically do this)

The ability to update fileInfo fields for an existing media file (without have to re-upload the files).

Thanks for the insight. Would you mind sharing the util service you use for this?

First create a new utils service from your project root:

npx strapi generate service utils

Now copy in the code contents to
/src/api/utils/services/utils.js

'use strict';

/**
 * My Strapi utils service.
 */

 module.exports = ({ strapi }) => ({
 	
		async deleteFilesFromEntity(ctx, entity, field) {
			if (entity != null && entity[field] != null) {
				var params;
				for (var i = 0; i < entity[field].length; i++) {
					params = {id:  entity[field][i].id };
					await strapi.plugins['upload'].controllers['content-api'].destroy({state: ctx.state, params: params});
				}
			}
		},
		
		async updateFileInfo(user, fileInfo) {
			for (var i = 0; i < fileInfo.length; i++) {
				if(fileInfo[i].id && fileInfo[i].id>0) {
					await strapi.plugin('upload').service('upload').updateFileInfo(fileInfo[i].id, {alternativeText: fileInfo[i].alternativeText, caption: fileInfo[i].caption}, user);
				}
			}
		},		 	
 	
 	
 }); 	

Here is an example of a ‘restaurant’ content type with a media field called ‘images’. When a restaurant record is deleted, then the associated images will also be deleted. Please note that in this example I assume that the media files are not being shared amongst multiple records.

/src/api/restaurant/controllers/restaurant.js contents:

'use strict';

/**
 *  restaurant controller
 */

const { createCoreController } = require('@strapi/strapi').factories;

module.exports = createCoreController('api::restaurant.restaurant' , ({ strapi }) => ({
     
  async delete(ctx) {
    const user = ctx.state.user;
    if (!user) return ctx.badRequest(null, [{ messages: [{ id: 'No authorization header was found' }] }]);
    const {id} = ctx.params;
    const restaurant = await strapi.entityService.findOne('api::restaurant.restaurant', id, {
      populate: ['images'],
    });			
    
    if (restaurant) {
      // delete related files
      await strapi.service('api::utils.utils').deleteFilesFromEntity(ctx, restaurant, 'images');
    }
    const response = await super.delete(ctx);
    return response;
  }
}));

Awesome this works great! Thank you.

Do you know of a way to use this service for the content-manager as well? It only deletes the images when using the API but not when deleting from the admin panel

The admin is a separate api, and I am sure it can be also programmed, but I haven’t looked at doing that.

Hey I’m using the Upload file during entry creation"

It’s working and uploading the files when I create my entry:

const formData = new FormData()
formData.append('data', JSON.stringify({
  title,
  // etc
}))

data.images.forEach((f) => {
  formData.append(`files.images`, f, f.name)
})

data.documents.forEach((f) => {
  formData.append(`files.documents`, f, f.name)
})

fetch(`http://localhost:1337/api/my-entries`, {
  method: 'post',
  headers: new Headers({
    'Authorization': `Bearer ${jwt}`,
  }),
  body: formData,
})

But I’d like to add the fileInfo to add caption etc.
I have tried doing various things but can’t seem to get anything to work e.g.:

data.images.forEach((f) => {
  formData.append(`files.images`, f, f.name)
  formData.append(`fileInfo.images`, JSON.stringify({ caption: "test" }))
})

I’m trying to work out how to set caption and alternativeText and also the order field

Any ideas @cajazzer thanks!

Hey @ptimson. The secret to finding out what the upload plugin expects for fileInfo entries is to look in this file:
[YourProject]/node_modules/@strapi/plugin-upload/server/services/upload.js
If you look at the upload method, you will see that the fileInfo is being extracted from the data object being passed. Also, look at the formatFileInfo method. I believe if you create a fileInfo array of objects with one entry for every file, and add the array to your ‘data’ object before stringifying it, the upload method should recognize it. Maybe something like this:

const fileInfo = [
	{
		"caption" : "file1 Caption",
		"alternativeText" : "file1 alt text"
	},
	{
		"caption" : "file2 Caption",
		"alternativeText" : "file2 alt text"
	},
];

const data = {};
data['fileInfo'] = fileInfo;
formData.append('data', JSON.stringify(data));

Hey @cajazzer thanks for your response.

That’s one of the things I tried. That def works if you are using the /upload API but I am trying to upload against entry creation API /api/<entry>.

In my <entry> I have 2 media types:

"images": {
  "allowedTypes": [
    "images"
  ],
  "type": "media",
  "multiple": true
},
"documents": {
  "allowedTypes": [
    "files"
  ],
  "type": "media",
  "multiple": true
}

So when I upload from the browser I do:

formData.append('data', JSON.stringify({
 // fields
}))

data.images.forEach((f) => {
  formData.append(`files.images`, f, f.name)
})

data.documents.forEach((f) => {
  formData.append(`files.documents`, f, f.name)
})

That’s why I thought setting fileInfo.image may work. But yes setting that or just fileInfo doesn’t seem to work.

These are what I’ve tried so far:

// 1
formData.append(`fileInfo.images`, JSON.stringify([{ caption: 'test' }]))

// 2
formData.append(`fileInfo`, JSON.stringify([{ caption: 'test' }]))

// 3 (your suggestion)
formData.append('data', JSON.stringify({
  // fields
  fileInfo: [
    { caption: 'test' }
  ]
})

// 4
formData.append('data', JSON.stringify({
  // fields
  fileInfo: {
    images: [
      { caption: 'test' }
    ]
  }
})

// 5 - This errors -  error: Invalid id, expected a string or integer, got [object Object]
formData.append('data', JSON.stringify({
  // fields
  images: [
    { caption: 'test' }
  ]
}))

I’ll have another dig into the upload plugin but I think strapi may be handling these /api/entry things differently and then passing down to upload plugin

@ptimson. Yes, you are correct. I used Postman to create a simple client to Post and I debugged into the upload code I mentioned. It never got called. I then found where the content type and associated files is created in the create method of [YourProject]/node_modules/@strapi/strapi/lib/services/entity-service/index.js. The fileInfo is totally ignored as far as I can see. Sorry I was not more help.