nazirov91 / ra-strapi-rest

React Admin data provider for Strapi.js
125 stars 40 forks source link

File Upload #11

Closed yeung108 closed 4 years ago

yeung108 commented 4 years ago

Hi is is possible to handle file uploading to Strapi for some records in the future? I have tried using FormData and FileInput from react-admin but was not able to produce successful result. Thanks

nazirov91 commented 4 years ago

@yeung108 This feature is in progress. I will provide an update as soon as it is ready

devalexandre commented 4 years ago

I try do it too :), @nazirov91 have some alternate to make uploads?

nazirov91 commented 4 years ago

Yes, I am currently testing it. I will update the repo soon

devalexandre commented 4 years ago

I use 64encode and save i text field in strapi

nazirov91 commented 4 years ago

@yeung108 @devalexandre Hi guys,

I have added the file upload feature to the data provider. Could you please try it on your projects and let me know if it works correctly? Instructions are given in the Readme file on the main page of this repo.

Thank you!

ismatim commented 4 years ago

@nazirov91: Thank you for the effort to do this adapter ! It really looks good. I'm having some troubles trying to upload an image and I thought you might want to know.

I followed your instructions but I got this error:

[2020-03-22T16:02:04.873Z] error TypeError: Cannot read property 'toString' of undefined at /../backend/node_modules/strapi-connector-mongoose/lib/relations.js:288:24 at Array.map (<anonymous>) at /../backend/node_modules/strapi-connector-mongoose/lib/relations.js:287:68 at Array.reduce (<anonymous>) at Function.update [as updateRelations] (/../backend/node_modules/strapi-connector-mongoose/lib/relations.js:38:59) at runMicrotasks (<anonymous>) at processTicksAndRejections (internal/process/task_queues.js:85:5) at async Object.update (/../backend/node_modules/strapi/lib/services/entity-service.js:91:17) at async Object.update (/../backend/node_modules/strapi/lib/core-api/controller.js:125:18) at async /../backend/node_modules/strapi/lib/middlewares/router/utils/routerChecker.js:77:22 at async module.exports (/../backend/node_modules/strapi-plugin-users-permissions/config/policies/isAuthenticated.js:6:3) at async Object.module.exports [as isauthenticated] (/../backend/node_modules/strapi-plugin-users-permissions/config/policies/isAuthenticated.js:6:3) at async module.exports (/../backend/node_modules/strapi-plugin-users-permissions/config/policies/permissions.js:106:12) at async /../backend/node_modules/strapi-utils/lib/policy.js:52:5

My component looks like:

      <ImageInput source="image" label="New Image" accept="image/*">
        <ImageField source="url" title="Record image" />
      </ImageInput>

The records.setting.json

    "image": {
      "model": "file",
      "via": "related",
      "plugin": "upload",
      "required": false
    }

In the App.js

const uploadFields = ["image"];
const dataProvider = strapiProvider('http://localhost:1337', httpClient, uploadFields);

What am I missing ?

nazirov91 commented 4 years ago

Hi @ismatim,

I was able to reproduce the issue you are having. And after doing some research I realized that the issue is probably related to a bug instrapi-connecter-mongoose, not react-admin. Maybe this one https://github.com/strapi/strapi/issues/4918? I am not sure.

But I have tested this upload feature with SqlLite3, MySql and Postgres, and those are working fine.

Unfortunately, I have to assume that the upload feature for this data provider won't work until somebody fixes the bug in the mongoose connector.

If you do find a workaround for this, please let me know.

Thanks! Sardor

ismatim commented 4 years ago

Hey @nazirov91 thanks for taking the time!

I kept checking and I found that the issue might come from the strapi.entityService.uploadFile

I override the create function in the controller and the services:

  create: async ctx => {
    let entity;
    if (ctx.is('multipart')) {
      const { data, files } = parseMultipartData(ctx);
      entity = await strapi.services.records.create(data, { files });
    } else {
      entity = await strapi.services.records.create(ctx.request.body);
    }
    return sanitizeEntity(entity, { model: strapi.models.records });
  }

The controller works fine but, in the service:

  async create(data, { files } = {}) {
    console.log('service:create');
    console.log('data:');
    console.log(data)
    delete data.image // I got to delete this to create the entity
    const entry = await strapi.query('records').create(data);
    console.log(files);
    if (files) {
      // automatically uploads the files based on the entry and the model
      await strapi.entityService.uploadFiles(entry, files, {
        model: strapi.models.records,
      });  //but, still got an exception here!
      return this.findOne({ id: entry.id });
    }
    return entry;
  }

I didn't get along enough with strapi.entityService to know what's happening buy you might be right it has something to do with mongoose-connector !

nazirov91 commented 4 years ago

Are you able to upload images through Postman?

ismatim commented 4 years ago

Yes, it works to the endpoint /upload.

nazirov91 commented 4 years ago

I think I found a solution. Can you try the following change and see if it works?

Inside the index.js (aka ra-strapi-rest.js), there is function called handleFileUpload. Inside the for loop, there is this piece

data[fieldName] = [...newFilesToAdd, ...existingFiles];

So comment this line out, and add this piece instead

const existingFileIds = [];
for (let ef of existingFiles) {
    existingFileIds.push(ef._id);
}
data[fieldName] = [...existingFileIds];

Your handleFileUpload function should look like this:

const handleFileUpload = (type, resource, params, uploadFieldNames) => {
    const {
        created_at,
        updated_at,
        createdAt,
        updatedAt,
        ...data
    } = params.data;
    const id = type === UPDATE ? `/${params.id}` : "";
    const url = `${apiUrl}/${resource}${id}`;
    const requestMethod = type === UPDATE ? "PUT" : "POST";
    const formData = new FormData();
    const newFilesToAdd = [];

    for (let fieldName of uploadFieldNames) {
        let fieldData = params.data[fieldName];
        fieldData = !Array.isArray(fieldData) ? [fieldData] : fieldData;
        let newFiles = fieldData.filter(f => f.rawFile instanceof File);
        let existingFiles = fieldData.filter(f => !(f.rawFile instanceof File));

        for (let newFile of newFiles) {
            newFilesToAdd.push({
                src: newFile.rawFile,
                title: params.data.title
            });
            formData.append(`files.${fieldName}`, newFile.rawFile);
        }

        //data[fieldName] = [...newFilesToAdd, ...existingFiles];

        const existingFileIds = [];
        for (let ef of existingFiles) {
            existingFileIds.push(ef._id);
        }
        data[fieldName] = [...existingFileIds];
    }
    formData.append("data", JSON.stringify(data));

    return httpClient(url, {
        method: requestMethod,
        body: formData
    }).then(response => ({ data: response.json }));
};
nazirov91 commented 4 years ago

Also revert back all the overrides you did in Strapi controllers

ismatim commented 4 years ago

Yes, it works perfectly ! Without the revert I did receive the message of error about attributes but, anyway, It's already working perfectly ! 👏 👏 👏

Are you going to add the change ?

Thank you !

nazirov91 commented 4 years ago

Sweet! Yes, I will add this change. I just have to find a clean way to identify the db type and run different logics for file IDs

nazirov91 commented 4 years ago

Thank you for discovering this issue and reporting!

ismatim commented 4 years ago

Sweet! Yes, I will add this change. I just have to find a clean way to identify the db type and run different logics for file IDs

I think, the simplest way is to added to the constructor, I doubt there is a way to get that from strapi api.

Do you know if the property _id is not needed for other DBs ? I don't think that might affect others connectors.

What I read and understood so far is that Strapi has better support for mongoose than other dbs.

Ping me back if you need any help.

nazirov91 commented 4 years ago

I actually found an even better way to handle SQL and NoSQL DBs. Take a look at the latest index.js file if you are interested 😃

Turns out we do not need to know the DB type. We just need to provide the IDs of the existing files during (only) UPDATE. And that can be done by simply adding this logic item.id || item._id.

ismatim commented 4 years ago

Exactly, that's what I thought. I've already downloaded.

BTW, did you try the multiple delete from react-admin list ? Check on each row and then delete them ? I couldn't but, individually it works. Should I create an issue for that ?

nazirov91 commented 4 years ago

What kind of error are you getting? I just tested it with MongoDB and it worked fine. I had 5 items on the list, I randomly selected 2 of them, and deleted. Both items were deleted successfully. If it is not too much trouble for you, please go ahead and create an issue. We will figure it out

ismatim commented 4 years ago

What kind of error are you getting? I just tested it with MongoDB and it worked fine. I had 5 items on the list, I randomly selected 2 of them, and deleted. Both items were deleted successfully. If it is not too much trouble for you, please go ahead and create an issue. We will figure it out

Yes, you're right. :)

nazirov91 commented 4 years ago

File upload functionality is working fine with all available databases. Closing this issue. Thanks!