Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src-node/download-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const { pipeline } = require('stream/promises');
const { Transform } = require('stream');
const fs = require('fs');
const path = require('path');

const args = process.argv.slice(2); // Skip the first two elements
const {downloadURL, appdataDir} = JSON.parse(args[0]);
console.log("Download URL is: ", downloadURL);
console.log("AppdataDir is: ", appdataDir);

const EVENT_PROGRESS= "progress:";
const EVENT_INSTALL_PATH= "InstallerPath:";

const fileName = path.basename(new URL(downloadURL).pathname);
const installerFolder = path.join(appdataDir, 'installer');
const savePath = path.join(appdataDir, 'installer', fileName);

async function getFileSize(url) {
try {
const response = await fetch(url, { method: 'HEAD' });
if (!response.ok) {
return 0;
}

// Follow redirects by recursively calling getFileSize with the new location
if (response.status >= 300 && response.status < 400 && response.headers.get('location')) {
console.log(`Redirecting to ${response.headers.get('location')}`);
return getFileSize(response.headers.get('location'));
}

const contentLength = parseInt(response.headers.get('content-length'));

if (contentLength) {
console.log(`File size: ${contentLength} bytes`);
return contentLength;
} else {
console.error('Content-Length header is missing');
return 0;
}
} catch (error) {
console.error(`An error occurred: ${error.message}`);
return 0;
}
}

let previousSentProgress = -1;
async function downloadFile(url, outputPath) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}

const totalBytes = response.headers.get('content-length');
let downloadedBytes = 0;

// Create a Transform stream to monitor download progress
const progressStream = new Transform({
transform(chunk, encoding, callback) {
downloadedBytes += chunk.length;
const percent = Math.floor(((downloadedBytes / totalBytes) * 100));
const totalSize = Math.floor(totalBytes/1024/1024);
if(previousSentProgress !== percent){
previousSentProgress = percent;
console.log(`${EVENT_PROGRESS}${percent}:${totalSize}`);
}
callback(null, chunk);
}
});

const destinationStream = fs.createWriteStream(outputPath);

await pipeline(response.body, progressStream, destinationStream);
console.log(`File has been downloaded and saved to ${outputPath}`);
}

async function downloadFileIfNeeded() {
try {

// Ensure the installer directory exists
fs.mkdirSync(installerFolder, { recursive: true });

const fileStats = fs.existsSync(savePath) ? fs.statSync(savePath) : null;
console.log("Existing file stats:", fileStats);

const totalBytes = await getFileSize(downloadURL);
console.log("Existing and new file size:", fileStats && fileStats.size, totalBytes);
if(fileStats && totalBytes && fileStats.size === totalBytes) {
console.log('File already downloaded and complete.');
const totalSize = Math.floor(totalBytes/1024/1024);
console.log(`${EVENT_PROGRESS}${100}:${totalSize}`);
return;
}

// if we are here, then it is a fresh installer download or there is a partial corrupt download or
// a new version installer has to be downloaded while the old outdated installer exists.
// we have to clean the installerFolder.
await fs.promises.rm(installerFolder, { recursive: true, force: true });
fs.mkdirSync(installerFolder, { recursive: true });
console.log('Downloading installer...');
await downloadFile(downloadURL, savePath);

} catch (error) {
console.error('An error occurred:', error);
}
}

downloadFileIfNeeded()
.then(()=>{
console.log(`${EVENT_INSTALL_PATH}${savePath}`); // do not change this name
})
.catch(()=>process.exit(1));
8 changes: 8 additions & 0 deletions src/document/DocumentCommandHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2209,4 +2209,12 @@ define(function (require, exports, module) {

// Reset the untitled document counter before changing projects
ProjectManager.on("beforeProjectClose", function () { _nextUntitledIndexToUse = 1; });

let windowsUpdateInstallerPlatformLocation;
function setWindowsUpdateInstallerLocation(location) {
windowsUpdateInstallerPlatformLocation = location;
}

// private api
exports._setWindowsUpdateInstallerLocation = setWindowsUpdateInstallerLocation;
});
47 changes: 39 additions & 8 deletions src/extensionsIntegrated/appUpdater/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ define(function (require, exports, module) {
marked = require('thirdparty/marked.min'),
semver = require("thirdparty/semver.browser"),
TaskManager = require("features/TaskManager"),
StringUtils = require("utils/StringUtils"),
NativeApp = require("utils/NativeApp"),
DocumentCommandHandlers = require("document/DocumentCommandHandlers"),
PreferencesManager = require("preferences/PreferencesManager");
let updaterWindow, updateTask, updatePendingRestart, updateFailed;

Expand Down Expand Up @@ -80,15 +82,18 @@ define(function (require, exports, module) {
});
}

function createTauriUpdateWindow() {
function createTauriUpdateWindow(downloadURL) {
if(updaterWindow){
return;
}
Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'window', "create"+Phoenix.platform);
// as we are a single instance app, and there can be multiple phoenix windows that comes in and goes out,
// the updater lives in its own independent hidden window.
const url = downloadURL ?
`tauri-updater.html?stage=${Phoenix.config.environment}&downloadURL=${encodeURIComponent(downloadURL)}` :
`tauri-updater.html?stage=${Phoenix.config.environment}`;
updaterWindow = new window.__TAURI__.window.WebviewWindow(TAURI_UPDATER_WINDOW_LABEL, {
url: "tauri-updater.html?stage=" + Phoenix.config.environment,
url: url,
title: "Desktop App Updater",
fullscreen: false,
resizable: false,
Expand All @@ -104,8 +109,8 @@ define(function (require, exports, module) {
}
}

async function doUpdate() {
createTauriUpdateWindow();
async function doUpdate(downloadURL) {
createTauriUpdateWindow(downloadURL);
showOrHideUpdateIcon();
}

Expand Down Expand Up @@ -223,7 +228,7 @@ define(function (require, exports, module) {
return;
}
if(option === Dialogs.DIALOG_BTN_OK && !updaterWindow){
doUpdate();
doUpdate(updateDetails.downloadURL);
return;
}
Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'dialog', "cancel"+Phoenix.platform);
Expand All @@ -232,14 +237,20 @@ define(function (require, exports, module) {
}

const UPDATE_COMMANDS = {
GET_STATUS: "GET_STATUS"
GET_STATUS: "GET_STATUS",
GET_DOWNLOAD_PROGRESS: "GET_DOWNLOAD_PROGRESS",
GET_INSTALLER_LOCATION: "GET_INSTALLER_LOCATION"
};
const UPDATE_EVENT = {
STATUS: "STATUS",
LOG_ERROR: "LOG_ERROR"
LOG_ERROR: "LOG_ERROR",
DOWNLOAD_PROGRESS: "DOWNLOAD_PROGRESS",
INSTALLER_LOCATION: "INSTALLER_LOCATION"
};
const UPDATE_STATUS = {
STARTED: "STARTED",
DOWNLOADING: "DOWNLOADING",
INSTALLER_DOWNLOADED: "INSTALLER_DOWNLOADED",
FAILED: "FAILED",
FAILED_UNKNOWN_OS: "FAILED_UNKNOWN_OS",
INSTALLED: "INSTALLED"
Expand Down Expand Up @@ -285,9 +296,29 @@ define(function (require, exports, module) {
updateTask.setTitle(Strings.UPDATE_DONE);
updateTask.setMessage(Strings.UPDATE_RESTART);
Dialogs.showInfoDialog(Strings.UPDATE_READY_RESTART_TITLE, Strings.UPDATE_READY_RESTART_MESSAGE);
} else if(data === UPDATE_STATUS.INSTALLER_DOWNLOADED && !updateInstalledDialogShown){
updateInstalledDialogShown = true;
Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'downloaded', Phoenix.platform);
updatePendingRestart = true;
updateTask.setSucceded();
updateTask.setTitle(Strings.UPDATE_DONE);
updateTask.setMessage(Strings.UPDATE_RESTART_INSTALL);
Dialogs.showInfoDialog(Strings.UPDATE_READY_RESTART_TITLE, Strings.UPDATE_READY_RESTART_INSTALL_MESSAGE);
_sendUpdateCommand(UPDATE_COMMANDS.GET_INSTALLER_LOCATION);
} else if(data === UPDATE_STATUS.DOWNLOADING){
updateTask.setMessage(Strings.UPDATE_DOWNLOADING);
_sendUpdateCommand(UPDATE_COMMANDS.GET_DOWNLOAD_PROGRESS);
}
showOrHideUpdateIcon();
} if(eventName === UPDATE_EVENT.LOG_ERROR) {
} else if(eventName === UPDATE_EVENT.DOWNLOAD_PROGRESS) {
const {progressPercent, fileSize} = data;
updateTask.setProgressPercent(progressPercent);
updateTask.setMessage(StringUtils.format(Strings.UPDATE_DOWNLOAD_PROGRESS,
Math.floor(fileSize*progressPercent/100),
fileSize));
} else if(eventName === UPDATE_EVENT.INSTALLER_LOCATION) {
DocumentCommandHandlers._setWindowsUpdateInstallerLocation(data);
} else if(eventName === UPDATE_EVENT.LOG_ERROR) {
logger.reportErrorMessage(data);
}
});
Expand Down
8 changes: 6 additions & 2 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -569,15 +569,19 @@ define({
"UPDATE_NOT_AVAILABLE_TITLE": "No Updates Available",
"UPDATE_AVAILABLE_TITLE": "Update Available",
"UPDATE_READY_RESTART_TITLE": "Update Ready: Restart Required",
"UPDATE_READY_RESTART_MESSAGE": "Close all {APP_NAME} windows and restart the app to launch the updated version.",
"UPDATE_READY_RESTART_MESSAGE": "Close all {APP_NAME} app windows and restart the app to launch the updated version.",
"UPDATE_READY_RESTART_INSTALL_MESSAGE": "Update Successfully Downloaded: Please close all {APP_NAME} app windows to apply the latest updates.",
"UPDATE_FAILED_TITLE": "Update Failed",
"UPDATE_FAILED_MESSAGE": "Please close all {APP_NAME} windows and reopen the application to attempt the update again.",
"UPDATE_FAILED_MESSAGE": "Please close all {APP_NAME} app windows and reopen the application to attempt the update again.",
"UPDATE_UP_TO_DATE": "{APP_NAME} is up to date.",
"UPDATE_MESSAGE": "Hey, there's a new build of {APP_NAME} available. Here are some of the new features:",
"GET_IT_NOW": "Get it now!",
"UPDATE_LATER": "Remind Me Later",
"UPDATE_DONE": "{APP_NAME} Updated",
"UPDATE_RESTART": "Restart to apply updates",
"UPDATE_RESTART_INSTALL": "Restart to install updates",
"UPDATE_DOWNLOADING": "Downloading Installer",
"UPDATE_DOWNLOAD_PROGRESS": "Downloading- {0} of {1} MB",
"UPDATING_APP": "Updating {APP_NAME}",
"UPDATING_APP_MESSAGE": "This may take a while",
"UPDATING_APP_DIALOG_MESSAGE": "Update in progress. You can continue using {APP_NAME} while we upgrade.",
Expand Down
2 changes: 1 addition & 1 deletion src/node-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ function nodeLoader() {
if(Phoenix.platform === "linux") {
// in linux installed distributions, src-node is present in the same dir as the executable.
const cliArgs = await window.__TAURI__.invoke('_get_commandline_args');
nodeSrcPath = `${window.path.dirname(cliArgs[0])}/src-node`;
nodeSrcPath = `${window.path.dirname(cliArgs[0])}/src-node/index.js`;
}
// node is designed such that it is not required at boot time to lower startup time.
// Keep this so to increase boot speed.
Expand Down
59 changes: 56 additions & 3 deletions src/tauri-updater.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@
<title>Tauri Updater Hidden Window</title>
<script>
const UPDATE_COMMANDS = {
GET_STATUS: "GET_STATUS"
GET_STATUS: "GET_STATUS",
GET_DOWNLOAD_PROGRESS: "GET_DOWNLOAD_PROGRESS",
GET_INSTALLER_LOCATION: "GET_INSTALLER_LOCATION"
};
const UPDATE_EVENT = {
STATUS: "STATUS",
LOG_ERROR: "LOG_ERROR"
LOG_ERROR: "LOG_ERROR",
DOWNLOAD_PROGRESS: "DOWNLOAD_PROGRESS",
INSTALLER_LOCATION: "INSTALLER_LOCATION"
};
const UPDATE_STATUS = {
STARTED: "STARTED",
DOWNLOADING: "DOWNLOADING",
INSTALLER_DOWNLOADED: "INSTALLER_DOWNLOADED",
FAILED: "FAILED",
FAILED_UNKNOWN_OS: "FAILED_UNKNOWN_OS",
INSTALLED: "INSTALLED"
};
let status = UPDATE_STATUS.STARTED;
let progressPercent = 0, fileSize = 0, installerLocation;
window.__TAURI__.event.listen("updateCommands", (receivedEvent)=> {
console.log("Updater received Event updateCommands", receivedEvent);
const {command, data} = receivedEvent.payload;
if(command === UPDATE_COMMANDS.GET_STATUS){
sendUpdateEvent(UPDATE_EVENT.STATUS, status);
} else if(command === UPDATE_COMMANDS.GET_DOWNLOAD_PROGRESS){
sendUpdateEvent(UPDATE_EVENT.DOWNLOAD_PROGRESS, {progressPercent, fileSize});
} else if(command === UPDATE_COMMANDS.GET_INSTALLER_LOCATION){
sendUpdateEvent(UPDATE_EVENT.INSTALLER_LOCATION, installerLocation);
}
});
function sendUpdateEvent(eventName, data) {
Expand Down Expand Up @@ -76,7 +87,49 @@
}

async function updateWindows() {

const EVENT_PROGRESS= "progress:";
const EVENT_INSTALL_PATH= "InstallerPath:";
const downloadURL = decodeURIComponent(getQueryStringParam('downloadURL'));
const appdataDir = await window.__TAURI__.path.appLocalDataDir();
console.log('InstallerDownloadURL:', downloadURL);
const argJson = JSON.stringify({downloadURL, appdataDir});
window.__TAURI__.path.resolveResource("src-node/download-file.js")
.then(async nodeSrcPath=>{
// this is not supposed to work in linux.
const argsArray = [nodeSrcPath, argJson];
const command = window.__TAURI__.shell.Command.sidecar('phnode', argsArray);
command.on('close', data => {
console.log(`PhNode: command finished with code ${data.code} and signal ${data.signal}`);
if(data.code !== 0) {
status = UPDATE_STATUS.FAILED;
sendUpdateEvent(UPDATE_EVENT.LOG_ERROR, "Download Error: " + downloadURL);
sendUpdateEvent(UPDATE_EVENT.STATUS, UPDATE_STATUS.FAILED);
return;
}
status = UPDATE_STATUS.INSTALLER_DOWNLOADED;
sendUpdateEvent(UPDATE_EVENT.STATUS, UPDATE_STATUS.INSTALLER_DOWNLOADED);
});
command.on('error', error => {
console.error(`PhNode: command error: "${error}"`);
status = UPDATE_STATUS.FAILED;
sendUpdateEvent(UPDATE_EVENT.LOG_ERROR, "Download Error: " + downloadURL);
sendUpdateEvent(UPDATE_EVENT.STATUS, UPDATE_STATUS.FAILED);
});
command.stdout.on('data', line => {
console.log(`PhNode: ${line}`);
if(line.startsWith(EVENT_PROGRESS)){
progressPercent = parseInt(line.split(":")[1].trim());
fileSize = parseInt(line.split(":")[2].trim());
sendUpdateEvent(UPDATE_EVENT.DOWNLOAD_PROGRESS, {progressPercent, fileSize});
} else if(line.startsWith(EVENT_INSTALL_PATH)){
installerLocation = line.split(":")[1].trim();
console.log("Final installer location is: ", installerLocation);
sendUpdateEvent(UPDATE_EVENT.INSTALLER_LOCATION, installerLocation);
}
});
command.stderr.on('data', line => console.error(`PhNode: ${line}`));
command.spawn();
});
}

async function update() {
Expand Down