Коли вільного місця на диску стає критично мало, система починає гальмувати та вередувати. Давайте зробимо розумну автоматизацію задач: стискати великі файли та переносити їх на інший диск чи NAS автоматично, щойно простір тане. Сьогодні показую, як зібрати це з inotify, rsync і systemd timers — просто і надійно. Так, це будуть звичайні bash скрипти, але з дорослою дисципліною 😉

Як це працює

Схема така:

  • inotify стежить за папкою з великими файлами та ловить події створення/закриття файлів;
  • скрипт перевіряє використання диска (скільки вільного місця лишилося);
  • якщо вільного місця менше за поріг — стискає великі файли (zstd) і переносить їх rsync на інший том/диск;
  • systemd timers періодично запускають перевірку, навіть якщо події непомітні.

Чому не cron? Можна, але cron та systemd timers — різні інструменти. Тут зручніше саме systemd timers: вони виживають перезавантаження, мають Persistent=true і гарно логуються в journal.

Підготовка

Поставте потрібні утиліти. Приклади для різних дистрибутивів:

# Debian/Ubuntu
sudo apt update && sudo apt install -y inotify-tools rsync zstd

# Fedora
sudo dnf install -y inotify-tools rsync zstd

# Arch
sudo pacman -S --noconfirm inotify-tools rsync zstd

Основний How-to: скрипт + systemd timers

1) Створюємо скрипт

Цей скрипт стискає великі файли з робочої папки та переносить їх на інший диск, коли вільного місця стає менше за заданий поріг. Він уміє працювати в режимі одноразового сканування (scan) і режимі спостереження (watch) через inotify.

sudo mkdir -p /usr/local/bin
sudo nano /usr/local/bin/auto-archive-move.sh
#!/usr/bin/env bash
set -euo pipefail

# Налаштування (підкоригуйте під себе)
WATCH_DIR="/data/incoming"     # де з'являються великі файли
DEST_DIR="/mnt/archive"        # куди переносимо архіви (інший диск/NAS)
STAGING_DIR="/var/tmp/auto-archive"  # тимчасове місце для стискання
SIZE_MIN_MB=512                 # мінімальний розмір файлу для обробки
MIN_FREE_GB=10                  # якщо вільного місця менше або дорівнює — запускаємо
NICE=10                         # пріоритет CPU
IONICE_CLASS=2                  # 2=best-effort
IONICE_PRIORITY=7               # 0..7 (7 — найнижче)
LOG_FILE="/var/log/auto-archive.log"

need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1"; exit 1; }; }
need_cmd inotifywait; need_cmd rsync; need_cmd zstd; need_cmd df; need_cmd awk; need_cmd find; need_cmd du

log() { echo "$(date -Is) $*" | tee -a "$LOG_FILE"; }
free_gb() { df -BG --output=avail "$WATCH_DIR" | tail -1 | tr -dc '0-9'; }

big_enough() { local f="$1"; local sz_mb; sz_mb=$(du -m --apparent-size -- "$f" | cut -f1); [ "$sz_mb" -ge "$SIZE_MIN_MB" ]; }

archive_and_move() {
  local src="$1"
  [ -f "$src" ] || { log "Skip (not a file): $src"; return; }
  local rel="${src#$WATCH_DIR/}"
  local base; base="$(basename -- "$src")"
  local rel_dir; rel_dir="$(dirname -- "$rel")"
  local stage_dir="$STAGING_DIR/$rel_dir"
  local dest_dir="$DEST_DIR/$rel_dir"
  mkdir -p "$stage_dir" "$dest_dir"
  local tmp="$stage_dir/${base}.zst.part"
  local out="$stage_dir/${base}.zst"
  log "Compressing '$src' -> '$out' (zstd)"
  if nice -n "$NICE" ionice -c "$IONICE_CLASS" -n "$IONICE_PRIORITY" \
     zstd -T0 -19 --force -o "$tmp" -- "$src"; then
    mv -f -- "$tmp" "$out"
    log "Rsync to '$dest_dir'"
    rsync -a --remove-source-files -- "$out" "$dest_dir"/
    rm -f -- "$src"
    log "Done: archived and moved '$src'"
  else
    log "Error: compression failed for '$src'"; rm -f -- "$tmp" || true
  fi
}

scan_once() {
  local avail; avail=$(free_gb)
  if [ "$avail" -le "$MIN_FREE_GB" ]; then
    log "Free space ${avail}G <= ${MIN_FREE_GB}G: scanning for big files"
    find "$WATCH_DIR" -type f -size +"${SIZE_MIN_MB}"M -print0 |
      while IFS= read -r -d '' f; do archive_and_move "$f"; done
  else
    log "Free space OK (${avail}G > ${MIN_FREE_GB}G). Nothing to do."
  fi
}

watch_events() {
  log "Watching $WATCH_DIR for new/closed files..."
  inotifywait -m -r -e close_write,create,move --format '%w%f' -- "$WATCH_DIR" |
  while read -r f; do
    if [ -f "$f" ] && big_enough "$f"; then
      if [ "$(free_gb)" -le "$MIN_FREE_GB" ]; then
        archive_and_move "$f"
      else
        log "Event on '$f' but free space OK — skipping for now"
      fi
    fi
  done
}

case "${1:-scan}" in
  scan) scan_once ;;
  watch) watch_events ;;
  *) echo "Usage: $0 [scan|watch]"; exit 1 ;;
esac
sudo chmod +x /usr/local/bin/auto-archive-move.sh
sudo touch /var/log/auto-archive.log && sudo chown root:adm /var/log/auto-archive.log || true

2) Сервіс і таймер systemd

Один сервіс виконує разове сканування, інший — постійно слухає події inotify. Таймер періодично підстраховує.

# auto-archive.service (разовий прогін)
sudo tee /etc/systemd/system/auto-archive.service >/dev/null <<'UNIT'
[Unit]
Description=Auto-archive big files when low free space
RequiresMountsFor=/data/incoming /mnt/archive

[Service]
Type=oneshot
ExecStart=/usr/local/bin/auto-archive-move.sh scan
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

[Install]
WantedBy=multi-user.target
UNIT
# auto-archive.timer (кожні 10 хвилин)
sudo tee /etc/systemd/system/auto-archive.timer >/dev/null <<'UNIT'
[Unit]
Description=Run auto-archive scan every 10 minutes

[Timer]
OnBootSec=5min
OnUnitActiveSec=10min
Persistent=true
Unit=auto-archive.service

[Install]
WantedBy=timers.target
UNIT
# auto-archive-watch.service (постійний моніторинг)
sudo tee /etc/systemd/system/auto-archive-watch.service >/dev/null <<'UNIT'
[Unit]
Description=Watch for new large files and archive on low free space
RequiresMountsFor=/data/incoming /mnt/archive
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/auto-archive-move.sh watch
Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now auto-archive.timer
sudo systemctl enable --now auto-archive-watch.service

# Перевірка
systemctl list-timers | grep auto-archive
journalctl -u auto-archive.service -u auto-archive-watch.service -f

Альтернативні способи

  • systemd.path замість inotify-tools: менш гнучко за подіями, але без додаткових пакетів.
# auto-archive.path – тригерить сервіс при змінах у каталозі
sudo tee /etc/systemd/system/auto-archive.path >/dev/null <<'UNIT'
[Unit]
Description=Trigger auto-archive on changes

[Path]
PathChanged=/data/incoming
Unit=auto-archive.service

[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl enable --now auto-archive.path
  • Швидке стискання: замініть zstd на lz4 для максимальної швидкості (менший коефіцієнт стиснення).
  • Якщо архів і призначення на одному файловому системному томі — можна обійтися без rsync та використовувати mv, але rsync надійніший (перевірка й перезапуск).
  • Для директорій — замініть рядок зі zstd на tar з компресією: tar -I "zstd -T0 -19" -cf file.tar.zst dir/

GUI-спосіб (коли хочете мишкою)

Спробуйте Back In Time (Qt/GTK). Це GUI-утиліта для інкрементних копій, яка вміє за розкладом копіювати файли на інший диск. Налаштуйте правило на вашу папку з великими файлами та винятки.

# Debian/Ubuntu
sudo apt install -y backintime-qt
# Fedora
sudo dnf install -y backintime-qt
# Arch
sudo pacman -S --noconfirm backintime
  1. Запустіть Back In Time, додайте каталог-джерело (наприклад, /data/incoming) і ціль (зовнішній диск).
  2. У розкладі вкажіть періодичність (наприклад, кожні 10–15 хвилин).
  3. Додайте виключення для дрібних файлів (патерн за розміром або за розширеннями).

Це не чистий inotify, але результат дуже схожий і зручно для тих, хто боїться терміналу Linux.

FAQ

Як змінити поріг і розмір файлів?

Угорі скрипта є змінні MIN_FREE_GB та SIZE_MIN_MB. Підставте свої значення й перезапустіть сервіси.

inotifywait: command not found

Встановіть пакет inotify-tools (див. розділ Підготовка) і переконайтеся, що він є в PATH.

Стискання заповнює /var/tmp — що робити?

Змініть STAGING_DIR на розділ із запасом місця (наприклад, на той самий диск, де DEST_DIR), або зменште рівень стиснення.

Як виключити розширення, які вже стискати безглуздо (mp4, jpg)?

Додайте у scan_once фільтр find, наприклад: -not -iname "*.mp4" -not -iname "*.jpg". Або перевіряйте у функції big_enough ще й розширення.

Отримую Permission denied на цільовому диску

Перевірте права доступу (права доступу Linux) і власника DEST_DIR: sudo chown -R root:root /mnt/archive та належні маски дозволів.

Як подивитися швидко використання диска?

Команди: df -h (простір на розділах), du -sh /data/incoming/* (найбільші підкаталоги). Це допоможе налаштувати пороги.

Обмеження inotify: забагато директорій

Підніміть ліміт спостерігачів: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

Rsync на віддалений сервер по SSH?

Задайте DEST_DIR як user@host:/path і додайте ключі SSH. Не забудьте про мережеву надійність та After=network-online.target.

Порада від Kernelka

Щоб не ловити сюрпризів, додайте умову RequiresMountsFor у сервіс (ми вже додали) і тримайте DEST_DIR на окремому фізичному диску. Так ви збережете продуктивність і нерви 🧠

Підсумок

  • Налаштували спостереження inotify та періодичний запуск через systemd timers.
  • Створили надійні bash скрипти для стиснення великих файлів і перенесення через rsync.
  • Врахували використання диска та пороги для автообробки.
  • Розглянули альтернативи (systemd.path, інші компресори) і навіть GUI-вариант.
  • Тепер ваш Linux сам піклується про вільне місце — красиво й без паніки ✨