duplicati/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js
Kenneth Skovhede 6a8edd75dc
Added additional folders to MacOS. (#5899)
This adds the common "Movies" and "Public" folders to the list of special folders for MacOS (and Linux, if applicable)
2025-01-25 12:42:16 +01:00

816 lines
28 KiB
JavaScript

backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, AppService, DialogService, gettextCatalog) {
var apputils = this;
this.exampleOptionString = '--dblock-size=100MB';
function setMomentLocale() {
try {
var browser_lang = navigator.languages ?
navigator.languages[0] :
(navigator.language || navigator.userLanguage);
moment.locale($cookies.get('ui-locale') ? $cookies.get('ui-locale') : browser_lang);
} catch (e) {
}
}
setMomentLocale();
$rootScope.$on('ui_language_changed', setMomentLocale);
this.formatSizeString = function(val) {
var formatSizes = ['TB', 'GB', 'MB', 'KB'];
val = parseInt(val || 0);
var max = formatSizes.length;
for(var i = 0; i < formatSizes.length; i++) {
var m = Math.pow(1024, max - i);
if (val > m) {
return (val / m).toFixed(2) + ' ' + formatSizes[i];
}
}
return val + ' bytes';
};
this.watch = function(scope, m) {
scope.$on('apputillookupschanged', function() {
if (m) m();
$timeout(function() {
scope.$digest();
});
});
if (m) $timeout(m);
};
this.getEntryTypeFromIconCls = function(cls)
{
// Entry type is used in as the ALT entry,
// to guide screen reading software for visually
// impaired users
var res = gettextCatalog.getString('Folder');
if (cls == 'x-tree-icon-mydocuments')
res = gettextCatalog.getString('My Documents');
else if (cls == 'x-tree-icon-mymusic')
res = gettextCatalog.getString('My Music');
else if (cls == 'x-tree-icon-mypictures')
res = gettextCatalog.getString('My Pictures');
else if (cls == 'x-tree-icon-desktop')
res = gettextCatalog.getString('Desktop');
else if (cls == 'x-tree-icon-home')
res = gettextCatalog.getString('Home');
else if (cls == 'x-tree-icon-mymovies')
res = gettextCatalog.getString('My Movies');
else if (cls == 'x-tree-icon-mydownloads')
res = gettextCatalog.getString('My Downloads');
else if (cls == 'x-tree-icon-mypublic')
res = gettextCatalog.getString('Public');
else if (cls == 'x-tree-icon-hypervmachine')
res = gettextCatalog.getString('Hyper-V Machine');
else if (cls == 'x-tree-icon-hyperv')
res = gettextCatalog.getString('Hyper-V Machines');
else if (cls == 'x-tree-icon-broken')
res = gettextCatalog.getString('Broken access');
else if (cls == 'x-tree-icon-locked')
res = gettextCatalog.getString('Access denied');
else if (cls == 'x-tree-icon-symlink')
res = gettextCatalog.getString('Symbolic link');
else if (cls == 'x-tree-icon-leaf')
res = gettextCatalog.getString('File');
return res;
};
function reloadTexts() {
apputils.fileSizeMultipliers = [
{name: gettextCatalog.getString('byte'), value: 'b'},
{name: gettextCatalog.getString('KByte'), value: 'KB'},
{name: gettextCatalog.getString('MByte'), value: 'MB'},
{name: gettextCatalog.getString('GByte'), value: 'GB'},
{name: gettextCatalog.getString('TByte'), value: 'TB'}
];
apputils.timerangeMultipliers = [
{name: gettextCatalog.getString('Minutes'), value: 'm'},
{name: gettextCatalog.getString('Hours'), value: 'h'},
{name: gettextCatalog.getString('Days'), value: 'D'},
{name: gettextCatalog.getString('Weeks'), value: 'W'},
{name: gettextCatalog.getString('Months'), value: 'M'},
{name: gettextCatalog.getString('Years'), value: 'Y'}
];
apputils.shorttimerangeMultipliers = [
{name: gettextCatalog.getString('Seconds'), value: 's'},
{name: gettextCatalog.getString('Minutes'), value: 'm'},
{name: gettextCatalog.getString('Hours'), value: 'h'}
];
apputils.daysOfWeek = [
{name: gettextCatalog.getString('Mon'), value: 'mon'},
{name: gettextCatalog.getString('Tue'), value: 'tue'},
{name: gettextCatalog.getString('Wed'), value: 'wed'},
{name: gettextCatalog.getString('Thu'), value: 'thu'},
{name: gettextCatalog.getString('Fri'), value: 'fri'},
{name: gettextCatalog.getString('Sat'), value: 'sat'},
{name: gettextCatalog.getString('Sun'), value: 'sun'}
];
apputils.speedMultipliers = [
{name: gettextCatalog.getString('byte/s'), value: 'b'},
{name: gettextCatalog.getString('KByte/s'), value: 'KB'},
{name: gettextCatalog.getString('MByte/s'), value: 'MB'},
{name: gettextCatalog.getString('GByte/s'), value: 'GB'},
{name: gettextCatalog.getString('TByte/s'), value: 'TB'}
];
apputils.exampleOptionString = gettextCatalog.getString('Enter one option per line in command-line format, e.g. {0}');
apputils.filterClasses = [{
name: gettextCatalog.getString('Exclude directories whose names contain'),
key: '-dir*',
prefix: '-*',
suffix: '*!',
rx: '\\-\\*([^\\!]+)\\*\\!'
}, {
name: gettextCatalog.getString('Exclude files whose names contain'),
key: '-file*',
prefix: '-[.*',
suffix: '[^\\!]*]', // Escape dirsep inside regexp. Required on Windows, no effect on other platforms.
rx: '\\-\\[\\.\\*[\\^\\!\\]\\*\\]'
}, {
name: gettextCatalog.getString('Exclude folder'),
key: '-folder',
prefix: '-',
suffix: '!',
stripSep: true,
rx: '\\-(.*)\\!'
}, {
name: gettextCatalog.getString('Exclude file'),
key: '-path',
prefix: '-',
stripSep: true,
exclude: ['*', '?', '{'],
rx: '\\-([^\\[\\{\\*\\?]+)'
}, {
name: gettextCatalog.getString('Exclude file extension'),
key: '-ext',
rx: '\\-\\*\.(.*)',
prefix: '-*.'
}, {
name: gettextCatalog.getString('Exclude regular expression'),
key: '-[]',
prefix: '-[',
suffix: ']'
}, {
name: gettextCatalog.getString('Include regular expression'),
key: '+[]',
prefix: '+[',
suffix: ']'
}, {
name: gettextCatalog.getString('Exclude filter group'),
key: '-{}',
prefix: '-{',
suffix: '}'
}, {
//// Since all the current filter groups are intended for exclusion, there isn't a reason to show the 'Include group' section in the UI yet.
// name: gettextCatalog.getString('Include filter group'),
// key: '+{}',
// prefix: '+{',
// suffix: '}'
//}, {
name: gettextCatalog.getString('Include expression'),
key: '+',
prefix: '+'
}, {
name: gettextCatalog.getString('Exclude expression'),
key: '-',
prefix: '-'
}];
apputils.filterGroups = [{
name: gettextCatalog.getString('Default excludes'),
value: 'DefaultExcludes'
}, {
//// As the DefaultIncludes are currently empty, we don't need to include them in the UI yet.
// name: gettextCatalog.getString('Default includes'),
// value: 'DefaultIncludes'
//}, {
name: gettextCatalog.getString('System Files'),
value: 'SystemFiles'
}, {
name: gettextCatalog.getString('Operating System'),
value: 'OperatingSystem'
}, {
name: gettextCatalog.getString('Cache Files'),
value: 'CacheFiles'
}, {
name: gettextCatalog.getString('Temporary Files'),
value: 'TemporaryFiles'
}, {
name: gettextCatalog.getString('Applications'),
value: 'Applications'
}];
apputils.filterTypeMap = {};
for (var i = apputils.filterClasses.length - 1; i >= 0; i--)
apputils.filterTypeMap[apputils.filterClasses[i].key] = apputils.filterClasses[i];
$rootScope.$broadcast('apputillookupschanged');
};
apputils.filterGroupMap = null;
apputils.loadFilterGroups = function (reload) {
if (reload || apputils.filterGroupMap === null) {
AppService.get('/systeminfo/filtergroups').then(function (data) {
apputils.filterGroupMap = angular.copy(data.data.FilterGroups);
$rootScope.$broadcast('apputillookupschanged');
}, apputils.connectionError);
}
}
reloadTexts();
$rootScope.$on('gettextLanguageChanged', reloadTexts);
this.parseBoolString = function(txt, def) {
txt = (txt || '').toLowerCase();
if (txt == '0' || txt == 'false' || txt == 'off' || txt == 'no' || txt == 'f')
return false;
else if (txt == '1' || txt == 'true' || txt == 'on' || txt == 'yes' || txt == 't')
return true;
else
return def === undefined ? false : def;
};
this.splitSizeString = function (val) {
var m = (/(\d*)(\w*)/mg).exec(val);
var mul = null;
if (!m)
return [parseInt(val), null];
else
return [parseInt(m[1]), m[2]];
};
this.parseSizeString = function (val) {
if (val == null) {
return null;
}
var split = this.splitSizeString(val);
var formatSizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var idx = formatSizes.indexOf((split[1]||'').toUpperCase());
if (idx == -1) {
idx = 0;
}
return split[0] * Math.pow(1024, idx);
};
this.toDisplayDateAndTime = function(dt) {
return moment(dt).format('lll');
};
this.parseDate = function(dt) {
if (typeof(dt) == typeof('')) {
var msec = Date.parse(dt);
if (isNaN(msec)) {
if (dt.length == 16 && dt[8] == 'T' && dt[15] == 'Z') {
dt = dt.substr(0, 4) + '-' + dt.substr(4, 2) + '-' + dt.substr(6, 2) + 'T' +
dt.substr(9, 2) + ':' + dt.substr(11, 2) + ':' + dt.substr(13, 2) + 'Z';
}
return new Date(dt);
} else {
return new Date(msec);
}
}
else
return new Date(dt);
};
this.parseOptionStrings = function (val, dict, validateCallback) {
// Parse options and return a dict with dict['--key']=value options
// Include and exclude filters are returned as arrays in dict['include'], dict['exclude'] if present
dict = dict || {};
var lines = null;
if (val != null && Array.isArray(val))
lines = val;
else
lines = this.replace_all(val || '', '\r', '\n').split('\n');
for(var i in lines) {
var line = lines[i].trim();
if (line != '' && line[0] != '#') {
if (line.indexOf('--') == 0) {
line = line.substr(2);
}
var eqpos = line.indexOf('=');
var key = line;
var value = true;
if (eqpos > 0) {
key = line.substr(0, eqpos).trim();
value = line.substr(eqpos + 1).trim();
}
if (validateCallback)
if (!validateCallback(dict, key, value))
return null;
// Special handing for --include and --exclude, which are the only options that can appear multiple times (compare FilterCollector.DoExtractOptions)
// In all other cases, the last value overrides the earlier values
var lower = key.toLowerCase();
if (lower == 'include' || lower == 'exclude') {
if (!Array.isArray(dict[lower])) {
dict[lower] = [value];
} else {
dict[lower].push(value);
}
} else {
dict['--' + key] = value;
}
}
}
return dict;
};
this.parse_extra_options = function(str, dict) {
return this.parseOptionStrings(str, dict, function(d, k, v) {
if (d['--' + k] !== undefined) {
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Duplicate option {{opt}}', { opt: k }));
return false;
} else if (k == 'include' || k == 'exclude') {
// Cannot specify filters in extra options
// Not sure that this is really called.
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Cannot specify filter include or excludes in extra options'));
return false;
}
return true;
}) != null;
};
this.serializeAdvancedOptions = function(opts) {
return this.serializeAdvancedOptionsToArray(opts).join('\n');
};
this.serializeAdvancedOptionsToArray = function(opts) {
var res = [];
for(var n in opts)
if (n.indexOf('--') == 0)
res.push(n + '=' + opts[n]);
return res;
};
this.mergeAdvancedOptions = function(advStr, target, source) {
var adv = {}
if (!this.parse_extra_options(advStr, adv))
return false;
angular.extend(target, adv);
// Remove advanced options, no longer in the list
for(var n in source)
if (n.indexOf('--') == 0)
if (target[n] === undefined)
target[n] = null;
};
this.notifyInputError = function(msg) {
DialogService.dialog('Error', msg);
return false;
};
this.connectionError = function(txt, msg) {
if (typeof(txt) == typeof('')) {
if (msg == null)
return function (msg) {
var msgText = AppService.responseErrorMessage(msg);
DialogService.dialog(gettextCatalog.getString('Error'), txt + msgText);
};
} else {
msg = txt;
txt = '';
}
var msgText = AppService.responseErrorMessage(msg);
DialogService.dialog(gettextCatalog.getString('Error'), txt + msgText);
};
this.generatePassphrase = function() {
var specials = '!@#$%^&*()_+{}:"<>?[];\',./';
var lowercase = 'abcdefghijklmnopqrstuvwxyz';
var uppercase = lowercase.toUpperCase();
var numbers = '0123456789';
var all = specials + lowercase + uppercase + numbers;
function choose(str, n) {
var res = '';
for (var i = 0; i < n; i++) {
res += str.charAt(Math.floor(Math.random() * str.length));
}
return res;
};
var pwd = (
choose(specials, 2) +
choose(lowercase, 2) +
choose(uppercase, 2) +
choose(numbers, 2) +
choose(all, (Math.random()*5) + 5)
).split('');
for(var i = 0; i < pwd.length; i++) {
var pos = parseInt(Math.random() * pwd.length);
var t = pwd[i]
pwd[i] = pwd[pos];
pwd[pos] = t;
}
return pwd.join('');
}
this.nl2br = function(str, is_xhtml) {
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
};
this.preg_quote = function( str ) {
// http://kevin.vanzonneveld.net
// + original by: booeyOH
// + improved by: Ates Goral (http://magnetiq.com)
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + bugfixed by: Onno Marsman
// * example 1: preg_quote("$40");
// * returns 1: '\$40'
// * example 2: preg_quote("*RRRING* Hello?");
// * returns 2: '\*RRRING\* Hello\?'
// * example 3: preg_quote("\\.+*?[^]$(){}=!<>|:");
// * returns 3: '\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:'
return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
}
this.replace_all_insensitive = function(str, pattern, replacement) {
return str.replace( new RegExp( "(" + this.preg_quote(pattern) + ")" , 'gi' ), replacement );
};
this.replace_all = function(str, pattern, replacement) {
return str.replace( new RegExp( "(" + this.preg_quote(pattern) + ")" , 'g' ), replacement );
};
this.globToRegexp = function (str) {
// Escape special chars, except ? and *
str = (str + '').replace(/([\\\.\+\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
// Replace ? and * with .? and .*
return str.replace(/(\?|\*)/g, ".$1");
};
this.format = function() {
if (arguments == null || arguments.length < 1)
return null;
if (arguments.length == 1)
return arguments[0];
var msg = arguments[0];
for(var i = 1; i < arguments.length; i++)
msg = this.replace_all(msg, '{' + (i-1) + '}', arguments[i]);
return msg;
};
this.encodeDictAsUrl = function(obj) {
if (obj == null)
return '';
var str = [];
for(var p in obj) {
var x = encodeURIComponent(p);
if (obj[p] != null)
x += "=" + encodeURIComponent(obj[p]);
str.push(x);
}
if (str.length == 0)
return '';
return '?' + str.join("&");
};
var URL_REGEXP_FIELDS = ['source_uri', 'backend-type', '--auth-username', '--auth-password', 'server-name', 'server-port', 'server-path', 'querystring'];
var URL_REGEXP = /([^:]+)\:\/\/(?:(?:([^\:]+)(?:\:?:([^@]*))?\@))?(?:([^\/\?\:]*)(?:\:(\d+))?)(?:\/([^\?]*))?(?:\?(.+))?/;
// Same as URL_REGEXP, but also accepts :\\ as a separator between drive letter (server for legacy reasons) and path
var FILE_REGEXP = /(file)\:\/\/(?:(?:([^\:]+)(?:\:?:([^@]*))?\@))?(?:([^\/\?\:]*)(?:\:(\d+))?)(?:(?:\/|\:\\)([^\?]*))?(?:\?(.+))?/;
var QUERY_REGEXP = /(?:^|&)([^&=]*)=?([^&]*)/g;
this.decode_uri = function(uri, backendlist) {
var i = 0;
var res = {};
// File URLs contain backslashes on Win which breaks the other regexp
// This is not standard, but for compatibility it is allowed for now
var fileMatch = FILE_REGEXP.exec(uri);
if (fileMatch) {
for (i = 0; i < URL_REGEXP_FIELDS.length; ++i) {
res[URL_REGEXP_FIELDS[i]] = fileMatch[i] || "";
}
} else {
var m = URL_REGEXP.exec(uri);
// Invalid URI
if (!m)
return res;
for (i = 0; i < URL_REGEXP_FIELDS.length; ++i) {
res[URL_REGEXP_FIELDS[i]] = m[i] || "";
}
}
res.querystring.replace(QUERY_REGEXP, function(str, key, val) {
if (key)
res['--' + key] = decodeURIComponent((val || '').replace(/\+/g, '%20'));
});
var backends = {};
for(i in backendlist)
backends[backendlist[i].Key] = true;
var scheme = res['backend-type'];
if (scheme && scheme[scheme.length - 1] == 's' && !backends[scheme] && backends[scheme.substr(0, scheme.length-1)]) {
res['backend-type'] = scheme.substr(0, scheme.length-1);
res['--use-ssl'] = true;
}
return res;
};
this.find_scheme = function(uri, backendlist) {
if (!uri || uri.length == 0)
return null;
uri = uri.trim().toLowerCase();
var ix = uri.indexOf('://');
if (ix <= 0) {
for(var i in backendlist)
if (backendlist[i].Key == 'file')
return 'file';
return backendlist[0];
}
return (EDIT_URI.decode_uri(uri)['backend-type'] || '').toLowerCase()
};
this.contains_value = function(dict, value) {
for(var k in dict)
if (dict[k] == value)
return true;
return false;
};
this.contains_key = function(dict, key, keyname) {
for(var i in dict)
if (keyname == null ? i == key : dict[i][keyname] == key)
return true;
return false;
};
this.removeEmptyEntries = function(x) {
for (var i = x.length - 1; i >= 0; i--) {
if (x[i] == null || x[i] == '')
x.splice(i, 1);
};
return x;
};
this.splitFilterIntoTypeAndBody = function(src, dirsep) {
if (src == null)
return null;
if (dirsep == null) {
throw new Error('No dirsep provided!');
}
function matches(txt, n) {
var pre = apputils.replace_all(n.prefix || '', '!', dirsep);
var suf = apputils.replace_all(n.suffix || '', '!', dirsep);
if (txt.indexOf(pre) != 0 || txt.lastIndexOf(suf) != txt.length - suf.length || txt.lastIndexOf(suf) < 0)
return null;
var type = n.key;
var body = txt.substr(pre.length);
body = body.substr(0, body.length - suf.length);
if (body.length >= 2 && body[1] == '[') {
body = body.substr(1, body.length - 2);
}
if (n.exclude != null) {
for (var i = n.exclude.length - 1; i >= 0; i--)
if (body.indexOf(apputils.replace_all(n.exclude[i] || '', '!', dirsep)) >= 0)
return null;
}
return [type, body];
}
var m = [];
for (var i in this.filterClasses) {
var n = matches(src, this.filterClasses[i]);
if (n != null)
m.push(n);
}
if (m.length == 0) {
return null;
}
// Select match with shortest body, meaning longest prefix and suffix
var shortestIdx = 0;
var shortestLen = src.length;
for (i = 0; i < m.length; ++i) {
if (m[i][1].length < shortestLen) {
shortestIdx = i;
shortestLen = m[i][1].length;
}
}
return m[shortestIdx];
};
this.buildFilter = function(type, body, dirsep) {
if (type == null || this.filterTypeMap[type] == null)
return body;
body = body || '';
var f = this.filterTypeMap[type];
var pre = this.replace_all(f.prefix || '', '!', dirsep);
var suf = this.replace_all(f.suffix || '', '!', dirsep);
if (pre.length >= 2 && pre[1] == '[') {
//Regexp encode body ....
}
if (f.stripSep == true && body[body.length - 1] == dirsep) {
// Remove trailing dirsep
body = body.slice(0, -1);
}
return pre + body + suf;
};
this.filterToRegexpStr = function (filter) {
var firstChar = filter.substr(0, 1);
var lastChar = filter.substr(filter.length - 1, 1);
var isFilterGroup = firstChar == '{' && lastChar == '}';
if (isFilterGroup && this.filterGroupMap !== null) {
// Replace filter groups with filter strings
var filterGroups = filter.substr(1,filter.length - 2).split(',');
var filterStrings = [];
for (var i = 0; i < filterGroups.length; ++i) {
filterStrings = filterStrings.concat(this.filterGroupMap[filterGroups[i].trim()] || []);
}
filter = filterStrings.map(function (s) {
return '(?:' + apputils.filterToRegexpStr(s) + ')'
}).join('|');
} else {
var rx = firstChar == '[' && lastChar == ']';
if (rx)
filter = filter.substr(1, filter.length - 2);
else {
filter = this.globToRegexp(filter);
}
}
return filter;
};
this.filterListToRegexps = function(filters, caseSensitive) {
var res = [];
for(var i = 0; i < filters.length; i++) {
var f = filters[i];
if (f == null || f.length == 0)
continue;
var flag = f.substr(0, 1);
var filter = f.substr(1);
try {
var regexp = this.filterToRegexpStr(filter);
res.push([flag == '+', new RegExp(regexp, caseSensitive ? 'g' : 'gi')]);
} catch (e) {
}
}
return res;
};
this.evalFilter = function(path, filters, include) {
for (var i = 0; i < filters.length; i++) {
var m = path.match(filters[i][1]);
// Regex such as '.*' might match empty string at the end which is unwanted
// Check that the first match covers the full string
if (m && m.length >= 1 && m[0].length == path.length)
return filters[i][0];
}
return include === undefined ? true : include;
};
this.buildOptionList = function(sysinfo, encmodule, compmodule, backmodule) {
if (sysinfo == null || sysinfo.Options == null)
return null;
var items = angular.copy(sysinfo.Options);
for(var n in items)
items[n].Category = gettextCatalog.getString('Core options');
function copyToList(lst, key) {
if (key != null && typeof(key) != typeof(''))
key = null;
for(var n in lst)
{
if (key == null || key.toLowerCase() == lst[n].Key.toLowerCase())
{
var m = angular.copy(lst[n].Options);
for(var i in m)
m[i].Category = lst[n].DisplayName;
items.push.apply(items, m);
}
}
}
copyToList(sysinfo.GenericModules);
if (encmodule !== false)
copyToList(sysinfo.EncryptionModules, encmodule);
if (compmodule !== false)
copyToList(sysinfo.CompressionModules, compmodule);
if (backmodule !== false)
copyToList(sysinfo.BackendModules, backmodule);
return items;
};
this.extractServerModuleOptions = function(advOptionsList, servermodulelist, servermodulesettings, optionlistname) {
if (optionlistname == null)
optionlistname = 'SupportedCommands';
for(var mix in servermodulelist) {
var mod = servermodulelist[mix];
for(var oix in mod[optionlistname]) {
var opt = mod[optionlistname][oix];
var prefixstr = '--' + opt.Name + '=';
for (var i = advOptionsList.length - 1; i >= 0; i--) {
if (advOptionsList[i].indexOf(prefixstr) == 0) {
servermodulesettings[opt.Name] = advOptionsList[i].substr(prefixstr.length);
advOptionsList.splice(i, 1);
}
}
}
}
};
this.formatDuration = function(duration) {
// duration is a timespan string in format (dd.)hh:mm:ss.sss
// the part (dd.) is as indicated optional
if (duration == null) {
return;
}
var timespanArray = duration.split(':');
if (timespanArray.length < 3) {
return;
}
if (timespanArray[0].indexOf('.') > 0) {
timespanArray[0] = timespanArray[0].replace('.', " day(s) and ");
}
// round second according to ms
timespanArray[2] = Math.round(parseFloat(timespanArray[2])).toString();
// zero-padding
if (timespanArray[2].length == 1) timespanArray[2] = '0' + timespanArray[2];
return timespanArray.join(':');
};
});