Skip to main content

Frosty CMS: Image Uploads ๐Ÿ“ท

Introโ€‹

Up until now we have been serving images by hotlinking to images on other servers (specifically, this server, my blog server, not great). Now we are going to change that by implementing an actual image upload.

Packagesโ€‹

This is going to take two packages. Multer and Cloudinary.

Multer is the middleware that will handle the multipart/form-data submissions. Cloudinary is the package for the Cloudinary media hosting service. There are other ways to accomplish this, this is just what Iโ€™m going for. We get our packages setup and then we move on.

Upload Media Formโ€‹

We need to update our form with an enctype for multipart/form-data. Thank you multer for handling this.

<form action="/blogs" method="POST" enctype="multipart/form-data">

<div class="form-group">
<label for="featuredImageUpload">
<h4>Featured Image</h4>
</label>
<input type="file" id="featuredImageUpload" name="blog[image]" accept="image/*" required>
</div>

We have also changed our input type to file instead of string.

Schema Changesโ€‹

In our schema for blogs we still have image set as a string, because we are going to be sending our images to Cloudinary and then they are going to be responding with a URL, which we can store in a string, along with an image ID, which will remain constant when we update our images.

const blogSchema = new mongoose.Schema({
image: String,
imageId: String,
title: String,
author: {
id: {
type: mongoose.Schema.Types.ObjectID,
ref: "User"
},
username: String,
firstname: String,
lastname: String
},
date: {type: Date, default: Date.now},
short: String,
content: String,
comments: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Comment"
}
]
});

Package Configurationโ€‹

There is a decent amount of package configuration that we need to go through here.

// ***************************
// IMPORT PACKAGES
// ***************************
const express = require("express"),
router = express.Router({mergeParams: true}),
middleware = require('../middleware'),
moment = require('moment'),
multer = require('multer'),
storage = multer.diskStorage({
filename: (req, file, callback)=>{
callback(null, Date.now() + file.originalname);
}
}),
imageFilter = (req, file, cb)=>{
// accept image files only
if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/i)) {
return cb(new Error('Only image files are allowed!'), false);
}
cb(null, true);
},
upload = multer({storage: storage, fileFilter: imageFilter}),
cloudinary = require('cloudinary'),
Blog = require('../models/blogs');

// ***************************
// Cloudinary Config
// ***************************
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});

One particular thing to note is that we need to store our cloudinary config information in process.env variables with dotenv, which I covered in the linked post.

Route Changesโ€‹

And of course we have big changes coming into our routes. We have a new middleware that sends our files to Cloudinary and then gets back our secure URL and imageID that we store as strings with our blog object. Here is the route for new blogs.

//----------------------------
// .POST routes
//----------------------------

// new blog: receive and save
router.post("/", middleware.isLoggedIn, upload.single('blog[image]'), (req, res) => {
// sanitize inputs
req.body.blog.title = req.sanitize(req.body.blog.title);
req.body.blog.short = req.sanitize(req.body.blog.short);
req.body.blog.content = req.sanitize(req.body.blog.content);

// assign variables to incoming data
let title = req.body.blog.title,
short = req.body.blog.short,
content = req.body.blog.content,
date = req.body.blog.date;

// retriever user data
let author = {
id: req.user._id,
username: req.user.username,
firstname: req.user.firstname,
lastname: req.user.lastname
};

// add cloudinary url for the image to the blog object under image property
cloudinary.v2.uploader.upload(req.file.path, (error, result)=> {
console.log(result, error);

let image = result.secure_url,
imageId = result.public_id;

// combine all data into new variable
let newBlog = {title: title, image: image, imageId: imageId, short: short, content: content, date: date, author: author};

// save combined data to new blog
Blog.create(newBlog,(err, newDatabaseRecord) => {
if(err){
console.log(err);
req.flash('error', "Failed to write post to database.");
res.redirect('back');
return;
} else {
console.log("Blog successfully saved to database.");
console.log(newDatabaseRecord);
req.flash('success', 'New blog saved to database.');
// redirect back to blogs page
res.redirect("/");
return;
}
});
});
});

and then our route for blog updating needs to first check if the image was changed, and only then delete the old image and upload a new one.

//----------------------------
// .PUT routes
//----------------------------

// edit blog
router.put("/:id",middleware.isLoggedIn, upload.single('blog[image]'), (req, res) => {
// only upload new photo if requested
Blog.findById(req.params.id, async function(err, blog){
if(err){
req.flash('error', err.message);
res.redirect('back');
return;
} else {
if (req.file) {
try {
await cloudinary.v2.uploader.destroy(blog.imageId);
let newImage = await cloudinary.v2.uploader.upload(req.file.path);
blog.imageId = newImage.public_id;
blog.image = newImage.secure_url;

} catch(err) {
req.flash('error', err.message);
res.redirect('back');
return;
}
}
// sanitize inputs
req.body.blog.title = req.sanitize(req.body.blog.title);
req.body.blog.short = req.sanitize(req.body.blog.short);
req.body.blog.content = req.sanitize(req.body.blog.content);

// assign variables to incoming data
blog.title = req.body.blog.title;
blog.author = blog.author;
blog.short = req.body.blog.short;
blog.content = req.body.blog.content;
blog.date = req.body.blog.date;
blog.imageId = blog.imageId;
blog.image = blog.image;

blog.save();
req.flash('success', "Blog updated.");
res.redirect('/blogs/' + blog._id);
return;
}
});
});

And we now have the ability to upload files for our blogs.

image upload preview

Git Repoโ€‹

Ncoughlin: Frosty

Comments

Recent Work

Free desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.

Learn More

BidBear

bidbear.io

Bidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.

Learn More