86 lines
5.5 KiB
TypeScript
86 lines
5.5 KiB
TypeScript
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}` };
|
|
}
|