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 }