fix: admin/plugins/updates: "bad request" sometimes, resulting in plugin being lost

Massimo Melina committed Apr 9, 2025 at 22:54 UTC 7a521e216b160c8d1749439f3e44f0c10d17ecce
2 files changed +13 -6
src/github.ts
+5 -5
@@ -6,7 +6,7 @@ import {
6 } from './misc'
7 import {
8 DISABLING_SUFFIX, enablePlugin, findPluginByRepo, getAvailablePlugins, getPluginInfo, isPluginRunning, mapPlugins,
9 - parsePluginSource, PATH as PLUGINS_PATH, Repo, startPlugin, stopPlugin, STORAGE_FOLDER
9 + parsePluginSource, PATH as PLUGINS_PATH, Repo, startPlugin, stopPlugin, STORAGE_FOLDER, DELETE_ME_SUFFIX
10 } from './plugins'
11 import { ApiError } from './apiMiddleware'
12 import _ from 'lodash'
@@ -105,10 +105,10 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
105 if (await res.then(() => true, e => e.code === 'ENOENT')) break
106 await wait(1000)
107 }
108 - // delete old
109 - await rm(installPath, { recursive: true }).catch(e => {
110 - if (e.code !== 'ENOENT') throw e
111 - })
108 + // delete old folder, but it may fail in the presence of .node files, so we rename it first as a precaution (clearing require.cache doesn't help)
109 + const deleteMe = installPath + DELETE_ME_SUFFIX
110 + await rename(installPath, deleteMe)
111 + await rm(deleteMe, { recursive: true, force: true }).catch(e => console.warn(String(e)))
112 // final replace
113 await rename(tempInstallPath, installPath)
114 .catch(e => { throw e.code !== 'ENOENT' ? e : new ApiError(HTTP_NOT_ACCEPTABLE, "missing main file") })
src/plugins.ts
+8 -1
@@ -18,7 +18,7 @@ import { DirEntry } from './api.get_file_list'
18 import { VfsNode } from './vfs'
19 import { serveFile } from './serveFile'
20 import events from './events'
21 -import { mkdir, readFile, rename } from 'fs/promises'
21 +import { mkdir, readdir, readFile, rename, rm } from 'fs/promises'
22 import { existsSync, mkdirSync } from 'fs'
23 import { getConnections } from './connections'
24 import { dirname, join, resolve } from 'path'
@@ -36,8 +36,15 @@ import { CustomizedIcons, watchIconsFolder } from './icons'
36
37 export const PATH = 'plugins'
38 export const DISABLING_SUFFIX = '-disabled'
39 +export const DELETE_ME_SUFFIX = '-delete_me' + DISABLING_SUFFIX
40 export const STORAGE_FOLDER = 'storage'
41
42 +setTimeout(async () => { // delete leftovers, if any
43 + for (const x of await readdir(PATH))
44 + if (x.endsWith(DELETE_ME_SUFFIX))
45 + await rm(join(PATH, x), { recursive: true, force: true }).catch(() => {})
46 +}, 1000)
47 +
48 const plugins = new Map<string, Plugin>() // now that we care about the order, a simple object wouldn't do, because numbers are always at the beginning
49
50 export function isPluginRunning(id: string) {