From 9ce4ef7609ef4686d157bc5b2dc1e4aefe69f975 Mon Sep 17 00:00:00 2001 From: dialmaster Date: Tue, 5 May 2026 15:57:38 -0700 Subject: [PATCH 1/3] fix: reduce bind-mount mariadb corruption risk Some users have reported MariaDB tables becoming unreadable ("Table ... doesn't exist in engine", "Incorrect information in file") after schema migrations on Docker Desktop, ARM hosts, and some NAS/virtualized filesystems. The bundled MariaDB defaulted to a bind mount on ./database, which appears to be involved in those reports. Default fresh installs started by ./start.sh to the named-volume override on every platform. Existing installs with data in ./database are preserved on the bind mount and shown a migration warning instead of being silently switched, including when --arm is passed or an ARM host is auto-detected. Add scripts/migrate-to-named-volume.sh, an opt-in helper that dumps the bind-mounted database, renames ./database to a timestamped backup directory, imports into a fresh named volume, verifies table set and per-table row counts, and only then pins the override in .env. The script supports a passwordless-sudo fallback for installs where the project directory is not directly writable, and refuses to run on ambiguous or non-default setups. Centralize storage-mode detection in scripts/_env_helpers.sh so backup.sh and restore.sh share the same logic and refuse to act when both ./database and a named volume exist. Update README, DATABASE, DOCKER, INSTALLATION, DEVELOPMENT, and TROUBLESHOOTING docs to describe the new defaults and migration path. --- .gitignore | 4 + README.md | 2 + docker-compose.arm.yml | 7 +- docker-compose.yml | 12 +- docs/DATABASE.md | 104 ++++-- docs/DEVELOPMENT.md | 6 +- docs/DOCKER.md | 25 +- docs/INSTALLATION.md | 8 +- docs/TROUBLESHOOTING.md | 50 ++- scripts/_env_helpers.sh | 281 ++++++++++++++ scripts/_shared_start_tasks.sh | 106 +++++- scripts/_start_template.sh | 9 +- scripts/backup.sh | 35 +- scripts/migrate-to-named-volume.sh | 572 +++++++++++++++++++++++++++++ scripts/restore.sh | 40 +- 15 files changed, 1153 insertions(+), 108 deletions(-) create mode 100755 scripts/_env_helpers.sh create mode 100755 scripts/migrate-to-named-volume.sh diff --git a/.gitignore b/.gitignore index 1e353b75..afd73f11 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ server/images .env .env.dev +.env.bak.* # Local dev exclusion ffmpeg.exe @@ -57,6 +58,9 @@ downloads/* # Backup archive location backups/ +# Database migration safety backups +database.bind-mount-backup.*/ + # Superpowers brainstorming session artifacts (local-only, ephemeral mockups) .superpowers/ diff --git a/README.md b/README.md index 05a64605..a08a5ea8 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ You'll need Docker, Docker Compose, Git, and a Bash shell (Git Bash on Windows). > Want to try unreleased features? See [Using Development Builds](docs/DEVELOPMENT.md#using-development-builds) for the bleeding-edge `dev-latest` image. +> **Database note for Docker Desktop/ARM/NAS users:** Fresh installs started with `./start.sh` use Docker named-volume storage for MariaDB. Existing bind-mounted installs and plain `docker compose up -d` installs may use `./database`; on virtualized filesystems this can be risky for MariaDB schema migrations. If you see `Table ... doesn't exist in engine` or `Incorrect information in file` errors, or if you want to proactively migrate, see [Database Management](docs/DATABASE.md#migrating-from-bind-mount-to-named-volume). + ## Documentation ### Setup & Configuration diff --git a/docker-compose.arm.yml b/docker-compose.arm.yml index c5d156d2..c21c85fe 100644 --- a/docker-compose.arm.yml +++ b/docker-compose.arm.yml @@ -1,5 +1,7 @@ -# Override file for ARM-based systems (Apple Silicon, Raspberry Pi, etc.) -# This uses a named volume for MariaDB to work around virtiofs bugs on ARM +# Named-volume database override. +# The filename is historical: it was originally added for ARM systems, but it is +# also useful on Docker Desktop and NAS/virtualized filesystems where bind-mounted +# MariaDB data can corrupt during schema migrations. services: youtarr-db: @@ -8,4 +10,3 @@ services: volumes: youtarr-db-data: - diff --git a/docker-compose.yml b/docker-compose.yml index 1122b84b..a66b6af8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,10 +15,12 @@ # mkdir -p config jobs server/images /path/to/youtube_videos # sudo chown -R 1000:1000 config jobs server/images /path/to/youtube_videos # -# 2) Database setup notes for Synology and ARM-based systems (Apple Silicon, Raspberry Pi): -# There are known issues with using bind mounts for MariaDB data directories due to permission and virtiofs bugs. -# The start scripts automatically detect ARM architecture and use docker-compose.arm.yml override to switch to named volumes. -# For Synology or manual override, run: docker compose -f docker-compose.yml -f docker-compose.arm.yml up -d +# 2) Database setup notes: +# The default bundled MariaDB uses ./database as a bind mount for backwards compatibility. +# On Docker Desktop (Windows/macOS), ARM hosts, and some NAS/virtualized filesystems, +# bind-mounted MariaDB data can corrupt during schema migrations. For a safer named +# volume, use docker-compose.arm.yml or run ./scripts/migrate-to-named-volume.sh +# for an existing bind-mounted install. services: youtarr-db: @@ -96,6 +98,6 @@ networks: default: name: youtarr-network -# Named volume definition (used by docker-compose.arm.yml override for ARM systems) +# Named volume definition (used by docker-compose.arm.yml override) volumes: youtarr-db-data: diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 6bdcdca3..1e3f821c 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -51,46 +51,96 @@ volumes: - ./database:/var/lib/mysql ``` - Data stored in `./database` directory on the host -- May have permission issues on Synology/QNAP and or virtiofs issues on Apple Silicon macOS, leading to database issues/corruption +- Kept for backwards compatibility with existing bind-mounted installs and plain `docker compose up -d` users +- Works well on native Linux Docker hosts +- Can have permission issues on Synology/QNAP +- Can corrupt during MariaDB schema migrations on Docker Desktop for Windows/macOS, ARM hosts, and some virtualized filesystems -#### Option 2: Named Volume (Recommended for Synology/Apple) +#### Option 2: Named Volume (Recommended for Docker Desktop/ARM/NAS) ```yaml volumes: - youtarr-db-data:/var/lib/mysql ``` - Better compatibility with Synology/QNAP -- Avoids permission issues -- Required for macOS Apple Silicon -- Not easily visible on host +- Avoids the virtualized-filesystem write semantics problem that can affect bind-mounted MariaDB +- Used automatically for fresh installs started with `./start.sh` on every platform (Linux included, since v1.69) +- Recommended for Docker Desktop on Windows/macOS, ARM systems, and NAS setups +- Not easily visible on host: data lives under `/var/lib/docker/volumes/_youtarr-db-data/_data` rather than `./database/`. `./scripts/backup.sh` dumps from the running MariaDB container when it is already up; when it has to start MariaDB for a backup, it detects whether this install uses the bind mount or named volume first. -### Switching to Named Volume +### Migrating from Bind Mount to Named Volume -If experiencing permission errors on Synology/QNAP or corruption on Apple Silicon: +If you already have Youtarr data in `./database/`, do **not** switch the compose mount by hand unless you intentionally want to start with an empty database. Use the migration helper instead: -**IMPORTANT**: Changing your DB volume mount will *not* migrate your existing database! If you are not experiencing problems, leave this setting alone! +```bash +./scripts/migrate-to-named-volume.sh +``` -1. Stop the stack: - `docker compose down` or `./stop.sh` +What the script does (in this order, so any failure leaves the simplest possible recovery state): +1. Runs a pre-flight permissions check so it fails fast (instead of stalling on an interactive `sudo` prompt) if it cannot write to the project directory. +2. Stops Youtarr. +3. Starts the existing bind-mounted MariaDB long enough to run `mysqldump` and to capture per-table row counts. +4. Renames `./database/` to `./database.bind-mount-backup./` so the original files are preserved. +5. Starts a fresh named-volume MariaDB and imports the dump. +6. Verifies that the table set matches the source **and** that every table has the same row count as the source. +7. **Only after verification succeeds**, snapshots `.env` to `./.env.bak.` and pins `COMPOSE_PATH_SEPARATOR=:` and `COMPOSE_FILE=docker-compose.yml:docker-compose.arm.yml` in `.env`. This means a failure during step 5 or 6 leaves `.env` untouched, and recovery is just `mv ./database.bind-mount-backup. ./database` plus removing the partial named volume. +8. Brings the full stack (app + database) back up so Youtarr is immediately usable. -2. Edit `docker-compose.yml`: - ```yaml - # Change from: - volumes: - - ./database:/var/lib/mysql +**What the migration does *not* copy**: `mysqldump` runs with `--single-transaction --routines --triggers --events`. Schema, data, stored routines, triggers, and events all migrate. MariaDB users and `GRANT` statements (anything in `mysql.user` / `mysql.db`) do **not**. The default Youtarr install only uses the bundled `root` user, so this is a no-op for almost everyone. If you have created additional database users on the bundled MariaDB, recreate them after the migration completes. - # To: - volumes: - - youtarr-db-data:/var/lib/mysql - ``` +**Password note**: for the bundled `root` database user, `DB_ROOT_PASSWORD` seeds the root password when a fresh MariaDB data directory is initialized, while Youtarr connects with `DB_PASSWORD`. The migration requires those two values to match before it creates the new named-volume database. -3. Add volume definition at bottom of file: - ```yaml - volumes: - youtarr-db-data: +After it completes, the stack is already running. Subsequent restarts can use any of: + +```bash +./start.sh # recommended +docker compose up -d # the script pins COMPOSE_FILE in .env +docker compose -f docker-compose.yml -f docker-compose.arm.yml up -d # explicit override +``` + +### Reverting to Bind Mount + +The migration is reversible: + +1. Stop the stack: + ```bash + ./stop.sh + ``` +2. Restore the `.env` snapshot: + ```bash + mv ./.env.bak. .env ``` +3. Remove the named volume for this install. The name is usually `_youtarr-db-data`: + ```bash + docker volume ls --format '{{.Name}}' | grep -E '(^|_)youtarr-db-data$' + docker volume rm + ``` +4. Restore the original bind-mounted database directory: + ```bash + mv ./database.bind-mount-backup. ./database + ``` +5. Start Youtarr: + ```bash + ./start.sh + ``` + +Changes made while running on the named volume are not present in the old bind-mounted backup. If you have used the named volume for a while and want to keep those newer changes, take a backup first with `./scripts/backup.sh`. + +### Fresh Installs with Named Volume + +For a new install with no data to preserve, you can start directly with the named-volume override: + +```bash +docker compose -f docker-compose.yml -f docker-compose.arm.yml up -d +``` + +Or pin the override in `.env` so plain `docker compose up -d` uses it: + +```env +COMPOSE_PATH_SEPARATOR=: +COMPOSE_FILE=docker-compose.yml:docker-compose.arm.yml +``` -4. Restart: - `docker compose up -d` or `./start.sh` +`COMPOSE_PATH_SEPARATOR=:` is important on Windows so Compose parses the file list consistently. ### Security Considerations @@ -214,11 +264,11 @@ npm run db:create-migration -- --name my-migration-name #### Common Causes 1. **Synology/QNAP NAS**: MariaDB runs as UID 999, which may not exist -2. **macOS Apple Silicon**: virtiofs incompatibility with MariaDB 10.3 +2. **Docker Desktop/ARM/NAS**: virtualized filesystem or permission issues with bind-mounted MariaDB data 3. **Wrong ownership**: Database files owned by incorrect user #### Solutions -1. **Switch to named volume** (see above) +1. **Migrate to named volume** (see above) 2. **Fix permissions**: ```bash # Check current ownership diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 58ee0cd6..849a5cd5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -14,11 +14,11 @@ The `dev-latest` tag always points at the most recent dev build. Each commit als This pulls `dialmaster/youtarr:dev-latest` and starts the stack. On later runs, drop `--pull-latest` if you want to stay on the image you already have locally. -### Alternative: bypass `./start.sh` (safer for existing installs on ARM) +### Alternative: bypass `./start.sh` -`./start.sh` auto-detects ARM hosts (Apple Silicon, Raspberry Pi, ARM Linux) and layers `docker-compose.arm.yml` on top of the default compose file. That override switches MariaDB from the bind mount at `./database/` to a named volume. If you already have data in `./database/` and you run `./start.sh` on ARM for the first time, the container boots against an empty database and it looks like all your channels and videos disappeared. The data is fine, it's still sitting in `./database/`, but the running container isn't pointing at it. +`./start.sh` layers `docker-compose.arm.yml` on top of the default compose file for fresh installs so MariaDB uses a named volume. Existing installs with real MariaDB data in `./database/` keep using the bind mount and print a migration warning instead. ARM installs continue to use the named-volume override. -To pull the dev image without letting `./start.sh` touch your compose file selection, use docker directly: +To pull the dev image while managing compose file selection yourself, use docker directly: ```bash ./stop.sh diff --git a/docs/DOCKER.md b/docs/DOCKER.md index 32d80826..95f0f279 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -14,7 +14,7 @@ Youtarr ships four Compose files so each supported runtime can layer the right o |------|---------| | `docker-compose.yml` | Production defaults with the bundled MariaDB container. Used by `./start.sh`. | | `docker-compose.dev.yml` | Development mode: mounts `./server/` and migrations into the container, runs the backend with `node --watch` for hot reload, and uses a separate `youtarr-db-dev` database with its own named volume. Used by `./scripts/start-dev.sh`. See [DEVELOPMENT.md](DEVELOPMENT.md). | -| `docker-compose.arm.yml` | ARM override (Apple Silicon, Raspberry Pi) that switches the MariaDB data directory to a named volume to avoid virtiofs issues. Layered on top of `docker-compose.yml` via `-f`. | +| `docker-compose.arm.yml` | Named-volume database override. The filename is historical: it was originally added for ARM systems, but it is also useful on Docker Desktop and NAS/virtualized filesystems. Layered on top of `docker-compose.yml` via `-f`. | | `docker-compose.external-db.yml` | Runs Youtarr against an external MariaDB/MySQL instance instead of the bundled database. Used by `./start-with-external-db.sh`. | ## Container Details @@ -34,10 +34,10 @@ Youtarr ships four Compose files so each supported runtime can layer the right o - **Port**: 3321 (both host and container) - **Volumes**: - `./database:/var/lib/mysql` - Database persistence (default) - - `youtarr-db-data:/var/lib/mysql` - Named volume (required for ARM/Synology) + - `youtarr-db-data:/var/lib/mysql` - Named volume (recommended for Docker Desktop/ARM/NAS) - **Character Set**: utf8mb4 (full Unicode support) -> **ARM Users**: See [ARM Architecture Notes](#arm-architecture-apple-silicon-raspberry-pi) below. +> **Docker Desktop/ARM/NAS users**: See [Named-Volume Database Override](#named-volume-database-override) below. ## ⚠️ Important: Do Not Mount the Migrations Directory @@ -62,25 +62,30 @@ volumes: If your automation creates a migrations directory, remove it from both directory creation and volume mounts. -## ARM Architecture (Apple Silicon, Raspberry Pi) +## Named-Volume Database Override -ARM-based systems (Apple Silicon Macs, Raspberry Pi, etc.) have known issues with MariaDB bind mounts due to virtiofs bugs. The start scripts automatically detect ARM and apply the fix. +Docker Desktop on Windows/macOS, ARM hosts, Synology/QNAP, and some virtualized filesystems can have trouble with MariaDB data stored on a bind mount. The named-volume override avoids that class of issue. ### Using Start Scripts (Recommended) -The `./start.sh` script automatically detects ARM architecture and applies the correct configuration: +`./start.sh` automatically uses `docker-compose.arm.yml` for fresh installs on every platform. Existing installs with real MariaDB data in `./database/` keep using the bind mount and print a migration warning instead, including on ARM hosts. ```bash ./start.sh ``` +For an existing bind-mounted install, migrate with: +```bash +./scripts/migrate-to-named-volume.sh +``` + ### Using Docker Compose Directly -If you prefer running `docker compose` commands directly on ARM systems, use the override file: +For a fresh install, use the override file: ```bash docker compose -f docker-compose.yml -f docker-compose.arm.yml up -d ``` -This uses a named Docker volume instead of a bind mount for MariaDB data, avoiding the virtiofs issues. +This uses a named Docker volume instead of a bind mount for MariaDB data. ### Manual Configuration @@ -99,7 +104,7 @@ volumes: youtarr-db-data: ``` -See [Troubleshooting](TROUBLESHOOTING.md#apple-silicon--arm-incorrect-information-in-file-errors) for more details on the underlying issue. +See [Troubleshooting](TROUBLESHOOTING.md#docker-desktop--arm-incorrect-information-in-file-errors) for more details on the underlying issue. ## Configuration Setup - **Create a .env file** to configure environment variables: @@ -246,7 +251,7 @@ On most Linux hosts, Docker will auto-create `./database` on first run and Maria sudo chown -R 999:999 database ``` -If you hit `InnoDB: Operating system error number 13` at startup, you have hit this case - see [Switching to Named Volume](DATABASE.md#switching-to-named-volume) in the database docs for an alternative that sidesteps bind-mount permission issues entirely. +If you hit `InnoDB: Operating system error number 13` at startup, you have hit this case - see [Migrating from Bind Mount to Named Volume](DATABASE.md#migrating-from-bind-mount-to-named-volume) in the database docs for an alternative that sidesteps bind-mount permission issues entirely. #### 5. Set Permissions diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index be0b621a..959ca2ef 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -33,7 +33,7 @@ Choose your preferred installation method - `--pull-latest`: Pull latest code from Github and latest image from DockerHub - `--debug`: Set log level to debug - This automatically creates a `.env` file from the included `.env.example` and starts both the Youtarr application and MariaDB database containers. + This automatically creates a `.env` file from the included `.env.example` and starts both the Youtarr application and MariaDB database containers. On a fresh install, `./start.sh` uses Docker named-volume storage for MariaDB. If an existing `./database/` MariaDB directory is present, it preserves that bind-mounted database and prints a migration warning. 3. **Access the web interface**: Open your browser and navigate to `http://localhost:3087` @@ -81,11 +81,11 @@ If you prefer to use standard `docker compose up` commands: docker compose up -d ``` - > **ARM Users (Apple Silicon, Raspberry Pi)**: Use the ARM override to avoid MariaDB volume issues: + > **Docker Desktop/ARM/NAS users**: For a fresh install, use the named-volume database override to avoid MariaDB bind-mount issues on virtualized filesystems: > ```bash > docker compose -f docker-compose.yml -f docker-compose.arm.yml up -d > ``` - > See [Troubleshooting](TROUBLESHOOTING.md#apple-silicon--arm-incorrect-information-in-file-errors) for details. + > If you already have data in `./database/`, use `./scripts/migrate-to-named-volume.sh` instead. See [Database Management](DATABASE.md#migrating-from-bind-mount-to-named-volume) and [Troubleshooting](TROUBLESHOOTING.md#docker-desktop--arm-incorrect-information-in-file-errors) for details. 5. **Access the web interface**: - Navigate to `http://localhost:3087` @@ -95,7 +95,7 @@ If you prefer to use standard `docker compose up` commands: > **Important**: Ensure the path you assign to `YOUTUBE_OUTPUT_DIR` already exists on the host and is writable before starting the stack. Otherwise Docker will create it as root-owned and the container may not be able to write downloads. -This method is **functionally equivalent** to using the start.sh script, but gives you direct control over environment variables. It's the preferred approach for any Docker-native workflow. +This method gives you direct control over environment variables and compose files, but it is not identical to `./start.sh`: plain `docker compose up -d` uses the legacy bind-mounted database unless you include or pin `docker-compose.arm.yml`. ### Method 3: Manual Setup Without Git (Advanced Users Only) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 43331842..8bbdc3cd 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -360,50 +360,42 @@ ERROR 1396 (HY000) at line 21: Operation CREATE USER failed for 'root'@'%' - Exit MySQL and restart the stack. 4. Once the stack is back online, verify the latest schema under **Configuration → System → Database Health**. -Tip: run with a named volume (see Apple Silicon/Synology sections) so filesystem corruption is less likely to recur. +Tip: run with a named volume (see Docker Desktop/ARM/Synology sections) so filesystem corruption is less likely to recur. -### Apple Silicon / ARM: `Incorrect information in file` errors +### Docker Desktop / ARM: `Incorrect information in file` errors -**Problem**: On Apple Silicon (M1/M2/M3/M4) or other ARM systems running Docker Desktop, MariaDB logs errors like: +**Problem**: MariaDB logs errors like: ``` ERROR 1033 (HY000): Incorrect information in file: './youtarr/videos.frm' ``` -This happens whenever MariaDB touches tables stored on a bind-mounted host directory (our default `./database:/var/lib/mysql`). Docker Desktop shares bind mounts over `virtiofs`, and MariaDB 10.3 cannot reliably reopen InnoDB tables on that filesystem ([MariaDB issue #447](https://github.com/MariaDB/mariadb-docker/issues/447), [#481](https://github.com/MariaDB/mariadb-docker/issues/481)). Linux and WSL users are unaffected. +or: +``` +errno 1932 - Table 'youtarr.videos' doesn't exist in engine +``` + +This can happen when MariaDB data lives on the bind-mounted host directory (`./database:/var/lib/mysql`) and Docker proxies that directory through a virtualized filesystem, most often Docker Desktop on Windows/macOS, ARM hosts, or some NAS setups. During DDL operations such as `CREATE TABLE` or `ALTER TABLE`, InnoDB can end up out of sync with MariaDB's table metadata. Native Linux Docker hosts are usually unaffected. -**Solution A: Use the start scripts (Recommended)** +**Existing install with data to preserve** -The `./start.sh` and `./start-dev.sh` scripts automatically detect ARM architecture and apply the fix: +Use the migration helper. It dumps the bind-mounted DB, preserves `./database/` as a timestamped backup directory, pins the named-volume override in `.env`, and imports the dump into a fresh named-volume MariaDB: ```bash -./start.sh +./scripts/migrate-to-named-volume.sh ``` -No manual configuration needed—the scripts use `docker-compose.arm.yml` as an override on ARM systems. -**Solution B: Manual docker compose (if not using start scripts)** +See [Database Management](DATABASE.md#migrating-from-bind-mount-to-named-volume) for the full migration and revert details. + +**Fresh install with no data to preserve** -If you run `docker compose up` directly, use the ARM override file: +Start with the named-volume override: ```bash docker compose -f docker-compose.yml -f docker-compose.arm.yml up -d ``` -Or manually edit `docker-compose.yml` to use a named volume: - -**NOTE:** Existing data will *not* be migrated! -1. Stop the stack: `docker compose down` -2. Edit `docker-compose.yml`: - ```yaml - services: - youtarr-db: - volumes: - # Comment out the bind mount: - # - ./database:/var/lib/mysql - # Use named volume instead: - - youtarr-db-data:/var/lib/mysql - - # Add at the bottom of the file: - volumes: - youtarr-db-data: - ``` -3. Start Youtarr: `docker compose up -d` +Or add this to `.env` before plain `docker compose up -d`: +```env +COMPOSE_PATH_SEPARATOR=: +COMPOSE_FILE=docker-compose.yml:docker-compose.arm.yml +``` **Alternatives**: - Point Youtarr at an external MariaDB/MySQL instance via `./start-with-external-db.sh`. diff --git a/scripts/_env_helpers.sh b/scripts/_env_helpers.sh new file mode 100755 index 00000000..a116204e --- /dev/null +++ b/scripts/_env_helpers.sh @@ -0,0 +1,281 @@ +#!/bin/bash + +# Shared helpers for managing Youtarr's .env file and Docker Compose project state. +# Pure functions only; safe to source from any script without triggering side effects. + +# Read a key from an env file without sourcing it. This intentionally supports +# the simple KEY=value format used by Youtarr's .env files. +youtarr_get_env_file_value() { + local env_file="$1" + local key="$2" + local default="${3:-}" + local value="" + + if [[ -f "$env_file" ]]; then + value=$(grep -E "^[[:space:]]*${key}[[:space:]]*=" "$env_file" 2>/dev/null \ + | tail -n 1 \ + | sed -E "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*//" \ + | sed -E 's/[[:space:]]+#.*$//' \ + | sed -E 's/[[:space:]]+$//' \ + | sed -E 's/^"(.*)"$/\1/' \ + | sed -E "s/^'(.*)'$/\1/" || true) + fi + + if [[ -n "$value" ]]; then + printf '%s' "$value" + else + printf '%s' "$default" + fi +} + +# Compute the Docker Compose project name for a checkout. Mirrors Compose's own +# normalization (lowercase basename of the project dir, with anything outside +# [a-z0-9_-] stripped). Honors COMPOSE_PROJECT_NAME (env, then .env) when set. +youtarr_get_compose_project_name() { + local project_dir="$1" + local project_name="${COMPOSE_PROJECT_NAME:-}" + + if [[ -z "$project_name" ]] && [[ -f "$project_dir/.env" ]]; then + project_name=$(youtarr_get_env_file_value "$project_dir/.env" "COMPOSE_PROJECT_NAME" "") + fi + + if [[ -z "$project_name" ]]; then + project_name=$(basename "$project_dir" | tr '[:upper:]' '[:lower:]' | tr -dc 'a-z0-9_-') + fi + + printf '%s' "$project_name" +} + +# Compute the expected MariaDB named-volume name for a checkout. +youtarr_expected_db_volume_name() { + local project_dir="$1" + printf '%s_youtarr-db-data' "$(youtarr_get_compose_project_name "$project_dir")" +} + +# Returns 0 if THIS install's named MariaDB volume exists, 1 otherwise. +# Scoped to the current Compose project; does not match volumes from other +# Youtarr checkouts on the same host. +youtarr_named_volume_exists() { + local project_dir="$1" + local volume_name + volume_name=$(youtarr_expected_db_volume_name "$project_dir") + docker volume inspect "$volume_name" >/dev/null 2>&1 +} + +youtarr_database_dir_has_content() { + local project_dir="$1" + [[ -f "$project_dir/database/ibdata1" || -d "$project_dir/database/mysql" ]] +} + +youtarr_is_arm_host() { + local arch + arch=$(uname -m) + [[ "$arch" == "arm64" || "$arch" == "aarch64" ]] +} + +youtarr_effective_compose_file_value() { + local project_dir="$1" + if [[ -n "${COMPOSE_FILE:-}" ]]; then + printf '%s' "$COMPOSE_FILE" + else + youtarr_get_env_file_value "$project_dir/.env" "COMPOSE_FILE" "" + fi +} + +youtarr_compose_file_has_named_volume_override() { + local project_dir="$1" + local value + value=$(youtarr_effective_compose_file_value "$project_dir") + [[ "$value" == *"docker-compose.arm.yml"* ]] +} + +# Returns one of: named-volume, bind-mount, ambiguous. +# Fresh bundled-DB installs default to named-volume storage. +youtarr_detect_bundled_db_storage_mode() { + local project_dir="$1" + local force_named="${2:-}" + local bind_has_content=false + local named_exists=false + + if youtarr_database_dir_has_content "$project_dir"; then + bind_has_content=true + fi + + if youtarr_named_volume_exists "$project_dir"; then + named_exists=true + fi + + if [[ "$bind_has_content" == "true" && "$named_exists" == "true" ]]; then + printf '%s' "ambiguous" + elif [[ "$force_named" == "--force-named" ]]; then + printf '%s' "named-volume" + elif youtarr_compose_file_has_named_volume_override "$project_dir"; then + printf '%s' "named-volume" + elif [[ "$named_exists" == "true" ]]; then + printf '%s' "named-volume" + elif [[ "$bind_has_content" == "true" ]]; then + printf '%s' "bind-mount" + elif youtarr_is_arm_host; then + printf '%s' "named-volume" + else + printf '%s' "named-volume" + fi +} + +youtarr_compose_args_for_storage_mode() { + local project_dir="$1" + local storage_mode="$2" + + case "$storage_mode" in + named-volume) + if youtarr_compose_file_has_named_volume_override "$project_dir"; then + # Let Compose honor the user's full COMPOSE_FILE list from the shell/.env. + printf '' + elif [[ -n "$(youtarr_effective_compose_file_value "$project_dir")" ]]; then + local compose_file_value + local compose_args="" + local compose_file + local -a compose_files + compose_file_value=$(youtarr_effective_compose_file_value "$project_dir") + IFS=':' read -r -a compose_files <<< "$compose_file_value" + for compose_file in "${compose_files[@]}"; do + [[ -z "$compose_file" ]] && continue + compose_args="$compose_args -f $compose_file" + done + printf '%s' "${compose_args# } -f $project_dir/docker-compose.arm.yml" + else + printf '%s' "-f $project_dir/docker-compose.yml -f $project_dir/docker-compose.arm.yml" + fi + ;; + bind-mount) + printf '%s' "-f $project_dir/docker-compose.yml" + ;; + *) + return 1 + ;; + esac +} + +# Pin the named-volume Compose override in .env so plain `docker compose up -d` +# (without -f flags) picks up docker-compose.arm.yml automatically. +# +# Safe by default: preserves an existing COMPOSE_FILE setting and appends the +# named-volume override if missing. +# Pass --force to strip and replace any existing COMPOSE_FILE / COMPOSE_PATH_SEPARATOR. +# Pass --use-sudo to run the final `mv` under `sudo -n` (for installs where the +# project directory is not directly writable by the current user). The flag may +# be passed in either positional slot. +# +# Return codes: +# 0 - pinned successfully +# 1 - error (env file missing, mktemp failed, or final mv failed; under +# --use-sudo this includes `sudo -n` being unavailable). The temp file +# is cleaned up on mv failure on both the sudo and non-sudo paths. +# 2 - skipped because COMPOSE_FILE already includes docker-compose.arm.yml +youtarr_pin_named_volume_in_env() { + local env_file="$1" + shift || true + + local force="" + local use_sudo="" + local arg + for arg in "$@"; do + case "$arg" in + --force) force="--force" ;; + --use-sudo) use_sudo="--use-sudo" ;; + esac + done + + if [[ ! -f "$env_file" ]]; then + return 1 + fi + + local existing + existing=$(grep -E '^[[:space:]]*COMPOSE_FILE[[:space:]]*=' "$env_file" 2>/dev/null | tail -n 1 || true) + + local existing_value="" + if [[ -n "$existing" ]]; then + existing_value=$(printf '%s' "$existing" \ + | sed -E 's/^[[:space:]]*COMPOSE_FILE[[:space:]]*=[[:space:]]*//' \ + | sed -E 's/[[:space:]]+#.*$//' \ + | sed -E 's/[[:space:]]+$//' \ + | sed -E 's/^"(.*)"$/\1/' \ + | sed -E "s/^'(.*)'$/\1/") + fi + + local existing_separator=":" + local configured_separator + configured_separator=$(youtarr_get_env_file_value "$env_file" "COMPOSE_PATH_SEPARATOR" "") + if [[ "$configured_separator" == ";" ]]; then + existing_separator=";" + elif [[ "$existing_value" == *";"* && "$existing_value" != *":"* ]]; then + existing_separator=";" + fi + + local normalized_existing_value="$existing_value" + if [[ -n "$normalized_existing_value" && "$existing_separator" == ";" ]]; then + normalized_existing_value=${normalized_existing_value//;/:} + fi + + local existing_has_named_volume_override=false + if [[ -n "$normalized_existing_value" && "$normalized_existing_value" == *"docker-compose.arm.yml"* ]]; then + existing_has_named_volume_override=true + fi + + local needs_separator_normalization=false + if [[ "$configured_separator" == ";" || "$normalized_existing_value" != "$existing_value" ]]; then + needs_separator_normalization=true + fi + + if [[ "$force" != "--force" && "$existing_has_named_volume_override" == "true" && "$needs_separator_normalization" != "true" ]]; then + return 2 + fi + + local tmp_file + tmp_file=$(mktemp) || return 1 + + grep -vE '^[[:space:]]*COMPOSE_FILE[[:space:]]*=' "$env_file" \ + | grep -vE '^[[:space:]]*COMPOSE_PATH_SEPARATOR[[:space:]]*=' \ + > "$tmp_file" || true + + if [[ -s "$tmp_file" ]] && [[ -n "$(tail -c1 "$tmp_file")" ]]; then + printf '\n' >> "$tmp_file" + fi + + local compose_file_value="docker-compose.yml:docker-compose.arm.yml" + if [[ "$force" != "--force" && "$existing_has_named_volume_override" == "true" ]]; then + compose_file_value="$normalized_existing_value" + elif [[ -n "$normalized_existing_value" && "$force" != "--force" ]]; then + compose_file_value="${normalized_existing_value}:docker-compose.arm.yml" + fi + + { + printf '# Use named-volume database storage (managed by Youtarr)\n' + printf 'COMPOSE_PATH_SEPARATOR=:\n' + printf 'COMPOSE_FILE=%s\n' "$compose_file_value" + } >> "$tmp_file" + + local mv_rc=0 + if [[ "$use_sudo" == "--use-sudo" ]]; then + sudo -n mv "$tmp_file" "$env_file" || mv_rc=$? + else + mv "$tmp_file" "$env_file" || mv_rc=$? + fi + + if [[ "$mv_rc" -ne 0 ]]; then + rm -f "$tmp_file" 2>/dev/null || true + return 1 + fi +} + +# Returns 0 if the COMPOSE_FILE value in .env contains docker-compose.arm.yml, +# 1 otherwise (including when COMPOSE_FILE is unset). Used to detect whether +# plain `docker compose up -d` would include the named-volume override. +youtarr_env_has_named_volume_pin() { + local env_file="$1" + [[ -f "$env_file" ]] || return 1 + local value + value=$(grep -E '^[[:space:]]*COMPOSE_FILE[[:space:]]*=' "$env_file" 2>/dev/null | tail -n 1 || true) + [[ -n "$value" ]] || return 1 + [[ "$value" == *"docker-compose.arm.yml"* ]] +} diff --git a/scripts/_shared_start_tasks.sh b/scripts/_shared_start_tasks.sh index 679c66a5..cdc7bc82 100755 --- a/scripts/_shared_start_tasks.sh +++ b/scripts/_shared_start_tasks.sh @@ -5,6 +5,8 @@ START_SCRIPT_NAME=$(basename "$0") # shellcheck source=scripts/_console_output.sh source "$SHARED_SCRIPT_DIR/_console_output.sh" +# shellcheck source=scripts/_env_helpers.sh +source "$SHARED_SCRIPT_DIR/_env_helpers.sh" print_usage() { cat <" + yt_detail " Keep the named volume: mv ./database ./database.unused.\$(date +%Y%m%d)" + yt_detail "" + yt_detail "If you are migrating from the bind mount to a named volume for the first time," + yt_detail "use the helper instead: ./scripts/migrate-to-named-volume.sh" + exit 1 + fi + + if [ "$USE_ARM" == "true" ]; then + if [ "$DATABASE_HAS_CONTENT" == "true" ]; then + # Existing bind-mounted installs keep using their current database even + # when --arm is passed. Silently switching would make their data appear + # to vanish behind an empty named volume. + unset COMPOSE_FILES + yt_warn "--arm requested, but bind-mounted MariaDB data was detected in ./database/." + yt_warn "Keeping the existing bind-mounted database for this run." + yt_detail "To move this install to named-volume storage, run:" + yt_detail " ./scripts/migrate-to-named-volume.sh" + else + # Explicit --arm uses the named-volume override when there is no bind data to preserve. + prepare_named_volume_compose_selection + fi + elif [ "$DETECTED_ARM" == "true" ]; then + if [ "$DATABASE_HAS_CONTENT" == "true" ]; then + # Existing bind-mounted installs keep using their current database on ARM. + unset COMPOSE_FILES + yt_warn "ARM host detected, but bind-mounted MariaDB data was detected in ./database/." + yt_warn "Keeping the existing bind-mounted database for this run." + yt_detail "To move this install to named-volume storage, run:" + yt_detail " ./scripts/migrate-to-named-volume.sh" + else + # ARM fresh installs use the named-volume override. + prepare_named_volume_compose_selection + yt_info "ARM host detected; using named-volume database storage." + fi + elif [ "$NAMED_VOLUME_EXISTS" == "true" ]; then + # An existing named volume with no bind-mount data: keep using the named volume. + # This is the common case after a successful migration, or after the user manually + # switched to the named-volume override on a previous run. + prepare_named_volume_compose_selection + yt_info "Existing named-volume database detected; using named-volume storage." + elif [ "$DATABASE_HAS_CONTENT" != "true" ]; then + # Fresh bundled-DB installs use the named-volume override on every platform. + prepare_named_volume_compose_selection + yt_info "Fresh install detected; using named-volume database storage." + else + # Default - use standard docker-compose.yml (existing bind-mounted install). + unset COMPOSE_FILES + yt_warn "Bind-mounted MariaDB data detected in ./database/." + yt_detail "Docker Desktop on Windows/Mac and some NAS/virtualized filesystems can corrupt bind-mounted MariaDB during schema migrations." + yt_detail "Linux native Docker hosts are usually unaffected. To migrate to a named volume, run:" + yt_detail " ./scripts/migrate-to-named-volume.sh" + fi fi yt_section "Docker" +# shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_FILES intentionally expand into command/flag words. $COMPOSE_CMD $COMPOSE_FILES down yt_success "Existing containers stopped." diff --git a/scripts/_start_template.sh b/scripts/_start_template.sh index b378c5ff..a718d361 100755 --- a/scripts/_start_template.sh +++ b/scripts/_start_template.sh @@ -45,15 +45,8 @@ fi # shellcheck source=scripts/_shared_start_tasks.sh source "$SCRIPT_DIR/_shared_start_tasks.sh" "$@" -# Detect ARM architecture (Apple Silicon, Raspberry Pi, etc.) -ARCH=$(uname -m) -IS_ARM=false -if [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]]; then - IS_ARM=true - yt_info "Detected ARM architecture ($ARCH) - using named volume for MariaDB" -fi - # Bring up the stack using the selected compose files (COMPOSE_FILES is set in _shared_start_tasks.sh) +# shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_FILES intentionally expand into command/flag words. $COMPOSE_CMD $COMPOSE_FILES up -d yt_section "Environment" diff --git a/scripts/backup.sh b/scripts/backup.sh index b9b2db5f..057b470c 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -6,6 +6,8 @@ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=scripts/_console_output.sh source "$SCRIPT_DIR/_console_output.sh" +# shellcheck source=scripts/_env_helpers.sh +source "$SCRIPT_DIR/_env_helpers.sh" # Default configuration OUTPUT_DIR="$PROJECT_DIR/backups" @@ -112,7 +114,8 @@ fi COMPOSE_CMD=$(get_compose_command) yt_info "Docker compose command: $COMPOSE_CMD" -# Detect ARM architecture +# Detect ARM architecture for backup metadata only. Database storage selection +# is based on the configured/actual Youtarr storage mode below. ARCH=$(uname -m) IS_ARM=false if [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]]; then @@ -209,11 +212,27 @@ fi yt_section "Database Backup" mkdir -p "$BACKUP_DIR/database" -# Determine compose args based on architecture (needed for starting/stopping db) -if [[ "$IS_ARM" == "true" ]]; then - COMPOSE_ARGS="-f $PROJECT_DIR/docker-compose.yml -f $PROJECT_DIR/docker-compose.arm.yml" -else - COMPOSE_ARGS="-f $PROJECT_DIR/docker-compose.yml" +DB_STORAGE_MODE=$(youtarr_detect_bundled_db_storage_mode "$PROJECT_DIR") +if [[ "$DB_STORAGE_MODE" == "ambiguous" ]]; then + if [[ "$DB_RUNNING" == "true" ]]; then + yt_warn "Both ./database/ and the named MariaDB volume exist; dumping the currently running database container." + else + yt_error "Ambiguous database storage: both ./database/ and the Docker named volume exist." + yt_detail "Refusing to start a database for backup because that could dump the wrong storage." + yt_detail "Start Youtarr normally and re-run backup, or remove the unused storage first." + exit 1 + fi +fi + +if [[ "$DB_STORAGE_MODE" == "named-volume" ]]; then + yt_info "Backup storage mode: named-volume database." +elif [[ "$DB_STORAGE_MODE" == "bind-mount" ]]; then + yt_info "Backup storage mode: bind-mounted ./database/." +fi + +COMPOSE_ARGS="" +if [[ "$DB_STORAGE_MODE" != "ambiguous" ]]; then + COMPOSE_ARGS=$(youtarr_compose_args_for_storage_mode "$PROJECT_DIR" "$DB_STORAGE_MODE") fi DB_STARTED_FOR_BACKUP=false @@ -225,6 +244,7 @@ if [[ "$DB_RUNNING" == "false" ]]; then export YOUTUBE_OUTPUT_DIR="${YOUTUBE_OUTPUT_DIR:-/tmp}" # Start only the database container + # shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_ARGS intentionally expand into command/flag words. (cd "$PROJECT_DIR" && $COMPOSE_CMD $COMPOSE_ARGS up -d youtarr-db) DB_STARTED_FOR_BACKUP=true @@ -243,6 +263,7 @@ if [[ "$DB_RUNNING" == "false" ]]; then if [[ $WAITED -ge $MAX_WAIT ]]; then yt_error "Database failed to become ready within ${MAX_WAIT}s" # Clean up + # shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_ARGS intentionally expand into command/flag words. (cd "$PROJECT_DIR" && $COMPOSE_CMD $COMPOSE_ARGS down) rm -rf "$STAGING_DIR" exit 1 @@ -266,6 +287,7 @@ else yt_error "Database dump failed" # Clean up if we started the container if [[ "$DB_STARTED_FOR_BACKUP" == "true" ]]; then + # shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_ARGS intentionally expand into command/flag words. (cd "$PROJECT_DIR" && $COMPOSE_CMD $COMPOSE_ARGS down) fi rm -rf "$STAGING_DIR" @@ -275,6 +297,7 @@ fi # Stop database if we started it if [[ "$DB_STARTED_FOR_BACKUP" == "true" ]]; then yt_info "Stopping database container..." + # shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_ARGS intentionally expand into command/flag words. (cd "$PROJECT_DIR" && $COMPOSE_CMD $COMPOSE_ARGS down) yt_success "Database container stopped" fi diff --git a/scripts/migrate-to-named-volume.sh b/scripts/migrate-to-named-volume.sh new file mode 100755 index 00000000..f753f360 --- /dev/null +++ b/scripts/migrate-to-named-volume.sh @@ -0,0 +1,572 @@ +#!/usr/bin/env bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# shellcheck source=scripts/_console_output.sh +source "$SCRIPT_DIR/_console_output.sh" +# shellcheck source=scripts/_env_helpers.sh +source "$SCRIPT_DIR/_env_helpers.sh" + +FORCE=false +STAGING_DIR="" +COMPOSE_CMD="" +ENV_BACKUP_PATH="" +BACKUP_DIR_NAME="" + +print_usage() { + cat </ + - .env is snapshotted to ./.env.bak. + - .env is updated so future docker compose and ./start.sh runs use the + named-volume override + +Options: + --force Skip the confirmation prompt + --help Show this help message +EOF +} + +cleanup() { + if [[ -n "$STAGING_DIR" ]] && [[ -d "$STAGING_DIR" ]]; then + rm -rf "$STAGING_DIR" 2>/dev/null || true + fi +} +trap cleanup EXIT + +print_error_log() { + local log_file="$1" + if [[ -s "$log_file" ]]; then + yt_detail "Error output (last 40 lines):" + while IFS= read -r line; do + yt_detail " $line" + done < <(tail -n 40 "$log_file") + fi +} + +get_compose_command() { + if docker compose version &>/dev/null; then + echo "docker compose" + elif docker-compose version &>/dev/null; then + echo "docker-compose" + else + yt_error "Neither 'docker compose' nor 'docker-compose' command found." + exit 1 + fi +} + +get_env_value() { + local key="$1" + local default="${2:-}" + local value="${!key:-}" + + if [[ -n "$value" ]]; then + printf '%s' "$value" + return + fi + + if [[ -f "$PROJECT_DIR/.env" ]]; then + value=$(grep -E "^[[:space:]]*${key}[[:space:]]*=" "$PROJECT_DIR/.env" 2>/dev/null \ + | tail -n 1 \ + | sed -E "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*//" \ + | sed -E 's/[[:space:]]+#.*$//' \ + | sed -E 's/[[:space:]]+$//' \ + | sed -E 's/^"(.*)"$/\1/' \ + | sed -E "s/^'(.*)'$/\1/" || true) + fi + + if [[ -n "$value" ]]; then + printf '%s' "$value" + else + printf '%s' "$default" + fi +} + +database_dir_has_real_content() { + [[ -f "$PROJECT_DIR/database/ibdata1" || -d "$PROJECT_DIR/database/mysql" ]] +} + +# Single, consistent message for any failure that happens after we have +# renamed ./database/ to the timestamped backup but before the migration is +# fully verified. .env has not been touched at this point, so the user can +# get back to a clean pre-migration state with a few commands. +print_post_rename_failure_help() { + yt_detail "" + yt_detail "Your .env file was NOT modified." + yt_detail "Your original database is preserved at ./$BACKUP_DIR_NAME/." + yt_detail "" + yt_detail "To return to the pre-migration state:" + yt_detail " 1. Restore the database directory:" + yt_detail " mv ./$BACKUP_DIR_NAME ./database" + yt_detail " 2. Remove the partially-populated named volume:" + yt_detail " docker volume ls --format '{{.Name}}' | grep youtarr-db-data" + yt_detail " docker volume rm " + yt_detail " 3. Start normally:" + yt_detail " ./start.sh" +} + +wait_for_db_ready() { + local db_user="$1" + local db_password="$2" + local db_port="$3" + local max_wait=180 + local waited=0 + + while [[ $waited -lt $max_wait ]]; do + if docker exec youtarr-db mysqladmin ping -h localhost -P "$db_port" -u "$db_user" -p"$db_password" &>/dev/null; then + if docker exec youtarr-db mysql -h 127.0.0.1 -P "$db_port" -u "$db_user" -p"$db_password" -e "SELECT 1;" &>/dev/null; then + return 0 + fi + fi + sleep 2 + waited=$((waited + 2)) + done + + return 1 +} + +# Distinguishes "MariaDB never came up" from "MariaDB is up but our credentials are wrong." +# Probes with a deliberately-bogus user; "Access denied" means the server is alive +# and answering, so the failure mode is auth, not startup. +db_running_but_auth_rejected() { + local db_port="$1" + local probe_output + probe_output=$(docker exec youtarr-db mysqladmin ping \ + -h 127.0.0.1 -P "$db_port" \ + -u __youtarr_migrate_probe -p__not_a_real_password 2>&1) || true + [[ "$probe_output" == *"Access denied"* ]] +} + +print_db_auth_failure_hint() { + yt_detail "MariaDB is running, but the credentials in .env did not authenticate." + yt_detail "The bundled compose seeds MYSQL_ROOT_PASSWORD from DB_ROOT_PASSWORD on the very first DB" + yt_detail "init only; subsequent edits to DB_ROOT_PASSWORD do not change an existing DB. The app connects with DB_USER" + yt_detail "and DB_PASSWORD, so DB_PASSWORD in .env must match the password that was actually seeded." + yt_detail "Verify which password Youtarr currently uses to connect, set DB_PASSWORD in .env to that value," + yt_detail "then re-run this script." +} + +stop_started_db() { + local compose_file_args="$1" + if [[ -n "$COMPOSE_CMD" ]]; then + # shellcheck disable=SC2086 # COMPOSE_CMD and compose_file_args intentionally expand into command/flag words. + (cd "$PROJECT_DIR" && $COMPOSE_CMD $compose_file_args down) >/dev/null 2>&1 || true + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --force) + FORCE=true + shift + ;; + --help) + print_usage + exit 0 + ;; + *) + yt_error "Unknown option: $1" + print_usage + exit 1 + ;; + esac +done + +yt_banner "Youtarr DB Migration: bind mount to named volume" + +if [[ ! -f "$PROJECT_DIR/docker-compose.yml" || ! -f "$PROJECT_DIR/docker-compose.arm.yml" ]]; then + yt_error "docker-compose.yml and docker-compose.arm.yml must both exist." + exit 1 +fi + +if [[ ! -f "$PROJECT_DIR/.env" ]]; then + yt_error "No .env file found. Run ./start.sh once before migrating." + exit 1 +fi + +if ! docker info >/dev/null 2>&1; then + yt_error "Docker does not appear to be running." + exit 1 +fi + +if ! database_dir_has_real_content; then + yt_error "No bind-mounted MariaDB data found in ./database/." + yt_detail "This script only migrates existing bind-mounted bundled MariaDB installs." + exit 1 +fi + +COMPOSE_FILE_VALUE=$(get_env_value "COMPOSE_FILE" "") +if [[ "$COMPOSE_FILE_VALUE" == *"docker-compose.external-db.yml"* || "${COMPOSE_FILE:-}" == *"docker-compose.external-db.yml"* ]]; then + yt_error "External database configuration detected." + yt_detail "This script only migrates the bundled MariaDB container." + exit 1 +fi + +if [[ "$COMPOSE_FILE_VALUE" == *"docker-compose.arm.yml"* || "${COMPOSE_FILE:-}" == *"docker-compose.arm.yml"* ]]; then + yt_error "Named-volume override already appears to be configured." + yt_detail "Refusing to guess whether ./database/ is active or leftover data." + exit 1 +fi + +if [[ -n "${COMPOSE_FILE:-}" ]]; then + yt_error "COMPOSE_FILE is set in your shell environment: ${COMPOSE_FILE}" + yt_detail "Unset it before migrating so the .env named-volume pin will take effect." + yt_detail "Run: unset COMPOSE_FILE" + exit 1 +fi + +if [[ -n "${COMPOSE_PATH_SEPARATOR:-}" && "${COMPOSE_PATH_SEPARATOR}" != ":" ]]; then + yt_error "COMPOSE_PATH_SEPARATOR is set in your shell environment to: ${COMPOSE_PATH_SEPARATOR}" + yt_detail "Unset it or set it to ':' before migrating so Compose reads the new .env pin correctly." + exit 1 +fi + +COMPOSE_FILE_TRIMMED=$(printf '%s' "$COMPOSE_FILE_VALUE" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//') +if [[ -n "$COMPOSE_FILE_TRIMMED" && "$COMPOSE_FILE_TRIMMED" != "docker-compose.yml" ]]; then + yt_error "Custom COMPOSE_FILE detected: $COMPOSE_FILE_VALUE" + yt_detail "This migration only supports the default bundled MariaDB stack." + yt_detail "Restore custom compose behavior from .env after migration, or migrate manually." + exit 1 +fi + +COMPOSE_PATH_SEPARATOR_VALUE=$(get_env_value "COMPOSE_PATH_SEPARATOR" "") +if [[ -n "$COMPOSE_FILE_TRIMMED" || -n "$COMPOSE_PATH_SEPARATOR_VALUE" ]]; then + yt_warn "Existing COMPOSE_FILE/COMPOSE_PATH_SEPARATOR settings will be replaced in .env." + yt_detail "A full .env snapshot will be written before any change: ./.env.bak." +fi + +COMPOSE_CMD=$(get_compose_command) +yt_info "Docker compose command: $COMPOSE_CMD" + +EXPECTED_DB_VOLUME="$(youtarr_expected_db_volume_name "$PROJECT_DIR")" +if docker volume inspect "$EXPECTED_DB_VOLUME" >/dev/null 2>&1; then + yt_error "Named-volume database already exists: $EXPECTED_DB_VOLUME" + yt_detail "Remove it only if you are certain it is empty or disposable, then re-run this script." + exit 1 +fi + +# Pre-flight permissions check. We need to write a .env backup and rename +# ./database/ to ./database.bind-mount-backup./. If the user can't do +# either directly, we want to know now (before the dump) rather than stalling +# on an interactive sudo prompt mid-migration on a headless box. +USE_SUDO_FOR_FS=false +PRE_FLIGHT_TEST_PATH="$PROJECT_DIR/.youtarr-migrate-write-test.$$" +if touch "$PRE_FLIGHT_TEST_PATH" 2>/dev/null; then + rm -f "$PRE_FLIGHT_TEST_PATH" +else + if sudo -n true 2>/dev/null; then + USE_SUDO_FOR_FS=true + yt_warn "Cannot write to $PROJECT_DIR directly; will use passwordless sudo for filesystem operations." + else + yt_error "Cannot write to $PROJECT_DIR and passwordless sudo is not available." + yt_detail "This migration must rename ./database/ to ./database.bind-mount-backup./" + yt_detail "and write a .env backup, both in $PROJECT_DIR." + yt_detail "" + yt_detail "Resolve one of these and re-run:" + yt_detail " - Re-run the script as root: sudo $(basename "$0")" + yt_detail " - Configure passwordless sudo for the current user" + yt_detail " - Make $PROJECT_DIR writable by the current user (chown / chmod)" + exit 1 + fi +fi + +DB_USER=$(get_env_value "DB_USER" "root") +DB_PASSWORD=$(get_env_value "DB_PASSWORD" "123qweasd") +DB_ROOT_PASSWORD=$(get_env_value "DB_ROOT_PASSWORD" "123qweasd") +DB_NAME=$(get_env_value "DB_NAME" "youtarr") +DB_PORT=$(get_env_value "DB_PORT" "3321") +YOUTUBE_OUTPUT_DIR_VALUE=$(get_env_value "YOUTUBE_OUTPUT_DIR" "/tmp") +export YOUTUBE_OUTPUT_DIR="$YOUTUBE_OUTPUT_DIR_VALUE" + +if [[ "$DB_USER" == "root" && "$DB_ROOT_PASSWORD" != "$DB_PASSWORD" ]]; then + yt_error "DB_ROOT_PASSWORD and DB_PASSWORD differ in .env." + yt_detail "The fresh named-volume MariaDB initializes root from DB_ROOT_PASSWORD," + yt_detail "but this migration connects as root using DB_PASSWORD." + yt_detail "Set DB_ROOT_PASSWORD to the same value as DB_PASSWORD before migrating, then re-run this script." + exit 1 +fi + +TIMESTAMP=$(date +"%Y%m%d-%H%M%S") +BACKUP_DIR_NAME="database.bind-mount-backup.$TIMESTAMP" +ENV_BACKUP_PATH="$PROJECT_DIR/.env.bak.$TIMESTAMP" +STAGING_DIR=$(mktemp -d) +DUMP_PATH="$STAGING_DIR/youtarr-$TIMESTAMP.sql" +ERROR_LOG="$STAGING_DIR/error.log" + +yt_section "Plan" +yt_info "Bind-mount source: ./database/" +yt_info "Preserved as: ./$BACKUP_DIR_NAME/" +yt_info ".env snapshot: ./.env.bak.$TIMESTAMP" +yt_info "Target storage: Docker named volume from docker-compose.arm.yml" + +if [[ "$FORCE" != "true" ]]; then + echo "" + read -r -p "Type 'MIGRATE' to proceed or anything else to abort: " confirmation + if [[ "$confirmation" != "MIGRATE" ]]; then + yt_info "Aborted. Nothing changed." + exit 0 + fi +fi + +yt_section "Stopping Youtarr" +if [[ -x "$PROJECT_DIR/stop.sh" ]]; then + (cd "$PROJECT_DIR" && ./stop.sh) || yt_warn "stop.sh returned non-zero; continuing." +else + (cd "$PROJECT_DIR" && $COMPOSE_CMD down) || yt_warn "compose down returned non-zero; continuing." +fi + +yt_section "Dumping Bind-Mounted Database" +(cd "$PROJECT_DIR" && $COMPOSE_CMD -f docker-compose.yml up -d youtarr-db) + +if ! wait_for_db_ready "$DB_USER" "$DB_PASSWORD" "$DB_PORT"; then + yt_error "Bind-mounted database did not become ready." + if db_running_but_auth_rejected "$DB_PORT"; then + print_db_auth_failure_hint + fi + stop_started_db "-f docker-compose.yml" + exit 1 +fi + +: > "$ERROR_LOG" +if ! SOURCE_TABLE_COUNT=$(docker exec youtarr-db mysql -N -B -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" \ + -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME';" 2>"$ERROR_LOG"); then + yt_error "Could not inspect source database tables." + print_error_log "$ERROR_LOG" + stop_started_db "-f docker-compose.yml" + exit 1 +fi + +if [[ ! "$SOURCE_TABLE_COUNT" =~ ^[0-9]+$ ]]; then + yt_error "Unexpected source table count: $SOURCE_TABLE_COUNT" + stop_started_db "-f docker-compose.yml" + exit 1 +fi + +if [[ "$SOURCE_TABLE_COUNT" -lt 1 ]]; then + yt_warn "Source database has no tables; nothing needs to be migrated." + yt_detail "For a fresh named-volume install, stop the stack, move or remove ./database/, then run ./start.sh." + stop_started_db "-f docker-compose.yml" + exit 0 +fi + +: > "$ERROR_LOG" +if ! docker exec youtarr-db mysqldump \ + --single-transaction \ + --routines --triggers --events \ + -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" > "$DUMP_PATH" 2>"$ERROR_LOG"; then + yt_error "mysqldump failed. Your ./database/ data was not modified." + print_error_log "$ERROR_LOG" + stop_started_db "-f docker-compose.yml" + exit 1 +fi + +yt_success "Database dump created." + +# Capture per-table row counts from the source DB so we can verify the import is +# complete (not just structurally valid). A truncated dump (disk full, broken +# pipe) can produce a syntactically valid SQL file with all CREATE TABLEs but +# missing INSERTs; the table-count check alone wouldn't notice that. +SOURCE_COUNTS_FILE="$STAGING_DIR/source_counts.tsv" +: > "$SOURCE_COUNTS_FILE" +: > "$ERROR_LOG" +SOURCE_TABLE_LIST=$(docker exec youtarr-db mysql -N -B -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" \ + -e "SELECT table_name FROM information_schema.tables WHERE table_schema='$DB_NAME' AND table_type='BASE TABLE';" 2>"$ERROR_LOG") || { + yt_error "Could not enumerate source tables for verification." + print_error_log "$ERROR_LOG" + stop_started_db "-f docker-compose.yml" + exit 1 +} + +while IFS= read -r table; do + [ -z "$table" ] && continue + count=$(docker exec youtarr-db mysql -N -B -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" \ + -e "SELECT COUNT(*) FROM \`$table\`;" 2>"$ERROR_LOG") || { + yt_error "Could not count rows in source table: $table" + print_error_log "$ERROR_LOG" + stop_started_db "-f docker-compose.yml" + exit 1 + } + printf '%s\t%s\n' "$table" "$count" >> "$SOURCE_COUNTS_FILE" +done <<< "$SOURCE_TABLE_LIST" + +yt_success "Captured row counts for $(wc -l < "$SOURCE_COUNTS_FILE" | tr -d ' ') source tables." +stop_started_db "-f docker-compose.yml" + +yt_section "Preserving Bind-Mount Directory" +RENAME_RC=0 +if [ "$USE_SUDO_FOR_FS" = "true" ]; then + sudo -n mv "$PROJECT_DIR/database" "$PROJECT_DIR/$BACKUP_DIR_NAME" || RENAME_RC=$? +else + mv "$PROJECT_DIR/database" "$PROJECT_DIR/$BACKUP_DIR_NAME" || RENAME_RC=$? +fi +if [ "$RENAME_RC" -ne 0 ]; then + yt_error "Failed to rename ./database/. No configuration changes were made." + exit 1 +fi +if [ "$USE_SUDO_FOR_FS" = "true" ]; then + yt_success "Renamed ./database/ to ./$BACKUP_DIR_NAME/ (using sudo)" +else + yt_success "Renamed ./database/ to ./$BACKUP_DIR_NAME/" +fi + +# IMPORTANT: .env is NOT modified here. We hold off on rewriting .env until +# after the import is fully verified, so that any failure in this section +# leaves the user's .env intact and recovery is just a directory rename. +# The migration steps below all use explicit -f flags and do not depend on +# the .env pin to function. + +yt_section "Importing Into Named Volume" +(cd "$PROJECT_DIR" && $COMPOSE_CMD -f docker-compose.yml -f docker-compose.arm.yml up -d youtarr-db) + +if ! wait_for_db_ready "$DB_USER" "$DB_PASSWORD" "$DB_PORT"; then + yt_error "Named-volume database did not become ready." + if db_running_but_auth_rejected "$DB_PORT"; then + print_db_auth_failure_hint + fi + print_post_rename_failure_help + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi + +# Belt-and-suspenders: the up-front docker-volume-inspect check guesses the project +# name; if Compose ended up reusing a stale volume under a slightly different name, +# our DROP/CREATE below would wipe whatever was in it. Refuse instead. +TARGET_TABLE_COUNT=$(docker exec youtarr-db mysql -N -B -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" \ + -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME';" 2>/dev/null || echo "") +if [[ "$TARGET_TABLE_COUNT" =~ ^[0-9]+$ ]] && [[ "$TARGET_TABLE_COUNT" -gt 0 ]]; then + yt_error "Target named-volume database is not empty: $TARGET_TABLE_COUNT tables already in '$DB_NAME'." + yt_detail "Refusing to overwrite an existing volume." + print_post_rename_failure_help + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi + +: > "$ERROR_LOG" +if ! docker exec youtarr-db mysql -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" \ + -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>"$ERROR_LOG"; then + yt_error "Failed to prepare named-volume database for import." + print_error_log "$ERROR_LOG" + print_post_rename_failure_help + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi + +: > "$ERROR_LOG" +if ! docker exec -i youtarr-db mysql -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" < "$DUMP_PATH" 2>"$ERROR_LOG"; then + yt_error "SQL import failed." + print_error_log "$ERROR_LOG" + print_post_rename_failure_help + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi + +TABLE_COUNT=$(docker exec youtarr-db mysql -N -B -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" \ + -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME';" 2>/dev/null || echo "0") +if [[ ! "$TABLE_COUNT" =~ ^[0-9]+$ ]] || [[ "$TABLE_COUNT" -lt 1 ]]; then + yt_error "Import verification failed: no tables found." + print_post_rename_failure_help + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi + +if [[ "$TABLE_COUNT" != "$SOURCE_TABLE_COUNT" ]]; then + yt_error "Import verification failed: source had $SOURCE_TABLE_COUNT tables, target has $TABLE_COUNT." + print_post_rename_failure_help + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi + +# Per-table row count verification. Catches truncated imports that produced all +# tables but only a partial set of rows. +MISMATCH_REPORT="$STAGING_DIR/mismatch.txt" +: > "$MISMATCH_REPORT" +MISMATCH_COUNT=0 +while IFS=$'\t' read -r table src_count; do + [ -z "$table" ] && continue + tgt_count=$(docker exec youtarr-db mysql -N -B -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" \ + -e "SELECT COUNT(*) FROM \`$table\`;" 2>/dev/null) || tgt_count="MISSING" + if [[ "$tgt_count" != "$src_count" ]]; then + printf ' %s: source=%s target=%s\n' "$table" "$src_count" "$tgt_count" >> "$MISMATCH_REPORT" + MISMATCH_COUNT=$((MISMATCH_COUNT + 1)) + fi +done < "$SOURCE_COUNTS_FILE" + +if [ "$MISMATCH_COUNT" -gt 0 ]; then + yt_error "Per-table row count verification failed: $MISMATCH_COUNT table(s) differ." + while IFS= read -r line; do yt_detail "$line"; done < "$MISMATCH_REPORT" + print_post_rename_failure_help + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi + +yt_success "Import verified: $TABLE_COUNT tables, all per-table row counts match source." + +# Verification passed. Now (and only now) update .env so future plain +# `docker compose up -d` runs pick up the named-volume override. +yt_section "Updating .env" +SNAPSHOT_RC=0 +if [ "$USE_SUDO_FOR_FS" = "true" ]; then + sudo -n cp "$PROJECT_DIR/.env" "$ENV_BACKUP_PATH" || SNAPSHOT_RC=$? +else + cp "$PROJECT_DIR/.env" "$ENV_BACKUP_PATH" || SNAPSHOT_RC=$? +fi +if [ "$SNAPSHOT_RC" -ne 0 ]; then + yt_error "Failed to snapshot .env to $ENV_BACKUP_PATH." + yt_detail "Migration data is in place, but .env was not pinned. Either retry the snapshot manually" + yt_detail "or pin the override yourself by adding these two lines to .env:" + yt_detail " COMPOSE_PATH_SEPARATOR=:" + yt_detail " COMPOSE_FILE=docker-compose.yml:docker-compose.arm.yml" + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi + +PIN_ARGS=(--force) +if [ "$USE_SUDO_FOR_FS" = "true" ]; then + PIN_ARGS+=(--use-sudo) +fi +if ! youtarr_pin_named_volume_in_env "$PROJECT_DIR/.env" "${PIN_ARGS[@]}"; then + yt_error "Failed to update .env." + yt_detail "Restore .env with: mv ./.env.bak.$TIMESTAMP .env" + yt_detail "Then add these two lines to .env manually:" + yt_detail " COMPOSE_PATH_SEPARATOR=:" + yt_detail " COMPOSE_FILE=docker-compose.yml:docker-compose.arm.yml" + stop_started_db "-f docker-compose.yml -f docker-compose.arm.yml" + exit 1 +fi +yt_success "Pinned docker-compose.arm.yml named-volume override in .env." + +yt_section "Starting Full Stack" +if (cd "$PROJECT_DIR" && $COMPOSE_CMD -f docker-compose.yml -f docker-compose.arm.yml up -d); then + yt_success "Youtarr is now running on the named-volume database." +else + yt_warn "Could not auto-start the full Youtarr stack." + yt_detail "Bring it up manually with: ./start.sh" +fi + +yt_section "Migration Complete" +yt_success "Youtarr is now using the named-volume database override." +yt_detail "" +yt_detail "Going forward, any of these will start the stack against the named volume:" +yt_detail " Recommended: ./start.sh" +yt_detail " Plain docker compose: docker compose up -d" +yt_detail " (this script pinned COMPOSE_FILE in .env, so plain compose" +yt_detail " will pick up the named-volume override automatically)" +yt_detail " Without the .env pin: docker compose -f docker-compose.yml -f docker-compose.arm.yml up -d" +yt_detail "" +yt_detail "Backups (preserved for safety):" +yt_detail " Original DB: ./$BACKUP_DIR_NAME/" +yt_detail " Original .env: ./.env.bak.$TIMESTAMP" +yt_detail "" +yt_detail "To revert: restore the .env backup, remove the named volume, rename the database backup" +yt_detail "back to ./database, then run ./start.sh." +yt_detail "" +yt_detail "After confirming the install is healthy for a few days, you can reclaim space by removing" +yt_detail "the backups above. This script will not delete them automatically." diff --git a/scripts/restore.sh b/scripts/restore.sh index 6cab73e4..fcbe7b97 100755 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -6,6 +6,8 @@ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=scripts/_console_output.sh source "$SCRIPT_DIR/_console_output.sh" +# shellcheck source=scripts/_env_helpers.sh +source "$SCRIPT_DIR/_env_helpers.sh" # Default configuration FORCE=false @@ -136,9 +138,7 @@ yt_info "Docker compose command: $COMPOSE_CMD" # Detect ARM architecture ARCH=$(uname -m) -IS_ARM=false if [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]]; then - IS_ARM=true yt_info "Detected ARM architecture ($ARCH)" fi @@ -318,17 +318,37 @@ elif [[ -f "$BACKUP_DIR/database/youtarr.sql" ]]; then DB_PORT=$(get_env_value "$PROJECT_DIR/.env" "DB_PORT" "3321") # Need YOUTUBE_OUTPUT_DIR for compose to work - export YOUTUBE_OUTPUT_DIR=$(get_env_value "$PROJECT_DIR/.env" "YOUTUBE_OUTPUT_DIR" "/tmp") + YOUTUBE_OUTPUT_DIR=$(get_env_value "$PROJECT_DIR/.env" "YOUTUBE_OUTPUT_DIR" "/tmp") + export YOUTUBE_OUTPUT_DIR + + DB_STORAGE_MODE=$(youtarr_detect_bundled_db_storage_mode "$PROJECT_DIR") + if [[ "$DB_STORAGE_MODE" == "ambiguous" ]]; then + yt_error "Ambiguous database storage: both ./database/ and the Docker named volume exist." + yt_detail "Refusing to restore because that could overwrite the wrong database." + yt_detail "Remove the unused storage, then re-run restore." + rm -rf "$EXTRACT_DIR" + exit 1 + fi - # Determine compose args based on architecture - if [[ "$IS_ARM" == "true" ]]; then - COMPOSE_ARGS="-f $PROJECT_DIR/docker-compose.yml -f $PROJECT_DIR/docker-compose.arm.yml" + if [[ "$DB_STORAGE_MODE" == "named-volume" ]]; then + yt_info "Restore storage mode: named-volume database." + if youtarr_pin_named_volume_in_env "$PROJECT_DIR/.env"; then + yt_detail "Pinned named-volume override in restored .env." + else + PIN_RC=$? + if [[ "$PIN_RC" -ne 2 ]]; then + yt_warn "Could not pin named-volume override in restored .env." + yt_detail "Start future runs with ./start.sh, or add docker-compose.arm.yml to COMPOSE_FILE manually." + fi + fi else - COMPOSE_ARGS="-f $PROJECT_DIR/docker-compose.yml" + yt_info "Restore storage mode: bind-mounted ./database/." fi + COMPOSE_ARGS=$(youtarr_compose_args_for_storage_mode "$PROJECT_DIR" "$DB_STORAGE_MODE") + # Clear existing database directory for fresh import (only for bind mount, not named volume) - if [[ "$IS_ARM" != "true" ]] && [[ -d "$PROJECT_DIR/database" ]]; then + if [[ "$DB_STORAGE_MODE" == "bind-mount" ]] && [[ -d "$PROJECT_DIR/database" ]]; then yt_info "Clearing existing database directory..." sudo rm -rf "$PROJECT_DIR/database" mkdir -p "$PROJECT_DIR/database" @@ -336,6 +356,7 @@ elif [[ -f "$BACKUP_DIR/database/youtarr.sql" ]]; then # Start database container yt_info "Starting database container..." + # shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_ARGS intentionally expand into command/flag words. (cd "$PROJECT_DIR" && $COMPOSE_CMD $COMPOSE_ARGS up -d youtarr-db) # Wait for database to be healthy @@ -357,6 +378,7 @@ elif [[ -f "$BACKUP_DIR/database/youtarr.sql" ]]; then if [[ $WAITED -ge $MAX_WAIT ]]; then yt_error "Database failed to become ready within ${MAX_WAIT}s" yt_detail "Try starting Youtarr normally with ./start.sh" + # shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_ARGS intentionally expand into command/flag words. (cd "$PROJECT_DIR" && $COMPOSE_CMD $COMPOSE_ARGS down) rm -rf "$EXTRACT_DIR" exit 1 @@ -386,6 +408,7 @@ elif [[ -f "$BACKUP_DIR/database/youtarr.sql" ]]; then else yt_error "Database import failed" yt_detail "Error: $IMPORT_ERROR" + # shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_ARGS intentionally expand into command/flag words. (cd "$PROJECT_DIR" && $COMPOSE_CMD $COMPOSE_ARGS down) rm -rf "$EXTRACT_DIR" exit 1 @@ -393,6 +416,7 @@ elif [[ -f "$BACKUP_DIR/database/youtarr.sql" ]]; then # Stop containers - let user start manually yt_info "Stopping database container..." + # shellcheck disable=SC2086 # COMPOSE_CMD and COMPOSE_ARGS intentionally expand into command/flag words. (cd "$PROJECT_DIR" && $COMPOSE_CMD $COMPOSE_ARGS down) yt_success "Database container stopped" else From 6f01c154e572e7d9dd276d22dc91803827ab9a26 Mon Sep 17 00:00:00 2001 From: dialmaster Date: Wed, 6 May 2026 11:20:06 -0700 Subject: [PATCH 2/3] fix(deps): bump axios to clear high-sev advisory npm audit gate (--audit-level=high --omit=dev) was failing CI on GHSA-w9j2-pvgh-6h63 and related axios 1.x advisories. Run npm audit fix in both root and client/ to advance axios to 1.16.0 and pull forward follow-redirects, ip-address, and express-rate-limit within their existing semver ranges. Lockfile-only change. --- client/package-lock.json | 329 +++++++++++++++++++-------------------- package-lock.json | 29 ++-- 2 files changed, 172 insertions(+), 186 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index f91c3b81..3c532e8f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -149,7 +149,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -775,7 +774,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -799,7 +797,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2019,26 +2016,26 @@ } }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/glob": { @@ -2070,16 +2067,16 @@ } }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/minimatch": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", - "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3669,9 +3666,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", "cpu": [ "arm" ], @@ -3683,9 +3680,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", "cpu": [ "arm64" ], @@ -3697,9 +3694,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", "cpu": [ "arm64" ], @@ -3711,9 +3708,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", "cpu": [ "x64" ], @@ -3725,9 +3722,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", "cpu": [ "arm64" ], @@ -3739,9 +3736,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", "cpu": [ "x64" ], @@ -3753,9 +3750,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", "cpu": [ "arm" ], @@ -3767,9 +3764,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", "cpu": [ "arm" ], @@ -3781,9 +3778,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", "cpu": [ "arm64" ], @@ -3795,9 +3792,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", "cpu": [ "arm64" ], @@ -3809,9 +3806,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", "cpu": [ "loong64" ], @@ -3823,9 +3820,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", "cpu": [ "loong64" ], @@ -3837,9 +3834,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", "cpu": [ "ppc64" ], @@ -3851,9 +3848,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", "cpu": [ "ppc64" ], @@ -3865,9 +3862,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", "cpu": [ "riscv64" ], @@ -3879,9 +3876,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", "cpu": [ "riscv64" ], @@ -3893,9 +3890,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", "cpu": [ "s390x" ], @@ -3907,9 +3904,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", "cpu": [ "x64" ], @@ -3921,9 +3918,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", "cpu": [ "x64" ], @@ -3935,9 +3932,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", "cpu": [ "x64" ], @@ -3949,9 +3946,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", "cpu": [ "arm64" ], @@ -3963,9 +3960,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", "cpu": [ "arm64" ], @@ -3977,9 +3974,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", "cpu": [ "ia32" ], @@ -3991,9 +3988,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", "cpu": [ "x64" ], @@ -4005,9 +4002,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", "cpu": [ "x64" ], @@ -4236,7 +4233,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -4602,7 +4598,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4774,7 +4771,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz", "integrity": "sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4788,7 +4784,6 @@ "version": "18.2.6", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz", "integrity": "sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4799,7 +4794,6 @@ "version": "18.2.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", - "peer": true, "dependencies": { "@types/react": "*" } @@ -5316,9 +5310,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -5443,12 +5437,12 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -5592,9 +5586,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -5634,7 +5628,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6383,7 +6376,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -6526,7 +6520,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6838,15 +6831,16 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -8588,7 +8582,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8618,7 +8611,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -8791,6 +8783,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9457,9 +9450,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -9510,13 +9503,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10035,9 +10028,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -10081,9 +10074,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -10100,7 +10093,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10358,7 +10350,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10415,7 +10406,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -10626,9 +10616,9 @@ } }, "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10782,12 +10772,11 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10799,31 +10788,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" } }, @@ -11020,7 +11009,6 @@ "integrity": "sha512-yueTpl5YJqLzQqs3CanxNdAAfFU23iP0j+JVJURE4ghfEtRmWfWoZWLGkVcyjmgum7UmjwAlqRuOjQDNvH89kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -11422,9 +11410,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -11455,9 +11443,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -11641,7 +11629,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11739,7 +11726,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12058,7 +12044,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/package-lock.json b/package-lock.json index 13c48d3a..5705a6ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2793,12 +2793,12 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -4503,12 +4503,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -4699,15 +4699,16 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -5363,9 +5364,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" From f6c066413c45ea3fcc56d9ba93a595b6d4f880cb Mon Sep 17 00:00:00 2001 From: dialmaster Date: Thu, 7 May 2026 14:36:37 -0700 Subject: [PATCH 3/3] fix(migrate): preserve source db collation Hardcoding the new database to utf8mb4_unicode_ci could silently change the DB-level default collation for installs whose source DB used a different utf8mb4 collation, affecting future ALTERs and any tables created by later migrations. Read DEFAULT_CHARACTER_SET_NAME and DEFAULT_COLLATION_NAME from the source schema before the dump and reuse them in the target's CREATE DATABASE. If the source default is not utf8mb4_*, fall back to utf8mb4_unicode_ci and warn. Either way, log the chosen collation so the user has a breadcrumb. Also rename the "Option 1: Bind Mount (Default)" heading in docs/DATABASE.md; with this PR fresh installs default to the named volume, so calling bind mount the default is misleading. Refs: #598 --- docs/DATABASE.md | 2 +- scripts/migrate-to-named-volume.sh | 33 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 1e3f821c..3f288b2b 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -45,7 +45,7 @@ Youtarr uses MariaDB/MySQL for storing: ### Storage Options -#### Option 1: Bind Mount (Default) +#### Option 1: Bind Mount (legacy / pre-existing installs) ```yaml volumes: - ./database:/var/lib/mysql diff --git a/scripts/migrate-to-named-volume.sh b/scripts/migrate-to-named-volume.sh index f753f360..1fabe6dc 100755 --- a/scripts/migrate-to-named-volume.sh +++ b/scripts/migrate-to-named-volume.sh @@ -357,6 +357,37 @@ if [[ "$SOURCE_TABLE_COUNT" -lt 1 ]]; then exit 0 fi +# Capture the source database's default charset/collation so we can preserve it +# on the target. mysqldump emits per-table CHARSET/COLLATE, but any future +# migrations or ALTER TABLE ... CONVERT TO operations inherit the DB-level +# default; silently switching it can change sort/compare semantics. If the +# source default is not utf8mb4_*, fall back to utf8mb4_unicode_ci and warn. +TARGET_DB_CHARSET="utf8mb4" +TARGET_DB_COLLATION="utf8mb4_unicode_ci" + +: > "$ERROR_LOG" +SOURCE_DB_DEFAULTS=$(docker exec youtarr-db mysql -N -B -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" \ + -e "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$DB_NAME';" 2>"$ERROR_LOG") || { + yt_error "Could not read source database default charset/collation." + print_error_log "$ERROR_LOG" + stop_started_db "-f docker-compose.yml" + exit 1 +} + +SOURCE_DB_CHARSET="" +SOURCE_DB_COLLATION="" +if [[ -n "$SOURCE_DB_DEFAULTS" ]]; then + read -r SOURCE_DB_CHARSET SOURCE_DB_COLLATION <<<"$SOURCE_DB_DEFAULTS" +fi + +if [[ "$SOURCE_DB_CHARSET" == "utf8mb4" && "$SOURCE_DB_COLLATION" == utf8mb4_* ]]; then + TARGET_DB_COLLATION="$SOURCE_DB_COLLATION" + yt_info "Source database default: $SOURCE_DB_CHARSET / $SOURCE_DB_COLLATION (will reuse on target)" +else + yt_warn "Source database default is '${SOURCE_DB_CHARSET:-unknown}' / '${SOURCE_DB_COLLATION:-unknown}'; target will use $TARGET_DB_CHARSET / $TARGET_DB_COLLATION." + yt_detail "Per-table CHARSET/COLLATE in the dump is preserved verbatim, so existing tables keep their column collations. This only affects the DB-level default used by future ALTERs or new tables." +fi + : > "$ERROR_LOG" if ! docker exec youtarr-db mysqldump \ --single-transaction \ @@ -451,7 +482,7 @@ fi : > "$ERROR_LOG" if ! docker exec youtarr-db mysql -h 127.0.0.1 -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" \ - -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>"$ERROR_LOG"; then + -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\` CHARACTER SET $TARGET_DB_CHARSET COLLATE $TARGET_DB_COLLATION;" 2>"$ERROR_LOG"; then yt_error "Failed to prepare named-volume database for import." print_error_log "$ERROR_LOG" print_post_rename_failure_help