Working with the asset manager
Introduction
Preside provides an asset management system that allows users of the system to upload, and add information about, multimedia files. Files can be organised into a folder tree and folders can be configured with permission rules and upload restrictions.
Data model
The metadata and folder structure of your assets are all stored in your application's database using Data objects. The objects and their relationships are modelled below:
These objects can all be modified to take on requirements of your application. See the links below for reference documentation on each object:
When making additions and modifications, you may also want to change the appearance of various forms for uploading and editing assets, folders, etc. Reference documentation on those forms can be found below:
- Asset: add form
- Asset: add through picker form
- Asset: edit form
- Asset: new version form
- Asset folder: add form
- Asset folder: edit form
- Asset storage location: add form
- Asset storage location: edit form
Integrating assets in your application
Link to assets in your data model
To reference an asset in your own data model and page types, you should create a relationship property with the asset
object. For instance, an 'Author' object that has a profile image property:
component {
// ...
property name="profile_image" relationship="many-to-one" relatedTo="asset" allowedTypes="image";
// ...
}
Or a "Consultation" object that has many associated documents:
component {
// ...
property name="documents" relationship="many-to-many" relatedTo="asset";
// ...
}
Allow picking of assets in your forms
The Asset picker form control provides a GUI for selecting and uploading one or more assets in a form.
The form control will automatically be used for object properties that have a relationship with the asset
object. However, you can specify the control directly in a form (for a widget, for example) with:
<?xml version="1.0" encoding="UTF-8"?>
<form i18nBaseUri="widgets.mywidget:">
<tab id="default">
<fieldset id="default" sortorder="10">
<field name="images" control="assetpicker" allowtypes="png,jpg" maxFileSize="512" multiple="true" />
</fieldset>
</tab>
</form>
Getting a raw link to an asset
This can be done with:
event.buildLink(
assetId = idOfAsset
, derivative = "optionalDerivative"
, versionId = optionalVersionId
);
Here, assetId
is the ID of the asset whose link we want to build, derivative
is the name of a configured asset derivative (see below), and versionId
is the ID of a specific version of an asset.
Render assets in your views
The renderAsset()
helper function will render the asset referenced by the passed asset ID. It is a proxy to the renderAsset() method of the Asset Renderer Service. Usage looks like this:
<cfoutput>
<!-- ... -->
#renderAsset(
assetId = myauthor.profile_image
, context = "preview"
, args = { derivative="authorprofile" }
)#
<!-- ... -->
</cfoutput>
Image asset dimensions
Introduced in 10.12.0, the getAssetDimensions()
helper function will return the dimensions of an image asset. It is a proxy to the getAssetDimensions() method of the AssetManager Service. Usage looks like this:
dimensions = getAssetDimensions(
id = myauthor.profile_image
, derivativeName = "authorprofile"
);
A struct with height
and width
values will be returned (or an empty struct if not available for some reason), which can then be used in your HTML code.
Create custom contexts for asset rendering
The renderAsset() method will choose a viewlet with which to render your asset based on:
- The type of asset, or "super-type" of the asset
- The supplied context
The type of the asset is simply its extension. A "super type" is the file type group, i.e. "image", "document", etc. Types and super types are configured in your application's Config.cfc
file (see below).
The asset manager will try to use the most specific viewlet it can find to render your asset. For example, if the supplied asset was a jpg image and the supplied context was "thumbnail", the system would go through the following viewlet names and use the first available one:
renderers.asset.jpg.thumbnail
renderers.asset.image.thumbnail
renderers.asset.jpg.default
renderers.asset.image.default
renderers.asset.default
A "banner" context viewlet for images could therefor be implemented as a view at /application/views/renderers/asset/image/banner.cfm
and look like:
<cfscript>
id = args.id ?: "";
label = args.label ?: "";
imageUrl = event.buildLink( assetId=id, derivative="bannerimage" );
</cfscript>
<cfoutput>
<div class="banner-image">
<img src="#imageUrl#" alt="#label#" title="#label#" />
</div>
</cfoutput>
Configuration
Overall configuration of asset manager behaviour is made in the settings.assetmanager
struct in your application's Config.cfc
file.
Valid keys are:
- maxFileSize This controls the default maximum file upload size in MB. The default value is 5MB.
- types Configures the allowed file types to be uploaded to the asset manager (see File types, below)
- derivatives Configures named derivates (see Derivatives, below)
- folders Configures system folders that will always be available in your asset manager (see System folders, below)
An example configuration section for the asset manager (Config.cfc
):
settings.assetmanager.maxFileSize = 10;
settings.assetmanager.types.video.ogv = { serveAsAttachment=true, mimeType="video/ogg" };
settings.assetmanager.derivatives.leadimage = {
permissions = "inherit"
, inEditor = true
, transformations = [ { method="resize", args={ width=800, height=400 } } ]
, autoQueue = [ "image" ]
};
settings.assetmanager.folders.profileImages = {
label = "Profile images"
, hidden = false
, autoQueue = []
, children = {
members = { label="Members" , hidden=false }
, nonMembers = { label="Non-Members", hidden=false }
}
};
settings.assetmanager.location.public = ExpandPath( "/uploads/public" );
settings.assetmanager.location.private = ExpandPath( "/uploads/private" );
settings.assetmanager.location.trash = ExpandPath( "/uploads/.trash" );
settings.assetmanager.location.publicUrl = "//static.mysite.com/";
File types
Configured file types allows you to specify the filetypes that are uploadable to the asset manager by default. File types are grouped into "super types", for example "image", and the configuration allows you to specify download behaviour and mimetype of each type. The structure of configuration is as follows:
settings.assetmanager.types.supertype.fileextension = {
serveAsAttachment = trueOrFalse
, mimetype = stringMimeType
};
Here is an excerpt from the core configuration to give a fuller picture:
settings.assetmanager.types.image = {
jpg = { serveAsAttachment=false, mimeType="image/jpeg" }
, jpeg = { serveAsAttachment=false, mimeType="image/jpeg" }
, gif = { serveAsAttachment=false, mimeType="image/gif" }
, png = { serveAsAttachment=false, mimeType="image/png" }
};
settings.assetmanager.types.document = {
pdf = { serveAsAttachment=true, mimeType="application/pdf" }
, csv = { serveAsAttachment=true, mimeType="application/csv" }
, doc = { serveAsAttachment=true, mimeType="application/msword" }
, dot = { serveAsAttachment=true, mimeType="application/msword" }
Labelling
In addition to the file type configuration above, you are also able to supply labels for the file types and super types. These are displayed when choosing file type restrictions for uploading to your asset manager folders.
Labels are added in /i18n/filetypes.properties
and take the form: {typeOrSuperType}.picker.label=Human readable label
. For example:
image.picker.label=Image: any type
gif.picker.label=Image: gif
png.picker.label=Image: png
jpg.picker.label=Image: jpg
jpeg.picker.label=Image: jpeg
Derivatives
Derivatives are transformed versions of an asset. This could be a particular crop of a picture, a preview image of a PDF, etc. They are configured in your application's Config.cfc
, for example:
settings.assetmanager.derivatives.leadImage = {
permissions = "inherit"
, inEditor = true
, autoQueue = []
, transformations = [ { method="shrinkToFit", args={ width=800, height=400 } } ]
};
Once defined, a derivative can then be used when building a link to an asset and in the core default contexts of renderAsset()
. For example:
assetUrl = event.buildLink( assetId=myImageId, derivative="leadImage" );
// ...
renderedAsset = renderAsset( assetId=myImageId, args={ derivative="leadImage" } );
Configuration options
Permissions
The permissions
configuration option relates to access permissions defined on the core asset and how they should apply to the derivative. Valid values are "inherit" and "public". The default value is "inherit" and this means that the derivative will share the same access permissions as the asset that it is based on. Derivatives with permissions
set to "public" will have no permissions checking at all, regardless of the permissions set on the base asset.
inEditor
A boolean value indicating whether or not the derivative should be selectable by system editors when embedding images in content. Derivatives with this option set to true
appear in the "Preset" dropdown in the Image picker:
The default value is false
. If set to true
, you should also supply a human readable label for the derivative in a i18n/derivatives.properties
file. This can be done using {derivativeid}.title=Some title
:
leadimage.title=Lead image (800x400)
thumbnail.title=Thumbnail (100x100)
autoQueue
As of 10.11.0, and if the asset processing queue feature is enabled, a derivative can be configured to be automatically processed in the background as soon as a matching asset is uploaded.
The option expects an array of matching file types, or file type groups upon which it will auto queue the derivative for generation. For example:
settings.assetmanager.derivatives.thumnail = {
autoQueue = [ "image", "pdf" ] // autoqueue for all images + pdfs
// ...
}
See Enabling the asset processing queue for more details on the asset processing queue.
Transformations
An array of configured transformations that the original asset binary will be passed through in order to create a new version.
A transformation is defined as a CFML structure, with the following keys:
- method (required): Method that matches a method implemented in the api-assettransformer service object
- args (optional): Structure of arguments passed to the transformation method.
- inputfiletype (optional): Only apply this transformation to images of this type. e.g. "pdf".
- outputfiletype (optional): Expected output filetype of the transformation
An example using all of the above arguments, is the admin thumbnail derivative that works for both PDFs and images:
settings.assetmanager.derivatives.adminthumbnail = {
permissions = "inherit"
, inEditor = false
, transformations = [
{ method="pdfPreview" , args={ page=1 }, inputfiletype="pdf", outputfiletype="jpg" }
, { method="shrinkToFit", args={ width=200, height=200 } }
]
};
For more information on image transformations, see Image asset transformations.
Restricting application of derivatives
As of 10.11.5, Preside allows you to configure image size limits for derivative generation so that you can protect your server from heavy image transformation operations that would be better performed offline. You can set a max width, height, resolution and even specify a file path to a placeholder image to use instead when images are too large. In Config.cfc
:
settings.assetmanager.derivativeLimits.maxHeight = 3000; // default 0, no limit
settings.assetmanager.derivativeLimits.maxWidth = 3000; // default 0, no limit
settings.assetmanager.derivativeLimits.maxResolution = 2000*2000; // default 0, no limit
settings.assetmanager.derivativeLimits.tooBigPlaceholder = "/preside/system/assets/images/placeholders/largeimage.jpg" // this is the default
If an image breaches any of these limits, no derivatives will be generated for it. Instead, the placeholder image will be used.
System folders
System folders are pre-defined asset manager folders that will always exist in your asset manager folder structure. They cannot be deleted through the admin UI and can optionally be completely hidden from the UI. They are configured in Config.cfc
, for example:
settings.assetmanager.folders.profileImages = {
label = "Profile images"
, hidden = false
, children = {
memberProfileImages = { label="Members" , hidden=false }
, nonMemberProfileImages = { label="Non-Members", hidden=false }
}
};
The purpose of system folders is to be able to programatically upload assets directly to a named folder that you know will exist. This can be achieved with the addAsset() method:
assetManagerService.addAsset(
fileBinary = uploadedFileBinary
, fileName = uploadedFileName
, folder = "memberProfileImages"
, assetData = { description="Uploaded profile image for #loggedInMemberName#", title=loggedInMemberName }
);
Warning
Asset titles must be unique within any given folder. If you are programatically uploading assets to the asset manager, you need to code for this uniqueness to avoid duplicate key errors.
Storage providers and locations
The asset manager allows you to define and use multiple storage locations. For example, you might have a shared drive on your server for private documents, and an Amazon Cloudfront CDN for your public images. Once your locations have been configured, you are then able to map folders in the asset manager to different locations.
Storage providers
The system works with a concept of storage providers. The core system implements a single 'file storage' provider for you to use. Custom storage providers can be created by creating a CFC that adheres to the core Storage provider interface and by supplying configuration forms that can be used by administrators of the system to configure an instance of your provider.
Defining a custom provider is as follows:
1. Create a CFC file
Create a CFC that implements the Storage provider interface, i.e.
compoment implements="preside.system.services.fileStorage.StorageProvider" {
// ...
}
You will need to thoroughly read the interface documentation and be sure to implement each method appropriately. In addition, you will almost certainly want to implement an init()
constructor method to take any configuration that your provider requires (i.e. security credentials, etc.).
2. Declare the provider in config
You must declare the storage provider in your application's Config.cfc
file, this is simply mapping an ID to a CFC path:
settings.storageProviders.myProvider = {
class = "app.services.filestorage.MyProvider"
};
Here we declare a provider named "myProvider", whose CFC file lives at "app.services.filestorage.MyProvider".
3. Provide a configuration form for the provider
You must provide a configuration form for the provider. This will be used by administrators when managing a specific storage location that uses your provider. By convention, this is expected to live at /forms/storage-providers/{providerid}.xml
. In our example above, the form would live at /forms/storage-providers/myProvider.xml
. The form fields defined here must map to arguments passed to your custom provider CFC's init() method.
Info
The form definition will be merged with either Asset storage location: add form or Asset storage location: edit form depending on whether a storage location is being added or edited.
For example:
<?xml version="1.0" encoding="UTF-8"?>
<form i18nBaseUri="storage-providers.filesystem:">
<tab id="default">
<fieldset id="filesystem">
<field sortorder="10" name="rootDirectory" control="textinput" required="true" />
<field sortorder="20" name="trashDirectory" control="textinput" required="true" />
</fieldset>
</tab>
</form>
4. Provider i18n resources to describe the provider and its configuration
By convention, you must create a .properties
file at /i18n/storage-providers/{providerid}.properties
. For example: /i18n/storage-providers/myProvider.properties
. It should contain title
, description
and iconclass
keys to describe the provider itself plus any keys for describing form fields, etc. For example:
title=File system
description=The file system storage provider stores files in the local file system. Suitable for sites without any clustering requirements.
iconclass=fa-folder
field.rootDirectory.title=Root path
field.rootDirectory.placeholder=e.g. /uploads/assets
field.trashDirectory.title=Trash path
field.trashDirectory.placeholder=e.g. /uploads/.trash
error.creating.directory=The directory, {1}, does not exist and could not be created. Error: {2}. Please note, you must supply full directory paths
Default location
The asset manager system works out of the box without the need to configure any storage locations through the UI. For this, it uses a default configured storage provider through Wirebox. The core configuration of this provider is located at /system/config/Wirebox.cfc
and looks like this:
map( "assetStorageProvider" ).asSingleton().to( "preside.system.services.fileStorage.FileSystemStorageProvider" ).parent( "baseService" ).noAutoWire()
.initArg( name="rootDirectory" , value=settings.assetmanager.storage.public )
.initArg( name="privateDirectory", value=settings.assetmanager.storage.private )
.initArg( name="trashDirectory" , value=settings.assetmanager.storage.trash )
.initArg( name="rootUrl" , value=settings.assetmanager.storage.publicUrl );
Overriding the default storage location
This can be done in two ways. Firstly, you could change settings.assetmanager.storage
settings to point to different physical paths (or full mapped ftp/s3/etc Lucee paths). This might be a mounted shared drive for example, or just a directory outside of the webroot (recommended). This can also be achieved with environment variables, for example:
# env vars:
PRESIDE_assetmanager.storage.public=sftp://user:[email protected]/public
PRESIDE_assetmanager.storage.private=sftp://user:[email protected]/private
PRESIDE_assetmanager.storage.trash=sftp://user:[email protected]/.trash
PRESIDE_assetmanager.storage.publicUrl=//static.mysite.com
The second option would be to manually configure an entirely different Storage provider that maps to "assetStorageProvider". This would be done in your site's /config/Wirebox.cfc
file, for example:
component extends="preside.system.config.WireBox" {
public void function configure() {
super.configure();
var settings = getColdbox().getSettingStructure();
if ( IsBoolean( settings.myProvider.enabled ?: "" ) && settings.myProvider.enabled ) {
map( "assetStorageProvider" ).asSingleton().to( "app.services.fileStorage.MyProvider" ).noAutoWire()
.initArg( name="apiKey" , value=settings.myProvider.apiKey )
.initArg( name="uploadPath", value=settings.myProvider.uploadPath & "/assets" )
.initArg( name="trashPath" , value=settings.myProvider.uploadPath & "/.trash" )
.initArg( name="rootUrl" , value=settings.myProvider.rootUrl );
}
}
}
Info
You should consider that your application may run in multiple environments and need to be able to configure these settings per environment. Using the technique above that uses ColdBox settings to configure your provider could help with that as these are able to be set per environment (see the ColdBox documentation for further details). If you're super smart and have beautifully setup environments, you could use environment variables to setup the settings, making your default storage provider configuration truly portable.