Skip to content

Instantly share code, notes, and snippets.

@jarulsamy
Last active August 17, 2026 23:04
Show Gist options
  • Select an option

  • Save jarulsamy/d326a32e8db094f0df2a03c84d972c79 to your computer and use it in GitHub Desktop.

Select an option

Save jarulsamy/d326a32e8db094f0df2a03c84d972c79 to your computer and use it in GitHub Desktop.
tmp
@echo off
setlocal enabledelayedexpansion
rem ===========================================================================
rem backup.bat - daily restic backup of a user profile.
rem
rem Restic equivalent of the rsync --link-dest script: content-addressed
rem dedup instead of hard links, encrypted at rest, VSS for locked files.
rem
rem Snapshots are browsable with:
rem restic snapshots
rem restic mount Z: (needs WinFsp)
rem restic restore latest --target C:\restore
rem ===========================================================================
rem --- Configuration ---------------------------------------------------------
set "SOURCE_DIR=%SystemDrive%\Users\joshua"
set "RESTIC_REPOSITORY=D:\backups\restic-home"
set "RESTIC_PASSWORD_FILE=C:\Scripts\restic-password.txt"
set "EXCLUDE_FILE=%~dp0excludes.txt"
set "LOG_DIR=D:\backups\logs"
set "RESTIC=restic.exe"
rem Retention. Roughly: two weeks of dailies, then thinning out.
set "KEEP=--keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3"
rem Run a full repository integrity check on this day of the week (0=Sunday).
set "CHECK_DOW=0"
rem --- Preflight -------------------------------------------------------------
net session >nul 2>&1
if errorlevel 1 (
echo Please run elevated ^(--use-fs-snapshot needs admin for VSS^).
exit /b 1
)
where %RESTIC% >nul 2>&1
if errorlevel 1 (
echo restic.exe not found in PATH.
exit /b 1
)
if not exist "%RESTIC_PASSWORD_FILE%" (
echo Password file not found: %RESTIC_PASSWORD_FILE%
exit /b 1
)
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format yyyy-MM-dd_HH-mm-ss"') do set "STAMP=%%i"
for /f %%i in ('powershell -NoProfile -Command "[int](Get-Date).DayOfWeek"') do set "DOW=%%i"
set "LOG=%LOG_DIR%\restic_%STAMP%.log"
call :log "=== restic backup started: %STAMP% ==="
rem --- Initialise the repo on first run --------------------------------------
%RESTIC% cat config >nul 2>&1
if errorlevel 1 (
call :log "Repository not found, initialising %RESTIC_REPOSITORY%"
%RESTIC% init >>"%LOG%" 2>&1
if errorlevel 1 (
call :log "FATAL: restic init failed"
exit /b 1
)
)
rem --- Backup ----------------------------------------------------------------
rem --use-fs-snapshot reads from a VSS snapshot, so locked files (NTUSER.DAT,
rem PST, database files) are captured consistently. Note that VSS skips Outlook
rem OST files by default; that is a Windows policy, not restic.
%RESTIC% backup "%SOURCE_DIR%" ^
--use-fs-snapshot ^
--exclude-file="%EXCLUDE_FILE%" ^
--exclude-caches ^
--tag daily ^
--verbose >>"%LOG%" 2>&1
set "RC=%errorlevel%"
if "%RC%"=="0" (
call :log "Backup OK"
) else if "%RC%"=="3" (
call :log "WARNING: backup completed but some files could not be read (exit 3)"
) else (
call :log "FATAL: backup failed with exit code %RC%"
exit /b %RC%
)
rem --- Retention -------------------------------------------------------------
%RESTIC% forget %KEEP% --prune >>"%LOG%" 2>&1
if errorlevel 1 call :log "WARNING: forget/prune failed"
rem --- Weekly integrity check ------------------------------------------------
if "%DOW%"=="%CHECK_DOW%" (
call :log "Running integrity check with 5%% data read"
%RESTIC% check --read-data-subset=5%% >>"%LOG%" 2>&1
if errorlevel 1 call :log "WARNING: restic check reported problems"
)
rem --- Trim old logs ---------------------------------------------------------
forfiles /p "%LOG_DIR%" /m restic_*.log /d -60 /c "cmd /c del @path" >nul 2>&1
call :log "=== finished ==="
exit /b 0
rem ---------------------------------------------------------------------------
:log
echo [%date% %time%] %~1
echo [%date% %time%] %~1>>"%LOG%"
exit /b 0
#!/usr/bin/env bash
#
# Backup home directory (MSYS2 / Windows port of the Linux backup.sh)
#
# Install: pacman -S rsync
# MSYS2 maps drives at /c, /d, ... (not /cygdrive/c like plain Cygwin).
#
set -o errexit
set -o nounset
set -o pipefail
readonly SOURCE_DIR="/c/Users/joshua"
readonly BACKUP_DIR="/d/backups/home"
readonly DATETIME="$(date '+%Y-%m-%d_%H-%M-%S')"
readonly BACKUP_PATH="${BACKUP_DIR}/${DATETIME}"
readonly LATEST_LINK="${BACKUP_DIR}/latest"
readonly LOG_DIR="${BACKUP_DIR}/logs"
# NTFS allows at most 1024 hard links per file, so an unchanged file cannot
# survive more than ~1023 snapshots. Prune well under that.
readonly KEEP_SNAPSHOTS=300
if [ ! -d "${SOURCE_DIR}" ]; then
echo "Source does not exist: ${SOURCE_DIR}" >&2
exit 1
fi
mkdir -p "${BACKUP_DIR}" "${LOG_DIR}"
readonly LOG="${LOG_DIR}/${DATETIME}.log"
# -rlt instead of -a: Cygwin/MSYS2 fakes POSIX ownership and modes over NTFS
# ACLs, and -p -g -o against a real Windows profile produces a stream of
# chown/chmod failures for no benefit.
# --modify-window=1 absorbs the FAT/NTFS timestamp granularity difference.
rsync -rlt --delete --stats --human-readable \
--modify-window=1 \
"${SOURCE_DIR}/" \
--link-dest "${LATEST_LINK}" \
--exclude=".cache" \
--exclude=".cpan" \
--exclude=".npm" \
--exclude=".vim" \
--exclude=".vscode-server" \
--exclude="node_modules" \
--exclude="__pycache__" \
--exclude="AppData/Local/Temp" \
--exclude="AppData/Local/Packages" \
--exclude="AppData/Local/CrashDumps" \
--exclude="AppData/Local/Microsoft/Windows/INetCache" \
--exclude="AppData/Local/Microsoft/Windows/WebCache" \
--exclude="AppData/Local/Google/Chrome/User Data/*/Cache" \
--exclude="AppData/Local/D3DSCache" \
--exclude="AppData/Local/NVIDIA" \
--exclude="NTUSER.DAT*" \
--exclude="UsrClass.dat*" \
--exclude="Thumbs.db" \
--exclude="desktop.ini" \
--exclude="*.tmp" \
"${BACKUP_PATH}" 2>&1 | tee "${LOG}"
# Repoint 'latest'. A junction is used rather than ln -s: MSYS2 symlinks are
# not visible as links to Explorer or native tools unless MSYS=winsymlinks:
# nativestrict is set, and native symlinks need admin or Developer Mode.
# cmd //c avoids MSYS2 rewriting the /c as a path.
win_latest="$(cygpath -w "${LATEST_LINK}")"
win_backup="$(cygpath -w "${BACKUP_PATH}")"
if [ -e "${LATEST_LINK}" ]; then
# rmdir removes the junction only, never the tree it points at.
MSYS2_ARG_CONV_EXCL='*' cmd //c rmdir "${win_latest}" >/dev/null
fi
MSYS2_ARG_CONV_EXCL='*' cmd //c mklink /J "${win_latest}" "${win_backup}" >/dev/null
# Prune old snapshots (oldest first, keep the newest KEEP_SNAPSHOTS).
if [ "${KEEP_SNAPSHOTS}" -gt 0 ]; then
find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 -type d \
-regextype posix-extended \
-regex '.*/[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}$' \
| sort -r \
| tail -n "+$((KEEP_SNAPSHOTS + 1))" \
| while read -r old; do
echo "Pruning ${old}"
rm -rf "${old}"
done
fi
find "${LOG_DIR}" -name '*.log' -mtime +60 -delete
echo "Backup complete: ${BACKUP_PATH}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment