Robocopy mit ntfy.sh-Pushmeldungen

Einleitung

Ich gleiche mehrere Verzeichnisse mit Robocopy ab. Früher lief das per Aufgabenplanung als Batch, inzwischen übernimmt das ein PowerShell-Skript mit integrierter ntfy-Benachrichtigungslogik: Es meldet Erfolg oder Fehler und wertet für maximale Transparenz die Robocopy-Exitcodes aus:

Code Status Bedeutung
0 Keine Dateien kopiert
1 Dateien kopiert
2 Zusätzliche Dateien/Ordner gelöscht
3 Dateien kopiert und gelöscht
4 Unterschiede festgestellt
5 Dateien kopiert und Unterschiede festgestellt
6 Dateien gelöscht und Unterschiede festgestellt
7 Dateien kopiert, gelöscht und Unterschiede festgestellt
≥ 8 Fehler aufgetreten

Da ich mit PowerShells Invoke-RestMethod und der POST-Methode irgendwie so meine Schwierigkeiten hatte – vermutlich ein klassisches Layer-8-Problem 😁 – nutze ich für den Versand der Benachrichtigungen nun die ntfy-CLI (ntfy.exe).

Dank dieses neuen Skripts werde ich nun wie folgt über den Status meiner Sync-Jobs informiert:

Robocopy→Ntfy

Anleitung

Am einfachsten erstellst du das Skript direkt auf deinem eigenen Rechner:

  1. Code kopieren: Kopiere den untenstehenden Code und füge ihn in die PowerShell ISE ein.
  2. Konfiguration anpassen: Passe den Konfigurationsblock an deine eigene Infrastruktur an.
  3. Skript speichern: Speichere die Datei an einem Ort deiner Wahl.

💡 Wichtig: Vergiss nicht, zuvor die ntfy.exe herunterzuladen!

Anschließend kannst du das Skript auf zwei Arten ausführen:

  • Über die Aufgabenplanung automatisch starten lassen
  • Oder manuell in der PowerShell ISE bzw. über die Eingabeaufforderung ausführen

RobocopyNtfy.ps1

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
$ErrorActionPreference = "Stop"

# -------------------- Konfiguration --------------------
$LogDir            = "C:\Scripts\Logs"
$LogFile           = Join-Path $LogDir "backup_ntfy.log"
$LogRotateMaxBytes = 5MB   # bei Überschreiten wird rotiert (alte Datei weggesichert)

$Robo              = "C:\Windows\System32\Robocopy.exe"
$RoboArgsCommon    = @("/MIR", "/MT", "/R:1", "/W:5")

$Jobs = @(
    @{ Name="Job1"; Src="Source1"; Dst="Destination1" },
    @{ Name="Job2"; Src="Source2"; Dst="Destination2" },
    @{ Name="Job3"; Src="Source3"; Dst="Destination3" },
    @{ Name="Job4"; Src="Source4"; Dst="Destination4" }
)

# ntfy CLI
$NtfyExe   = "C:\Scripts\ntfy.exe"
$NtfyToken = "TOKEN"
$NtfyUrl   = "https://your.ntfy.server.domain/topic"

# -------------------- Helpers --------------------
function Write-Log([string]$Msg, [string]$Level = "INFO") {
    $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss,fff"
    "$ts [$Level] $Msg" | Out-File -FilePath $LogFile -Append -Encoding utf8
}

function Rotate-Log {
    if (Test-Path $LogFile) {
        $size = (Get-Item $LogFile).Length
        if ($size -gt $LogRotateMaxBytes) {
            $ts = Get-Date -Format "yyyyMMdd_HHmmss"
            $dest = Join-Path $LogDir "backup_ntfy_$ts.log"
            Copy-Item $LogFile $dest -Force
            Clear-Content $LogFile
        }
    }
}

# Neue Funktion: Statuscode in lesbare Flags dekodieren
function Decode-RoboCode([int]$Code) {
    $flags = @()
    if ($Code -band 1)  { $flags += "Copied/Updated" }
    if ($Code -band 2)  { $flags += "Extras/Deleted" }
    if ($Code -band 4)  { $flags += "Mismatched" }
    if ($Code -band 8)  { $flags += "CopyErrors" }
    if ($Code -band 16) { $flags += "SeriousError" }
    if (-not $flags)    { $flags = @("NoChanges") }
    return ($flags -join ", ")
}

# Schweregrad (nur zur Gesamtauswertung)
function Get-RoboSeverity([int]$Code) {
    if ($Code -ge 16) { return "ERROR" }
    elseif ($Code -ge 8) { return "ERROR" }
    else { return "OK" }
}

function Invoke-RoboJob($Job) {
    $name = $Job.Name; $src = $Job.Src; $dst = $Job.Dst
    Write-Log "Starte Robocopy [$name] : `"$src`" -> `"$dst`""
    $sw = [System.Diagnostics.Stopwatch]::StartNew()

    & $Robo $src $dst @RoboArgsCommon | Out-Null
    $sw.Stop()
    $code = $LASTEXITCODE
    $decoded = Decode-RoboCode $code
    $sev  = Get-RoboSeverity $code

    Write-Log "Robocopy [$name] beendet. Code=$code ($decoded), Severity=$sev, Dauer=$([math]::Round($sw.Elapsed.TotalSeconds,1))s"

    return @{ 
        Name       = $name
        Code       = $code
        Flags      = $decoded
        Severity   = $sev
        DurationSec = $sw.Elapsed.TotalSeconds 
    }
}

# -------------------- Hauptlogik --------------------
try {
    New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
    Rotate-Log
    New-Item -ItemType File -Path $LogFile -Force | Out-Null

    Write-Log "Backup gestartet (User=$(whoami))"
    $swTotal = [System.Diagnostics.Stopwatch]::StartNew()

    if (-not (Test-Path $Robo -PathType Leaf))    { Write-Log "Robocopy fehlt: $Robo" "ERROR"; throw "Robocopy fehlt" }
    if (-not (Test-Path $NtfyExe -PathType Leaf)) { Write-Log "ntfy.exe fehlt: $NtfyExe" "ERROR"; throw "ntfy fehlt" }

    $results = @()
    foreach ($job in $Jobs) { $results += Invoke-RoboJob $job }

    # Neues Summary mit Flags
    $lines   = $results | ForEach-Object { 
        "{0}: {1} (Code {2}, {3}s)" -f $_.Name, $_.Flags, $_.Code, [math]::Round($_.DurationSec,1) 
    }
    $summary = ($lines -join "`n")

    # Gesamtstatus: nur Fehler bei >=8
    $overall = "OK"
    if ($results.Severity -contains "ERROR") { $overall = "ERROR" }

    $swTotal.Stop()
    Write-Log "Gesamtstatus: $overall"
    Write-Log "Gesamtdauer: $([math]::Round($swTotal.Elapsed.TotalSeconds,1))s"
    Write-Log "Zusammenfassung:`n$summary"

    # ---------- ntfy senden ----------
    $title = "Backup Sync: $overall"
    $msg   = "Dauer: $([math]::Round($swTotal.Elapsed.TotalSeconds,1))s`n$summary"

    $args = @("publish","--token",$NtfyToken,"--title",$title,$NtfyUrl,$msg)
    $argsForLog = $args | ForEach-Object { if ($_ -eq $NtfyToken) { "***" } else { $_ } }
    Write-Log ("Rufe ntfy.exe auf: " + $NtfyExe + " " + ($argsForLog -join " "))

    $output = & $NtfyExe @args 2>&1
    $code   = $LASTEXITCODE
    if ($output) { Write-Log ("ntfy.exe output:`n" + $output.ToString().Trim()) }
    Write-Log "ntfy.exe ExitCode=$code"

    Write-Log "Backup fertig"
}
catch {
    Write-Log ("FEHLER: " + $_.Exception.Message) "ERROR"
    if ($_.ScriptStackTrace) { Write-Log ("Stack: " + $_.ScriptStackTrace) "ERROR" }
}
finally {
    exit 0
}
formerly known as struband.net
Built with Hugo
Theme Stack designed by Jimmy