@frontify/frontify-api
Version:
Simplifies asset management and UI pattern creation within Frontify.
455 lines (392 loc) • 12.6 kB
JavaScript
var path = require('path');
var globby = require('globby');
var rp = require('request-promise');
var extend = require('extend');
var _ = require('lodash');
var fs = require('mz/fs');
var mime = require('mime-types');
var del = require('del');
var moment = require('moment');
var slash = require('slash');
var events = require('./events');
var auth = require('./auth');
var cache = require('./cache');
var sha = require('./sha');
/*
* Diff status objects and produce a list of transformations
*/
function diff(src, dest, options) {
var changes = [];
_.forIn(src.assets, function (asset, id) {
var dasset = dest.assets[id];
var change = null;
if(dasset) {
if(asset.action === 'created') {
// file exists in destination
if (asset.sha !== dasset.sha) {
change = extend(true, {}, dasset, asset, { action: 'update', remote: dest.remote});
}
}
else if(asset.action === 'deleted' && dasset.action !== 'deleted') {
change = extend(true, {}, dasset, asset, { action: 'delete', remote: dest.remote});
}
else if(asset.action === 'updated' && asset.sha !== dasset.sha) {
change = extend(true, {}, dasset, asset, { action: 'update', remote: dest.remote});
}
if(change) {
if(options.strategy === 'newest') {
if(asset.modified > dasset.modified) {
changes.push(change);
}
}
else {
changes.push(change);
}
}
}
else if(asset.action === 'created') {
changes.push(extend(true, {}, asset, { action: 'create', remote: dest.remote}));
}
});
// cleaning up the destination
if(options.clean) {
_.forIn(dest.assets, function (asset, id) {
var sasset = src.assets[id];
var change;
if (!sasset) {
// file does only exists in destination
change = extend(true, {}, asset, { action: 'delete', remote: dest.remote});
changes.push(change);
}
});
}
return changes;
}
function syncAssets(meta, assets) {
// reset library cache
cache.reset();
meta = sanitizeMeta(meta);
// get local and remote status
var promises = [];
promises.push(getLocalStatus({path: meta.cwd, target: meta.target, globs: assets}));
promises.push(getRemoteStatus(meta));
return Promise.all(promises).then(function (statusList) {
var changes = diff(statusList[0], statusList[1], { clean: meta.clean });
return patch(meta, changes);
});
}
function patch(meta, changes) {
meta = sanitizeMeta(meta);
return Promise.all(changes.map(function (change) {
switch (change.action) {
case 'create':
if(change.remote) {
return createRemoteAsset(meta, change);
}
else {
return createLocalAsset(meta, change);
}
break;
case 'update':
if(change.remote) {
return updateRemoteAsset(meta, change);
}
else {
return updateLocalAsset(meta, change);
}
break;
case 'delete':
if(change.remote) {
return deleteRemoteAsset(meta, change);
}
else {
return deleteLocalAsset(meta, change);
}
break;
}
}));
}
/*
* Get status of remote assets in Frontify
*/
function getRemoteStatus(meta, status) {
meta = sanitizeMeta(meta);
var options = auth(meta);
// extend with filters
var filters = {
path : meta.target,
cursor : meta.cursor || 0
};
options.qs = extend(true, {}, options.qs, filters);
options.uri = '/v1/assets/status/' + meta.project;
options.method = 'GET';
return rp(options).then(function(data) {
// preprocess assets to match the structure of the local status
var assets = {};
// only detect action if cursor (history option) is set
if (status) {
_.forIn(data.assets, function (asset, id) {
if (asset === null) {
assets[id] = extend(true, {}, asset, {action: 'deleted'});
}
else if (asset.created > filters.cursor) {
assets[id] = extend(true, {}, asset, {action: 'created'});
}
else if (asset.modified > filters.cursor) {
assets[id] = extend(true, {}, asset, {action: 'updated'});
}
});
// add non-modified assets
_.forIn(status.assets, function (asset, id) {
if (!assets[id]) {
// file does not exist in new status tree
assets[id] = extend(true, {}, asset, {
action: 'none'
});
}
});
}
else {
// mark all as 'created'
_.forIn(data.assets, function (asset, id) {
assets[id] = extend(true, {}, asset, { action: 'created' });
});
}
return { assets: assets, remote: true, time: data.time };
}).catch(function (err) {
throw new Error('Unable to get asset library data. ' + err.message);
});
}
/*
* Get status of assets in local filesystem
*/
function getLocalStatus(filters, status) {
filters = extend(true, {}, {path: '', globs: ['**/*.*']}, filters);
var opts = {
cwd: filters.path
};
return globby(filters.globs, opts).then(function (files) {
if (files.length === 0) {
throw new Error('No files found');
}
return Promise.all(files.map(function (file) {
// create sha1 and modified filelist
var filename = path.resolve(opts.cwd, file);
return fs.stat(filename).then(function(stats) {
return sha(filename).then(function (sha) {
return {
filename: path.basename(file),
path: path.join(filters.target, path.dirname(file), '/'),
sha: sha,
modified: stats.mtime.toISOString().replace(/T/, ' ').replace(/\..+/, '')
};
});
});
}));
}).then(function (assetList) {
var assets = {};
if(status) {
var tmpAssets = {};
// build tmp assets object
assetList.forEach(function (asset) {
tmpAssets[slash(path.join(asset.path, asset.filename))] = asset;
});
// diff asset list with given status to detect updated and created assets
_.forIn(tmpAssets, function (asset, id) {
var sasset = status.assets[id];
// check if file exists in old status tree
if (sasset) {
if (asset.sha !== sasset.sha) {
// mark as 'updated'
assets[id] = extend(true, {}, sasset, asset, {action: 'updated'});
}
else {
assets[id] = extend(true, {}, sasset, asset, {action: 'none'});
}
}
else {
// mark as 'created'
assets[id] = extend(true, {}, asset, { action: 'created'});
}
});
// diff given status with asset list to detect deleted assets
_.forIn(status.assets, function (asset, id) {
if (!tmpAssets[id]) {
// file does not exist in new status tree. make sure that deletions are only considered once
if(asset.action !== 'deleted') {
assets[id] = extend(true, {}, asset, {action: 'deleted', modified: moment.utc().format("YYYY-MM-DD HH:mm:ss")});
}
}
});
} else {
// mark all as 'created'
assetList.forEach(function (asset) {
assets[slash(path.join(asset.path, asset.filename))] = extend(true, {}, asset, { action: 'created'});
});
}
return {assets: assets, remote: false};
}).catch(function (err) {
events.emit('log', 'error', err);
throw err;
});
}
/*
* Create asset in local filesystem
*/
function createLocalAsset(meta, asset) {
meta = sanitizeMeta(meta);
var folder = path.resolve('', path.join(meta.cwd, path.relative(meta.target, asset.path)));
var filename = path.join(folder, asset.filename);
return fs.stat(folder).catch(function (err) {
// create directory
events.emit('log', 'info', 'Create folder: ' + folder);
return fs.mkdir(folder);
}).then(function () {
// get asset from remote
var options = auth(meta);
options.uri = '/v1/screen/download/' + asset.id;
options.method = 'GET';
options.encoding = null;
if (meta.dryRun) {
return Promise.resolve({ success: true });
}
return rp(options).then(function(content) {
return fs.writeFile(filename, content).then(function () {
if(meta.update) {
events.emit('log', 'info', 'Asset ' + path.join(asset.path, asset.filename) + ' has been updated locally');
}
else {
events.emit('log', 'info', 'Asset ' + path.join(asset.path, asset.filename) + ' has been created locally');
}
return {success: true};
});
}).catch(function (err) {
throw new Error('Unable to create asset. ' + err.message);
});
}).catch(function (err) {
events.emit('log', 'error', err);
});
}
/*
* Create remote asset in frontify
*/
function createRemoteAsset(meta, asset) {
meta = sanitizeMeta(meta);
var folder = path.resolve('', path.join(meta.cwd, path.relative(meta.target, asset.path)));
var filename = path.join(folder, asset.filename);
return fs.readFile(filename, 'base64').then(function (content) {
// create asset remote
var options = auth(meta);
options.uri = '/v1/assets/';
if (asset.id) {
options.uri += asset.id; // update
}
options.method = 'POST';
options.body = {
project_id: meta.project,
encoding: 'base64',
content: content,
mimetype: mime.lookup(asset.filename),
filename: path.basename(asset.filename),
path: slash(asset.path)
};
if (meta.dryRun) {
return Promise.resolve(options.body);
}
return rp(options).then(function (data) {
if(asset.id) {
events.emit('log', 'info', 'Asset ' + path.join(asset.path, asset.filename) + ' has been updated');
}
else {
events.emit('log', 'info', 'Asset ' + path.join(asset.path, asset.filename) + ' has been created');
}
return data;
}).catch(function (err) {
throw new Error('Unable to create/update asset. ' + err.message);
});
});
}
/*
* Update asset in local filesystem
*/
function updateLocalAsset(meta, asset) {
meta = sanitizeMeta(meta);
meta.update = true;
return createLocalAsset(meta, asset);
}
/*
* Update remote asset in frontify
*/
function updateRemoteAsset(meta, asset) {
meta = sanitizeMeta(meta);
if (!asset.id) {
return Promise.resolve().then(function () {
events.emit('log', 'error', 'Unable to update asset ' + asset.filename + '. Please provide an asset id');
});
}
else {
return createRemoteAsset(meta, asset);
}
}
/*
* Delete asset in local filesystem
*/
function deleteLocalAsset(meta, asset) {
meta = sanitizeMeta(meta);
var folder = path.resolve('', path.join(meta.cwd, path.relative(meta.target, asset.path)));
var filename = path.join(folder, asset.filename);
return del(filename).then(function () {
events.emit('log', 'info', 'Asset ' + path.join(asset.path, asset.filename) + ' has been deleted locally');
return {success: true};
}).catch(function (err) {
events.emit('log', 'error', err);
});
}
/*
* Delete remote asset in frontify
*/
function deleteRemoteAsset(meta, asset) {
if (!asset.id) {
return Promise.resolve().then(function () {
events.emit('log', 'error', 'Unable to delete asset ' + asset.filename + '. Please provide an asset id');
});
}
else {
meta = sanitizeMeta(meta);
// create asset remote
var options = auth(meta);
options.uri = '/v1/assets/' + asset.id;
options.method = 'DELETE';
if (meta.dryRun) {
return Promise.resolve({success: true});
}
return rp(options).then(function (data) {
events.emit('log', 'info', 'Asset ' + path.join(asset.path, asset.filename) + ' has been deleted');
return data;
}).catch(function (err) {
throw new Error('Unable to delete asset. ' + err.message);
});
}
}
/*
* Sanitize meta options
*/
function sanitizeMeta(meta) {
meta = extend(true, {}, { cwd: '', clean: false, target: '/'}, meta);
meta.target = slash(path.join('/', meta.target));
return meta;
}
module.exports = {
createLocalAsset: createLocalAsset,
updateLocalAsset: updateLocalAsset,
deleteLocalAsset: deleteLocalAsset,
getLocalStatus: getLocalStatus,
createRemoteAsset: createRemoteAsset,
updateRemoteAsset: updateRemoteAsset,
deleteRemoteAsset: deleteRemoteAsset,
getRemoteStatus: getRemoteStatus,
syncAssets: syncAssets,
sanitizeMeta: sanitizeMeta,
patch: patch,
diff: diff
};