Merge pull request #122 from Ravinou/develop

v2.1.0
This commit is contained in:
Ravinou 2024-01-01 16:01:55 +01:00 committed by GitHub
commit 8a771df290
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 290 additions and 126 deletions

41
.env.sample Normal file
View file

@ -0,0 +1,41 @@
## Required variables section ##
# Host port mappings
WEB_SERVER_PORT=3000
SSH_SERVER_PORT=2222
# Hostname and URL
FQDN=your.domain.com
NEXTAUTH_URL=https://your.domain.com
# Secrects
NEXTAUTH_SECRET=your-secret
CRONJOB_KEY=your-other-secret
# UID:GID must match the user and group ID of the host folders and must be > 1000
# If you want to use a different user than 1001:1001, you must rebuild the image yourself.
UID=1001
GID=1001
# Config and data folders (volume mounts)
# The host folders must be owned by the user with UID and GID specified above
CONFIG_PATH=./config
SSH_PATH=./ssh
SSH_HOST=./ssh_host
BORG_REPOSITORY_PATH=./repos
TMP_PATH=./tmp
LOGS_PATH=./logs
## Optional variables section ##
# LAN feature
FQDN_LAN=
SSH_SERVER_PORT_LAN=
# SMTP server settings
MAIL_SMTP_FROM=
MAIL_SMTP_HOST=
MAIL_SMTP_PORT=
MAIL_SMTP_LOGIN=
MAIL_SMTP_PWD=
MAIL_REJECT_SELFSIGNED_TLS=

View file

@ -0,0 +1,29 @@
name: Build and Push Docker Image for Develop Branch
on:
push:
branches:
- 'develop'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: borgwarehouse/borgwarehouse:develop

View file

@ -1,34 +1,29 @@
name: Build and Push Docker Image name: Build and Push Docker Image
on: on:
push: push:
branches: branches:
- 'main' - 'main'
jobs: jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout uses: actions/checkout@v4
uses: actions/checkout@v4 - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
name: Set up QEMU - name: Set up Docker Buildx
uses: docker/setup-qemu-action@v3 uses: docker/setup-buildx-action@v3
- - name: Login to Docker Hub
name: Set up Docker Buildx uses: docker/login-action@v3
uses: docker/setup-buildx-action@v3 with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
name: Login to Docker Hub password: ${{ secrets.DOCKERHUB_TOKEN }}
uses: docker/login-action@v3 - name: Build and push
with: uses: docker/build-push-action@v5
username: ${{ secrets.DOCKERHUB_USERNAME }} with:
password: ${{ secrets.DOCKERHUB_TOKEN }} context: .
- push: true
name: Build and push platforms: linux/amd64,linux/arm64,linux/arm/v7
uses: docker/build-push-action@v5 tags: borgwarehouse/borgwarehouse:latest
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm
tags: borgwarehouse/borgwarehouse:latest

View file

@ -1,38 +1,32 @@
name: Build and Push Docker Image on Release name: Build and Push Docker Image on Release
on: on:
release: release:
types: types:
- published - published
jobs: jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout uses: actions/checkout@v4
uses: actions/checkout@v4 - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
name: Set up QEMU - name: Set up Docker Buildx
uses: docker/setup-qemu-action@v3 uses: docker/setup-buildx-action@v3
- - name: Login to Docker Hub
name: Set up Docker Buildx uses: docker/login-action@v3
uses: docker/setup-buildx-action@v3 with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
name: Login to Docker Hub password: ${{ secrets.DOCKERHUB_TOKEN }}
uses: docker/login-action@v3 - name: Get Release Tag
with: id: get_release_tag
username: ${{ secrets.DOCKERHUB_USERNAME }} run: echo "::set-output name=TAG::${{ github.event.release.tag_name }}"
password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push
- uses: docker/build-push-action@v5
name: Get Release Tag with:
id: get_release_tag context: .
run: echo "::set-output name=TAG::${{ github.event.release.tag_name }}" push: true
- platforms: linux/amd64,linux/arm64,linux/arm/v7
name: Build and push tags: borgwarehouse/borgwarehouse:${{ steps.get_release_tag.outputs.TAG }}
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: borgwarehouse/borgwarehouse:${{ steps.get_release_tag.outputs.TAG }}

View file

@ -18,4 +18,4 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build Docker Container - name: Build Docker Container
run: | run: |
docker buildx build --platform linux/amd64,linux/arm64 -t borgwarehouse:pr-${{ github.event.pull_request.number }} . docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t borgwarehouse:pr-${{ github.event.pull_request.number }} .

View file

@ -50,11 +50,13 @@ export default function QuickCommands(props) {
<div className={classes.copyValid}>Copied !</div> <div className={classes.copyValid}>Copied !</div>
) : ( ) : (
<div className={classes.tooltip}> <div className={classes.tooltip}>
ssh://{wizardEnv.UNIX_USER}@ ssh://{wizardEnv.UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
{FQDN}:{SSH_SERVER_PORT}/./
{props.repositoryName} {props.repositoryName}
</div> </div>
)} )}
{props.lanCommand && <div className={classes.lanBadge}>LAN</div>}
<div className={classes.icons}> <div className={classes.icons}>
<button onClick={handleCopy} className={classes.copyButton}> <button onClick={handleCopy} className={classes.copyButton}>
<IconCopy color='#65748b' stroke={1.25} /> <IconCopy color='#65748b' stroke={1.25} />

View file

@ -7,7 +7,7 @@
.icons { .icons {
position: relative; position: relative;
bottom: 14px; bottom: 13px;
} }
.quickSetting { .quickSetting {
@ -16,6 +16,15 @@
opacity: 1; opacity: 1;
} }
.lanBadge {
border-radius: 5px;
border: 1px solid #6d4aff;
color: #6d4aff;
font-size: 0.9em;
padding: 2px 5px;
margin-right: 8px;
}
.tooltip { .tooltip {
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
@ -91,6 +100,15 @@
opacity: 0; opacity: 0;
} }
.container:hover .lanBadge {
visibility: hidden;
opacity: 0;
width: 0;
height: 0;
margin: 0;
padding: 0;
}
@media all and (max-width: 1000px) { @media all and (max-width: 1000px) {
.container { .container {
display: none; display: none;

View file

@ -6,6 +6,7 @@ import {
IconInfoCircle, IconInfoCircle,
IconChevronDown, IconChevronDown,
IconChevronUp, IconChevronUp,
IconBellOff,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import timestampConverter from '../../helpers/functions/timestampConverter'; import timestampConverter from '../../helpers/functions/timestampConverter';
import StorageBar from '../UI/StorageBar/StorageBar'; import StorageBar from '../UI/StorageBar/StorageBar';
@ -53,22 +54,33 @@ export default function Repo(props) {
setDisplayDetails(boolean); setDisplayDetails(boolean);
}; };
//Status indicator
const statusIndicator = () => {
return props.status
? classes.statusIndicatorGreen
: classes.statusIndicatorRed;
};
//Alert indicator
const alertIndicator = () => {
if (props.alert === 0) {
return (
<div className={classes.alertIcon}>
<IconBellOff size={16} color='grey' />
</div>
);
}
};
return ( return (
<> <>
{displayDetails ? ( {displayDetails ? (
<> <>
<div className={classes.RepoOpen}> <div className={classes.RepoOpen}>
<div className={classes.openFlex}> <div className={classes.openFlex}>
{props.status ? ( <div className={statusIndicator()} />
<div
className={classes.statusIndicatorGreen}
></div>
) : (
<div
className={classes.statusIndicatorRed}
></div>
)}
<div className={classes.alias}>{props.alias}</div> <div className={classes.alias}>{props.alias}</div>
{alertIndicator()}
{props.comment && ( {props.comment && (
<div className={classes.comment}> <div className={classes.comment}>
<IconInfoCircle size={16} color='grey' /> <IconInfoCircle size={16} color='grey' />
@ -141,16 +153,9 @@ export default function Repo(props) {
<> <>
<div className={classes.RepoClose}> <div className={classes.RepoClose}>
<div className={classes.closeFlex}> <div className={classes.closeFlex}>
{props.status ? ( <div className={statusIndicator()} />
<div
className={classes.statusIndicatorGreen}
></div>
) : (
<div
className={classes.statusIndicatorRed}
></div>
)}
<div className={classes.alias}>{props.alias}</div> <div className={classes.alias}>{props.alias}</div>
{alertIndicator()}
{props.comment && ( {props.comment && (
<div className={classes.comment}> <div className={classes.comment}>
<IconInfoCircle size={16} color='#637381' /> <IconInfoCircle size={16} color='#637381' />

View file

@ -148,6 +148,15 @@
} }
} }
/* Alert icon */
.alertIcon {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 10px;
}
/* GENERAL */ /* GENERAL */
.alias { .alias {
font-weight: bold; font-weight: bold;

View file

@ -116,6 +116,7 @@ export default function RepoList() {
alias={repo.alias} alias={repo.alias}
status={repo.status} status={repo.status}
lastSave={repo.lastSave} lastSave={repo.lastSave}
alert={repo.alert}
repositoryName={repo.repositoryName} repositoryName={repo.repositoryName}
storageSize={repo.storageSize} storageSize={repo.storageSize}
storageUsed={repo.storageUsed} storageUsed={repo.storageUsed}

View file

@ -23,6 +23,7 @@ export default function RepoManage(props) {
} = useForm({ mode: 'onChange' }); } = useForm({ mode: 'onChange' });
//List of possible times for alerts //List of possible times for alerts
const alertOptions = [ const alertOptions = [
{ value: 0, label: 'Disabled' },
{ value: 3600, label: '1 hour' }, { value: 3600, label: '1 hour' },
{ value: 21600, label: '6 hours' }, { value: 21600, label: '6 hours' },
{ value: 43200, label: '12 hours' }, { value: 43200, label: '12 hours' },
@ -471,7 +472,7 @@ export default function RepoManage(props) {
x.value === x.value ===
targetRepo.alert targetRepo.alert
) )
: alertOptions[3] : alertOptions[4]
} }
control={control} control={control}
render={({ render={({

View file

@ -1,5 +1,8 @@
FROM node:20-bookworm-slim as base FROM node:20-bookworm-slim as base
ARG UID=1001
ARG GID=1001
# build stage # build stage
FROM base AS deps FROM base AS deps
@ -27,31 +30,26 @@ FROM base AS runner
ENV NODE_ENV production ENV NODE_ENV production
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
curl jq jc borgbackup openssh-server sudo cron && \ supervisor \
curl jq jc borgbackup openssh-server sudo cron rsyslog && \
apt-get clean && rm -rf /var/lib/apt/lists/* apt-get clean && rm -rf /var/lib/apt/lists/*
RUN echo "borgwarehouse ALL=(ALL) NOPASSWD: /usr/sbin/service ssh restart" >> /etc/sudoers RUN groupadd -g ${GID} borgwarehouse && useradd -m -u ${UID} -g ${GID} borgwarehouse
RUN echo "borgwarehouse ALL=(ALL) NOPASSWD: /usr/sbin/service cron restart" >> /etc/sudoers
RUN groupadd borgwarehouse
RUN useradd -m -g borgwarehouse borgwarehouse
RUN cp /etc/ssh/sshd_config /etc/ssh/moduli /home/borgwarehouse/ RUN cp /etc/ssh/sshd_config /etc/ssh/moduli /home/borgwarehouse/
WORKDIR /home/borgwarehouse/app WORKDIR /home/borgwarehouse/app
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker-bw-init.sh /app/LICENSE ./ COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/docker-bw-init.sh /app/LICENSE ./
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/helpers/shells ./helpers/shells COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/helpers/shells ./helpers/shells
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/.next/standalone ./ COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/.next/standalone ./
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/public ./public COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/public ./public
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/.next/static ./.next/static COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/.next/static ./.next/static
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/supervisord.conf ./
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/rsyslog.conf /etc/rsyslog.conf
USER borgwarehouse USER borgwarehouse
EXPOSE 3000 22 EXPOSE 3000 22
ENTRYPOINT ["./docker-bw-init.sh"] ENTRYPOINT ["./docker-bw-init.sh"]
CMD ["node", "server.js"]

View file

@ -6,25 +6,23 @@ services:
#build: #build:
# context: . # context: .
# dockerfile: Dockerfile # dockerfile: Dockerfile
# args:
# - UID=${UID}
# - GID=${GID}
image: borgwarehouse/borgwarehouse image: borgwarehouse/borgwarehouse
# UID:GID must match the user and group ID of the host folders and must be > 1000 user: '${UID:?UID variable missing}:${GID:?GID variable missing}'
user: '1001:1001'
ports: ports:
- '3000:3000' - '${WEB_SERVER_PORT:?WEB_SERVER_PORT variable missing}:3000'
- '2222:22' - '${SSH_SERVER_PORT:?SSH_SERVER_PORT variable missing}:22'
environment: env_file:
- NEXTAUTH_URL=https://your.domain.com - .env
- NEXTAUTH_SECRET=your-secret
- CRONJOB_KEY=your-other-secret
# The SSH_SERVER_PORT must match the port exposed above
- SSH_SERVER_PORT=2222
- FQDN=your.domain.com
volumes: volumes:
# The host folders must be owned by the user with UID and GID specified above - ${CONFIG_PATH:?CONFIG_PATH variable missing}:/home/borgwarehouse/app/config
- <host-folder>/config:/home/borgwarehouse/app/config - ${SSH_PATH:?SSH_PATH variable missing}:/home/borgwarehouse/.ssh
- <host-folder>/ssh:/home/borgwarehouse/.ssh - ${SSH_HOST:?SSH_HOST variable missing}:/etc/ssh
- <host-folder>/ssh_host:/etc/ssh - ${BORG_REPOSITORY_PATH:?BORG_REPOSITORY_PATH variable missing}:/home/borgwarehouse/repos
- <host-folder>/repos:/home/borgwarehouse/repos - ${TMP_PATH:?TMP_PATH variable missing}:/home/borgwarehouse/tmp
- ${LOGS_PATH:?LOGS_PATH variable missing}:/home/borgwarehouse/logs
# Apprise is used to send notifications, it's optional. http://apprise:8000 is the URL to use in BorgWarehouse. # Apprise is used to send notifications, it's optional. http://apprise:8000 is the URL to use in BorgWarehouse.
apprise: apprise:
container_name: apprise container_name: apprise

View file

@ -47,12 +47,6 @@ check_repos_directory() {
fi fi
} }
add_cron_job() {
print_green "Adding cron job..."
local CRON_JOB="* * * * * curl --request POST --url 'http://$HOSTNAME:3000/api/cronjob/checkStatus' --header 'Authorization: Bearer $CRONJOB_KEY'; curl --request POST --url 'http://$HOSTNAME:3000/api/cronjob/getStorageUsed' --header 'Authorization: Bearer $CRONJOB_KEY'"
echo "$CRON_JOB" | crontab -u borgwarehouse -
}
get_SSH_fingerprints() { get_SSH_fingerprints() {
print_green "Getting SSH fingerprints..." print_green "Getting SSH fingerprints..."
RSA_FINGERPRINT=$(ssh-keygen -lf /etc/ssh/ssh_host_rsa_key | awk '{print $2}') RSA_FINGERPRINT=$(ssh-keygen -lf /etc/ssh/ssh_host_rsa_key | awk '{print $2}')
@ -82,10 +76,6 @@ init_ssh_server
check_ssh_directory check_ssh_directory
create_authorized_keys_file create_authorized_keys_file
check_repos_directory check_repos_directory
add_cron_job
get_SSH_fingerprints get_SSH_fingerprints
sudo service ssh restart exec supervisord -c /home/borgwarehouse/app/supervisord.conf
sudo service cron restart
exec "$@"

40
docker/rsyslog.conf Normal file
View file

@ -0,0 +1,40 @@
# rsyslog configuration file
$WorkDirectory /home/borgwarehouse/tmp
$FileOwner borgwarehouse
$FileGroup borgwarehouse
$FileCreateMode 0640
$DirCreateMode 0755
$Umask 0022
$RepeatedMsgReduction on
module(load="imfile" PollingInterval="10")
input(type="imfile"
File="/home/borgwarehouse/tmp/borgwarehouse.log"
Tag="BorgWarehouse"
Severity="info"
Facility="local7"
ruleset="bwLogs")
input(type="imfile"
File="/home/borgwarehouse/tmp/sshd.log"
Tag="sshd"
Severity="info"
Facility="local7"
ruleset="sshdLogs")
$template myFormat,"%timegenerated:::date-rfc3339% %syslogtag% %msg%\n"
ruleset(name="bwLogs") {
action(type="omfile"
File="/home/borgwarehouse/logs/borgwarehouse.log"
Template="myFormat")
}
ruleset(name="sshdLogs") {
action(type="omfile"
File="/home/borgwarehouse/logs/sshd.log"
Template="myFormat")
}

24
docker/supervisord.conf Normal file
View file

@ -0,0 +1,24 @@
[supervisord]
nodaemon=true
logfile=/home/borgwarehouse/logs/supervisord.log
loglevel=error
pidfile=/home/borgwarehouse/tmp/supervisord.pid
logfile_maxbytes=10MB
logfile_backups=5
[program:sshd]
command=/usr/sbin/sshd -D -e -o PidFile=/home/borgwarehouse/tmp/sshd.pid -o SyslogFacility=AUTH -o LogLevel=INFO -o PasswordAuthentication=no -o ChallengeResponseAuthentication=no -o UsePAM=no -o PermitRootLogin=no
stdout_logfile=/home/borgwarehouse/tmp/sshd.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
redirect_stderr=true
[program:borgwarehouse]
command=/usr/local/bin/node server.js
stdout_logfile=/home/borgwarehouse/tmp/borgwarehouse.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
redirect_stderr=true
[program:rsyslogd]
command=rsyslogd -n -i /home/borgwarehouse/tmp/rsyslogd.pid -f /etc/rsyslog.conf

View file

@ -26,6 +26,8 @@ export const authOptions = {
password: password:
'$2a$12$20yqRnuaDBH6AE0EvIUcEOzqkuBtn1wDzJdw2Beg8w9S.vEqdso0a', '$2a$12$20yqRnuaDBH6AE0EvIUcEOzqkuBtn1wDzJdw2Beg8w9S.vEqdso0a',
roles: ['admin'], roles: ['admin'],
emailAlert: false,
appriseAlert: false,
}, },
]) ])
); );

View file

@ -102,6 +102,7 @@ export default async function handler(req, res) {
for (let index in newRepoList) { for (let index in newRepoList) {
if ( if (
!newRepoList[index].status && !newRepoList[index].status &&
newRepoList[index].alert !== 0 &&
(!newRepoList[index].lastStatusAlertSend || (!newRepoList[index].lastStatusAlertSend ||
date - newRepoList[index].lastStatusAlertSend > 90000) date - newRepoList[index].lastStatusAlertSend > 90000)
) { ) {

View file

@ -19,7 +19,7 @@ export default async function handler(req, res) {
const { alias, sshPublicKey, size, comment, alert, lanCommand } = const { alias, sshPublicKey, size, comment, alert, lanCommand } =
req.body; req.body;
//We check that we receive data for each variable. Only "comment" and "lanCommand" are optional in the form. //We check that we receive data for each variable. Only "comment" and "lanCommand" are optional in the form.
if (!alias || !sshPublicKey || !size || !alert) { if (!alias || !sshPublicKey || !size || (!alert && alert !== 0)) {
//If a variable is empty. //If a variable is empty.
res.status(422).json({ res.status(422).json({
message: 'Unexpected data', message: 'Unexpected data',

View file

@ -19,7 +19,7 @@ export default async function handler(req, res) {
const { alias, sshPublicKey, size, comment, alert, lanCommand } = const { alias, sshPublicKey, size, comment, alert, lanCommand } =
req.body; req.body;
//We check that we receive data for each variable. Only "comment" and "lanCommand" are optional in the form. //We check that we receive data for each variable. Only "comment" and "lanCommand" are optional in the form.
if (!alias || !sshPublicKey || !size || !alert) { if (!alias || !sshPublicKey || !size || (!alert && alert !== 0)) {
//If a variable is empty. //If a variable is empty.
res.status(422).json({ res.status(422).json({
message: 'Unexpected data', message: 'Unexpected data',

View file

@ -0,0 +1,16 @@
import packageInfo from '../../../package.json';
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
res.status(200).json({ version: packageInfo.version });
return;
} catch (error) {
res.status(500).json({
status: 500,
message: 'API error, contact the administrator !',
});
return;
}
}
}