plugins: update deletes old files (preserving storage folder)

Massimo Melina committed Jun 2, 2024 at 11:18 UTC f67f2e5a2675eaa2fc171c1132c91539579c60dd
2 files changed +25 -13
src/api.plugins.ts
-4
@@ -143,11 +143,7 @@ const apis: ApiHandlers = {
143 if (!online)
144 return new ApiError(HTTP_CONFLICT)
145 await checkDependencies(online)
146 - const enabled = isPluginEnabled(id)
147 - await stopPlugin(id)
146 await downloadPlugin(found.repo, { branch: online.branch, overwrite: true })
149 - if (enabled)
150 - void startPlugin(id) // don't wait, in case it fails to start
147 return {}
148 },
149
src/github.ts
+25 -9
@@ -1,9 +1,11 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import events from './events'
4 -import { DAY, httpString, httpStream, unzip, AsapStream, debounceAsync, asyncGeneratorToArray } from './misc'
5 -import { DISABLING_SUFFIX, findPluginByRepo, getAvailablePlugins, getPluginInfo, mapPlugins,
6 - parsePluginSource, PATH as PLUGINS_PATH, Repo } from './plugins'
4 +import { DAY, httpString, httpStream, unzip, AsapStream, debounceAsync, asyncGeneratorToArray, wait } from './misc'
5 +import {
6 + DISABLING_SUFFIX, findPluginByRepo, getAvailablePlugins, getPluginInfo, isPluginEnabled, mapPlugins,
7 + parsePluginSource, PATH as PLUGINS_PATH, Repo, startPlugin, stopPlugin, STORAGE_FOLDER
8 +} from './plugins'
9 import { ApiError } from './apiMiddleware'
10 import _ from 'lodash'
11 import { DEV, HFS_REPO, HFS_REPO_BRANCH, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_NOT_ACCEPTABLE,
@@ -66,6 +68,7 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
68
69 async function go(url: string, folder: string, zipRoot: string) {
70 const installPath = PLUGINS_PATH + '/' + folder
71 + const tempInstallPath = installPath + '--' + DISABLING_SUFFIX
72 const foldersToCopy = [ // from longer to shorter, so we first test the longer
73 zipRoot + '-' + process.platform + '-' + process.arch,
74 zipRoot + '-' + process.platform,
@@ -73,19 +76,32 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
76 ].map(x => x + '/')
77 // github zip doesn't have content-length, so we cannot produce progress event
78 const stream = await httpStream(url)
76 - const MAIN = 'plugin.js'
79 await unzip(stream, async path => {
80 const folder = foldersToCopy.find(x => path.startsWith(x))
81 if (!folder || path.endsWith('/')) return false
82 let dest = path.slice(folder.length)
81 - if (dest === MAIN) // avoid being possibly loaded before the download is complete
82 - dest += DISABLING_SUFFIX
83 - dest = join(installPath, dest)
83 + dest = join(tempInstallPath, dest)
84 return rm(dest, { force: true }).then(() => dest, () => false)
85 })
86 - const main = join(installPath, MAIN)
87 - await rename(main + DISABLING_SUFFIX, main) // we are good now, restore name
86 + // ready to replace
87 + const wasEnabled = isPluginEnabled(folder)
88 + if (wasEnabled)
89 + await stopPlugin(folder) // stop old
90 + let retry = 3
91 + while (retry--) { // move data, and consider late release of the resource, up to a few seconds
92 + const res = rename(join(installPath, STORAGE_FOLDER), join(tempInstallPath, STORAGE_FOLDER))
93 + if (await res.then(() => true, e => e.code === 'ENOENT')) break
94 + await wait(1000)
95 + }
96 + // delete old
97 + await rm(installPath, { recursive: true }).catch(e => {
98 + if (e.code !== 'ENOENT') throw e
99 + })
100 + // final replace
101 + await rename(tempInstallPath, installPath)
102 .catch(e => { throw e.code !== 'ENOENT' ? e : new ApiError(HTTP_NOT_ACCEPTABLE, "missing main file") })
103 + if (wasEnabled)
104 + void startPlugin(folder) // don't wait, in case it fails to start
105 return folder
106 }
107 }