plugins: support for different api versions through repo branches
Massimo Melina committed
May 14, 2022 at 00:53 UTC
289b2388db6dece6b592474971f555f71853a33e
4 files changed
+52
-27
admin/src/OnlinePlugins.ts
+2
-2
@@ -48,7 +48,7 @@ export default function OnlinePlugins() {
48
hideSortIcons: true,
49
disableColumnMenu: true,
50
renderCell({ row }) {
51
- const { id } = row
51
+ const { id, branch } = row
52
return h('div', {},
53
repoLink(id),
54
h(IconBtn, {
@@ -59,7 +59,7 @@ export default function OnlinePlugins() {
59
tooltipProps: { placement:'bottom-end' }, // workaround problem with horizontal scrolling by moving the tooltip leftward
60
confirm: "WARNING - Proceed only if you trust this author and this plugin",
61
async onClick() {
62
- await apiCall('download_plugin', { id })
62
+ await apiCall('download_plugin', { id, branch })
63
toast("Plugin downloaded: " + id)
64
}
65
})
dev-plugins.md
+7
-1
@@ -133,7 +133,7 @@ This is a list of available frontend events, with respective parameters and outp
133
134
## Publish your plug-in
135
136
-Suggested method for publishing is to have a dedicated repository on github, with topic `hfs-plugin`.
136
+Suggested method for publishing is to have a dedicated repository on GitHub, with topic `hfs-plugin`.
137
To set the topic go on the repo home and click on the gear icon near the "About" box.
138
Be sure to also fill the "description" field, especially with words that people may search for.
139
@@ -141,3 +141,9 @@ The files intended to be installed must go in a folder named `dist`.
141
You can keep other files outside.
142
143
You can refer to this dummy plugin for reference https://github.com/rejetto/demo-plugin .
144
+
145
+Published plugins are required to specify the `apiRequired` property.
146
+
147
+It is possible to publish different versions of the plugin to be compatible with different versions of HFS.
148
+To do that, just have your other versions in branches with name starting with `api`.
149
+HFS will scan through them in alphabetical order searching for a compatible one.
server/src/api.plugins.ts
+40
-22
@@ -40,13 +40,12 @@ const apis: ApiHandlers = {
40
async get_plugin_updates() {
41
const list = sendList()
42
setTimeout(async () => {
43
- const repo2id = getRepo2id()
44
- for (const repo in repo2id)
43
+ const repo2folder = getRepo2folder()
44
+ for (const repo in repo2folder)
45
try {
46
const online = await readOnlinePlugin(await getRepoInfo(repo))
47
if (!online.apiRequired || online.badApi) continue
48
- const id = repo2id[repo]
49
- const disk = getPluginInfo(id)
48
+ const disk = getPluginInfo(repo2folder[repo])
49
if (online.version! > disk.version)
50
list.add(online)
51
}
@@ -85,15 +84,29 @@ const apis: ApiHandlers = {
84
85
search_online_plugins({ text }, ctx) {
86
const list = sendList()
88
- const repo2id = getRepo2id()
87
+ const repo2folder = getRepo2folder()
88
+ // do our job in then() so api can return in the meantime
89
apiGithub('search/repositories?q=topic:hfs-plugin+' + encodeURI(text)).then(async res => {
90
for (const it of res.items) {
91
const repo = it.full_name
92
- const pl = await readOnlinePlugin(it)
93
- if (!pl.apiRequired || pl.badApi) continue
92
+ let pl = await readOnlinePlugin(it)
93
+ if (!pl.apiRequired) continue // mandatory field
94
+ if (pl.badApi) { // we try other branches (starting with 'api')
95
+ const branches: string[] = (await apiGithub('repos/' + it.full_name + '/branches'))
96
+ .map((x:any) => x.name).filter((x: string) => x.startsWith('api')).sort()
97
+ for (const branch of branches) {
98
+ pl = await readOnlinePlugin(it, branch)
99
+ if (!pl.apiRequired)
100
+ pl.badApi = '-'
101
+ if (!pl.badApi)
102
+ break
103
+ }
104
+ }
105
+ if (pl.badApi)
106
+ continue
107
Object.assign(pl, { // inject some extra useful fields
108
downloading: downloading[repo],
96
- installed: repo2id[repo]
109
+ installed: repo2folder[repo]
110
})
111
list.add(pl)
112
// watch for events about this plugin, until this request is closed
@@ -103,7 +116,7 @@ const apis: ApiHandlers = {
116
list.update({ id: repo }, { installed: true })
117
},
118
pluginUninstalled: id => {
106
- if (repo === _.findKey(repo2id, x => x === id))
119
+ if (repo === _.findKey(repo2folder, x => x === id))
120
list.update({ id: repo }, { installed: false })
121
},
122
['pluginDownload_'+repo](status) {
@@ -118,17 +131,17 @@ const apis: ApiHandlers = {
131
return list.return
132
},
133
121
- async download_plugin({ id }) {
122
- if (downloading[id])
134
+ async download_plugin(pl) {
135
+ if (downloading[pl.id])
136
return new ApiError(409, "already downloading")
124
- await downloadPlugin(id)
137
+ await downloadPlugin(pl.id, pl.branch)
138
return {}
139
},
140
128
- async update_plugin({ id }) {
129
- if (downloading[id])
141
+ async update_plugin(pl) {
142
+ if (downloading[pl.id])
143
return new ApiError(409, "already downloading")
131
- await downloadPlugin(id, true)
144
+ await downloadPlugin(pl.id, pl.branch, true)
145
return {}
146
},
147
@@ -154,17 +167,19 @@ function downloadProgress(id: string, status: DownloadStatus) {
167
events.emit('pluginDownload_'+id, status)
168
}
169
157
-async function downloadPlugin(repo: string, overwrite?: boolean) {
170
+async function downloadPlugin(repo: string, branch='', overwrite?: boolean) {
171
downloadProgress(repo, true)
172
const rec = await getRepoInfo(repo)
160
- const url = `https://github.com/${repo}/archive/${rec.default_branch}.zip`
173
+ if (!branch)
174
+ branch = rec.default_branch
175
+ const url = `https://github.com/${repo}/archive/refs/heads/${branch}.zip`
176
const res = await httpsStream(url)
177
const repo2 = repo.split('/')[1] // second part, repo without the owner
178
const repo2clash = !overwrite
179
&& (getAvailablePlugins().find(x => x.id === repo2) || mapPlugins(x => x.id === repo2).some(Boolean))
180
const pluginFolder = repo2clash ? repo.replace('/','-') : repo2 // longer form only if necessary
181
const installFolder = PLUGINS_PATH + '/' + pluginFolder
167
- const GITHUB_ZIP_ROOT = repo2 + '-' + rec.default_branch // github puts everything within this folder
182
+ const GITHUB_ZIP_ROOT = repo2 + '-' + (branch) // github puts everything within this folder
183
const rootWithinZip = GITHUB_ZIP_ROOT + '/' + DIST_ROOT
184
return new Promise(resolve =>
185
res.pipe(unzipper.Parse())
@@ -202,14 +217,17 @@ function getRepoInfo(id: string) {
217
return apiGithub('repos/'+id)
218
}
219
205
-async function readOnlinePlugin(repo: { full_name: string, default_branch: string }) {
206
- const res = await httpsString(`https://raw.githubusercontent.com/${repo.full_name}/${repo.default_branch}/${DIST_ROOT}plugin.js`)
220
+async function readOnlinePlugin(repoInfo: { full_name: string, default_branch: string }, branch='') {
221
+ const url = `https://raw.githubusercontent.com/${repoInfo.full_name}/${branch || repoInfo.default_branch}/${DIST_ROOT}plugin.js`
222
+ const res = await httpsString(url)
223
if (!res.ok)
224
throw res.statusCode
209
- return parsePluginSource(repo.full_name, res.body) // use 'repo' as 'id' client-side
225
+ const pl = parsePluginSource(repoInfo.full_name, res.body) // use 'repo' as 'id' client-side
226
+ pl.branch = branch || undefined
227
+ return pl
228
}
229
212
-function getRepo2id() {
230
+function getRepo2folder() {
231
const ret = Object.fromEntries(getAvailablePlugins().map(x => [x.repo, x.id]))
232
Object.assign(ret, Object.fromEntries(mapPlugins(x => [x.getData().repo, x.id]))) // started ones
233
delete ret.undefined
server/src/plugins.ts
+3
-2
@@ -162,6 +162,7 @@ export interface AvailablePlugin {
162
version?: number
163
apiRequired?: number
164
repo?: string
165
+ branch?: string
166
badApi?: string
167
}
168
@@ -282,8 +283,8 @@ export function parsePluginSource(id: string, source: string) {
283
try { pl.description = JSON.parse(`"${v}"`) }
284
catch {}
285
pl.repo = /exports.repo *= *"(.*)"/.exec(source)?.[1]
285
- pl.version = Number(/exports.version *= *(\d*\.?\d+)/.exec(source)?.[1]) || undefined
286
- pl.apiRequired = Number(/exports.apiRequired *= *(\d+)/.exec(source)?.[1]) || undefined
286
+ pl.version = Number(/exports.version *= *(\d*\.?\d+)/.exec(source)?.[1]) ?? undefined
287
+ pl.apiRequired = Number(/exports.apiRequired *= *(\d*\.?\d+)/.exec(source)?.[1]) ?? undefined
288
calculateBadApi(pl)
289
return pl
290
}