all-in-one backend using docker
This commit is contained in:
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM bfren/ftps:latest
|
||||
|
||||
# Install required tools for Alpine
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
inotify-tools \
|
||||
libavif-apps \
|
||||
findutils
|
||||
|
||||
# Copy post-upload script
|
||||
COPY post_upload.sh /usr/local/bin/post_upload.sh
|
||||
RUN chmod +x /usr/local/bin/post_upload.sh
|
||||
|
||||
# Launch script via bash
|
||||
CMD ["bash", "/usr/local/bin/post_upload.sh"]
|
83
README.md
83
README.md
@ -1,8 +1,79 @@
|
||||
# msv-webcam-backend
|
||||
|
||||
- `mkdir /opt/msv-webcam-backend/`
|
||||
- `cd /opt/msv-webcam-backend/`
|
||||
- import `msv_webcam_backend.py`
|
||||
- `chmod +x /opt/msv-webcam-backend/msv_webcam_backend.py`
|
||||
- `nano /etc/systemd/system/msv-webcam-backend.service`
|
||||
- `systemctl daemon-reload && systemctl enable --now msv-webcam-backend.service`
|
||||
A lightweight FTPS-based backend that automatically watches camera upload folders, converts the latest image from each camera to AVIF format, and stores them in a `current/` directory for easy access (e.g., via Nginx).
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
- FTPS server for receiving image uploads
|
||||
- Automatic detection of the newest image per camera folder
|
||||
- Conversion to AVIF format using `avifenc`
|
||||
- Persistent storage of converted images in a shared `current/` directory
|
||||
- Designed for integration with a frontend or Nginx for live webcam feeds
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 0) Import project files
|
||||
Make sure the following files are present in your repository:
|
||||
```
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
post_upload.sh
|
||||
```
|
||||
|
||||
### 1) Configure user credentials
|
||||
Edit `docker-compose.yml` and set your FTPS username and password:
|
||||
```yaml
|
||||
environment:
|
||||
- BF_FTPS_VSFTPD_USER=username
|
||||
- BF_FTPS_VSFTPD_PASS=userpw
|
||||
```
|
||||
|
||||
### 2) Make the post-upload script executable
|
||||
```bash
|
||||
chmod +x post_upload.sh
|
||||
```
|
||||
|
||||
### 3) Build the Docker image
|
||||
```bash
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
### 4) Start the service
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
```
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── post_upload.sh
|
||||
└── files/
|
||||
├── msvcam1/ # uploads from camera 1
|
||||
├── msvcam2/ # uploads from camera 2
|
||||
└── current/ # latest .avif images (msvcam1.avif, msvcam2.avif, ...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
1. Cameras upload images via FTPS into `files/msvcamN/` folders.
|
||||
2. `post_upload.sh` periodically checks for the newest image in each folder.
|
||||
3. If the newest image has changed (SHA256 hash comparison), it is converted to `.avif` format and saved to `files/current/` as `msvcamN.avif`.
|
||||
4. The `current/` directory can be served directly by a web server (e.g., Nginx) for live display.
|
||||
|
||||
---
|
||||
|
||||
## Example Nginx Integration
|
||||
See [MSV Webcam Frontend](https://git.mosad.xyz/localhorst/msv-webcam-frontend)
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@ -0,0 +1,15 @@
|
||||
services:
|
||||
ftps:
|
||||
build: .
|
||||
restart: always
|
||||
volumes:
|
||||
- ./files:/files
|
||||
environment:
|
||||
- BF_FTPS_VSFTPD_USER=__PW_DB__
|
||||
- BF_FTPS_VSFTPD_PASS=__PW_DB__
|
||||
- BF_FTPS_EXTERNAL_URI=ftps.msv-buehl-moos.de
|
||||
ports:
|
||||
- "21:21"
|
||||
- "990:990"
|
||||
- "18700-18710:18700-18710"
|
||||
|
@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=MSV-Webcam-Backend
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
WorkingDirectory=/opt/msv-webcam-backend/
|
||||
ExecStart=/usr/bin/python3 /opt/msv-webcam-backend/msv_webcam_backend.py interval=10 search_dir=/home/dockeruser/ftps_server/files/msvcamS search_dir=/home/dockeruser/ftps_server/files/msvcamN destination_dir=/tmp/msv_webcam_current destination_owner=wwwrun
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Author: Hendrik Schutter, mail@hendrikschutter.com
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
signal_pipe_path = "/tmp/msv-webcam-signal-pipe"
|
||||
signal_pipe_msg = "msv-webcam-new-images"
|
||||
|
||||
def copy_image(src, destination_dir_path, destination_owner):
|
||||
# print("src: " + str(src))
|
||||
# print("dst: " + str(destination_dir_path))
|
||||
os.makedirs(os.path.dirname(destination_dir_path), exist_ok=True)
|
||||
shutil.copyfile(src, destination_dir_path)
|
||||
os.chmod(destination_dir_path, 0o0644)
|
||||
shutil.chown(destination_dir_path, destination_owner)
|
||||
|
||||
def signal_new_images():
|
||||
if not os.path.exists(signal_pipe_path):
|
||||
os.mkfifo(signal_pipe_path)
|
||||
with open(signal_pipe_path, "w") as f:
|
||||
f.write(signal_pipe_msg)
|
||||
f.close()
|
||||
|
||||
def main():
|
||||
print("starting ...")
|
||||
|
||||
update_interval = -1
|
||||
destination_dir_path = -1
|
||||
destination_owner = -1
|
||||
search_dirs = list()
|
||||
|
||||
for argument in sys.argv:
|
||||
if argument.startswith('interval'):
|
||||
update_interval = int(argument.split("=")[1])
|
||||
if argument.startswith('destination_dir'):
|
||||
destination_dir_path = argument.split("=")[1]
|
||||
if argument.startswith('destination_owner'):
|
||||
destination_owner = argument.split("=")[1]
|
||||
if argument.startswith('search_dir'):
|
||||
tmp_search_dir = {
|
||||
"search_dir_path" : argument.split("=")[1],
|
||||
"newest_file_path" : "",
|
||||
"newest_file_size" : 0
|
||||
}
|
||||
search_dirs.append(tmp_search_dir)
|
||||
|
||||
if ((update_interval == -1) or (destination_dir_path == -1) or (destination_owner == -1) or (len(search_dirs) == 0)):
|
||||
print("Unable to parse config!")
|
||||
print("Example usage:")
|
||||
print(" python msv_webcam_backend.py interval=5 search_dir=test_images/msvcamS search_dir=test_images/msvcamN destination_dir=/tmp/msv_webcam_current destination_owner=hendrik")
|
||||
sys.exit(-1)
|
||||
|
||||
while True:
|
||||
new_images_found = False
|
||||
for search_dir in search_dirs:
|
||||
try:
|
||||
# print("\nSearch dir: " + str(search_dir["search_dir_path"]))
|
||||
list_of_files = glob.glob(search_dir["search_dir_path"]+"/**/*.jpg", recursive=True)
|
||||
# print(list_of_files)
|
||||
newest_file_path_tmp = max(list_of_files, key=os.path.getctime)
|
||||
newest_file_size_tmp = os.stat(newest_file_path_tmp).st_size
|
||||
list_of_files.clear()
|
||||
|
||||
if (newest_file_path_tmp == search_dir["newest_file_path"]) and (newest_file_size_tmp == search_dir["newest_file_size"]):
|
||||
# already found the newest file
|
||||
print("no newer file found")
|
||||
else:
|
||||
time.sleep(1) # wait 1sec to see if the upload is still in progress
|
||||
|
||||
list_of_files = glob.glob(search_dir["search_dir_path"]+"/**/*.jpg", recursive=True)
|
||||
newest_file_path_tmp_delayed = max(list_of_files, key=os.path.getctime)
|
||||
newest_file_size_tmp_delayed = os.stat(newest_file_path_tmp_delayed).st_size
|
||||
list_of_files.clear()
|
||||
|
||||
if (newest_file_path_tmp == newest_file_path_tmp_delayed) and (newest_file_size_tmp == newest_file_size_tmp_delayed):
|
||||
search_dir["newest_file_path"] = newest_file_path_tmp_delayed
|
||||
search_dir["newest_file_size"] = newest_file_size_tmp_delayed
|
||||
print("Found new file:")
|
||||
print("Name: " + str(search_dir["newest_file_path"]))
|
||||
print("Size: " + str(search_dir["newest_file_size"]))
|
||||
dst_file_tmp = os.path.join(destination_dir_path, (os.path.basename(os.path.normpath(search_dir["search_dir_path"])) + os.path.splitext(search_dir["newest_file_path"])[1]))
|
||||
copy_image(search_dir["newest_file_path"], dst_file_tmp, destination_owner)
|
||||
new_images_found = True
|
||||
#break
|
||||
else:
|
||||
print("Upload not finished yet!")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
#break
|
||||
if(new_images_found == True):
|
||||
signal_new_images()
|
||||
time.sleep(update_interval)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
39
post_upload.sh
Normal file
39
post_upload.sh
Normal file
@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE_DIR="/files"
|
||||
CURRENT_DIR="$BASE_DIR/current"
|
||||
|
||||
mkdir -p "$CURRENT_DIR"
|
||||
|
||||
# Store last SHA256 checksums for each camera
|
||||
declare -A LAST_SHA
|
||||
|
||||
echo "[Watcher] Starting to monitor subfolders in $BASE_DIR ..."
|
||||
|
||||
while true; do
|
||||
for CAM_DIR in "$BASE_DIR"/msvcam*; do
|
||||
[ -d "$CAM_DIR" ] || continue
|
||||
|
||||
CAM_NAME=$(basename "$CAM_DIR")
|
||||
OUT_FILE="$CURRENT_DIR/${CAM_NAME}.avif"
|
||||
|
||||
# Find the newest image
|
||||
NEWEST_FILE=$(find "$CAM_DIR" -type f -iregex ".*\.\(jpg\|jpeg\|png\|bmp\|tif\|tiff\)" -printf "%T@ %p\n" | sort -n | tail -1 | awk '{print $2}')
|
||||
|
||||
if [ -n "$NEWEST_FILE" ]; then
|
||||
# Calculate SHA256 of the newest image
|
||||
SHA=$(sha256sum "$NEWEST_FILE" | awk '{print $1}')
|
||||
|
||||
# Check if this file has already been converted
|
||||
if [ "${LAST_SHA[$CAM_NAME]}" != "$SHA" ]; then
|
||||
echo "[Watcher] New or changed file detected for $CAM_NAME: $NEWEST_FILE"
|
||||
avifenc "$NEWEST_FILE" "$OUT_FILE"
|
||||
sync
|
||||
LAST_SHA[$CAM_NAME]="$SHA"
|
||||
else
|
||||
echo "[Watcher] No change detected for $CAM_NAME, skipping conversion."
|
||||
fi
|
||||
fi
|
||||
done
|
||||
sleep 5
|
||||
done
|
Reference in New Issue
Block a user