Files
kopia/app/public/electron.js
2021-10-14 23:21:07 -07:00

389 lines
11 KiB
JavaScript

const { app, BrowserWindow, Notification, Menu, Tray, ipcMain, dialog, shell } = require('electron')
const path = require('path');
const isDev = require('electron-is-dev');
const { autoUpdater } = require("electron-updater");
const { resourcesPath, selectByOS } = require('./utils');
const { toggleLaunchAtStartup, willLaunchAtStartup, refreshWillLaunchAtStartup } = require('./auto-launch');
const { serverForRepo } = require('./server');
const log = require("electron-log");
const { loadConfigs, allConfigs, deleteConfigIfDisconnected, addNewConfig, configDir, isFirstRun, isPortableConfig } = require('./config');
app.name = 'KopiaUI';
if (isPortableConfig()) {
// in portable mode, write cache under 'repositories'
app.setPath('userData', path.join(configDir(), 'cache'));
}
let tray = null
let repoWindows = {};
let repoIDForWebContents = {};
function showRepoWindow(repoID) {
if (repoWindows[repoID]) {
repoWindows[repoID].focus();
return;
}
let rw = new BrowserWindow({
width: 1000,
height: 700,
title: 'Kopia UI Loading...',
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
})
repoWindows[repoID] = rw
const wcID = rw.webContents.id;
repoIDForWebContents[wcID] = repoID
rw.webContents.on('did-fail-load', () => {
log.error('failed to load');
// schedule another attempt in 0.5s
if (repoWindows[repoID]) {
setTimeout(() => {
log.info('reloading');
if (repoWindows[repoID]) {
repoWindows[repoID].loadURL(serverForRepo(repoID).getServerAddress() + '/?ts=' + new Date().valueOf());
}
}, 500)
}
})
rw.loadURL(serverForRepo(repoID).getServerAddress() + '/?ts=' + new Date().valueOf());
updateDockIcon();
rw.on('closed', function () {
// forget the reference.
rw = null;
delete (repoWindows[repoID]);
delete (repoIDForWebContents[wcID]);
const s = serverForRepo(repoID);
if (deleteConfigIfDisconnected(repoID)) {
s.stopServer();
}
updateDockIcon();
});
}
if (!app.requestSingleInstanceLock()) {
app.quit()
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
for (let repoID in repoWindows) {
let rw = repoWindows[repoID];
if (rw.isMinimized()) {
rw.restore()
}
rw.focus()
}
})
}
app.on('will-quit', function () {
allConfigs().forEach(v => serverForRepo(v).stopServer());
});
app.on('login', (event, webContents, request, authInfo, callback) => {
const repoID = repoIDForWebContents[webContents.id];
// intercept password prompts and automatically enter password that the server has printed for us.
const p = serverForRepo(repoID).getServerPassword();
if (p) {
event.preventDefault();
log.info('automatically logging in...');
callback('kopia', p);
}
});
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
const repoID = repoIDForWebContents[webContents.id];
// intercept certificate errors and automatically trust the certificate the server has printed for us.
const expected = 'sha256/' + Buffer.from(serverForRepo(repoID).getServerCertSHA256(), 'hex').toString('base64');
if (certificate.fingerprint === expected) {
log.debug('accepting server certificate.');
// On certificate error we disable default behaviour (stop loading the page)
// and we then say "it is all fine - true" to the callback
event.preventDefault();
callback(true);
return;
}
log.warn('certificate error:', certificate.fingerprint, expected);
});
// Ignore
app.on('window-all-closed', function () { })
ipcMain.handle('select-dir', async (event, arg) => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory']
});
if (result.filePaths) {
return result.filePaths[0];
} else {
return null;
};
})
ipcMain.on('server-status-updated', updateTrayContextMenu);
ipcMain.on('launch-at-startup-updated', updateTrayContextMenu);
let updateAvailableInfo = null;
let updateDownloadStatusInfo = "";
// set this environment variable when developing
// to allow offering downgrade to the latest released version.
autoUpdater.allowDowngrade = process.env["KOPIA_UI_ALLOW_DOWNGRADE"] == "1";
// we will be manually triggering download and quit&install.
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
let lastNotifiedVersion = "";
autoUpdater.on('update-available', a => {
log.info('update available ' + a.version);
updateAvailableInfo = a;
updateDownloadStatusInfo = "";
updateTrayContextMenu();
// do not notify more than once for a particular version.
if (lastNotifiedVersion != a.version) {
lastNotifiedVersion = a.version;
const n = new Notification({
title: "New version of KopiaUI",
body: "Version v" + a.version + " is available.\n\nClick here to download and install it.",
});
n.on('click', () => installUpdate());
n.show();
}
})
autoUpdater.on('update-not-available', () => {
updateAvailableInfo = null;
updateDownloadStatusInfo = "";
updateTrayContextMenu();
})
autoUpdater.on('download-progress', progress => {
if (updateAvailableInfo) {
updateDownloadStatusInfo = "Downloading Update: v" + updateAvailableInfo.version + " (" + (Math.round(progress.percent * 10) / 10.0) + "%) ...";
updateTrayContextMenu();
}
});
autoUpdater.on('update-downloaded', info => {
updateDownloadStatusInfo = "Installing Update: v" + updateAvailableInfo.version + " ...";
updateTrayContextMenu();
autoUpdater.quitAndInstall();
});
autoUpdater.on('error', a => {
updateAvailableInfo = null;
updateDownloadStatusInfo = "Error checking for updates.";
updateTrayContextMenu();
});
function checkForUpdates() {
updateReadyToInstall = false;
updateDownloadStatusInfo = "Checking for update...";
updateAvailableInfo = null;
updateTrayContextMenu();
autoUpdater.checkForUpdates();
}
function installUpdate() {
updateDownloadStatusInfo = "Downloading and installing update...";
autoUpdater.downloadUpdate();
}
function viewReleaseNotes() {
const ver = updateAvailableInfo.version + "";
if (ver.match(/^\d{8}\./)) {
// kopia-test builds are named yyyymmdd.0.hhmmss
shell.openExternal("https://github.com/kopia/kopia-test-builds/releases/v" + ver);
} else {
shell.openExternal("https://github.com/kopia/kopia/releases/v" + ver);
}
}
function isOutsideOfApplicationsFolderOnMac() {
if (isDev || isPortableConfig()) {
return false;
}
// this method is only available on Mac.
if (!app.isInApplicationsFolder) {
return false;
}
return !app.isInApplicationsFolder();
}
function maybeMoveToApplicationsFolder() {
dialog.showMessageBox({
buttons: ["Yes", "No"],
message: "For best experience, Kopia needs to be installed in Applications folder.\n\nDo you want to move it now?"
}).then(r => {
if (r.response == 0) {
app.moveToApplicationsFolder();
} else {
checkForUpdates();
}
}).catch(e => {
log.info(e);
});
}
function updateDockIcon() {
if (process.platform === 'darwin') {
let any = false
for (const k in repoWindows) {
any = true;
}
if (any) {
app.dock.show();
} else {
app.dock.hide();
}
}
}
function showAllRepoWindows() {
allConfigs().forEach(showRepoWindow);
}
app.on('ready', () => {
loadConfigs();
if (isPortableConfig()) {
const logDir = path.join(configDir(), "logs");
log.transports.file.resolvePath = (variables) => path.join(logDir, variables.fileName);
}
log.transports.console.level = "warn"
log.transports.file.level = "debug"
autoUpdater.logger = log
// re-check for updates every 24 hours
setInterval(checkForUpdates, 86400000);
tray = new Tray(
path.join(
resourcesPath(), 'icons',
selectByOS({ mac: 'kopia-tray.png', win: 'kopia-tray.ico', linux: 'kopia-tray.png' })));
tray.setToolTip('Kopia');
updateTrayContextMenu();
refreshWillLaunchAtStartup();
updateDockIcon();
allConfigs().forEach(repoID => serverForRepo(repoID).actuateServer());
if (isFirstRun()) {
// open all repo windows on first run.
showAllRepoWindows();
// on Windows, also show the notification.
if (process.platform === "win32") {
tray.displayBalloon({
title: "Kopia is running in the background",
content: "Click on the system tray icon to open the menu",
});
}
}
if (isOutsideOfApplicationsFolderOnMac()) {
setTimeout(maybeMoveToApplicationsFolder, 1000);
} else {
checkForUpdates();
}
})
ipcMain.addListener('config-list-updated-event', () => updateTrayContextMenu());
ipcMain.addListener('status-updated-event', () => updateTrayContextMenu());
function addAnotherRepository() {
const repoID = addNewConfig();
serverForRepo(repoID).actuateServer();
showRepoWindow(repoID);
}
function updateTrayContextMenu() {
if (!tray) {
return;
}
let defaultReposTemplates = [];
let additionalReposTemplates = [];
allConfigs().forEach(repoID => {
const sd = serverForRepo(repoID).getServerStatusDetails();
let desc = "";
if (sd.connecting) {
desc = "<starting up>";
} else if (!sd.connected) {
desc = "<not connected>";
} else {
desc = sd.description;
}
// put primary repository first.
const collection = repoID === ("repository") ? defaultReposTemplates : additionalReposTemplates
collection.push(
{
label: desc,
click: () => showRepoWindow(repoID),
toolTip: desc + " (" + repoID + ")",
},
);
});
if (additionalReposTemplates.length > 0) {
additionalReposTemplates.sort((a, b) => a.label.localeCompare(b.label));
}
let autoUpdateMenuItems = [];
if (updateDownloadStatusInfo) {
autoUpdateMenuItems.push({ label: updateDownloadStatusInfo, enabled: false });
} else if (updateAvailableInfo) {
autoUpdateMenuItems.push({ label: 'Update Available: v' + updateAvailableInfo.version, click: viewReleaseNotes });
autoUpdateMenuItems.push({ label: 'Download And Install...', click: installUpdate });
} else {
autoUpdateMenuItems.push({ label: "KopiaUI is up-to-date: " + app.getVersion(), enabled: false });
}
template = defaultReposTemplates.concat(additionalReposTemplates).concat([
{ type: 'separator' },
{ label: 'Connect To Another Repository...', click: addAnotherRepository },
{ type: 'separator' },
{ label: 'Check For Updates Now', click: checkForUpdates },
]).concat(autoUpdateMenuItems).concat([
{ type: 'separator' },
{ label: 'Launch At Startup', type: 'checkbox', click: toggleLaunchAtStartup, checked: willLaunchAtStartup() },
{ label: 'Quit', role: 'quit' },
]);
tray.setContextMenu(Menu.buildFromTemplate(template));
}