mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-07-25 03:17:00 +00:00
Initial commit of new updater
This commit is contained in:
parent
698c10f356
commit
869ac1433f
16 changed files with 952 additions and 295 deletions
|
@ -1,41 +1,50 @@
|
|||
import * as fs from 'fs';
|
||||
import * as https from 'https';
|
||||
import * as path from 'path';
|
||||
import { URL } from 'url';
|
||||
import * as crypto from 'crypto';
|
||||
import * as log4js from "log4js";
|
||||
import { app } from 'electron';
|
||||
import { Store } from './Store';
|
||||
const extract = require('extract-zip');
|
||||
const logger = log4js.getLogger();
|
||||
|
||||
enum UpdateState {
|
||||
Copy,
|
||||
Cleanup,
|
||||
};
|
||||
|
||||
interface ReleaseInfo {
|
||||
previousVersions: [string];
|
||||
currentVersion: string;
|
||||
currentReleases: [
|
||||
string: [ // channel
|
||||
string: [ // os
|
||||
string: [ // arch
|
||||
string: []
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
channelCurrentVersions: [string: number];
|
||||
allVersions: [string];
|
||||
}
|
||||
|
||||
interface UpdateInfo {
|
||||
updateState: UpdateState;
|
||||
installPath: string;
|
||||
tempPath: string;
|
||||
currentVersion: string;
|
||||
}
|
||||
|
||||
export class Updater {
|
||||
private basePath: string;
|
||||
private baseUrl: string;
|
||||
private appFiles: string[];
|
||||
private static appPath: string = app.getAppPath();
|
||||
private static installPath: string = path.join(Updater.appPath, '../../');
|
||||
private static updateDataPath: string = path.join(app.getPath('userData'), 'updater');
|
||||
private static updateMetadataPath = path.join(Updater.updateDataPath, './update.json');
|
||||
private static baseUrl: string = 'https://dl.fcast.org/electron';
|
||||
|
||||
constructor(basePath: string, baseUrl: string) {
|
||||
this.basePath = basePath;
|
||||
this.baseUrl = baseUrl;
|
||||
this.appFiles = [
|
||||
'dist/main/c.mp4',
|
||||
'dist/main/index.html',
|
||||
'dist/main/preload.js',
|
||||
'dist/main/qrcode.min.js',
|
||||
'dist/main/renderer.js',
|
||||
'dist/main/style.css',
|
||||
'dist/main/video-js.min.css',
|
||||
'dist/main/video.min.js',
|
||||
|
||||
'dist/player/index.html',
|
||||
'dist/player/preload.js',
|
||||
'dist/player/renderer.js',
|
||||
'dist/player/style.css',
|
||||
'dist/player/video-js.min.css',
|
||||
'dist/player/video.min.js',
|
||||
|
||||
'dist/app.ico',
|
||||
'dist/app.png',
|
||||
'dist/bundle.js',
|
||||
'package.json'
|
||||
];
|
||||
}
|
||||
|
||||
private async fetchJSON(url: string): Promise<any> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static async fetchJSON(url: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(url, (res) => {
|
||||
let data = '';
|
||||
|
@ -56,7 +65,7 @@ export class Updater {
|
|||
});
|
||||
}
|
||||
|
||||
private async downloadFile(url: string, destination: string): Promise<void> {
|
||||
private static async downloadFile(url: string, destination: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(destination);
|
||||
https.get(url, (response) => {
|
||||
|
@ -72,40 +81,151 @@ export class Updater {
|
|||
});
|
||||
}
|
||||
|
||||
private compareVersions(v1: string, v2: string): number {
|
||||
const v1Parts = v1.split('.').map(Number);
|
||||
const v2Parts = v2.split('.').map(Number);
|
||||
private static getDownloadFile(version: string) {
|
||||
let target: string = process.platform; // linux
|
||||
|
||||
for (let i = 0; i < v1Parts.length; i++) {
|
||||
if (v1Parts[i] > v2Parts[i]) {
|
||||
return 1;
|
||||
} else if (v1Parts[i] < v2Parts[i]) {
|
||||
return -1;
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
target = 'windows';
|
||||
}
|
||||
else if (process.platform === 'darwin') {
|
||||
target = 'macOS';
|
||||
}
|
||||
|
||||
return 0;
|
||||
return `fcast-receiver-${version}-${target}-${process.arch}.zip`;
|
||||
}
|
||||
|
||||
public async update(): Promise<Boolean> {
|
||||
console.log("Updater invoked", { baseUrl: this.baseUrl, basePath: this.basePath });
|
||||
public static isUpdating() {
|
||||
return fs.existsSync(Updater.updateMetadataPath);
|
||||
}
|
||||
|
||||
const localPackage = JSON.parse(fs.readFileSync(path.join(this.basePath, './package.json'), 'utf-8'));
|
||||
const remotePackage = await this.fetchJSON(`${this.baseUrl}/package.json`.toString());
|
||||
public static async processUpdate(): Promise<void> {
|
||||
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf-8'));
|
||||
const extractionDir = process.platform === 'darwin' ? 'FCast Receiver.app' : `fcast-receiver-${process.platform}-${process.arch}`;
|
||||
|
||||
console.log('Update check', { localVersion: localPackage.version, remoteVersion: remotePackage.version });
|
||||
if (this.compareVersions(remotePackage.version, localPackage.version) === 1) {
|
||||
for (const file of this.appFiles) {
|
||||
const fileUrl = `${this.baseUrl}/${file}`;
|
||||
const destination = path.join(this.basePath, file);
|
||||
switch (updateInfo.updateState) {
|
||||
case UpdateState.Copy: {
|
||||
const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver';
|
||||
|
||||
console.log(`Downloading '${fileUrl}' to '${destination}'.`);
|
||||
await this.downloadFile(fileUrl.toString(), destination);
|
||||
if (Updater.installPath === updateInfo.installPath) {
|
||||
logger.info('Update in progress. Restarting application to perform update...')
|
||||
const updateBinPath = process.platform === 'darwin' ? path.join(updateInfo.tempPath, extractionDir) : path.join(updateInfo.tempPath, extractionDir, binaryName);
|
||||
|
||||
log4js.shutdown();
|
||||
app.relaunch({ execPath: updateBinPath });
|
||||
app.exit();
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Updater process started...');
|
||||
const src = path.join(updateInfo.tempPath, extractionDir);
|
||||
logger.info(`Copying files from update directory ${src} to install directory ${updateInfo.installPath}`);
|
||||
|
||||
// Electron runtime sees .asar file as directory and causes errors during copy
|
||||
process.noAsar = true
|
||||
fs.cpSync(src, updateInfo.installPath, { recursive: true, force: true });
|
||||
process.noAsar = false
|
||||
|
||||
updateInfo.updateState = UpdateState.Cleanup;
|
||||
await fs.promises.writeFile(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
|
||||
const installBinPath = path.join(updateInfo.installPath, binaryName);
|
||||
log4js.shutdown();
|
||||
app.relaunch({ execPath: installBinPath });
|
||||
app.exit();
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('Error while applying update...');
|
||||
logger.error(err);
|
||||
log4js.shutdown();
|
||||
app.exit();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case UpdateState.Cleanup: {
|
||||
try {
|
||||
logger.info('Performing update cleanup...')
|
||||
// Electron runtime sees .asar file as directory and causes errors during copy
|
||||
process.noAsar = true
|
||||
fs.rmSync(path.join(Updater.updateDataPath, extractionDir), { recursive: true, force: true });
|
||||
process.noAsar = false
|
||||
|
||||
fs.rmSync(path.join(Updater.updateDataPath, Updater.getDownloadFile(updateInfo.currentVersion)));
|
||||
fs.rmSync(Updater.updateMetadataPath);
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('Error while performing update cleanup...');
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
log4js.shutdown();
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async update(): Promise<boolean> {
|
||||
logger.info('Updater invoked');
|
||||
if (!fs.existsSync(Updater.updateDataPath)) {
|
||||
fs.mkdirSync(Updater.updateDataPath);
|
||||
}
|
||||
|
||||
const localPackage = JSON.parse(fs.readFileSync(path.join(Updater.appPath, './package.json'), 'utf-8'));
|
||||
const releases = await Updater.fetchJSON(`${Updater.baseUrl}/releases.json`.toString()) as ReleaseInfo;
|
||||
|
||||
let updaterSettings = Store.get('updater');
|
||||
if (updaterSettings === null) {
|
||||
updaterSettings = {
|
||||
'channel': localPackage.channel,
|
||||
}
|
||||
|
||||
Store.set('updater', updaterSettings);
|
||||
}
|
||||
|
||||
const localChannelVersion: number = localPackage.channelVersion ? localPackage.channelVersion : 0
|
||||
const currentChannelVersion: number = releases.channelCurrentVersions[localPackage.channel] ? releases.channelCurrentVersions[localPackage.channel] : 0
|
||||
logger.info('Update check', { channel: localPackage.channel, localVersion: localPackage.version, currentVersion: releases.currentVersion });
|
||||
|
||||
if (localPackage.version !== releases.currentVersion || (localPackage.channel !== 'stable' && localChannelVersion < currentChannelVersion)) {
|
||||
const channel = localPackage.version !== releases.currentVersion ? 'stable' : localPackage.channel;
|
||||
const file = Updater.getDownloadFile(releases.currentVersion);
|
||||
const fileInfo = releases.currentReleases[channel][process.platform][process.arch]
|
||||
|
||||
const destination = path.join(Updater.updateDataPath, file);
|
||||
logger.info(`Downloading '${fileInfo.url}' to '${destination}'.`);
|
||||
await Updater.downloadFile(fileInfo.url.toString(), destination);
|
||||
|
||||
const downloadedFile = await fs.promises.readFile(destination);
|
||||
const hash = crypto.createHash('sha256').end(downloadedFile).digest('hex');
|
||||
if (fileInfo.sha256Digest !== hash) {
|
||||
const message = 'Update failed integrity check. Please try checking for updates again or downloading the update manually.';
|
||||
logger.error(`Update failed integrity check. Expected hash: ${fileInfo.sha256Digest}, actual hash: ${hash}`);
|
||||
throw message;
|
||||
}
|
||||
|
||||
// Electron runtime sees .asar file as directory and causes errors during extraction
|
||||
logger.info('Extracting update...');
|
||||
process.noAsar = true
|
||||
await extract(destination, { dir: path.dirname(destination) });
|
||||
process.noAsar = false
|
||||
|
||||
logger.info('Extraction complete.');
|
||||
const updateInfo: UpdateInfo = {
|
||||
updateState: UpdateState.Copy,
|
||||
installPath: Updater.installPath,
|
||||
tempPath: path.dirname(destination),
|
||||
currentVersion: releases.currentVersion,
|
||||
};
|
||||
|
||||
await fs.promises.writeFile(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
logger.info('Written update metadata.');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue