160 lines
5.8 KiB
TypeScript
160 lines
5.8 KiB
TypeScript
import Controller from "swaf/Controller";
|
|
import {NextFunction, Request, Response} from "express";
|
|
import * as https from "https";
|
|
import config from "config";
|
|
import {NotFoundHttpError, ServiceUnavailableHttpError} from "swaf/HttpError";
|
|
import * as fs from "fs";
|
|
import {promisify} from "util";
|
|
import path from "path";
|
|
import sendRanges, {SendRangeGetStreamFn} from "send-ranges";
|
|
import mime from "mime";
|
|
import {logger} from "swaf/Logger";
|
|
import {ParsedUrlQueryInput} from "querystring";
|
|
|
|
export const ASSETS_BASE_DIR = config.get<string>('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<void> {
|
|
logger.info('Serving ' + req.path + ' ...');
|
|
const {owner, name, file} = req.params;
|
|
if (!owner || !name) return next();
|
|
|
|
// User redirections
|
|
const userRedirections = config.get<{ from: string, to: string }[]>('user_redirections');
|
|
for (const redirection of userRedirections) {
|
|
if (owner === redirection.from) {
|
|
return res.redirect(Controller.route('get-repo-release-file', [redirection.to, name, file], req.query as ParsedUrlQueryInput));
|
|
}
|
|
}
|
|
|
|
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 () => {
|
|
if (r.statusCode === 404) return next(new NotFoundHttpError('file', req.url));
|
|
|
|
try {
|
|
const releases = JSON.parse(data);
|
|
|
|
if (file) {
|
|
for (const release of releases) {
|
|
for (const asset of release.assets) {
|
|
if (asset.name === file) {
|
|
logger.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 {
|
|
logger.debug('List files');
|
|
return res.render('list-files', {
|
|
owner: owner,
|
|
name: name,
|
|
releases: releases,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
});
|
|
});
|
|
httpRequest.on('error', err => {
|
|
logger.error(err);
|
|
});
|
|
httpRequest.end();
|
|
}
|
|
|
|
protected async download(
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction,
|
|
downloadProperties: DownloadProperties,
|
|
): Promise<void> {
|
|
// 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<void>((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);
|
|
}
|
|
|
|
logger.debug('Download', assetPath, downloadProperties.asset.name);
|
|
|
|
sendRanges(async _ => {
|
|
const filePath = assetPath;
|
|
const getStream: SendRangeGetStreamFn = range => fs.createReadStream(filePath, range);
|
|
const type = mime.getType(downloadProperties.asset.name) || 'application/json';
|
|
const stats = await promisify(fs.stat)(filePath);
|
|
|
|
return {getStream, type, size: stats.size};
|
|
}, {
|
|
maxRanges: 1024,
|
|
})(req, res, (err: unknown) => {
|
|
if (err) return next(err);
|
|
|
|
logger.info('Fallback to express download.');
|
|
|
|
// 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;
|
|
};
|
|
};
|