import { prettyBinarySize } from '@cloudron/pankow/utils'; import { RELAY_PROVIDERS, ISTATES, STORAGE_PROVIDERS, EVENTS } from './constants.js'; import { Marked } from 'marked'; function safeMarked() { const marked = new Marked({ renderer: { link({ href, title, text }) { if (href && href.startsWith('mailto:')) return text; // mailto is rendered as text const titleAttr = title ? ` title="${title}"` : ''; const isAbsolute = href && (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')); const targetAttr = isAbsolute ? ' target="_blank"' : ''; return `${text}`; } } }); return marked; } function renderSafeMarkdown(text) { return safeMarked().parse(text); } function prettyRelayProviderName(provider) { if (provider === 'noop') return 'Disabled (no email will be sent)'; const tmp = RELAY_PROVIDERS.find(p => p.provider === provider); return tmp ? tmp.name : provider; } // https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341 function download(filename, text) { var element = document.createElement('a'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); element.setAttribute('download', filename); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } function mountlike(provider) { return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk'; } function s3like(provider) { return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces' || provider === 'hetzner-objectstorage' || provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2' || provider === 'contabo-objectstorage' || provider === 'synology-c2-objectstorage'; } function regionName(provider, endpoint) { const storageProvider = STORAGE_PROVIDERS.find(sp => sp.value === provider); const regions = storageProvider.regions; if (!regions) return endpoint; const region = regions.find(r => r.value === endpoint); if (!region) return endpoint; return region.name; } function prettySiteLocation(site) { switch (site.provider) { case 'filesystem': return site.config.backupDir + (site.config.prefix ? `/${site.config.prefix}` : ''); case 'disk': case 'ext4': case 'xfs': case 'mountpoint': return (site.config.mountOptions.diskPath || site.config.mountPoint) + (site.config.prefix ? ` / ${site.config.prefix}` : ''); case 'cifs': case 'nfs': case 'sshfs': return site.config.mountOptions.host + ':' + site.config.mountOptions.remoteDir + (site.config.prefix ? ` / ${site.config.prefix}` : ''); case 's3': return site.config.region + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : ''); case 'minio': return site.config.endpoint + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : ''); case 'gcs': return site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : ''); default: return regionName(site.provider, site.config.endpoint) + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : ''); } } function eventlogDetails(eventLog, app = null, appIdContext = '') { const data = eventLog.data; const errorMessage = data.errorMessage; let details; function appName(pre, app, defaultValue) { if (appIdContext) return defaultValue || ''; pre = pre ? (pre + ' ') : ''; return pre + (app.label || app.fqdn || app.subdomain) + ' (' + app.manifest.title + ') '; } switch (eventLog.action) { case EVENTS.ACTIVATE: return 'Cloudron was activated'; case EVENTS.PROVISION: return 'Cloudron was setup'; case EVENTS.RESTORE: return 'Cloudron was restored using backup at ' + data.remotePath; case EVENTS.APP_CONFIGURE: { if (!data.app) return ''; app = data.app; if ('accessRestriction' in data) { // since it can be null return 'Access restriction ' + appName('of', app) + ' was changed'; } else if ('operators' in data) { return 'Operators ' + appName('of', app) + ' was changed'; } else if (data.label) { return `Label ${appName('of', app)} was set to ${data.label}`; } else if (data.tags) { return `Tags ${appName('of', app)} was set to ${data.tags.join(', ')}`; } else if (data.icon) { return 'Icon ' + appName('of', app) + ' was changed'; } else if (data.memoryLimit) { return 'Memory limit ' + appName('of', app) + ' was set to ' + prettyBinarySize(data.memoryLimit); } else if (data.cpuShares) { // replaced by cpuQuota in 8.0 return 'CPU shares ' + appName('of', app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%'; } else if (data.cpuQuota) { return 'CPU quota ' + appName('of', app) + ' was set to ' + data.cpuQuota + '%'; } else if (data.env) { return 'Env vars ' + appName('of', app) + ' was changed'; } else if ('debugMode' in data) { // since it can be null if (data.debugMode) return appName('', app, 'App') + ' was placed in repair mode'; else return appName('', app, 'App') + ' was taken out of repair mode'; } else if ('enableBackup' in data) { return 'Automatic backups ' + appName('of', app) + ' was ' + (data.enableBackup ? 'enabled' : 'disabled'); } else if ('enableAutomaticUpdate' in data) { return 'Automatic updates ' + appName('of', app) + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled'); } else if ('reverseProxyConfig' in data) { return 'Reverse proxy configuration ' + appName('of', app) + ' was updated'; } else if ('upstreamUri' in data) { return 'Upstream URI ' + appName('of', app) + ' was updated'; } else if ('cert' in data) { if (data.cert) return 'Custom certificate was set ' + appName('for', app); else return 'Certificate ' + appName('of', app) + ' was reset'; } else if (data.subdomain) { if (data.fqdn !== data.app.fqdn) { return 'Location ' + appName('of', app) + ' was changed to ' + data.fqdn; } else if (data.redirectDomains !== data.app.redirectDomains) { const altFqdns = data.redirectDomains.map(a => a.fqdn); return 'Alternate domains ' + appName('of', app) + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset'); } else if (data.aliasDomains !== data.app.aliasDomains) { const aliasDomains = data.aliasDomains.map(a => a.fqdn); return 'Alias domains ' + appName('of', app) + ' was ' + (aliasDomains.length ? 'set to ' + aliasDomains.join(', ') : 'reset'); } else if (data.portBindings !== data.app.portBindings) { return 'Port bindings ' + appName('of', app) + ' was changed'; } } else if ('dataDir' in data) { if (data.dataDir) return 'Data directory ' + appName('of', app) + ' was set ' + data.dataDir; else return 'Data directory ' + appName('of', app) + ' was reset'; } else if ('icon' in data) { if (data.icon) return 'Icon ' + appName('of', app) + ' was set'; else return 'Icon ' + appName('of', app) + ' was reset'; } else if ('mailboxName' in data) { if (data.mailboxName) return `Mailbox ${appName('of', app)} was set to ${data.mailboxDisplayName || '' } ${data.mailboxName}@${data.mailboxDomain}`; else return 'Mailbox ' + appName('of', app) + ' was disabled'; } return appName('', app, 'App ') + 'was re-configured'; } case EVENTS.APP_INSTALL: if (!data.app) return ''; details = data.app.versionsUrl ? 'Community app ' : ''; details += data.app.manifest.title + ' (package v' + data.app.manifest.version + ')'; details += data.sourceBuild ? ' was built and installed' : ' was installed'; details += appIdContext ? '' : ` at ${data.app.fqdn}`; return details; case EVENTS.APP_RESTORE: if (!data.app) return ''; details = appName('', data.app, 'App') + ' was restored'; // older versions (<3.5) did not have these fields if (data.fromManifest) details += ' from version ' + data.fromManifest.version; if (data.toManifest) details += ' to version ' + data.toManifest.version; if (data.remotePath) details += ' using backup at ' + data.remotePath; return details; case EVENTS.APP_IMPORT: if (!data.app) return ''; details = appName('', data.app, 'App') + ' was imported'; if (data.toManifest) details += ' to version ' + data.toManifest.version; if (data.remotePath) details += ' using backup at ' + data.remotePath; return details; case EVENTS.APP_UNINSTALL: if (!data.app) return ''; return appName('', data.app, 'App') + ' (package v' + data.app.manifest.version + ') was uninstalled'; case EVENTS.APP_UPDATE: if (!data.app) return ''; return 'Update ' + appName('of', data.app) + ' started from v' + data.fromManifest.version + ' to v' + data.toManifest.version; case EVENTS.APP_UPDATE_FINISH: if (!data.app) return ''; return appName('', data.app, 'App') + ' was updated to v' + data.app.manifest.version; case EVENTS.APP_BACKUP: if (!data.app) return ''; return 'Backup ' + appName('of', data.app) + ' started'; case EVENTS.APP_BACKUP_FINISH: if (!data.app) return ''; if (errorMessage) return 'Backup ' + appName('of', data.app) + ' failed: ' + errorMessage; else return 'Backup ' + appName('of', data.app) + ' succeeded'; case EVENTS.APP_CLONE: if (appIdContext === data.oldAppId) return 'App was cloned to ' + data.newApp.fqdn + ' using backup at ' + data.remotePath; else if (appIdContext === data.appId) return 'App was cloned from ' + data.oldApp.fqdn + ' using backup at ' + data.remotePath; else return appName('', data.newApp, 'App') + ' was cloned ' + appName('from', data.oldApp) + ' using backup at ' + data.remotePath; case EVENTS.APP_REPAIR: return appName('', data.app, 'App') + ' was re-configured'; // re-configure of email apps is more common? case EVENTS.APP_LOGIN: { if (!app) return ''; return appName('', app, 'App') + ' logged in'; } case EVENTS.APP_OOM: if (!data.app) return ''; return appName('', data.app, 'App') + ' ran out of memory'; case EVENTS.APP_DOWN: if (!data.app) return ''; return appName('', data.app, 'App') + ' is down'; case EVENTS.APP_UP: if (!data.app) return ''; return appName('', data.app, 'App') + ' is back online'; case EVENTS.APP_START: if (!data.app) return ''; return appName('', data.app, 'App') + ' was started'; case EVENTS.APP_STOP: if (!data.app) return ''; return appName('', data.app, 'App') + ' was stopped'; case EVENTS.APP_RESTART: if (!data.app) return ''; return appName('', data.app, 'App') + ' was restarted'; case EVENTS.ARCHIVES_ADD: return 'Backup ' + data.backupId + ' added to archive'; case EVENTS.ARCHIVES_DEL: return 'Backup ' + data.backupId + ' deleted from archive'; case EVENTS.BACKUP_START: return `Backup started at site ${data.siteName}`; case EVENTS.BACKUP_FINISH: if (!errorMessage) return `Cloudron backup created at site ${data.siteName}`; else return `Cloudron backup at site ${data.siteName} errored with error: ${errorMessage}`; case EVENTS.BACKUP_CLEANUP_START: return 'Backup cleaner started'; case EVENTS.BACKUP_CLEANUP_FINISH: return errorMessage ? 'Backup cleaner errored: ' + errorMessage : 'Backup cleaner removed ' + (data.removedBoxBackupPaths ? data.removedBoxBackupPaths.length : '0') + ' backup(s)'; case EVENTS.BACKUP_SITE_ADD: return `New backup site ${data.name} added with provider ${data.provider} and format ${data.format}`; case EVENTS.BACKUP_SITE_UPDATE: if (data.schedule) { return `Backup site ${data.name} schedule was updated to ${data.schedule}`; } else if (data.limits) { return `Backup site ${data.name} limits were updated`; } else if (data.retention) { return `Backup site ${data.name} retention was updated`; } else if (data.contents) { return `Backup site ${data.name} contents were updated`; } else if (data.encryption) { return `Backup site ${data.name} encryption was ${data.encryption ? 'enabled' : 'disabled'}`; } else if (data.config) { return `Backup site ${data.name} credentials were updated`; } else if (data.oldName) { return `Backup site was renamed from ${data.oldName} to ${data.name}`; } else { return `Backup site ${data.name} was updated`; } case EVENTS.BACKUP_SITE_REMOVE: return `Backup site ${data.name} removed`; case EVENTS.BACKUP_INTEGRITY_START: return 'Backup integrity check started'; // for ${data.backupId} case EVENTS.BACKUP_INTEGRITY_FINISH: if (!errorMessage) return `Backup integrity check ${data.status}`; // passed or failed else return `Backup integrity check errored: ${errorMessage}`; case EVENTS.BRANDING_AVATAR: return 'Cloudron Avatar Changed'; case EVENTS.BRANDING_NAME: return 'Cloudron Name set to ' + data.name; case EVENTS.BRANDING_FOOTER: return 'Cloudron Footer set to ' + data.footer; case EVENTS.CERTIFICATE_NEW: details = 'Certificate installation for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); if (data.renewalInfo) details += `. Recommended renewal time is between ${data.renewalInfo.start} and ${data.renewalInfo.end}`; return details; case EVENTS.CERTIFICATE_RENEWAL: return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); case EVENTS.CERTIFICATE_CLEANUP: return 'Certificate(s) of ' + data.domains.join(',') + ' was cleaned up since they expired 6 months ago'; case EVENTS.DASHBOARD_DOMAIN_UPDATE: return 'Dashboard domain set to ' + data.fqdn || (data.subdomain + '.' + data.domain); case EVENTS.DIRECTORY_SERVER_CONFIGURE: if (data.fromEnabled !== data.toEnabled) return 'Directory server was ' + (data.toEnabled ? 'enabled' : 'disabled'); else return 'Directory server configuration was changed'; case EVENTS.DOMAIN_ADD: return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added'; case EVENTS.DOMAIN_UPDATE: return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was updated'; case EVENTS.DOMAIN_REMOVE: return 'Domain ' + data.domain + ' was removed'; case EVENTS.EXTERNAL_LDAP_CONFIGURE: if (data.config.provider === 'noop') return 'External Directory disabled'; else return 'External Directory set to ' + data.config.url + ' (' + data.config.provider + ')'; case EVENTS.GROUP_ADD: return 'Group ' + data.name + ' was added'; case EVENTS.GROUP_UPDATE: return 'Group name changed from ' + data.oldName + ' to ' + data.group.name; case EVENTS.GROUP_REMOVE: return 'Group ' + data.group.name + ' was removed'; case EVENTS.GROUP_MEMBERSHIP: return 'Group membership of ' + data.group.name + ' changed. Now was ' + data.userIds.length + ' member(s).'; case EVENTS.INSTALL_FINISH: return 'Cloudron version ' + data.version + ' installed'; case EVENTS.MAIL_LOCATION: return 'Mail server location was changed to ' + data.subdomain + (data.subdomain ? '.' : '') + data.domain; case EVENTS.MAIL_ENABLED: return 'Mail was enabled for domain ' + data.domain; case EVENTS.MAIL_DISABLED: return 'Mail was disabled for domain ' + data.domain; case EVENTS.MAIL_MAILBOX_ADD: return 'Mailbox ' + data.name + '@' + data.domain + ' was added'; case EVENTS.MAIL_MAILBOX_UPDATE: if (data.aliases) return 'Mailbox aliases of ' + data.name + '@' + data.domain + ' was updated'; else return 'Mailbox ' + data.name + '@' + data.domain + ' was updated'; case EVENTS.MAIL_MAILBOX_REMOVE: return 'Mailbox ' + data.name + '@' + data.domain + ' was removed'; case EVENTS.MAIL_LIST_ADD: return 'Mail list ' + data.name + '@' + data.domain + 'was added'; case EVENTS.MAIL_LIST_UPDATE: return 'Mail list ' + data.name + '@' + data.domain + ' was updated'; case EVENTS.MAIL_LIST_REMOVE: return 'Mail list ' + data.name + '@' + data.domain + ' was removed'; case EVENTS.REGISTRY_ADD: return 'Docker registry ' + data.registry.provider + '@' + data.registry.serverAddress + ' was added'; case EVENTS.REGISTRY_UPDATE: return 'Docker registry updated to ' + data.newRegistry.provider + '@' + data.newRegistry.serverAddress; case EVENTS.REGISTRY_DEL: return 'Docker registry ' + data.registry.provider + '@' + data.registry.serverAddress + ' was removed'; case EVENTS.START: return 'Cloudron started with version ' + data.version; case EVENTS.SERVICE_CONFIGURE: return 'Service ' + data.id + ' was configured'; case EVENTS.SERVICE_REBUILD: return 'Service ' + data.id + ' was rebuilt'; case EVENTS.SERVICE_RESTART: return 'Service ' + data.id + ' was restarted'; case EVENTS.UPDATE: return 'Cloudron update to version ' + data.boxUpdateInfo.version + ' was started'; case EVENTS.UPDATE_FINISH: if (errorMessage) return 'Cloudron update errored. Error: ' + errorMessage; else return 'Cloudron updated to version ' + data.newVersion; case EVENTS.USER_ADD: return 'User ' + data.email + (data.user.username ? ' (' + data.user.username + ')' : '') + ' was added'; case EVENTS.USER_UPDATE: return 'User ' + (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was updated'; case EVENTS.USER_REMOVE: return 'User ' + (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was removed'; case EVENTS.USER_TRANSFER: return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId; case EVENTS.USER_LOGIN: if (data.mailboxId) { return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to mailbox ' + data.mailboxId; } else if (data.appId) { return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in' + (appIdContext ? '' : ' to ' + (app ? app.fqdn : data.appId)); } else { // can happen with directoryserver return 'User ' + (data.user ? data.user.username : data.userId) + ' authenticated'; } case EVENTS.USER_LOGIN_GHOST: return 'User ' + (data.user ? data.user.username : data.userId) + ' was impersonated'; case EVENTS.USER_LOGOUT: return 'User ' + (data.user ? data.user.username : data.userId) + ' logged out'; case EVENTS.USER_DIRECTORY_PROFILE_CONFIG_UPDATE: return 'User directory profile config updated. Mandatory 2FA: ' + (data.config.mandatory2FA) + ' Lock profiles: ' + (data.config.lockUserProfiles); case EVENTS.DYNDNS_UPDATE: { details = errorMessage ? 'Error updating DNS. ' : 'Updated DNS. '; if (data.fromIpv4 !== data.toIpv4) details += 'From IPv4 ' + data.fromIpv4 + ' to ' + data.toIpv4 + '. '; if (data.fromIpv6 !== data.toIpv6) details += 'From IPv6 ' + data.fromIpv6 + ' to ' + data.toIpv6 + '.'; if (errorMessage) details += ' ' + errorMessage; return details; } case EVENTS.SUPPORT_SSH: return 'Remote Support was ' + (data.enable ? 'enabled' : 'disabled'); case EVENTS.SUPPORT_TICKET: return 'Support ticket was created'; case EVENTS.VOLUME_ADD: return 'Volume "' + (data.volume || data).name + '" was added'; case EVENTS.VOLUME_UPDATE: return 'Volme "' + (data.volume || data).name + '" was updated'; case EVENTS.VOLUME_REMOUNT: return 'Volume "' + (data.volume || data).name + '" was remounted'; case EVENTS.VOLUME_REMOVE: return 'Volume "' + (data.volume || data).name + '" was removed'; default: return eventLog.action; } } function eventlogSource(eventlog, app = null) { const source = eventlog.source; let line = source.username || source.userId || source.mailboxId || source.authType || 'system'; if (source.appId) { // const app = null; //getApp(source.appId); line += ' - ' + (app ? app.fqdn : source.appId); } return line; } function taskNameFromInstallationState(installationState) { switch (installationState) { case ISTATES.PENDING_INSTALL: return 'install'; case ISTATES.PENDING_CLONE: return 'clone'; case ISTATES.PENDING_LOCATION_CHANGE: return 'location change'; case ISTATES.PENDING_CONFIGURE: return 'configure'; case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container'; case ISTATES.PENDING_DEBUG: return 'debug'; case ISTATES.PENDING_RESIZE: return 'resize'; case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration'; case ISTATES.PENDING_SERVICES_CHANGE: return 'service change'; case ISTATES.PENDING_UNINSTALL: return 'uninstall'; case ISTATES.PENDING_RESTORE: return 'restore'; case ISTATES.PENDING_IMPORT: return 'import'; case ISTATES.PENDING_UPDATE: return 'update'; case ISTATES.PENDING_BACKUP: return 'backup'; case ISTATES.PENDING_START: return 'start app'; case ISTATES.PENDING_STOP: return 'stop app'; case ISTATES.PENDING_RESTART: return 'restart app'; default: return installationState || ''; } } // checks provision status and redirects to correct view // { // setup: { active, message, errorMessage } // restore { active, message, errorMessage } // activated // adminFqn // } // returns true if redirected . currentView is one of 'dashboard', 'restore', 'setup', 'activation' function redirectIfNeeded(status, currentView) { var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); if ('develop' in search || localStorage.getItem('develop')) { console.warn('Cloudron develop mode on. To disable run localStorage.removeItem(\'develop\')'); localStorage.setItem('develop', true); return false; } if (status.activated) { if (currentView === 'dashboard') { // support local development with localhost check if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) { // user is accessing by IP or by the old admin location (pre-migration) window.location.href = '/setup.html' + window.location.search; return true; } return false; } window.location.href = 'https://' + status.adminFqdn + '/'; return true; } if (status.setup.active) { console.log('Setup is active'); if (currentView === 'setup') return false; window.location.href = '/setup.html' + window.location.search; return true; } if (status.restore.active) { console.log('Restore is active'); if (currentView === 'restore') return; window.location.href = '/restore.html' + window.location.search; return true; } if (status.adminFqdn) { console.log('adminFqdn is set'); // if we are here from https://ip/activation.html ,go to https://admin/activation.html if (status.adminFqdn !== window.location.hostname) { window.location.href = 'https://' + status.adminFqdn + '/activation.html' + (window.location.search); return true; } if (currentView === 'activation') return false; window.location.href = 'https://' + status.adminFqdn + '/activation.html' + (window.location.search); return true; } if (currentView !== 'setup' && currentView !== 'restore') { window.location.href = '/setup.html' + window.location.search; return true; } // if we are here, proceed with current view return false; } // strip potential sso tags in postinstall notes function stripSsoInfo(notes, sso) { if (sso) return notes.replace(/.*?<\/nosso>/gs, '').replace(//gs, '').replace(/<\/sso>/gs, ''); return notes.replace(/.*?<\/sso>/gs, '').replace(//gs, '').replace(/<\/nosso>/gs, ''); } function getDataURLFromFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(event) { resolve(event.target.result); }; reader.onerror = function(event) { reject(event.target.error); }; reader.readAsDataURL(file); }); } function getTextFromFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = (e) => reject(e); reader.readAsText(file); }); } // values correspond to cron days const cronDays = [ { id: 0, name: 'Sunday' }, { id: 1, name: 'Monday' }, { id: 2, name: 'Tuesday' }, { id: 3, name: 'Wednesday' }, { id: 4, name: 'Thursday' }, { id: 5, name: 'Friday' }, { id: 6, name: 'Saturday' }, ]; // generates 24h time sets (instead of american 12h) to avoid having to translate everything to locales eg. 12:00 const cronHours = Array.from({ length: 24 }).map(function (v, i) { return { id: i, name: (i < 10 ? '0' : '') + i + ':00' }; }); function prettySchedule(pattern) { if (!pattern) return ''; const tmp = pattern.trim().split(/\s+/); // remove extra spaces between tokens, which is valid cron if (tmp.length === 1) return pattern.charAt(0).toUpperCase() + pattern.slice(1); // case for 'never' - capitalize if (tmp.length === 5) tmp.unshift('0'); // if seconds is missing, add it if (tmp.length !== 6) return `Unrecognized pattern - ${pattern}`; const hours = tmp[2].split(',').sort((a, b) => Number(a) - Number(b)); const days = tmp[5].split(',').sort((a, b) => Number(a) - Number(b)); try { const prettyDay = (days.length === 7 || days[0] === '*') ? 'Every day' : days.map((day) => { return cronDays[parseInt(day, 10)].name.substr(0, 3); }).join(', '); const prettyHour = (hours.length === 24 || hours[0] === '*') ? 'hourly' : hours.map((hour) => { return cronHours[parseInt(hour, 10)]; }).sort((a,b) => a.id - b.id).map(h => h.name).join(', '); return `${prettyDay} @ ${prettyHour}`; } catch (error) { console.error('Unable to build pattern.', error); return `Unrecognized pattern - ${pattern}`; } }; function parseSchedule(pattern) { const tmp = pattern.trim().split(/\s+/); // remove extra spaces between tokens, which is valid cron if (tmp.length === 1) return console.error(`Never pattern should not be passed here - ${pattern}`); if (tmp.length === 5) tmp.unshift('0'); // if seconds is missing, add it if (tmp.length !== 6) return console.error(`Unrecognized pattern - ${pattern}`); const tmpHours = tmp[2].split(','); const tmpDays = tmp[5].split(','); let days, hours; if (tmpDays[0] === '*') days = cronDays.map((day) => { return day.id; }); else days = tmpDays.map((day) => { return parseInt(day, 10); }); if (tmpHours[0] === '*') hours = cronHours.map(h => h.id); else hours = tmpHours.map((hour) => { return parseInt(hour, 10); }); return { days, hours }; } function getColor(numOfSteps, step) { const deg = 360/numOfSteps; return `hsl(${deg*step} 70% 50%)`; } // split path into a prefix (anything before timestamp or 'snapshot') and the remaining remotePath function parseFullBackupPath(fullPath) { const parts = fullPath.split('/'); const timestampRegex = /^\d{4}-\d{2}-\d{2}-\d{6}-\d{3}$/; // timestamp (tag) const idx = parts.findIndex(p => timestampRegex.test(p) || p === 'snapshot'); let remotePath, prefix; if (idx === -1) { remotePath = parts.pop() || parts.pop(); // if fs+rsync there may be a trailing slash, so this removes it. this is basename() prefix = parts.join('/'); // this is dirname() } else { prefix = parts.slice(0, idx).join('/'); remotePath = parts.slice(idx).join('/'); } return { prefix, remotePath }; } // named exports export { renderSafeMarkdown, prettyRelayProviderName, download, mountlike, s3like, eventlogDetails, eventlogSource, taskNameFromInstallationState, redirectIfNeeded, stripSsoInfo, getDataURLFromFile, getTextFromFile, cronDays, cronHours, getColor, prettySchedule, parseSchedule, prettySiteLocation, parseFullBackupPath }; // default export export default { renderSafeMarkdown, prettyRelayProviderName, download, mountlike, s3like, eventlogDetails, eventlogSource, taskNameFromInstallationState, redirectIfNeeded, stripSsoInfo, getDataURLFromFile, getTextFromFile, cronDays, cronHours, getColor, prettySchedule, parseSchedule, prettySiteLocation, parseFullBackupPath };