Compare commits

..

33 Commits

Author SHA1 Message Date
62c9ba4afd Version 1.3.0 2021-03-06 18:38:44 +01:00
0c00ef85b9 Add global user redirections 2021-03-06 18:38:29 +01:00
827d3c3f65 Version 1.2.4 2021-03-06 18:18:18 +01:00
ebed818824 Respond with a 404 when requested file is not found instead of crashing 2021-03-06 18:17:49 +01:00
6d34829263 Switch to new swaf logger 2021-03-06 18:17:21 +01:00
6a2931e699 Install and upgrade dependencies 2021-03-06 18:17:03 +01:00
7870e4aa3c Merge remote-tracking branch 'boilerplate/master' into develop 2021-03-06 18:16:46 +01:00
1cdd118806 Merge branch 'develop' 2021-01-26 13:37:58 +01:00
145d8df70a css: move menu .tip visibility to layout.scss 2021-01-26 13:37:41 +01:00
2726fd16cc Merge branch 'develop' 2021-01-26 13:17:51 +01:00
8612b0f33a css: move lighter theme to layout.scss and make header css mobile first 2021-01-26 13:00:38 +01:00
bda464ab16 css: move a hover color and td.actions improvements to layout.scss 2021-01-26 13:00:23 +01:00
e7165edafd Add hover color on warning buttons 2021-01-26 11:43:42 +01:00
16394c0d4c Set icon size for titles 2021-01-26 11:43:13 +01:00
d2a67a12ee Fix subpanel leaking out of their container 2021-01-26 11:42:24 +01:00
1043690a5d Add bigger container max-width for desktop view 2021-01-26 11:38:26 +01:00
46168c0e90 Merge branch 'develop' 2021-01-25 13:20:25 +01:00
5eaebd5d12 Upgrade dependencies and bump swaf to ^0.23.0 2021-01-25 13:18:49 +01:00
9f3a541c3e Add maildev as a dev dependency 2021-01-25 11:44:22 +01:00
a4566067c5 Version 1.2.3 2020-11-20 15:34:32 +01:00
c519b8a884 Use send-range and mime packages to send multipart/byteranges responses 2020-11-20 15:34:17 +01:00
18f3f93db6 Revert "Properly set content-type when Range header is present"
This reverts commit 5c002f7ff2.
2020-11-20 13:46:52 +01:00
bea4325aa4 Version 1.2.2 2020-11-20 13:42:24 +01:00
31601cadd0 Fix missing promise type 2020-11-20 13:42:09 +01:00
f7c9686269 Version 1.2.1 2020-11-20 13:40:16 +01:00
497dcc71dc Upgrade dependencies 2020-11-20 13:40:07 +01:00
5c002f7ff2 Properly set content-type when Range header is present 2020-11-20 13:38:47 +01:00
00e21b27a5 Make file download unavailable while caching is in progress 2020-11-20 13:38:32 +01:00
44b32401a7 Merge branch 'develop' 2020-07-16 09:25:10 +02:00
edccef4e42 Merge branch 'develop' into 'master'
Merge latest changes

See merge request ArisuOngaku/wms-boilerplate!4
2020-07-12 10:17:56 +00:00
db3cf2178a Merge branch 'boilerplate-icon-only-menu' into 'master'
Boilerplate icon only menu

See merge request ArisuOngaku/wms-boilerplate!3
2020-07-06 10:51:46 +00:00
8aad521014 Fix last menu item tip overflowing out of document 2020-07-06 12:50:39 +02:00
19224d592a Add optional icon only menu layout 2020-07-06 12:50:25 +02:00
15 changed files with 3489 additions and 3001 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ node_modules
public public
dist dist
yarn-error.log yarn-error.log
src/package.json

View File

@ -6,9 +6,10 @@ $secondaryForeground: $primaryForeground;
$backgroundColor: darken($primary, 4%); $backgroundColor: darken($primary, 4%);
$defaultTextColor: #ffffff; $defaultTextColor: #ffffff;
$headerBackground: darken($primary, 7.5%); $headerBackground: transparent;
$footerBackground: lighten($headerBackground, 1%); $headerContainer: true;
$panelBackground: lighten($headerBackground, 1%); $footerBackground: transparent;
$panelBackground: darken($backgroundColor, 3.2%);
$inputBackground: darken($panelBackground, 4%); $inputBackground: darken($panelBackground, 4%);
$info: #4499ff; $info: #4499ff;
@ -29,3 +30,4 @@ $errorColor: desaturate($errorText, 50%);
// Responsivity // Responsivity
$mobileThreshold: 632px; $mobileThreshold: 632px;
$desktopThreshold: 940px;

View File

@ -1,5 +1,6 @@
@import "vars"; @import "vars";
@import 'fonts'; @import 'fonts';
@import "responsivity_tools";
* { * {
box-sizing: border-box; box-sizing: border-box;
@ -69,21 +70,31 @@ body {
body > header { body > header {
z-index: 50; z-index: 50;
display: flex; display: flex;
flex-direction: row; flex-direction: row-reverse;
justify-content: space-between; justify-content: space-between;
align-items: center;
$headerHeight: 64px; $headerHeight: 64px;
height: $headerHeight; height: $headerHeight;
line-height: $headerHeight; line-height: $headerHeight;
background-color: $headerBackground; background: $headerBackground;
@if $headerContainer {
@include container;
}
@media (max-width: $mobileThreshold) {
padding: 0;
}
.logo { .logo {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center;
padding: 0 24px 0 16px; padding: 0 16px 0 8px;
font-size: 32px; font-size: 24px;
color: $defaultTextColor; color: $defaultTextColor;
&:hover { &:hover {
@ -91,34 +102,48 @@ body > header {
} }
img { img {
width: $headerHeight; width: initial;
height: $headerHeight; height: calc(#{$headerHeight} - 16px);
margin-right: 16px; margin-right: 8px;
flex-shrink: 0;
} }
} }
nav { nav {
ul { > ul {
position: fixed;
z-index: -1;
top: 0;
left: 0;
height: 100%;
transform: translateX(-100%);
transition: transform ease-out 150ms;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: $headerHeight 8px 8px;
font-size: 20px; font-size: 20px;
background: $panelBackground;
li { li {
position: relative; position: relative;
list-style: none; list-style: none;
margin-top: 8px;
a, button { a, button {
position: relative; position: relative;
height: 64px;
margin: 0; margin: 0;
padding: 0 24px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
height: auto;
padding: 8px;
border-radius: 3px;
&:hover, &:active { &:hover, &:active {
&:not(button) { &:not(button) {
@ -127,13 +152,40 @@ body > header {
} }
.feather { .feather {
--icon-size: 24px; --icon-size: 16px;
}
.tip {
position: static;
visibility: visible;
opacity: 1;
display: block;
height: auto;
margin-left: 8px;
padding: 0 0 0 4px;
transform: none;
font-size: 16px;
line-height: 16px;
color: inherit;
text-transform: uppercase;
font-weight: inherit;
background: transparent;
}
&:hover {
.tip {
visibility: visible;
opacity: 1;
transition: opacity ease-out 100ms;
transition-delay: 150ms;
}
} }
} }
button { button {
margin: 8px; margin: 0;
padding: 24px;
height: 32px; height: 32px;
.feather { .feather {
@ -158,114 +210,117 @@ body > header {
} }
.dropdown { .dropdown {
position: absolute; position: initial;
z-index: -1;
top: 100%;
right: 0;
white-space: nowrap;
background: $headerBackground;
border-radius: 0 0 3px 3px;
a {
padding: 0 8px;
}
}
&:hover .dropdown {
display: block; display: block;
padding-left: 0;
} }
} }
> li:not(:first-child) {
border-top: 1px solid transparentize($defaultTextColor, 0.8);
padding-top: 8px;
}
&.open {
transform: translateX(0%);
box-shadow: 0 0 5px darken($panelBackground, 20%);
}
} }
#menu-button { #menu-button {
display: none; position: fixed;
} top: 0;
} left: 0;
display: block;
margin: 0;
padding: 0 16px;
line-height: $headerHeight;
@media (max-width: $mobileThreshold) { cursor: pointer;
flex-direction: row-reverse; background: transparent;
border-radius: 0;
.logo { .feather {
padding: 0 16px 0 8px; --icon-size: 28px;
font-size: 24px; margin: 0 8px;
img {
margin-right: 8px;
} }
} }
nav { hr {
#menu-button { border: 0;
display: block; border-bottom: 1px solid $defaultTextColor;
margin: 0; opacity: 0.2;
padding: 0 16px;
line-height: $headerHeight;
cursor: pointer;
background: transparent;
border-radius: 0;
.feather {
--icon-size: 28px;
margin: 0 8px;
}
}
> ul {
flex-direction: column;
position: absolute;
z-index: 10;
left: 0;
transform: translateX(-100%);
transition: transform ease-out 150ms;
background-color: $headerBackground;
&.open {
transform: translateX(0%);
}
li {
a, button {
.tip {
display: block;
margin-left: 8px;
text-transform: inherit;
font-weight: inherit;
}
}
.dropdown {
position: initial;
display: block;
padding-left: 32px;
}
}
}
} }
} }
@media (min-width: $mobileThreshold) { @media (min-width: $mobileThreshold) {
nav ul li { flex-direction: row;
a, button, .button {
@include tip; nav {
#menu-button {
display: none;
} }
&:last-child { ul {
a, button, .button { position: static;
.tip { flex-direction: row;
left: unset; transform: none;
right: 4px; padding: 0;
transform: none; background: transparent;
li {
margin-top: 0;
margin-left: 8px;
&:last-child {
a, button, .button {
.tip {
left: unset;
right: 4px;
transform: none;
}
}
} }
.dropdown {
position: absolute;
z-index: -1;
top: 100%;
right: 0;
display: none;
padding: 8px;
white-space: nowrap;
background: $panelBackground;
border-radius: 0 0 3px 3px;
box-shadow: 0 2px 2px transparentize(darken($panelBackground, 20%), 0.75);
border-top: 4px solid lighten($panelBackground, 5%);
li {
margin-left: 0;
&:not(:first-child) {
margin-top: 8px;
}
}
}
&:hover .dropdown {
display: block;
}
}
> li:not(:first-child) {
border-top: 0;
padding-top: 0;
} }
} }
} }
} }
} }
footer { body > footer {
padding: 8px; padding: 8px;
margin-top: 8px; margin-top: 8px;
text-align: center; text-align: center;
@ -338,7 +393,7 @@ a {
text-decoration: none; text-decoration: none;
&:hover { &:hover {
color: lighten($secondary, 10%); color: lighten($secondary, 30%);
} }
.feather.feather-external-link { .feather.feather-external-link {
@ -584,6 +639,10 @@ button, .button {
&.warning { &.warning {
background-color: $warningColor; background-color: $warningColor;
&:hover {
background-color: lighten($warningColor, 10%);
}
} }
&.error, &.danger { &.error, &.danger {
@ -603,6 +662,35 @@ button, .button {
} }
} }
// ---
// --- Tables
// ---
td.actions {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
form {
padding: 0;
display: inline;
}
button, .button {
margin: 0;
padding: 8px;
.feather {
margin-right: 0;
}
}
> *:not(:first-child) {
margin-left: 8px;
}
}
.data-table { .data-table {
width: 100%; width: 100%;
text-align: left; text-align: left;
@ -629,6 +717,10 @@ button, .button {
} }
} }
// ---
// --- Breadcrumb widget
// ---
.breadcrumb { .breadcrumb {
list-style: none; list-style: none;
display: flex; display: flex;
@ -642,6 +734,7 @@ button, .button {
} }
} }
// --- // ---
// --- Layout helpers // --- Layout helpers
// --- // ---
@ -649,24 +742,6 @@ button, .button {
text-align: center; text-align: center;
} }
@mixin container {
width: $mobileThreshold;
padding: 0 16px;
@media (min-width: $mobileThreshold) {
margin: 0 auto;
}
@media (max-width: $mobileThreshold) {
width: 100%;
padding: 0 8px;
}
}
.container {
@include container;
}
.panel { .panel {
position: relative; position: relative;
margin: 16px 0 48px; margin: 16px 0 48px;
@ -688,10 +763,14 @@ button, .button {
} }
.sub-panel { .sub-panel {
margin: 32px -18px; margin: 32px 0;
padding: 1px 16px; padding: 1px 16px;
border: 2px solid lighten($panelBackground, 4%); border: 2px solid lighten($panelBackground, 4%);
border-radius: 5px; border-radius: 5px;
form > & {
margin: 32px -18px;
}
} }
@ -715,6 +794,10 @@ button, .button {
stroke-linejoin: miter; stroke-linejoin: miter;
fill: none; fill: none;
vertical-align: middle; vertical-align: middle;
h1 > &, h2 > &, h3 > & {
--icon-size: 24px;
}
} }
// --- // ---

View File

@ -0,0 +1,19 @@
@import "vars";
@mixin container {
width: 100%;
padding: 0 8px;
@media (min-width: $mobileThreshold) {
margin: 0 auto;
padding: 0 16px;
}
@media (min-width: $desktopThreshold) {
width: $desktopThreshold;
}
}
.container {
@include container;
}

View File

@ -12,4 +12,7 @@
cache: false, cache: false,
}, },
assets_base_dir: 'downloads', assets_base_dir: 'downloads',
user_redirections: [
{from: 'arisu', to: 'ashpie'}
],
} }

View File

@ -2,4 +2,5 @@
log_level: "DEBUG", log_level: "DEBUG",
db_log_level: "ERROR", db_log_level: "ERROR",
public_url: "https://update.eternae.ink", public_url: "https://update.eternae.ink",
user_redirections: [],
} }

View File

@ -1,31 +1,32 @@
{ {
"name": "eternae.ink.update", "name": "eternae.ink.update",
"version": "1.2.0", "version": "1.3.0",
"description": "A simple redirection to serve a gitea instance's repositories' latest release files as an http file server. (302 redirections)", "description": "A simple redirection to serve a gitea instance's repositories' latest release files as an http file server. (302 redirections)",
"repository": "https://eternae.ink/arisu/update.eternae.ink", "repository": "https://eternae.ink/arisu/update.eternae.ink",
"author": "Alice Gaudon <alice@gaudon.pro>", "author": "Alice Gaudon <alice@gaudon.pro>",
"main": "dist/src/main.js", "main": "dist/main.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dist-webpack": "webpack --mode production", "test": "jest --verbose --runInBand",
"clean": "(test ! -d dist || rm -r dist)", "clean": "(test ! -d dist || rm -r dist)",
"prepareSources": "cp package.json src/",
"compile": "yarn clean && tsc", "compile": "yarn clean && tsc",
"build": "yarn compile && yarn dist-webpack", "build": "yarn prepareSources && yarn compile && webpack --mode production",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx", "dev": "yarn prepareSources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
"dev": "concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"", "start": "yarn build && node",
"start": "yarn build && node dist/src/main.js", "lint": "eslint . --ext .js,.jsx,.ts,.tsx"
"test": "jest --verbose --runInBand"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5", "@babel/preset-env": "^7.9.5",
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.14.0",
"@types/config": "^0.0.36", "@types/config": "^0.0.38",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/express-session": "^1.17.0", "@types/express-session": "^1.17.0",
"@types/feather-icons": "^4.7.0", "@types/feather-icons": "^4.7.0",
"@types/formidable": "^1.0.31", "@types/formidable": "^1.0.31",
"@types/jest": "^26.0.4", "@types/jest": "^26.0.4",
"@types/mime": "^2.0.3",
"@types/mysql": "^2.15.15", "@types/mysql": "^2.15.15",
"@types/node": "^14.6.3", "@types/node": "^14.6.3",
"@types/nodemailer": "^6.4.0", "@types/nodemailer": "^6.4.0",
@ -34,7 +35,7 @@
"@typescript-eslint/eslint-plugin": "^4.3.0", "@typescript-eslint/eslint-plugin": "^4.3.0",
"@typescript-eslint/parser": "^4.3.0", "@typescript-eslint/parser": "^4.3.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"concurrently": "^5.1.0", "concurrently": "^6.0.0",
"css-loader": "^5.0.0", "css-loader": "^5.0.0",
"eslint": "^7.10.0", "eslint": "^7.10.0",
"feather-icons": "^4.28.0", "feather-icons": "^4.28.0",
@ -43,13 +44,14 @@
"imagemin-gifsicle": "^7.0.0", "imagemin-gifsicle": "^7.0.0",
"imagemin-mozjpeg": "^9.0.0", "imagemin-mozjpeg": "^9.0.0",
"imagemin-pngquant": "^9.0.0", "imagemin-pngquant": "^9.0.0",
"imagemin-svgo": "^8.0.0", "imagemin-svgo": "^9.0.0",
"img-loader": "^3.0.1", "img-loader": "^3.0.1",
"jest": "^26.1.0", "jest": "^26.1.0",
"maildev": "^1.1.0",
"mini-css-extract-plugin": "^1.2.1", "mini-css-extract-plugin": "^1.2.1",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"nodemon": "^2.0.3", "nodemon": "^2.0.3",
"sass-loader": "^10.0.1", "sass-loader": "^11.0.1",
"terser-webpack-plugin": "^5.0.3", "terser-webpack-plugin": "^5.0.3",
"ts-jest": "^26.1.1", "ts-jest": "^26.1.1",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
@ -60,6 +62,8 @@
"dependencies": { "dependencies": {
"config": "^3.3.1", "config": "^3.3.1",
"express": "^4.17.1", "express": "^4.17.1",
"swaf": "^0.22.5" "mime": "^2.4.6",
"send-ranges": "^4.0.0",
"swaf": "^0.23.0"
} }
} }

View File

@ -4,7 +4,7 @@ import ExpressAppComponent from "swaf/components/ExpressAppComponent";
import LogRequestsComponent from "swaf/components/LogRequestsComponent"; import LogRequestsComponent from "swaf/components/LogRequestsComponent";
import GiteaRepoLatestReleaseController from "./controllers/GiteaRepoLatestReleaseController"; import GiteaRepoLatestReleaseController from "./controllers/GiteaRepoLatestReleaseController";
import NunjucksComponent from "swaf/components/NunjucksComponent"; import NunjucksComponent from "swaf/components/NunjucksComponent";
import packageJson = require('../package.json'); import packageJson = require('./package.json');
export default class App extends Application { export default class App extends Application {
public constructor( public constructor(

View File

@ -2,11 +2,14 @@ import Controller from "swaf/Controller";
import {NextFunction, Request, Response} from "express"; import {NextFunction, Request, Response} from "express";
import * as https from "https"; import * as https from "https";
import config from "config"; import config from "config";
import {log} from "swaf/Logger"; import {NotFoundHttpError, ServiceUnavailableHttpError} from "swaf/HttpError";
import {NotFoundHttpError} from "swaf/HttpError";
import * as fs from "fs"; import * as fs from "fs";
import {promisify} from "util"; import {promisify} from "util";
import path from "path"; 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 const ASSETS_BASE_DIR = config.get<string>('assets_base_dir');
@ -16,10 +19,18 @@ export default class GiteaRepoLatestReleaseController extends Controller {
} }
protected async getFile(req: Request, res: Response, next: NextFunction): Promise<void> { protected async getFile(req: Request, res: Response, next: NextFunction): Promise<void> {
log.info('Serving ' + req.path + ' ...'); logger.info('Serving ' + req.path + ' ...');
const {owner, name, file} = req.params; const {owner, name, file} = req.params;
if (!owner || !name) return next(); 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`, { const httpRequest = https.get(`${config.get('gitea_instance_url')}/api/v1/repos/${owner}/${name}/releases`, {
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -30,6 +41,8 @@ export default class GiteaRepoLatestReleaseController extends Controller {
data += c; data += c;
}); });
r.on('end', async () => { r.on('end', async () => {
if (r.statusCode === 404) return next(new NotFoundHttpError('file', req.url));
try { try {
const releases = JSON.parse(data); const releases = JSON.parse(data);
@ -37,7 +50,7 @@ export default class GiteaRepoLatestReleaseController extends Controller {
for (const release of releases) { for (const release of releases) {
for (const asset of release.assets) { for (const asset of release.assets) {
if (asset.name === file) { if (asset.name === file) {
log.debug('Download', asset.browser_download_url); logger.debug('Download', asset.browser_download_url);
return await this.download(req, res, next, { return await this.download(req, res, next, {
repo: { repo: {
owner: owner, owner: owner,
@ -55,7 +68,7 @@ export default class GiteaRepoLatestReleaseController extends Controller {
} }
throw new NotFoundHttpError('Asset', req.url); throw new NotFoundHttpError('Asset', req.url);
} else { } else {
log.debug('List files'); logger.debug('List files');
return res.render('list-files', { return res.render('list-files', {
owner: owner, owner: owner,
name: name, name: name,
@ -68,7 +81,7 @@ export default class GiteaRepoLatestReleaseController extends Controller {
}); });
}); });
httpRequest.on('error', err => { httpRequest.on('error', err => {
log.error(err); logger.error(err);
}); });
httpRequest.end(); httpRequest.end();
} }
@ -85,11 +98,16 @@ export default class GiteaRepoLatestReleaseController extends Controller {
} }
const assetPath = path.resolve(ASSETS_BASE_DIR, '' + downloadProperties.asset.id); const assetPath = path.resolve(ASSETS_BASE_DIR, '' + downloadProperties.asset.id);
const tmpAssetPath = assetPath + '.tmp';
// Download asset if it doesn't exist // Download asset if it doesn't exist
if (!await promisify(fs.exists)(assetPath)) { if (!await promisify(fs.exists)(assetPath)) {
const file = fs.createWriteStream(assetPath); if (await promisify(fs.exists)(tmpAssetPath)) {
await new Promise((resolve, reject) => { 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 => { const httpRequest = https.get(downloadProperties.asset.url, res => {
res.on('end', () => { res.on('end', () => {
resolve(); resolve();
@ -102,10 +120,28 @@ export default class GiteaRepoLatestReleaseController extends Controller {
httpRequest.end(); httpRequest.end();
}); });
file.close(); file.close();
await promisify(fs.rename)(tmpAssetPath, assetPath);
} }
// Respond logger.debug('Download', assetPath, downloadProperties.asset.name);
return res.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);
});
} }
} }

View File

@ -0,0 +1,25 @@
import Controller from "swaf/Controller";
import {Request, Response} from "express";
export default class HomeController extends Controller {
public routes(): void {
this.get('/', this.getHome, 'home');
this.get('/about', this.getAbout, 'about');
this.get('/back', this.goBack, 'about');
}
protected async getHome(req: Request, res: Response): Promise<void> {
res.render('home');
}
protected async getAbout(req: Request, res: Response): Promise<void> {
res.render('about');
}
/**
* This is to test and assert that swaf extended types are available
*/
protected async goBack(req: Request, res: Response): Promise<void> {
res.redirect(req.getPreviousUrl() || Controller.route('home'));
}
}

View File

@ -2,19 +2,19 @@ import {delimiter} from "path";
// Load config from specified path or default + swaf/config (default defaults) // Load config from specified path or default + swaf/config (default defaults)
process.env['NODE_CONFIG_DIR'] = process.env['NODE_CONFIG_DIR'] =
__dirname + '/../../node_modules/swaf/config/' __dirname + '/../node_modules/swaf/config/'
+ delimiter + delimiter
+ (process.env['NODE_CONFIG_DIR'] || __dirname + '/../../config/'); + (process.env['NODE_CONFIG_DIR'] || __dirname + '/../config/');
import {log} from "swaf/Logger"; import {logger} from "swaf/Logger";
import App from "./App"; import App from "./App";
import config from "config"; import config from "config";
(async () => { (async () => {
log.debug('Config path:', process.env['NODE_CONFIG_DIR']); logger.debug('Config path:', process.env['NODE_CONFIG_DIR']);
const app = new App(config.get<string>('listen_addr'), config.get<number>('port')); const app = new App(config.get<string>('listen_addr'), config.get<number>('port'));
await app.start(); await app.start();
})().catch(err => { })().catch(err => {
log.error(err); logger.error(err);
}); });

24
src/types/send-ranges.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
declare module 'send-ranges' {
import {NextFunction, Request, RequestHandler} from "express";
import {ReadStream} from "fs";
export default function (
fetchStream: ((req: Request) => Promise<SendRangeParams | null>),
options: SendRangeOptions = {},
): RequestHandler;
export type SendRangeOptions = {
beforeSend?: (info, next: NextFunction) => void,
maxRanges?: number,
};
export type SendRangeParams = {
getStream: SendRangeGetStreamFn;
type: string;
size: number;
};
export type SendRangeGetStreamFn = (range: {
start?: number;
end?: number;
}) => ReadStream;
}

View File

@ -2,6 +2,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "public/js", "outDir": "public/js",
"rootDir": "./assets",
"target": "ES6", "target": "ES6",
"strict": true, "strict": true,
"lib": [ "lib": [

View File

@ -3,6 +3,7 @@
"module": "CommonJS", "module": "CommonJS",
"esModuleInterop": true, "esModuleInterop": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "./src",
"target": "ES6", "target": "ES6",
"strict": true, "strict": true,
"lib": [ "lib": [

5987
yarn.lock

File diff suppressed because it is too large Load Diff