import * as fs from 'fs'; import { createWriteStream } from 'fs'; import * as path from 'path'; import { pipeline } from 'stream/promises'; import axios from 'axios'; import { GitHubRelease, GitHubReleaseAsset } from '../utils/checkForUpdatesUtils'; export interface Context { repo: string; // {owner}/{repo} githubToken: string; targetTag: string; } const apiBaseUrl = 'https://api.github.com/repos/'; const defaultApiHeaders = (context: Context) => ({ 'User-Agent': 'Joplin', 'Authorization': `token ${context.githubToken}`, 'X-GitHub-Api-Version': '2022-11-28', 'Accept': 'application/vnd.github+json', }); export const getTargetRelease = async (context: Context, targetTag: string): Promise<GitHubRelease> => { console.log('Fetching releases...'); // Note: We need to fetch all releases, not just /releases/tag/tag-name-here. // The latter doesn't include draft releases. const result = await fetch(`${apiBaseUrl}${context.repo}/releases`, { method: 'GET', headers: defaultApiHeaders(context), }); const releases = await result.json(); if (!result.ok) { throw new Error(`Error fetching release: ${JSON.stringify(releases)}`); } for (const release of releases) { if (release.tag_name === targetTag) { return release; } } throw new Error(`No release with tag ${targetTag} found!`); }; // Download a file from Joplin Desktop releases export const downloadFileFromGitHub = async (context: Context, asset: GitHubReleaseAsset, destinationDir: string) => { const downloadPath = path.join(destinationDir, asset.name); if (!fs.existsSync(destinationDir)) { fs.mkdirSync(destinationDir); } /* eslint-disable no-console */ console.log(`Downloading ${asset.name} from ${asset.url} to ${downloadPath}`); try { const response = await axios({ method: 'get', url: asset.url, responseType: 'stream', headers: { ...defaultApiHeaders(context), 'Accept': 'application/octet-stream', }, }); if (response.status < 200 || response.status >= 300) { throw new Error(`Failed to download file: Status Code ${response.status}`); } await pipeline(response.data, createWriteStream(downloadPath)); console.log('Download successful!'); /* eslint-enable no-console */ return downloadPath; } catch (error) { throw new Error('Download not successful.'); } }; export const updateReleaseAsset = async (context: Context, assetUrl: string, newName: string) => { console.log('Updating asset with URL', assetUrl, 'to have name, ', newName); // See https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#update-a-release-asset const result = await fetch(assetUrl, { method: 'PATCH', headers: defaultApiHeaders(context), body: JSON.stringify({ name: newName, }), }); if (!result.ok) { throw new Error(`Unable to update release asset: ${await result.text()}`); } }; export const uploadReleaseAsset = async (context: Context, release: GitHubRelease, filePath: string): Promise<void> => { console.log(`Uploading file from ${filePath} to release ${release.tag_name}`); const fileContent = fs.readFileSync(filePath); const fileName = path.basename(filePath); const uploadUrl = `https://uploads.github.com/repos/${context.repo}/releases/${release.id}/assets?name=${encodeURIComponent(fileName)}`; const response = await fetch(uploadUrl, { method: 'POST', headers: { ...defaultApiHeaders(context), 'Content-Type': 'application/octet-stream', }, body: fileContent, }); if (!response.ok) { throw new Error(`Failed to upload asset: ${await response.text()}`); } else { console.log(`${fileName} uploaded successfully.`); } };