mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-18 10:57:19 +02:00
Addresses review feedback on the sqlite persistence fix (PR #2812): - validate_safe_path now rejects ".." path segments so a relative output.sqlite.path can't traverse outside /config when rewritten. - The rewrite now creates the destination's parent directory, since SQLite won't create one itself for a path like "db/birdnet.db". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KHJ4o22cdcgdNtb81tBTPJ
195 lines
8.8 KiB
Bash
Executable File
195 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/with-contenv bashio
|
|
# shellcheck shell=bash
|
|
set -e
|
|
|
|
# Default Variables
|
|
DEFAULT_BIRDSONGS_FOLDER="/data/clips"
|
|
CONFIG_LOCATION="/config/config.yaml"
|
|
|
|
# Strip trailing slashes; canonical internal form has none.
|
|
normalize_path() {
|
|
local p="$1"
|
|
while [[ "$p" == */ && "$p" != "/" ]]; do
|
|
p="${p%/}"
|
|
done
|
|
printf '%s' "$p"
|
|
}
|
|
|
|
# Reject paths containing characters that would break the SQL/YAML literals
|
|
# we substitute into below. We deliberately allow only "safe" filename chars.
|
|
# Also reject ".." path segments so a relative path can't traverse outside
|
|
# the directory we prefix it with (e.g. "/config") when we rewrite it below.
|
|
validate_safe_path() {
|
|
local p="$1"
|
|
if [[ ! "$p" =~ ^[A-Za-z0-9._/-]+$ ]]; then
|
|
bashio::log.fatal "Refusing unsafe path: '$p' (only [A-Za-z0-9._/-] allowed)"
|
|
exit 1
|
|
fi
|
|
local segment
|
|
local IFS=/
|
|
for segment in $p; do
|
|
if [[ "$segment" == ".." ]]; then
|
|
bashio::log.fatal "Refusing unsafe path: '$p' (path traversal '..' is not allowed)"
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
if [ ! -f "$CONFIG_LOCATION" ]; then
|
|
bashio::log.warning "There is no config.yaml yet in the config folder, downloading a default one. Please customize"
|
|
# Network may be unreachable on first boot. If the download fails, seed
|
|
# an empty YAML document so the yq reads/writes below succeed and the
|
|
# default-value seeding logic later in this script populates a usable
|
|
# config. (We can't remove the file and continue — subsequent yq calls
|
|
# under set -e would abort the init script.)
|
|
if ! curl -fL -s -S \
|
|
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
|
-o "$CONFIG_LOCATION"; then
|
|
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
|
|
echo '{}' > "$CONFIG_LOCATION"
|
|
fi
|
|
fi
|
|
|
|
#################
|
|
# Migrate Database
|
|
#################
|
|
if [ -f /data/birdnet.db ]; then
|
|
bashio::log.warning "Moving birdnet.db to /config/birdnet.db"
|
|
mv /data/birdnet.db /config
|
|
fi
|
|
|
|
######################
|
|
# Birdsongs Location
|
|
######################
|
|
# Read the current folder from config.yaml; "// """ collapses both missing
|
|
# keys and explicit nulls (e.g. in a freshly seeded "{}" doc) to an empty
|
|
# string so the ${VAR:-DEFAULT} fallback below kicks in.
|
|
CURRENT_BIRDSONGS_FOLDER="$(yq -r '.realtime.audio.export.path // ""' "$CONFIG_LOCATION")"
|
|
CURRENT_BIRDSONGS_FOLDER="${CURRENT_BIRDSONGS_FOLDER:-$DEFAULT_BIRDSONGS_FOLDER}"
|
|
# Treat the upstream-shipped relative "clips/" as the legacy default.
|
|
if [[ "$CURRENT_BIRDSONGS_FOLDER" == "clips" || "$CURRENT_BIRDSONGS_FOLDER" == "clips/" ]]; then
|
|
CURRENT_BIRDSONGS_FOLDER="$DEFAULT_BIRDSONGS_FOLDER"
|
|
fi
|
|
CURRENT_BIRDSONGS_FOLDER="$(normalize_path "$CURRENT_BIRDSONGS_FOLDER")"
|
|
|
|
# Set the new birdsongs folder from addon options (default: relative "clips").
|
|
BIRDSONGS_FOLDER="$(bashio::config "BIRDSONGS_FOLDER")"
|
|
BIRDSONGS_FOLDER="$(normalize_path "${BIRDSONGS_FOLDER:-clips}")"
|
|
|
|
validate_safe_path "$BIRDSONGS_FOLDER"
|
|
validate_safe_path "$CURRENT_BIRDSONGS_FOLDER"
|
|
|
|
if [[ ! "$BIRDSONGS_FOLDER" == /* ]]; then
|
|
if [ ! -d "/config/$BIRDSONGS_FOLDER" ]; then
|
|
mkdir -p "/config/$BIRDSONGS_FOLDER"
|
|
fi
|
|
if [ -d "/data/$BIRDSONGS_FOLDER" ]; then
|
|
if [ -n "$(ls -A /data/"$BIRDSONGS_FOLDER" 2> /dev/null)" ]; then
|
|
cp -rf /data/"$BIRDSONGS_FOLDER"/* "/config/$BIRDSONGS_FOLDER"/
|
|
fi
|
|
rm -r "/data/$BIRDSONGS_FOLDER"
|
|
fi
|
|
ln -sf "/config/$BIRDSONGS_FOLDER" "/data/$BIRDSONGS_FOLDER"
|
|
fi
|
|
|
|
bashio::log.info "... audio clips saved to $BIRDSONGS_FOLDER according to addon options"
|
|
|
|
# Migrate data if the folder has changed
|
|
if [[ "$CURRENT_BIRDSONGS_FOLDER" != "$BIRDSONGS_FOLDER" ]]; then
|
|
bashio::log.warning "Birdsongs folder changed from $CURRENT_BIRDSONGS_FOLDER to $BIRDSONGS_FOLDER"
|
|
# Update config.yaml with the new birdsongs folder path (trailing slash
|
|
# restored only at the boundary, since birdnet-go expects it).
|
|
yq -i -y ".realtime.audio.export.path = \"${BIRDSONGS_FOLDER}/\"" "$CONFIG_LOCATION"
|
|
# Move files only if sqlite paths changed
|
|
if [[ -d "$CURRENT_BIRDSONGS_FOLDER" && "$(ls -A "$CURRENT_BIRDSONGS_FOLDER")" ]]; then
|
|
bashio::log.warning "Migrating files from $CURRENT_BIRDSONGS_FOLDER to $BIRDSONGS_FOLDER"
|
|
# The absolute-path target (e.g. the default /config/clips) is never
|
|
# created by the relative-path block above, so ensure it exists before
|
|
# copying into it; otherwise cp aborts the init script under set -e.
|
|
mkdir -p "$BIRDSONGS_FOLDER"
|
|
cp -rnf "$CURRENT_BIRDSONGS_FOLDER"/* "$BIRDSONGS_FOLDER"/
|
|
mv "$CURRENT_BIRDSONGS_FOLDER" "${CURRENT_BIRDSONGS_FOLDER}_migrated"
|
|
fi
|
|
# Adapt the database
|
|
if [ -f /config/birdnet.db ]; then
|
|
backup="$(date +%Y%m%d_%H%M%S)"
|
|
BACKUP_FILE="/config/birdnet.db_${backup}"
|
|
bashio::log.warning "Modifying database paths from $CURRENT_BIRDSONGS_FOLDER to $BIRDSONGS_FOLDER. A backup will be created at ${BACKUP_FILE}"
|
|
|
|
# Create backup at the absolute path we'll restore from on failure.
|
|
if ! cp /config/birdnet.db "$BACKUP_FILE"; then
|
|
bashio::log.error "Failed to create a backup of the database. Aborting path modification."
|
|
exit 1
|
|
fi
|
|
|
|
# Paths were validated above against [A-Za-z0-9._/-]+ so quote
|
|
# escaping in the SQL literal is not a concern.
|
|
SQL_QUERY="UPDATE notes SET clip_name = '${BIRDSONGS_FOLDER}/' || substr(clip_name, length('${CURRENT_BIRDSONGS_FOLDER}/') + 1) WHERE clip_name LIKE '${CURRENT_BIRDSONGS_FOLDER}/%';"
|
|
|
|
if ! sqlite3 /config/birdnet.db "$SQL_QUERY"; then
|
|
bashio::log.warning "An error occurred while updating the paths. The database backup will be restored."
|
|
if [ -f "$BACKUP_FILE" ]; then
|
|
mv "$BACKUP_FILE" /config/birdnet.db
|
|
bashio::log.info "The database backup has been restored."
|
|
else
|
|
bashio::log.error "Backup file $BACKUP_FILE not found! Manual intervention required."
|
|
fi
|
|
else
|
|
bashio::log.info "Paths have been successfully updated."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
####################
|
|
# Correct Defaults
|
|
####################
|
|
# Seed addon-specific defaults only if the user has not set them in
|
|
# config.yaml. The "//=" form leaves any user-edited value alone, so
|
|
# changes made via the BirdNET-Go UI or by hand-editing /config/config.yaml
|
|
# survive container restarts.
|
|
bashio::log.info "Seeding default configuration values (only if missing)"
|
|
|
|
# Upstream's shipped default config.yaml explicitly sets output.sqlite.path
|
|
# to the relative "birdnet.db", so the "//=" below (default-if-missing)
|
|
# never fires for it. A relative path resolves against the app's working
|
|
# directory, which lives in the ephemeral container filesystem rather than
|
|
# a persistent volume, so the database is silently recreated empty on every
|
|
# restart. Rewrite any relative sqlite path to live under the persistent
|
|
# /config so detections survive reboots.
|
|
CURRENT_SQLITE_PATH="$(yq -r '.output.sqlite.path // ""' "$CONFIG_LOCATION")"
|
|
if [[ -n "$CURRENT_SQLITE_PATH" && "$CURRENT_SQLITE_PATH" != /* ]]; then
|
|
validate_safe_path "$CURRENT_SQLITE_PATH"
|
|
NEW_SQLITE_PATH="/config/${CURRENT_SQLITE_PATH}"
|
|
bashio::log.warning "output.sqlite.path ('$CURRENT_SQLITE_PATH') is relative and would not persist across restarts; rewriting to $NEW_SQLITE_PATH"
|
|
# SQLite does not create missing parent directories, so ensure one exists
|
|
# if the (validated, traversal-free) path includes a subdirectory.
|
|
mkdir -p "$(dirname "$NEW_SQLITE_PATH")"
|
|
yq -i -y ".output.sqlite.path = \"${NEW_SQLITE_PATH}\"" "$CONFIG_LOCATION"
|
|
fi
|
|
|
|
yq -i -y '.output.sqlite.path //= "/config/birdnet.db"' "$CONFIG_LOCATION"
|
|
|
|
####################
|
|
# Log Management
|
|
####################
|
|
LOG_MAX_SIZE_MB="$(bashio::config "LOG_MAX_SIZE_MB")"
|
|
LOG_MAX_SIZE_MB="${LOG_MAX_SIZE_MB:-50}"
|
|
LOG_MAX_AGE_DAYS="$(bashio::config "LOG_MAX_AGE_DAYS")"
|
|
LOG_MAX_AGE_DAYS="${LOG_MAX_AGE_DAYS:-7}"
|
|
|
|
bashio::log.info "Seeding default log rotation: max ${LOG_MAX_SIZE_MB}MB per file, max ${LOG_MAX_AGE_DAYS} days retention (only applied if not already set)"
|
|
|
|
# Seed log-rotation defaults; do not clobber user-edited values.
|
|
yq -i -y ".logging.file_output.max_size //= ${LOG_MAX_SIZE_MB}" "$CONFIG_LOCATION"
|
|
yq -i -y ".logging.file_output.max_age //= ${LOG_MAX_AGE_DAYS}" "$CONFIG_LOCATION"
|
|
yq -i -y '.logging.file_output.max_rotated_files //= 3' "$CONFIG_LOCATION"
|
|
yq -i -y '.logging.file_output.compress //= true' "$CONFIG_LOCATION"
|
|
|
|
# Trim existing log files that exceed the configured max age
|
|
LOG_DIR="/config/logs"
|
|
if [ -d "$LOG_DIR" ]; then
|
|
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
|
ln -sf "$LOG_DIR" /logs
|
|
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
|
|
fi
|