1 |
rakinar2 |
577 |
/** |
2 |
|
|
* This file is part of SudoBot. |
3 |
|
|
* |
4 |
|
|
* Copyright (C) 2021-2023 OSN Developers. |
5 |
|
|
* |
6 |
|
|
* SudoBot is free software; you can redistribute it and/or modify it |
7 |
|
|
* under the terms of the GNU Affero General Public License as published by |
8 |
|
|
* the Free Software Foundation, either version 3 of the License, or |
9 |
|
|
* (at your option) any later version. |
10 |
|
|
* |
11 |
|
|
* SudoBot is distributed in the hope that it will be useful, but |
12 |
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of |
13 |
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 |
|
|
* GNU Affero General Public License for more details. |
15 |
|
|
* |
16 |
|
|
* You should have received a copy of the GNU Affero General Public License |
17 |
|
|
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>. |
18 |
|
|
*/ |
19 |
|
|
|
20 |
|
|
import axios from "axios"; |
21 |
|
|
import { spawnSync } from "child_process"; |
22 |
|
|
import { |
23 |
|
|
ActionRowBuilder, |
24 |
|
|
ButtonBuilder, |
25 |
|
|
ButtonInteraction, |
26 |
|
|
ButtonStyle, |
27 |
|
|
Colors, |
28 |
|
|
ComponentType, |
29 |
|
|
StringSelectMenuBuilder, |
30 |
|
|
StringSelectMenuInteraction, |
31 |
|
|
StringSelectMenuOptionBuilder |
32 |
|
|
} from "discord.js"; |
33 |
|
|
import { existsSync } from "fs"; |
34 |
|
|
import { cp, mkdir, rename, rm } from "fs/promises"; |
35 |
|
|
import path, { basename, join } from "path"; |
36 |
|
|
import semver from "semver"; |
37 |
|
|
import Command, { AnyCommandContext, CommandMessage, CommandReturn, ValidationRule } from "../../core/Command"; |
38 |
|
|
import { downloadFile } from "../../utils/download"; |
39 |
|
|
import { log, logError, logInfo, logWarn } from "../../utils/logger"; |
40 |
|
|
import { developmentMode, sudoPrefix } from "../../utils/utils"; |
41 |
|
|
|
42 |
|
|
export default class UpdateCommand extends Command { |
43 |
|
|
public readonly name = "update"; |
44 |
|
|
public readonly validationRules: ValidationRule[] = []; |
45 |
|
|
public readonly permissions = []; |
46 |
|
|
public readonly description = "Updates the bot to the latest version."; |
47 |
|
|
public readonly systemAdminOnly = true; |
48 |
|
|
protected readonly RELEASE_API_URL = "https://api.github.com/repos/onesoft-sudo/sudobot/releases/latest"; |
49 |
|
|
protected readonly UNSTABLE_DOWNLOAD_URL = "https://github.com/onesoft-sudo/sudobot/archive/refs/heads/main.zip"; |
50 |
|
|
public updateChannel?: "stable" | "unstable"; |
51 |
|
|
public readonly beta = true; |
52 |
|
|
|
53 |
|
|
async execute(message: CommandMessage, context: AnyCommandContext): Promise<CommandReturn> { |
54 |
|
|
const unsatisfiedRequirement = this.checkRequirements(); |
55 |
|
|
|
56 |
|
|
if (unsatisfiedRequirement) { |
57 |
|
|
await this.error( |
58 |
|
|
message, |
59 |
|
|
`The \`${unsatisfiedRequirement}\` program is not installed in the current system. Please install it if you want to use this command.` |
60 |
|
|
); |
61 |
|
|
return; |
62 |
|
|
} |
63 |
|
|
|
64 |
|
|
await this.deferIfInteraction(message); |
65 |
|
|
|
66 |
|
|
try { |
67 |
|
|
const response = await axios.get(this.RELEASE_API_URL); |
68 |
|
|
const tagName = response.data?.tag_name; |
69 |
|
|
const version = tagName.replace(/^v/, ""); |
70 |
|
|
const stableDownloadURL = `https://github.com/onesoft-sudo/sudobot/archive/refs/tags/${tagName}.zip`; |
71 |
|
|
const updateAvailable = semver.gt(version, this.client.metadata.data.version); |
72 |
|
|
this.updateChannel = updateAvailable ? "stable" : "unstable"; |
73 |
|
|
|
74 |
|
|
await this.deferredReply(message, { |
75 |
|
|
embeds: [ |
76 |
|
|
{ |
77 |
|
|
author: { |
78 |
|
|
name: "System Update", |
79 |
|
|
icon_url: this.client.user?.displayAvatarURL() ?? undefined |
80 |
|
|
}, |
81 |
|
|
description: |
82 |
|
|
"Are you sure you want to continue? This will download an update, install it, push schema changes to the database, and then restart the bot system. Make sure you have a backup in case if a data loss occurs.", |
83 |
|
|
color: 0x007bff |
84 |
|
|
} |
85 |
|
|
], |
86 |
|
|
components: this.actionRow({ updateAvailable, version }) |
87 |
|
|
}); |
88 |
|
|
|
89 |
|
|
const updateChannelCollector = message.channel!.createMessageComponentCollector({ |
90 |
|
|
componentType: ComponentType.StringSelect, |
91 |
|
|
filter: (interaction: StringSelectMenuInteraction) => { |
92 |
|
|
if (interaction.user.id === message.member!.user.id && interaction.customId === "system_update_channel") { |
93 |
|
|
return true; |
94 |
|
|
} |
95 |
|
|
|
96 |
|
|
interaction |
97 |
|
|
.reply({ |
98 |
|
|
ephemeral: true, |
99 |
|
|
content: `That's not under your control.` |
100 |
|
|
}) |
101 |
|
|
.catch(logError); |
102 |
|
|
|
103 |
|
|
return false; |
104 |
|
|
}, |
105 |
|
|
time: 120_000 |
106 |
|
|
}); |
107 |
|
|
|
108 |
|
|
const confirmationCollector = message.channel!.createMessageComponentCollector({ |
109 |
|
|
componentType: ComponentType.Button, |
110 |
|
|
filter: (interaction: ButtonInteraction) => { |
111 |
|
|
if (interaction.user.id === message.member!.user.id && interaction.customId.startsWith(`system_update__`)) { |
112 |
|
|
return true; |
113 |
|
|
} |
114 |
|
|
|
115 |
|
|
interaction |
116 |
|
|
.reply({ |
117 |
|
|
ephemeral: true, |
118 |
|
|
content: `That's not under your control.` |
119 |
|
|
}) |
120 |
|
|
.catch(logError); |
121 |
|
|
|
122 |
|
|
return false; |
123 |
|
|
}, |
124 |
|
|
time: 120_000 |
125 |
|
|
}); |
126 |
|
|
|
127 |
|
|
confirmationCollector.on("collect", async interaction => { |
128 |
|
|
if (!interaction.isButton()) { |
129 |
|
|
return; |
130 |
|
|
} |
131 |
|
|
|
132 |
|
|
if (interaction.customId === "system_update__cancel") { |
133 |
|
|
confirmationCollector.stop(); |
134 |
|
|
updateChannelCollector.stop(); |
135 |
|
|
|
136 |
|
|
await interaction.update({ |
137 |
|
|
embeds: [ |
138 |
|
|
{ |
139 |
|
|
author: { |
140 |
|
|
name: "System Update", |
141 |
|
|
icon_url: this.client.user?.displayAvatarURL() ?? undefined |
142 |
|
|
}, |
143 |
|
|
description: "Update cancelled.", |
144 |
|
|
color: 0xf14a60 |
145 |
|
|
} |
146 |
|
|
], |
147 |
|
|
components: this.actionRow({ updateAvailable, version, disabled: true }) |
148 |
|
|
}); |
149 |
|
|
|
150 |
|
|
return; |
151 |
|
|
} |
152 |
|
|
|
153 |
|
|
if (!this.updateChannel) { |
154 |
|
|
await interaction.reply({ |
155 |
|
|
content: "Please select an update channel first!" |
156 |
|
|
}); |
157 |
|
|
|
158 |
|
|
return; |
159 |
|
|
} |
160 |
|
|
|
161 |
|
|
await interaction.update({ |
162 |
|
|
embeds: [ |
163 |
|
|
{ |
164 |
|
|
author: { |
165 |
|
|
name: "System Update", |
166 |
|
|
icon_url: this.client.user?.displayAvatarURL() ?? undefined |
167 |
|
|
}, |
168 |
|
|
description: `${this.emoji("loading")} Update in progress...`, |
169 |
|
|
color: 0x007bff |
170 |
|
|
} |
171 |
|
|
], |
172 |
|
|
components: this.actionRow({ updateAvailable, version, disabled: true }) |
173 |
|
|
}); |
174 |
|
|
|
175 |
|
|
let success = false; |
176 |
|
|
|
177 |
|
|
try { |
178 |
|
|
success = await this.update({ |
179 |
|
|
stableDownloadURL, |
180 |
|
|
version |
181 |
|
|
}); |
182 |
|
|
} catch (e) {} |
183 |
|
|
|
184 |
|
|
await interaction.message.edit({ |
185 |
|
|
embeds: [ |
186 |
|
|
{ |
187 |
|
|
author: { |
188 |
|
|
name: "System Update", |
189 |
|
|
icon_url: this.client.user?.displayAvatarURL() ?? undefined |
190 |
|
|
}, |
191 |
|
|
description: success |
192 |
|
|
? `${this.emoji("check")} Successfully installed the update. Restarting now.` |
193 |
|
|
: `${this.emoji("error")} An error has occurred while performing the update.`, |
194 |
|
|
color: Colors.Green |
195 |
|
|
} |
196 |
|
|
] |
197 |
|
|
}); |
198 |
|
|
|
199 |
|
|
process.exit(this.client.configManager.systemConfig.restart_exit_code); |
200 |
|
|
}); |
201 |
|
|
|
202 |
|
|
updateChannelCollector.on("collect", async interaction => { |
203 |
|
|
if (!interaction.isStringSelectMenu()) { |
204 |
|
|
return; |
205 |
|
|
} |
206 |
|
|
|
207 |
|
|
const updateChannel = interaction.component.options[0].value; |
208 |
|
|
|
209 |
|
|
if (!["stable", "unstable"].includes(updateChannel)) { |
210 |
|
|
return; |
211 |
|
|
} |
212 |
|
|
|
213 |
|
|
this.updateChannel = updateChannel as (typeof this)["updateChannel"]; |
214 |
|
|
await interaction.deferUpdate(); |
215 |
|
|
}); |
216 |
|
|
} catch (e) { |
217 |
|
|
logError(e); |
218 |
|
|
await this.error(message, "An unknown error has occurred while trying to fetch information about the updates."); |
219 |
|
|
} |
220 |
|
|
} |
221 |
|
|
|
222 |
|
|
actionRow({ version, updateAvailable, disabled = false }: { version: string; updateAvailable: boolean; disabled?: boolean }) { |
223 |
|
|
return [ |
224 |
|
|
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents( |
225 |
|
|
new StringSelectMenuBuilder() |
226 |
|
|
.addOptions( |
227 |
|
|
new StringSelectMenuOptionBuilder({ |
228 |
|
|
label: "Latest Stable", |
229 |
|
|
description: `${version} • ${updateAvailable ? "Update available" : "Up to date"}`, |
230 |
|
|
value: "stable", |
231 |
|
|
default: updateAvailable |
232 |
|
|
}).setEmoji("⚙"), |
233 |
|
|
new StringSelectMenuOptionBuilder({ |
234 |
|
|
label: "Latest Unstable", |
235 |
|
|
description: `main • Unstable versions may break things unexpectedly`, |
236 |
|
|
value: "unstable", |
237 |
|
|
default: !updateAvailable |
238 |
|
|
}).setEmoji("⚒️") |
239 |
|
|
) |
240 |
|
|
.setCustomId("system_update_channel") |
241 |
|
|
.setMaxValues(1) |
242 |
|
|
.setMinValues(1) |
243 |
|
|
.setPlaceholder("Select an update channel") |
244 |
|
|
.setDisabled(disabled) |
245 |
|
|
), |
246 |
|
|
new ActionRowBuilder<ButtonBuilder>().addComponents( |
247 |
|
|
new ButtonBuilder() |
248 |
|
|
.setCustomId("system_update__cancel") |
249 |
|
|
.setLabel("Cancel") |
250 |
|
|
.setStyle(ButtonStyle.Danger) |
251 |
|
|
.setDisabled(disabled), |
252 |
|
|
new ButtonBuilder() |
253 |
|
|
.setCustomId("system_update__continue") |
254 |
|
|
.setLabel("Continue") |
255 |
|
|
.setStyle(ButtonStyle.Success) |
256 |
|
|
.setDisabled(disabled) |
257 |
|
|
) |
258 |
|
|
]; |
259 |
|
|
} |
260 |
|
|
|
261 |
|
|
downloadUpdate({ stableDownloadURL, version }: { stableDownloadURL: string; version: string }): Promise<{ |
262 |
|
|
filePath?: string; |
263 |
|
|
storagePath?: string; |
264 |
|
|
error?: Error; |
265 |
|
|
}> { |
266 |
|
|
const url = this.updateChannel === "stable" ? stableDownloadURL : this.UNSTABLE_DOWNLOAD_URL; |
267 |
|
|
const tmpdir = sudoPrefix("tmp", true); |
268 |
|
|
const dirname = `update-${this.updateChannel === "stable" ? version : "unstable"}`; |
269 |
|
|
|
270 |
|
|
try { |
271 |
|
|
return downloadFile({ |
272 |
|
|
url, |
273 |
|
|
path: tmpdir, |
274 |
|
|
name: `${dirname}.zip` |
275 |
|
|
}); |
276 |
|
|
} catch (error) { |
277 |
|
|
logError(error); |
278 |
|
|
logError("Failed to download the update package. Aborting"); |
279 |
|
|
return Promise.resolve({ error: error as Error }); |
280 |
|
|
} |
281 |
|
|
} |
282 |
|
|
|
283 |
|
|
checkRequirements() { |
284 |
|
|
const paths = process.env.PATH?.split(process.platform === "win32" ? ";" : ":") ?? []; |
285 |
|
|
|
286 |
|
|
for (const path of paths) { |
287 |
|
|
if (process.platform === "win32" && existsSync(join(path, "powershell.exe"))) { |
288 |
|
|
return null; |
289 |
|
|
} |
290 |
|
|
|
291 |
|
|
if (existsSync(join(path, "unzip"))) { |
292 |
|
|
return null; |
293 |
|
|
} |
294 |
|
|
} |
295 |
|
|
|
296 |
|
|
return process.platform === "win32" ? "powershell.exe" : "unzip"; |
297 |
|
|
} |
298 |
|
|
|
299 |
|
|
async unpackUpdate({ filePath, storagePath, version }: { version: string; filePath: string; storagePath: string }) { |
300 |
|
|
const dirname = `update-${this.updateChannel === "stable" ? version : "unstable"}`; |
301 |
|
|
const unpackedDirectory = join(storagePath!, dirname); |
302 |
|
|
|
303 |
|
|
try { |
304 |
|
|
const cwd = process.cwd(); |
305 |
|
|
|
306 |
|
|
if (existsSync(unpackedDirectory)) { |
307 |
|
|
await rm(unpackedDirectory, { recursive: true }); |
308 |
|
|
} |
309 |
|
|
|
310 |
|
|
await mkdir(unpackedDirectory); |
311 |
|
|
process.chdir(unpackedDirectory); |
312 |
|
|
|
313 |
|
|
const { status, error } = spawnSync( |
314 |
|
|
process.platform === "win32" |
315 |
|
|
? `powershell -command "Expand-Archive -Force '${filePath}' '${unpackedDirectory}'"` |
316 |
|
|
: `unzip ../${basename(filePath)}`, |
317 |
|
|
{ |
318 |
|
|
shell: true, |
319 |
|
|
encoding: "utf-8", |
320 |
|
|
stdio: "inherit", |
321 |
|
|
cwd: unpackedDirectory |
322 |
|
|
} |
323 |
|
|
); |
324 |
|
|
|
325 |
|
|
if (status !== 0 || error) { |
326 |
|
|
throw error; |
327 |
|
|
} |
328 |
|
|
|
329 |
|
|
process.chdir(cwd); |
330 |
|
|
return { unpackedDirectory }; |
331 |
|
|
} catch (error) { |
332 |
|
|
logError(error); |
333 |
|
|
logError("Failed to unpack the update package. Aborting."); |
334 |
|
|
return { error }; |
335 |
|
|
} |
336 |
|
|
} |
337 |
|
|
|
338 |
|
|
private createDirectoryBackupPair(name: string) { |
339 |
|
|
return [path.join(__dirname, "../../../", name), path.join(__dirname, `../../../.backup/${name}`)] as const; |
340 |
|
|
} |
341 |
|
|
|
342 |
|
|
async installUpdate({ unpackedDirectory, version }: { unpackedDirectory: string; version: string }) { |
343 |
|
|
const { error, dirpairs } = await this.backupCurrentSystem(["build", "src", "prisma", "scripts"]); |
344 |
|
|
|
345 |
|
|
if (error) { |
346 |
|
|
return false; |
347 |
|
|
} |
348 |
|
|
|
349 |
|
|
const installFiles = ["src", "prisma", "scripts", "package.json", "tsconfig.json"]; |
350 |
|
|
|
351 |
|
|
try { |
352 |
|
|
for (const installFile of installFiles) { |
353 |
|
|
const src = path.join( |
354 |
|
|
unpackedDirectory, |
355 |
|
|
`sudobot-${this.updateChannel === "stable" ? version : "main"}`, |
356 |
|
|
installFile |
357 |
|
|
); |
358 |
|
|
const dest = path.join(__dirname, "../../../", installFile); |
359 |
|
|
|
360 |
|
|
log(`Installing ${src} to ${dest}`); |
361 |
|
|
|
362 |
|
|
await rm(dest, { |
363 |
|
|
recursive: true |
364 |
|
|
}); |
365 |
|
|
|
366 |
|
|
await cp(src, dest, { |
367 |
|
|
recursive: true |
368 |
|
|
}); |
369 |
|
|
} |
370 |
|
|
} catch (error) { |
371 |
|
|
logError(error); |
372 |
|
|
logError("Failed to install the update package. Attempting to rollback the changes"); |
373 |
|
|
await this.rollbackUpdate(dirpairs!); |
374 |
|
|
return false; |
375 |
|
|
} |
376 |
|
|
|
377 |
|
|
return await this.buildNewInstallation(dirpairs!); |
378 |
|
|
} |
379 |
|
|
|
380 |
|
|
async backupCurrentSystem(dirsToBackup: string[]) { |
381 |
|
|
const dirpairs = dirsToBackup.map(dir => this.createDirectoryBackupPair(dir)); |
382 |
|
|
|
383 |
|
|
try { |
384 |
|
|
await this.createBackupDirectoryIfNeeded(); |
385 |
|
|
|
386 |
|
|
for (const [src, dest] of dirpairs) { |
387 |
|
|
log(`Copying ${src} to ${dest}`); |
388 |
|
|
|
389 |
|
|
await cp(src, dest, { |
390 |
|
|
recursive: true |
391 |
|
|
}); |
392 |
|
|
} |
393 |
|
|
} catch (error) { |
394 |
|
|
logError(error); |
395 |
|
|
logError("Failed to backup the current bot system. Attempting to revert changes"); |
396 |
|
|
await this.rollbackUpdate(dirpairs); |
397 |
|
|
return { error }; |
398 |
|
|
} |
399 |
|
|
|
400 |
|
|
return { dirpairs }; |
401 |
|
|
} |
402 |
|
|
|
403 |
|
|
async createBackupDirectoryIfNeeded() { |
404 |
|
|
const backupDir = path.join(__dirname, `../../../.backup`); |
405 |
|
|
|
406 |
|
|
if (!existsSync(backupDir)) { |
407 |
|
|
await mkdir(backupDir); |
408 |
|
|
} |
409 |
|
|
|
410 |
|
|
return backupDir; |
411 |
|
|
} |
412 |
|
|
|
413 |
|
|
async rollbackUpdate(dirpairs: Array<readonly [string, string]>) { |
414 |
|
|
try { |
415 |
|
|
const backupDir = await this.createBackupDirectoryIfNeeded(); |
416 |
|
|
|
417 |
|
|
for (const [src, dest] of dirpairs) { |
418 |
|
|
if (!existsSync(dest)) { |
419 |
|
|
log(`No backup found for ${src}`); |
420 |
|
|
continue; |
421 |
|
|
} |
422 |
|
|
|
423 |
|
|
log(`Restoring ${src} from ${dest}`); |
424 |
|
|
|
425 |
|
|
if (existsSync(src)) { |
426 |
|
|
log(`Saving current state of ${src}`); |
427 |
|
|
await rename(src, path.join(backupDir, `${basename(src)}.current`)).catch(log); |
428 |
|
|
} |
429 |
|
|
|
430 |
|
|
await rename(dest, src); |
431 |
|
|
} |
432 |
|
|
|
433 |
|
|
logInfo("Rolled back the update successfully"); |
434 |
|
|
} catch (error) { |
435 |
|
|
logError(error); |
436 |
|
|
logError("Error rolling back the update"); |
437 |
|
|
return false; |
438 |
|
|
} |
439 |
|
|
|
440 |
|
|
return true; |
441 |
|
|
} |
442 |
|
|
|
443 |
|
|
async cleanup({ unpackedDirectory, downloadedFile }: { unpackedDirectory: string; downloadedFile: string }) { |
444 |
|
|
await rm(unpackedDirectory, { recursive: true }); |
445 |
|
|
await rm(downloadedFile); |
446 |
|
|
} |
447 |
|
|
|
448 |
|
|
buildNewInstallation(dirpairs: Array<readonly [string, string]>) { |
449 |
|
|
const { status: buildStatus } = spawnSync(`npm run build`, { |
450 |
|
|
stdio: "inherit", |
451 |
|
|
cwd: path.join(__dirname, "../../.."), |
452 |
|
|
encoding: "utf-8", |
453 |
|
|
shell: true |
454 |
|
|
}); |
455 |
|
|
|
456 |
|
|
if (buildStatus !== 0) { |
457 |
|
|
logError("Failed to build the update. Rolling back"); |
458 |
|
|
return this.rollbackUpdate(dirpairs); |
459 |
|
|
} |
460 |
|
|
|
461 |
|
|
const { status: dbPushStatus } = spawnSync(`npx prisma db push`, { |
462 |
|
|
stdio: "inherit", |
463 |
|
|
cwd: path.join(__dirname, "../../.."), |
464 |
|
|
encoding: "utf-8", |
465 |
|
|
shell: true |
466 |
|
|
}); |
467 |
|
|
|
468 |
|
|
if (dbPushStatus !== 0) { |
469 |
|
|
logError("Failed to push database schema changes. Rolling back"); |
470 |
|
|
return this.rollbackUpdate(dirpairs); |
471 |
|
|
} |
472 |
|
|
|
473 |
|
|
const { status: slashCommandStatus } = spawnSync(`npm run deploy${developmentMode() ? " -- --guild" : ""}`, { |
474 |
|
|
stdio: "inherit", |
475 |
|
|
cwd: path.join(__dirname, "../../.."), |
476 |
|
|
encoding: "utf-8", |
477 |
|
|
shell: true |
478 |
|
|
}); |
479 |
|
|
|
480 |
|
|
if (slashCommandStatus !== 0) { |
481 |
|
|
logWarn("Failed to update application commands. Please manually update them."); |
482 |
|
|
} |
483 |
|
|
|
484 |
|
|
return true; |
485 |
|
|
} |
486 |
|
|
|
487 |
|
|
async update({ stableDownloadURL, version }: { stableDownloadURL: string; version: string }) { |
488 |
|
|
const updateChannel = this.updateChannel; |
489 |
|
|
|
490 |
|
|
if (!updateChannel) { |
491 |
|
|
return false; |
492 |
|
|
} |
493 |
|
|
|
494 |
|
|
const { error: downloadError, filePath, storagePath } = await this.downloadUpdate({ stableDownloadURL, version }); |
495 |
|
|
|
496 |
|
|
if (downloadError) { |
497 |
|
|
return false; |
498 |
|
|
} |
499 |
|
|
|
500 |
|
|
const { error: unpackError, unpackedDirectory } = await this.unpackUpdate({ |
501 |
|
|
filePath: filePath!, |
502 |
|
|
storagePath: storagePath!, |
503 |
|
|
version |
504 |
|
|
}); |
505 |
|
|
|
506 |
|
|
if (unpackError) { |
507 |
|
|
return false; |
508 |
|
|
} |
509 |
|
|
|
510 |
|
|
const successfullyInstalled = await this.installUpdate({ unpackedDirectory: unpackedDirectory!, version }); |
511 |
|
|
|
512 |
|
|
if (!successfullyInstalled) { |
513 |
|
|
return false; |
514 |
|
|
} |
515 |
|
|
|
516 |
|
|
await this.cleanup({ unpackedDirectory: unpackedDirectory!, downloadedFile: filePath! }).catch(log); |
517 |
|
|
return true; |
518 |
|
|
} |
519 |
|
|
} |