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)