Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
62c9ba4afd | |||
0c00ef85b9 | |||
827d3c3f65 | |||
ebed818824 | |||
6d34829263 | |||
6a2931e699 | |||
7870e4aa3c | |||
1cdd118806 | |||
145d8df70a | |||
2726fd16cc | |||
8612b0f33a | |||
bda464ab16 | |||
e7165edafd | |||
16394c0d4c | |||
d2a67a12ee | |||
1043690a5d | |||
46168c0e90 | |||
5eaebd5d12 | |||
9f3a541c3e | |||
a4566067c5 | |||
c519b8a884 | |||
18f3f93db6 | |||
bea4325aa4 | |||
31601cadd0 | |||
f7c9686269 | |||
497dcc71dc | |||
5c002f7ff2 | |||
00e21b27a5 | |||
44b32401a7 | |||
edccef4e42 | |||
db3cf2178a | |||
8aad521014 | |||
19224d592a |
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@ node_modules
|
|||||||
public
|
public
|
||||||
dist
|
dist
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
|
src/package.json
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
19
assets/sass/responsivity_tools.scss
Normal file
19
assets/sass/responsivity_tools.scss
Normal 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;
|
||||||
|
}
|
@ -12,4 +12,7 @@
|
|||||||
cache: false,
|
cache: false,
|
||||||
},
|
},
|
||||||
assets_base_dir: 'downloads',
|
assets_base_dir: 'downloads',
|
||||||
|
user_redirections: [
|
||||||
|
{from: 'arisu', to: 'ashpie'}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
@ -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: [],
|
||||||
}
|
}
|
||||||
|
30
package.json
30
package.json
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
25
src/controllers/HomeController.ts
Normal file
25
src/controllers/HomeController.ts
Normal 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'));
|
||||||
|
}
|
||||||
|
}
|
10
src/main.ts
10
src/main.ts
@ -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
24
src/types/send-ranges.d.ts
vendored
Normal 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;
|
||||||
|
}
|
@ -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": [
|
||||||
|
@ -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": [
|
||||||
|
Loading…
Reference in New Issue
Block a user