mirror of
https://github.com/caronc/apprise-api.git
synced 2024-12-04 22:10:42 +01:00
Healthcheck web improvements + PUID & PGID support added to Docker (#198)
This commit is contained in:
parent
6e57e33b8f
commit
c6b9c1161d
32
Dockerfile
32
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim as base
|
||||
FROM python:3.11-slim AS base
|
||||
|
||||
# set version label
|
||||
ARG BUILD_DATE
|
||||
@ -7,13 +7,13 @@ LABEL build_version="Apprise API version:- ${VERSION} Build-date:- ${BUILD_DATE}
|
||||
LABEL maintainer="Chris-Caron"
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV APPRISE_CONFIG_DIR /config
|
||||
ENV APPRISE_ATTACH_DIR /attach
|
||||
ENV APPRISE_PLUGIN_PATHS /plugin
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV APPRISE_CONFIG_DIR=/config
|
||||
ENV APPRISE_ATTACH_DIR=/attach
|
||||
ENV APPRISE_PLUGIN_PATHS=/plugin
|
||||
|
||||
FROM base as builder
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /build/
|
||||
|
||||
@ -41,11 +41,11 @@ RUN set -eux && \
|
||||
--no-binary cryptography \
|
||||
cryptography
|
||||
|
||||
FROM base as runtime
|
||||
FROM base AS runtime
|
||||
|
||||
# Install requirements and gunicorn
|
||||
COPY ./requirements.txt /etc/requirements.txt
|
||||
COPY --from=builder /build/*.whl .
|
||||
COPY --from=builder /build/*.whl ./
|
||||
RUN set -eux && \
|
||||
echo "Installing cryptography" && \
|
||||
pip3 install *.whl && \
|
||||
@ -55,6 +55,9 @@ RUN set -eux && \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y -qq \
|
||||
nginx && \
|
||||
echo "Installing tools" && \
|
||||
apt-get install -y -qq \
|
||||
sed && \
|
||||
echo "Cleaning up" && \
|
||||
apt-get --yes autoremove --purge && \
|
||||
apt-get clean --yes && \
|
||||
@ -73,16 +76,13 @@ WORKDIR /opt/apprise
|
||||
# Copy over Apprise API
|
||||
COPY apprise_api/ webapp
|
||||
|
||||
#
|
||||
# # Configuration Permissions (to run nginx as a non-root user)
|
||||
# Configuration Permissions (to run nginx as a non-root user)
|
||||
RUN umask 0002 && \
|
||||
mkdir -p /attach /config /plugin /run/apprise && \
|
||||
chown www-data:www-data -R /run/apprise /var/lib/nginx /attach /config /plugin
|
||||
touch /etc/nginx/server-override.conf && \
|
||||
touch /etc/nginx/location-override.conf
|
||||
|
||||
# Handle running as a non-root user (www-data is id/gid 33)
|
||||
USER www-data
|
||||
VOLUME /config
|
||||
VOLUME /attach
|
||||
VOLUME /plugin
|
||||
EXPOSE 8000
|
||||
CMD ["/usr/local/bin/supervisord", "-c", "/opt/apprise/webapp/etc/supervisord.conf"]
|
||||
CMD ["/opt/apprise/webapp/supervisord-startup"]
|
||||
|
104
README.md
104
README.md
@ -56,11 +56,17 @@ docker pull caronc/apprise:latest
|
||||
# setting APPRISE_STATEFUL_MODE to simple allows you to map your defined {key}
|
||||
# straight to a file found in the `/config` path. In simple home configurations
|
||||
# this is sometimes the ideal expectation.
|
||||
#
|
||||
# Set your User ID or Group ID if you wish to over-ride the default of 1000
|
||||
# in the below example, we make sure it runs as the user we created the container as
|
||||
|
||||
docker run --name apprise \
|
||||
-p 8000:8000 \
|
||||
-v /var/lib/apprise/config:/config \
|
||||
-v /var/lib/apprise/plugin:/plugin \
|
||||
-v /var/lib/apprise/attach:/attach \
|
||||
-e PUID=$(id -u) \
|
||||
-e PGID=$(id -g) \
|
||||
-v /path/to/local/config:/config \
|
||||
-v /path/to/local/plugin:/plugin \
|
||||
-v /path/to/local/attach:/attach \
|
||||
-e APPRISE_STATEFUL_MODE=simple \
|
||||
-e APPRISE_WORKER_COUNT=1 \
|
||||
-d caronc/apprise:latest
|
||||
@ -72,11 +78,17 @@ A common change one might make is to update the Dockerfile to point to the maste
|
||||
# Setup your environment the way you like
|
||||
docker build -t apprise/local:latest -f Dockerfile .
|
||||
|
||||
# Set up a directory you wish to store your configuration in:
|
||||
mkdir -p /etc/apprise
|
||||
|
||||
# Launch your instance
|
||||
docker run --name apprise \
|
||||
-p 8000:8000 \
|
||||
-e PUID=$(id -u) \
|
||||
-e PGID=$(id -g) \
|
||||
-e APPRISE_STATEFUL_MODE=simple \
|
||||
-e APPRISE_WORKER_COUNT=1 \
|
||||
-v /etc/apprise:/config \
|
||||
-d apprise/local:latest
|
||||
```
|
||||
A `docker-compose.yml` file is already set up to grant you an instant production ready simulated environment:
|
||||
@ -86,40 +98,6 @@ A `docker-compose.yml` file is already set up to grant you an instant production
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Config Directory Permissions
|
||||
|
||||
Under the hood, An NginX services is reading/writing your configuration files as the user (and group) `www-data` which generally has the id of `33`. In preparation so that you don't get the error: `An error occured saving configuration.` consider also setting up your local `/var/lib/apprise/config` permissions as:
|
||||
|
||||
```bash
|
||||
# Create a user/group (if one doesn't already exist) owned
|
||||
# by the user and group id of 33
|
||||
id 33 &>/dev/null || sudo useradd \
|
||||
--system --no-create-home --shell /bin/false \
|
||||
-u 33 -g 33 www-data
|
||||
|
||||
# Securely set the directory limiting access to only those who
|
||||
# are part of the www-data group:
|
||||
sudo chmod 770 -R /var/lib/apprise/config
|
||||
sudo chown 33:33 -R /var/lib/apprise/config
|
||||
|
||||
# Now optionally add yourself to the group if you wish to be able to view
|
||||
# contents.
|
||||
sudo usermod -a -G 33 $(whoami)
|
||||
|
||||
# You may need to log out and back in again for the above usermod
|
||||
# to reflect on you. Alternatively you can just type the following
|
||||
# and it will work as a temporary solution:
|
||||
sudo su - $(whoami)
|
||||
```
|
||||
|
||||
Alternatively a dirty solution is to just set the directory with full read/write permissions (which is not ideal in a production environment):
|
||||
|
||||
```bash
|
||||
# Grant full permission to the local directory you're saving your
|
||||
# Apprise configuration to:
|
||||
chmod 777 /var/lib/apprise/config
|
||||
```
|
||||
|
||||
## Dockerfile Details
|
||||
|
||||
The following architectures are supported: `amd64`, `arm/v7`, and `arm64`. The following tags can be used:
|
||||
@ -398,6 +376,8 @@ The use of environment variables allow you to provide over-rides to default sett
|
||||
|
||||
| Variable | Description |
|
||||
|--------------------- | ----------- |
|
||||
| `PUID` | The User ID you wish the Apprise instance under the hood to run as. The default is `1000` if not otherwise specified.
|
||||
| `PGID` | The Group ID you wish the Apprise instance under the hood to run as. The default is `1000` if not otherwise specified.
|
||||
| `APPRISE_DEFAULT_THEME` | Can be set to `light` or `dark`; it defaults to `light` if not otherwise provided. The theme can be toggled from within the website as well.
|
||||
| `APPRISE_DEFAULT_CONFIG_ID` | Defaults to `apprise`. This is the presumed configuration ID you always default to when accessing the configuration manager via the website.
|
||||
| `APPRISE_CONFIG_DIR` | Defines an (optional) persistent store location of all configuration files saved. By default:<br/> - Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. However for the path for the container is `/config`.
|
||||
@ -421,8 +401,56 @@ The use of environment variables allow you to provide over-rides to default sett
|
||||
| `DEBUG` | This defaults to `no` and can however be set to `yes` by simply defining the global variable as such.
|
||||
|
||||
|
||||
## Development Environment
|
||||
## Nginx Overrides
|
||||
|
||||
The 2 files you can override are:
|
||||
1. `/etc/nginx/location-override.conf` which is included within all of the Apprise API NginX `location` references.
|
||||
1. `/etc/nginx/server-override.conf` which is included within Apprise API `server` reference.
|
||||
|
||||
### Authentication
|
||||
Under the hood, Apprise-API is running a small NginX instance. It allows for you to inject your own configuration into it. One thing you may wish to add is basic authentication.
|
||||
|
||||
Below we create ourselves some nginx directives we'd like to apply to our Apprise API:
|
||||
```nginx
|
||||
# Our override.conf file:
|
||||
auth_basic "Apprise API Restricted Area";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
```
|
||||
|
||||
Now let's set ourselves up with a simple password file (for more info on htpasswd files, see [here](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/)
|
||||
```bash
|
||||
# Create ourselves a for our user 'foobar'; the below will prompt you for the pass
|
||||
# you want to provide:
|
||||
htpasswd -c apprise_api.htpasswd foobar
|
||||
|
||||
# Note: the -c above is only needed to create the database for the first time
|
||||
```
|
||||
|
||||
Now we can create our docker container with this new authentication information:
|
||||
```bash
|
||||
# Create our container containing Basic Auth:
|
||||
docker run --name apprise \
|
||||
-p 8000:8000 \
|
||||
-e PUID=$(id -u) \
|
||||
-e PGID=$(id -g) \
|
||||
-v /path/to/local/config:/config \
|
||||
-v /path/to/local/attach:/attach \
|
||||
-v ./override.conf:/etc/nginx/location-override.conf:ro \
|
||||
-v ./apprise_api.htpasswd:/etc/nginx/.htpasswd:ro \
|
||||
-e APPRISE_STATEFUL_MODE=simple \
|
||||
-e APPRISE_WORKER_COUNT=1 \
|
||||
-d caronc/apprise:latest
|
||||
```
|
||||
|
||||
Visit http://localhost:8000 to see if things are working as expected. If you followed the example above, you should log in as the user `foobar` using the credentials you provided the account.
|
||||
|
||||
You can add further accounts to the existing database by omitting the `-c` switch:
|
||||
```bash
|
||||
# Add another account
|
||||
htpasswd apprise_api.htpasswd user2
|
||||
```
|
||||
|
||||
## Development Environment
|
||||
The following should get you a working development environment to test with:
|
||||
|
||||
```bash
|
||||
|
@ -37,9 +37,9 @@
|
||||
</a>
|
||||
<h1>{% trans "Apprise API" %}</h1>
|
||||
<ul>
|
||||
<li>APPRISE v{{APPRISE_VERSION}}</li>
|
||||
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
|
||||
</ul>
|
||||
<li>APPRISE v{{APPRISE_VERSION}}</li>
|
||||
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Layout here -->
|
||||
@ -50,10 +50,10 @@
|
||||
<ul class="collection z-depth-1">
|
||||
<a class="collection-item" href="{% url 'config' DEFAULT_CONFIG_ID %}"><i class="material-icons">settings</i>
|
||||
{% trans "Configuration Manager" %}</a>
|
||||
{% if not CONFIG_LOCK %}
|
||||
{% if not CONFIG_LOCK %}
|
||||
<a class="collection-item" href="{% url 'config' UNIQUE_CONFIG_ID %}"><i class="material-icons">refresh</i>
|
||||
{% trans "New Configuration" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="collection z-depth-1">
|
||||
@ -80,6 +80,28 @@
|
||||
</div>
|
||||
|
||||
<div class="col s9">
|
||||
<div id="health-check" class="section" style="display: none">
|
||||
<h4><i class="material-icons" style="color: orange">warning</i> {% trans "Apprise Health Check Failed" %} <i class="material-icons" style="color: orange">warning</i></h4>
|
||||
{% blocktrans %}The following disk access errors have been detected with your Apprise instance{% endblocktrans %}:
|
||||
<ul>
|
||||
<li class="can_write_config" style="display: none"><strong>
|
||||
<i class="material-icons"
|
||||
style="color: red">cancel</i> {% trans "Configuration Write Failure" %}</strong>
|
||||
<p>{% blocktrans %}Apprise can not write new configuration information to the directory:{% endblocktrans %} <code>{{CONFIG_DIR}}</code>.</p>
|
||||
<p>{% blocktrans %}<em>Note:</em> If this is the expected behavior, you should pre-set the environment variable <code>APPRISE_CONFIG_LOCK=yes</code> and reload your Apprise instance.{% endblocktrans %}</p>
|
||||
</li>
|
||||
|
||||
<li class="can_write_attach" style="display: none"><strong>
|
||||
<i class="material-icons"
|
||||
style="color: red">cancel</i> {% trans "Attachment Temporary Storage Write Failure" %}</strong>
|
||||
<p>{% blocktrans %}Apprise can not circulate attachments (if provided) along to supported endpoints due to not having write access to the directory:{% endblocktrans %} <code>{{ATTACH_DIR}}</code>.</p>
|
||||
<p>{% blocktrans %}<em>Note:</em> If this is the expected behavior, you should pre-set the environment variable <code>APPRISE_ATTACH_SIZE=0</code> and reload your Apprise instance.{% endblocktrans %}</p>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>{% blocktrans %}Under most circumstances, the issue(s) identified here are usually related to permission issues. Make sure you set the correct <code>PUID</code> and <code>GUID</code> to reflect the permissions you wish Apprise to utilize when it is reading and writing its files. In addition to this, you may need to make sure the permissions are set correctly on the directories you mapped them too.{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}The issue(s) identified here can also be associated with SELinux too. You may wish to rule out SELinux by first temporarily disabling it using the command <code>setenforce 0</code>. You can always re-enstate it with <code>setenforce 1</code>{% endblocktrans %}.</p>
|
||||
</div>
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
@ -91,9 +113,37 @@
|
||||
M.AutoInit();
|
||||
// highlightjs
|
||||
hljs.initHighlightingOnLoad();
|
||||
{% block onload %} {% endblock %}
|
||||
{% block onload %}{% endblock %}
|
||||
// healthcheck
|
||||
health_check()
|
||||
});
|
||||
{% block jsfooter %} {% endblock %}
|
||||
function health_check() {
|
||||
// perform our health check
|
||||
document.querySelector('#health-check').style.display = 'none';
|
||||
document.querySelector('#health-check li.can_write_config').style.display = 'none';
|
||||
document.querySelector('#health-check li.can_write_attach').style.display = 'none';
|
||||
let response = fetch('{% url "health" %}', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json;charset=utf-8'
|
||||
},
|
||||
|
||||
}).then(function(response) {
|
||||
if(response.status != 200)
|
||||
{
|
||||
response.json().then(function(content) {
|
||||
if (content['status']['can_write_config'] === false && content['config_lock'] === false) {
|
||||
document.querySelector('#health-check li.can_write_config').style.display = '';
|
||||
}
|
||||
if (content['status']['can_write_attach'] === false && content['attach_lock'] === false) {
|
||||
document.querySelector('#health-check li.can_write_attach').style.display = '';
|
||||
}
|
||||
document.querySelector('#health-check').style.display = '';
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
@ -188,7 +188,7 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block jsfooter %}
|
||||
|
||||
{{ block.super }}
|
||||
{% if STATEFUL_MODE != 'disabled' %}
|
||||
function update_count() {
|
||||
const p_count = document.querySelectorAll('#url-list li.card-panel.selected').length;
|
||||
@ -695,8 +695,8 @@ function notify_init() {
|
||||
{% endblock %}
|
||||
|
||||
{% block onload %}
|
||||
{% if STATEFUL_MODE != 'disabled' %}
|
||||
{{ block.super }}
|
||||
{% if STATEFUL_MODE != 'disabled' %}
|
||||
document.querySelector('label [for="id_tag"]')
|
||||
{
|
||||
// create a new div with the class 'chips' assigned to it
|
||||
|
@ -1,6 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<h4>{% trans "The Apprise API" %}</h4>
|
||||
<p>
|
||||
|
@ -64,6 +64,7 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': False,
|
||||
'attach_lock': False,
|
||||
'status': {
|
||||
'can_write_config': True,
|
||||
'can_write_attach': True,
|
||||
@ -87,6 +88,7 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': True,
|
||||
'attach_lock': False,
|
||||
'status': {
|
||||
'can_write_config': False,
|
||||
'can_write_attach': True,
|
||||
@ -109,6 +111,7 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': False,
|
||||
'attach_lock': False,
|
||||
'status': {
|
||||
'can_write_config': False,
|
||||
'can_write_attach': True,
|
||||
@ -131,6 +134,7 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': False,
|
||||
'attach_lock': True,
|
||||
'status': {
|
||||
'can_write_config': True,
|
||||
'can_write_attach': False,
|
||||
@ -153,9 +157,10 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': False,
|
||||
'attach_lock': False,
|
||||
'status': {
|
||||
'can_write_config': True,
|
||||
'can_write_attach': False,
|
||||
'can_write_attach': True,
|
||||
'details': ['OK']
|
||||
}
|
||||
}
|
||||
|
@ -181,6 +181,7 @@ class NotifyTests(SimpleTestCase):
|
||||
# Reset our mock object
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# A setting of zero means unlimited attachments are allowed
|
||||
with override_settings(APPRISE_MAX_ATTACHMENTS=0):
|
||||
|
||||
# Preare our form data
|
||||
@ -196,6 +197,67 @@ class NotifyTests(SimpleTestCase):
|
||||
form = NotifyForm(form_data, attach_data)
|
||||
assert form.is_valid()
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form.cleaned_data)
|
||||
|
||||
# We're good!
|
||||
assert response.status_code == 200
|
||||
assert mock_notify.call_count == 1
|
||||
|
||||
# Reset our mock object
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Only allow 1 attachment, but we'll attempt to send more...
|
||||
with override_settings(APPRISE_MAX_ATTACHMENTS=1):
|
||||
|
||||
# Preare our form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
}
|
||||
|
||||
# At a minimum, just a body is required
|
||||
form = NotifyForm(form_data)
|
||||
|
||||
assert form.is_valid()
|
||||
# Required to prevent None from being passed into self.client.post()
|
||||
del form.cleaned_data['attachment']
|
||||
|
||||
data = {
|
||||
**form.cleaned_data,
|
||||
'file1': SimpleUploadedFile(
|
||||
"attach1.txt", b"content here", content_type="text/plain"),
|
||||
'file2': SimpleUploadedFile(
|
||||
"attach2.txt", b"more content here", content_type="text/plain"),
|
||||
}
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), data, format='multipart')
|
||||
|
||||
# Too many attachments
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
# Reset our mock object
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# A setting of zero means unlimited attachments are allowed
|
||||
with override_settings(APPRISE_ATTACH_SIZE=0):
|
||||
|
||||
# Preare our form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
}
|
||||
attach_data = {
|
||||
'attachment': SimpleUploadedFile(
|
||||
"attach.txt", b"content here", content_type="text/plain")
|
||||
}
|
||||
|
||||
# At a minimum, just a body is required
|
||||
form = NotifyForm(form_data, attach_data)
|
||||
assert form.is_valid()
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form.cleaned_data)
|
||||
|
@ -207,6 +207,22 @@ class HTTPAttachment(A_MGR['http']):
|
||||
pass
|
||||
|
||||
|
||||
def touch(fname, mode=0o666, dir_fd=None, **kwargs):
|
||||
"""
|
||||
Acts like a Linux touch and updates a file with a current timestamp
|
||||
"""
|
||||
flags = os.O_CREAT | os.O_APPEND
|
||||
try:
|
||||
with os.fdopen(os.open(fname, flags=flags, mode=mode, dir_fd=dir_fd)) as f:
|
||||
os.utime(f.fileno() if os.utime in os.supports_fd else fname,
|
||||
dir_fd=None if os.supports_fd else dir_fd, **kwargs)
|
||||
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def parse_attachments(attachment_payload, files_request):
|
||||
"""
|
||||
Takes the payload provided in a `/notify` call and extracts the
|
||||
@ -230,15 +246,12 @@ def parse_attachments(attachment_payload, files_request):
|
||||
count += 1
|
||||
|
||||
if settings.APPRISE_ATTACH_SIZE <= 0:
|
||||
raise ValueError("The attachment size is restricted to 0MB")
|
||||
raise ValueError("Attachment support has been disabled")
|
||||
|
||||
if settings.APPRISE_MAX_ATTACHMENTS <= 0 or \
|
||||
(settings.APPRISE_MAX_ATTACHMENTS > 0 and
|
||||
count > settings.APPRISE_MAX_ATTACHMENTS):
|
||||
if settings.APPRISE_MAX_ATTACHMENTS > 0 and count > settings.APPRISE_MAX_ATTACHMENTS:
|
||||
raise ValueError(
|
||||
"There is a maximum of %d attachments" %
|
||||
settings.APPRISE_MAX_ATTACHMENTS
|
||||
if settings.APPRISE_MAX_ATTACHMENTS > 0 else 0)
|
||||
settings.APPRISE_MAX_ATTACHMENTS)
|
||||
|
||||
if isinstance(attachment_payload, (tuple, list, set)):
|
||||
for no, entry in enumerate(attachment_payload, start=1):
|
||||
@ -772,67 +785,70 @@ def healthcheck(lazy=True):
|
||||
|
||||
if not (settings.APPRISE_STATEFUL_MODE == AppriseStoreMode.DISABLED or settings.APPRISE_CONFIG_LOCK):
|
||||
# Update our Configuration Check Block
|
||||
path = os.path.join(ConfigCache.root, '.tmp_healthcheck')
|
||||
path = os.path.join(ConfigCache.root, '.tmp_hc')
|
||||
if lazy:
|
||||
try:
|
||||
modify_date = datetime.fromtimestamp(os.path.getmtime(path))
|
||||
delta = (datetime.now() - modify_date).total_seconds()
|
||||
if delta <= 7200.00: # 2hrs
|
||||
if delta <= 30.00: # 30s
|
||||
response['can_write_config'] = True
|
||||
|
||||
except FileNotFoundError:
|
||||
# No worries... continue with below testing
|
||||
pass
|
||||
|
||||
if not response['can_write_config']:
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
# Write a small file
|
||||
with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
|
||||
# Test writing 1 block
|
||||
fp.write(b'.')
|
||||
# Read it back
|
||||
fp.seek(0)
|
||||
fp.read(1) == b'.'
|
||||
# Toggle our status
|
||||
response['can_write_config'] = True
|
||||
|
||||
except OSError:
|
||||
# Permission Issue or something else likely
|
||||
# We can take an early exit
|
||||
response['details'].append('CONFIG_PERMISSION_ISSUE')
|
||||
|
||||
if settings.APPRISE_MAX_ATTACHMENTS > 0 and settings.APPRISE_ATTACH_SIZE > 0:
|
||||
if not (response['can_write_config'] or 'CONFIG_PERMISSION_ISSUE' in response['details']):
|
||||
try:
|
||||
os.makedirs(ConfigCache.root, exist_ok=True)
|
||||
if touch(path):
|
||||
# Toggle our status
|
||||
response['can_write_config'] = True
|
||||
|
||||
else:
|
||||
# We can take an early exit as there is already a permission issue detected
|
||||
response['details'].append('CONFIG_PERMISSION_ISSUE')
|
||||
|
||||
except OSError:
|
||||
# We can take an early exit as there is already a permission issue detected
|
||||
response['details'].append('CONFIG_PERMISSION_ISSUE')
|
||||
|
||||
if settings.APPRISE_ATTACH_SIZE > 0:
|
||||
# Test our ability to access write attachments
|
||||
|
||||
# Update our Configuration Check Block
|
||||
path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_healthcheck')
|
||||
path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_hc')
|
||||
if lazy:
|
||||
try:
|
||||
modify_date = datetime.fromtimestamp(os.path.getmtime(path))
|
||||
delta = (datetime.now() - modify_date).total_seconds()
|
||||
if delta <= 7200.00: # 2hrs
|
||||
if delta <= 30.00: # 30s
|
||||
response['can_write_attach'] = True
|
||||
|
||||
except FileNotFoundError:
|
||||
# No worries... continue with below testing
|
||||
pass
|
||||
|
||||
if not response['can_write_attach']:
|
||||
except OSError:
|
||||
# We can take an early exit as there is already a permission issue detected
|
||||
response['details'].append('ATTACH_PERMISSION_ISSUE')
|
||||
|
||||
if not (response['can_write_attach'] or 'ATTACH_PERMISSION_ISSUE' in response['details']):
|
||||
# No lazy mode set or content require a refresh
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
# Write a small file
|
||||
with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
|
||||
# Test writing 1 block
|
||||
fp.write(b'.')
|
||||
# Read it back
|
||||
fp.seek(0)
|
||||
fp.read(1) == b'.'
|
||||
os.makedirs(settings.APPRISE_ATTACH_DIR, exist_ok=True)
|
||||
if touch(path):
|
||||
# Toggle our status
|
||||
response['can_write_attach'] = True
|
||||
|
||||
else:
|
||||
# We can take an early exit as there is already a permission issue detected
|
||||
response['details'].append('ATTACH_PERMISSION_ISSUE')
|
||||
|
||||
except OSError:
|
||||
# We can take an early exit
|
||||
response['details'].append('ATTACH_PERMISSION_ISSUE')
|
||||
|
@ -161,8 +161,8 @@ class HealthCheckView(View):
|
||||
and ACCEPT_ALL.match(request.headers.get('accept', '')) else \
|
||||
MIME_IS_JSON.match(request.headers.get('accept', '')) is not None
|
||||
|
||||
# Run our healthcheck
|
||||
response = healthcheck()
|
||||
# Run our healthcheck; allow ?force which will cause the check to run each time
|
||||
response = healthcheck(lazy='force' not in request.GET)
|
||||
|
||||
# Prepare our response
|
||||
status = ResponseCode.okay if 'OK' in response['details'] else ResponseCode.expectation_failed
|
||||
@ -172,6 +172,7 @@ class HealthCheckView(View):
|
||||
return HttpResponse(response, status=status, content_type='text/plain') \
|
||||
if not json_response else JsonResponse({
|
||||
'config_lock': settings.APPRISE_CONFIG_LOCK,
|
||||
'attach_lock': settings.APPRISE_ATTACH_SIZE <= 0,
|
||||
'status': response,
|
||||
}, encoder=JSONEncoder, safe=False, status=status)
|
||||
|
||||
|
@ -29,4 +29,8 @@ def base_url(request):
|
||||
"""
|
||||
Returns our defined BASE_URL object
|
||||
"""
|
||||
return {'BASE_URL': settings.BASE_URL}
|
||||
return {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'CONFIG_DIR': settings.APPRISE_CONFIG_DIR,
|
||||
'ATTACH_DIR': settings.APPRISE_ATTACH_DIR,
|
||||
}
|
||||
|
@ -16,11 +16,13 @@ http {
|
||||
types_hash_max_size 2048;
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
# Do not display Nginx Version
|
||||
server_tokens off;
|
||||
|
||||
##
|
||||
# Upload Restriction
|
||||
##
|
||||
client_max_body_size 500M;
|
||||
##
|
||||
# Upload Restriction
|
||||
##
|
||||
client_max_body_size 500M;
|
||||
|
||||
##
|
||||
# Logging Settings
|
||||
@ -43,6 +45,10 @@ http {
|
||||
listen 8000;
|
||||
listen [::]:8000;
|
||||
|
||||
# Allow users to map to this file and provide their own custom
|
||||
# overrides such as
|
||||
include /etc/nginx/server-override.conf;
|
||||
|
||||
# Main Website
|
||||
location / {
|
||||
include /etc/nginx/uwsgi_params;
|
||||
@ -52,12 +58,14 @@ http {
|
||||
proxy_pass http://localhost:8080;
|
||||
# Give ample time for notifications to fire
|
||||
proxy_read_timeout 120s;
|
||||
include /etc/nginx/location-override.conf;
|
||||
}
|
||||
|
||||
# Static Content
|
||||
location /s/ {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
include /etc/nginx/location-override.conf;
|
||||
}
|
||||
|
||||
# 404 error handling
|
||||
|
@ -4,9 +4,11 @@ pidfile=/run/apprise/supervisord.pid
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
user=www-data
|
||||
group=www-data
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -c /opt/apprise/webapp/etc/nginx.conf
|
||||
command=/usr/sbin/nginx -c /opt/apprise/webapp/etc/nginx.conf -p /opt/apprise
|
||||
directory=/opt/apprise
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
|
@ -238,3 +238,33 @@ code.config-id {
|
||||
.chip.selected {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#health-check {
|
||||
background-color: #f883791f;
|
||||
border-radius: 25px;
|
||||
padding: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
#health-check h4 {
|
||||
font-size: 30px;
|
||||
}
|
||||
#health-check h4 .material-icons {
|
||||
margin-top: -0.2em;
|
||||
}
|
||||
|
||||
#health-check li .material-icons {
|
||||
font-size: 30px;
|
||||
margin-top: -0.2em;
|
||||
}
|
||||
|
||||
#health-check ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
#health-check ul strong {
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
1
apprise_api/static/css/theme-dark.min.css
vendored
1
apprise_api/static/css/theme-dark.min.css
vendored
@ -273,7 +273,6 @@ h5 {
|
||||
|
||||
em {
|
||||
color: #5e81ac !important;
|
||||
text-shadow: 6px 3px #2e3440
|
||||
}
|
||||
|
||||
.card-panel {
|
||||
|
78
apprise_api/supervisord-startup
Executable file
78
apprise_api/supervisord-startup
Executable file
@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# Copyright (C) 2024 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
if [ $(id -u) -ne 0 ]; then
|
||||
echo "You must be root to run this script."
|
||||
echo "Caution: This should only be ran in a dockerized instance!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Default values
|
||||
PUID=${PUID:=1000}
|
||||
PGID=${PGID:=1000}
|
||||
|
||||
# lookup our identifier
|
||||
GROUP=$(getent group $PGID 2>/dev/null | cut -d: -f1)
|
||||
[ -z "$GROUP" ] && groupadd --force -g $PGID apprise &>/dev/null && \
|
||||
GROUP=apprise
|
||||
|
||||
USER=$(id -un $PUID 2>/dev/null)
|
||||
[ $? -ne 0 ] && useradd -M -N \
|
||||
-o -u $PUID -G $GROUP -c "Apprise API User" -d /opt/apprise apprise && \
|
||||
USER=apprise
|
||||
|
||||
if [ -z "$USER" ]; then
|
||||
echo "The specified User ID (PUID) of $PUID is invalid; Aborting operation."
|
||||
exit 1
|
||||
|
||||
elif [ -z "$GROUP" ]; then
|
||||
echo "The specified Group ID (PGID) of $PGID is invalid; Aborting operation."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure our group has been correctly assigned
|
||||
usermod -a -G $GROUP $USER &>/dev/null
|
||||
chmod o+w /dev/stdout /dev/stderr
|
||||
|
||||
[ ! -d /attach ] && mkdir -p /attach
|
||||
chown -R $USER:$GROUP /attach
|
||||
[ ! -d /config ] && mkdir -p /config
|
||||
chown $USER:$GROUP /config
|
||||
[ ! -d /plugin ] && mkdir -p /plugin
|
||||
[ ! -d /run/apprise ] && mkdir -p /run/apprise
|
||||
|
||||
# Some Directories require enforced permissions to play it safe
|
||||
chown $USER:$GROUP -R /run/apprise /var/lib/nginx /opt/apprise
|
||||
sed -i -e "s/^\(user[ \t]*=[ \t]*\).*$/\1$USER/g" \
|
||||
/opt/apprise/webapp/etc/supervisord.conf
|
||||
sed -i -e "s/^\(group[ \t]*=[ \t]*\).*$/\1$GROUP/g" \
|
||||
/opt/apprise/webapp/etc/supervisord.conf
|
||||
|
||||
# Working directory
|
||||
cd /opt/apprise
|
||||
|
||||
# Launch our SupervisorD
|
||||
/usr/local/bin/supervisord -c /opt/apprise/webapp/etc/supervisord.conf
|
||||
# Always return our SupervisorD return code
|
||||
exit $?
|
Loading…
Reference in New Issue
Block a user