API File and Image Uploads
This guide gives code examples on how to set up an API for File and Image transfers to your KeystoneJS installation. It also shows how to set up the server for local file and image hosting. These examples contain the same code used by ConnextCMS.
Setup
This guide assumes that you have a working version of
KeystoneJS. All file path
references assume that you are working from the same directory as the
keystone.js
file.
Creating Models
This guide does not treat images differently than generic files, since they are all files to KeystoneJS. The difference is how you use them on the front end once you retrieve the file url from the server. With a little modification of the code below, you can create a different file model called ‘images.js’ along with a corresponding API and send ‘images’ to their own directory.
At any rate, below is an example model that leverages the Types.File
introduced in KeystoneJS v4.0 Beta. This code should be copied into the
file models/FileUpload.js
.
var keystone = require('keystone');
var Types = keystone.Field.Types;
/**
* File Upload Model
* ===========
* A database model for uploading images to the local file system
*/
var FileUpload = new keystone.List('FileUpload');
var myStorage = new keystone.Storage({
adapter: keystone.Storage.Adapters.FS,
fs: {
path: keystone.expandPath('./public/uploads/files'), // required; path where the files should be stored
publicPath: '/public/uploads/files', // path where files will be served
}
});
FileUpload.add({
name: { type: Types.Key, index: true},
file: {
type: Types.File,
storage: myStorage
},
createdTimeStamp: { type: String },
alt1: { type: String },
attributes1: { type: String },
category: { type: String }, //Used to categorize widgets.
priorityId: { type: String }, //Used to prioritize display order.
parent: { type: String },
children: { type: String },
url: {type: String},
fileType: {type: String}
});
FileUpload.defaultColumns = 'name';
FileUpload.register();
A lot of the additional fields like alt1
, or category
are metadata
and may be unnecessary for your purposes. I put them in the model as
suggestions. Feel free to take them out, and it won’t hurt anything if
you leave them in
Opening an API in KeystoneJS
Now we are going to create an API that can be used to upload and
download files to KeystoneJS. This is a two step process. The first step
is add a few lines of code to the routes/index.js
file:
var keystone = require('keystone');
var middleware = require('./middleware');
var importRoutes = keystone.importer(__dirname);
// Common Middleware
keystone.pre('routes', middleware.initLocals);
keystone.pre('render', middleware.flashMessages);
// Import Route Controllers
var routes = {
views: importRoutes('./views'),
api: importRoutes('./api') // ADD THIS LINE TOO!
};
// Setup Route Bindings
exports = module.exports = function (app) {
// Views
app.get('/', routes.views.index);
app.get('/blog/:category?', routes.views.blog);
app.get('/blog/post/:post', routes.views.post);
app.get('/gallery', routes.views.gallery);
app.all('/contact', routes.views.contact);
// COPY THE CODE FROM HERE...
//File Upload Route
app.get('/api/fileupload/list', keystone.middleware.api, routes.api.fileupload.list);
app.get('/api/fileupload/:id', keystone.middleware.api, routes.api.fileupload.get);
app.all('/api/fileupload/:id/update', keystone.middleware.api, routes.api.fileupload.update);
app.all('/api/fileupload/create', keystone.middleware.api, routes.api.fileupload.create);
app.get('/api/fileupload/:id/remove', keystone.middleware.api, routes.api.fileupload.remove);
// ...TO HERE.
// NOTE: To protect a route so that only admins can see it, use the requireUser middleware:
// app.get('/protected', middleware.requireUser, routes.views.protected);
};
Create the API Handler
The second step is to create the new file routes/api/fileupload.js
. If
the routes/api
directory doesn’t exist, you’ll need to create it. Copy
the code below into routes/api/fileupload.js
.
var async = require('async'),
keystone = require('keystone');
var exec = require('child_process').exec;
var FileData = keystone.list('FileUpload');
/**
* List Files
*/
exports.list = function(req, res) {
FileData.model.find(function(err, items) {
if (err) return res.apiError('database error', err);
res.apiResponse({
collections: items
});
});
}
/**
* Get File by ID
*/
exports.get = function(req, res) {
FileData.model.findById(req.params.id).exec(function(err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
res.apiResponse({
collection: item
});
});
}
/**
* Update File by ID
*/
exports.update = function(req, res) {
FileData.model.findById(req.params.id).exec(function(err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
var data = (req.method == 'POST') ? req.body : req.query;
item.getUpdateHandler(req).process(data, function(err) {
if (err) return res.apiError('create error', err);
res.apiResponse({
collection: item
});
});
});
}
/**
* Upload a New File
*/
exports.create = function(req, res) {
var item = new FileData.model(),
data = (req.method == 'POST') ? req.body : req.query;
item.getUpdateHandler(req).process(req.files, function(err) {
if (err) return res.apiError('error', err);
res.apiResponse({
file_upload: item
});
});
}
/**
* Delete File by ID
*/
exports.remove = function(req, res) {
var fileId = req.params.id;
FileData.model.findById(req.params.id).exec(function (err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
item.remove(function (err) {
if (err) return res.apiError('database error', err);
//Delete the file
exec('rm public/uploads/files/'+fileId+'.*', function(err, stdout, stderr) {
if (err) {
console.log('child process exited with error code ' + err.code);
return;
}
console.log(stdout);
});
return res.apiResponse({
success: true
});
});
});
}
Create the upload directory
Finally, you’ll want to create the public/uploads/files
directory.
This is where files will be end up when uploaded via the API.
Upload a file
That’s it! Your new API is ready to use. Drop the following code into
public/fileAPITest.html
, start KeystoneJS, and open the test file in
your web browser. If KeystoneJS is already running you’ll need to kill
the process and restart it. If KeystoneJS won’t start after you create
the new API files, go back and check your code.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
<h1>Upload a new file</h1>
<form id="form2" action="/api/fileupload/create" method="POST" enctype='multipart/form-data'>
<div>
<input type="file" id="file_upload" />
<br>
<label for="file_name">Give the file a name:<input type="text" name="file_name" id="file_name" /></label>
</div>
<br>
<div>
<center><input type="button" value="Upload" onclick="uploadFile()"></center>
</div>
</form>
<br>
<div>
<h2>Uploaded File List:</h2>
<ul id="file_list">
</ul>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
//debugger;
});
function uploadFile() {
//debugger;
var selectedFile = $('#file_upload').get(0).files[0];
//Error handling
if(selectedFile == undefined)
alert('You did not select a file!');
//Create the FormData data object and append the file to it.
var newFile = new FormData();
newFile.append('file_upload', selectedFile); //This is the raw file that was selected
//Set the form options.
var opts = {
url: '/api/fileupload/create',
data: newFile,
cache: false,
contentType: false,
processData: false,
type: 'POST',
//This function is executed when the file uploads successfully.
success: function(data){
//Dev Note: KeystoneAPI only allows file and image uploads with the file itself. Any extra metadata will have to
//be uploaded/updated with a second call.
//debugger;
console.log('File upload succeeded! ID: ' + data.file_upload._id);
//Fill out the file metadata information
data.file_upload.name = $('#file_name').val();
data.file_upload.url = '/uploads/files/'+data.file_upload.file.filename;
data.file_upload.fileType = data.file_upload.file.mimetype;
data.file_upload.createdTimeStamp = new Date();
//Update the file with the information above.
$.get('/api/fileupload/'+data.file_upload._id+'/update', data.file_upload, function(data) {
//debugger;
console.log('File information updated.');
//Add the uploaded file to the uploaded file list.
$('#file_list').append('<li><a href="'+data.collection.url+'" download>'+data.collection.name+'</a></li>');
})
//If the metadata update fails:
.fail(function(data) {
debugger;
console.error('The file metadata was not updated. Here is the error message from the server:');
console.error('Server status: '+err.status);
console.error('Server message: '+err.statusText);
alert('Failed to connect to the server while trying to update file metadata!');
});
},
//This error function is called if the POST fails for submitting the file itself.
error: function(err) {
//debugger;
console.error('The file was not uploaded to the server. Here is the error message from the server:');
console.error('Server status: '+err.status);
console.error('Server message: '+err.statusText);
alert('Failed to connect to the server!');
}
};
//Execute the AJAX call.
jQuery.ajax(opts);
}
</script>
</body>
</html>
Gotcha: Two POST Calls Needed
You’ll notice that in fileAPITest.html
there are two POST calls. The
first uploads the file itself. The second updates the metadata for the
file entry in the database. It’s irritating to make two server calls
instead of one, but right now that’s the way it has to be.
Download a file
fileAPITest.html
creates a list item for each file that is uploaded.
The list item includes a link to the file and uses the download
attribute to tell the browser to download the file instead of trying to
open it. You can use the example code above to figure out how to
generate your own download URL for uploaded files.
README
This guide was written by Chris Troutner. It was originally inspired by this gist by Jed Watson and this tutorial on my own blog. The technique displayed here is the same used in the ConnextCMS software. ConnextCMS is a front end extension for KeystoneJS that mimicks the WordPress user interface.
The latest version of this file can be found in the christroutner/keystone-guides repository.