Add GitHub plugin release imports and update discovery
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { db } from '../db/index.js';
|
||||
import { createScript, getScript, getVersion, uploadNewVersion, type ScopeType } from './scripts.js';
|
||||
import { findRunningBetaForScript } from './beta.js';
|
||||
import { downloadGithubAsset, getGithubRelease, GithubReleaseError } from '../integrations/github-releases.js';
|
||||
import type { PluginSource, PluginUpdate } from '$lib/plugin-github.js';
|
||||
|
||||
export function pluginSource(pluginId: number): PluginSource | null {
|
||||
return db().prepare(`SELECT p.* FROM plugin_imports p JOIN script_versions v ON v.id = p.version_id
|
||||
WHERE v.script_id = ? ORDER BY v.version DESC LIMIT 1`).get(pluginId) as PluginSource | undefined ?? null;
|
||||
}
|
||||
|
||||
export async function checkPluginUpdate(pluginId: number): Promise<PluginUpdate> {
|
||||
const plugin = getScript(pluginId);
|
||||
if (!plugin || plugin.kind !== 'plugin') throw new GithubReleaseError('Plugin not found.', 404);
|
||||
const source = pluginSource(pluginId);
|
||||
const base = { pluginId, name: plugin.name, source };
|
||||
if (!source) return { ...base, release: null, status: 'untracked' };
|
||||
try {
|
||||
const release = await getGithubRelease(`https://github.com/${source.repository}`);
|
||||
// A latest-stable release may already be in history when a newer prerelease is imported.
|
||||
if (release.id !== source.release_id && Date.parse(release.publishedAt) <= Date.parse(source.published_at)) {
|
||||
return { ...base, release, status: 'ahead' };
|
||||
}
|
||||
const imported = db().prepare(`SELECT p.version_id FROM plugin_imports p JOIN script_versions v ON v.id = p.version_id
|
||||
WHERE v.script_id = ? AND lower(p.repository) = lower(?) AND p.release_id = ? AND p.asset_name = ?
|
||||
ORDER BY v.version DESC LIMIT 1`).get(pluginId, source.repository, release.id, source.asset_name) as { version_id: number } | undefined;
|
||||
if (imported) return { ...base, release, status: imported.version_id === plugin.current_version_id ? 'current' : 'imported', versionId: imported.version_id };
|
||||
return { ...base, release, status: 'available' };
|
||||
} catch (error) {
|
||||
return { ...base, release: null, status: 'error', error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export interface ImportPluginReleaseInput {
|
||||
url: string;
|
||||
assetId: number;
|
||||
pluginId?: number;
|
||||
name?: string;
|
||||
scopeType?: ScopeType | null;
|
||||
scopeValue?: string | null;
|
||||
}
|
||||
|
||||
export async function importPluginRelease(input: ImportPluginReleaseInput) {
|
||||
if (typeof input.url !== 'string' || !Number.isSafeInteger(input.assetId) || input.assetId < 1) {
|
||||
throw new GithubReleaseError('A release URL and DLL asset selection are required.');
|
||||
}
|
||||
if (input.pluginId !== undefined && (!Number.isSafeInteger(input.pluginId) || input.pluginId < 1)) {
|
||||
throw new GithubReleaseError('Invalid plugin id.');
|
||||
}
|
||||
const requirePlugin = () => {
|
||||
const plugin = input.pluginId === undefined ? undefined : getScript(input.pluginId);
|
||||
if (input.pluginId !== undefined && (!plugin || plugin.kind !== 'plugin')) throw new GithubReleaseError('Plugin not found.', 404);
|
||||
if (plugin && findRunningBetaForScript(plugin.id)) throw new GithubReleaseError('Finish the running beta before importing another release.', 409);
|
||||
return plugin;
|
||||
};
|
||||
requirePlugin();
|
||||
if (input.pluginId === undefined && (typeof input.name !== 'string' || !input.name.trim())) throw new GithubReleaseError('A plugin name is required.');
|
||||
// Refetch the exact reviewed release: asset names, notes, tags and digests are never trusted from the client.
|
||||
const release = await getGithubRelease(input.url, true);
|
||||
const asset = release.assets.find(a => a.id === input.assetId);
|
||||
if (!asset) throw new GithubReleaseError('That DLL is no longer available in this release. Preview it again.');
|
||||
const content = await downloadGithubAsset(release, asset);
|
||||
const sha = createHash('sha256').update(content).digest('hex');
|
||||
return db().transaction(() => {
|
||||
const plugin = requirePlugin();
|
||||
if (plugin) {
|
||||
const prior = db().prepare(`SELECT v.id FROM script_versions v JOIN plugin_imports p ON p.version_id = v.id
|
||||
WHERE v.script_id = ? AND lower(p.repository) = lower(?) AND p.release_id = ? AND p.asset_id = ? AND v.sha256 = ?
|
||||
ORDER BY v.version DESC LIMIT 1`).get(plugin.id, release.repository, release.id, asset.id, sha) as { id: number } | undefined;
|
||||
if (prior) return result(plugin.id, getVersion(prior.id)!, true);
|
||||
}
|
||||
const created = plugin ? { script: plugin, version: uploadNewVersion(plugin.id, content, release.notes, release.tag) }
|
||||
: createScript({ name: input.name!.trim(), kind: 'plugin', targetRelPath: asset.name, content,
|
||||
releaseVersion: release.tag, notes: release.notes, scopeType: input.scopeType, scopeValue: input.scopeValue });
|
||||
db().prepare(`INSERT INTO plugin_imports (version_id, repository, release_id, release_url, asset_id, asset_name, published_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(created.version.id, release.repository, release.id, release.url, asset.id, asset.name, release.publishedAt);
|
||||
return result(created.script.id, created.version, false);
|
||||
})();
|
||||
}
|
||||
|
||||
function result(pluginId: number, version: NonNullable<ReturnType<typeof getVersion>>, alreadyImported: boolean) {
|
||||
return { pluginId, versionId: version.id, releaseVersion: version.release_version, sha256: version.sha256,
|
||||
alreadyImported, betaUrl: `/betas?pluginId=${pluginId}&versionId=${version.id}` };
|
||||
}
|
||||
Reference in New Issue
Block a user