import Controller from "swaf/Controller"; import {NextFunction, Request, Response} from "express"; import * as https from "https"; import config from "config"; import {log} from "swaf/Logger"; import {NotFoundHttpError, ServiceUnavailableHttpError} from "swaf/HttpError"; import * as fs from "fs"; import {promisify} from "util"; import path from "path"; export const ASSETS_BASE_DIR = config.get('assets_base_dir'); export default class GiteaRepoLatestReleaseController extends Controller { public routes(): void { this.get('/:owner/:name/:file?', this.getFile, 'get-repo-release-file'); } protected async getFile(req: Request, res: Response, next: NextFunction): Promise { log.info('Serving ' + req.path + ' ...'); const {owner, name, file} = req.params; if (!owner || !name) return next(); const httpRequest = https.get(`${config.get('gitea_instance_url')}/api/v1/repos/${owner}/${name}/releases`, { headers: { 'Accept': 'application/json', }, }, r => { let data = ''; r.on('data', c => { data += c; }); r.on('end', async () => { try { const releases = JSON.parse(data); if (file) { for (const release of releases) { for (const asset of release.assets) { if (asset.name === file) { log.debug('Download', asset.browser_download_url); return await this.download(req, res, next, { repo: { owner: owner, name: name, }, asset: { id: asset.id, name: asset.name, url: asset.browser_download_url, size: asset.size, }, }); } } } throw new NotFoundHttpError('Asset', req.url); } else { log.debug('List files'); return res.render('list-files', { owner: owner, name: name, releases: releases, }); } } catch (e) { return next(e); } }); }); httpRequest.on('error', err => { log.error(err); }); httpRequest.end(); } protected async download( req: Request, res: Response, next: NextFunction, downloadProperties: DownloadProperties, ): Promise { // Make base dir if (!await promisify(fs.exists)(ASSETS_BASE_DIR)) { await promisify(fs.mkdir)(ASSETS_BASE_DIR); } const assetPath = path.resolve(ASSETS_BASE_DIR, '' + downloadProperties.asset.id); const tmpAssetPath = assetPath + '.tmp'; // Download asset if it doesn't exist if (!await promisify(fs.exists)(assetPath)) { if (await promisify(fs.exists)(tmpAssetPath)) { throw new ServiceUnavailableHttpError('This file is currently being cached. Please try again later.'); } const file = fs.createWriteStream(tmpAssetPath); await new Promise((resolve, reject) => { const httpRequest = https.get(downloadProperties.asset.url, res => { res.on('end', () => { resolve(); }); res.pipe(file); }); httpRequest.on('error', () => { reject(); }); httpRequest.end(); }); file.close(); await promisify(fs.rename)(tmpAssetPath, assetPath); } const options: Record = {}; if (req.header('Range')) { options.headers = { 'Content-Type': 'multipart/byteranges', }; } // Respond return res.download(assetPath, downloadProperties.asset.name, options); } } export type DownloadProperties = { repo: { owner: string; name: string; }; asset: { id: string, name: string; url: string; size: number; }; };