From 8f6a47536c44b3e89322c1e1a2ccbc2c2ef9d790 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Tue, 1 Jun 2021 11:09:46 +0200 Subject: [PATCH] Desktop: Download plugins from GitHub release --- .../tests/services/plugins/RepositoryApi.ts | 2 +- .../controls/plugins/PluginsStates.tsx | 2 +- .../lib/services/plugins/RepositoryApi.ts | 69 +++++++++++++++++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/packages/app-cli/tests/services/plugins/RepositoryApi.ts b/packages/app-cli/tests/services/plugins/RepositoryApi.ts index 1c8c1c1531..73298ed343 100644 --- a/packages/app-cli/tests/services/plugins/RepositoryApi.ts +++ b/packages/app-cli/tests/services/plugins/RepositoryApi.ts @@ -4,7 +4,7 @@ import { setupDatabaseAndSynchronizer, switchClient, supportDir, createTempDir } async function newRepoApi(): Promise { const repo = new RepositoryApi(`${supportDir}/pluginRepo`, await createTempDir()); - await repo.loadManifests(); + await repo.initialize(); return repo; } diff --git a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx index b307021df9..af0eee0a8e 100644 --- a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx +++ b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx @@ -113,7 +113,7 @@ export default function(props: Props) { let loadError: Error = null; try { - await repoApi().loadManifests(); + await repoApi().initialize(); } catch (error) { logger.error(error); loadError = error; diff --git a/packages/lib/services/plugins/RepositoryApi.ts b/packages/lib/services/plugins/RepositoryApi.ts index 09117f7d64..c302100494 100644 --- a/packages/lib/services/plugins/RepositoryApi.ts +++ b/packages/lib/services/plugins/RepositoryApi.ts @@ -1,8 +1,21 @@ +import Logger from '../../Logger'; import shim from '../../shim'; import { PluginManifest } from './utils/types'; const md5 = require('md5'); const compareVersions = require('compare-versions'); +const logger = Logger.create('RepositoryApi'); + +interface ReleaseAsset { + name: string; + browser_download_url: string; +} + +interface Release { + upload_url: string; + assets: ReleaseAsset[]; +} + export default class RepositoryApi { // As a base URL, this class can support either a remote repository or a @@ -14,6 +27,7 @@ export default class RepositoryApi { // Later on, other repo types could be supported. private baseUrl_: string; private tempDir_: string; + private release_: Release = null; private manifests_: PluginManifest[] = null; public constructor(baseUrl: string, tempDir: string) { @@ -21,7 +35,12 @@ export default class RepositoryApi { this.tempDir_ = tempDir; } - public async loadManifests() { + public async initialize() { + await this.loadManifests(); + await this.loadRelease(); + } + + private async loadManifests() { const manifestsText = await this.fetchText('manifests.json'); try { const manifests = JSON.parse(manifestsText); @@ -34,6 +53,27 @@ export default class RepositoryApi { } } + private get githubApiUrl(): string { + // https://github.com/joplin/plugins + // https://api.github.com/repos/joplin/plugins/releases + return this.baseUrl_.replace(/^(https:\/\/)(github\.com\/)(.*)$/, '$1api.$2repos/$3'); + } + + private async loadRelease() { + this.release_ = null; + + if (this.isLocalRepo) return; + + try { + const response = await fetch(`${this.githubApiUrl}/releases`); + const releases = await response.json(); + if (!releases.length) throw new Error('No release was found'); + this.release_ = releases[0]; + } catch (error) { + logger.warn('Could not load release - files will be downloaded from the repository directly:', error); + } + } + private get isLocalRepo(): boolean { return this.baseUrl_.indexOf('http') !== 0; } @@ -46,15 +86,34 @@ export default class RepositoryApi { } } - private fileUrl(relativePath: string): string { + private assetFileUrl(pluginId: string): string { + if (this.release_) { + const asset = this.release_.assets.find(asset => { + const s = asset.name.split('@'); + s.pop(); + const id = s.join('@'); + return id === pluginId; + }); + + if (asset) return asset.browser_download_url; + + logger.warn(`Could not get plugin from release: ${pluginId}`); + } + + // If we couldn't get the plugin file from the release, get it directly + // from the repository instead. + return this.repoFileUrl(`plugins/${pluginId}/plugin.jpl`); + } + + private repoFileUrl(relativePath: string): string { return `${this.contentBaseUrl}/${relativePath}`; } private async fetchText(path: string): Promise { if (this.isLocalRepo) { - return shim.fsDriver().readFile(this.fileUrl(path), 'utf8'); + return shim.fsDriver().readFile(this.repoFileUrl(path), 'utf8'); } else { - return shim.fetchText(this.fileUrl(path)); + return shim.fetchText(this.repoFileUrl(path)); } } @@ -86,7 +145,7 @@ export default class RepositoryApi { const manifest = manifests.find(m => m.id === pluginId); if (!manifest) throw new Error(`No manifest for plugin ID "${pluginId}"`); - const fileUrl = this.fileUrl(`plugins/${manifest.id}/plugin.jpl`); + const fileUrl = this.assetFileUrl(manifest.id); // this.repoFileUrl(`plugins/${manifest.id}/plugin.jpl`); const hash = md5(Date.now() + Math.random()); const targetPath = `${this.tempDir_}/${hash}_${manifest.id}.jpl`;