viofo_backup: use std/log, error catching, smarter drive select filter, network connectivity, backup destination, space availability check, existing mount check

This commit is contained in:
2025-06-07 14:22:51 -05:00
parent f8572c7f8a
commit 1e2e2935c6

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env nu #!/usr/bin/env nu
use std use std
use std/log
# This script is intended to backup the contents of my Viofo A229 dashcam to my computer, very quickly and efficiently. # This script is intended to backup the contents of my Viofo A229 dashcam to my computer, very quickly and efficiently.
# It is intended to be ran semi-rarely (every month or two), and is also a limited test of the Fish shell/scripting language. # It is intended to be ran semi-rarely (every month or two), and is also a limited test of the Fish shell/scripting language.
# It is intended to be cross-platform, but targets Ubuntu 22.04 LTS (with WSL2 support). # It is intended to be cross-platform, but targets Ubuntu 22.04 LTS (with WSL2 support).
try {
# Configuration details # Configuration details
let host = "roman" # The host to backup to. This is defined in the ~/.ssh/config file. let host = "roman" # The host to backup to. This is defined in the ~/.ssh/config file.
let host_path = "/mnt/user/media/backup/dashcam" # The path on the remote host to backup to. let host_path = "/mnt/user/media/backup/dashcam" # The path on the remote host to backup to.
@@ -13,7 +15,7 @@ let host_path = "/mnt/user/media/backup/dashcam" # The path on the remote host
let required_commands = ["rsync", "sudo", "mount", "umount", "cmd.exe", "ssh"] let required_commands = ["rsync", "sudo", "mount", "umount", "cmd.exe", "ssh"]
for cmd in $required_commands { for cmd in $required_commands {
if (which $cmd | length) == 0 { if (which $cmd | length) == 0 {
print $"Error: Required command ($cmd) not found." log error $"Error: Required command ($cmd) not found."
exit 1 exit 1
} }
} }
@@ -22,53 +24,127 @@ for cmd in $required_commands {
let host_name: string = (ssh -G $host | lines | find -r "^hostname\\s+" | str trim | split column " " | get column2).0 let host_name: string = (ssh -G $host | lines | find -r "^hostname\\s+" | str trim | split column " " | get column2).0
# Check network connectivity to backup target # Check network connectivity to backup target
print "Checking network connectivity to backup server..." log debug "Checking network connectivity to backup server..."
try { let ping_check = ping -c 1 $host_name | complete
ping -c 1 $host_name if $ping_check.exit_code != 0 {
} catch { log error $"Error: Could not verify network connectivity to '($host_name)'"
print $"Error: Cannot reach backup server '($host_name)'" log error $ping_check.stderr
exit 1 exit 1
} }
# Check if backup destination exists and is writable # Check if backup destination exists and is writable
print "Checking backup destination..." log debug "Checking backup destination..."
try { try {
ssh $host "test -d $host_path && test -w $host_path" ssh $host "test -d $host_path && test -w $host_path"
} catch { } catch {
print "Error: Backup destination is not accessible or writable" log error "Error: Backup destination is not accessible or writable"
exit 1 exit 1
} }
# Check available space on backup destination # Check available space on backup destination
print "Checking available space on backup destination..." log debug "Checking available space on backup destination..."
let required_space = 10GB # 10GB in bytes let required_space = 10GB # 10GB in bytes
try { try {
let available_space = ssh $host $"df --output=avail /mnt/user" | lines | skip 1 | str trim | get 0 | append "KB" | str join " " | into filesize let available_space = ssh $host $"df --output=avail /mnt/user" | lines | skip 1 | str trim | get 0 | append "KB" | str join " " | into filesize
if $available_space < $required_space { if $available_space < $required_space {
print $"Error: Insufficient space on backup destination" log error $"Error: Insufficient space on backup destination"
print $"Required: ($required_space), Available: ($available_space)" log error $"Required: ($required_space), Available: ($available_space)"
exit 1 exit 1
} }
print $"Available space: ($available_space)" log debug $"Available space: ($available_space)"
} catch { |err| } catch { |err|
print $"Error: Could not check available space on backup destination: ($err.msg)" log error $"Error: Could not check available space on backup destination: ($err.msg)"
exit 1 exit 1
} }
# Acquire a list of potential mountable drive letters # Acquire a list of potential mountable drive letters
let irrelevant = mount | grep drvfs | split column " " | get column1 | split column ":" | get column1 let mountable = (cd /mnt/c; cmd.exe /C "wmic logicaldisk where DriveType=2 get DeviceID,Name /format:csv" | from csv)
mut mountable = cmd.exe /C "wmic logicaldisk get name" e> (std null-device) | split row "\n" | skip 1 | split column ":" | get column1 | filter {|x| ($irrelevant | find $x | length) == 0} log info "Pick a USB device to mount for Viofo backup:"
print "Pick a USB device to mount for Viofo backup:" # Check that at least one drive is available
let letter = $mountable | input list if ($mountable | length) == 0 {
log error "No USB drives found"
exit 1
}
let drive_letter = $"($letter):" # Have the user choose a drive
let selected_drive = $mountable | input list -d "DeviceID" "Select a drive to mount for backup"
let letter = $selected_drive.DeviceId | str replace --regex ":$" "" | str downcase
let win_drive_path = $"($letter | str upcase):"
# Check if already mounted
log info "Checking if drive is already mounted..."
let findmnt_check = findmnt -J --mountpoint /mnt/($letter) | complete
if $findmnt_check.exit_code == 0 {
log debug "Drive is already mounted"
# Check was successful, meaning something must be mounted there
let mounts = $findmnt_check.stdout | from json
log debug "Mount JSON acquired"
# If multiple mounts are found, print them, then exit
if ($mounts.filesystems | length) > 1 {
log error ("Multiple mounts found, cannot continue \n" + ($mounts | to json --indent 2))
exit 1
}
log debug $"($mounts.filesystems | length) mounts found"
let current_mount = $mounts.filesystems.0
log debug $"Current mount: ($current_mount)"
# Check that the mount is probably already correct
if $current_mount.source != $win_drive_path {
log error "Error: Drive is mounted at incorrect path"
log error $"Mount Source expected ($win_drive_path), but found: ($current_mount.source)"
exit 1
} else if ($current_mount.options | str contains "rw") == false {
log error "Error: Drive is not mounted read-write"
log error $"Mount options: ($current_mount.options)"
exit 1
}
log info $"Mount Details: ($current_mount.options)"
while true {
let continue = input "Continue anyways? (y/n)"
if $continue == "y" {
break
} else if $continue == "n" {
log error "User declined to continue"
exit 1
}
}
}
try {
# Mount the drive # Mount the drive
# TODO: Check if the drive is already mounted log info "Mounting drive (requires sudo)..."
try {
log debug "Preparing mount point folder..."
sudo mkdir --parents /mnt/($letter) sudo mkdir --parents /mnt/($letter)
sudo mount -t drvfs ($drive_letter) /mnt/($letter) -o uid=(id -u $env.USER),gid=(id -g $env.USER),metadata } catch { |err|
log error $"Error: Could not prepare mount point folder: ($err.msg)"
exit 1
}
try {
log debug "Mounting drive..."
sudo mount -t drvfs ($win_drive_path) /mnt/($letter) -o uid=(id -u $env.USER),gid=(id -g $env.USER),metadata
} catch { |err|
log error $"Error: Could not mount drive: ($err.msg)"
exit 1
}
log debug "Drive mounted"
# Verify mount was created
log debug "Verifying mount was successful..."
let mount_check = findmnt -J --mountpoint /mnt/($letter) | complete
if $mount_check.exit_code != 0 {
log error $mount_check.stderr
error "Failed to mount drive"
}
# Test permissions & folder structure # Test permissions & folder structure
let expected_folders = [ let expected_folders = [
@@ -78,49 +154,66 @@ let expected_folders = [
"DCIM/Photo" "DCIM/Photo"
] ]
print "Checking folder structure..." # We don't need to check permissions, the mount won't support them (generally)
log debug "Checking folder structure..."
for folder_suffix in $expected_folders { for folder_suffix in $expected_folders {
# Test folder existence # Test folder existence
let path = $"/mnt/($letter)/($folder_suffix)" let path = $"/mnt/($letter)/($folder_suffix)"
let status = test -d ($path) | complete let status = test -d ($path) | complete
if $status.exit_code != 0 { if $status.exit_code != 0 {
print $"Error: Expected folder ($path) does not exist." error $"Expected folder "($path)" does not exist."
exit 1
} else {
log debug $"Folder "($path)" exists"
}
} }
# TODO: Test folder permissions (READ, EXECUTE required) # Get total size of files to copy
# TODO: Test file permissions (all RO/Photo need READ/WRITE) log debug "Calculating total size of files..."
}
let video_source = $"/mnt/($letter)/DCIM/Movie/RO"
let video_target = $"($host):($host_path)/video"
let video_size = du $video_source | get 0 | get apparent | into filesize
log info $"Video size: ($video_size)"
let photo_source = $"/mnt/($letter)/DCIM/Photo"
let photo_target = $"($host):($host_path)/photo"
let photo_size = du $photo_source | get 0 | get apparent | into filesize
log info $"Photo size: ($photo_size)"
log info $"Total size: ($video_size + $photo_size | into filesize)"
# Invoke rsync to copy the files # Invoke rsync to copy the files
print "Copying video files..." log info "Copying video files..."
try { try {
let source = $"/mnt/($letter)/DCIM/Movie/RO" do -c { rsync -avh --progress --remove-source-files ($video_source) ($video_target) }
let target = $"($host):($host_path)/video"
do -c { rsync -avh --progress --remove-source-files ($source) ($target) }
} catch { } catch {
print $env.LAST_EXIT_CODE error $"Could not copy video files: ($env.LAST_EXIT_CODE)"
} }
print "Copying photo files..." log info "Copying photo files..."
try { try {
let source = $"/mnt/($letter)/DCIM/Photo" do -c { rsync -avh --progress --remove-source-files ($photo_source) ($photo_target) }
let target = $"($host):($host_path)/photo"
do -c { rsync -avh --progress --remove-source-files ($source) ($target) }
} catch { } catch {
print $env.LAST_EXIT_CODE error $"Could not copy photo files: ($env.LAST_EXIT_CODE)"
} }
print "Sync complete." log info "Sync complete."
# Unmount the drive # Unmount the drive
print "Unmounting drive..." log info "Unmounting drive..."
sudo umount /mnt/($letter) sudo umount /mnt/($letter)
sudo rmdir /mnt/($letter) sudo rmdir /mnt/($letter)
# TODO: Check if duplicate mounts exist # TODO: Check if duplicate mounts exist
log info "All backed up."
} catch { |err|
log error $"Error: Could not unmount drive: ($err.msg)"
exit 1
}
print "All backed up."
# TODO: Statistical analysis of file duration # TODO: Statistical analysis of file duration
# On average, how far back do my recordings go? 2 months? # On average, how far back do my recordings go? 2 months?
@@ -128,3 +221,7 @@ print "All backed up."
# If you know the average bitrate, you can use the current space occupied by all files to estimate the total duration you could store on the drive. # If you know the average bitrate, you can use the current space occupied by all files to estimate the total duration you could store on the drive.
# Then, if you can estimate the daily recording duration on average, you can estimate how many days back you can record on average. # Then, if you can estimate the daily recording duration on average, you can estimate how many days back you can record on average.
# Additionally, since this script removes files, it might be better to use the 'full size' of the disk to estimate maximum duration capacity (minus 1GB). # Additionally, since this script removes files, it might be better to use the 'full size' of the disk to estimate maximum duration capacity (minus 1GB).
} catch {|err|
log error $"Uncaught error: ($err.msg)"
exit 1
}