Auto copy zu USB NTFS formatiert
Auto copy script to copy images and temp files to an USB drive formatted with NTFS when plugged in.
This script uses rsync to copy files from the Photobooth data folder to the USB drive when it is plugged in. It also creates a status file that can be used to monitor the copy progress via a web
page.
The script is triggered via a udev rule when an USB drive with NTFS filesystem is plugged in.
Requires:
- python3
- rsync
- ntfs-3g
- udev
Installation
sudo touch /usr/local/bin/fotobox-copy.sh
sudo chmod +x /usr/local/bin/fotobox-copy.sh
sudo nano /usr/local/bin/fotobox-copy.sh
#!/usr/bin/env bash
set -Eeuo pipefail
# Configuration
PART="${1}" # e.g. sdb1
DEV="/dev/${PART}"
SRC="/var/www/html/data"
SUB1="images"
SUB2="tmp"
STATUS="/var/www/html/private/copystatus.json"
# ---------- Status helper ----------
write_status() {
local state="$1" pct="$2" msg="$3"
local tmp="${STATUS}.tmp"
# Create JSON safely
printf '{"state":"%s","percent":%s,"message":%s}\n' \
"$state" "$pct" "$(python3 -c "import json; print(json.dumps('$msg'))")" | sudo tee "$tmp" >/dev/null
sudo mv "$tmp" "$STATUS"
}
fail() {
local code="$1"
local line="$2"
write_status "error" 0 "Error (code ${code}) in line ${line}."
exit "$code"
}
trap 'fail $? $LINENO' ERR
# ---------- Start ----------
write_status "starting" 0 "USB detected: ${DEV}. Prepare..."
# Check if device exists
if [[ ! -b "$DEV" ]]; then
write_status "error" 0 "Device ${DEV} not found."
exit 1
fi
# Mount device if not already mounted
MNT="$(lsblk -no MOUNTPOINT "$DEV" | head -n1 || true)"
if [[ -z "${MNT}" ]]; then
sudo mkdir -p /mnt/usbdrive
# mount mit 'flush' Option für exFAT hilft, Puffer schneller zu leeren
sudo mount -o flush "$DEV" /mnt/usbdrive
MNT="/mnt/usbdrive"
fi
DEST="${MNT}/images"
sudo mkdir -p "$DEST"
# Existenz der Quellordner prüfen
[[ -d "${SRC}/${SUB1}" ]] || { write_status "error" 0 "Source ${SUB1} missing"; exit 2; }
[[ -d "${SRC}/${SUB2}" ]] || { write_status "error" 0 "Source ${SUB2} missing"; exit 2; }
# ---------- Prepare Rsync ----------
# --no-inc-recursive: No jumping progress percentage
# --info=progress2: Compact progress output
RSYNC_OPTS=(
-rltD
--info=progress2
--no-inc-recursive
--no-owner --no-group --no-perms
--no-acls --no-xattrs
--omit-dir-times
--modify-window=2
)
write_status "copying" 1 "Calculate copy size..."
# Run rdync in dry-run to get total size
# stdbuf & tr to handle progress output line by line
set +e
sudo stdbuf -oL rsync "${RSYNC_OPTS[@]}" \
"${SRC}/${SUB1}" "${SRC}/${SUB2}" \
"$DEST/" 2>&1 | tr '\r' '\n' | while IFS= read -r line; do
if [[ "$line" =~ ([0-9]{1,3})% ]]; then
p="${BASH_REMATCH[1]}"
# Scale to 95% to leave room for final sync step
# Prevents user from thinking it hung at 100%
display_p=$(( p * 95 / 100 ))
# Only update every percent to reduce writes
write_status "copying" "$display_p" "Copied ${p}%..."
fi
done
RSYNC_EXIT=${PIPESTATUS[0]}
set -e
if [ "$RSYNC_EXIT" -ne 0 ] && [ "$RSYNC_EXIT" -ne 24 ]; then
fail "$RSYNC_EXIT" "rsync failed"
fi
# ---------- Final sync ----------
write_status "syncing" 96 "Finalise copy..."
# Sync to ensure all data is written to stick to avoid corruption
sync
write_status "done" 100 "Done. You can remove the USB drive now."
# Unmount only if we mounted it
if [[ "${MNT}" == "/mnt/usbdrive" ]]; then
# Give some time to ensure all writes are done
sleep 1
sudo umount /mnt/usbdrive || sudo umount -l /mnt/usbdrive
fi
exit 0
Setup udev rule
echo 'ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="partition", ENV{ID_FS_TYPE}=="ntfs", RUN+="/usr/local/bin/fotobox-copy.sh %k"' | sudo tee /etc/udev/rules.d/99-fotobox-autocopy.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
Create status file and set permissions
sudo touch /var/www/html/private/copystatus.json
sudo chown root:www-data /var/www/html/private/copystatus.json
sudo chmod 664 /var/www/html/private/copystatus.json
sudo chown -R www-data:www-data /var/www/html/private
Create status page to monitor progress in folder private
sudo -u www-data nano /var/www/html/private/status.php
<?php
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kopiervorgang…</title>
</head>
<body style="
background-image: url('/private/images/background/Copy_Background.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
margin: 0;
padding: 0;
overflow-x:hidden;
overflow-y:hidden;">
<div id="wrap" style="min-height:100vh; display:flex; align-items:center; justify-content:center; padding:20px;">
<div style="background:#fff; padding:20px; border-radius:12px; width:min(520px,90vw); box-shadow:0 10px 30px rgba(0,0,0,.3);">
<div id="copyMsg" style="margin-bottom:10px;">Warte auf Status…</div>
<div style="height:18px; background:#eee; border-radius:10px; overflow:hidden;">
<div id="copyBar" style="height:100%; width:0%; background:#3b82f6;"></div>
</div>
<div id="copyPct" style="margin-top:8px; font-size:14px; opacity:.8;">0%</div>
</div>
</div>
<script>
(async function(){
const bar = document.getElementById('copyBar');
const msg = document.getElementById('copyMsg');
const pct = document.getElementById('copyPct');
// Passe ggf. den Pfad an:
const STATUS_URL = '/private/copystatus.json';
const BACK_URL = '../index.php';
async function poll(){
try {
const r = await fetch(STATUS_URL, { cache: 'no-store' });
if(!r.ok) throw new Error('Status page not reachable: ' + r.status);
const s = await r.json();
const state = (s.state || '').toLowerCase();
const p = Math.max(0, Math.min(100, Number(s.percent ?? 0)));
bar.style.width = p + '%';
msg.textContent = s.message || (state ? ('Status: ' + state) : 'Kopiere ');
pct.textContent = p + '%';
if (state === 'done') {
bar.style.width = '100%';
msg.textContent = s.message || 'Fertig.';
pct.textContent = '100%';
setTimeout(() => window.location.href = BACK_URL, 3000);
return;
}
// Optional: handle error state
if (state === 'error') {
msg.textContent = s.message || 'Copy error.';
return;
}
} catch(e) {
msg.textContent = 'No status available.';
// do not jump to status page, it will flicker otherwise
} finally {
setTimeout(poll, 400);
}
}
poll();
})();
</script>
</body>
</html>
Create auto-refresh script to redirect to status page when copy is running
sudo nano /var/www/html/private/copyrun.js
(async function(){
const STATUS_URL = '/private/copystatus.json';
const STATUS_PAGE = '/private/status.php';
async function check(){
try {
const r = await fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store' });
if (!r.ok) throw new Error('no status');
const s = await r.json();
const state = (s.state || '').toLowerCase();
if (state === 'starting' || state === 'copying') {
// >>> HIER passiert der Sprung <<<
window.location.href = STATUS_PAGE;
return;
}
} catch(e) {
// no status file or error - do nothing
}
setTimeout(check, 600);
}
check();
})();