0
votes

I can successfully upload an image using the multer-storage-cloudinary package but I have not pieced together an implementation for how to destroy the image in cloudinary from my update or delete route. All resources I have found only reference uploading images. Here is what I have to upload the image

const cloudinary = require('cloudinary').v2;
const { CloudinaryStorage } = require('multer-storage-cloudinary');
const multer = require('multer');

cloudinary.config({
  cloud_name: process.env.CLOUD_NAME,
  api_key: process.env.API_KEY,
  api_secret: process.env.API_SECRET
});

const storage = new CloudinaryStorage({
  cloudinary: cloudinary,
  params: {
    folder: "works",
    allowedFormats: ["jpg", "png"]
  }
});
const imgUpload = multer({ storage: storage });

router.post('/', withAuth, imgUpload.single('work-img'), (req, res) => {
  console.log(req.file);

  Post.create({
        title       : req.body.title,
        dimension   : req.body.dimensions,
        description : req.body.description,
        media       : req.body.media,
        img_url     : req.file.path,
        user_id     : req.session.user_id
    })
        .then((dbPostData) => res.json(dbPostData))
        .catch((err) => {
            console.log(err);
            res.status(500).json(err);
        });
});

Here is the front-end code that handles the form submission

async function newFormHandler(event) {
    event.preventDefault();

    const form = document.querySelector('#new-post-form');

    const formData = new FormData(form);

    const response = await fetch(`/api/posts`, {
        method : 'POST',
        body   : formData
    });

    if (response.ok) {
        document.location.replace('/dashboard');
    } else {
        alert(response.statusText);
    }
}

document.querySelector('#new-post-form').addEventListener('submit', newFormHandler);

And finally my update route which works to upload a new image and save it's new path to the database, but I also want to remove the old image from Cloudinary so I don't have unused artifacts building up there.

router.put('/:id', withAuth, imgUpload.single('work-img'), (req, res) => {
    Post.update(
        {
            title       : req.body.title,
            dimension   : req.body.dimensions,
            description : req.body.description,
            media       : req.body.media,
            img_url     : req.file.path
        },
        {
            where : {
                id : req.params.id
            }
        }
    )
        .then((dbPostData) => {
            if (!dbPostData) {
                res.status(404).json({ message: 'No post found with this id' });
                return;
            }
            res.json(dbPostData);
        })
        .catch((err) => {
      console.log(err);
            res.status(500).json(err);
        });
});

If I look at the console.log(req.file) in the post route, it gives me something like this

{
  fieldname: 'work-img',
  originalname: '20171005_134508.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  path: 'https://res.cloudinary.com/xxxxxxxxx/image/upload/xxxxxxxx/works/ujrrf13kyil8l5rjccwf.jpg',
  size: 3647252,
  filename: 'works/ujrrf13kyil8l5rjccwf'
}
According to the Cloudinary API docs, the response looks much different with keys such as "public_id".

{
  "public_id": "eneivicys42bq5f2jpn2",
  "version": 1570979139,
  "signature": "abcdefghijklmnopqrstuvwxyz12345",
  "width": 1000,
  "height": 672,
  "format": "jpg",
  "resource_type": "image",
  "created_at": "2017-08-11T12:24:32Z",
  "tags": [],
  "bytes": 350749,
  "type": "upload",
  "etag": "5297bd123ad4ddad723483c176e35f6e",
  "url": "http://res.cloudinary.com/demo/image/upload/v1570979139/eneivicys42bq5f2jpn2.jpg",
  "secure_url": "https://res.cloudinary.com/demo/image/upload/v1570979139/eneivicys42bq5f2jpn2.jpg",
  "original_filename": "sample",
  "eager": [
    { "transformation": "c_pad,h_300,w_400",
      "width": 400,
      "height": 300,
      "url": "http://res.cloudinary.com/demo/image/upload/c_pad,h_300,w_400/v1570979139/eneivicys42bq5f2jpn2.jpg",
      "secure_url": "https://res.cloudinary.com/demo/image/upload/c_pad,h_300,w_400/v1570979139/eneivicys42bq5f2jpn2.jpg" },

I could then use 'public_id' with the destroy method like so, but I don't have access to the public_id to configure this: cloudinary.v2.uploader.destroy(public_id, options, callback);

I am unclear how to implement this or if it is even possible using the multer-storage-cloudinary package

2

2 Answers

1
votes

So for anyone who may be interested the way I ended up doing this was as follows

router.put('/:id', withAuth, imgUpload.single('work-img'), (req, res) => {
    // if there was a picture updated -- NOTE THIS TECHNIQUE IS NOT DRY, IS THERE A WAY TO REFACTOR?
    if (req.file) {
        // find old public_id for image so we can delete it from cloudinary before updating the db with new path
        Post.findOne(
            {
                where : {
                    id : req.params.id
                }
            },
            {
                attributes : [ 'title', 'public_id' ]
            }
        )
            .then((oldPostData) => {
                const oldPublicId = oldPostData.get({ plain: true });

                // remove old image from cloudinary db
                cloudinary.uploader.destroy(oldPublicId.public_id, (err) => {
                    console.log(err);
                    console.log(oldPublicId, ' deleted');
                });

                // not in cloudinary callback since deletion from cloudinary is not critical to UX
                Post.update(
                    {
                        title       : req.body.title,
                        artist_name : req.body.artist,
                        dimension   : req.body.dimensions,
                        description : req.body.description,
                        media       : req.body.media,
                        img_url     : req.file.path,
                        public_id   : req.file.filename
                    },
                    {
                        where : {
                            id : req.params.id
                        }
                    }
                ).then((newPostData) => {
                    if (!newPostData) {
                        res.status(404).json({ message: 'No post found with this id' });
                        return;
                    }

                    req.flash('success', 'Your work has been updated!');
                    res.json(newPostData);
                });
            })
            .catch((err) => {
                console.log(err);
                res.status(500).json(err);
            });
    // in the else i just added the post to the db without the image file.
    } else {...}

as discussed above, the public_id is what you use to access the image in cloudinary. so the multer-storage-cloudinary middleware takes care of grabbing the image from the form and uploading it directly up to cloudinary then packages the response from cloudinary up into the req.file which multer usually adds to the req object. (Obviously the schema needs to be updated with the new public_id field). in the PUT callback, query the db for the post and get the old public_id. parse this and pass it to cloudinay.uploader.destroy() method. You'll need to require cloudinary if your route is in a separate file from your imgUpload configuration. then you can update your db from inside the cloudinary callback or not since they don't depend on each other and the cloudinary operation is not critical to the user experience.

0
votes

For deletion, public_id is required. If you do not get public_id in the response when using multer-storage-cloudinary, you can pass it on upload as a param and that will be the name of the image on Cloudinary. To delete the image you can simply use that public_id. Or you could use cloudinary.uploader.upload function and that will return the JSON with the public_id and other details as you mentioned in the sample response.