With your pretty basic requirements, you might want to have a look at RSYNC for Windows: https://itefix.net/cwrsync/client
There's good support for including/excluding files and directories, there's a lot of knowledge in the internet about how RSYNC works, you can easily create a script on your own calling things with different complex commandline arguments, it can be scheduled using the Windows Task Planner, with custom scripts you might even create VSS-based snapshots etc.
#!/usr/bin/env bash
##
# Backup the same named host using RSYNC running locally to the NAS.
# <p>
# Consider this a PoC for creating a file-based backup, most likely in addition to other available
# backup. Some of those might store files in additional containers like VHDX, some like Active
# Backup for Business use deduplication and custom repos and all of those are difficult to repair
# case of errors. Having a file-based backup additionally, updated once a while, might be another
# safety-net.
# </p>
# <p>
# If the script runs elevated, currently simply notified using an additional argument, it takes
# care of creating a snapshot, syncing from there and cleaning it up again afterwards. This prevents
# problem with denied access to files exclusively used by some apps.
# </p>
# @param[in,opt] {@code 1} if the process is elevated.
#
set -o errexit
set -o errtrace
#set -o functrace
set -o pipefail
#set -o xtrace
declare -r RUNS_ELEVATED="${1:-0}"
declare -r BAK_DST='/volume1/bak_sch.tst.ts-nb_rsync/c'
declare BAK_SRC='/cygdrive/c'
declare -r DIR_SCRIPT="$(readlink -f -n "$(dirname "$(cygpath -u ${0})")")"
declare -r FNAME="$(basename "${0}" '.sh')"
declare -r FILE_LOG="${DIR_SCRIPT}/${FNAME}.log"
declare -r FILE_LOG_CYG="/cygdrive/${FILE_LOG#/}"
# Some snapshot related global data like paths, which simply don't change by convention. Snaps need
# to be created and deleted by Windows tools using Windows paths.
#
# The snap is created in this directory currently, because I'm unable to directly mount it as e.g.
# a drive-letter, only something like "subst" might work. But RSYNC should be able to handle even
# long paths properly and support long Unicode paths for Windows, so it seems to make sense to keep
# everything of interest grouped together.
declare -r SNAP_DIR_LIN="${DIR_SCRIPT}/${FNAME}.snap"
declare -r SNAP_DIR_WIN="$(cygpath -w "${SNAP_DIR_LIN}")"
declare SNAP_ID=
# Order is important, RSYNC is not compatible with SSH from Git for Windows. SSH bundled by cwRsync
# needs to be configured specially as well, because it expects "known_hosts" in non-default folder.
# Have a look et /etc/ssh_config" and e.g. comment the existing path option.
# dup() in/out/err failed
declare -r PATH="/c/Program Files/cwRsync/bin:${PATH}"
# Git Bash must not convert paths to something RSYNC can't make use of.
export MSYS_NO_PATHCONV=1
##
# Create a snapshot if the script runs elevated.
#
# @return the ID fo the snapshot for later deletion.
#
snap_create_if() {
if [ "${RUNS_ELEVATED}" != '1' ]; then
return
fi
local -a args=(
'-NonInteractive'
'-NoLogo'
'-NoProfile'
'-OutputFormat' 'Text'
)
local -r details="$(powershell.exe "${args[@]}" << 'EOT' 2> '/dev/null'
$basics = (Get-WmiObject -List Win32_ShadowCopy).Create("C:\\", "ClientAccessible")
$details = Get-WmiObject Win32_ShadowCopy | Where-Object { $_.ID -eq $basics.ShadowID }
echo $details
EOT
)"
# Don't understand the ouput of PS too much, contains Windows line endings, tabs etc. But even after normalizing
# I'm unable to capture some values including their "column names", only the "column values".
local -r parsable="$(echo "${details}" | tr '\r\n\t\v' ' ')"
local -r snap_dev="$(echo "${parsable}" | sed -E 's!.+ : (.+HarddiskVolumeShadowCopy[0-9]+) .+!\1!')"
local -r snap_id="$( echo "${parsable}" | sed -E 's!.+.ID="(\{.+\})" .+!\1!')"
# It's important to end the shadow device with a "\" or access won't work, even if the link is created!
cmd.exe /C "mklink /D "${SNAP_DIR_WIN}" "${snap_dev}\\"" 1> '/dev/null'
printf '%s' "${snap_id}"
}
##
# Delete a possibly created snapshot if an ID is available.
#
snap_delete_if() {
if [ -z "${SNAP_ID}" ]; then
return
fi
vssadmin 'Delete' 'Shadows' "/Shadow=${SNAP_ID}" '/Quiet'
cmd.exe /C "rmdir "${SNAP_DIR_WIN}""
}
##
# Sync the data of interest.
#
rsync_main() {
# Some filters should be applied on the receiving side only, which requires them to be "global"
# or e.g. placed in rule files transferred to the receiving side to be taken into account there.
# The latter seems unnecessary difficult, BUT "global" DOES NOT include providing rules using
# STDIN here. So these special rules really need to be part of the commandline, because that is
# forwarded to the receiving side. OTOH, STIND most likely is simply handled like rule files and
# aren't considered to be "global" nor are they transferred to the receiver magically somehow.
local -a filters=(
# The backup might be indexed by Synology to easier find files, but DSM might create private
# dirs and files not known in the backup source. So without special care, RSYNC would delete
# them like it were files deleted by the user in the source, which would break the whole
# indexing and searching process.
'--filter=P /**/@eaDir/'
)
local -a args=(
'--compress-level=6'
'--delete'
'--delete-during'
'--delete-excluded'
"${filters[@]}"
'--hard-links'
'--include-from=-'
'--inplace'
'--links'
"--log-file=${FILE_LOG_CYG}"
'--progress'
'--recursive'
'--rsync-path=/bin/rsync'
'--times'
'--verbose'
)
rsync "${args[@]}" "${BAK_SRC}/" "sch.tst.ds218:${BAK_DST}/" << 'EOT'
+ /Program Files/cwRsync/
+ /Program Files/diskshadow/
+ /Program Files/HxD/
+ /Program Files (x86)/
+ /Program Files (x86)/moneyplex/
+ /Program Files (x86)/ShadowExplorer/
+ /Windows/Logs/
+ /Windows/System32/
+ /Windows/System32/drivers/
+ /Windows/System32/drivers/etc/
- /$Recycle.Bin/
- /$WinREAgent/
- /Recovery/
- /Program Files/*
- /Program Files (x86)/*
- /ProgramData/Adobe/
- /System Volume Information/
- /Windows/*
- /Windows/System32/*
- /Windows/System32/drivers/*
- /hiberfil.sys
- /pagefile.sys
- /swapfile.sys
EOT
}
##
# Sync the log file.
# <p>
# If a snapshot is created, the log file isn't updated at runtime, so the current approach is to
# sync that in an additional step. This way we can put it into the root of the backup destination
# as well, where it's easier to get recognized.
# </p>
#
rsync_log() {
local -a args=(
'--compress-level=6'
'--inplace'
'--progress'
'--rsync-path=/bin/rsync'
'--times'
'--verbose'
)
rsync "${args[@]}" "${FILE_LOG_CYG}" "sch.tst.ds218:${BAK_DST}/.."
}
rm "${FILE_LOG}" || true
SNAP_ID="$(snap_create_if)"
if [ -n "${SNAP_ID}" ]; then
BAK_SRC="/cygdrive/${SNAP_DIR_LIN#/}"
fi
rsync_main || true
snap_delete_if
rsync_log