keystone
Version:
Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose
177 lines (144 loc) • 5.65 kB
JavaScript
var assign = require('object-assign');
var fs = require('fs-extra');
var path = require('path');
var mime = require('mime-types');
var nameFunctions = require('keystone-storage-namefunctions');
var debug = require('debug')('keystone:storage:adapter:fs');
/**
The Adapter Compatibility Level is used to ensure that provided adapters
have been designed to work with the version of Storage implemented in the
version of Keystone being used.
The Adapter Compatibility Level must be an exact match or an error will be
thrown when the Storage instance is initialised.
*/
var ADAPTER_COMPATIBILITY_LEVEL = 1;
/**
This is the default storage schema for file fields. These paths will be
persisted to the database and represent the value of the file field.
Storage Adapters can specify additional schema properties.
*/
var SCHEMA_TYPES = {
// filename: String, // the filename, without the full path
size: Number, // the size of the file
mimetype: String, // the mime type of the file
path: String, // the path (e.g directory) the file is stored in; not the full path to the file
originalname: String, // the original (uploaded) name of the file; useful when filename generated
url: String, // publicly accessible URL of the stored file
};
/**
This is the default set of schema fields that are enabled. The storage
options can override these (must be supported by the adapter, or the
adapter must use the default schema implementation)
*/
var SCHEMA_FIELD_DEFAULTS = {
// filename: true,
size: true,
mimetype: true,
path: false,
originalname: false,
url: false,
};
// TODO: We could support custom schema mappings for backwards compatibilty
// with Keystone 0.3 or imported schamas... not sure if it's worth it though.
/**
# Storage Prototype
Creates a new Keystone Storage instance, and validates and initialises the
specified adapter. Storage schema is configured from the adapter or default.
*/
function Storage (options) {
// we're going to mutate options so get a clean copy of it
options = assign({}, options);
var AdapterType = options.adapter;
delete options.adapter;
if (typeof AdapterType !== 'function') {
throw new Error('Invalid Storage Adapter\n'
+ 'The storage adapter specified is not a function. Did you '
+ 'require the right package?\n');
}
debug('Initialising Storage with adapter ' + AdapterType.name);
// ensure the adapter compatibilityLevel of the adapter matches
if (AdapterType.compatibilityLevel !== ADAPTER_COMPATIBILITY_LEVEL) {
throw new Error('Incompatible Storage Adapter\n'
+ 'The storage adapter specified (' + AdapterType.name + ') '
+ 'does not match the compatibility level required for this '
+ 'version of Keystone.\n');
}
// assign ensures the default schema constant isn't exposed as a property
var schemaFields = assign({}, SCHEMA_FIELD_DEFAULTS, AdapterType.SCHEMA_FIELD_DEFAULTS, options.schema);
delete options.schema;
// Copy requested fields into local schema.
var schema = this.schema = {};
for (var path in schemaFields) {
if (!schemaFields[path]) continue;
var type = AdapterType.SCHEMA_TYPES[path] || SCHEMA_TYPES[path];
if (!type) throw Error('Unknown type for requested schema field ' + path);
schema[path] = type;
}
// ensure Storage schema features are supported by the Adapter
if (schema.url && typeof AdapterType.prototype.getFileURL !== 'function') {
throw Error('URL schema field is not supported by the ' + AdapterType.name + ' adapter');
}
// create the adapter
this.adapter = new AdapterType(options, schema);
}
// Helper function for figuring out the size of a file before uploading it.
function getSize (file, callback) {
if (file.size) return callback(null, file.size);
fs.stat(file.path, function (err, stats) {
if (!stats.isFile()) {
return callback(Error(file.path + ' is not a file'));
}
callback(err, stats ? stats.size : null);
});
}
// Helper to fill out missing fields in the file object
function normalizeFile (file, schema, callback) {
// Detect required information if it wasn't provided by inspecting the
// file stored at file.path
if (schema.mimetype && !file.mimetype) file.mimetype = mime(file.path);
if (!file.originalname) {
file.originalname = file.name
|| (file.path) ? path.parse(file.path).base : 'unnamedfile';
}
if (schema.size && !file.size) {
getSize(file, function (err, size) {
if (err) return callback(err);
file.size = size;
callback(null, file);
});
} else callback(null, file);
}
/**
Uploads a new file via the adapter, then decorates the result with the
public url for the file. The result is what is saved back to the field.
*/
Storage.prototype.uploadFile = function (file, callback) {
var self = this;
// Ensure a file object has been provided
if (!file) return callback(Error('Cannot upload file - No file object provided'));
// The path to the file is required
if (!file.path) return callback(Error('Cannot upload file - No source path'));
normalizeFile(file, this.schema, function (err, file) {
if (err) return callback(err);
self.adapter.uploadFile(file, function (err, result) {
if (err) return callback(err);
if (self.schema.url && self.adapter.getFileURL) {
result.url = self.adapter.getFileURL(result);
}
callback(null, result);
});
});
};
/**
Removes a stored file by passing the field value to the adapter.
*/
Storage.prototype.removeFile = function (value, callback) {
this.adapter.removeFile(value, callback);
};
/*
Built-in Adapters
*/
Storage.Adapters = {};
Storage.Adapters.FS = require('./adapters/fs');
assign(Storage, nameFunctions);
module.exports = Storage;