Background
In AEM Assets 6.5 and prior, a single post request to a servlet that manges asset binaries is enough for uploading files. Newer versions of AEM can be configured
to use direct binary upload, which means that asset binaries are no longer uploaded straight to AEM. Because of this there is a more complex
algorithm to follow when uploading asset binaries. This library will check the configuration of the target AEM instance, and will either use the direct binary upload algorithm or the create asset servlet, depending on the configuration.
This tool is provided for making uploading easier, and can be used as a command line executable
or required as a Node.js module.
Command Line
A command line tool for for uploading assets to an AEM instance is available as a plugin for the Adobe I/O CLI. Please
see the plugin repository for more information.
Usage
This library supports uploading files to a target instance, while providing support for monitoring
transfer progress, cancelling transfers, and other features.
Install
This project uses node and npm. Go check them out if you don't have them locally installed.
It can be installed like any other Node.js module.
$ npm install @adobe/aem-upload
Requiring the Module
To add the module to your Node.js project:
- Install the module in your project.
- Require the module in the javascript file where it will be consumed:
const DirectBinary = require('@adobe/aem-upload');
Uploading Files
Following is the minimum amount of code required to upload files to a target AEM instance.
const DirectBinary = require('@adobe/aem-upload');
const targetUrl = 'http://localhost:4502/content/dam/target';
const uploadFiles = [
{
fileName: 'file1.jpg',
fileSize: 1024,
filePath: '/Users/me/Documents/my_file.jpg'
},
{
fileName: 'file2.jpg',
fileSize: 512,
filePath: '/Users/me/Documents/file2.jpg'
}
];
const upload = new DirectBinary.DirectBinaryUpload();
const options = new DirectBinary.DirectBinaryUploadOptions()
.withUrl(targetUrl)
.withUploadFiles(uploadFiles);
upload.uploadFiles(options)
.then(result => {
})
.catch(err => {
});
Supported Options
The DirectBinaryUploadOptions
class supports the following options. Items with * are required.
Option | Type | Description |
---|
* URL | string |
Full, absolute URL to the Folder in the target instance where the specified files will be uploaded. This value is expected to be URI encoded.
Example
options.withUrl('http://localhost:4502/content/dam/target');
|
* Upload Files | Array |
List of files that will be uploaded to the target URL. Each item in the array should be
an object consisting of the following properties:
Property | Type | Description |
---|
fileName | string |
The name of the file as it will appear in AEM. This value does not need to be URI encoded.
| fileSize | number |
Total size, in bytes, of the file to upload.
| filePath | string |
Full path to a local file to upload. Note that either this value
or blob must be specified. This option is
typically most useful when running the upload tool from a Node.js
process.
| blob | File |
Data for a file. The only tested and supported value for this property is the value of an HTML <input type='file' /> .
Note that either this property or filePath
must be specified. This option is typically most useful when running the
upload tool from a browser.
| partHeaders | object |
Header values to be included with each part of this file that is transferred. The headers from `DirectBinaryUploadOptions` are
only included in requests that are sent to the target instance; they are ignored when sending requests to the direct binary upload URIs
provided by the instance. This option provides a means for specifying any additional headers that should be included in requests sent to
these URIs.
Default: {}
Example: { 'user-agent': 'My User Agent' }
| createVersion | boolean |
If true and an asset with the given name already exists,
the process will create a new version of the asset instead of updating the
current version with the new binary.
Default: false
| versionLabel | string |
If the process creates a new version of the asset, the label to
associated with the newly created version.
Default: null
| versionComment | string |
If the process creates a new version of the asset, the comment to
associated with the newly created version.
Default: null
| replace | boolean |
If true and an asset with the given name already exists,
the process will delete the existing asset and create a new one with the same
name and the new binary.
Note that if both this option and "create version" are specified, "create version"
will take priority.
Default: false
|
Example
options.withUploadFiles([
{
fileName: 'file1.jpg',
fileSize: 1024,
filePath: '/Users/me/Documents/file1.jpg'
},
{
fileName: 'file2.jpg',
fileSize: 2048,
// note that this assumes HTML similar to:
// <form name="formName">
// <input type="file" name="fileInputName" />
// </form>
blob: document.forms['formName']['fileInputName'].files[0]
}
]);
|
fetch options | object |
Consumers can control the options that the library will provide to the Fetch API when submitting HTTP requests. These options will be passed as-is to Fetch, so consumers should reference the Fetch API documentation to determine which values are applicable.
Example
options.withHttpOptions({
agent: myAgent,
headers: {
'authorization': '12345'
'cookie': 'mycookie=myvalue'
}
});
|
concurrent | boolean |
If true , multiple files in the supplied list of upload files will
transfer simultaneously. If false , only one file will transfer at
a time, and the next file will not begin transferring until the current file
finishes.
Default: false .
Example
options.withConcurrent(true);
|
max concurrent requests | number |
The maximum number of concurrent HTTP requests that are allowed at any one time. As explained in the
concurrent option, the library will concurrently upload multiple files at once. This value
essentially indicates the maximum number of files that the process will upload at once.
A value less than 2 will instruct the library not to upload more than one file concurrently.
Default: 5 .
Example
options.withMaxConcurrent(2);
|
http retry count | number |
The number of times that the process will retry a failed HTTP request before
giving up. For example, if the retry count is 3 then the process will submit
the same HTTP request up to 3 times if the response indicates a failure.
Default: 3
Example
options.withHttpRetryCount(5);
|
http retry delay | number |
The amount of time that the process will wait before retrying a failed HTTP
request. The value is specified in milliseconds. With each increasing retry,
the delay will increase by its value. For example, if the delay is 5000 then
the first retry will wait 5 seconds, the second 10 seconds, the third 15
seconds, etc.
Default: 5000
Example
options.withHttpRetryDelay(3000);
|
Error Handling
If a file fails to upload, the process will move to the next file in the list. The overall process
itself will only fail if something catastrophic happens that prevents it from continuing to
upload files. It's left up to the consumer to determine if there were individual file upload
failures and react to them accordingly.
All errors reported by the upload process will be instances of UploadError
, which are
standard javascript Error
instances with an additional code
value that indicates
the type of error. Specific codes are available in DirectBinary.DirectBinaryUploadErrorCodes
.
The following is an example of handling errors, at either the process or file level.
const codes = DirectBinary.DirectBinaryUploadErrorCodes;
const upload = new DirectBinary.DirectBinaryUpload();
upload.uploadFiles(options)
.then(result => {
const { errors = [] } = result;
errors.forEach(error => {
if (error.code === codes.ALREADY_EXISTS) {
}
});
const { detailedResult = [] } = result;
detailedResult.forEach((fileResult) => {
const { result = {} } = fileResult;
const { errors = [] } = result;
errors.forEach((fileErr) => {
});
});
})
.catch(err => {
if (err.code === codes.NOT_SUPPORTED) {
}
});
Another way of handling individual file errors is to listen for the upload process's
Events.
The process implements automatic HTTP retry handling, meaning that if an HTTP request fails
then the process will wait for a specified interval and retry the same HTTP request a given
number of times. If the request still fails after the given number of retries, it will
report the error as normal using the last error. Any errors that caused a retry, in
either a success scenario or failure scenario, will be reported in the result in a dedicated
structure.
Upload Events
As the upload process moves through individual files, it will send events as it goes
through the stages of uploading a file. These events are listed below.
Event | Description | Data |
---|
fileuploadstart | Indicates an upload of one or more files is starting. |
The data sent with the event will be a simple javascript object
with the following properties:
Property | Type | Description |
---|
uploadId | string |
A unique identifier that can be used to identify the upload session.
| fileCount | number |
The total number of files that will be uploaded as part of this upload session.
| totalSize | number |
The total size, in bytes, of all files that will be uploaded as part of this upload session.
| directoryCount | number | The number of directories included in the upload session. |
|
fileuploadend | Indicates an upload of one or more files has finished. |
The data sent with the event will be the same as the `fileuploadstart` event, with the following additional elements:
Property | Type | Description |
---|
result | object |
Simple javascript object containing the same upload result information that is returned by the library's `upload()` methods.
|
|
filestart | Indicates that a file has started to upload. |
The data sent with the event will be a simple javascript object
with the following properties:
Property | Type | Description |
---|
fileName | string |
The name of the file, as it was specified in the upload options. This will not be a URI encoded value.
| fileSize | number |
The size of the file, in bytes, as it was specified in the upload
options.
| targetFolder | string |
Full path to the AEM folder where the file is being uploaded. This will not be a URI encoded value.
| targetFile | string | Full path to the asset in AEM. This will not be a URI encoded value. | mimeType | string | HTTP Content-Type value of the file. |
|
fileprogress |
Sent periodically and includes information about how much of the file has uploaded.
|
A simple javascript object containing the same properties as "filestart,"
in addition to the following properties:
Property | Type | Description |
---|
transferred | number |
The number of the file's bytes that have been uploaded so far. This will
be a cumulative value, increasing each time the event is sent.
|
|
fileend |
Indicates that a file has uploaded successfully. This event will not be sent if
the file failed to upload, or if the file upload was cancelled.
|
A simple javascript object containing the same properties as "filestart."
|
fileerror |
Sent if a file fails to upload. This event will not be sent if the file uploads
successfully, or if the file upload was cancelled.
|
A simple javascript object containing the same properties as "filestart,"
in addition to the following properties:
Property | Type | Description |
---|
errors | Array |
A list of all the errors that occurred while trying to upload
the file. Each item in the array will be an instance of type
UploadError . See "Error Handling" for more details.
|
|
filecancelled |
Indicates that a file upload was cancelled.
|
A simple javascript object containing the same properties as "filestart."
|
foldercreated |
Indicates that the upload process created a new folder in the target.
|
The data sent with the event will be a simple javascript object
with the following properties:
Property | Type | Description |
---|
folderName | string |
The name (i.e. title) of the folder, as it was created. This will not be a URI encoded value.
| targetParent | number |
Full path to the AEM folder where the folder was created. This will not be a URI encoded value.
| targetFolder | string |
Full path to the AEM folder that was created. This will not be a URI encoded value.
|
|
The following is an example of how to handle various events.
const upload = new DirectBinary.DirectBinaryUpload();
upload.on('filestart', data => {
const { fileName } = data;
});
upload.on('fileprogress', data => {
const { fileName, transferred } = data;
});
upload.on('fileend', data => {
const { fileName } = data;
});
upload.on('fileerror', data => {
const { fileName, errors } = data;
});
upload.uploadFiles(options);
Uploading Local Files
The library supports uploading local files and folders. For folders, the tool
will include all immediate children files in the folder. It will not process sub-folders unless the "deep upload" option is specified.
When deep uploading, the library will create a folder structure in the target that mirrors the folder being uploaded. The title of the newly
created folders will match the name of the folder as it exists on the local filesystem. The path of the target may be modified depending on
path character restrictions in AEM, and depending on the options provided in the upload (see "Function for processing folder node names" for
more information).
Whenever the library creates a new folder, it will emit the foldercreated
event. See event documentation for details.
The following example illustrates how to upload local files.
const {
FileSystemUploadOptions,
FileSystemUpload
} = require('@adobe/aem-upload');
const options = new FileSystemUploadOptions()
.withUrl('http://localhost:4502/content/dam/target-folder')
.withHttpOptions({
headers: {
Authorization: Buffer.from('admin:admin').toString('base64')
}
});
const fileUpload = new FileSystemUpload();
await fileUpload.upload(options, [
'/Users/me/myasset.jpg',
'/Users/me/myfolder'
]);
Supported File Options
There is a set of options, FileSystemUploadOptions
, that are specific to uploading local files. In addition to default options, the following options are available.
Option | Type | Description |
---|
Maximum number of files | number |
The maximum number of files that the library will attempt to upload. If the target upload exceeds this number then the process will fail with an exception. Default: 1000.
Example
options.withMaxUploadFiles(100);
|
Perform deep upload | boolean |
If true, the process will include all descendent folders and files when given a folder to upload. If false, the process will only upload those files immediately inside the folder to upload. Default: false.
Example
options.withDeepUpload(true);
|
Function for processing folder node names | function |
When performing a deep upload, the tool will create folders in AEM that match local folders
being uploaded. The tool will "clean" the folder names of certain characters when creating node names
for each folder. The unmodified folder name will become the node's title.
This option allows customization of the functionality that cleans the folder's name. The
option should be a function . It will receive a single argument value: the name of the
folder to be cleaned. The return value of the function should be a Promise , which
should resolve with the clean folder name.
The default functionality will convert the folder name to lower case and replace whitespace and any of
the characters %;#,+?^{} with the replacement value specified in the options.
Regardless of this function, the library will always replace any of the characters
./:[]|*\ with the replacement value specified in the options.
Example
// This example will skip any special processing
options.withFolderNodeNameProcessor((folderName) => {
return new Promise((resolve) => resolve(folderName));
});
|
Function for processing asset node names | function |
When performing a deep upload, the tool will create assets in AEM that match local files
being uploaded. The tool will "clean" the file names of certain characters when creating node names
for each asset.
This option allows customization of the functionality that cleans the file's name. The
option should be a function . It will receive a single argument value: the name of the
file to be cleaned. The return value of the function should be a Promise , which
should resolve with the clean asset name.
The default functionality will replace any of
the characters #%{}?& with the replacement value specified in the options.
Regardless of this function, the library will always replace any of the characters
./:[]|*\ with the replacement value specified in the options.
Example
// This example will skip any special processing
options.withAssetNodeNameProcessor((fileName) => {
return new Promise((resolve) => resolve(fileName));
});
|
Replacement value for invalid node characters | string |
Specifies the value to use when replacing invalid characters in folder and asset node names. This value is used in the default functions that clean
folder/asset names, and is always used when replacing any of the characters ./:[]|*\ ; the value of this option cannot
contain any of those characters. Default: -
For example, assume the folder name My Test Folder #2 . With the default settings, the folder's node would be my-test-folder--2 .
Example
options.withInvalidCharacterReplaceValue('_');
|
Upload file options | object |
Specifies the options to use when uploading each file as part of the file system upload. Most of the options provided when using
`DirectBinaryUploadOptions.withUploadFiles()` are valid. The exceptions are `fileName`, `fileSize`, `filePath`, and `blob`, which
will be ignored.
Example
options.withUploadFileOptions({
createVersion: true,
versionLabel: 'version-label'
});
|
Logging
The library will log various messages as it goes through the process of uploading items. It will use whichever logger it's given, as long as the object supports methods debug()
, info()
, warn()
, and error()
. For maximum detail, the library also assumes that each of these methods can accept formatted messages: log.info('message with %s', 'formatting');
. The logging will work regardless of formatting support, but there will be more information when formatting works correctly.
To provide a logger to the library, pass a log
element in the options sent into the DirectBinaryUpload
constructor. Here is a simple example that will log all the library's messages to console
:
const upload = new DirectBinary.DirectBinaryUpload({
log: {
debug: (...theArguments) => console.log.apply(null, theArguments),
info: (...theArguments) => console.log.apply(null, theArguments),
warn: (...theArguments) => console.log.apply(null, theArguments),
error: (...theArguments) => console.log.apply(null, theArguments),
}
});
Note that this will also work with the FileSystemUpload
constructor.
Proxy Support
The library utilizes the Fetch API, so when running in a browser, proxy settings are detected and applied by the browser. In Node.JS, all HTTP requests are sent directly to the target, without going through a proxy. Auto detecting a system's proxy settings is not supported in Node.JS, but
consumers can use DirectBinaryUploadOptions.withHttpOptions()
to provde an agent
value as recommended by node-fetch
.
Features
- Well tuning to take advantage of nodejs for best uploading performance
- Track transfer progress of files
- Cancel in-progress transfers
- Transfer multiple files "in batch"
- Upload local folder/file structures
Releasing
This module uses semantic-release when publishing new versions. The process is initiated upon merging commits to the master
branch. Review semantic-release's documentation for commit message format.
PRs whose messages do not meet semantic-release's format will not generate a new release.
Release notes are generated based on git commit messages. Release notes will appear in CHANGELOG.md
.
Todo
Contributing
Contributions are welcomed! Read the Contributing Guide for more information.
Licensing
This project is licensed under the Apache V2 License. See LICENSE for more information.
Maintainers