From 20d276cd10cf55016ae19d9c2205fc2eea4b961d Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Fri, 20 Nov 2020 13:21:56 +0100 Subject: [PATCH] Change redirection to caching local copy and then serving download i.e. this allows clients to request partial downloads with range headers --- config/default.json5 | 1 + .../GiteaRepoLatestReleaseController.ts | 76 +++++++++++++++++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/config/default.json5 b/config/default.json5 index 2fe7beb..206a77b 100644 --- a/config/default.json5 +++ b/config/default.json5 @@ -11,4 +11,5 @@ view: { cache: false, }, + assets_base_dir: 'downloads', } diff --git a/src/controllers/GiteaRepoLatestReleaseController.ts b/src/controllers/GiteaRepoLatestReleaseController.ts index 62dabb6..7312285 100644 --- a/src/controllers/GiteaRepoLatestReleaseController.ts +++ b/src/controllers/GiteaRepoLatestReleaseController.ts @@ -4,6 +4,11 @@ import * as https from "https"; import config from "config"; import {log} from "swaf/Logger"; import {NotFoundHttpError} 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 { @@ -15,7 +20,7 @@ export default class GiteaRepoLatestReleaseController extends Controller { const {owner, name, file} = req.params; if (!owner || !name) return next(); - https.get(`${config.get('gitea_instance_url')}/api/v1/repos/${owner}/${name}/releases`, { + const httpRequest = https.get(`${config.get('gitea_instance_url')}/api/v1/repos/${owner}/${name}/releases`, { headers: { 'Accept': 'application/json', }, @@ -24,7 +29,7 @@ export default class GiteaRepoLatestReleaseController extends Controller { r.on('data', c => { data += c; }); - r.on('end', () => { + r.on('end', async () => { try { const releases = JSON.parse(data); @@ -32,8 +37,19 @@ export default class GiteaRepoLatestReleaseController extends Controller { for (const release of releases) { for (const asset of release.assets) { if (asset.name === file) { - log.debug('Redirect to', asset.browser_download_url); - return res.redirect(302, asset.browser_download_url); + 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, + }, + }); } } } @@ -50,8 +66,58 @@ export default class GiteaRepoLatestReleaseController extends Controller { return next(e); } }); - }).on('error', err => { + }); + 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); + + // Download asset if it doesn't exist + if (!await promisify(fs.exists)(assetPath)) { + const file = fs.createWriteStream(assetPath); + 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(); + } + + // Respond + return res.download(assetPath, downloadProperties.asset.name); } } + +export type DownloadProperties = { + repo: { + owner: string; + name: string; + }; + asset: { + id: string, + name: string; + url: string; + size: number; + }; +};