admin/plugins: auto-correct 'repo' information
Massimo Melina committed
Jun 13, 2025 at 19:10 UTC
94f343c322335be5a89bc6bf5baa42eb55478c6c
3 files changed
+32
-16
src/api.plugins.ts
+2
-2
@@ -126,10 +126,10 @@ const apis: ApiHandlers = {
126
}
127
})
128
try {
129
- const already = Object.values(getFolder2repo()).map(String)
129
+ const already = Object.values(getFolder2repo()).filter(Boolean).map(String)
130
for await (const pl of await searchPlugins(text, { skipRepos: already })) {
131
const repo = pl.repo || pl.id // .repo property can be more trustworthy in case github user renamed and left the previous link in 'repo'
132
- const missing = await getMissingDependencies(pl)
132
+ const missing = getMissingDependencies(pl)
133
if (missing.length) pl.missing = missing
134
list.add(pl)
135
repos.push(repo)
src/github.ts
+20
-5
@@ -6,7 +6,8 @@ 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, DELETE_ME_SUFFIX
9
+ parsePluginSource, PATH as PLUGINS_PATH, Repo, startPlugin, stopPlugin, STORAGE_FOLDER, DELETE_ME_SUFFIX,
10
+ PLUGIN_MAIN_FILE
11
} from './plugins'
12
import { ApiError } from './apiMiddleware'
13
import _ from 'lodash'
@@ -14,7 +15,7 @@ import {
15
HFS_REPO, HFS_REPO_BRANCH, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_NOT_ACCEPTABLE,
16
HTTP_SERVER_ERROR, VERSION
17
} from './const'
17
-import { rename, rm } from 'fs/promises'
18
+import { readFile, rename, rm, writeFile } from 'fs/promises'
19
import { join } from 'path'
20
import { readFileSync } from 'fs'
21
import { storedMap } from './persistence'
@@ -50,14 +51,15 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
51
repo = repo.main
52
if (downloading[repo])
53
throw new ApiError(HTTP_CONFLICT, "already downloading")
53
- const msg = await isPluginBlacklisted(repo)
54
+ const msg = await isPluginBlacklisted(repo) // check before downloading, in case other filters were passed somehow
55
if (msg)
56
throw new ApiError(HTTP_FORBIDDEN, "blacklisted: " + msg)
57
console.log('downloading plugin', repo)
58
downloadProgress(repo, true)
59
try {
60
const pl = findPluginByRepo(repo)
60
- if (repo.includes('//')) { // custom repo
61
+ const customRepo = repo.includes('//')
62
+ if (customRepo) { // custom repo
63
if (!pl)
64
throw new ApiError(HTTP_BAD_REQUEST, "bad repo")
65
const customRepo = ((pl as any).getData?.() || pl).repo
@@ -95,6 +97,18 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
97
dest = join(tempInstallPath, dest)
98
return rm(dest, { force: true }).then(() => dest, () => false)
99
})
100
+ if (!customRepo) {
101
+ const mainFile = join(tempInstallPath, PLUGIN_MAIN_FILE)
102
+ const content = await readFile(mainFile, 'utf8')
103
+ // force github plugins to have correct repo, in case it is missing, wrong, or just outdated after a rename
104
+ if (repo !== parsePluginSource('', content).repo) {
105
+ const correct = `exports.repo = ${JSON.stringify(repo)}\n`
106
+ let newContent = content.replace(/exports.repo\s*=\s*\S*/g, correct)
107
+ if (newContent === content)
108
+ newContent = correct + content
109
+ await writeFile(mainFile, newContent) // first, as our parsing will consider that (unlike javascript)
110
+ }
111
+ }
112
// ready to replace
113
const wasRunning = isPluginRunning(folder)
114
if (wasRunning)
@@ -144,7 +158,7 @@ export async function readOnlinePlugin(repo: Repo, branch='') {
158
return parsePluginSource(main, await httpString(main)) // use 'repo' as 'id' client-side
159
}
160
branch ||= await getGithubDefaultBranch(repo)
147
- const res = await readGithubFile(`${repo}/${branch}/${DIST_ROOT}/plugin.js`)
161
+ const res = await readGithubFile(`${repo}/${branch}/${DIST_ROOT}/${PLUGIN_MAIN_FILE}`)
162
const pl = parsePluginSource(repo, res) // use 'repo' as 'id' client-side
163
pl.branch = branch
164
return pl
@@ -224,6 +238,7 @@ export async function searchPlugins(text='', { skipRepos=[''] }={}) {
238
const pl = await readOnlineCompatiblePlugin(repo, it.default_branch).catch(() => undefined)
239
if (!pl) return
240
Object.assign(pl, { // inject some extra useful fields
241
+ repo, // overwrite parsed value, that may be wrong
242
downloading: downloading[repo],
243
license: it.license?.spdx_id,
244
}, _.pick(it, ['pushed_at', 'stargazers_count', 'default_branch']))
src/plugins.ts
+10
-9
@@ -404,6 +404,7 @@ enablePlugins.sub(rescanAsap)
404
export const suspendPlugins = defineConfig(CFG.suspend_plugins, false)
405
406
export const pluginsConfig = defineConfig('plugins_config', {} as Record<string,any>)
407
+export const PLUGIN_MAIN_FILE = 'plugin.js'
408
409
const pluginWatchers = new Map<string, ReturnType<typeof watchPlugin>>()
410
@@ -418,7 +419,7 @@ export async function rescan() {
419
const id = path.split('/').slice(-1)[0]!
420
met.push(id)
421
if (!pluginWatchers.has(id))
421
- pluginWatchers.set(id, watchPlugin(id, join(path, 'plugin.js')))
422
+ pluginWatchers.set(id, watchPlugin(id, join(path, PLUGIN_MAIN_FILE)))
423
}
424
for (const [id, cancelWatcher] of pluginWatchers.entries())
425
if (!met.includes(id)) {
@@ -647,16 +648,16 @@ onProcessExit(() =>
648
649
export function parsePluginSource(id: string, source: string) {
650
const pl: AvailablePlugin = { id }
650
- pl.description = tryJson(/exports.description *= *(".*")/.exec(source)?.[1])
651
- pl.repo = tryJson(/exports.repo *= *(.*);? *$/m.exec(source)?.[1])
652
- pl.version = Number(/exports.version *= *(\d*\.?\d+)/.exec(source)?.[1]) ?? undefined
653
- pl.apiRequired = tryJson(/exports.apiRequired *= *([ \d.,[\]]+)/.exec(source)?.[1]) ?? undefined
654
- pl.isTheme = tryJson(/exports.isTheme *= *(true|false|"light"|"dark")/.exec(source)?.[1]) ?? (id.endsWith('-theme') || undefined)
655
- pl.preview = tryJson(/exports.preview *= *(.+)/.exec(source)?.[1]) ?? undefined
656
- pl.depend = tryJson(/exports.depend *= *(\[[\s\S]*?])/m.exec(source)?.[1])?.filter((x: any) =>
651
+ pl.description = tryJson(/exports.description\s*=\s*(".*")/.exec(source)?.[1])
652
+ pl.repo = tryJson(/exports.repo\s*=\s*(\S*)/.exec(source)?.[1])
653
+ pl.version = Number(/exports.version\s*=\s*(\d*\.?\d+)/.exec(source)?.[1]) ?? undefined
654
+ pl.apiRequired = tryJson(/exports.apiRequired\s*=\s*([ \d.,[\]]+)/.exec(source)?.[1]) ?? undefined
655
+ pl.isTheme = tryJson(/exports.isTheme\s*=\s*(true|false|"light"|"dark")/.exec(source)?.[1]) ?? (id.endsWith('-theme') || undefined)
656
+ pl.preview = tryJson(/exports.preview\s*=\s*(.+)/.exec(source)?.[1]) ?? undefined
657
+ pl.depend = tryJson(/exports.depend\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1])?.filter((x: any) =>
658
typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
659
|| console.warn("plugin dependency discarded", x) )
659
- pl.changelog = tryJson(/exports.changelog *= *(\[[\s\S]*?])/m.exec(source)?.[1])
660
+ pl.changelog = tryJson(/exports.changelog\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1])
661
if (Array.isArray(pl.apiRequired) && (pl.apiRequired.length !== 2 || !pl.apiRequired.every(_.isFinite))) // validate [from,to] form
662
pl.apiRequired = undefined
663
calculateBadApi(pl)