fix: (regression 0.48.0) couldn't update non-github plugins
Massimo Melina committed
Sep 9, 2023 at 13:16 UTC
5192e356745457922b0f7fa68939f67eca443917
5 files changed
+66
-66
admin/src/InstalledPlugins.ts
+16
-17
@@ -4,8 +4,8 @@ import { apiCall, useApiList } from './api'
4
import { createElement as h, Fragment, ReactNode } from 'react'
5
import { Alert, Box, Link, Tooltip } from '@mui/material'
6
import { DataTable } from './DataTable'
7
-import { Delete, Error, PlayCircle, Settings, StopCircle, Upgrade } from '@mui/icons-material'
8
-import { IconBtn, with_, xlate } from './misc'
7
+import { Delete, Error as ErrorIcon, PlayCircle, Settings, StopCircle, Upgrade } from '@mui/icons-material'
8
+import { IconBtn, prefix, with_, xlate } from './misc'
9
import { formDialog, toast } from './dialog'
10
import _ from 'lodash'
11
import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from '@hfs/mui-grid-form'
@@ -44,7 +44,19 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
44
},
45
],
46
actions: ({ row, id }) => updates ? [
47
- h(UpdateButton, { id, updated: row.updated, then: () => updateEntry({ id }, { updated: true }) })
47
+ h(IconBtn, {
48
+ icon: Upgrade,
49
+ title: row.updated ? "Already updated" : "Update",
50
+ disabled: row.updated,
51
+ async onClick() {
52
+ await apiCall('update_plugin', { id }, { timeout: false }).catch(e => {
53
+ throw e.code !== 424 ? e
54
+ : Error("Failed dependencies: " + e.cause?.map((x: any) => prefix(`plugin "`, x.id || x.repo, `" `) + x.error).join('; '))
55
+ })
56
+ updateEntry({ id }, { updated: true })
57
+ toast("Plugin updated")
58
+ }
59
+ })
60
] : [
61
h(IconBtn, row.started ? {
62
icon: StopCircle,
@@ -115,7 +127,7 @@ export function renderName({ row, value }: any) {
127
function errorIcon(msg: ReactNode, warning=false) {
128
return msg && h(Tooltip, {
129
title: msg,
118
- children: h(Error, { fontSize: 'small', color: warning ? 'warning' : 'error', sx: { ml: -.5, mr: .5 } })
130
+ children: h(ErrorIcon, { fontSize: 'small', color: warning ? 'warning' : 'error', sx: { ml: -.5, mr: .5 } })
131
})
132
}
133
}
@@ -151,19 +163,6 @@ export function showError(error: any) {
163
}))
164
}
165
154
-export function UpdateButton({ id, updated, then }: { id: string, updated?: boolean, then: (id:string)=>void }) {
155
- return h(IconBtn, {
156
- icon: Upgrade,
157
- title: updated ? "Already updated" : "Update",
158
- disabled: updated,
159
- async onClick() {
160
- await apiCall('update_plugin', { id }, { timeout: false })
161
- then?.(id)
162
- toast("Plugin updated")
163
- }
164
- })
165
-}
166
-
166
export async function startPlugin(id: string) {
167
await apiCall('start_plugin', { id })
168
toast("Plugin started", h(PlayCircle, { color: 'success' }))
admin/src/OnlinePlugins.ts
+2
-9
@@ -7,9 +7,8 @@ import { IconBtn } from './misc'
7
import { Download, Search } from '@mui/icons-material'
8
import { StringField } from '@hfs/mui-grid-form'
9
import { useDebounce } from 'usehooks-ts'
10
-import { renderName, showError, startPlugin, UpdateButton } from './InstalledPlugins'
10
+import { renderName, showError, startPlugin } from './InstalledPlugins'
11
import { state, useSnapState } from './state'
12
-import _ from 'lodash'
12
import { alertDialog } from './dialog'
13
14
export default function OnlinePlugins() {
@@ -68,13 +67,7 @@ export default function OnlinePlugins() {
67
},
68
],
69
actions: ({ row, id }) => [
71
- row.update ? h(UpdateButton, {
72
- id,
73
- then() {
74
- updateList(list =>
75
- _.find(list, { id }).update = false )
76
- }
77
- }) : h(IconBtn, {
70
+ h(IconBtn, {
71
icon: Download,
72
title: "Install",
73
progress: row.downloading,
dev-plugins.md
+3
-1
@@ -59,10 +59,12 @@ All the following properties are optional unless otherwise specified.
59
{
60
"web": "https://github.com/rejetto/file-icons",
61
"zip": "/archive/refs/heads/main.zip",
62
- "zipRoot: "file-icons/main",
62
+ "zipRoot: "file-icons-main/dist",
63
"main": "https://raw.githubusercontent.com/rejetto/file-icons/main/dist/plugin.js"
64
}
65
```
66
+ Note that in this example we are pointing to a github repo just for clarity. You are not supposed to use this
67
+ complicated object form to link github, use the string form.
68
Plugins with custom repos are not included in search results, but the update feature will still work.
69
70
WARNING: All the properties above are a bit special and must go in `exports` only (thus, not returned in `init`) and the syntax
src/api.plugins.ts
+26
-22
@@ -14,7 +14,7 @@ import {
14
setPluginConfig,
15
findPluginByRepo,
16
isPluginEnabled,
17
- isPluginRunning, stopPlugin, startPlugin,
17
+ isPluginRunning, stopPlugin, startPlugin, Repo,
18
} from './plugins'
19
import _ from 'lodash'
20
import assert from 'assert'
@@ -37,14 +37,6 @@ const apis: ApiHandlers = {
37
},
38
pluginUninstalled: id => list.remove({ id }),
39
})
40
-
41
- function serialize(p: Readonly<Plugin> | AvailablePlugin) {
42
- const o = 'getData' in p ? Object.assign(_.pick(p, ['id','started']), p.getData())
43
- : { ...p } // _.defaults mutates object, and we don't want that
44
- if (typeof o.repo === 'object') // custom repo
45
- o.repo = o.repo.web
46
- return _.defaults(o, { started: null, badApi: null }) // nulls should be used to be sure to overwrite previous values,
47
- }
40
},
41
42
async get_plugin_updates() {
@@ -57,8 +49,12 @@ const apis: ApiHandlers = {
49
const online = await readOnlinePlugin(repo)
50
if (!online?.apiRequired || online.badApi) return
51
const disk = getPluginInfo(folder)
60
- if (online.version! > disk.version)
52
+ if (!disk) return // plugin removed in the meantime?
53
+ if (online.version! > disk.version) { // it IS newer
54
+ online.id = disk.id // id is installation-dependant, and online cannot know
55
+ online.repo = serialize(disk).repo // show the user the current repo we are getting this update from, not a possibly-changed future one
56
list.add(online)
57
+ }
58
} catch (err: any) {
59
if (err.message === '404') // the plugin is declaring a wrong repo
60
return
@@ -116,7 +112,7 @@ const apis: ApiHandlers = {
112
113
const folder2repo = getFolder2repo()
114
for await (const pl of searchPlugins(text)) {
119
- const repo = pl.id
115
+ const repo = pl.repo || pl.id // .repo property can be more trustworthy in case github user renamed and left the previous link in 'repo'
116
if (_.includes(folder2repo, repo)) continue // don't include installed plugins
117
list.add(pl)
118
// watch for events about this plugin, until this request is closed
@@ -142,25 +138,25 @@ const apis: ApiHandlers = {
138
})
139
},
140
145
- async download_plugin(pl) {
146
- await checkDependencies(pl.id, pl.branch)
147
- const res = await downloadPlugin(pl.id, pl.branch)
141
+ async download_plugin({ id, branch }) {
142
+ await checkDependencies(id, branch)
143
+ const res = await downloadPlugin(id, { branch })
144
if (typeof res !== 'string')
145
return res
146
return (await waitFor(() => getPluginInfo(res), { timeout: 5000 }))
147
|| new ApiError(HTTP_SERVER_ERROR)
148
},
149
154
- async update_plugin(pl) {
155
- await checkDependencies(pl.id, pl.branch)
156
- const found = findPluginByRepo(pl.id) // github id !== local id
150
+ async update_plugin({ id }) {
151
+ const found = getPluginInfo(id)
152
if (!found)
153
return new ApiError(HTTP_NOT_FOUND)
159
- const enabled = isPluginEnabled(found.id)
160
- await stopPlugin(found.id)
161
- await downloadPlugin(pl.id, pl.branch, true)
154
+ await checkDependencies(found.repo, )
155
+ const enabled = isPluginEnabled(id)
156
+ await stopPlugin(id)
157
+ await downloadPlugin(found.repo, { overwrite: true })
158
if (enabled)
163
- startPlugin(found.id).then() // don't wait, in case it fails to start
159
+ startPlugin(id).then() // don't wait, in case it fails to start
160
return {}
161
},
162
@@ -174,7 +170,15 @@ const apis: ApiHandlers = {
170
171
export default apis
172
177
-async function checkDependencies(repo: string, branch: string) {
173
+function serialize(p: Readonly<Plugin> | AvailablePlugin) {
174
+ const o = 'getData' in p ? Object.assign(_.pick(p, ['id','started']), p.getData())
175
+ : { ...p } // _.defaults mutates object, and we don't want that
176
+ if (typeof o.repo === 'object') // custom repo
177
+ o.repo = o.repo.web
178
+ return _.defaults(o, { started: null, badApi: null }) // nulls should be used to be sure to overwrite previous values,
179
+}
180
+
181
+async function checkDependencies(repo: Repo, branch?: string) {
182
const rec = await readOnlinePlugin(repo, branch)
183
if (!rec) return
184
const miss = rec.depend && rec.depend.map((dep: any) => {
src/github.ts
+19
-17
@@ -12,7 +12,7 @@ import {
12
} from './plugins'
13
import { ApiError } from './apiMiddleware'
14
import _ from 'lodash'
15
-import { DAY, HFS_REPO, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FAILED_DEPENDENCY, HTTP_SERVER_ERROR } from './const'
15
+import { DAY, HFS_REPO, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE, HTTP_SERVER_ERROR } from './const'
16
import { rename, rm } from 'fs/promises'
17
import { join } from 'path'
18
import { readFileSync } from 'fs'
@@ -30,7 +30,16 @@ function downloadProgress(id: string, status: DownloadStatus) {
30
events.emit('pluginDownload_'+id, status)
31
}
32
33
-export async function downloadPlugin(repo: string, branch='', overwrite?: boolean) {
33
+// determine default branch, possibly without consuming api quota
34
+async function getGithubDefaultBranch(repo: string) {
35
+ const res = await httpString(`https://github.com/${repo}/archive/refs/heads/main.zip`, { method: 'HEAD' })
36
+ return res.ok ? 'main'
37
+ : (await getRepoInfo(repo))?.default_branch as string
38
+}
39
+
40
+export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }={}) {
41
+ if (typeof repo !== 'string')
42
+ repo = repo.main
43
if (downloading[repo])
44
return new ApiError(HTTP_CONFLICT, "already downloading")
45
console.log('downloading plugin', repo)
@@ -48,9 +57,7 @@ export async function downloadPlugin(repo: string, branch='', overwrite?: boolea
57
url = customRepo.web + url
58
return await go(url, pl?.id, customRepo.zipRoot ?? DIST_ROOT)
59
}
51
- const rec = await getRepoInfo(repo)
52
- if (!branch)
53
- branch = rec.default_branch
60
+ branch ||= await getGithubDefaultBranch(repo)
61
const short = repo.split('/')[1] // second part, repo without the owner
62
if (!short)
63
return new ApiError(HTTP_BAD_REQUEST, "bad repo")
@@ -81,7 +88,7 @@ export async function downloadPlugin(repo: string, branch='', overwrite?: boolea
88
})
89
const main = join(installPath, MAIN)
90
await rename(main + DISABLING_POSTFIX, main) // we are good now, restore name
84
- .catch(e => { throw e.code !== 'ENOENT' ? e : new ApiError(HTTP_FAILED_DEPENDENCY, "missing main file") })
91
+ .catch(e => { throw e.code !== 'ENOENT' ? e : new ApiError(HTTP_NOT_ACCEPTABLE, "missing main file") })
92
return folder
93
}
94
}
@@ -112,17 +119,12 @@ export async function readOnlinePlugin(repo: Repo, branch='') {
119
if (!res.ok) throw Error("bad repo.main")
120
return parsePluginSource(main, res.body) // use 'repo' as 'id' client-side
121
}
115
- const branches = branch ? [branch] : (async function*() {
116
- yield 'main' // getRepoInfo consumes github-api-quota, so give 'main' a shot first, and if it fails we'll ask
117
- yield (await getRepoInfo(repo))?.default_branch
118
- })()
119
- for await (const b of branches) {
120
- const res = await readGithubFile(`${repo}/${b}/${DIST_ROOT}/plugin.js`)
121
- if (!res) continue
122
- const pl = parsePluginSource(repo, res) // use 'repo' as 'id' client-side
123
- pl.branch = b || undefined
124
- return pl
125
- }
122
+ branch ||= await getGithubDefaultBranch(repo)
123
+ const res = await readGithubFile(`${repo}/${branch}/${DIST_ROOT}/plugin.js`)
124
+ if (!res) throw Error("missing plugin.js")
125
+ const pl = parsePluginSource(repo, res) // use 'repo' as 'id' client-side
126
+ pl.branch = branch
127
+ return pl
128
}
129
130
export function getFolder2repo() {